134 lines
4.4 KiB
TypeScript
134 lines
4.4 KiB
TypeScript
import type { SuaveplanErrorOptions } from "@suaveplan/error";
|
|
import { defineCodes, SuaveplanError } from "@suaveplan/error";
|
|
|
|
/**
|
|
* Stable error code registry for `@suaveplan/clipboard`. Every error path
|
|
* the package can take surfaces one of these codes so consumers (loggers,
|
|
* retry policies, UI surfaces, telemetry pipelines) can classify failures
|
|
* programmatically without parsing message strings.
|
|
*/
|
|
export const CLIPBOARD_CODES = defineCodes("CLIPBOARD", [
|
|
"API_UNAVAILABLE",
|
|
"PERMISSION_DENIED",
|
|
"FALLBACK_FAILED",
|
|
"INSECURE_CONTEXT",
|
|
"NO_CONTENT",
|
|
"READ_FAILED",
|
|
"DOCUMENT_UNAVAILABLE",
|
|
"QUEUE_DRAINED",
|
|
] as const);
|
|
|
|
/** Union of every error code emitted by `@suaveplan/clipboard`. */
|
|
export type ClipboardErrorCode =
|
|
(typeof CLIPBOARD_CODES)[keyof typeof CLIPBOARD_CODES];
|
|
|
|
/** Per-code metadata for HTTP / retry / severity classification. */
|
|
export interface ClipboardCodeMetadata {
|
|
readonly httpStatus: number;
|
|
readonly severity: "info" | "warning" | "error";
|
|
readonly retryable: boolean;
|
|
readonly description: string;
|
|
}
|
|
|
|
/** Mapping of every {@link ClipboardErrorCode} to its metadata. */
|
|
export const CLIPBOARD_ERROR_METADATA: Readonly<
|
|
Record<ClipboardErrorCode, ClipboardCodeMetadata>
|
|
> = Object.freeze({
|
|
[CLIPBOARD_CODES.API_UNAVAILABLE]: {
|
|
httpStatus: 501,
|
|
severity: "warning",
|
|
retryable: false,
|
|
description:
|
|
"navigator.clipboard is undefined; the host environment does not expose the async Clipboard API.",
|
|
},
|
|
[CLIPBOARD_CODES.PERMISSION_DENIED]: {
|
|
httpStatus: 403,
|
|
severity: "warning",
|
|
retryable: false,
|
|
description:
|
|
"The Permissions API reported state=denied for clipboard-read or clipboard-write.",
|
|
},
|
|
[CLIPBOARD_CODES.FALLBACK_FAILED]: {
|
|
httpStatus: 500,
|
|
severity: "error",
|
|
retryable: false,
|
|
description:
|
|
"document.execCommand('copy') returned false or threw during the textarea fallback.",
|
|
},
|
|
[CLIPBOARD_CODES.INSECURE_CONTEXT]: {
|
|
httpStatus: 400,
|
|
severity: "error",
|
|
retryable: false,
|
|
description:
|
|
"window.isSecureContext is false; clipboard operations require HTTPS or localhost when requireSecureContext is enabled.",
|
|
},
|
|
[CLIPBOARD_CODES.NO_CONTENT]: {
|
|
httpStatus: 400,
|
|
severity: "warning",
|
|
retryable: false,
|
|
description:
|
|
"copyRichContent was called with empty ClipboardData (no text, html, image, or blobs).",
|
|
},
|
|
[CLIPBOARD_CODES.READ_FAILED]: {
|
|
httpStatus: 500,
|
|
severity: "error",
|
|
retryable: true,
|
|
description:
|
|
"Clipboard read failed; the underlying API rejected or the paste-event fallback timed out.",
|
|
},
|
|
[CLIPBOARD_CODES.DOCUMENT_UNAVAILABLE]: {
|
|
httpStatus: 501,
|
|
severity: "error",
|
|
retryable: false,
|
|
description:
|
|
"document is undefined; the fallback path cannot run outside a DOM environment.",
|
|
},
|
|
[CLIPBOARD_CODES.QUEUE_DRAINED]: {
|
|
httpStatus: 503,
|
|
severity: "warning",
|
|
retryable: true,
|
|
description:
|
|
"ClipboardManager was disposed while a queued operation was still pending.",
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Base error class for every failure raised by `@suaveplan/clipboard`.
|
|
* Extends {@link SuaveplanError} so consumers receive cause-chain traversal,
|
|
* Zod-validated wire format, redaction hooks, and the OTel span-event
|
|
* adapter for free.
|
|
*/
|
|
export class ClipboardError extends SuaveplanError<ClipboardErrorCode> {
|
|
constructor(
|
|
code: ClipboardErrorCode,
|
|
message: string,
|
|
options?: SuaveplanErrorOptions
|
|
) {
|
|
super(code, message, options);
|
|
this.name = "ClipboardError";
|
|
}
|
|
|
|
/** Metadata for this error's code. */
|
|
get metadata(): ClipboardCodeMetadata {
|
|
return CLIPBOARD_ERROR_METADATA[this.code];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Lift an unknown thrown value into a {@link ClipboardError}. Used at every
|
|
* `catch` site so call sites can rely on the typed error contract.
|
|
*/
|
|
export function asClipboardError(
|
|
code: ClipboardErrorCode,
|
|
cause: unknown,
|
|
fallbackMessage: string
|
|
): ClipboardError {
|
|
const message =
|
|
cause instanceof Error && cause.message
|
|
? cause.message
|
|
: fallbackMessage;
|
|
const causeError =
|
|
cause instanceof Error ? cause : new Error(String(cause));
|
|
return new ClipboardError(code, message, { cause: causeError });
|
|
}
|