@nice-code/error 0.10.0 → 0.12.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.
@@ -0,0 +1,1056 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let http_status_codes = require("http-status-codes");
3
+ let tslog = require("tslog");
4
+ //#region src/utils/jsErrorOrCastJsError.ts
5
+ function jsErrorOrCastJsError(error, logMessage = true) {
6
+ if (error instanceof Error) return Object.assign(error, { message: error.message });
7
+ const message = error?.message ?? (typeof error === "string" ? error : "No error message found");
8
+ if (logMessage) console.error(`An unknown and unstructured error was thrown: ${message}`, error);
9
+ return {
10
+ ...new Error(message),
11
+ ...error
12
+ };
13
+ }
14
+ //#endregion
15
+ //#region src/NiceError/nice_error.static.ts
16
+ const DUR_OBJ_PACK_PREFIX = "NE_DUROBJ[[";
17
+ const DUR_OBJ_PACK_SUFFIX = "]]NE_DUROBJ";
18
+ //#endregion
19
+ //#region src/utils/packError/packError.enums.ts
20
+ let EErrorPackType = /* @__PURE__ */ function(EErrorPackType) {
21
+ EErrorPackType["no_pack"] = "no_pack";
22
+ EErrorPackType["msg_pack"] = "msg_pack";
23
+ EErrorPackType["cause_pack"] = "cause_pack";
24
+ return EErrorPackType;
25
+ }({});
26
+ //#endregion
27
+ //#region src/utils/packError/causePack.ts
28
+ const causePack = (error) => {
29
+ error._packedState = {
30
+ cause: error.cause,
31
+ packedAs: "cause_pack"
32
+ };
33
+ error.cause = `${DUR_OBJ_PACK_PREFIX}${JSON.stringify(error.toJsonObject())}${DUR_OBJ_PACK_SUFFIX}`;
34
+ return error;
35
+ };
36
+ //#endregion
37
+ //#region src/utils/packError/msgPack.ts
38
+ const msgPack = (error) => {
39
+ error._packedState = {
40
+ message: error.cleanMessage,
41
+ packedAs: "msg_pack"
42
+ };
43
+ error.message = `${DUR_OBJ_PACK_PREFIX}${JSON.stringify(error.toJsonObject())}${DUR_OBJ_PACK_SUFFIX}`;
44
+ return error;
45
+ };
46
+ //#endregion
47
+ //#region src/utils/packError/packError.ts
48
+ const packError = (error, packType = "msg_pack") => {
49
+ if (packType === "no_pack") return error;
50
+ if (packType === "msg_pack") return msgPack(error);
51
+ return causePack(error);
52
+ };
53
+ //#endregion
54
+ //#region src/NiceError/NiceError.ts
55
+ var NiceError = class extends Error {
56
+ name = "NiceError";
57
+ def;
58
+ /** Primary id is first entry in ids. */
59
+ ids;
60
+ wasntNice;
61
+ httpStatusCode;
62
+ timeCreated;
63
+ cleanMessage;
64
+ originError;
65
+ _packedState;
66
+ /** Internal: all active id → reconciled data pairs. */
67
+ _errorDataMap;
68
+ constructor(options) {
69
+ const messagePure = options.message;
70
+ const prefixedMessage = `[${options.def.domain}](${options.ids.join(",")}) ${messagePure}`;
71
+ super(prefixedMessage);
72
+ this.cleanMessage = messagePure;
73
+ this.def = options.def;
74
+ this.ids = options.ids;
75
+ this._errorDataMap = options.errorData;
76
+ this.wasntNice = options.wasntNice ?? false;
77
+ this.httpStatusCode = options.httpStatusCode ?? 500;
78
+ if (options.originError != null) this.originError = options.originError;
79
+ this.timeCreated = options.timeCreated ?? Date.now();
80
+ }
81
+ /**
82
+ * Type guard: returns `true` if this error was created with (or contains) the
83
+ * given `id`. After the guard, `getContext(id)` will be strongly typed.
84
+ */
85
+ hasId(id) {
86
+ return id in this._errorDataMap;
87
+ }
88
+ /**
89
+ * Returns `true` if this error contains **at least one** of the supplied ids.
90
+ * Narrows `ACTIVE_IDS` to the matching subset of `IDS`.
91
+ */
92
+ hasOneOfIds(ids) {
93
+ return ids.some((id) => id in this._errorDataMap);
94
+ }
95
+ /** `true` when this error was created with more than one id (via `fromContext`). */
96
+ get hasMultiple() {
97
+ return Object.keys(this._errorDataMap).length > 1;
98
+ }
99
+ /** Returns all active error ids on this instance. */
100
+ getIds() {
101
+ return Object.keys(this._errorDataMap);
102
+ }
103
+ /**
104
+ * Returns the typed context value for the given error id.
105
+ *
106
+ * TypeScript will only allow you to call this with an id that is part of
107
+ * `ACTIVE_IDS` (i.e. an id confirmed via `hasId` / `hasOneOfIds`, or passed
108
+ * to `fromId` / `fromContext`).
109
+ *
110
+ * @throws If the context is in the `"unhydrated"` state — the error was
111
+ * reconstructed from a JSON payload and its context has a custom serializer
112
+ * that hasn't been run yet. Call `niceErrorDefined.hydrate(error)` first.
113
+ */
114
+ getContext(id) {
115
+ const state = this._errorDataMap[id]?.contextState;
116
+ if (state == null) return;
117
+ if (state.kind === "unhydrated") throw new Error(`[NiceError.getContext] Context for id "${String(id)}" is in the "unhydrated" state. The error was reconstructed from a serialized payload but has not been deserialized yet. Call \`niceErrorDefined.hydrate(error)\` to reconstruct the typed context.`);
118
+ return state.value;
119
+ }
120
+ getErrorDataForId(id) {
121
+ return this._errorDataMap[id];
122
+ }
123
+ withOriginError(error) {
124
+ this.originError = jsErrorOrCastJsError(error);
125
+ if (this._packedState?.packedAs !== "cause_pack") this.cause = this.originError;
126
+ return this;
127
+ }
128
+ /**
129
+ * Returns `true` if `other` has the same domain and the exact same set of
130
+ * active error ids as this error (order-independent).
131
+ *
132
+ * Useful for deduplication, retry logic, and asserting that two errors
133
+ * represent the same "kind" of problem without comparing context values.
134
+ *
135
+ * ```ts
136
+ * const a = err_auth.fromId("invalid_credentials", { username: "alice" });
137
+ * const b = err_auth.fromId("invalid_credentials", { username: "bob" });
138
+ * a.matches(b); // true — same domain + same id set
139
+ *
140
+ * const c = err_auth.fromId("account_locked");
141
+ * a.matches(c); // false — same domain, different id
142
+ * ```
143
+ */
144
+ matches(other) {
145
+ const myDef = this.def;
146
+ const otherDef = other.def;
147
+ if (myDef.domain !== otherDef.domain) return false;
148
+ const myIds = this.getIds().map(String).sort();
149
+ const otherIds = other.getIds().map(String).sort();
150
+ if (myIds.length !== otherIds.length) return false;
151
+ return myIds.every((id, i) => id === otherIds[i]);
152
+ }
153
+ toJsonObject() {
154
+ const originError = this.originError ? {
155
+ name: this.originError.name,
156
+ message: this.originError.cleanMessage ?? this.originError.message,
157
+ stack: this.originError.stack,
158
+ cause: this.originError.cause
159
+ } : void 0;
160
+ const def = {
161
+ domain: this.def.domain,
162
+ allDomains: this.def.allDomains
163
+ };
164
+ if (this.def.defaultHttpStatusCode != null) def["defaultHttpStatusCode"] = this.def.defaultHttpStatusCode;
165
+ if (this.def.defaultMessage != null) def["defaultMessage"] = this.def.defaultMessage;
166
+ const errorData = {};
167
+ for (const rawId of Object.keys(this._errorDataMap)) {
168
+ const id = rawId;
169
+ const data = this._errorDataMap[id];
170
+ if (data == null) continue;
171
+ let contextState;
172
+ if (data.contextState.kind === "hydrated") contextState = {
173
+ kind: "unhydrated",
174
+ serialized: data.contextState.serialized
175
+ };
176
+ else contextState = data.contextState;
177
+ errorData[id] = {
178
+ contextState,
179
+ message: data.cleanMessage ?? data.message,
180
+ httpStatusCode: data.httpStatusCode,
181
+ timeAdded: data.timeAdded
182
+ };
183
+ }
184
+ return {
185
+ name: "NiceError",
186
+ def,
187
+ ids: this.ids,
188
+ errorData,
189
+ wasntNice: this.wasntNice,
190
+ message: this.cleanMessage,
191
+ httpStatusCode: this.httpStatusCode,
192
+ timeCreated: this.timeCreated,
193
+ ...this.stack != null ? { stack: this.stack } : {},
194
+ originError
195
+ };
196
+ }
197
+ toJSON() {
198
+ return this.toJsonObject();
199
+ }
200
+ toJsonString() {
201
+ return JSON.stringify(this.toJsonObject());
202
+ }
203
+ toHttpResponse() {
204
+ return new Response(this.toJsonString(), {
205
+ status: this.httpStatusCode,
206
+ headers: { "Content-Type": "application/json" }
207
+ });
208
+ }
209
+ hydrate(definedNiceError) {
210
+ return definedNiceError.hydrate(this);
211
+ }
212
+ /**
213
+ * Iterates `cases` in order, finds the first whose domain matches this error
214
+ * (via `is()`), optionally further filters by active ids, hydrates the error,
215
+ * calls the handler, and returns `true`. Returns `false` if no case matched.
216
+ *
217
+ * Build cases with `forDomain` (any id in the domain) or `forIds` (specific
218
+ * id subset). Handlers are invoked synchronously — any returned Promise is
219
+ * not awaited. Use `handleWithAsync` when handlers are async.
220
+ *
221
+ * @example
222
+ * ```ts
223
+ * const handled = error.handleWith([
224
+ * forIds(err_feature, ["not_found"], (h) => {
225
+ * res.status(404).json({ missing: h.getContext("not_found").resource });
226
+ * }),
227
+ * forDomain(err_feature, (h) => {
228
+ * matchFirst(h, {
229
+ * forbidden: ({ userId }) => res.status(403).json({ userId }),
230
+ * _: () => res.status(500).end(),
231
+ * });
232
+ * }),
233
+ * forDomain(err_service, (h) => {
234
+ * res.status(h.httpStatusCode).json({ error: h.message });
235
+ * }),
236
+ * ]);
237
+ * if (!handled) next(error);
238
+ * ```
239
+ */
240
+ handleWithSync(handlerInput, handlerOptions = {}) {
241
+ const handlersArray = Array.isArray(handlerInput) ? handlerInput : [handlerInput];
242
+ for (const handler of handlersArray) {
243
+ const result = handler.handleErrorWithPromiseInspection(this, handlerOptions);
244
+ if (result.matched) {
245
+ if (result.isPromise) console.warn(`[NiceError.handleWith] Handler ${result.target.identifier} returned a Promise but was called via \`handleWith\` (synchronous). The Promise will not be awaited. Use \`handleWithAsync\` for async handlers.`);
246
+ return result.isPromise ? void 0 : result.handlerResponse;
247
+ }
248
+ }
249
+ if (handlerOptions.throwOnUnhandled === true) throw this;
250
+ }
251
+ /**
252
+ * Same matching logic as `handleWith`, but `await`s the handler's returned
253
+ * Promise before resolving. Use this when your handlers perform async work
254
+ * (database writes, HTTP calls, etc.).
255
+ *
256
+ * @example
257
+ * ```ts
258
+ * const handled = await error.handleWithAsync([
259
+ * forDomain(err_payments, async (h) => {
260
+ * await db.logFailedPayment(h);
261
+ * await notifyOps(h.message);
262
+ * }),
263
+ * ]);
264
+ * ```
265
+ */
266
+ async handleWithAsync(handlerInput, handlerOptions = {}) {
267
+ const handlersArray = Array.isArray(handlerInput) ? handlerInput : [handlerInput];
268
+ for (const handler of handlersArray) {
269
+ const result = handler.handleErrorWithPromiseInspection(this, handlerOptions);
270
+ if (result.matched) return result.isPromise ? await result.handlerPromise : result.handlerResponse;
271
+ }
272
+ if (handlerOptions.throwOnUnhandled === true) throw this;
273
+ }
274
+ get isPacked() {
275
+ return this._packedState != null;
276
+ }
277
+ pack(packType = "msg_pack") {
278
+ if (this.isPacked) return this;
279
+ return packError(this, packType);
280
+ }
281
+ unpack() {
282
+ if (this._packedState == null) return this;
283
+ if (this._packedState.packedAs === "msg_pack") this.message = this._packedState.message;
284
+ if (this._packedState.packedAs === "cause_pack") this.cause = this._packedState.cause;
285
+ this._packedState = void 0;
286
+ delete this._packedState;
287
+ return this;
288
+ }
289
+ };
290
+ //#endregion
291
+ //#region src/NiceError/NiceErrorHydrated.ts
292
+ var NiceErrorHydrated = class NiceErrorHydrated extends NiceError {
293
+ def;
294
+ niceErrorDefined;
295
+ constructor(options) {
296
+ super(options);
297
+ this.def = options.def;
298
+ this.niceErrorDefined = options.niceErrorDefined;
299
+ }
300
+ /**
301
+ * Returns a **new** `NiceErrorHydrated` with additional id+context entries merged in.
302
+ * The returned error's `ACTIVE_IDS` is the union of the original ids and the
303
+ * newly supplied keys.
304
+ *
305
+ * ```ts
306
+ * const err = errDef.fromId("id_a", { a: 1 })
307
+ * .addContext({ id_b: { b: "x" } });
308
+ * err.getIds(); // ["id_a", "id_b"]
309
+ * ```
310
+ */
311
+ addContext(context) {
312
+ const newIds = Object.keys(context);
313
+ const newErrorData = {};
314
+ for (const id of newIds) newErrorData[id] = this.niceErrorDefined.reconcileErrorDataForId(id, context[id]);
315
+ const mergedErrorData = {
316
+ ...this._errorDataMap,
317
+ ...newErrorData
318
+ };
319
+ const mergedIds = Array.from(new Set([...this.getIds(), ...Object.keys(context)]));
320
+ return new NiceErrorHydrated({
321
+ def: this.def,
322
+ niceErrorDefined: this.niceErrorDefined,
323
+ ids: mergedIds,
324
+ errorData: mergedErrorData,
325
+ message: this.cleanMessage,
326
+ wasntNice: this.wasntNice,
327
+ httpStatusCode: this.httpStatusCode,
328
+ originError: this.originError
329
+ });
330
+ }
331
+ /**
332
+ * Returns a **new** `NiceErrorHydrated` with an additional error id (and its context,
333
+ * if the schema requires one). Equivalent to `addContext({ [id]: context })`
334
+ * but mirrors the `fromId` ergonomics for single-id additions.
335
+ */
336
+ addId(...args) {
337
+ const [id, context] = args;
338
+ const reconciledData = this.niceErrorDefined.reconcileErrorDataForId(id, context);
339
+ const errorDataMap = {};
340
+ errorDataMap[id] = reconciledData;
341
+ const mergedContexts = {
342
+ ...this._errorDataMap,
343
+ ...errorDataMap
344
+ };
345
+ const mergedIds = Array.from(new Set([...this.getIds(), id]));
346
+ return new NiceErrorHydrated({
347
+ def: this.def,
348
+ niceErrorDefined: this.niceErrorDefined,
349
+ ids: mergedIds,
350
+ errorData: mergedContexts,
351
+ message: this.cleanMessage,
352
+ wasntNice: this.wasntNice,
353
+ httpStatusCode: this.httpStatusCode,
354
+ originError: this.originError
355
+ });
356
+ }
357
+ };
358
+ //#endregion
359
+ //#region src/NiceErrorDefined/NiceErrorDefined.ts
360
+ var NiceErrorDomain = class NiceErrorDomain {
361
+ domain;
362
+ allDomains;
363
+ defaultHttpStatusCode;
364
+ defaultMessage;
365
+ /** Kept for runtime use (message resolution, httpStatusCode, context serialization, etc.). */
366
+ _schema;
367
+ _definedChildNiceErrors = [];
368
+ _definedParentNiceError;
369
+ /** Set by `.packAs()` — explicit per-instance override, takes priority over `_packAsFn`. */
370
+ _setPack;
371
+ /** Set at definition time — called dynamically each time an error is created. */
372
+ _packAsFn;
373
+ constructor(definition) {
374
+ this.domain = definition.domain;
375
+ this.allDomains = definition.allDomains;
376
+ this._schema = definition.schema;
377
+ if (definition.packAs != null) this._packAsFn = definition.packAs;
378
+ if (definition.defaultHttpStatusCode != null) this.defaultHttpStatusCode = definition.defaultHttpStatusCode;
379
+ if (definition.defaultMessage != null) this.defaultMessage = definition.defaultMessage;
380
+ }
381
+ /**
382
+ * Creates a child domain that inherits this domain in `allDomains`.
383
+ * The child has its own schema and its own domain string.
384
+ */
385
+ createChildDomain(subErrorDef) {
386
+ const child = new NiceErrorDomain({
387
+ domain: subErrorDef.domain,
388
+ allDomains: [subErrorDef.domain, ...this.allDomains],
389
+ schema: subErrorDef.schema,
390
+ defaultHttpStatusCode: subErrorDef.defaultHttpStatusCode,
391
+ defaultMessage: subErrorDef.defaultMessage
392
+ });
393
+ this.addChildNiceErrorDefined(child);
394
+ child.addParentNiceErrorDefined(this);
395
+ if (subErrorDef.packAs != null) child._packAsFn = subErrorDef.packAs;
396
+ else if (this._setPack) child.packAs(this._setPack);
397
+ else if (this._packAsFn) child._packAsFn = this._packAsFn;
398
+ return child;
399
+ }
400
+ addParentNiceErrorDefined(parentError) {
401
+ if (this._definedParentNiceError?.domain === parentError.domain) return;
402
+ this._definedParentNiceError = {
403
+ domain: parentError.domain,
404
+ definedError: parentError
405
+ };
406
+ }
407
+ addChildNiceErrorDefined(child) {
408
+ if (this._definedChildNiceErrors.some((linked) => linked.domain === child.domain)) return;
409
+ this._definedChildNiceErrors.push({
410
+ domain: child.domain,
411
+ definedError: child
412
+ });
413
+ if (this._definedParentNiceError) this._definedParentNiceError.definedError.addChildNiceErrorDefined(child);
414
+ }
415
+ packAs(pack) {
416
+ this._setPack = pack;
417
+ return this;
418
+ }
419
+ createError(input) {
420
+ const err = new NiceErrorHydrated(input);
421
+ const packType = this._setPack ?? this._packAsFn?.();
422
+ if (packType != null && packType !== "no_pack") return err.pack(packType);
423
+ return err;
424
+ }
425
+ /**
426
+ * Promotes a plain `NiceError<ERR_DEF>` back into a `NiceErrorHydrated` so
427
+ * that builder methods (`addId`, `addContext`, etc.) are available again.
428
+ *
429
+ * For each active id, if the context is in the `"unhydrated"` state (i.e. the
430
+ * error was reconstructed from a JSON payload), `hydrate` calls
431
+ * `fromJsonSerializable` to reconstruct the typed value and advances the state
432
+ * to `"hydrated"`. Ids already in `"hydrated"` or `"raw_unset"` state
433
+ * are passed through unchanged.
434
+ *
435
+ * @throws If `error.def.domain` does not match this definition's domain. Use
436
+ * `niceErrorDefined.is(error)` before calling `hydrate` to ensure compatibility.
437
+ *
438
+ * ```ts
439
+ * const raw = castNiceError(apiResponseBody);
440
+ *
441
+ * if (err_user_auth.is(raw)) {
442
+ * const hydrated = err_user_auth.hydrate(raw);
443
+ * // hydrated.getContext("invalid_credentials") — fully typed, no throw
444
+ * // hydrated.addId / addContext — available again
445
+ * }
446
+ * ```
447
+ */
448
+ hydrate(error) {
449
+ const errDef = error.def;
450
+ if (errDef.domain !== this.domain) throw new Error(`[NiceErrorDefined.hydrate] Domain mismatch: this definition is "${this.domain}" but the error belongs to "${errDef.domain}". Call \`niceErrorDefined.is(error)\` before hydrating to ensure compatibility.`);
451
+ const finalError = error instanceof NiceError ? error : new NiceError(error);
452
+ const reconciledErrorData = {};
453
+ for (const id of finalError.getIds()) {
454
+ const existingData = finalError.getErrorDataForId(id);
455
+ if (existingData == null) continue;
456
+ let contextState = existingData.contextState;
457
+ if (contextState.kind === "unhydrated") {
458
+ const deserialize = this._schema[id]?.context?.serialization?.fromJsonSerializable;
459
+ if (deserialize != null) contextState = {
460
+ kind: "hydrated",
461
+ value: deserialize(contextState.serialized),
462
+ serialized: contextState.serialized
463
+ };
464
+ }
465
+ reconciledErrorData[id] = {
466
+ contextState,
467
+ message: existingData.cleanMessage ?? existingData.message,
468
+ httpStatusCode: existingData.httpStatusCode,
469
+ timeAdded: existingData.timeAdded
470
+ };
471
+ }
472
+ return new NiceErrorHydrated({
473
+ def: this._buildDef(),
474
+ niceErrorDefined: this,
475
+ ids: finalError.ids,
476
+ errorData: reconciledErrorData,
477
+ message: finalError.cleanMessage ?? finalError.message,
478
+ httpStatusCode: finalError.httpStatusCode,
479
+ wasntNice: finalError.wasntNice,
480
+ originError: finalError.originError,
481
+ timeCreated: finalError.timeCreated
482
+ });
483
+ }
484
+ /**
485
+ * Creates a `NiceErrorHydrated` for a single error id.
486
+ *
487
+ * - `id` autocompletes to the schema keys.
488
+ * - The second argument `context` is required / optional / absent based on
489
+ * whether the schema entry declares `context.required: true`.
490
+ * - The returned error has `ACTIVE_IDS` narrowed to exactly `K`, so
491
+ * `getContext(id)` is immediately strongly typed.
492
+ */
493
+ fromId(...args) {
494
+ const [id, context] = args;
495
+ const reconciledData = this.reconcileErrorDataForId(id, context);
496
+ const errorData = {};
497
+ errorData[id] = reconciledData;
498
+ const err = this.createError({
499
+ def: this._buildDef(),
500
+ niceErrorDefined: this,
501
+ ids: [id],
502
+ errorData,
503
+ message: reconciledData.cleanMessage ?? reconciledData.message,
504
+ httpStatusCode: reconciledData.httpStatusCode
505
+ });
506
+ if (typeof Error.captureStackTrace === "function") Error.captureStackTrace(err, this.fromId);
507
+ return err;
508
+ }
509
+ fromContext(context) {
510
+ const ids = Object.keys(context);
511
+ if (ids.length === 0) throw new Error("[NiceErrorDefined.fromContext] context object must contain at least one error id.");
512
+ const errorData = {};
513
+ for (const id of ids) errorData[id] = this.reconcileErrorDataForId(id, context[id]);
514
+ const primaryId = ids[0];
515
+ const err = this.createError({
516
+ def: this._buildDef(),
517
+ niceErrorDefined: this,
518
+ ids,
519
+ errorData,
520
+ message: errorData[primaryId].cleanMessage ?? errorData[primaryId].message,
521
+ httpStatusCode: errorData[primaryId].httpStatusCode
522
+ });
523
+ if (typeof Error.captureStackTrace === "function") Error.captureStackTrace(err, this.fromContext);
524
+ return err;
525
+ }
526
+ /**
527
+ * Returns `true` if `error` is a `NiceError` whose `def.domain` exactly matches
528
+ * this definition's domain.
529
+ *
530
+ * Use this after `castNiceError` to narrow an unknown error to this specific
531
+ * domain before accessing its typed ids/context:
532
+ *
533
+ * ```ts
534
+ * const caught = castNiceError(e);
535
+ *
536
+ * if (err_user_auth.is(caught)) {
537
+ * // caught is now NiceError<typeof err_user_auth's ERR_DEF>
538
+ * const hydrated = err_user_auth.hydrate(caught);
539
+ * const { username } = hydrated.getContext("invalid_credentials");
540
+ * }
541
+ * ```
542
+ */
543
+ isExact(error) {
544
+ if (!(error instanceof NiceError)) return false;
545
+ return error.def.domain === this.domain;
546
+ }
547
+ isThisOrChild(error) {
548
+ if (!(error instanceof NiceError)) return false;
549
+ const errDef = error.def;
550
+ return errDef.domain === this.domain || this.allDomains.includes(errDef.domain);
551
+ }
552
+ /**
553
+ * Returns `true` if this domain appears anywhere in the target's ancestry
554
+ * chain (including an exact domain match).
555
+ *
556
+ * Accepts either a `NiceErrorDefined` (domain definition) or a `NiceError`
557
+ * instance (extracts the domain from its `def`).
558
+ */
559
+ isParentOf(target) {
560
+ const allDomains = target instanceof NiceError ? target.def.allDomains : target.allDomains;
561
+ return Array.isArray(allDomains) && allDomains.includes(this.domain);
562
+ }
563
+ _buildDef() {
564
+ return {
565
+ domain: this.domain,
566
+ allDomains: this.allDomains,
567
+ schema: this._schema
568
+ };
569
+ }
570
+ _resolveMessage(id, context) {
571
+ const entry = this._schema[id];
572
+ if (typeof entry?.message === "function") return entry.message(context);
573
+ if (typeof entry?.message === "string") return entry.message;
574
+ return this.defaultMessage ?? `[${this.domain}::${id}] An error occurred.`;
575
+ }
576
+ _resolveHttpStatusCode(id, context) {
577
+ const entry = this._schema[id];
578
+ let httpStatusCode;
579
+ if (typeof entry?.httpStatusCode === "function") httpStatusCode = entry.httpStatusCode(context);
580
+ if (typeof entry?.httpStatusCode === "number") httpStatusCode = entry.httpStatusCode;
581
+ return typeof httpStatusCode === "number" ? httpStatusCode : this.defaultHttpStatusCode ?? 500;
582
+ }
583
+ reconcileErrorDataForId(id, context) {
584
+ const message = this._resolveMessage(id, context);
585
+ const httpStatusCode = this._resolveHttpStatusCode(id, context);
586
+ const entry = this._schema[id];
587
+ let contextState;
588
+ if (context != null && entry?.context?.serialization != null) contextState = {
589
+ kind: "hydrated",
590
+ value: context,
591
+ serialized: entry.context.serialization.toJsonSerializable(context)
592
+ };
593
+ else contextState = {
594
+ kind: "serde_unset",
595
+ value: context
596
+ };
597
+ return {
598
+ contextState,
599
+ message,
600
+ httpStatusCode,
601
+ timeAdded: Date.now()
602
+ };
603
+ }
604
+ };
605
+ //#endregion
606
+ //#region src/NiceErrorDefined/defineNiceError.ts
607
+ const defineNiceError = (definition) => {
608
+ return new NiceErrorDomain({
609
+ domain: definition.domain,
610
+ allDomains: [definition.domain],
611
+ schema: definition.schema,
612
+ ...definition.packAs != null ? { packAs: definition.packAs } : {}
613
+ });
614
+ };
615
+ //#endregion
616
+ //#region src/NiceErrorDefined/err.ts
617
+ function err(meta) {
618
+ return meta ?? {};
619
+ }
620
+ //#endregion
621
+ //#region src/internal/nice_core_errors.ts
622
+ const err_nice = defineNiceError({
623
+ domain: "err_nice",
624
+ schema: {}
625
+ });
626
+ let EErrId_CastNotNice = /* @__PURE__ */ function(EErrId_CastNotNice) {
627
+ EErrId_CastNotNice["js_error"] = "native_error";
628
+ EErrId_CastNotNice["js_error_like_object"] = "js_error_like_object";
629
+ EErrId_CastNotNice["nullish_value"] = "nullish_value";
630
+ EErrId_CastNotNice["js_data_type"] = "js_data_type";
631
+ EErrId_CastNotNice["js_other"] = "js_other";
632
+ return EErrId_CastNotNice;
633
+ }({});
634
+ const err_cast_not_nice = err_nice.createChildDomain({
635
+ domain: "err_cast_not_nice",
636
+ defaultHttpStatusCode: http_status_codes.StatusCodes.UNPROCESSABLE_ENTITY,
637
+ schema: {
638
+ ["native_error"]: err({
639
+ context: { required: true },
640
+ message: ({ jsError }) => `A native JavaScript Error was encountered during casting: ${jsError.message}`,
641
+ httpStatusCode: http_status_codes.StatusCodes.INTERNAL_SERVER_ERROR
642
+ }),
643
+ ["js_error_like_object"]: err({
644
+ context: { required: true },
645
+ message: ({ jsErrorObject }) => `An object resembling a JavaScript Error was encountered during casting: [${jsErrorObject.name}] ${jsErrorObject.message}`,
646
+ httpStatusCode: http_status_codes.StatusCodes.INTERNAL_SERVER_ERROR
647
+ }),
648
+ ["nullish_value"]: err({
649
+ context: { required: true },
650
+ message: ({ value }) => `A nullish value [${value === null ? "null" : "undefined"}] was encountered during casting`
651
+ }),
652
+ ["js_data_type"]: err({
653
+ context: { required: true },
654
+ message: ({ jsDataType, jsDataValue }) => {
655
+ let inspectedValue;
656
+ try {
657
+ inspectedValue = JSON.stringify(jsDataValue);
658
+ } catch {}
659
+ return `A value of type [${jsDataType}] with value [${inspectedValue ?? "UNSERIALIZABLE"}] was encountered during casting, which is not a valid error type`;
660
+ }
661
+ }),
662
+ ["js_other"]: err({
663
+ context: { required: true },
664
+ message: ({ jsDataValue }) => {
665
+ let inspectedValue;
666
+ try {
667
+ inspectedValue = JSON.stringify(jsDataValue);
668
+ } catch {}
669
+ return `An unhandled type [${typeof jsDataValue}] with value [${inspectedValue ?? "UNSERIALIZABLE"}] was encountered during casting, which is not a valid error type`;
670
+ }
671
+ })
672
+ }
673
+ });
674
+ const err_nice_handler = err_nice.createChildDomain({
675
+ domain: "err_nice_handler",
676
+ schema: {}
677
+ });
678
+ //#endregion
679
+ //#region src/NiceErrorHandler/NiceErrorHandler.types.ts
680
+ let EErrorHandlerTargetType = /* @__PURE__ */ function(EErrorHandlerTargetType) {
681
+ EErrorHandlerTargetType["ids"] = "ids";
682
+ EErrorHandlerTargetType["domain"] = "domain";
683
+ EErrorHandlerTargetType["default"] = "default";
684
+ return EErrorHandlerTargetType;
685
+ }({});
686
+ //#endregion
687
+ //#region src/NiceErrorHandler/NiceErrorHandler.ts
688
+ var NiceErrorHandler = class {
689
+ handlerConfigs = [];
690
+ _defaultRequester;
691
+ handleErrorWithPromiseInspection(error, options) {
692
+ for (const handlerConfig of this.handlerConfigs) {
693
+ if (!handlerConfig._matcher(error)) continue;
694
+ const errorResult = handlerConfig._requester(error);
695
+ if (errorResult instanceof Promise) return {
696
+ isPromise: true,
697
+ matched: true,
698
+ target: handlerConfig.target,
699
+ handlerPromise: errorResult
700
+ };
701
+ return {
702
+ isPromise: false,
703
+ matched: true,
704
+ target: handlerConfig.target,
705
+ handlerResponse: errorResult
706
+ };
707
+ }
708
+ if (this._defaultRequester) {
709
+ const defaultResult = this._defaultRequester(error);
710
+ if (defaultResult instanceof Promise) return {
711
+ isPromise: true,
712
+ matched: true,
713
+ target: {
714
+ type: "default",
715
+ identifier: "[matched:default]"
716
+ },
717
+ handlerPromise: defaultResult
718
+ };
719
+ return {
720
+ isPromise: false,
721
+ matched: true,
722
+ target: {
723
+ type: "default",
724
+ identifier: "[matched:default]"
725
+ },
726
+ handlerResponse: defaultResult
727
+ };
728
+ }
729
+ if (options?.throwOnUnhandled === true) throw error;
730
+ return {
731
+ matched: false,
732
+ attemptedTargets: this.handlerConfigs.map((config) => config.target)
733
+ };
734
+ }
735
+ /**
736
+ * Register a handler that fires for **any** error whose domain matches `domain`.
737
+ * The handler receives a fully hydrated error — `getContext`, `addId`, and `addContext`
738
+ * are all available. First matching case wins.
739
+ */
740
+ forDomain(domain, handler) {
741
+ this.handlerConfigs.push({
742
+ target: {
743
+ type: "domain",
744
+ domain: domain.domain,
745
+ identifier: `[matched:domain:${domain.domain}]`
746
+ },
747
+ _matcher: (error) => domain.isExact(error),
748
+ _requester: (error) => handler(domain.hydrate(error))
749
+ });
750
+ return this;
751
+ }
752
+ forId(domain, id, handler) {
753
+ this.handlerConfigs.push({
754
+ target: {
755
+ type: "ids",
756
+ domain: domain.domain,
757
+ ids: [id],
758
+ identifier: `[matched:ids:${domain.domain}:${id}]`
759
+ },
760
+ _matcher: (error) => domain.isExact(error) && error.hasId(id),
761
+ _requester: (error) => handler(domain.hydrate(error))
762
+ });
763
+ return this;
764
+ }
765
+ forIds(domain, ids, handler) {
766
+ this.handlerConfigs.push({
767
+ target: {
768
+ type: "ids",
769
+ domain: domain.domain,
770
+ ids,
771
+ identifier: `[matched:ids:${domain.domain}:${ids.join(",")}]`
772
+ },
773
+ _matcher: (error) => domain.isExact(error) && ids.some((id) => error.hasId(id)),
774
+ _requester: (error) => handler(domain.hydrate(error))
775
+ });
776
+ return this;
777
+ }
778
+ /**
779
+ * Register a fallback handler that fires when no other case matches.
780
+ * Only one default handler can be registered — calling this twice replaces the previous one.
781
+ */
782
+ setDefaultHandler(handler) {
783
+ this._defaultRequester = handler;
784
+ return this;
785
+ }
786
+ };
787
+ //#endregion
788
+ //#region src/NiceErrorHandler/handleWith.ts
789
+ function forDomain(domain, handler) {
790
+ return new NiceErrorHandler().forDomain(domain, handler);
791
+ }
792
+ function forId(domain, id, handler) {
793
+ return new NiceErrorHandler().forId(domain, id, handler);
794
+ }
795
+ function forIds(domain, ids, handler) {
796
+ return new NiceErrorHandler().forIds(domain, ids, handler);
797
+ }
798
+ //#endregion
799
+ //#region src/utils/isNiceErrorObject.ts
800
+ /**
801
+ * Returns `true` if `obj` is a JSON-serialised `NiceError` object matching the
802
+ * current wire format (contextState-based errorData entries).
803
+ *
804
+ * Validates:
805
+ * - Top-level shape (`name`, `message`, `wasntNice`, `httpStatusCode`, `def`)
806
+ * - Each `errorData` entry has a `contextState` with a valid `kind` discriminant
807
+ * (`"no_serialization"` or `"unhydrated"`) — rejecting payloads in the old
808
+ * format (`context` / `serialized` fields) to prevent silent data corruption.
809
+ */
810
+ function isNiceErrorObject(obj) {
811
+ if (typeof obj !== "object" || obj == null) return false;
812
+ const o = obj;
813
+ if (o["name"] !== "NiceError" || typeof o["message"] !== "string" || typeof o["wasntNice"] !== "boolean" || typeof o["httpStatusCode"] !== "number") return false;
814
+ const def = o["def"];
815
+ if (typeof def !== "object" || def == null) return false;
816
+ const d = def;
817
+ if (typeof d["domain"] !== "string" || !Array.isArray(d["allDomains"])) return false;
818
+ const errorData = o["errorData"];
819
+ if (errorData != null) {
820
+ if (typeof errorData !== "object") return false;
821
+ for (const entry of Object.values(errorData)) {
822
+ if (entry == null) continue;
823
+ if (typeof entry !== "object") return false;
824
+ const state = entry["contextState"];
825
+ if (state == null || typeof state !== "object") return false;
826
+ const kind = state["kind"];
827
+ if (kind !== "serde_unset" && kind !== "unhydrated") return false;
828
+ }
829
+ }
830
+ return true;
831
+ }
832
+ //#endregion
833
+ //#region src/utils/isRegularErrorObject.ts
834
+ function isRegularErrorJsonObject(obj) {
835
+ if (typeof obj !== "object" || obj == null) return false;
836
+ const o = obj;
837
+ return typeof o["name"] === "string" && typeof o["message"] === "string";
838
+ }
839
+ //#endregion
840
+ //#region src/utils/logger.ts
841
+ const logger_NiceError = new tslog.Logger({ name: "NiceErrorLogger" });
842
+ logger_NiceError.getSubLogger({ name: "NiceErrorTestingLogger" });
843
+ //#endregion
844
+ //#region src/utils/inspectPotentialError/inspectPotentialError.ts
845
+ function interpretMessagePackedError(parsedError) {
846
+ let packedErrorStr;
847
+ if (typeof parsedError.message === "string" && parsedError.message.includes("NE_DUROBJ[[") && parsedError.message.includes("]]NE_DUROBJ")) packedErrorStr = parsedError.message;
848
+ if (typeof parsedError.cause === "string" && parsedError.cause.includes("NE_DUROBJ[[") && parsedError.cause.includes("]]NE_DUROBJ")) packedErrorStr = parsedError.cause;
849
+ if (packedErrorStr != null) {
850
+ const jsonStr = packedErrorStr.split(DUR_OBJ_PACK_PREFIX)[1].split(DUR_OBJ_PACK_SUFFIX)[0];
851
+ try {
852
+ const errorObj = JSON.parse(jsonStr);
853
+ if (isNiceErrorObject(errorObj)) return {
854
+ type: "niceErrorObject",
855
+ niceErrorObject: errorObj
856
+ };
857
+ } catch {}
858
+ }
859
+ return null;
860
+ }
861
+ const inspectPotentialError = (potentialError) => {
862
+ if (potentialError == null) return {
863
+ type: "nullish",
864
+ value: potentialError
865
+ };
866
+ if (typeof potentialError === "number") return {
867
+ type: "jsDataType",
868
+ jsDataType: "number",
869
+ jsDataValue: potentialError
870
+ };
871
+ if (typeof potentialError === "boolean") return {
872
+ type: "jsDataType",
873
+ jsDataType: "boolean",
874
+ jsDataValue: potentialError
875
+ };
876
+ let parsedError = potentialError;
877
+ if (typeof potentialError === "string") if (potentialError.includes("{") && potentialError.includes("name")) try {
878
+ parsedError = JSON.parse(potentialError);
879
+ } catch {
880
+ return {
881
+ type: "jsDataType",
882
+ jsDataType: "string",
883
+ jsDataValue: potentialError
884
+ };
885
+ }
886
+ else return {
887
+ type: "jsDataType",
888
+ jsDataType: "string",
889
+ jsDataValue: potentialError
890
+ };
891
+ if (typeof parsedError !== "object" || parsedError == null) {
892
+ logger_NiceError.warn({
893
+ message: "Received a potential error that is a primitive data type other than string, number, or boolean. This is unexpected and may indicate an issue with error handling in the code.",
894
+ potentialError
895
+ });
896
+ return {
897
+ jsDataValue: potentialError,
898
+ type: "jsOther"
899
+ };
900
+ }
901
+ if (parsedError instanceof NiceError) return {
902
+ type: "niceError",
903
+ niceError: parsedError
904
+ };
905
+ if (isNiceErrorObject(parsedError)) return {
906
+ type: "niceErrorObject",
907
+ niceErrorObject: parsedError
908
+ };
909
+ if (parsedError instanceof Error) {
910
+ const durObjResult = interpretMessagePackedError(parsedError);
911
+ if (durObjResult != null) return durObjResult;
912
+ return {
913
+ type: "jsError",
914
+ jsError: parsedError
915
+ };
916
+ }
917
+ if (isRegularErrorJsonObject(parsedError)) {
918
+ const durObjResult = interpretMessagePackedError(parsedError);
919
+ if (durObjResult != null) return durObjResult;
920
+ return {
921
+ type: "jsErrorObject",
922
+ jsErrorObject: parsedError
923
+ };
924
+ }
925
+ return {
926
+ type: "jsDataType",
927
+ jsDataType: "object",
928
+ jsDataValue: parsedError
929
+ };
930
+ };
931
+ //#endregion
932
+ //#region src/utils/castNiceError.ts
933
+ /**
934
+ * Casts any unknown value into a `NiceError`.
935
+ *
936
+ * - If the value is already a `NiceError` instance, it is returned as-is.
937
+ * - If the value is a plain `Error`, it is wrapped with the original as `originError`.
938
+ * - If the value is a JSON-serialised `NiceError` object (e.g. from an API
939
+ * response), a best-effort `NiceError` is re-created from it.
940
+ * - For all other values, a generic `NiceError` is created with a descriptive
941
+ * message.
942
+ *
943
+ * After casting, use `NiceErrorDefined.is(error)` to narrow the error to a
944
+ * specific domain and access its strongly-typed ids and context.
945
+ */
946
+ const castNiceError = (error) => {
947
+ const inspected = inspectPotentialError(error);
948
+ switch (inspected.type) {
949
+ case "niceError": return inspected.niceError;
950
+ case "niceErrorObject": {
951
+ const obj = inspected.niceErrorObject;
952
+ return new NiceError(obj);
953
+ }
954
+ case "jsError": return err_cast_not_nice.fromContext({ ["native_error"]: inspected }).withOriginError(inspected.jsError);
955
+ case "jsErrorObject": {
956
+ const err = err_cast_not_nice.fromContext({ ["js_error_like_object"]: inspected });
957
+ err.cause = inspected.jsErrorObject;
958
+ return err;
959
+ }
960
+ case "nullish": return err_cast_not_nice.fromContext({ ["nullish_value"]: inspected });
961
+ case "jsDataType": return err_cast_not_nice.fromContext({ ["js_data_type"]: inspected });
962
+ default: return err_cast_not_nice.fromContext({ ["js_other"]: inspected });
963
+ }
964
+ };
965
+ //#endregion
966
+ //#region src/utils/castAndHydrate.ts
967
+ /**
968
+ * Combines `castNiceError`, `is()`, and `hydrate()` in a single call — the
969
+ * idiomatic way to handle an unknown value arriving from a remote boundary
970
+ * (API response, message queue, IPC, etc.) when you have a specific domain in mind.
971
+ *
972
+ * - Casts `value` to a `NiceError` using `castNiceError`.
973
+ * - If the result belongs to `niceErrorDefined`'s domain (`is()` returns `true`),
974
+ * hydrates it and returns a fully-typed `NiceErrorHydrated`.
975
+ * - Otherwise returns the raw cast `NiceError` (which may be a `wasntNice` error
976
+ * if `value` was not a NiceError at all).
977
+ *
978
+ * @example
979
+ * ```ts
980
+ * // In an Express error handler:
981
+ * app.use((err, req, res, next) => {
982
+ * const error = castAndHydrate(err, err_user_auth);
983
+ *
984
+ * if (err_user_auth.is(error)) {
985
+ * // error is NiceErrorHydrated — getContext / addId available
986
+ * const result = matchFirst(error, {
987
+ * invalid_credentials: ({ username }) => res.status(401).json({ username }),
988
+ * account_locked: () => res.status(403).json({ locked: true }),
989
+ * });
990
+ * if (result) return;
991
+ * }
992
+ *
993
+ * next(err);
994
+ * });
995
+ * ```
996
+ */
997
+ function castAndHydrate(value, niceErrorDefined) {
998
+ const casted = castNiceError(value);
999
+ if (niceErrorDefined.isExact(casted)) return niceErrorDefined.hydrate(casted);
1000
+ return casted;
1001
+ }
1002
+ //#endregion
1003
+ //#region src/utils/matchFirst.ts
1004
+ /**
1005
+ * Pattern-matches an error against a map of id → handler functions, returning the
1006
+ * result of the first handler whose id is active on the error.
1007
+ *
1008
+ * - Ids are tested in the order returned by `error.getIds()`.
1009
+ * - If no id-specific handler matched and `_` is provided, the fallback is called.
1010
+ * - Returns `undefined` when neither any id handler nor the fallback fires.
1011
+ *
1012
+ * **Requires hydrated context.** If any matched id is in the `"unhydrated"` state,
1013
+ * `getContext` will throw. Call `niceErrorDefined.hydrate(error)` beforehand when
1014
+ * working with errors deserialized from a JSON payload.
1015
+ *
1016
+ * @example
1017
+ * ```ts
1018
+ * const result = matchFirst(error, {
1019
+ * invalid_credentials: ({ username }) => `Wrong password for ${username}`,
1020
+ * account_locked: () => "Account is locked",
1021
+ * _: () => "Unknown auth error",
1022
+ * });
1023
+ * ```
1024
+ */
1025
+ function matchFirst(error, handlers) {
1026
+ for (const id of error.getIds()) {
1027
+ const handler = handlers[id];
1028
+ if (typeof handler === "function") return handler(error.getContext(id));
1029
+ }
1030
+ if (typeof handlers._ === "function") return handlers._();
1031
+ }
1032
+ //#endregion
1033
+ exports.EErrId_CastNotNice = EErrId_CastNotNice;
1034
+ exports.EErrorHandlerTargetType = EErrorHandlerTargetType;
1035
+ exports.EErrorPackType = EErrorPackType;
1036
+ exports.NiceError = NiceError;
1037
+ exports.NiceErrorDomain = NiceErrorDomain;
1038
+ exports.NiceErrorHandler = NiceErrorHandler;
1039
+ exports.NiceErrorHydrated = NiceErrorHydrated;
1040
+ exports.castAndHydrate = castAndHydrate;
1041
+ exports.castNiceError = castNiceError;
1042
+ exports.causePack = causePack;
1043
+ exports.defineNiceError = defineNiceError;
1044
+ exports.err = err;
1045
+ exports.err_cast_not_nice = err_cast_not_nice;
1046
+ exports.err_nice = err_nice;
1047
+ exports.err_nice_handler = err_nice_handler;
1048
+ exports.forDomain = forDomain;
1049
+ exports.forId = forId;
1050
+ exports.forIds = forIds;
1051
+ exports.isNiceErrorObject = isNiceErrorObject;
1052
+ exports.isRegularErrorJsonObject = isRegularErrorJsonObject;
1053
+ exports.matchFirst = matchFirst;
1054
+ exports.msgPack = msgPack;
1055
+
1056
+ //# sourceMappingURL=index.cjs.map