feat(state): allow multiple State instances; expose State.preload#31
Conversation
Replaces the 'one State per process' singleton with an explicit preload
marker. The loader registers its State as preload (the unique
lockfile-owning instance); any number of additional non-preload States
may coexist alongside it.
- new State(root, { preload: true }) registers as the preload State;
constructing a second preload throws.
- new State(root) and new State(root, { ... }) without preload are now
unrestricted -- a bundler plugin may construct its own State for a
sidecar bundle without colliding with an active preload.
- State.preload returns the preload instance (or undefined).
- State.instance is preserved for back-compat: returns the preload if
set, otherwise the sole live State, and throws when several
non-preload States coexist and there is no preload to disambiguate.
No behavior change for existing call sites: the loader is the only
real-world preload constructor, and every other caller (tests,
plugins) creates a single State per process, so State.instance keeps
returning the same thing it did before.
This is the structural groundwork for letting a bundler plugin run
alongside the stasis preload with its own bundle output.
There was a problem hiding this comment.
Pull request overview
Replaces the strict "one State per process" singleton in src/state.js with a softer model: any number of non-preload State instances may coexist, but at most one may be flagged as the "preload" instance (the lockfile owner created by the loader). State.preload exposes that flagged instance, while State.instance is retained as a back-compat accessor that prefers the preload, falls back to the sole live State, and throws when ambiguous. This is groundwork for a follow-up that lets StasisWebpack / StasisEsbuild run alongside the preload while emitting their own sidecar bundle.
Changes:
- Track States via a module-level
Setand a separatepreloadslot; gate preload uniqueness on the newoptions.preloadflag in theStateconstructor. - Update
src/loader.jsto construct its State with{ preload: true }. - Rewrite
tests/state-singleton.test.jsto cover both multi-State coexistence and preload uniqueness/rejection.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| src/state.js | Replaces singleton enforcement with preload-marker + live-set; adds State.preload and reworks State.instance semantics. |
| src/loader.js | Loader now constructs its State as the preload instance. |
| tests/state-singleton.test.js | Tests rewritten to assert multi-State coexistence, preload uniqueness, and back-compat State.instance behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…on test - Move preload = this and liveStates.add(this) to the end of the constructor so a throw during root discovery / lockfile / bundle parsing can't leave a dangling preload reference behind. The uniqueness invariant is still checked up front so two simultaneous preload attempts can't both pass through. - Replace the misleading 'previous test's States are gone' comment in state-singleton.test.js; the registry is monotonic across subtests in the same file, so the preload short-circuit is what makes test 2 independent of test 1. - Add a third test covering the reverse ordering: preload constructed first, then non-preloads added later.
Summary
Groundwork for letting a
StasisWebpack/StasisEsbuildplugin run alongside the stasis preload while emitting its own sidecar bundle. No user-visible behavior change.The "one
Stateper process" singleton is replaced with an explicit preload marker:new State(root, { preload: true })registers as the unique preloadState(the lockfile-owning instance). The loader is the only caller that does this. Constructing a second preload throws.new State(root)(ornew State(root, configOptions)withoutpreload) is now unrestricted — multiple non-preloadStates may coexist alongside the preload.State.preloadexposes the preload instance (undefinedwhen no loader is active).State.instanceis preserved for back-compat: returns the preload if set, otherwise the sole liveState, and throws ("Multiple Stasis instances; use State.preload") if several non-preloadStates coexist with no preload to disambiguate.The loader (
src/loader.js) is updated to construct itsStatewith{ preload: true }. No other call site changes — every existing caller (tests, plugins) creates exactly oneStateper process, soState.instancekeeps returning the same thing.Why this PR exists separately
The follow-up PR will add the actual plugin↔preload coordination rules (lockfile always unified; bundle may split when locations differ). That work needs the multi-
Statecapability landed here. Splitting keeps each PR small and reviewable.Test plan
node --run lint— cleannode --run test— 216/216 pass (existing 215 + the rewrittenstate-singleton.test.js)tests/state-singleton.test.jsexercises both: multi-State coexistence (State.instancethrows when ambiguous) and preload uniqueness (second{ preload: true }rejected;State.instancereturns the preload even when other non-preloadStates are alive).Generated by Claude Code