@owlmeans/error 0.1.18-rc.26 → 0.1.18-rc.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,7 +12,7 @@ framework packages already ship: authentication and authorization failures from
12
12
  ## Installation
13
13
 
14
14
  ```bash
15
- bun add @owlmeans/error@^0.1.18-rc.26
15
+ bun add @owlmeans/error@^0.1.18-rc.28
16
16
  ```
17
17
 
18
18
  ## Concepts
@@ -201,7 +201,8 @@ Panel components resolve an error through `errors.<type>` — a form- or screen-
201
201
  | `ResilientError` | class | Base class of every framework error; `constructor(type, message, stack?)` |
202
202
  | `ResilientError.typeName` | static property | Type identifier; override in each subclass (`'ResilientError'` on the base) |
203
203
  | `ResilientError.separator` | static property | Marshalling separator, initialised from `SEPARATOR` |
204
- | `ResilientError.converters` | static property | The registry of `Converter` entries |
204
+ | `ResilientError.converters` | static property | The registry of `Converter` entries — one array on `globalThis`, shared by every copy of the package in the process |
205
+ | `ResilientError[Symbol.hasInstance]` | static method | `instanceof` that also matches an instance of the same class lineage built by another copy of the package |
205
206
  | `ResilientError.registerErrorClass(Class, errorClass?)` | static method | Registers a subclass so it survives a round trip; returns its `Converter` |
206
207
  | `ResilientError.ensure(err, throwOnUnknown?)` | static method | Turns an `Error` or string into a `ResilientError`, unmarshalling registered classes |
207
208
  | `ResilientError.marshal(err)` | static method | Flattens an error into a plain `Error` whose message is `type`, `message` and stack joined by `SEPARATOR` |
@@ -216,8 +217,10 @@ Panel components resolve an error through `errors.<type>` — a form- or screen-
216
217
  |--------|------|---------|
217
218
  | `enuserError<T>(err, throwOnUnknown?)` | function | `ResilientError.ensure`, typed to the subclass you expect |
218
219
  | `marshalError(err)` | function | `ensure` then `marshal`, for a boundary that only carries an `Error` or a string |
220
+ | `isResilientError(value)` | function | Structural check (shared brand + `type` + `marshal`) that holds across duplicate module copies |
219
221
  | `SEPARATOR` | constant | Three pipe characters — joins the marshalled fields |
220
222
  | `RESILENT_ERROR` | constant | `'ResilientError'` — the base type name |
223
+ | `RESILIENT_BRAND`, `CONVERTER_REGISTRY`, `CATCH_ALL_CONVERTER` | constant | `Symbol.for` keys every copy of the package shares |
221
224
 
222
225
  ### Types
223
226
 
@@ -269,7 +272,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
269
272
  your project's skill store (`.agents/skills/`):
270
273
 
271
274
  ```sh
272
- npx @owlmeans/agent-skills@^0.1.18-rc.27
275
+ npx @owlmeans/agent-skills@^0.1.18-rc.30
273
276
  ```
274
277
 
275
278
  The embedded files are version-matched to this package release. Do not edit them
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/error",
4
- "version": "0.1.18-rc.26",
5
- "generatedAt": "2026-09-15T14:03:05.104Z",
4
+ "version": "0.1.18-rc.28",
5
+ "generatedAt": "2026-09-19T13:42:56.272Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -8,7 +8,7 @@ user-invocable: false
8
8
  # @owlmeans/error
9
9
 
10
10
  **Layer:** Core
11
- **Install:** `"@owlmeans/error": "^0.1.18-rc.26"` in `dependencies`
11
+ **Install:** `"@owlmeans/error": "^0.1.18-rc.28"` in `dependencies`
12
12
 
13
13
  ## Key Exports
14
14
 
@@ -20,7 +20,9 @@ user-invocable: false
20
20
  | `ResilientError.marshal(err)` / `err.marshal()` | Flatten into a plain `Error` a transport can carry |
21
21
  | `enuserError(err)` | `ensure`, typed to the subclass you expect |
22
22
  | `marshalError(err)` | `ensure` then `marshal`, for a boundary that only sends `Error` |
23
+ | `isResilientError(value)` | Structural check that holds across duplicate module copies |
23
24
  | `SEPARATOR` (`'\|\|\|'`), `RESILENT_ERROR` | The marshalling separator and the base type name |
25
+ | `RESILIENT_BRAND`, `CONVERTER_REGISTRY`, `CATCH_ALL_CONVERTER` | The `Symbol.for` keys every copy shares |
24
26
  | `Converter` | `{ match, convert, isMarshaled, unmarshal }` — one registry entry |
25
27
  | `ResilientErrorConstructor` | The constructor shape `registerErrorClass` accepts |
26
28
  | `ValueOrError<T>` | `T \| ResilientError`, for a result that carries either |
@@ -61,6 +63,25 @@ throw new RateLimitError('per-minute')
61
63
  A subclass of a subclass calls `super` with the message alone and then re-stamps `this.type` — the
62
64
  parent supplies its own prefix, so the final message reads `api:rate-limit:per-minute`.
63
65
 
66
+ **A refusal of the caller's condition declares its HTTP status** as `public static httpStatus = <4xx>`;
67
+ a fault — a broken peer, a timeout, a missing configuration, a bug — declares nothing and answers 500.
68
+ One principle picks the number:
69
+
70
+ | Status | The caller's condition |
71
+ |---|---|
72
+ | 400 | the request is malformed |
73
+ | 402 | the balance or plan does not pay for it |
74
+ | 404 | the addressed target does not exist, or is another organization's |
75
+ | 409 | the target's current state conflicts with the request |
76
+ | 422 | the request is understood, and its content or body is refused |
77
+
78
+ Declare it on the leaf class, never on a family base: a static is inherited, so a base's status
79
+ would reach every fault that extends it. A class thrown for several causes takes the status of the
80
+ cause a caller can reach, and one whose causes are told apart only by message declares nothing.
81
+ Nothing here reads it — `@owlmeans/server-api` does, structurally, and answers 500 for any class
82
+ without an integer 4xx declaration (see the `server-api` skill). A permission refusal extends
83
+ `AuthForbidden` (403) instead of declaring.
84
+
64
85
  `registerErrorClass` takes a second, native-class argument, and `ensure` takes a second
65
86
  `throwOnUnknown` argument. **Neither has any effect** — a catch-all converter is pushed onto the
66
87
  registry when this package loads and it is the first entry `ensure` tests for conversion, so no
@@ -84,7 +105,7 @@ What `ensure` actually does, in order:
84
105
 
85
106
  | Input | Result |
86
107
  |-------|--------|
87
- | A `ResilientError` | returned untouched |
108
+ | A `ResilientError` from any copy of this package | returned untouched |
88
109
  | A `SyntaxError` | rethrown — never converted |
89
110
  | An `Error` marshaled from a **registered** class | unmarshaled into that class, `type` and `message` restored |
90
111
  | Anything else | a bare `ResilientError` whose **`type` is the original `message`** and whose **`message` is the original stack** |
@@ -99,11 +120,32 @@ process is wired wrong (an unknown alias, a missing service, a route cycle), and
99
120
  rather than reach a user as a handled failure. Do not throw one for a runtime condition a caller is
100
121
  expected to handle.
101
122
 
123
+ ## Duplicate module copies behave as one
124
+
125
+ A process can load this package more than once — `bun --preserve-symlinks` over linked workspaces
126
+ keys a module by its unresolved path, so a package that keeps its own
127
+ `node_modules/@owlmeans/error` link loads a second copy. Every copy acts as one:
128
+
129
+ - **One registry.** `ResilientError.converters` is the array kept on `globalThis` under
130
+ `Symbol.for('@owlmeans/error:converters')`; every copy registers into it and unmarshals from it,
131
+ in registration order, so the **last registration of a type name wins** across copies. One
132
+ catch-all converter exists per process.
133
+ - **One brand.** Every instance inherits `Symbol.for('@owlmeans/error:resilient')` from its copy's
134
+ prototype. `isResilientError(value)` is brand + string `type` + `marshal` function; `ensure`
135
+ returns such a value untouched and `marshal` keeps its `type`. A lookalike without the brand is
136
+ not a resilient error.
137
+ - **`instanceof` across copies.** `ResilientError[Symbol.hasInstance]` answers natively first, then
138
+ structurally: the instance's lineage of OWN `typeName`s must end with the checked class's lineage.
139
+ A class from another copy matches itself and its ancestors, never a sibling or a parent. A class
140
+ that does not declare its own `typeName` matches natively only — declare one on every subclass.
141
+
102
142
  ## Crossing a service boundary
103
143
 
104
- `marshal` flattens `type`, `message` and the original stack into one `Error` message joined by
105
- `SEPARATOR`. On the far side `ensure` recognises the prefix and rebuilds the registered class, so a
106
- typed error thrown in a backend is caught as the same class in a client. Override
144
+ `marshal` flattens `type`, `message`, the selected stack exposure and an optional incident id into
145
+ one `Error` message joined by `SEPARATOR`. On the far side `ensure` recognises the prefix and rebuilds
146
+ the registered class, so a typed error thrown in a backend is caught as the same class in a client.
147
+ The HTTP boundary attaches the incident id to the logged error and sends only that id in production;
148
+ development callers may select the stack explicitly. Override
107
149
  `finalizeUnmarshal()` on a subclass that needs to rebuild state from its message after that.
108
150
 
109
151
  ## Messages are i18n keys
package/build/consts.d.ts CHANGED
@@ -1,3 +1,16 @@
1
1
  export declare const SEPARATOR = "|||";
2
2
  export declare const RESILENT_ERROR = "ResilientError";
3
+ /**
4
+ * Marks every `ResilientError` instance, whichever copy of this module built it.
5
+ *
6
+ * A process can hold several copies of this package (`bun --preserve-symlinks` over a linked
7
+ * workspace resolves one package through several `node_modules` paths), and `instanceof` answers
8
+ * for one copy only. `Symbol.for` returns the same symbol in every copy, so the brand is the
9
+ * structural proof `ensure`, `marshal` and `instanceof` read.
10
+ */
11
+ export declare const RESILIENT_BRAND: unique symbol;
12
+ /** The `globalThis` key of the converter registry every copy of this module shares. */
13
+ export declare const CONVERTER_REGISTRY: unique symbol;
14
+ /** Marks the catch-all converter, so a second module copy does not push another one. */
15
+ export declare const CATCH_ALL_CONVERTER: unique symbol;
3
16
  //# sourceMappingURL=consts.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,SAAS,QAAQ,CAAA;AAE9B,eAAO,MAAM,cAAc,mBAAmB,CAAA"}
1
+ {"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,SAAS,QAAQ,CAAA;AAE9B,eAAO,MAAM,cAAc,mBAAmB,CAAA;AAE9C;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,EAAE,OAAO,MAAgD,CAAA;AAErF,uFAAuF;AACvF,eAAO,MAAM,kBAAkB,EAAE,OAAO,MAAiD,CAAA;AAEzF,wFAAwF;AACxF,eAAO,MAAM,mBAAmB,EAAE,OAAO,MAAgD,CAAA"}
package/build/consts.js CHANGED
@@ -1,3 +1,16 @@
1
1
  export const SEPARATOR = '|||';
2
2
  export const RESILENT_ERROR = 'ResilientError';
3
+ /**
4
+ * Marks every `ResilientError` instance, whichever copy of this module built it.
5
+ *
6
+ * A process can hold several copies of this package (`bun --preserve-symlinks` over a linked
7
+ * workspace resolves one package through several `node_modules` paths), and `instanceof` answers
8
+ * for one copy only. `Symbol.for` returns the same symbol in every copy, so the brand is the
9
+ * structural proof `ensure`, `marshal` and `instanceof` read.
10
+ */
11
+ export const RESILIENT_BRAND = Symbol.for('@owlmeans/error:resilient');
12
+ /** The `globalThis` key of the converter registry every copy of this module shares. */
13
+ export const CONVERTER_REGISTRY = Symbol.for('@owlmeans/error:converters');
14
+ /** Marks the catch-all converter, so a second module copy does not push another one. */
15
+ export const CATCH_ALL_CONVERTER = Symbol.for('@owlmeans/error:catch-all');
3
16
  //# sourceMappingURL=consts.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,MAAM,CAAC,MAAM,SAAS,GAAG,KAAK,CAAA;AAE9B,MAAM,CAAC,MAAM,cAAc,GAAG,gBAAgB,CAAA"}
1
+ {"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,MAAM,CAAC,MAAM,SAAS,GAAG,KAAK,CAAA;AAE9B,MAAM,CAAC,MAAM,cAAc,GAAG,gBAAgB,CAAA;AAE9C;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,eAAe,GAAkB,MAAM,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAA;AAErF,uFAAuF;AACvF,MAAM,CAAC,MAAM,kBAAkB,GAAkB,MAAM,CAAC,GAAG,CAAC,4BAA4B,CAAC,CAAA;AAEzF,wFAAwF;AACxF,MAAM,CAAC,MAAM,mBAAmB,GAAkB,MAAM,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAA"}
@@ -1,15 +1,33 @@
1
- import type { Converter, ResilientErrorConstructor } from './types.js';
1
+ import type { Converter, MarshalErrorOptions, ResilientErrorConstructor } from './types.js';
2
+ /**
3
+ * Whether a value is a `ResilientError` built by ANY copy of this module.
4
+ *
5
+ * Structural: the shared brand plus the `type` and `marshal` every instance carries.
6
+ */
7
+ export declare const isResilientError: (value: unknown) => value is ResilientError;
2
8
  export declare class ResilientError extends Error {
3
9
  static separator: string;
4
10
  static typeName: string;
5
11
  static converters: Converter[];
12
+ /**
13
+ * `instanceof` that holds across module copies.
14
+ *
15
+ * The native prototype check answers first. Failing that, a branded instance matches when its
16
+ * class lineage ENDS WITH this class's lineage — the same own type names, in the same order, up
17
+ * to the base. So `instanceof AuthFailedError` accepts an `AuthFailedError` (or a subclass of it)
18
+ * from another copy, and never a sibling class or a parent. A class that does not declare its
19
+ * own `typeName` cannot be told apart structurally and answers natively only.
20
+ */
21
+ static [Symbol.hasInstance]<T extends abstract new (...args: any[]) => any>(this: T, instance: unknown): instance is InstanceType<T>;
6
22
  static registerErrorClass(resilientErrorClass: ResilientErrorConstructor, errorClass?: ErrorConstructor): Converter;
7
23
  static ensure(err: Error | string, throwOnUnknown?: boolean): ResilientError;
8
- static marshal(err: Error): Error;
24
+ static marshal(err: Error, opts?: MarshalErrorOptions): Error;
9
25
  type: string;
10
26
  oiriginalStack?: string;
27
+ /** Correlates a serialized boundary error with the server log entry that owns its full stack. */
28
+ incidentId?: string;
11
29
  constructor(type: string, message: string, stack?: string);
12
- marshal(): Error;
30
+ marshal(opts?: MarshalErrorOptions): Error;
13
31
  finalizeUnmarshal(): void;
14
32
  }
15
33
  //# sourceMappingURL=resilient.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"resilient.d.ts","sourceRoot":"","sources":["../src/resilient.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,yBAAyB,EAAE,MAAM,YAAY,CAAA;AAGtE,qBAAa,cAAe,SAAQ,KAAK;IACvC,OAAc,SAAS,EAAE,MAAM,CAAY;IAE3C,OAAc,QAAQ,EAAE,MAAM,CAAiB;IAE/C,OAAc,UAAU,EAAE,SAAS,EAAE,CAAK;IAE1C,OAAc,kBAAkB,CAAC,mBAAmB,EAAE,yBAAyB,EAAE,UAAU,CAAC,EAAE,gBAAgB,GAAG,SAAS,CAKzH;IAED,OAAc,MAAM,CAAC,GAAG,EAAE,KAAK,GAAG,MAAM,EAAE,cAAc,CAAC,EAAE,OAAO,GAAG,cAAc,CA4BlF;IAED,OAAc,OAAO,CAAC,GAAG,EAAE,KAAK,GAAG,KAAK,CAMvC;IAEM,IAAI,EAAE,MAAM,CAAiB;IAE7B,cAAc,CAAC,EAAE,MAAM,CAAA;IAE9B,YAAY,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAQxD;IAED,OAAO,IAAI,KAAK,CAEf;IAED,iBAAiB,IAAI,IAAI,CAAI;CAC9B"}
1
+ {"version":3,"file":"resilient.d.ts","sourceRoot":"","sources":["../src/resilient.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,mBAAmB,EAAE,yBAAyB,EAAE,MAAM,YAAY,CAAA;AAyC3F;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,UAAW,OAAO,KAAG,KAAK,IAAI,cAS1D,CAAA;AAED,qBAAa,cAAe,SAAQ,KAAK;IACvC,OAAc,SAAS,EAAE,MAAM,CAAY;IAE3C,OAAc,QAAQ,EAAE,MAAM,CAAiB;IAE/C,OAAc,UAAU,EAAE,SAAS,EAAE,CAAqB;IAE1D;;;;;;;;OAQG;IACH,OAAuB,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS,QAAQ,MAAM,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,EACxF,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,OAAO,GACzB,QAAQ,IAAI,YAAY,CAAC,CAAC,CAAC,CAY7B;IAED,OAAc,kBAAkB,CAAC,mBAAmB,EAAE,yBAAyB,EAAE,UAAU,CAAC,EAAE,gBAAgB,GAAG,SAAS,CAKzH;IAED,OAAc,MAAM,CAAC,GAAG,EAAE,KAAK,GAAG,MAAM,EAAE,cAAc,CAAC,EAAE,OAAO,GAAG,cAAc,CA4BlF;IAED,OAAc,OAAO,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,mBAAmB,GAAG,KAAK,CAenE;IAEM,IAAI,EAAE,MAAM,CAAiB;IAE7B,cAAc,CAAC,EAAE,MAAM,CAAA;IAE9B,iGAAiG;IAC1F,UAAU,CAAC,EAAE,MAAM,CAAA;IAE1B,YAAY,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAQxD;IAED,OAAO,CAAC,IAAI,CAAC,EAAE,mBAAmB,GAAG,KAAK,CAEzC;IAED,iBAAiB,IAAI,IAAI,CAAI;CAC9B"}
@@ -1,9 +1,76 @@
1
- import { RESILENT_ERROR, SEPARATOR } from './consts.js';
1
+ import { CATCH_ALL_CONVERTER, CONVERTER_REGISTRY, RESILENT_ERROR, RESILIENT_BRAND, SEPARATOR } from './consts.js';
2
2
  import { createErrorConverter } from './utils.js';
3
+ /**
4
+ * The one converter registry of the process.
5
+ *
6
+ * Kept on `globalThis` so every copy of this module registers into, and unmarshals from, the same
7
+ * list: a class registered by one copy is rebuilt by another. Registration order is kept across
8
+ * copies, so the last registration of a type name still wins.
9
+ */
10
+ const sharedConverters = () => {
11
+ const holder = globalThis;
12
+ holder[CONVERTER_REGISTRY] ??= [];
13
+ return holder[CONVERTER_REGISTRY];
14
+ };
15
+ /**
16
+ * The type names a class declares itself and inherits, nearest first — only names a class owns.
17
+ *
18
+ * A class that does not redeclare `typeName` inherits its parent's value, and counting that twice
19
+ * would let a check against the subclass match any instance of the parent.
20
+ */
21
+ const lineageOf = (ctor) => {
22
+ const names = [];
23
+ let current = ctor;
24
+ while (typeof current === 'function') {
25
+ if (Object.prototype.hasOwnProperty.call(current, 'typeName')) {
26
+ const name = current.typeName;
27
+ if (typeof name === 'string') {
28
+ names.push(name);
29
+ }
30
+ }
31
+ current = Object.getPrototypeOf(current);
32
+ }
33
+ return names;
34
+ };
35
+ /**
36
+ * Whether a value is a `ResilientError` built by ANY copy of this module.
37
+ *
38
+ * Structural: the shared brand plus the `type` and `marshal` every instance carries.
39
+ */
40
+ export const isResilientError = (value) => {
41
+ if (value == null || typeof value !== 'object') {
42
+ return false;
43
+ }
44
+ const candidate = value;
45
+ return candidate[RESILIENT_BRAND] === true
46
+ && typeof candidate.type === 'string'
47
+ && typeof candidate.marshal === 'function';
48
+ };
3
49
  export class ResilientError extends Error {
4
50
  static separator = SEPARATOR;
5
51
  static typeName = RESILENT_ERROR;
6
- static converters = [];
52
+ static converters = sharedConverters();
53
+ /**
54
+ * `instanceof` that holds across module copies.
55
+ *
56
+ * The native prototype check answers first. Failing that, a branded instance matches when its
57
+ * class lineage ENDS WITH this class's lineage — the same own type names, in the same order, up
58
+ * to the base. So `instanceof AuthFailedError` accepts an `AuthFailedError` (or a subclass of it)
59
+ * from another copy, and never a sibling class or a parent. A class that does not declare its
60
+ * own `typeName` cannot be told apart structurally and answers natively only.
61
+ */
62
+ static [Symbol.hasInstance](instance) {
63
+ if (Function.prototype[Symbol.hasInstance].call(this, instance)) {
64
+ return true;
65
+ }
66
+ if (!isResilientError(instance) || !Object.prototype.hasOwnProperty.call(this, 'typeName')) {
67
+ return false;
68
+ }
69
+ const expected = lineageOf(this);
70
+ const actual = lineageOf(Object.getPrototypeOf(instance)?.constructor);
71
+ const offset = actual.length - expected.length;
72
+ return offset >= 0 && expected.every((name, index) => actual[offset + index] === name);
73
+ }
7
74
  static registerErrorClass(resilientErrorClass, errorClass) {
8
75
  const converter = createErrorConverter(resilientErrorClass, errorClass);
9
76
  this.converters.push(converter);
@@ -11,7 +78,7 @@ export class ResilientError extends Error {
11
78
  }
12
79
  static ensure(err, throwOnUnknown) {
13
80
  err = typeof err === 'string' ? new Error(err) : err;
14
- if (err instanceof ResilientError) {
81
+ if (isResilientError(err)) {
15
82
  return err;
16
83
  }
17
84
  // We don't proceed SyntaxError - system should crash in this case
@@ -33,14 +100,25 @@ export class ResilientError extends Error {
33
100
  }
34
101
  return new ResilientError(this.typeName, err.message, err.stack);
35
102
  }
36
- static marshal(err) {
37
- if (err instanceof ResilientError) {
38
- return new Error([err.type, err.message, err.oiriginalStack].join(this.separator));
103
+ static marshal(err, opts) {
104
+ const resilient = isResilientError(err);
105
+ const structuralType = err.type;
106
+ const type = resilient
107
+ ? err.type
108
+ : typeof structuralType === 'string' ? structuralType : this.typeName;
109
+ const stack = opts?.includeStack === false
110
+ ? ''
111
+ : resilient ? err.oiriginalStack : err.stack;
112
+ const fields = [type, err.message, stack];
113
+ if (opts?.incidentId != null) {
114
+ fields.push(opts.incidentId);
39
115
  }
40
- return new Error([this.typeName, err.message, err.stack].join(this.separator));
116
+ return new Error(fields.join(this.separator));
41
117
  }
42
118
  type = RESILENT_ERROR;
43
119
  oiriginalStack;
120
+ /** Correlates a serialized boundary error with the server log entry that owns its full stack. */
121
+ incidentId;
44
122
  constructor(type, message, stack) {
45
123
  super(message);
46
124
  this.type = type;
@@ -51,13 +129,18 @@ export class ResilientError extends Error {
51
129
  this.oiriginalStack = this.stack;
52
130
  }
53
131
  }
54
- marshal() {
55
- return ResilientError.marshal(this);
132
+ marshal(opts) {
133
+ return ResilientError.marshal(this, opts);
56
134
  }
57
135
  finalizeUnmarshal() { }
58
136
  }
59
- ResilientError.converters.push({
60
- ...createErrorConverter(ResilientError),
61
- match: () => true
62
- });
137
+ // On the prototype, not the instance: non-enumerable, and inherited by every subclass of this copy.
138
+ Object.defineProperty(ResilientError.prototype, RESILIENT_BRAND, { value: true });
139
+ // One catch-all per process: a second module copy finds the first copy's entry and adds none.
140
+ if (!ResilientError.converters.some(converter => converter[CATCH_ALL_CONVERTER] === true)) {
141
+ ResilientError.converters.push(Object.assign({
142
+ ...createErrorConverter(ResilientError),
143
+ match: () => true,
144
+ }, { [CATCH_ALL_CONVERTER]: true }));
145
+ }
63
146
  //# sourceMappingURL=resilient.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"resilient.js","sourceRoot":"","sources":["../src/resilient.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"resilient.js","sourceRoot":"","sources":["../src/resilient.ts"],"names":[],"mappings":"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"}
package/build/types.d.ts CHANGED
@@ -1,5 +1,11 @@
1
1
  import type { ResilientError } from './resilient.js';
2
2
  export type ValueOrError<T> = T | ResilientError;
3
+ export interface MarshalErrorOptions {
4
+ /** Defaults to true for backwards compatibility outside an HTTP boundary. */
5
+ includeStack?: boolean;
6
+ /** Opaque correlation value appended as the fourth wire field. */
7
+ incidentId?: string;
8
+ }
3
9
  export interface Converter {
4
10
  match: (err: Error) => boolean;
5
11
  convert: (err: Error) => ResilientError;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAEpD,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI,CAAC,GAAG,cAAc,CAAA;AAEhD,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,OAAO,CAAA;IAC9B,OAAO,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,cAAc,CAAA;IACvC,WAAW,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,OAAO,CAAA;IACpC,SAAS,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,cAAc,CAAA;CAC1C;AAED,MAAM,WAAW,yBAAyB,CAAC,CAAC,SAAS,cAAc,GAAG,cAAc;IAClF,KAAK,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,CAAC,CAAA;IACtD,KAAK,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,CAAC,CAAA;IACxC,QAAQ,EAAE,MAAM,CAAA;CACjB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAEpD,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI,CAAC,GAAG,cAAc,CAAA;AAEhD,MAAM,WAAW,mBAAmB;IAClC,6EAA6E;IAC7E,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,kEAAkE;IAClE,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,OAAO,CAAA;IAC9B,OAAO,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,cAAc,CAAA;IACvC,WAAW,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,OAAO,CAAA;IACpC,SAAS,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,cAAc,CAAA;CAC1C;AAED,MAAM,WAAW,yBAAyB,CAAC,CAAC,SAAS,cAAc,GAAG,cAAc;IAClF,KAAK,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,CAAC,CAAA;IACtD,KAAK,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,CAAC,CAAA;IACxC,QAAQ,EAAE,MAAM,CAAA;CACjB"}
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAC/C,OAAO,KAAK,EAAE,SAAS,EAAE,yBAAyB,EAAE,MAAM,YAAY,CAAA;AAEtE,eAAO,MAAM,oBAAoB,wBACV,yBAAyB,eACjC,gBAAgB,KAC5B,SAQF,CAAA;AAED,eAAO,MAAM,SAAS,GAAI,CAAC,SAAS,cAAc,GAAG,cAAc,cAAc,yBAAyB,WAClG,KAAK,KAAG,CAiBb,CAAA"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAC/C,OAAO,KAAK,EAAE,SAAS,EAAE,yBAAyB,EAAE,MAAM,YAAY,CAAA;AAEtE,eAAO,MAAM,oBAAoB,wBACV,yBAAyB,eACjC,gBAAgB,KAC5B,SAQF,CAAA;AAED,eAAO,MAAM,SAAS,GAAI,CAAC,SAAS,cAAc,GAAG,cAAc,cAAc,yBAAyB,WAClG,KAAK,KAAG,CAmBb,CAAA"}
package/build/utils.js CHANGED
@@ -11,15 +11,19 @@ export const unmarshal = (errorClass) => (err) => {
11
11
  if (err instanceof errorClass) {
12
12
  return err;
13
13
  }
14
- const args = err.message.split(ResilientError.separator, 3);
15
- if (args.length < 2) {
14
+ const fields = err.message.split(ResilientError.separator, 4);
15
+ if (fields.length < 2) {
16
16
  throw SyntaxError('Invalid marshaled error');
17
17
  }
18
- if (args[0] === errorClass.typeName) {
19
- args.shift();
20
- }
21
- const error = new errorClass(...args);
22
- error.message = args[0];
18
+ const [type, message, stack, incidentId] = fields;
19
+ // The base constructor takes (type, message, stack); subclasses take (message, stack).
20
+ // Keeping the base's wire type in place avoids shifting a plain Error's message into `type`.
21
+ const error = errorClass === ResilientError
22
+ ? new errorClass(type, message, stack)
23
+ : new errorClass(message, stack);
24
+ error.type = type;
25
+ error.message = message;
26
+ error.incidentId = incidentId;
23
27
  error.finalizeUnmarshal();
24
28
  return error;
25
29
  };
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAG/C,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAClC,mBAA8C,EAC9C,UAA6B,EAClB,EAAE;IACb,OAAO;QACL,KAAK,EAAE,GAAG,CAAC,EAAE,CAAC,UAAU,IAAI,IAAI,IAAI,GAAG,YAAY,UAAU;QAC7D,OAAO,EAAE,GAAG,CAAC,EAAE,CAAC,IAAI,mBAAmB,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,KAAK,CAAC;QAC/D,WAAW,EAAE,GAAG,CAAC,EAAE,CACjB,GAAG,CAAC,OAAO,CAAC,UAAU,CAAC,mBAAmB,CAAC,QAAQ,GAAG,cAAc,CAAC,SAAS,CAAC;QACjF,SAAS,EAAE,SAAS,CAAC,mBAAmB,CAAC;KAC1C,CAAA;AACH,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,SAAS,GAAG,CAA4C,UAAqC,EAAE,EAAE,CAC5G,CAAC,GAAU,EAAK,EAAE;IAChB,IAAI,GAAG,YAAY,UAAU,EAAE,CAAC;QAC9B,OAAO,GAAQ,CAAA;IACjB,CAAC;IACD,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,cAAc,CAAC,SAAS,EAAE,CAAC,CAA8B,CAAA;IACxF,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACpB,MAAM,WAAW,CAAC,yBAAyB,CAAC,CAAA;IAC9C,CAAC;IAED,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,UAAU,CAAC,QAAQ,EAAE,CAAC;QACpC,IAAI,CAAC,KAAK,EAAE,CAAA;IACd,CAAC;IAED,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,GAAG,IAAI,CAAC,CAAA;IACrC,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,CAAA;IACvB,KAAK,CAAC,iBAAiB,EAAE,CAAA;IACzB,OAAO,KAAU,CAAA;AACnB,CAAC,CAAA"}
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAG/C,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAClC,mBAA8C,EAC9C,UAA6B,EAClB,EAAE;IACb,OAAO;QACL,KAAK,EAAE,GAAG,CAAC,EAAE,CAAC,UAAU,IAAI,IAAI,IAAI,GAAG,YAAY,UAAU;QAC7D,OAAO,EAAE,GAAG,CAAC,EAAE,CAAC,IAAI,mBAAmB,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,KAAK,CAAC;QAC/D,WAAW,EAAE,GAAG,CAAC,EAAE,CACjB,GAAG,CAAC,OAAO,CAAC,UAAU,CAAC,mBAAmB,CAAC,QAAQ,GAAG,cAAc,CAAC,SAAS,CAAC;QACjF,SAAS,EAAE,SAAS,CAAC,mBAAmB,CAAC;KAC1C,CAAA;AACH,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,SAAS,GAAG,CAA4C,UAAqC,EAAE,EAAE,CAC5G,CAAC,GAAU,EAAK,EAAE;IAChB,IAAI,GAAG,YAAY,UAAU,EAAE,CAAC;QAC9B,OAAO,GAAQ,CAAA;IACjB,CAAC;IACD,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,cAAc,CAAC,SAAS,EAAE,CAAC,CAAC,CAAA;IAC7D,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtB,MAAM,WAAW,CAAC,yBAAyB,CAAC,CAAA;IAC9C,CAAC;IACD,MAAM,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,CAAC,GAAG,MAAM,CAAA;IACjD,uFAAuF;IACvF,6FAA6F;IAC7F,MAAM,KAAK,GAAG,UAAU,KAAK,cAAc;QACzC,CAAC,CAAC,IAAI,UAAU,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC;QACtC,CAAC,CAAC,IAAI,UAAU,CAAC,OAAO,EAAE,KAAK,CAAC,CAAA;IAClC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAA;IACjB,KAAK,CAAC,OAAO,GAAG,OAAO,CAAA;IACvB,KAAK,CAAC,UAAU,GAAG,UAAU,CAAA;IAC7B,KAAK,CAAC,iBAAiB,EAAE,CAAA;IACzB,OAAO,KAAU,CAAA;AACnB,CAAC,CAAA"}
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@owlmeans/error",
3
- "version": "0.1.18-rc.26",
3
+ "version": "0.1.18-rc.28",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "build": "tsc -b",
8
+ "test": "bun test ./tests",
8
9
  "dev": "sleep 156 && nodemon -e ts,tsx,json --watch src --exec \"tsc -p ./tsconfig.json\"",
9
10
  "watch": "tsc -b -w --preserveWatchOutput --pretty"
10
11
  },
@@ -21,7 +22,7 @@
21
22
  }
22
23
  },
23
24
  "dependencies": {
24
- "@owlmeans/i18n": "^0.1.18-rc.27"
25
+ "@owlmeans/i18n": "^0.1.18-rc.29"
25
26
  },
26
27
  "devDependencies": {
27
28
  "@owlmeans/dep-config": "workspace:*",
package/src/consts.ts CHANGED
@@ -2,3 +2,19 @@
2
2
  export const SEPARATOR = '|||'
3
3
 
4
4
  export const RESILENT_ERROR = 'ResilientError'
5
+
6
+ /**
7
+ * Marks every `ResilientError` instance, whichever copy of this module built it.
8
+ *
9
+ * A process can hold several copies of this package (`bun --preserve-symlinks` over a linked
10
+ * workspace resolves one package through several `node_modules` paths), and `instanceof` answers
11
+ * for one copy only. `Symbol.for` returns the same symbol in every copy, so the brand is the
12
+ * structural proof `ensure`, `marshal` and `instanceof` read.
13
+ */
14
+ export const RESILIENT_BRAND: unique symbol = Symbol.for('@owlmeans/error:resilient')
15
+
16
+ /** The `globalThis` key of the converter registry every copy of this module shares. */
17
+ export const CONVERTER_REGISTRY: unique symbol = Symbol.for('@owlmeans/error:converters')
18
+
19
+ /** Marks the catch-all converter, so a second module copy does not push another one. */
20
+ export const CATCH_ALL_CONVERTER: unique symbol = Symbol.for('@owlmeans/error:catch-all')
package/src/resilient.ts CHANGED
@@ -1,13 +1,92 @@
1
- import { RESILENT_ERROR, SEPARATOR } from './consts.js'
2
- import type { Converter, ResilientErrorConstructor } from './types.js'
1
+ import { CATCH_ALL_CONVERTER, CONVERTER_REGISTRY, RESILENT_ERROR, RESILIENT_BRAND, SEPARATOR } from './consts.js'
2
+ import type { Converter, MarshalErrorOptions, ResilientErrorConstructor } from './types.js'
3
3
  import { createErrorConverter } from './utils.js'
4
4
 
5
+ type ConverterHolder = typeof globalThis & { [CONVERTER_REGISTRY]?: Converter[] }
6
+
7
+ /**
8
+ * The one converter registry of the process.
9
+ *
10
+ * Kept on `globalThis` so every copy of this module registers into, and unmarshals from, the same
11
+ * list: a class registered by one copy is rebuilt by another. Registration order is kept across
12
+ * copies, so the last registration of a type name still wins.
13
+ */
14
+ const sharedConverters = (): Converter[] => {
15
+ const holder = globalThis as ConverterHolder
16
+ holder[CONVERTER_REGISTRY] ??= []
17
+
18
+ return holder[CONVERTER_REGISTRY]
19
+ }
20
+
21
+ /**
22
+ * The type names a class declares itself and inherits, nearest first — only names a class owns.
23
+ *
24
+ * A class that does not redeclare `typeName` inherits its parent's value, and counting that twice
25
+ * would let a check against the subclass match any instance of the parent.
26
+ */
27
+ const lineageOf = (ctor: unknown): string[] => {
28
+ const names: string[] = []
29
+ let current = ctor
30
+ while (typeof current === 'function') {
31
+ if (Object.prototype.hasOwnProperty.call(current, 'typeName')) {
32
+ const name = (current as { typeName?: unknown }).typeName
33
+ if (typeof name === 'string') {
34
+ names.push(name)
35
+ }
36
+ }
37
+ current = Object.getPrototypeOf(current)
38
+ }
39
+
40
+ return names
41
+ }
42
+
43
+ /**
44
+ * Whether a value is a `ResilientError` built by ANY copy of this module.
45
+ *
46
+ * Structural: the shared brand plus the `type` and `marshal` every instance carries.
47
+ */
48
+ export const isResilientError = (value: unknown): value is ResilientError => {
49
+ if (value == null || typeof value !== 'object') {
50
+ return false
51
+ }
52
+ const candidate = value as { [RESILIENT_BRAND]?: unknown, type?: unknown, marshal?: unknown }
53
+
54
+ return candidate[RESILIENT_BRAND] === true
55
+ && typeof candidate.type === 'string'
56
+ && typeof candidate.marshal === 'function'
57
+ }
58
+
5
59
  export class ResilientError extends Error {
6
60
  public static separator: string = SEPARATOR
7
61
 
8
62
  public static typeName: string = RESILENT_ERROR
9
63
 
10
- public static converters: Converter[] = []
64
+ public static converters: Converter[] = sharedConverters()
65
+
66
+ /**
67
+ * `instanceof` that holds across module copies.
68
+ *
69
+ * The native prototype check answers first. Failing that, a branded instance matches when its
70
+ * class lineage ENDS WITH this class's lineage — the same own type names, in the same order, up
71
+ * to the base. So `instanceof AuthFailedError` accepts an `AuthFailedError` (or a subclass of it)
72
+ * from another copy, and never a sibling class or a parent. A class that does not declare its
73
+ * own `typeName` cannot be told apart structurally and answers natively only.
74
+ */
75
+ public static override [Symbol.hasInstance]<T extends abstract new (...args: any[]) => any>(
76
+ this: T, instance: unknown
77
+ ): instance is InstanceType<T> {
78
+ if (Function.prototype[Symbol.hasInstance].call(this, instance)) {
79
+ return true
80
+ }
81
+ if (!isResilientError(instance) || !Object.prototype.hasOwnProperty.call(this, 'typeName')) {
82
+ return false
83
+ }
84
+ const expected = lineageOf(this)
85
+ const actual = lineageOf(Object.getPrototypeOf(instance)?.constructor)
86
+ const offset = actual.length - expected.length
87
+
88
+ return offset >= 0 && expected.every((name, index) => actual[offset + index] === name)
89
+ }
11
90
 
12
91
  public static registerErrorClass(resilientErrorClass: ResilientErrorConstructor, errorClass?: ErrorConstructor): Converter {
13
92
  const converter = createErrorConverter(resilientErrorClass, errorClass)
@@ -18,7 +97,7 @@ export class ResilientError extends Error {
18
97
 
19
98
  public static ensure(err: Error | string, throwOnUnknown?: boolean): ResilientError {
20
99
  err = typeof err === 'string' ? new Error(err) : err
21
- if (err instanceof ResilientError) {
100
+ if (isResilientError(err)) {
22
101
  return err
23
102
  }
24
103
 
@@ -46,18 +125,30 @@ export class ResilientError extends Error {
46
125
  return new ResilientError(this.typeName, err.message, err.stack)
47
126
  }
48
127
 
49
- public static marshal(err: Error): Error {
50
- if (err instanceof ResilientError) {
51
- return new Error([err.type, err.message, err.oiriginalStack].join(this.separator))
128
+ public static marshal(err: Error, opts?: MarshalErrorOptions): Error {
129
+ const resilient = isResilientError(err)
130
+ const structuralType = (err as Error & { type?: unknown }).type
131
+ const type = resilient
132
+ ? err.type
133
+ : typeof structuralType === 'string' ? structuralType : this.typeName
134
+ const stack = opts?.includeStack === false
135
+ ? ''
136
+ : resilient ? err.oiriginalStack : err.stack
137
+ const fields: Array<string | undefined> = [type, err.message, stack]
138
+ if (opts?.incidentId != null) {
139
+ fields.push(opts.incidentId)
52
140
  }
53
141
 
54
- return new Error([this.typeName, err.message, err.stack].join(this.separator))
142
+ return new Error(fields.join(this.separator))
55
143
  }
56
144
 
57
145
  public type: string = RESILENT_ERROR
58
146
 
59
147
  public oiriginalStack?: string
60
148
 
149
+ /** Correlates a serialized boundary error with the server log entry that owns its full stack. */
150
+ public incidentId?: string
151
+
61
152
  constructor(type: string, message: string, stack?: string) {
62
153
  super(message)
63
154
  this.type = type
@@ -68,14 +159,20 @@ export class ResilientError extends Error {
68
159
  }
69
160
  }
70
161
 
71
- marshal(): Error {
72
- return ResilientError.marshal(this)
162
+ marshal(opts?: MarshalErrorOptions): Error {
163
+ return ResilientError.marshal(this, opts)
73
164
  }
74
165
 
75
166
  finalizeUnmarshal(): void { }
76
167
  }
77
168
 
78
- ResilientError.converters.push({
79
- ...createErrorConverter(ResilientError as ResilientErrorConstructor),
80
- match: () => true
81
- })
169
+ // On the prototype, not the instance: non-enumerable, and inherited by every subclass of this copy.
170
+ Object.defineProperty(ResilientError.prototype, RESILIENT_BRAND, { value: true })
171
+
172
+ // One catch-all per process: a second module copy finds the first copy's entry and adds none.
173
+ if (!ResilientError.converters.some(converter => (converter as { [CATCH_ALL_CONVERTER]?: unknown })[CATCH_ALL_CONVERTER] === true)) {
174
+ ResilientError.converters.push(Object.assign({
175
+ ...createErrorConverter(ResilientError as ResilientErrorConstructor),
176
+ match: () => true,
177
+ }, { [CATCH_ALL_CONVERTER]: true }))
178
+ }
package/src/types.ts CHANGED
@@ -2,6 +2,13 @@ import type { ResilientError } from './resilient.js'
2
2
 
3
3
  export type ValueOrError<T> = T | ResilientError
4
4
 
5
+ export interface MarshalErrorOptions {
6
+ /** Defaults to true for backwards compatibility outside an HTTP boundary. */
7
+ includeStack?: boolean
8
+ /** Opaque correlation value appended as the fourth wire field. */
9
+ incidentId?: string
10
+ }
11
+
5
12
  export interface Converter {
6
13
  match: (err: Error) => boolean
7
14
  convert: (err: Error) => ResilientError
package/src/utils.ts CHANGED
@@ -19,17 +19,19 @@ export const unmarshal = <T extends ResilientError = ResilientError>(errorClass:
19
19
  if (err instanceof errorClass) {
20
20
  return err as T
21
21
  }
22
- const args = err.message.split(ResilientError.separator, 3) as [string, string, string?]
23
- if (args.length < 2) {
22
+ const fields = err.message.split(ResilientError.separator, 4)
23
+ if (fields.length < 2) {
24
24
  throw SyntaxError('Invalid marshaled error')
25
25
  }
26
-
27
- if (args[0] === errorClass.typeName) {
28
- args.shift()
29
- }
30
-
31
- const error = new errorClass(...args)
32
- error.message = args[0]
26
+ const [type, message, stack, incidentId] = fields
27
+ // The base constructor takes (type, message, stack); subclasses take (message, stack).
28
+ // Keeping the base's wire type in place avoids shifting a plain Error's message into `type`.
29
+ const error = errorClass === ResilientError
30
+ ? new errorClass(type, message, stack)
31
+ : new errorClass(message, stack)
32
+ error.type = type
33
+ error.message = message
34
+ error.incidentId = incidentId
33
35
  error.finalizeUnmarshal()
34
36
  return error as T
35
37
  }
@@ -0,0 +1,138 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
2
+ import { cpSync, mkdtempSync, rmSync, symlinkSync } from 'node:fs'
3
+ import { tmpdir } from 'node:os'
4
+ import { join, resolve } from 'node:path'
5
+
6
+ type Lib = typeof import('../src/index.js')
7
+
8
+ /**
9
+ * Two module instances of this package in one process — what `bun --preserve-symlinks` produces
10
+ * when a linked workspace reaches `@owlmeans/error` through two `node_modules` paths.
11
+ *
12
+ * The sources are copied to two directories, so every module of the package (not only the entry)
13
+ * is loaded twice. `@owlmeans/i18n` resolves through a `node_modules` link to the workspace root.
14
+ */
15
+ const loadTwice = async (): Promise<{ root: string, a: Lib, b: Lib }> => {
16
+ const root = mkdtempSync(join(tmpdir(), 'owlmeans-error-copies-'))
17
+ const workspaceModules = resolve(import.meta.dir, '../../../node_modules')
18
+ for (const name of ['a', 'b']) {
19
+ cpSync(resolve(import.meta.dir, '../src'), join(root, name, 'src'), { recursive: true })
20
+ symlinkSync(workspaceModules, join(root, name, 'node_modules'))
21
+ }
22
+ const a = await import(join(root, 'a', 'src', 'index.ts')) as Lib
23
+ const b = await import(join(root, 'b', 'src', 'index.ts')) as Lib
24
+
25
+ return { root, a, b }
26
+ }
27
+
28
+ /** The same family declared by each copy, the way two copies of one package declare it. */
29
+ const familyOf = (lib: Lib) => {
30
+ class CopyApiError extends lib.ResilientError {
31
+ public static override typeName = 'CopyTestApiError'
32
+
33
+ constructor(message: string = 'error') {
34
+ super(CopyApiError.typeName, `copy-api:${message}`)
35
+ }
36
+ }
37
+
38
+ class CopyAuthFailed extends CopyApiError {
39
+ public static override typeName = 'CopyTestAuthFailedError'
40
+
41
+ constructor(message: string = 'error') {
42
+ super(`auth:${message}`)
43
+ this.type = CopyAuthFailed.typeName
44
+ }
45
+ }
46
+
47
+ class CopyAccess extends CopyApiError {
48
+ public static override typeName = 'CopyTestAccessError'
49
+
50
+ constructor(message: string = 'error') {
51
+ super(`access:${message}`)
52
+ this.type = CopyAccess.typeName
53
+ }
54
+ }
55
+
56
+ lib.ResilientError.registerErrorClass(CopyApiError)
57
+ lib.ResilientError.registerErrorClass(CopyAuthFailed)
58
+ lib.ResilientError.registerErrorClass(CopyAccess)
59
+
60
+ return { CopyApiError, CopyAuthFailed, CopyAccess }
61
+ }
62
+
63
+ describe('duplicate module copies behave as one', () => {
64
+ let root = ''
65
+ let a: Lib
66
+ let b: Lib
67
+ let A: ReturnType<typeof familyOf>
68
+ let B: ReturnType<typeof familyOf>
69
+
70
+ beforeAll(async () => {
71
+ ({ root, a, b } = await loadTwice())
72
+ A = familyOf(a)
73
+ B = familyOf(b)
74
+ })
75
+
76
+ afterAll(() => {
77
+ if (root !== '') {
78
+ rmSync(root, { recursive: true, force: true })
79
+ }
80
+ })
81
+
82
+ test('the two copies are distinct module instances sharing one registry', () => {
83
+ expect(a.ResilientError).not.toBe(b.ResilientError)
84
+ expect(a.ResilientError.converters).toBe(b.ResilientError.converters)
85
+ const catchAll = Symbol.for('@owlmeans/error:catch-all')
86
+ expect(b.ResilientError.converters.filter(converter => (converter as any)[catchAll] === true)).toHaveLength(1)
87
+ })
88
+
89
+ test('an error thrown by copy A is ensured as-is and marshalled with its type by copy B', () => {
90
+ const thrown = new A.CopyAuthFailed('no-guard')
91
+
92
+ expect(b.ResilientError.ensure(thrown)).toBe(thrown)
93
+ expect(b.isResilientError(thrown)).toBe(true)
94
+ const wire = b.ResilientError.marshal(b.ResilientError.ensure(thrown)).message
95
+ expect(wire.split(b.SEPARATOR).slice(0, 2)).toEqual(['CopyTestAuthFailedError', 'copy-api:auth:no-guard'])
96
+
97
+ const rebuilt = b.ResilientError.ensure(wire)
98
+ // The last registration of the type name wins — copy B registered after copy A.
99
+ expect(rebuilt.constructor).toBe(B.CopyAuthFailed)
100
+ expect(rebuilt).toBeInstanceOf(A.CopyAuthFailed)
101
+ expect(rebuilt.type).toBe('CopyTestAuthFailedError')
102
+ expect(rebuilt.message).toBe('copy-api:auth:no-guard')
103
+ })
104
+
105
+ test('a class registered by one copy only is rebuilt by the other', () => {
106
+ class OnlyInA extends a.ResilientError {
107
+ public static override typeName = 'CopyTestOnlyInA'
108
+
109
+ constructor(message: string = 'error') {
110
+ super(OnlyInA.typeName, `only-a:${message}`)
111
+ }
112
+ }
113
+ a.ResilientError.registerErrorClass(OnlyInA)
114
+
115
+ const rebuilt = b.ResilientError.ensure(new OnlyInA('x').marshal().message)
116
+ expect(rebuilt).toBeInstanceOf(OnlyInA)
117
+ expect(rebuilt.message).toBe('only-a:x')
118
+ })
119
+
120
+ test('instanceof across copies matches the class and its ancestors, never a sibling or a parent', () => {
121
+ const thrown = new A.CopyAuthFailed('x')
122
+ expect(thrown instanceof B.CopyAuthFailed).toBe(true)
123
+ expect(thrown instanceof B.CopyApiError).toBe(true)
124
+ expect(thrown instanceof b.ResilientError).toBe(true)
125
+ expect(thrown instanceof B.CopyAccess).toBe(false)
126
+ expect(new A.CopyApiError('x') instanceof B.CopyAuthFailed).toBe(false)
127
+
128
+ // A subclass without a type name of its own is matched natively only.
129
+ class Unnamed extends B.CopyAuthFailed { }
130
+ expect(new A.CopyAuthFailed('x') instanceof Unnamed).toBe(false)
131
+ expect(new Unnamed('x') instanceof A.CopyAuthFailed).toBe(true)
132
+
133
+ // Shape alone is not a resilient error: the brand is.
134
+ const lookalike = Object.assign(new Error('x'), { type: 'CopyTestAuthFailedError', marshal: () => new Error('x') })
135
+ expect(lookalike instanceof b.ResilientError).toBe(false)
136
+ expect(b.ResilientError.ensure(lookalike)).not.toBe(lookalike)
137
+ })
138
+ })
@@ -0,0 +1,17 @@
1
+ {
2
+ "extends": [
3
+ "@owlmeans/dep-config/tsconfig.base.json",
4
+ "@owlmeans/dep-config/tsconfig.node.json"
5
+ ],
6
+ "compilerOptions": {
7
+ "types": [
8
+ "bun"
9
+ ],
10
+ "rootDir": "../",
11
+ "noEmit": true
12
+ },
13
+ "include": [
14
+ "./**/*",
15
+ "../src/**/*"
16
+ ]
17
+ }
package/tsconfig.json CHANGED
@@ -6,5 +6,5 @@
6
6
  "rootDir": "./src/",
7
7
  "outDir": "./build/"
8
8
  },
9
- "exclude": ["./dist/**/*", "./build/**/*", "./*.ts"]
9
+ "exclude": ["./dist/**/*", "./build/**/*", "./tests/**/*", "./*.ts"]
10
10
  }