Skip to content

feat: share state symbols across module copies - #1

Merged
alvis merged 1 commit into
masterfrom
fix/copy-safe-state
Oct 1, 2026
Merged

alvis merged 1 commit into
masterfrom
fix/copy-safe-state

Conversation

@alvis

@alvis alvis commented Oct 1, 2026 •

Copy link
Copy Markdown
Owner

📌

Every xception state key was a module-local Symbol(), so when two copies of xception load in one process, as they do with two bundled copies, neither copy can read the other's state. This change registers every key as Symbol.for('xception.<name>'), which gives all copies the same symbols.

🎯 Goal

An Xception keeps its cause, severity, tags, namespace, meta and code when another copy of xception in the same process reads, wraps or converts it.

✅ Requirements

  • $cause, $severity, $tags, $namespace, $meta, $code and $brands are the same symbols in every copy of the module.
  • xception() from one copy, given an Xception from another copy, keeps that error's severity, tags, namespace and meta.
  • An Xception constructed with a cause from another copy takes on the cause's severity and tags instead of throwing TypeError: cause[$tags] is not iterable.
  • instanceof still works across copies for Xception and for named subclasses.

🧵 Context

theriety/core runs into this when a compiled runner bundles its own copy of xception next to the service's copy. In a check with two copies of xception 9.0.0:

  • a wrapped error from the other copy lost its namespace, meta and tags, and its severity fell back to error;
  • constructing with a cause from the other copy threw.

theriety/core#665 works around this today with patches/xception@9.0.0.patch, which makes the same change.

🛠️ Implementation

  • src/symbols.ts: each state key is Symbol.for('xception.<name>'), so it is registry-global.
  • spec/symbols.spec.ts: loads two independent module graphs with vi.resetModules() and tests real behavior across them. It checks that the classes really are separate and the symbols are shared; that instanceof works for Xception and for named subclasses; and that wrapping and converting a cause from the other copy keeps its severity, tags, namespace and meta.

🧪 Verification

  • pnpm test at b96358b: 80/80 pass in 7 files.
  • eslint --max-warnings 0 src spec at b96358b: clean.
  • tsc --noEmit at b96358b: the only error is spec/base.spec.ts:274 (TS2345). The base 9b8f086 has the same error, so this change does not introduce it.
  • In a standalone two-copy check, unpatched 9.0.0 loses the state and throws as described in Context. With this change applied to both copies, every field is kept.
  • Hosted CI is green at b96358b.

📋 Additional Notes

Publish review findings and the verdict as a separate PR review or comment. Never append them to the main PR description.

  • instanceof already worked across copies before this change: Xception matches on Symbol.for('xception') and named subclasses on their registered brand. The spec covers it as a regression check. The commit message says otherwise, and that commit is signed, so it is left as it is.
  • With Symbol.for, a class from createXceptionClass whose brand name equals an internal key (tags, cause, meta, code, severity, namespace, brands) can share a registry key with xception's own state.
  • No release is made here. A release is what lets theriety/core#665 drop its patch.

Each state key was a module-local Symbol(), so a second copy of xception
in the same process (e.g. two bundled copies) could not read the cause,
severity, tags, namespace, meta or code of an instance created by the
other copy, and instanceof checks across copies failed.

Register every key with Symbol.for('xception.<name>') so all copies
resolve the same registry-global symbol.
@alvis
alvis marked this pull request as ready for review October 1, 2026 19:59
@alvis
alvis merged commit 59ee407 into master Oct 1, 2026
24 checks passed
@alvis alvis changed the title fix: share state symbols across module copies feat: share state symbols across module copies Oct 1, 2026
alvis added a commit that referenced this pull request Oct 1, 2026
Each state key was a module-local Symbol(), so a second copy of xception
in the same process (e.g. two bundled copies) could not read the cause,
severity, tags, namespace, meta or code of an instance created by the
other copy, and instanceof checks across copies failed.

Register every key with Symbol.for('xception.<name>') so all copies
resolve the same registry-global symbol.
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.

1 participant