vercel / vercel/workflow

Worlds: move World-scoped state onto the World instance so bundling can't duplicate it

Open
#3,729 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

claude-code-assisted enhancement
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 registry
  • http-client.ts — the undici keep-alive pools and the events dispatcher recycler
  • runs.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
  1. Move per-World state into createWorld()'s closure / the returned World object in world-vercel and world-local.
  2. Keep globalSingleton() for the process-wide remainder.
  3. 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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.