Skip to content

feat(state): allow multiple State instances; expose State.preload#31

Merged
exo-nikita merged 2 commits into
masterfrom
claude/state-multi-instance
May 18, 2026
Merged

feat(state): allow multiple State instances; expose State.preload#31
exo-nikita merged 2 commits into
masterfrom
claude/state-multi-instance

Conversation

@exo-nikita
Copy link
Copy Markdown
Collaborator

Summary

Groundwork for letting a StasisWebpack / StasisEsbuild plugin run alongside the stasis preload while emitting its own sidecar bundle. No user-visible behavior change.

The "one State per process" singleton is replaced with an explicit preload marker:

  • new State(root, { preload: true }) registers as the unique preload State (the lockfile-owning instance). The loader is the only caller that does this. Constructing a second preload throws.
  • new State(root) (or new State(root, configOptions) without preload) is now unrestricted — multiple non-preload States may coexist alongside the preload.
  • State.preload exposes the preload instance (undefined when no loader is active).
  • State.instance is preserved for back-compat: returns the preload if set, otherwise the sole live State, and throws ("Multiple Stasis instances; use State.preload") if several non-preload States coexist with no preload to disambiguate.

The loader (src/loader.js) is updated to construct its State with { preload: true }. No other call site changes — every existing caller (tests, plugins) creates exactly one State per process, so State.instance keeps 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-State capability landed here. Splitting keeps each PR small and reviewable.

Test plan

  • node --run lint — clean
  • node --run test — 216/216 pass (existing 215 + the rewritten state-singleton.test.js)
  • tests/state-singleton.test.js exercises both: multi-State coexistence (State.instance throws when ambiguous) and preload uniqueness (second { preload: true } rejected; State.instance returns the preload even when other non-preload States are alive).

Generated by Claude Code

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.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 Set and a separate preload slot; gate preload uniqueness on the new options.preload flag in the State constructor.
  • Update src/loader.js to construct its State with { preload: true }.
  • Rewrite tests/state-singleton.test.js to 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.
@exo-nikita exo-nikita merged commit 7e8cfa6 into master May 18, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants