diff --git a/spec/symbols.spec.ts b/spec/symbols.spec.ts new file mode 100644 index 0000000..842a364 --- /dev/null +++ b/spec/symbols.spec.ts @@ -0,0 +1,120 @@ +/* + * *** MIT LICENSE *** + * ------------------------------------------------------------------------- + * This code may be modified and distributed under the MIT license. + * See the LICENSE file for details. + * ------------------------------------------------------------------------- + * + * @summary Tests on state symbols shared across module copies + * + * @author Alvis HT Tang + * @license MIT + * @copyright Copyright (c) 2026 - All Rights Reserved. + * ------------------------------------------------------------------------- + */ + +import { describe, expect, it, vi } from 'vitest'; + +type Copy = typeof import('#index'); + +/** + * load an independent instance of the whole module graph, as a second bundled + * copy of xception in the same process would be + * @returns the freshly evaluated module namespace + */ +async function loadCopy(): Promise { + vi.resetModules(); + + return import('#index'); +} + +const copyA = await loadCopy(); +const copyB = await loadCopy(); + +describe('two module copies', () => { + it('should evaluate each copy into its own class', () => { + expect(copyB.Xception).not.toBe(copyA.Xception); + }); + + it('should resolve every state symbol to the same registry-global key', () => { + const pick = ({ + $cause, + $code, + $meta, + $namespace, + $severity, + $tags, + }: Copy): Record => ({ + $cause, + $code, + $meta, + $namespace, + $severity, + $tags, + }); + + expect(pick(copyB)).toEqual(pick(copyA)); + }); + + it('should treat an instance from another copy as an Xception', () => { + const error = new copyA.Xception('from copy A'); + + expect(error).toBeInstanceOf(copyB.Xception); + }); + + it('should treat a named subclass instance from another copy as that subclass', () => { + const NotFoundA = copyA.createXceptionClass('NotFoundError'); + const NotFoundB = copyB.createXceptionClass('NotFoundError'); + + expect(new NotFoundA('missing')).toBeInstanceOf(NotFoundB); + }); + + it('should inherit severity and merge tags when wrapping a cause from another copy', () => { + const cause = new copyA.Xception('inner', { + severity: 'warning', + tags: ['inner'], + }); + + const error = new copyB.Xception('outer', { cause, tags: ['outer'] }); + + expect(error).toEqual( + expect.objectContaining({ + cause, + severity: 'warning', + tags: ['inner', 'outer'], + }), + ); + }); + + it('should carry namespace, meta and tags when converting an error from another copy', () => { + const cause = new copyA.Xception('inner', { + namespace: 'copy:a', + meta: { id: 1 }, + tags: ['inner'], + severity: 'warning', + }); + + const error = copyB.xception(cause, { tags: ['outer'] }); + + expect(error).toEqual( + expect.objectContaining({ + namespace: 'copy:a', + meta: { id: 1 }, + tags: ['inner', 'outer'], + severity: 'warning', + }), + ); + }); + + it('should serialize an error whose cause comes from another copy', () => { + const cause = new copyA.Xception('inner', { code: 'E_INNER' }); + + const error = new copyB.Xception('outer', { cause }); + + expect(error.toJSON()).toEqual( + expect.objectContaining({ + cause: expect.objectContaining({ code: 'E_INNER', message: 'inner' }), + }), + ); + }); +}); diff --git a/src/symbols.ts b/src/symbols.ts index fb92037..eecc655 100644 --- a/src/symbols.ts +++ b/src/symbols.ts @@ -12,10 +12,13 @@ * ------------------------------------------------------------------------- */ -export const $namespace = Symbol('namespace'); -export const $tags = Symbol('tags'); -export const $cause = Symbol('cause'); -export const $meta = Symbol('meta'); -export const $severity = Symbol('severity'); -export const $code = Symbol('code'); -export const $brands = Symbol('xception.brands'); +// NOTE: every key is registry-global so that an instance created by one copy of +// this module (e.g. a second bundled copy in the same process) keeps its state +// readable by another copy, which a module-local Symbol() would hide +export const $namespace: unique symbol = Symbol.for('xception.namespace'); +export const $tags: unique symbol = Symbol.for('xception.tags'); +export const $cause: unique symbol = Symbol.for('xception.cause'); +export const $meta: unique symbol = Symbol.for('xception.meta'); +export const $severity: unique symbol = Symbol.for('xception.severity'); +export const $code: unique symbol = Symbol.for('xception.code'); +export const $brands: unique symbol = Symbol.for('xception.brands');