Repository navigation
[world-local] Fix initDataDir crashing on Bun for a new data directory - #4838
Conversation
On Bun, opendir() resolves for a directory that does not exist and rejects on the first read, where Node rejects opendir() itself. inspectEntityDirs() caught ENOENT only from the opendir() call, so on Bun initDataDir() threw for any data directory without events/ or steps/, which is every new one. The read is now inside the same try. The new test runs every opendir() the way Bun does; both of its cases fail without the fix. Signed-off-by: Pranit <pranit@vercel.com>
🦋 Changeset detectedLatest commit: 95afa25 The changes in this PR will be included in the next version bump. This PR includes changesets to release 19 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
📊 Workflow Benchmarkscommit Backend:
📈 STSO distribution vs main (inline / queue-hop histograms)1020 steps (inline) Cumulative STSO time: main 115748ms → this run 115725ms (Δ -23ms, 0%) ℹ️ Metric definitions & methodologyThe collapsed STSO distribution section above buckets every step gap, split inline (same warm process — pure framework overhead) vs queue-hop (fresh process — dispatch, reinit, replay). Best/P75/P90/P99 deltas compare against the most recent benchmark run on Metrics — TTFS: time to first step body (in-deployment start() → first step body) · Fan-out TTFS: fan-out time to first step (in-deployment start() → first of the parallel step bodies to complete) · Fan-out TTLS: fan-out time to last step (in-deployment start() → last of the parallel step bodies to complete, i.e. when the Promise.all resolves) · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (whole-run time outside step bodies, in-deployment anchored) Scenarios — step: one trivial no-op step, no stream; no hooks, so the run stays in turbo mode (in-process fast path) · stream: one streaming step; no hooks, so the run stays in turbo mode (in-process fast path) · hook + stream: registers a hook before one step, which exits turbo mode (dispatch path) · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges, and WO is the whole-run overhead outside step bodies · Promise.all(100 steps): 100 trivial no-op steps started together in a single Promise.all; Fan-out TTFS is the first of them to complete and Fan-out TTLS the last, both from the in-deployment clientStart, so their gap is the spread the runtime adds across the fan-out 🔴 marks a percentile over its target (within target is left unmarked). Targets (p75/p90/p99, ms) — TTFS 200/300/600 All timestamps are deployment-side; runs are triggered in-deployment, so the CI runner and api.vercel.com sit outside every measured window. TTFS = Cold starts stay in the numbers (real bursty-workload latency, inflates P75+); Best is the warm floor. |
🧪 E2E Test Results✅ All tests passed
|
| Passed | Failed | Skipped | Total | |
|---|---|---|---|---|
| ✅ ▲ Vercel Production | 3982 | 0 | 878 | 4860 |
| ✅ 💻 Local Development | 4669 | 0 | 551 | 5220 |
| ✅ 📦 Local Production | 4669 | 0 | 551 | 5220 |
| ✅ 🐘 Local Postgres | 4669 | 0 | 551 | 5220 |
| ✅ 🪟 Windows | 348 | 0 | 12 | 360 |
| ✅ 🌐 Cross-language Conformance | 68 | 0 | 87 | 155 |
| ✅ dynamic-runs | 0 | 0 | 0 | 0 |
| ✅ vercel-http-transport | 897 | 0 | 183 | 1080 |
| ✅ vercel-multi-region | 27 | 0 | 0 | 27 |
| ✅ vercel-ws-transport | 607 | 0 | 113 | 720 |
| Total | 19936 | 0 | 2926 | 22862 |
Details by Category
✅ ▲ Vercel Production
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ astro-node | 145 | 0 | 35 |
| ✅ astro-quickjs | 145 | 0 | 35 |
| ✅ example-node | 145 | 0 | 35 |
| ✅ example-quickjs | 145 | 0 | 35 |
| ✅ express-node | 145 | 0 | 35 |
| ✅ express-quickjs | 145 | 0 | 35 |
| ✅ fastify-node | 145 | 0 | 35 |
| ✅ fastify-quickjs | 145 | 0 | 35 |
| ✅ hono-node | 145 | 0 | 35 |
| ✅ hono-quickjs | 145 | 0 | 35 |
| ✅ nest-node | 145 | 0 | 35 |
| ✅ nest-quickjs | 145 | 0 | 35 |
| ✅ nextjs-turbopack-node | 172 | 0 | 8 |
| ✅ nextjs-turbopack-quickjs | 172 | 0 | 8 |
| ✅ nextjs-webpack-node | 172 | 0 | 8 |
| ✅ nextjs-webpack-quickjs | 172 | 0 | 8 |
| ✅ nitro-node | 145 | 0 | 35 |
| ✅ nitro-quickjs | 145 | 0 | 35 |
| ✅ nuxt-node | 145 | 0 | 35 |
| ✅ nuxt-quickjs | 145 | 0 | 35 |
| ✅ python-node | 66 | 0 | 114 |
| ✅ sveltekit-node | 164 | 0 | 16 |
| ✅ sveltekit-quickjs | 164 | 0 | 16 |
| ✅ tanstack-start-node | 145 | 0 | 35 |
| ✅ tanstack-start-quickjs | 145 | 0 | 35 |
| ✅ vite-node | 145 | 0 | 35 |
| ✅ vite-quickjs | 145 | 0 | 35 |
✅ 💻 Local Development
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ astro-stable-node | 151 | 0 | 29 |
| ✅ astro-stable-quickjs | 151 | 0 | 29 |
| ✅ express-stable-node | 151 | 0 | 29 |
| ✅ express-stable-quickjs | 151 | 0 | 29 |
| ✅ fastify-stable-node | 151 | 0 | 29 |
| ✅ fastify-stable-quickjs | 151 | 0 | 29 |
| ✅ hono-stable-node | 151 | 0 | 29 |
| ✅ hono-stable-quickjs | 151 | 0 | 29 |
| ✅ nest-stable-node | 151 | 0 | 29 |
| ✅ nest-stable-quickjs | 151 | 0 | 29 |
| ✅ nextjs-turbopack-canary-node | 179 | 0 | 1 |
| ✅ nextjs-turbopack-canary-quickjs | 179 | 0 | 1 |
| ✅ nextjs-turbopack-quickjs-snapshot | 179 | 0 | 1 |
| ✅ nextjs-turbopack-stable-node | 179 | 0 | 1 |
| ✅ nextjs-turbopack-stable-quickjs | 179 | 0 | 1 |
| ✅ nextjs-webpack-canary-node | 179 | 0 | 1 |
| ✅ nextjs-webpack-canary-quickjs | 179 | 0 | 1 |
| ✅ nextjs-webpack-stable-node | 179 | 0 | 1 |
| ✅ nextjs-webpack-stable-quickjs | 179 | 0 | 1 |
| ✅ nitro-stable-node | 151 | 0 | 29 |
| ✅ nitro-stable-quickjs | 151 | 0 | 29 |
| ✅ nuxt-stable-node | 151 | 0 | 29 |
| ✅ nuxt-stable-quickjs | 151 | 0 | 29 |
| ✅ sveltekit-stable-node | 170 | 0 | 10 |
| ✅ sveltekit-stable-quickjs | 170 | 0 | 10 |
| ✅ tanstack-start-node | 151 | 0 | 29 |
| ✅ tanstack-start-quickjs | 151 | 0 | 29 |
| ✅ vite-stable-node | 151 | 0 | 29 |
| ✅ vite-stable-quickjs | 151 | 0 | 29 |
✅ 📦 Local Production
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ astro-stable-node | 151 | 0 | 29 |
| ✅ astro-stable-quickjs | 151 | 0 | 29 |
| ✅ express-stable-node | 151 | 0 | 29 |
| ✅ express-stable-quickjs | 151 | 0 | 29 |
| ✅ fastify-stable-node | 151 | 0 | 29 |
| ✅ fastify-stable-quickjs | 151 | 0 | 29 |
| ✅ hono-stable-node | 151 | 0 | 29 |
| ✅ hono-stable-quickjs | 151 | 0 | 29 |
| ✅ nest-stable-node | 151 | 0 | 29 |
| ✅ nest-stable-quickjs | 151 | 0 | 29 |
| ✅ nextjs-turbopack-canary-node | 179 | 0 | 1 |
| ✅ nextjs-turbopack-canary-quickjs | 179 | 0 | 1 |
| ✅ nextjs-turbopack-quickjs-snapshot | 179 | 0 | 1 |
| ✅ nextjs-turbopack-stable-node | 179 | 0 | 1 |
| ✅ nextjs-turbopack-stable-quickjs | 179 | 0 | 1 |
| ✅ nextjs-webpack-canary-node | 179 | 0 | 1 |
| ✅ nextjs-webpack-canary-quickjs | 179 | 0 | 1 |
| ✅ nextjs-webpack-stable-node | 179 | 0 | 1 |
| ✅ nextjs-webpack-stable-quickjs | 179 | 0 | 1 |
| ✅ nitro-stable-node | 151 | 0 | 29 |
| ✅ nitro-stable-quickjs | 151 | 0 | 29 |
| ✅ nuxt-stable-node | 151 | 0 | 29 |
| ✅ nuxt-stable-quickjs | 151 | 0 | 29 |
| ✅ sveltekit-stable-node | 170 | 0 | 10 |
| ✅ sveltekit-stable-quickjs | 170 | 0 | 10 |
| ✅ tanstack-start-node | 151 | 0 | 29 |
| ✅ tanstack-start-quickjs | 151 | 0 | 29 |
| ✅ vite-stable-node | 151 | 0 | 29 |
| ✅ vite-stable-quickjs | 151 | 0 | 29 |
✅ 🐘 Local Postgres
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ astro-stable-node | 151 | 0 | 29 |
| ✅ astro-stable-quickjs | 151 | 0 | 29 |
| ✅ express-stable-node | 151 | 0 | 29 |
| ✅ express-stable-quickjs | 151 | 0 | 29 |
| ✅ fastify-stable-node | 151 | 0 | 29 |
| ✅ fastify-stable-quickjs | 151 | 0 | 29 |
| ✅ hono-stable-node | 151 | 0 | 29 |
| ✅ hono-stable-quickjs | 151 | 0 | 29 |
| ✅ nest-stable-node | 151 | 0 | 29 |
| ✅ nest-stable-quickjs | 151 | 0 | 29 |
| ✅ nextjs-turbopack-canary-node | 179 | 0 | 1 |
| ✅ nextjs-turbopack-canary-quickjs | 179 | 0 | 1 |
| ✅ nextjs-turbopack-quickjs-snapshot | 179 | 0 | 1 |
| ✅ nextjs-turbopack-stable-node | 179 | 0 | 1 |
| ✅ nextjs-turbopack-stable-quickjs | 179 | 0 | 1 |
| ✅ nextjs-webpack-canary-node | 179 | 0 | 1 |
| ✅ nextjs-webpack-canary-quickjs | 179 | 0 | 1 |
| ✅ nextjs-webpack-stable-node | 179 | 0 | 1 |
| ✅ nextjs-webpack-stable-quickjs | 179 | 0 | 1 |
| ✅ nitro-stable-node | 151 | 0 | 29 |
| ✅ nitro-stable-quickjs | 151 | 0 | 29 |
| ✅ nuxt-stable-node | 151 | 0 | 29 |
| ✅ nuxt-stable-quickjs | 151 | 0 | 29 |
| ✅ sveltekit-stable-node | 170 | 0 | 10 |
| ✅ sveltekit-stable-quickjs | 170 | 0 | 10 |
| ✅ tanstack-start-node | 151 | 0 | 29 |
| ✅ tanstack-start-quickjs | 151 | 0 | 29 |
| ✅ vite-stable-node | 151 | 0 | 29 |
| ✅ vite-stable-quickjs | 151 | 0 | 29 |
✅ 🪟 Windows
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ nextjs-turbopack-node | 174 | 0 | 6 |
| ✅ nextjs-turbopack-quickjs | 174 | 0 | 6 |
✅ 🌐 Cross-language Conformance
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ python | 68 | 0 | 87 |
✅ dynamic-runs
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ astro-vercel | 0 | 0 | 0 |
| ✅ example-vercel | 0 | 0 | 0 |
| ✅ express-vercel | 0 | 0 | 0 |
| ✅ fastify-vercel | 0 | 0 | 0 |
| ✅ hono-vercel | 0 | 0 | 0 |
| ✅ nest-vercel | 0 | 0 | 0 |
| ✅ nextjs-turbopack-vercel | 0 | 0 | 0 |
| ✅ nextjs-webpack-vercel | 0 | 0 | 0 |
| ✅ nitro-vercel | 0 | 0 | 0 |
| ✅ nuxt-vercel | 0 | 0 | 0 |
| ✅ sveltekit-vercel | 0 | 0 | 0 |
| ✅ tanstack-start-vercel | 0 | 0 | 0 |
| ✅ vite-vercel | 0 | 0 | 0 |
✅ vercel-http-transport
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ example | 145 | 0 | 35 |
| ✅ express | 145 | 0 | 35 |
| ✅ hono | 145 | 0 | 35 |
| ✅ nextjs-turbopack | 172 | 0 | 8 |
| ✅ nitro | 145 | 0 | 35 |
| ✅ vite | 145 | 0 | 35 |
✅ vercel-multi-region
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ nextjs-turbopack | 27 | 0 | 0 |
✅ vercel-ws-transport
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ example | 145 | 0 | 35 |
| ✅ express | 145 | 0 | 35 |
| ✅ nextjs-turbopack | 172 | 0 | 8 |
| ✅ vite | 145 | 0 | 35 |
Sim WorldSimulated world deterministic testing for races. Traces 🟢 world-sim scenario book — 0 fail of 42 total
Full trace: |
About these numbersSizes are gzip; parentheses show the change against
|
pranaygp
left a comment
There was a problem hiding this comment.
Summary. Moves the for await read of each entity directory inside the try that handles ENOENT in inspectEntityDirs(). On Bun 1.3.x, opendir() succeeds for a missing path and the error shows up on the first read. I reproduced it with the built dist/init.js under Bun 1.3.12: main fails for every new or partial data directory, and this branch fixes it.
Upstream. This is a Bun bug, tracked as oven-sh/bun#17581 ("opendirSync successfully returns for a non-existent directory"). It was fixed by oven-sh/bun#31830, which first shipped in Bun 1.4.0. I checked: Bun 1.3.12 and 1.3.14 reject on the first read (syscall: scandir), while 1.4.0 and 1.4.3 reject at opendir() like Node. The main build of world-local already works on Bun ≥ 1.4.0. This PR is still worth merging for anyone on Bun 1.3.x, and the new structure is correct on both runtimes. Consider mentioning the Bun issue and the 1.4.0 fix in the code comment so this can be revisited once 1.3.x support is dropped.
Correctness. I found no bugs. I ran initDataDir() over all 81 combinations of 9 states for events/ and steps/ (missing, empty, run dirs, flat file, unknown .json, mixed, non-JSON only, a file in place of the dir, mode 000). Node on main, Node on this branch and Bun 1.3.12 on this branch give identical results in every case, including whether version.txt gets written. Errors other than ENOENT (such as ENOTDIR) still propagate on both runtimes. Reverting init.ts makes both new tests fail with the reported ENOENT. vitest run src in packages/world-local passes.
Risks / missing tests (none blocking)
packages/world-local/src/init.ts:361-368: thecatchnow also covers errors thrown while iterating. AnENOENTpartway through wouldcontinuewith a partly filledlayoutwhere it used to throw. I don't think either runtime can reach this: Bun 1.3.x fails on the first read, before any entries arrive, and on Linux reading a directory that was removed while open just ends the listing. The steady-state result is unchanged.- Nothing in CI runs world-local on Bun, and that gap let #4718 ship this regression. The mock-based test is a good guard for this particular behaviour. A small follow-up job (
setup-bun+initDataDir()on a temp directory) would catch the next difference between the runtimes. init-lazy-opendir.test.ts: the mock iterator has noreturn()/close(). That's fine today, because a rejectednext()doesn't triggerreturn(), but the mock will break if the code ever callsdir.close()explicitly.
Observability / security / performance / docs. No logging or telemetry changes. On Bun 1.3.x the error now reports syscall: 'scandir' rather than 'opendir', which only matters to log searches that key on it. No security impact: same paths, no symlink handling change, and rm still only runs after the same classification. Performance is unchanged (one opendir per entity dir). The patch changeset is correctly scoped.
Nits
init.ts:359: the comment could say "rejects on the first read (Bun < 1.4) rather than atopendir()(Node)" and link oven-sh/bun#17581.
LGTM, approving.
|
No backport to This fixes To override, re-run the Backport to stable workflow manually via |
…ol-error-listener * commit 'fcb554b621eb846af808acd258f31a6e4348a349': [world] Route-agnostic queue delivery: a consumer's own prefix, route, or in-process handler (#4835) [world-local] Fix initDataDir crashing on Bun for a new data directory (#4838) # Conflicts: # docs/content/worlds/v5/postgres.mdx
This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## @workflow/ai@5.0.3 ### Patch Changes - Updated dependencies [[`fcb554b`](fcb554b)]: - @workflow/utils@5.0.2 ## @workflow/astro@5.0.4 ### Patch Changes - Updated dependencies []: - @workflow/builders@5.0.4 - @workflow/rollup@5.0.4 - @workflow/vite@5.0.4 ## @workflow/builders@5.0.4 ### Patch Changes - Updated dependencies [[`854242e`](854242e), [`e6bd692`](e6bd692), [`fcb554b`](fcb554b), [`73a6ca3`](73a6ca3), [`fcb554b`](fcb554b)]: - @workflow/core@5.2.1 - @workflow/errors@5.0.4 - @workflow/utils@5.0.2 ## @workflow/cli@5.0.4 ### Patch Changes - [#4760](#4760) [`1342a96`](1342a96) Thanks [@pranaygp](https://github.com/pranaygp)! - Improve `workflow inspect` for long runs and scoped lookups: add `--all` for complete steps, events, and sleeps listings, print reusable cursor hints, and keep pagination on the read path that issued the cursor. Support event ID and short workflow-name lookups, require `--runId` for individual steps, events, and streams, fix `st` to mean streams, include run IDs in stream hints, and exit non-zero when a stream read fails. - Updated dependencies [[`854242e`](854242e), [`e6bd692`](e6bd692), [`fcb554b`](fcb554b), [`73a6ca3`](73a6ca3), [`fcb554b`](fcb554b), [`ebaa5b7`](ebaa5b7), [`b69fabe`](b69fabe), [`77ccb69`](77ccb69), [`c57820d`](c57820d), [`ebaa5b7`](ebaa5b7), [`ebaa5b7`](ebaa5b7), [`3e93033`](3e93033)]: - @workflow/core@5.2.1 - @workflow/errors@5.0.4 - @workflow/world@5.0.4 - @workflow/world-vercel@5.2.1 - @workflow/world-local@5.1.1 - @workflow/utils@5.0.2 - @workflow/builders@5.0.4 - @workflow/web@5.0.4 ## @workflow/core@5.2.1 ### Patch Changes - [#4669](#4669) [`854242e`](854242e) Thanks [@http-samc](https://github.com/http-samc)! - Compress workflow arguments on the libuv threadpool when starting a run, so large inputs no longer block the caller's event loop during zstd compression. - [#4670](#4670) [`73a6ca3`](73a6ca3) Thanks [@http-samc](https://github.com/http-samc)! - Add a `compressArguments` option to `start()`; pass `false` to store the run's arguments uncompressed when the inputs are known to be incompressible. - [#4835](#4835) [`fcb554b`](fcb554b) Thanks [@pranaygp](https://github.com/pranaygp)! - Record the queue namespace on a run's `executionContext.queueNamespace` in `start()`, so restart recovery re-enqueues the run to its own namespace. - Updated dependencies [[`e6bd692`](e6bd692), [`fcb554b`](fcb554b), [`ebaa5b7`](ebaa5b7), [`b69fabe`](b69fabe), [`77ccb69`](77ccb69), [`c57820d`](c57820d), [`ebaa5b7`](ebaa5b7), [`ebaa5b7`](ebaa5b7), [`3e93033`](3e93033)]: - @workflow/errors@5.0.4 - @workflow/world@5.0.4 - @workflow/world-vercel@5.2.1 - @workflow/world-local@5.1.1 - @workflow/utils@5.0.2 ## @workflow/errors@5.0.4 ### Patch Changes - [#4733](#4733) [`e6bd692`](e6bd692) Thanks [@VaguelySerious](https://github.com/VaguelySerious)! - Add `InBandSupersededError` and optional `inBand`/`expectedSeqInBand` event-create params for the in-band writer fence. `@workflow/world-vercel` forwards them, does not retry a fenced write in-process, and records the fence on its event spans. The runtime does not use them yet. - Updated dependencies [[`e6bd692`](e6bd692), [`fcb554b`](fcb554b), [`ebaa5b7`](ebaa5b7), [`ebaa5b7`](ebaa5b7), [`3e93033`](3e93033)]: - @workflow/world@5.0.4 - @workflow/utils@5.0.2 ## @workflow/nest@5.1.2 ### Patch Changes - Updated dependencies [[`fcb554b`](fcb554b)]: - @workflow/utils@5.0.2 - @workflow/builders@5.0.4 ## @workflow/next@5.0.4 ### Patch Changes - Updated dependencies [[`854242e`](854242e), [`73a6ca3`](73a6ca3), [`fcb554b`](fcb554b)]: - @workflow/core@5.2.1 - @workflow/builders@5.0.4 ## @workflow/nitro@5.0.4 ### Patch Changes - Updated dependencies [[`854242e`](854242e), [`73a6ca3`](73a6ca3), [`fcb554b`](fcb554b)]: - @workflow/core@5.2.1 - @workflow/builders@5.0.4 - @workflow/web@5.0.4 - @workflow/rollup@5.0.4 - @workflow/vite@5.0.4 ## @workflow/nuxt@5.0.4 ### Patch Changes - Updated dependencies []: - @workflow/nitro@5.0.4 ## @workflow/rollup@5.0.4 ### Patch Changes - Updated dependencies []: - @workflow/builders@5.0.4 ## @workflow/sveltekit@5.0.4 ### Patch Changes - Updated dependencies []: - @workflow/builders@5.0.4 - @workflow/rollup@5.0.4 - @workflow/vite@5.0.4 ## @workflow/utils@5.0.2 ### Patch Changes - [#4835](#4835) [`fcb554b`](fcb554b) Thanks [@pranaygp](https://github.com/pranaygp)! - Add optional `Queue.registerRoute()` and `Queue.registerHandler()`, implemented by `@workflow/world-local` and `@workflow/world-postgres`, so a queue consumer other than the Workflow SDK receives its own prefix's messages on its own route or in process. `reenqueueActiveRuns()` re-enqueues a run to the queue namespace it recorded, and `@workflow/world-postgres` keeps a job's queue prefix. ## @workflow/vite@5.0.4 ### Patch Changes - Updated dependencies []: - @workflow/builders@5.0.4 ## @workflow/vitest@5.0.4 ### Patch Changes - Updated dependencies [[`854242e`](854242e), [`e6bd692`](e6bd692), [`fcb554b`](fcb554b), [`73a6ca3`](73a6ca3), [`fcb554b`](fcb554b), [`ebaa5b7`](ebaa5b7), [`b69fabe`](b69fabe), [`77ccb69`](77ccb69), [`c57820d`](c57820d), [`ebaa5b7`](ebaa5b7), [`ebaa5b7`](ebaa5b7), [`3e93033`](3e93033)]: - @workflow/core@5.2.1 - @workflow/world@5.0.4 - @workflow/world-local@5.1.1 - @workflow/builders@5.0.4 - @workflow/rollup@5.0.4 ## @workflow/web@5.0.4 ### Patch Changes - Updated dependencies [[`fcb554b`](fcb554b), [`ebaa5b7`](ebaa5b7), [`b69fabe`](b69fabe), [`77ccb69`](77ccb69), [`c57820d`](c57820d), [`ebaa5b7`](ebaa5b7), [`ebaa5b7`](ebaa5b7)]: - @workflow/world-local@5.1.1 ## @workflow/web-shared@5.0.4 ### Patch Changes - Updated dependencies [[`854242e`](854242e), [`e6bd692`](e6bd692), [`fcb554b`](fcb554b), [`73a6ca3`](73a6ca3), [`fcb554b`](fcb554b), [`ebaa5b7`](ebaa5b7), [`ebaa5b7`](ebaa5b7), [`3e93033`](3e93033)]: - @workflow/core@5.2.1 - @workflow/world@5.0.4 - @workflow/utils@5.0.2 ## workflow@5.2.1 ### Patch Changes - [#4733](#4733) [`e6bd692`](e6bd692) Thanks [@VaguelySerious](https://github.com/VaguelySerious)! - Add `InBandSupersededError` and optional `inBand`/`expectedSeqInBand` event-create params for the in-band writer fence. `@workflow/world-vercel` forwards them, does not retry a fenced write in-process, and records the fence on its event spans. The runtime does not use them yet. - Updated dependencies [[`1342a96`](1342a96), [`854242e`](854242e), [`e6bd692`](e6bd692), [`fcb554b`](fcb554b), [`73a6ca3`](73a6ca3), [`fcb554b`](fcb554b)]: - @workflow/cli@5.0.4 - @workflow/core@5.2.1 - @workflow/errors@5.0.4 - @workflow/utils@5.0.2 - @workflow/next@5.0.4 - @workflow/nitro@5.0.4 - @workflow/typescript-plugin@5.0.0 - @workflow/nest@5.1.2 - @workflow/astro@5.0.4 - @workflow/rollup@5.0.4 - @workflow/sveltekit@5.0.4 - @workflow/nuxt@5.0.4 ## @workflow/world@5.0.4 ### Patch Changes - [#4733](#4733) [`e6bd692`](e6bd692) Thanks [@VaguelySerious](https://github.com/VaguelySerious)! - Add `InBandSupersededError` and optional `inBand`/`expectedSeqInBand` event-create params for the in-band writer fence. `@workflow/world-vercel` forwards them, does not retry a fenced write in-process, and records the fence on its event spans. The runtime does not use them yet. - [#4835](#4835) [`fcb554b`](fcb554b) Thanks [@pranaygp](https://github.com/pranaygp)! - Add optional `Queue.registerRoute()` and `Queue.registerHandler()`, implemented by `@workflow/world-local` and `@workflow/world-postgres`, so a queue consumer other than the Workflow SDK receives its own prefix's messages on its own route or in process. `reenqueueActiveRuns()` re-enqueues a run to the queue namespace it recorded, and `@workflow/world-postgres` keeps a job's queue prefix. - [#4821](#4821) [`ebaa5b7`](ebaa5b7) Thanks [@VaguelySerious](https://github.com/VaguelySerious)! - Implement the optional in-band writer fence and list snapshot in `@workflow/world-local` and `@workflow/world-postgres`, declared with the new `inBandFence` capability. - [#4821](#4821) [`ebaa5b7`](ebaa5b7) Thanks [@VaguelySerious](https://github.com/VaguelySerious)! - Report `deliveryCount` and `createdAt` in queue handler meta on `@workflow/world-local` and `@workflow/world-postgres`. - [#4833](#4833) [`3e93033`](3e93033) Thanks [@pranaygp](https://github.com/pranaygp)! - `@workflow/world-vercel` declares the `inBandFence` capability (its backend refuses a stale in-band write on every run with an event sequencer, and answers an in-band write on an older run with a 400), and passes on the in-band counts its backend reports: `snapshot` on `events.list` and `allocated` on a single `events.create`. The `inBandFence` contract lets a World list no snapshot when its refusals carry both counters, and `@workflow/world-testing` takes the counters from a refusal for such a World. ## @workflow/world-local@5.1.1 ### Patch Changes - [#4835](#4835) [`fcb554b`](fcb554b) Thanks [@pranaygp](https://github.com/pranaygp)! - Add optional `Queue.registerRoute()` and `Queue.registerHandler()`, implemented by `@workflow/world-local` and `@workflow/world-postgres`, so a queue consumer other than the Workflow SDK receives its own prefix's messages on its own route or in process. `reenqueueActiveRuns()` re-enqueues a run to the queue namespace it recorded, and `@workflow/world-postgres` keeps a job's queue prefix. - [#4821](#4821) [`ebaa5b7`](ebaa5b7) Thanks [@VaguelySerious](https://github.com/VaguelySerious)! - Implement the optional in-band writer fence and list snapshot in `@workflow/world-local` and `@workflow/world-postgres`, declared with the new `inBandFence` capability. - [#4838](#4838) [`b69fabe`](b69fabe) Thanks [@fazxes](https://github.com/fazxes)! - Fix `initDataDir()` crashing on Bun for a new data directory: Bun's `opendir()` reports a missing directory on the first read, not when it is opened. - [#4834](#4834) [`77ccb69`](77ccb69) Thanks [@pranaygp](https://github.com/pranaygp)! - Fix in-band event count conflicts when multiple processes write events, and fix count not surviving world restarts - [#4754](#4754) [`c57820d`](c57820d) Thanks [@pranaygp](https://github.com/pranaygp)! - Register a run's stream names under a lock so `streams.list` no longer drops a stream when several of the run's streams first write at once. A stream written again after `clear()` is listed again. - [#4821](#4821) [`ebaa5b7`](ebaa5b7) Thanks [@VaguelySerious](https://github.com/VaguelySerious)! - Report a message's cumulative `deliveryCount` across `{ timeoutSeconds }` redeliveries on `@workflow/world-postgres`. - [#4821](#4821) [`ebaa5b7`](ebaa5b7) Thanks [@VaguelySerious](https://github.com/VaguelySerious)! - Report `deliveryCount` and `createdAt` in queue handler meta on `@workflow/world-local` and `@workflow/world-postgres`. - Updated dependencies [[`e6bd692`](e6bd692), [`fcb554b`](fcb554b), [`ebaa5b7`](ebaa5b7), [`ebaa5b7`](ebaa5b7), [`3e93033`](3e93033)]: - @workflow/errors@5.0.4 - @workflow/world@5.0.4 - @workflow/utils@5.0.2 ## @workflow/world-postgres@5.0.4 ### Patch Changes - [#4817](#4817) [`026741b`](026741b) Thanks [@VaguelySerious](https://github.com/VaguelySerious)! - Fix a process crash when the database closes an idle connection - [#4835](#4835) [`fcb554b`](fcb554b) Thanks [@pranaygp](https://github.com/pranaygp)! - Add optional `Queue.registerRoute()` and `Queue.registerHandler()`, implemented by `@workflow/world-local` and `@workflow/world-postgres`, so a queue consumer other than the Workflow SDK receives its own prefix's messages on its own route or in process. `reenqueueActiveRuns()` re-enqueues a run to the queue namespace it recorded, and `@workflow/world-postgres` keeps a job's queue prefix. - [#4821](#4821) [`ebaa5b7`](ebaa5b7) Thanks [@VaguelySerious](https://github.com/VaguelySerious)! - Implement the optional in-band writer fence and list snapshot in `@workflow/world-local` and `@workflow/world-postgres`, declared with the new `inBandFence` capability. - [#4821](#4821) [`ebaa5b7`](ebaa5b7) Thanks [@VaguelySerious](https://github.com/VaguelySerious)! - Report a message's cumulative `deliveryCount` across `{ timeoutSeconds }` redeliveries on `@workflow/world-postgres`. - [#4756](#4756) [`e631e78`](e631e78) Thanks [@pranaygp](https://github.com/pranaygp)! - Honor `pagination.sortOrder` in `runs.list` and `steps.list`, which always returned newest first. - [#4821](#4821) [`ebaa5b7`](ebaa5b7) Thanks [@VaguelySerious](https://github.com/VaguelySerious)! - Report `deliveryCount` and `createdAt` in queue handler meta on `@workflow/world-local` and `@workflow/world-postgres`. - Updated dependencies [[`e6bd692`](e6bd692), [`fcb554b`](fcb554b), [`ebaa5b7`](ebaa5b7), [`b69fabe`](b69fabe), [`77ccb69`](77ccb69), [`c57820d`](c57820d), [`ebaa5b7`](ebaa5b7), [`ebaa5b7`](ebaa5b7), [`3e93033`](3e93033)]: - @workflow/errors@5.0.4 - @workflow/world@5.0.4 - @workflow/world-local@5.1.1 - @workflow/utils@5.0.2 ## @workflow/world-testing@5.0.4 ### Patch Changes - [#4821](#4821) [`ebaa5b7`](ebaa5b7) Thanks [@VaguelySerious](https://github.com/VaguelySerious)! - Implement the optional in-band writer fence and list snapshot in `@workflow/world-local` and `@workflow/world-postgres`, declared with the new `inBandFence` capability. - [#4833](#4833) [`3e93033`](3e93033) Thanks [@pranaygp](https://github.com/pranaygp)! - `@workflow/world-vercel` declares the `inBandFence` capability (its backend refuses a stale in-band write on every run with an event sequencer, and answers an in-band write on an older run with a 400), and passes on the in-band counts its backend reports: `snapshot` on `events.list` and `allocated` on a single `events.create`. The `inBandFence` contract lets a World list no snapshot when its refusals carry both counters, and `@workflow/world-testing` takes the counters from a refusal for such a World. - Updated dependencies [[`1342a96`](1342a96), [`854242e`](854242e), [`e6bd692`](e6bd692), [`fcb554b`](fcb554b), [`73a6ca3`](73a6ca3), [`fcb554b`](fcb554b), [`ebaa5b7`](ebaa5b7), [`ebaa5b7`](ebaa5b7), [`3e93033`](3e93033)]: - @workflow/cli@5.0.4 - @workflow/core@5.2.1 - @workflow/world@5.0.4 - workflow@5.2.1 ## @workflow/world-vercel@5.2.1 ### Patch Changes - [#4733](#4733) [`e6bd692`](e6bd692) Thanks [@VaguelySerious](https://github.com/VaguelySerious)! - Add `InBandSupersededError` and optional `inBand`/`expectedSeqInBand` event-create params for the in-band writer fence. `@workflow/world-vercel` forwards them, does not retry a fenced write in-process, and records the fence on its event spans. The runtime does not use them yet. - [#4833](#4833) [`3e93033`](3e93033) Thanks [@pranaygp](https://github.com/pranaygp)! - `@workflow/world-vercel` declares the `inBandFence` capability (its backend refuses a stale in-band write on every run with an event sequencer, and answers an in-band write on an older run with a 400), and passes on the in-band counts its backend reports: `snapshot` on `events.list` and `allocated` on a single `events.create`. The `inBandFence` contract lets a World list no snapshot when its refusals carry both counters, and `@workflow/world-testing` takes the counters from a refusal for such a World. - Updated dependencies [[`e6bd692`](e6bd692), [`fcb554b`](fcb554b), [`ebaa5b7`](ebaa5b7), [`ebaa5b7`](ebaa5b7), [`3e93033`](3e93033)]: - @workflow/errors@5.0.4 - @workflow/world@5.0.4 - @workflow/utils@5.0.2 Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Main now carries the World-side extractions from this branch (#4817, #4818, #4821, #4833) plus #4835, #4669/#4670, #4828 and #4838. Main's versions are the base; this branch keeps only its spec-9 additions: - World fence code is main's (#4821 review fixes; world-local's on-disk count under a lock file; world-vercel's lenient snapshot parsing from #4833). This branch adds occurredAt-as-createdAt, the step bookkeeping conformance test, and a required inBandFence (spec 9 requires a list snapshot; earlier runs may report counters on a refusal). - Per-run orchestrator serialization stays on top of #4821's queue code and #4835's routing, for the SDK's own prefix only (world-postgres gates on it, world-local keys its gate by prefix). - world-postgres keeps attempt per Graphile job (core's 48-delivery cap) with deliveryCount cumulative; tests use main's TestPool. - Main's "a same-run wake can release an inline step" queue test is replaced by a runtime test: an outside resume aborting a running inline step is committed by the blocked delivery through its log poll. - Resilient-dispatch leftovers in the QuickJS entrypoint are removed; stepInput carries the input for the step handler. - Main's loopback stream tests skip while WORKFLOW_SERVER_URL_OVERRIDE is set, like the branch's other loopback tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Description
On Bun,
initDataDir()throws for every new data directory:inspectEntityDirs()handles a missingevents/orsteps/directory by catchingENOENTfromopendir(). Node rejectsopendir()itself for a missing directory, but Bun resolves it and rejects on the first read, which happened outside thetry. This change moves the read inside the sametry, so a missing directory is skipped on both runtimes.The bug came in with the per-run layout check in #4718 and is in
@workflow/world-local5.1.0. libfx hits it on Bun withlocal().How did you test your changes?
src/init-lazy-opendir.test.tsmocksopendir()to behave as Bun's does and runsinitDataDir()on a new directory and on one with onlyevents/. Both cases fail without the fix with theENOENTabove, and pass with it.initDataDir()from the builtdist/init.json a new directory: the original code fails withENOENT ... scandir '<dataDir>/events', and the fixed build succeeds.vitest run srcinpackages/world-local: 27 files, 660 tests pass.biome checkon the changed files andnode scripts/check-changesets.mjspass.PR Checklist - Required to merge
pnpm changesetwas run to create a changelog for this PRpatchfor bug fixes,minorfor new features,majorfor breaking changes.pnpm changeset --emptyif you are changing documentation or workbench appsgit commit --signoffon your commits)@vercel/workflowin a comment once the PR is ready, and the above checklist is complete