@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/README.md CHANGED
@@ -10,12 +10,12 @@ pnpm add @codenhub/error
10
10
 
11
11
  ## Usage
12
12
 
13
- Configure the app-level `AppError.registry` during initialization, then normalize thrown or returned unknown values into a predictable error shape.
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 { AppError, err, ok, type Result } from "@codenhub/error";
16
+ import { createAppError, getErrorRegistry, err, ok, type Result } from "@codenhub/error";
17
17
 
18
- AppError.registry.codes.add("invalid_credentials", {
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 = new AppError({ code: "invalid_credentials" });
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 { AppError } from "@codenhub/error";
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
- AppError.registry.merge(browserErrorRegistry);
45
- AppError.registry.merge(supabaseErrorRegistry);
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
- AppError,
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. This is also available as `AppError.createRegistry()`.
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 app-level `AppError.registry`.
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 `AppError`.
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
- `AppError.registry` is mutable and starts empty. `createErrorRegistry()` creates another empty registry with the same bucket API.
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 `retryable` must be a boolean. Pattern buckets only accept `RegExp` patterns.
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
- retryable?: boolean;
243
+ isRetryable?: boolean;
164
244
  }
165
245
  ```
166
246
 
167
- `message` should be safe to show to users. `messageKey`, `source`, and `retryable` are optional metadata copied onto matched `AppError` instances.
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 | Description |
227
- | ----------------- | --------------- | --------------------------- | --------------------------------------------- |
228
- | `fallbackMessage` | `string` | `DEFAULT_APP_ERROR_MESSAGE` | Message used when no mapping matches. |
229
- | `registry` | `ErrorRegistry` | `AppError.registry` | Registry used to classify the provided error. |
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 `AppError.registry` or another app-owned registry. Importing a preset does not mutate `AppError.registry`.
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 mutable like any other `ErrorRegistry`. Treat imported presets as shared read-only inputs and merge them into an app-owned registry before adding app-specific mappings.
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 { AppError } from "@codenhub/error";
406
+ import { getErrorRegistry } from "@codenhub/error";
318
407
  import { browserErrorRegistry, supabaseErrorRegistry } from "@codenhub/error/registries";
319
408
 
320
- AppError.registry.merge(browserErrorRegistry);
321
- AppError.registry.merge(supabaseErrorRegistry);
409
+ getErrorRegistry().merge(browserErrorRegistry);
410
+ getErrorRegistry().merge(supabaseErrorRegistry);
322
411
  ```
323
412
 
324
413
  ### Add Multiple Mappings
325
414
 
326
415
  ```ts
327
- import { AppError } from "@codenhub/error";
416
+ import { getErrorRegistry } from "@codenhub/error";
328
417
 
329
- AppError.registry.codes.addList([
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
- - Registries, including `AppError.registry` and ready registry presets, are currently mutable. Configure shared registries during app initialization and avoid mutating imported presets at runtime. A future version is expected to provide stronger mutation safeguards.
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 };