You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

196 lines
5.6 KiB

import {
type ErrCode,
isEqualOrDescendantOf,
isModuleSeed,
moduleOf,
type ModuleSeed
} from './code.ts';
/**
* Metadata accumulated on an error as it travels through layers. Each
* `with*` decorator adds one piece of metadata without changing the
* error's identity (the `code`).
*
* All fields are optional — a freshly thrown `CodeError` carries only
* `code` and `message` until upper layers decorate it.
*/
export interface CodeErrorMeta {
/**
* User-facing message. Either a literal string or a `LangRef`
* (e.g. `'#?common.action_failed'`) that the presentation layer
* resolves through `App.Lang.t(...)`. Distinct from `Error.message`,
* which is the developer/log-facing message.
*/
readonly userMessage?: string;
/** Wall-clock timestamp when the error was decorated. */
readonly timestamp?: number;
/** Distributed trace identifier. */
readonly requestId?: string;
/** Tenant under which the error occurred (multi-tenant deployments). */
readonly tenantId?: string;
/** User affected by the error. */
readonly userId?: string;
/** Free-form structured data. Escape hatch for cases not covered above. */
readonly data?: Readonly<Record<string, unknown>>;
}
/**
* Single canonical error class for the framework. Carries an
* `ErrCode` (its identity), an optional dev-facing `message`, an
* optional `cause`, and a `meta` bag of contextual fields populated
* via the fluent `with*` decorators.
*
* Decorators are immutable — each `withX` returns a new `CodeError`
* with the same code and a clone of `meta` plus the new field. The
* `code` never changes through decoration, so the family-match
* predicate (`matches(err, family)`) keeps working at every layer.
*/
export class CodeError extends Error {
readonly code: ErrCode;
readonly meta: CodeErrorMeta;
constructor(
code: ErrCode,
init: {
readonly message?: string;
readonly cause?: unknown;
readonly meta?: CodeErrorMeta;
} = {}
) {
super(init.message ?? code, { cause: init.cause });
this.name = code;
this.code = code;
this.meta = Object.freeze({ ...init.meta });
}
// ── Fluent decorators ─────────────────────────────────────────────
withMessage(userMessage: string): CodeError {
return this.cloneWith({ userMessage });
}
withTime(timestamp: number = Date.now()): CodeError {
return this.cloneWith({ timestamp });
}
withRequestId(requestId: string): CodeError {
return this.cloneWith({ requestId });
}
withTenant(tenantId: string): CodeError {
return this.cloneWith({ tenantId });
}
withUser(userId: string): CodeError {
return this.cloneWith({ userId });
}
withData(data: Readonly<Record<string, unknown>>): CodeError {
return this.cloneWith({ data });
}
/** Generic decorator — add any subset of `CodeErrorMeta`. */
with(meta: Partial<CodeErrorMeta>): CodeError {
return this.cloneWith(meta);
}
// ── Convenience accessors ─────────────────────────────────────────
get module(): string {
return moduleOf(this.code);
}
get userMessage(): string | undefined {
return this.meta.userMessage;
}
get timestamp(): number | undefined {
return this.meta.timestamp;
}
get requestId(): string | undefined {
return this.meta.requestId;
}
get tenantId(): string | undefined {
return this.meta.tenantId;
}
get userId(): string | undefined {
return this.meta.userId;
}
// ── Serialization ─────────────────────────────────────────────────
toJSON(): {
readonly name: string;
readonly code: string;
readonly message: string;
readonly meta: CodeErrorMeta;
} {
return {
name: this.name,
code: this.code,
message: this.message,
meta: this.meta
};
}
// ── Internals ─────────────────────────────────────────────────────
private cloneWith(patch: Partial<CodeErrorMeta>): CodeError {
return new CodeError(this.code, {
message: this.message,
cause: this.cause,
meta: { ...this.meta, ...patch }
});
}
}
/**
* Type guard for `CodeError` (and its subclasses, since `instanceof`
* walks the prototype chain). Useful at error boundaries:
*
* ```ts
* catch (err) {
* if (isCodeError(err)) Logger.error(err.module, err.code, err.meta);
* else throw err;
* }
* ```
*/
export function isCodeError(value: unknown): value is CodeError {
return value instanceof CodeError;
}
/**
* `true` when `value` is a `CodeError` whose `code` matches `family`.
* `family` can be:
*
* - A `ModuleSeed` (e.g. `BUSS_ERR = 'buss::'`) — matches any code
* declared by that module.
* - An `ErrCode` — matches the code itself or any hierarchical
* descendant within the same module.
*
* ```ts
* matches(err, BUSS_ERR_DISPOSED) // exact code identity
* matches(err, BUSS_ERR_LISTENER) // any descendant of buss::listener
* matches(err, BUSS_ERR) // any error declared by buss (the seed)
* ```
*
* Boundary check on `ErrCode` (alphanumeric-only segments + `.`
* separator) prevents false positives like `'buss::listener_extra'`
* matching the family `'buss::listener'`.
*/
export function matches(value: unknown, family: ErrCode | ModuleSeed): boolean {
if (!isCodeError(value)) return false;
if (isModuleSeed(family)) {
return value.code.startsWith(family);
}
return isEqualOrDescendantOf(value.code, family);
}

Powered by TurnKey Linux.