@owlmeans/error 0.1.18-rc.3 → 0.1.18-rc.30
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 +228 -39
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/error/SKILL.md +129 -20
- package/build/consts.d.ts +13 -0
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +13 -0
- package/build/consts.js.map +1 -1
- package/build/i18n/fr.json +4 -0
- package/build/i18n.js +2 -0
- package/build/i18n.js.map +1 -1
- package/build/resilient.d.ts +21 -3
- package/build/resilient.d.ts.map +1 -1
- package/build/resilient.js +96 -13
- package/build/resilient.js.map +1 -1
- package/build/types.d.ts +6 -0
- package/build/types.d.ts.map +1 -1
- package/build/utils.d.ts.map +1 -1
- package/build/utils.js +11 -7
- package/build/utils.js.map +1 -1
- package/package.json +3 -2
- package/src/consts.ts +16 -0
- package/src/i18n/fr.json +4 -0
- package/src/i18n.ts +2 -0
- package/src/resilient.ts +111 -14
- package/src/types.ts +7 -0
- package/src/utils.ts +11 -9
- package/tests/copies.spec.ts +138 -0
- package/tests/tsconfig.json +17 -0
- package/tsconfig.json +1 -1
- package/build/.gitkeep +0 -0
package/build/resilient.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
38
|
-
|
|
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(
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
package/build/resilient.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resilient.js","sourceRoot":"","sources":["../src/resilient.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;
|
|
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;
|
package/build/types.d.ts.map
CHANGED
|
@@ -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"}
|
package/build/utils.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
|
15
|
-
if (
|
|
14
|
+
const fields = err.message.split(ResilientError.separator, 4);
|
|
15
|
+
if (fields.length < 2) {
|
|
16
16
|
throw SyntaxError('Invalid marshaled error');
|
|
17
17
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const error =
|
|
22
|
-
|
|
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
|
};
|
package/build/utils.js.map
CHANGED
|
@@ -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,
|
|
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.
|
|
3
|
+
"version": "0.1.18-rc.30",
|
|
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.
|
|
25
|
+
"@owlmeans/i18n": "^0.1.18-rc.31"
|
|
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/i18n/fr.json
ADDED
package/src/i18n.ts
CHANGED
|
@@ -7,6 +7,7 @@ import be from './i18n/be.json' with { type: 'json' }
|
|
|
7
7
|
import uk from './i18n/uk.json' with { type: 'json' }
|
|
8
8
|
import es from './i18n/es.json' with { type: 'json' }
|
|
9
9
|
import de from './i18n/de.json' with { type: 'json' }
|
|
10
|
+
import fr from './i18n/fr.json' with { type: 'json' }
|
|
10
11
|
|
|
11
12
|
addI18nLib('en', 'errors', en)
|
|
12
13
|
addI18nLib('pl', 'errors', pl)
|
|
@@ -15,3 +16,4 @@ addI18nLib('be', 'errors', be)
|
|
|
15
16
|
addI18nLib('uk', 'errors', uk)
|
|
16
17
|
addI18nLib('es', 'errors', es)
|
|
17
18
|
addI18nLib('de', 'errors', de)
|
|
19
|
+
addI18nLib('fr', 'errors', fr)
|
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
|
|
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
|
-
|
|
51
|
-
|
|
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(
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
23
|
-
if (
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
error.
|
|
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
|
+
}
|