@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.
Files changed (53) hide show
  1. package/README.md +228 -39
  2. package/agent-meta/manifest.json +3 -3
  3. package/agent-meta/skills/error/SKILL.md +47 -5
  4. package/build/consts.d.ts +13 -0
  5. package/build/consts.d.ts.map +1 -1
  6. package/build/consts.js +13 -0
  7. package/build/consts.js.map +1 -1
  8. package/build/errors.d.ts +33 -0
  9. package/build/errors.d.ts.map +1 -0
  10. package/build/errors.js +146 -0
  11. package/build/errors.js.map +1 -0
  12. package/build/helper/types.d.ts +9 -0
  13. package/build/helper/types.d.ts.map +1 -0
  14. package/build/helper/types.js +2 -0
  15. package/build/helper/types.js.map +1 -0
  16. package/build/helper.d.ts +3 -3
  17. package/build/helper.d.ts.map +1 -1
  18. package/build/helper.js +7 -3
  19. package/build/helper.js.map +1 -1
  20. package/build/i18n/fr.json +4 -0
  21. package/build/i18n.js +10 -8
  22. package/build/i18n.js.map +1 -1
  23. package/build/index.d.ts +2 -1
  24. package/build/index.d.ts.map +1 -1
  25. package/build/index.js +1 -1
  26. package/build/index.js.map +1 -1
  27. package/build/resilient.d.ts.map +1 -1
  28. package/build/resilient.js.map +1 -1
  29. package/build/types.d.ts +7 -1
  30. package/build/types.d.ts.map +1 -1
  31. package/build/types.local.d.ts +7 -0
  32. package/build/types.local.d.ts.map +1 -0
  33. package/build/types.local.js +2 -0
  34. package/build/types.local.js.map +1 -0
  35. package/build/utils.d.ts +0 -2
  36. package/build/utils.d.ts.map +1 -1
  37. package/build/utils.js +21 -17
  38. package/build/utils.js.map +1 -1
  39. package/package.json +4 -3
  40. package/src/consts.ts +16 -0
  41. package/src/errors.ts +177 -0
  42. package/src/helper/types.ts +9 -0
  43. package/src/helper.ts +12 -5
  44. package/src/i18n/fr.json +4 -0
  45. package/src/i18n.ts +10 -8
  46. package/src/index.ts +2 -1
  47. package/src/types.local.ts +7 -0
  48. package/src/types.ts +8 -1
  49. package/src/utils.ts +23 -21
  50. package/tests/copies.spec.ts +138 -0
  51. package/tests/tsconfig.json +17 -0
  52. package/tsconfig.json +1 -1
  53. package/src/resilient.ts +0 -81
package/src/utils.ts CHANGED
@@ -1,6 +1,28 @@
1
- import { ResilientError } from './resilient.js'
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
@@ -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
  }
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
- })