@owlmeans/error 0.1.18-rc.8 → 0.1.18
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 +3 -3
- package/agent-meta/skills/error/SKILL.md +47 -5
- 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/errors.d.ts +33 -0
- package/build/errors.d.ts.map +1 -0
- package/build/errors.js +146 -0
- package/build/errors.js.map +1 -0
- package/build/helper/types.d.ts +9 -0
- package/build/helper/types.d.ts.map +1 -0
- package/build/helper/types.js +2 -0
- package/build/helper/types.js.map +1 -0
- package/build/helper.d.ts +3 -3
- package/build/helper.d.ts.map +1 -1
- package/build/helper.js +7 -3
- package/build/helper.js.map +1 -1
- package/build/i18n/fr.json +4 -0
- package/build/i18n.js +10 -8
- package/build/i18n.js.map +1 -1
- package/build/index.d.ts +2 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -1
- package/build/index.js.map +1 -1
- package/build/resilient.d.ts.map +1 -1
- package/build/resilient.js.map +1 -1
- package/build/types.d.ts +7 -1
- package/build/types.d.ts.map +1 -1
- package/build/types.local.d.ts +7 -0
- package/build/types.local.d.ts.map +1 -0
- package/build/types.local.js +2 -0
- package/build/types.local.js.map +1 -0
- package/build/utils.d.ts +0 -2
- package/build/utils.d.ts.map +1 -1
- package/build/utils.js +21 -17
- package/build/utils.js.map +1 -1
- package/package.json +4 -3
- package/src/consts.ts +16 -0
- package/src/errors.ts +177 -0
- package/src/helper/types.ts +9 -0
- package/src/helper.ts +12 -5
- package/src/i18n/fr.json +4 -0
- package/src/i18n.ts +10 -8
- package/src/index.ts +2 -1
- package/src/types.local.ts +7 -0
- package/src/types.ts +8 -1
- package/src/utils.ts +23 -21
- package/tests/copies.spec.ts +138 -0
- package/tests/tsconfig.json +17 -0
- package/tsconfig.json +1 -1
- package/src/resilient.ts +0 -81
package/src/utils.ts
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
|
-
import { ResilientError } from './
|
|
1
|
+
import { ResilientError } from './errors.js'
|
|
2
2
|
import type { Converter, ResilientErrorConstructor } from './types.js'
|
|
3
3
|
|
|
4
|
+
const unmarshal = <T extends ResilientError = ResilientError>(errorClass: ResilientErrorConstructor) =>
|
|
5
|
+
(err: Error): T => {
|
|
6
|
+
if (err instanceof errorClass) {
|
|
7
|
+
return err as T
|
|
8
|
+
}
|
|
9
|
+
const fields = err.message.split(ResilientError.separator, 4)
|
|
10
|
+
if (fields.length < 2) {
|
|
11
|
+
throw SyntaxError('Invalid marshaled error')
|
|
12
|
+
}
|
|
13
|
+
const [type, message, stack, incidentId] = fields
|
|
14
|
+
// The base constructor takes (type, message, stack); subclasses take (message, stack).
|
|
15
|
+
// Keeping the base's wire type in place avoids shifting a plain Error's message into `type`.
|
|
16
|
+
const error = errorClass === ResilientError
|
|
17
|
+
? new errorClass(type, message, stack)
|
|
18
|
+
: new errorClass(message, stack)
|
|
19
|
+
error.type = type
|
|
20
|
+
error.message = message
|
|
21
|
+
error.incidentId = incidentId
|
|
22
|
+
error.finalizeUnmarshal()
|
|
23
|
+
return error as T
|
|
24
|
+
}
|
|
25
|
+
|
|
4
26
|
export const createErrorConverter = (
|
|
5
27
|
resilientErrorClass: ResilientErrorConstructor,
|
|
6
28
|
errorClass?: ErrorConstructor
|
|
@@ -13,23 +35,3 @@ export const createErrorConverter = (
|
|
|
13
35
|
unmarshal: unmarshal(resilientErrorClass)
|
|
14
36
|
}
|
|
15
37
|
}
|
|
16
|
-
|
|
17
|
-
export const unmarshal = <T extends ResilientError = ResilientError>(errorClass: ResilientErrorConstructor) =>
|
|
18
|
-
(err: Error): T => {
|
|
19
|
-
if (err instanceof errorClass) {
|
|
20
|
-
return err as T
|
|
21
|
-
}
|
|
22
|
-
const args = err.message.split(ResilientError.separator, 3) as [string, string, string?]
|
|
23
|
-
if (args.length < 2) {
|
|
24
|
-
throw SyntaxError('Invalid marshaled error')
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (args[0] === errorClass.typeName) {
|
|
28
|
-
args.shift()
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const error = new errorClass(...args)
|
|
32
|
-
error.message = args[0]
|
|
33
|
-
error.finalizeUnmarshal()
|
|
34
|
-
return error as T
|
|
35
|
-
}
|
|
@@ -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
package/src/resilient.ts
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { RESILENT_ERROR, SEPARATOR } from './consts.js'
|
|
2
|
-
import type { Converter, ResilientErrorConstructor } from './types.js'
|
|
3
|
-
import { createErrorConverter } from './utils.js'
|
|
4
|
-
|
|
5
|
-
export class ResilientError extends Error {
|
|
6
|
-
public static separator: string = SEPARATOR
|
|
7
|
-
|
|
8
|
-
public static typeName: string = RESILENT_ERROR
|
|
9
|
-
|
|
10
|
-
public static converters: Converter[] = []
|
|
11
|
-
|
|
12
|
-
public static registerErrorClass(resilientErrorClass: ResilientErrorConstructor, errorClass?: ErrorConstructor): Converter {
|
|
13
|
-
const converter = createErrorConverter(resilientErrorClass, errorClass)
|
|
14
|
-
this.converters.push(converter)
|
|
15
|
-
|
|
16
|
-
return converter
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
public static ensure(err: Error | string, throwOnUnknown?: boolean): ResilientError {
|
|
20
|
-
err = typeof err === 'string' ? new Error(err) : err
|
|
21
|
-
if (err instanceof ResilientError) {
|
|
22
|
-
return err
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// We don't proceed SyntaxError - system should crash in this case
|
|
26
|
-
if (err instanceof SyntaxError) {
|
|
27
|
-
throw err
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Umarshal marhalled error that is wrapepd to ordinary error
|
|
31
|
-
const unmarhaller = this.converters.toReversed().find(converter => converter.isMarshaled(err))
|
|
32
|
-
if (unmarhaller != null) {
|
|
33
|
-
return unmarhaller.unmarshal(err)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Convert object of Error subtypes to ResilientError subtype
|
|
37
|
-
const converter = this.converters.find(converter => converter.match(err))
|
|
38
|
-
if (converter != null) {
|
|
39
|
-
return converter.convert(err)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (throwOnUnknown === true) {
|
|
43
|
-
throw err
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return new ResilientError(this.typeName, err.message, err.stack)
|
|
47
|
-
}
|
|
48
|
-
|
|
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))
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return new Error([this.typeName, err.message, err.stack].join(this.separator))
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
public type: string = RESILENT_ERROR
|
|
58
|
-
|
|
59
|
-
public oiriginalStack?: string
|
|
60
|
-
|
|
61
|
-
constructor(type: string, message: string, stack?: string) {
|
|
62
|
-
super(message)
|
|
63
|
-
this.type = type
|
|
64
|
-
if (stack != null) {
|
|
65
|
-
this.oiriginalStack = stack
|
|
66
|
-
} else {
|
|
67
|
-
this.oiriginalStack = this.stack
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
marshal(): Error {
|
|
72
|
-
return ResilientError.marshal(this)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
finalizeUnmarshal(): void { }
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
ResilientError.converters.push({
|
|
79
|
-
...createErrorConverter(ResilientError as ResilientErrorConstructor),
|
|
80
|
-
match: () => true
|
|
81
|
-
})
|