import type { SuaveplanErrorOptions } from "@suaveplan/error"; import { defineCodes, SuaveplanError } from "@suaveplan/error"; /** * Stable error-code registry for `@suaveplan/broadcast-channel`. Every error * thrown by the package surfaces one of these codes so downstream consumers * (loggers, problem+json responders, retry wrappers) can map to retry policy, * HTTP status, and i18n keys without re-implementing the classifier. */ export const BROADCAST_CHANNEL_CODES = defineCodes("BROADCAST_CHANNEL", [ "CHANNEL_CLOSED", "MESSAGE_MALFORMED", "PAYLOAD_TOO_LARGE", "STORAGE_FAILED", "ASK_TIMEOUT", "ASK_NO_RESPONDER", "ASK_HANDLER_DUPLICATE", ] as const); /** Union of every fully-qualified code emitted by this package. */ export type BroadcastChannelErrorCode = (typeof BROADCAST_CHANNEL_CODES)[keyof typeof BROADCAST_CHANNEL_CODES]; /** Base class for every typed broadcast-channel error. */ export abstract class BroadcastChannelError extends SuaveplanError {} interface BaseOpts extends SuaveplanErrorOptions { readonly message?: string; } function pickOpts(opts: BaseOpts): SuaveplanErrorOptions { const out: { cause?: unknown; context?: Readonly>; traceId?: string; } = {}; if (opts.cause !== undefined) out.cause = opts.cause; if (opts.context !== undefined) out.context = opts.context; if (opts.traceId !== undefined) out.traceId = opts.traceId; return out; } /** Send attempted on a channel that has not been opened or has been closed. */ export class BroadcastChannelClosedError extends BroadcastChannelError { public constructor(opts: BaseOpts = {}) { super( BROADCAST_CHANNEL_CODES.CHANNEL_CLOSED, opts.message ?? "broadcast-channel.errors.channelClosed", pickOpts(opts) ); } } /** An inbound frame failed `ChannelMessageSchema` validation. */ export class BroadcastChannelMessageMalformedError extends BroadcastChannelError { public constructor(opts: BaseOpts = {}) { super( BROADCAST_CHANNEL_CODES.MESSAGE_MALFORMED, opts.message ?? "broadcast-channel.errors.messageMalformed", pickOpts(opts) ); } } /** Outbound payload exceeded the configured `maxMessageBytes` budget. */ export class BroadcastChannelPayloadTooLargeError extends BroadcastChannelError { public constructor( opts: BaseOpts & { readonly sizeBytes?: number; readonly maxBytes?: number; } = {} ) { const ctx: Record = { ...(opts.context ?? {}) }; if (opts.sizeBytes !== undefined) ctx["sizeBytes"] = opts.sizeBytes; if (opts.maxBytes !== undefined) ctx["maxBytes"] = opts.maxBytes; super( BROADCAST_CHANNEL_CODES.PAYLOAD_TOO_LARGE, opts.message ?? "broadcast-channel.errors.payloadTooLarge", pickOpts({ ...opts, context: ctx }) ); } } /** Injected `StorageLike` rejected a get/set/list call in the fallback transport. */ export class BroadcastChannelStorageFailedError extends BroadcastChannelError { public constructor(opts: BaseOpts = {}) { super( BROADCAST_CHANNEL_CODES.STORAGE_FAILED, opts.message ?? "broadcast-channel.errors.storageFailed", pickOpts(opts) ); } } /** Pending `ask()` timed out before any peer responded. */ export class BroadcastChannelAskTimeoutError extends BroadcastChannelError { public constructor(opts: BaseOpts = {}) { super( BROADCAST_CHANNEL_CODES.ASK_TIMEOUT, opts.message ?? "broadcast-channel.errors.askTimeout", pickOpts(opts) ); } } /** * `ask()` was called with `requireResponder: true` and timed out without any * peer claiming the handler key. Distinct from `ASK_TIMEOUT` so callers can * drive different retry policies for "no listener" vs. "slow listener". */ export class BroadcastChannelAskNoResponderError extends BroadcastChannelError { public constructor(opts: BaseOpts = {}) { super( BROADCAST_CHANNEL_CODES.ASK_NO_RESPONDER, opts.message ?? "broadcast-channel.errors.askNoResponder", pickOpts(opts) ); } } /** `onAsk()` called twice for the same `handlerKey` without disposal in between. */ export class BroadcastChannelAskHandlerDuplicateError extends BroadcastChannelError { public constructor(opts: BaseOpts = {}) { super( BROADCAST_CHANNEL_CODES.ASK_HANDLER_DUPLICATE, opts.message ?? "broadcast-channel.errors.askHandlerDuplicate", pickOpts(opts) ); } }