/** * Structured errors, Node.js-style: a plain `Error` with a semver-stable * `code` property (branch on it, like `err.code === 'ENOENT'`) and optional * JSON-serializable `details` — `details.reason` carries the fine grain. * Messages are for humans and may change in any release — never match on * them. No classes, no instanceof — a code check works on any copy of any * package. `PolotnoErrorDetailsMap` below is the canonical list; the repo root * CLAUDE.md states the rules around changing it. * * `details` is exhaustively typed per code (and, where a code carries a * `reason`, per reason) via `PolotnoErrorDetailsMap`: a caught `PolotnoError` * is a discriminated union, so `if (err.code === 'IMAGE_FAILED')` narrows * `err.details` to exactly the fields that code can carry, and a further * `err.details?.reason === 'unsupported-format'` narrows to that reason's * field set. The factory input is typed the same way, so throw sites can't * drift from the declared shapes. * * This is the one shared factory: each package declares its own (narrower) * `PolotnoErrorCode` union and re-exports a typed alias of `polotnoError`, * so its public surface documents exactly what it can throw. */ /** Canonical codes across all Polotno packages. Append-only. */ export type PolotnoErrorCode = 'DESIGN_INVALID' | 'EXPORT_FAILED' | 'IMAGE_FAILED' | 'FONT_FAILED' | 'VIDEO_FAILED' | 'FETCH_FAILED' | 'IMPORT_FAILED' | 'UNKNOWN'; /** * Fine-grained `details.reason` for the font domain — used by `fontFailed`'s * signature (`fonts/errors.ts`). The other codes discriminate their reasons * inline in `PolotnoErrorDetailsMap` below. Append-only. */ export type FontFailedReason = 'not-found' | 'fetch-failed' | 'decode-failed' | 'timeout'; /** * Transport-level classification the font fetcher attaches to `FETCH_FAILED` * (and that survives, as `status`/`attempts`, when a fetch failure is coerced * into an `IMAGE_FAILED`). Plain (non-font) fetchers throw `FETCH_FAILED` * without a `reason`. */ export type FetchFailedReason = 'http-client-error' | 'timeout' | 'network' | 'throttled-non-font' | 'rate-limited' | 'server-error'; export type ImportFailedFormat = 'psd' | 'svg' | 'pdf'; /** * Element-render context merged onto any error thrown while rendering a * specific element (see `rethrowWithRenderContext`). Present on the codes that * can surface during element/page render. */ export interface RenderContext { label?: string; elementId?: string; elementType?: string; } /** * The exact `details` shape carried by each code. The read type * (`PolotnoError`) and the factory input both key into this map, so it is the * single source of truth — a throw site that passes a field not listed here * is a type error, and a consumer sees exactly these fields after narrowing * on `code` (then `details.reason`). */ export interface PolotnoErrorDetailsMap { DESIGN_INVALID: { errors: { path: string; message: string; }[]; /** Recognised element types, when the design references an unknown one. */ knownTypes?: string[]; }; EXPORT_FAILED: ({ reason: 'page-not-found'; pageId?: string; availablePageIds?: string[]; } | { reason: 'workspace-not-mounted'; pageId?: string; } | { reason: 'unmounted-during-export'; pageId?: string; } | { reason: 'invalid-options'; option: string; value?: string | number; } | { reason: 'element'; } | { reason: 'busy'; } | { reason: 'pdfx'; }) & RenderContext; IMAGE_FAILED: ({ reason: 'load'; src: string; } | { reason: 'unsupported-format'; supported: string[]; mimeType?: string; src?: string; } | { reason: 'decode-failed'; src: string; mimeType?: string; } | { reason: 'invalid-data-url'; } | { reason: 'fetch-failed'; url?: string; status?: number; attempts?: number; } | { reason: 'timeout'; label: string; timeoutMs: number; }) & RenderContext; FONT_FAILED: ({ reason: 'not-found' | 'timeout'; family: string; } | { reason: 'fetch-failed' | 'decode-failed'; family: string; src?: string; }) & RenderContext; VIDEO_FAILED: ({ reason: 'load'; src: string; } | { reason: 'no-video-track' | 'unsupported-codec' | 'cannot-decode' | 'no-encoder' | 'zero-duration' | 'invalid-dimensions'; } | { reason: 'timeout'; label: string; timeoutMs: number; }) & RenderContext; FETCH_FAILED: { url: string; status?: number; attempts?: number; /** Transport classification — only the font fetcher sets this. */ reason?: FetchFailedReason; }; IMPORT_FAILED: { format: ImportFailedFormat; /** * Encryption classification — only pdf-import sets this. Both values mean * the file is intact, so a caller prompts for a password and imports * again. Absent means the bytes are not a readable document. */ reason?: 'password-required' | 'password-incorrect'; }; /** * The deliberate catch-all: an error with no structured classification (e.g. * a legacy string payload, or a loader timeout on a non-image/video asset). * Left open by design — `UNKNOWN` means "we don't know the shape". */ UNKNOWN: Record | undefined; } /** * A caught Polotno error. Distributes over `Code`, so the default (all codes) * is a discriminated union keyed on `code` and each package's narrowed * `CorePolotnoError` is the union of just its members — `details` * typed exactly per code via {@link PolotnoErrorDetailsMap}. */ export type PolotnoError = { [K in Code]: Error & { code: K; details?: PolotnoErrorDetailsMap[K]; cause?: unknown; }; }[Code]; export declare function polotnoError(code: Code, message: string, details?: PolotnoErrorDetailsMap[Code], cause?: unknown): PolotnoError; /** * The signature a package's narrowing `polotnoError` alias casts to: the * shared factory restricted to that package's own `Codes` union, with * `details` still typed per code. Each package re-exports * `basePolotnoError as TypedPolotnoError` — one line instead of * repeating the whole generic signature (and its unchecked cast) per package. */ export type TypedPolotnoError = (code: Code, message: string, details?: PolotnoErrorDetailsMap[Code], cause?: unknown) => PolotnoError; /** * Re-throw with rendering context prepended to the message. A specific code * on the inner error (e.g. IMAGE_FAILED) survives the wrap — including its * `details.reason` — so callers can branch on the root cause; errors without * a code become EXPORT_FAILED with `details.reason: 'element'`. The original * error is chained as `cause`. */ export declare function rethrowWithRenderContext(err: unknown, message: string, context: RenderContext): never; /** * Network failures inside the image pipeline read as IMAGE_FAILED/ * 'fetch-failed' — the domain code wins over the transport code (same * policy as fonts). Non-fetch errors pass through unchanged. Applied at * the image-domain boundaries (element render, page background), which * cover every acquisition path — the shared byte fetchers also serve * fonts/css and must stay domain-neutral. */ export declare function coerceFetchToImageFailure(err: unknown): unknown;