Worlds: move World-scoped state onto the World instance so bundling can't duplicate it
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 2.4k
- Forks
- 365
- Avg merge
- 2d 11h
- Merged PRs (30d)
- 169
Description
Follow-up to #3728, which fixed the immediate regression but left a discipline where an invariant belongs.
Background
@workflow/world-local and @workflow/world-vercel are bundled into the host application's server build. A bundler keys module identity on (resource, layer), and Next.js alone compiles instrument, app-route, ssr and edge as separate module graphs — so one process holds one copy of every module in these packages per layer. Any mutable module-scope binding is therefore per-copy state rather than the process singleton it reads as. #3493 introduced this exposure for world-vercel by removing it from serverExternalPackages; the visible casualty was the events WebSocket transport falling back to HTTP.
#3728 routes that state through globalSingleton() (globalThis + Symbol.for) and enforces it with scripts/lint/module-scope-state.mjs, run over every published packages/world-*.
The problem with stopping there
Most of the state that was converted is conceptually per-World, not per-process:
ws-transport.ts— the channel registryhttp-client.ts— the undici keep-alive pools and the events dispatcher recyclerruns.ts— the long-poll-unsupported negative cache
On the World instance, none of it can be duplicated by a bundler at all — it rides the World's existing singleton for free, and no rule is needed to keep it correct. globalThis would then be left holding only the genuinely process-wide things: ID generators whose sequence must not fork (create-run-id.ts, and world-local's evnt_/chnk_ ULID factories) and log-once latches. That is a surface small enough to reason about instead of police.
Direction
- Move per-World state into
createWorld()'s closure / the returned World object in world-vercel and world-local. - Keep
globalSingleton()for the process-wide remainder. - Leave the lint rule in place as the backstop — it should get quieter, not disappear.
Sequencing note: this pairs with #3665. Once request-time handlers and runtime execution share one World, instance state is automatically shared across every layer that reaches it — the two changes together turn "don't rely on module scope" from a rule into a property of the design.
Interaction to watch: setWorld() and the CLI/test paths construct Worlds directly, so anything moved onto the instance must be safe to have more than one of (pools already are; ID generators are not, which is why they stay global).
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start at createWorld() and the returned World object in world-vercel and world-local, then inspect ws-transport.ts, http-client.ts, and runs.ts alongside #3728's globalSingleton() changes. Verify how setWorld() and CLI/test paths construct Worlds. Done means per-World state is instance-owned, process-wide IDs and log-once latches remain global, and the module-scope-state lint still passes.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- backend, backend-api-design
- Issue type
- Refactor
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100