@codenhub/error 0.0.1 → 0.1.0
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/LICENSE +201 -201
- package/README.md +145 -52
- package/dist/bucket.js +167 -0
- package/dist/create-app-error.d.ts +33 -0
- package/dist/create-app-error.js +104 -0
- package/dist/index.d.ts +5 -53
- package/dist/index.js +4 -2
- package/dist/normalize.js +125 -0
- package/dist/registries/browser.d.ts +21 -5
- package/dist/registries/browser.js +70 -16
- package/dist/registries/index.d.ts +3 -3
- package/dist/registries/index.js +3 -3
- package/dist/registries/supabase.d.ts +22 -5
- package/dist/registries/supabase.js +118 -19
- package/dist/registry.d.ts +43 -0
- package/dist/registry.js +116 -0
- package/dist/result.d.ts +100 -0
- package/dist/result.js +90 -0
- package/dist/types.d.ts +286 -0
- package/package.json +8 -5
- package/dist/src-DrlQ39IZ.js +0 -322
- package/dist/types-D6n8W1xw.d.ts +0 -87
package/README.md
CHANGED
|
@@ -10,12 +10,12 @@ pnpm add @codenhub/error
|
|
|
10
10
|
|
|
11
11
|
## Usage
|
|
12
12
|
|
|
13
|
-
Configure the app-level
|
|
13
|
+
Configure the app-level global registry retrieved by `getErrorRegistry()` during initialization, then normalize thrown or returned unknown values into a predictable error shape using `createAppError()`.
|
|
14
14
|
|
|
15
15
|
```ts
|
|
16
|
-
import {
|
|
16
|
+
import { createAppError, getErrorRegistry, err, ok, type Result } from "@codenhub/error";
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
getErrorRegistry().codes.add("invalid_credentials", {
|
|
19
19
|
message: "Invalid email or password.",
|
|
20
20
|
messageKey: "error.auth.invalidCredentials",
|
|
21
21
|
source: "auth",
|
|
@@ -29,7 +29,7 @@ const signIn = async (): Promise<Result<string>> => {
|
|
|
29
29
|
}
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
-
const appError =
|
|
32
|
+
const appError = createAppError({ code: "invalid_credentials" });
|
|
33
33
|
|
|
34
34
|
console.log(appError.message, appError.messageKey);
|
|
35
35
|
```
|
|
@@ -37,12 +37,12 @@ console.log(appError.message, appError.messageKey);
|
|
|
37
37
|
Ready registries are opt-in. Merge them into an app-owned registry when the app wants those classifications.
|
|
38
38
|
|
|
39
39
|
```ts
|
|
40
|
-
import {
|
|
40
|
+
import { getErrorRegistry } from "@codenhub/error";
|
|
41
41
|
import { browserErrorRegistry } from "@codenhub/error/registries/browser";
|
|
42
42
|
import { supabaseErrorRegistry } from "@codenhub/error/registries/supabase";
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
getErrorRegistry().merge(browserErrorRegistry);
|
|
45
|
+
getErrorRegistry().merge(supabaseErrorRegistry);
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
## Reference
|
|
@@ -53,11 +53,16 @@ Primary entrypoint for app-owned registries, error normalization, and result hel
|
|
|
53
53
|
|
|
54
54
|
```ts
|
|
55
55
|
import {
|
|
56
|
-
|
|
56
|
+
createAppError,
|
|
57
|
+
isAppError,
|
|
57
58
|
DEFAULT_APP_ERROR_MESSAGE,
|
|
58
59
|
createErrorRegistry,
|
|
60
|
+
getErrorRegistry,
|
|
61
|
+
setErrorRegistry,
|
|
62
|
+
freezeRegistry,
|
|
59
63
|
err,
|
|
60
64
|
ok,
|
|
65
|
+
type AppError,
|
|
61
66
|
type AppErrorOptions,
|
|
62
67
|
type AppErrorSource,
|
|
63
68
|
type AppErrorType,
|
|
@@ -70,6 +75,7 @@ import {
|
|
|
70
75
|
type ErrorRegistry,
|
|
71
76
|
type ErrorRegistryBucket,
|
|
72
77
|
type Ok,
|
|
78
|
+
type ReadonlyErrorRegistry,
|
|
73
79
|
type Result,
|
|
74
80
|
} from "@codenhub/error";
|
|
75
81
|
```
|
|
@@ -83,15 +89,72 @@ Supported import paths:
|
|
|
83
89
|
| `@codenhub/error/registries/browser` | Browser and Web API error mappings. |
|
|
84
90
|
| `@codenhub/error/registries/supabase` | Supabase error mappings. |
|
|
85
91
|
|
|
92
|
+
#### `createAppError()`
|
|
93
|
+
|
|
94
|
+
Normalizes an unknown value into a predictable `AppError` object. By default, it classifies errors with the active global error registry.
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
function createAppError(error: unknown, options?: AppErrorOptions): AppError;
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
If no registry mapping matches, it uses `DEFAULT_APP_ERROR_MESSAGE` or `fallbackMessage` and classifies the value as `"unknown"`.
|
|
101
|
+
|
|
102
|
+
#### `isAppError()`
|
|
103
|
+
|
|
104
|
+
Type guard to check if a value is a normalized `AppError`.
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
function isAppError(value: unknown): value is AppError;
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
#### `AppError` Interface
|
|
111
|
+
|
|
112
|
+
Predictable normalized error shape extending the native `Error` class.
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
interface AppError extends Error {
|
|
116
|
+
readonly type: "known" | "unexpected" | "unknown";
|
|
117
|
+
readonly message: string;
|
|
118
|
+
readonly messageKey: string | null;
|
|
119
|
+
readonly source: string | null;
|
|
120
|
+
readonly originalError: unknown;
|
|
121
|
+
readonly isRetryable: boolean;
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
#### `getErrorRegistry()`
|
|
126
|
+
|
|
127
|
+
Retrieves the active global error registry.
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
function getErrorRegistry(): ErrorRegistry;
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
#### `setErrorRegistry()`
|
|
134
|
+
|
|
135
|
+
Sets the active global error registry.
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
function setErrorRegistry(registry: ErrorRegistry): void;
|
|
139
|
+
```
|
|
140
|
+
|
|
86
141
|
#### `createErrorRegistry()`
|
|
87
142
|
|
|
88
|
-
Creates an empty isolated registry.
|
|
143
|
+
Creates an empty isolated registry.
|
|
89
144
|
|
|
90
145
|
```ts
|
|
91
|
-
function createErrorRegistry(): ErrorRegistry;
|
|
146
|
+
function createErrorRegistry(presets?: readonly (ErrorRegistry | ReadonlyErrorRegistry)[]): ErrorRegistry;
|
|
92
147
|
```
|
|
93
148
|
|
|
94
|
-
Use isolated registries for tests, request scopes, tenant-specific mappings, or integrations where mappings should not use the
|
|
149
|
+
Use isolated registries for tests, request scopes, tenant-specific mappings, or integrations where mappings should not use the active global registry.
|
|
150
|
+
|
|
151
|
+
#### `freezeRegistry()`
|
|
152
|
+
|
|
153
|
+
Wraps an ErrorRegistry in a read-only Proxy to prevent future mutations.
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
function freezeRegistry(registry: ErrorRegistry): ReadonlyErrorRegistry;
|
|
157
|
+
```
|
|
95
158
|
|
|
96
159
|
#### `DEFAULT_APP_ERROR_MESSAGE`
|
|
97
160
|
|
|
@@ -103,7 +166,7 @@ const DEFAULT_APP_ERROR_MESSAGE = "An unexpected error occurred.";
|
|
|
103
166
|
|
|
104
167
|
#### `ErrorRegistry`
|
|
105
168
|
|
|
106
|
-
Stores deterministic and heuristic mappings used by `
|
|
169
|
+
Stores deterministic and heuristic mappings used by `createAppError`.
|
|
107
170
|
|
|
108
171
|
```ts
|
|
109
172
|
interface ErrorRegistry {
|
|
@@ -113,13 +176,27 @@ interface ErrorRegistry {
|
|
|
113
176
|
prefixes: ErrorPrefixRegistryBucket;
|
|
114
177
|
patterns: ErrorPatternRegistryBucket;
|
|
115
178
|
clear(): void;
|
|
116
|
-
merge(registry: ErrorRegistry): void;
|
|
179
|
+
merge(registry: ErrorRegistry | ReadonlyErrorRegistry): void;
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
#### `ReadonlyErrorRegistry`
|
|
184
|
+
|
|
185
|
+
Read-only view of a registry returned by `freezeRegistry`. Exposes only the read-facing surface of each bucket. Pass it directly to `createErrorRegistry` presets or `merge`.
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
interface ReadonlyErrorRegistry {
|
|
189
|
+
readonly codes: Pick<ErrorRegistryBucket, "get" | "values">;
|
|
190
|
+
readonly names: Pick<ErrorRegistryBucket, "get" | "values">;
|
|
191
|
+
readonly messages: Pick<ErrorRegistryBucket, "get" | "values">;
|
|
192
|
+
readonly prefixes: Pick<ErrorPrefixRegistryBucket, "values">;
|
|
193
|
+
readonly patterns: Pick<ErrorPatternRegistryBucket, "values">;
|
|
117
194
|
}
|
|
118
195
|
```
|
|
119
196
|
|
|
120
197
|
Code, name, exact message, and prefix matches are deterministic known errors. When multiple prefixes match the same message, the longest normalized prefix wins. Pattern matches are heuristic and should be treated as unexpected errors with better user-facing feedback.
|
|
121
198
|
|
|
122
|
-
|
|
199
|
+
The global error registry is mutable and starts empty. `createErrorRegistry()` creates another empty registry with the same bucket API.
|
|
123
200
|
|
|
124
201
|
Registry buckets support adding one mapping at a time or a tuple list:
|
|
125
202
|
|
|
@@ -128,6 +205,7 @@ interface ErrorRegistryBucket {
|
|
|
128
205
|
add(identifier: string, feedback: ErrorFeedback): void;
|
|
129
206
|
addList(entries: readonly (readonly [identifier: string, feedback: ErrorFeedback])[]): void;
|
|
130
207
|
clear(): void;
|
|
208
|
+
delete(identifier: string): boolean;
|
|
131
209
|
get(identifier: string): ErrorFeedback | undefined;
|
|
132
210
|
values(): IterableIterator<[string, ErrorFeedback]>;
|
|
133
211
|
}
|
|
@@ -136,6 +214,7 @@ interface ErrorPrefixRegistryBucket {
|
|
|
136
214
|
add(prefix: string, feedback: ErrorFeedback): void;
|
|
137
215
|
addList(entries: readonly (readonly [prefix: string, feedback: ErrorFeedback])[]): void;
|
|
138
216
|
clear(): void;
|
|
217
|
+
delete(prefix: string): boolean;
|
|
139
218
|
values(): readonly ErrorPrefixDefinition[];
|
|
140
219
|
}
|
|
141
220
|
|
|
@@ -143,13 +222,14 @@ interface ErrorPatternRegistryBucket {
|
|
|
143
222
|
add(pattern: RegExp, feedback: ErrorFeedback): void;
|
|
144
223
|
addList(entries: readonly (readonly [pattern: RegExp, feedback: ErrorFeedback])[]): void;
|
|
145
224
|
clear(): void;
|
|
225
|
+
delete(pattern: RegExp): boolean;
|
|
146
226
|
values(): readonly ErrorPatternDefinition[];
|
|
147
227
|
}
|
|
148
228
|
```
|
|
149
229
|
|
|
150
230
|
`values()` returns defensive copies. Pattern buckets clone `RegExp` values so global or sticky regex state does not leak across classifications.
|
|
151
231
|
|
|
152
|
-
`add()` and `addList()` validate entries immediately and throw `TypeError` for invalid input. Exact and prefix identifiers must be non-empty strings after trimming whitespace and trailing sentence punctuation. Exact identifiers are stored and looked up in that normalized form. Feedback must be an object with a non-empty `message`; optional `messageKey` and `source` values must be strings, and optional `
|
|
232
|
+
`add()` and `addList()` validate entries immediately and throw `TypeError` for invalid input. Exact and prefix identifiers must be non-empty strings after trimming whitespace and trailing sentence punctuation. Exact identifiers are stored and looked up in that normalized form. Feedback must be an object with a non-empty `message`; optional `messageKey` and `source` values must be strings, and optional `isRetryable` must be a boolean. Pattern buckets only accept `RegExp` patterns.
|
|
153
233
|
|
|
154
234
|
#### `ErrorFeedback`
|
|
155
235
|
|
|
@@ -160,11 +240,11 @@ interface ErrorFeedback {
|
|
|
160
240
|
message: string;
|
|
161
241
|
messageKey?: string;
|
|
162
242
|
source?: string;
|
|
163
|
-
|
|
243
|
+
isRetryable?: boolean;
|
|
164
244
|
}
|
|
165
245
|
```
|
|
166
246
|
|
|
167
|
-
`message` should be safe to show to users. `messageKey`, `source`, and `
|
|
247
|
+
`message` should be safe to show to users. `messageKey`, `source`, and `isRetryable` are optional metadata copied onto matched `AppError` instances.
|
|
168
248
|
|
|
169
249
|
#### `ErrorPrefixDefinition` and `ErrorPatternDefinition`
|
|
170
250
|
|
|
@@ -182,29 +262,6 @@ interface ErrorPatternDefinition extends ErrorFeedback {
|
|
|
182
262
|
|
|
183
263
|
Most consumers only need these when inspecting, copying, or testing registry contents.
|
|
184
264
|
|
|
185
|
-
#### `AppError`
|
|
186
|
-
|
|
187
|
-
Normalizes an unknown value into a predictable error object. By default, it classifies errors with `AppError.registry`.
|
|
188
|
-
|
|
189
|
-
```ts
|
|
190
|
-
class AppError extends Error {
|
|
191
|
-
static readonly registry: ErrorRegistry;
|
|
192
|
-
|
|
193
|
-
static createRegistry(): ErrorRegistry;
|
|
194
|
-
|
|
195
|
-
readonly type: "known" | "unexpected" | "unknown";
|
|
196
|
-
readonly message: string;
|
|
197
|
-
readonly messageKey: string | null;
|
|
198
|
-
readonly source: string | null;
|
|
199
|
-
readonly originalError: unknown;
|
|
200
|
-
readonly retryable: boolean;
|
|
201
|
-
|
|
202
|
-
constructor(error: unknown, options?: AppErrorOptions);
|
|
203
|
-
}
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
`AppError` does not throw during construction. If no registry mapping matches, it uses `DEFAULT_APP_ERROR_MESSAGE` or `fallbackMessage` and classifies the value as `"unknown"`.
|
|
207
|
-
|
|
208
265
|
#### `AppErrorType` and `AppErrorSource`
|
|
209
266
|
|
|
210
267
|
Reusable property types for consumers that store or pass around normalized error metadata.
|
|
@@ -220,13 +277,15 @@ type AppErrorSource = string | null;
|
|
|
220
277
|
interface AppErrorOptions {
|
|
221
278
|
fallbackMessage?: string;
|
|
222
279
|
registry?: ErrorRegistry;
|
|
280
|
+
maxDepth?: number;
|
|
223
281
|
}
|
|
224
282
|
```
|
|
225
283
|
|
|
226
|
-
| Option | Type | Default
|
|
227
|
-
| ----------------- | --------------- |
|
|
228
|
-
| `fallbackMessage` | `string` | `
|
|
229
|
-
| `registry` | `ErrorRegistry` |
|
|
284
|
+
| Option | Type | Default | Description |
|
|
285
|
+
| ----------------- | --------------- | ----------------- | ------------------------------------------------------------------------------------- |
|
|
286
|
+
| `fallbackMessage` | `string` | `DEFAULT_APP_...` | Message used when no mapping matches. |
|
|
287
|
+
| `registry` | `ErrorRegistry` | Global registry | Registry used to classify the provided error. |
|
|
288
|
+
| `maxDepth` | `number` | `3` | Maximum depth when unwrapping nested error wrappers (`cause`, `originalError`, etc.). |
|
|
230
289
|
|
|
231
290
|
#### `ok()`
|
|
232
291
|
|
|
@@ -258,6 +317,36 @@ Represents success or failure without exceptions.
|
|
|
258
317
|
type Result<T> = { ok: true; value: T } | { ok: false; error: AppError };
|
|
259
318
|
```
|
|
260
319
|
|
|
320
|
+
#### `unwrap()`
|
|
321
|
+
|
|
322
|
+
Unwraps a `Result`, returning the value if successful, or throwing the normalized `AppError` if failed.
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
function unwrap<T>(result: Result<T>): T;
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
#### `map()`
|
|
329
|
+
|
|
330
|
+
Maps the success value of a `Result` using the provided mapper function.
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
function map<T, U>(result: Result<T>, mapper: (value: T) => U): Result<U>;
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
#### `match()`
|
|
337
|
+
|
|
338
|
+
Pattern matches on a `Result`, executing the corresponding callback based on the outcome.
|
|
339
|
+
|
|
340
|
+
```ts
|
|
341
|
+
function match<T, U>(
|
|
342
|
+
result: Result<T>,
|
|
343
|
+
callbacks: {
|
|
344
|
+
readonly onOk: (value: T) => U;
|
|
345
|
+
readonly onErr: (error: AppError) => U;
|
|
346
|
+
},
|
|
347
|
+
): U;
|
|
348
|
+
```
|
|
349
|
+
|
|
261
350
|
### `@codenhub/error/registries`
|
|
262
351
|
|
|
263
352
|
Index entrypoint for ready registry presets.
|
|
@@ -266,9 +355,9 @@ Index entrypoint for ready registry presets.
|
|
|
266
355
|
import { browserErrorRegistry, supabaseErrorRegistry } from "@codenhub/error/registries";
|
|
267
356
|
```
|
|
268
357
|
|
|
269
|
-
Ready registries are plain `ErrorRegistry` values intended to be merged into
|
|
358
|
+
Ready registries are plain `ErrorRegistry` values intended to be merged into the global registry or another app-owned registry. Importing a preset does not mutate the global registry.
|
|
270
359
|
|
|
271
|
-
The preset registry objects are
|
|
360
|
+
The preset registry objects are frozen and read-only. Merge them into an app-owned registry before adding app-specific mappings.
|
|
272
361
|
|
|
273
362
|
### `@codenhub/error/registries/browser`
|
|
274
363
|
|
|
@@ -314,19 +403,19 @@ const result = err(new DOMException("Aborted", "AbortError"), { registry: errors
|
|
|
314
403
|
### Merge Presets
|
|
315
404
|
|
|
316
405
|
```ts
|
|
317
|
-
import {
|
|
406
|
+
import { getErrorRegistry } from "@codenhub/error";
|
|
318
407
|
import { browserErrorRegistry, supabaseErrorRegistry } from "@codenhub/error/registries";
|
|
319
408
|
|
|
320
|
-
|
|
321
|
-
|
|
409
|
+
getErrorRegistry().merge(browserErrorRegistry);
|
|
410
|
+
getErrorRegistry().merge(supabaseErrorRegistry);
|
|
322
411
|
```
|
|
323
412
|
|
|
324
413
|
### Add Multiple Mappings
|
|
325
414
|
|
|
326
415
|
```ts
|
|
327
|
-
import {
|
|
416
|
+
import { getErrorRegistry } from "@codenhub/error";
|
|
328
417
|
|
|
329
|
-
|
|
418
|
+
getErrorRegistry().codes.addList([
|
|
330
419
|
["invalid_credentials", { message: "Invalid email or password.", source: "auth" }],
|
|
331
420
|
["email_not_confirmed", { message: "Email address is not confirmed.", source: "auth" }],
|
|
332
421
|
]);
|
|
@@ -342,5 +431,9 @@ AppError.registry.codes.addList([
|
|
|
342
431
|
## Notes
|
|
343
432
|
|
|
344
433
|
- Registries are opt-in by design. Consumers start from a blank registry to avoid hidden global classifications.
|
|
345
|
-
-
|
|
434
|
+
- The global error registry is mutable. Ready registry presets are frozen and read-only. Configure registries during app initialization.
|
|
346
435
|
- Preset registries should prefer stable error codes or names over message matching when a library provides them.
|
|
436
|
+
|
|
437
|
+
## License
|
|
438
|
+
|
|
439
|
+
This project is licensed under the [Apache-2.0](LICENSE) license.
|
package/dist/bucket.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
//#region src/bucket.ts
|
|
2
|
+
const ERROR_IDENTIFIER_TRAILING_PUNCTUATION_PATTERN = /[.!?]+$/;
|
|
3
|
+
/**
|
|
4
|
+
* Normalizes an error identifier by trimming whitespace and stripping trailing punctuation (like `.`, `!`, `?`).
|
|
5
|
+
*
|
|
6
|
+
* @internal
|
|
7
|
+
* @param identifier - The raw error identifier string.
|
|
8
|
+
* @returns The normalized error identifier string.
|
|
9
|
+
*/
|
|
10
|
+
const normalizeErrorIdentifier = (identifier) => {
|
|
11
|
+
return identifier.trim().replace(ERROR_IDENTIFIER_TRAILING_PUNCTUATION_PATTERN, "");
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Asserts that an identifier normalizes to a non-empty string.
|
|
15
|
+
*
|
|
16
|
+
* @throws TypeError - If the identifier is empty after normalization.
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
const assertNonEmptyIdentifier = (identifier, label) => {
|
|
20
|
+
if (typeof identifier !== "string" || normalizeErrorIdentifier(identifier).length === 0) throw new TypeError(`Error registry ${label} must be a non-empty string.`);
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Validates that a feedback object has the required shape and field types.
|
|
24
|
+
*
|
|
25
|
+
* @throws TypeError - If feedback is missing or has invalid field types.
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
const assertFeedback = (feedback) => {
|
|
29
|
+
if (typeof feedback !== "object" || feedback === null) throw new TypeError("Error registry feedback must be an object.");
|
|
30
|
+
if (typeof feedback.message !== "string" || feedback.message.trim().length === 0) throw new TypeError("Error registry feedback.message must be a non-empty string.");
|
|
31
|
+
if (feedback.messageKey !== void 0 && typeof feedback.messageKey !== "string") throw new TypeError("Error registry feedback.messageKey must be a string when provided.");
|
|
32
|
+
if (feedback.source !== void 0 && typeof feedback.source !== "string") throw new TypeError("Error registry feedback.source must be a string when provided.");
|
|
33
|
+
if (feedback.isRetryable !== void 0 && typeof feedback.isRetryable !== "boolean") throw new TypeError("Error registry feedback.isRetryable must be a boolean when provided.");
|
|
34
|
+
};
|
|
35
|
+
/** @internal */
|
|
36
|
+
const cloneFeedback = (feedback) => ({ ...feedback });
|
|
37
|
+
/**
|
|
38
|
+
* Creates a feedback map bucket for exact identifier matching (codes, names, messages).
|
|
39
|
+
*
|
|
40
|
+
* @internal
|
|
41
|
+
*/
|
|
42
|
+
const createFeedbackMapBucket = () => {
|
|
43
|
+
const entries = /* @__PURE__ */ new Map();
|
|
44
|
+
const add = (identifier, feedback) => {
|
|
45
|
+
assertNonEmptyIdentifier(identifier, "identifier");
|
|
46
|
+
assertFeedback(feedback);
|
|
47
|
+
entries.set(normalizeErrorIdentifier(identifier), cloneFeedback(feedback));
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
add,
|
|
51
|
+
addList(errorEntries) {
|
|
52
|
+
for (const [identifier, feedback] of errorEntries) add(identifier, feedback);
|
|
53
|
+
},
|
|
54
|
+
clear() {
|
|
55
|
+
entries.clear();
|
|
56
|
+
},
|
|
57
|
+
delete(identifier) {
|
|
58
|
+
assertNonEmptyIdentifier(identifier, "identifier");
|
|
59
|
+
return entries.delete(normalizeErrorIdentifier(identifier));
|
|
60
|
+
},
|
|
61
|
+
get(identifier) {
|
|
62
|
+
const feedback = entries.get(normalizeErrorIdentifier(identifier));
|
|
63
|
+
return feedback === void 0 ? void 0 : cloneFeedback(feedback);
|
|
64
|
+
},
|
|
65
|
+
*values() {
|
|
66
|
+
for (const [identifier, feedback] of entries) yield [identifier, cloneFeedback(feedback)];
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Creates a prefix bucket for longest-prefix message matching.
|
|
72
|
+
*
|
|
73
|
+
* @internal
|
|
74
|
+
*/
|
|
75
|
+
const createPrefixBucket = () => {
|
|
76
|
+
const entries = /* @__PURE__ */ new Map();
|
|
77
|
+
let sortedCache = null;
|
|
78
|
+
const rebuildCache = () => {
|
|
79
|
+
sortedCache = Array.from(entries.entries(), ([prefix, feedback]) => ({
|
|
80
|
+
...cloneFeedback(feedback),
|
|
81
|
+
prefix
|
|
82
|
+
})).sort((a, b) => b.prefix.length - a.prefix.length);
|
|
83
|
+
};
|
|
84
|
+
const getSortedCache = () => {
|
|
85
|
+
if (sortedCache === null) rebuildCache();
|
|
86
|
+
return sortedCache;
|
|
87
|
+
};
|
|
88
|
+
const add = (prefix, feedback) => {
|
|
89
|
+
assertNonEmptyIdentifier(prefix, "prefix");
|
|
90
|
+
assertFeedback(feedback);
|
|
91
|
+
entries.set(normalizeErrorIdentifier(prefix), cloneFeedback(feedback));
|
|
92
|
+
sortedCache = null;
|
|
93
|
+
};
|
|
94
|
+
return {
|
|
95
|
+
add,
|
|
96
|
+
addList(errorEntries) {
|
|
97
|
+
for (const [prefix, feedback] of errorEntries) add(prefix, feedback);
|
|
98
|
+
},
|
|
99
|
+
clear() {
|
|
100
|
+
entries.clear();
|
|
101
|
+
sortedCache = [];
|
|
102
|
+
},
|
|
103
|
+
delete(prefix) {
|
|
104
|
+
assertNonEmptyIdentifier(prefix, "prefix");
|
|
105
|
+
const deleted = entries.delete(normalizeErrorIdentifier(prefix));
|
|
106
|
+
if (deleted) sortedCache = null;
|
|
107
|
+
return deleted;
|
|
108
|
+
},
|
|
109
|
+
values() {
|
|
110
|
+
return getSortedCache();
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* Creates a pattern bucket for heuristic regex-based error matching.
|
|
116
|
+
* Strips `g`/`y` flags from stored patterns to prevent stateful `lastIndex` drift.
|
|
117
|
+
*
|
|
118
|
+
* @internal
|
|
119
|
+
*/
|
|
120
|
+
const createPatternBucket = () => {
|
|
121
|
+
const entries = [];
|
|
122
|
+
let cachedValues = null;
|
|
123
|
+
const add = (pattern, feedback) => {
|
|
124
|
+
if (!(pattern instanceof RegExp)) throw new TypeError("Error registry pattern must be a RegExp.");
|
|
125
|
+
assertFeedback(feedback);
|
|
126
|
+
const source = pattern.source;
|
|
127
|
+
const flags = pattern.flags.replace(/[gy]/g, "");
|
|
128
|
+
const existingIndex = entries.findIndex((entry) => entry.pattern.source === source && entry.pattern.flags === flags);
|
|
129
|
+
const definition = {
|
|
130
|
+
...cloneFeedback(feedback),
|
|
131
|
+
pattern: new RegExp(source, flags)
|
|
132
|
+
};
|
|
133
|
+
if (existingIndex !== -1) entries[existingIndex] = definition;
|
|
134
|
+
else entries.push(definition);
|
|
135
|
+
cachedValues = null;
|
|
136
|
+
};
|
|
137
|
+
return {
|
|
138
|
+
add,
|
|
139
|
+
addList(errorEntries) {
|
|
140
|
+
for (const [pattern, feedback] of errorEntries) add(pattern, feedback);
|
|
141
|
+
},
|
|
142
|
+
clear() {
|
|
143
|
+
entries.length = 0;
|
|
144
|
+
cachedValues = [];
|
|
145
|
+
},
|
|
146
|
+
delete(pattern) {
|
|
147
|
+
if (!(pattern instanceof RegExp)) throw new TypeError("Error registry pattern must be a RegExp.");
|
|
148
|
+
let isDeleted = false;
|
|
149
|
+
const flags = pattern.flags.replace(/[gy]/g, "");
|
|
150
|
+
for (let i = entries.length - 1; i >= 0; i--) if (entries[i].pattern.source === pattern.source && entries[i].pattern.flags === flags) {
|
|
151
|
+
entries.splice(i, 1);
|
|
152
|
+
isDeleted = true;
|
|
153
|
+
}
|
|
154
|
+
if (isDeleted) cachedValues = null;
|
|
155
|
+
return isDeleted;
|
|
156
|
+
},
|
|
157
|
+
values() {
|
|
158
|
+
if (cachedValues === null) cachedValues = entries.map((entry) => ({
|
|
159
|
+
...entry,
|
|
160
|
+
pattern: new RegExp(entry.pattern.source, entry.pattern.flags)
|
|
161
|
+
}));
|
|
162
|
+
return cachedValues;
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
//#endregion
|
|
167
|
+
export { assertFeedback, assertNonEmptyIdentifier, cloneFeedback, createFeedbackMapBucket, createPatternBucket, createPrefixBucket, normalizeErrorIdentifier };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { AppError, AppErrorOptions } from "./types.js";
|
|
2
|
+
|
|
3
|
+
//#region src/create-app-error.d.ts
|
|
4
|
+
/** Default message used when no registry entry or fallback message can describe an error. */
|
|
5
|
+
declare const DEFAULT_APP_ERROR_MESSAGE = "An unexpected error occurred.";
|
|
6
|
+
/**
|
|
7
|
+
* Normalizes an unknown error value into a predictable, structured AppError shape.
|
|
8
|
+
*
|
|
9
|
+
* This function processes the error value by unrolling nested wrappers (e.g., `cause` or `originalError`)
|
|
10
|
+
* up to a fixed depth to locate a registered classification.
|
|
11
|
+
*
|
|
12
|
+
* Classifications are resolved in priority order across all candidates:
|
|
13
|
+
* 1. Known AppError or deterministic registry match (code, name, message, prefix).
|
|
14
|
+
* 2. Unexpected AppError or heuristic registry match (pattern).
|
|
15
|
+
* 3. Any remaining AppError candidate.
|
|
16
|
+
* 4. Fallback unknown error.
|
|
17
|
+
*
|
|
18
|
+
* @param error - The raw error value to normalize (e.g., Error instances, plain objects, or strings).
|
|
19
|
+
* @param options - Configuration options controlling fallback message and registry source.
|
|
20
|
+
* @returns A normalized AppError instance. If the input is already a normalized AppError, it is returned as-is.
|
|
21
|
+
*/
|
|
22
|
+
declare function createAppError(error: unknown, options?: AppErrorOptions): AppError;
|
|
23
|
+
/**
|
|
24
|
+
* Type guard to determine if an unknown value is a normalized AppError instance.
|
|
25
|
+
*
|
|
26
|
+
* Checks the value's prototype chain and verifies the unique internal AppError brand symbol.
|
|
27
|
+
*
|
|
28
|
+
* @param value - The value to inspect.
|
|
29
|
+
* @returns True if the value is a normalized AppError; otherwise, false.
|
|
30
|
+
*/
|
|
31
|
+
declare function isAppError(value: unknown): value is AppError;
|
|
32
|
+
//#endregion
|
|
33
|
+
export { DEFAULT_APP_ERROR_MESSAGE, createAppError, isAppError };
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { classifyErrorCandidateKnown, classifyErrorCandidateUnexpected, getErrorCandidates } from "./normalize.js";
|
|
2
|
+
import { getErrorRegistry } from "./registry.js";
|
|
3
|
+
//#region src/create-app-error.ts
|
|
4
|
+
/** Default message used when no registry entry or fallback message can describe an error. */
|
|
5
|
+
const DEFAULT_APP_ERROR_MESSAGE = "An unexpected error occurred.";
|
|
6
|
+
const APP_ERROR_BRAND = Symbol.for("@codenhub/error/AppError");
|
|
7
|
+
var AppErrorImpl = class extends Error {
|
|
8
|
+
[APP_ERROR_BRAND] = true;
|
|
9
|
+
type;
|
|
10
|
+
messageKey;
|
|
11
|
+
source;
|
|
12
|
+
originalError;
|
|
13
|
+
isRetryable;
|
|
14
|
+
constructor(resolved) {
|
|
15
|
+
super(resolved.message, { cause: resolved.originalError });
|
|
16
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
17
|
+
this.name = "AppError";
|
|
18
|
+
this.type = resolved.type;
|
|
19
|
+
this.messageKey = resolved.messageKey;
|
|
20
|
+
this.source = resolved.source;
|
|
21
|
+
this.originalError = resolved.originalError;
|
|
22
|
+
this.isRetryable = resolved.isRetryable;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
const resolveFromAppError = (appError, originalError) => ({
|
|
26
|
+
type: appError.type,
|
|
27
|
+
message: appError.message,
|
|
28
|
+
messageKey: appError.messageKey,
|
|
29
|
+
source: appError.source,
|
|
30
|
+
originalError,
|
|
31
|
+
isRetryable: appError.isRetryable
|
|
32
|
+
});
|
|
33
|
+
/**
|
|
34
|
+
* Normalizes an unknown error value into a predictable, structured AppError shape.
|
|
35
|
+
*
|
|
36
|
+
* This function processes the error value by unrolling nested wrappers (e.g., `cause` or `originalError`)
|
|
37
|
+
* up to a fixed depth to locate a registered classification.
|
|
38
|
+
*
|
|
39
|
+
* Classifications are resolved in priority order across all candidates:
|
|
40
|
+
* 1. Known AppError or deterministic registry match (code, name, message, prefix).
|
|
41
|
+
* 2. Unexpected AppError or heuristic registry match (pattern).
|
|
42
|
+
* 3. Any remaining AppError candidate.
|
|
43
|
+
* 4. Fallback unknown error.
|
|
44
|
+
*
|
|
45
|
+
* @param error - The raw error value to normalize (e.g., Error instances, plain objects, or strings).
|
|
46
|
+
* @param options - Configuration options controlling fallback message and registry source.
|
|
47
|
+
* @returns A normalized AppError instance. If the input is already a normalized AppError, it is returned as-is.
|
|
48
|
+
*/
|
|
49
|
+
function createAppError(error, options = {}) {
|
|
50
|
+
const hasCustomOptions = options.fallbackMessage !== void 0 || options.registry !== void 0 || options.maxDepth !== void 0;
|
|
51
|
+
if (isAppError(error) && !hasCustomOptions) return error;
|
|
52
|
+
const fallbackMessage = options.fallbackMessage ?? "An unexpected error occurred.";
|
|
53
|
+
const errorCandidates = getErrorCandidates(isAppError(error) ? error.originalError : error, options.maxDepth);
|
|
54
|
+
const registry = options.registry ?? getErrorRegistry();
|
|
55
|
+
let knownResult = null;
|
|
56
|
+
let unexpectedResult = null;
|
|
57
|
+
let appErrorFallback = null;
|
|
58
|
+
for (const candidate of errorCandidates) {
|
|
59
|
+
if (isAppError(candidate)) {
|
|
60
|
+
if (candidate.type === "known") {
|
|
61
|
+
knownResult = resolveFromAppError(candidate, error);
|
|
62
|
+
break;
|
|
63
|
+
} else if (candidate.type === "unexpected" && unexpectedResult === null) unexpectedResult = resolveFromAppError(candidate, error);
|
|
64
|
+
else if (appErrorFallback === null) appErrorFallback = resolveFromAppError(candidate, error);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const classification = classifyErrorCandidateKnown(registry, candidate);
|
|
68
|
+
if (classification !== null) {
|
|
69
|
+
knownResult = {
|
|
70
|
+
...classification,
|
|
71
|
+
originalError: error
|
|
72
|
+
};
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
if (unexpectedResult === null) {
|
|
76
|
+
const classification = classifyErrorCandidateUnexpected(registry, candidate);
|
|
77
|
+
if (classification !== null) unexpectedResult = {
|
|
78
|
+
...classification,
|
|
79
|
+
originalError: error
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return new AppErrorImpl(knownResult ?? unexpectedResult ?? appErrorFallback ?? {
|
|
84
|
+
type: "unknown",
|
|
85
|
+
message: fallbackMessage,
|
|
86
|
+
messageKey: null,
|
|
87
|
+
source: null,
|
|
88
|
+
originalError: error,
|
|
89
|
+
isRetryable: false
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Type guard to determine if an unknown value is a normalized AppError instance.
|
|
94
|
+
*
|
|
95
|
+
* Checks the value's prototype chain and verifies the unique internal AppError brand symbol.
|
|
96
|
+
*
|
|
97
|
+
* @param value - The value to inspect.
|
|
98
|
+
* @returns True if the value is a normalized AppError; otherwise, false.
|
|
99
|
+
*/
|
|
100
|
+
function isAppError(value) {
|
|
101
|
+
return value !== null && (typeof value === "object" || typeof value === "function") && APP_ERROR_BRAND in value && value[APP_ERROR_BRAND] === true;
|
|
102
|
+
}
|
|
103
|
+
//#endregion
|
|
104
|
+
export { DEFAULT_APP_ERROR_MESSAGE, createAppError, isAppError };
|