This commit is contained in:
Andras Schmelczer 2026-05-09 22:27:51 +01:00
commit 4e92913925
8 changed files with 734 additions and 115 deletions

View file

@ -4,12 +4,175 @@ export enum Severity {
ERROR = 'error',
}
export enum ErrorCode {
UNKNOWN = 'unknown',
WEBGPU_INSECURE_CONTEXT = 'webgpu-insecure-context',
WEBGPU_UNSUPPORTED = 'webgpu-unsupported',
WEBGPU_ADAPTER_UNAVAILABLE = 'webgpu-adapter-unavailable',
WEBGPU_DEVICE_UNAVAILABLE = 'webgpu-device-unavailable',
WEBGPU_CONTEXT_UNAVAILABLE = 'webgpu-context-unavailable',
WEBGPU_CONTEXT_CONFIGURATION_FAILED = 'webgpu-context-configuration-failed',
WEBGPU_UNCAPTURED_ERROR = 'webgpu-uncaptured-error',
WEBGPU_DEVICE_LOST = 'webgpu-device-lost',
}
type ErrorMetadataPrimitive = string | number | boolean | null;
export type ErrorMetadataValue =
| ErrorMetadataPrimitive
| Array<ErrorMetadataValue>
| { [key: string]: ErrorMetadataValue };
export type ErrorMetadata = { [key: string]: ErrorMetadataValue };
export interface RuntimeErrorOptions {
cause?: unknown;
details?: Record<string, unknown>;
}
export class RuntimeError extends Error {
public readonly code: ErrorCode | string;
public readonly details: ErrorMetadata;
public constructor(
code: ErrorCode | string,
message: string,
{ cause, details = {} }: RuntimeErrorOptions = {}
) {
super(message);
this.name = 'RuntimeError';
this.code = code;
this.details = serializeMetadataValue(details) as ErrorMetadata;
if (cause !== undefined) {
this.cause = cause;
}
}
}
export interface ErrorHandlerError {
severity: Severity;
message: string;
code?: ErrorCode | string;
details?: ErrorMetadata;
}
export type ErrorMetadata = { [key: string]: any };
export interface ErrorHandlerErrorOptions {
code?: ErrorCode | string;
details?: Record<string, unknown>;
}
export interface ErrorHandlerExceptionOptions extends ErrorHandlerErrorOptions {
fallbackMessage?: string;
severity?: Severity;
}
const MAX_METADATA_DEPTH = 4;
const UNREADABLE_VALUE = '[Unreadable]';
const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === 'object' && value !== null;
const safelyRead = (value: Record<string, unknown>, key: string): unknown => {
try {
return value[key];
} catch {
return undefined;
}
};
const isIterable = (value: unknown): value is Iterable<unknown> =>
isRecord(value) && Symbol.iterator in value;
const serializeMetadataValue = (value: unknown, depth = 0): ErrorMetadataValue => {
if (value === null) {
return null;
}
switch (typeof value) {
case 'string':
case 'boolean':
return value;
case 'number':
return Number.isFinite(value) ? value : value.toString();
case 'bigint':
return value.toString();
case 'undefined':
return null;
case 'symbol':
return value.toString();
case 'function':
return `[Function ${value.name || 'anonymous'}]`;
}
if (depth >= MAX_METADATA_DEPTH) {
return '[Object]';
}
if (Array.isArray(value)) {
return value.map((item) => serializeMetadataValue(item, depth + 1));
}
if (isIterable(value)) {
try {
return Array.from(value, (item) => serializeMetadataValue(item, depth + 1));
} catch {
return UNREADABLE_VALUE;
}
}
const serialized: ErrorMetadata = {};
const record = value as Record<string, unknown>;
for (const key of Object.keys(record)) {
try {
serialized[key] = serializeMetadataValue(record[key], depth + 1);
} catch {
serialized[key] = UNREADABLE_VALUE;
}
}
return serialized;
};
export const getErrorMessage = (
exception: unknown,
fallbackMessage = 'Unknown error'
): string => {
if (typeof exception === 'string') {
return exception || fallbackMessage;
}
if (exception instanceof Error) {
const record = exception as unknown as Record<string, unknown>;
const message = safelyRead(record, 'message');
if (typeof message === 'string' && message.length > 0) {
return message;
}
const name = safelyRead(record, 'name');
if (typeof name === 'string' && name.length > 0) {
return name;
}
return fallbackMessage;
}
if (isRecord(exception)) {
const message = safelyRead(exception, 'message');
if (typeof message === 'string' && message.length > 0) {
return message;
}
}
if (
typeof exception === 'number' ||
typeof exception === 'boolean' ||
typeof exception === 'bigint' ||
typeof exception === 'symbol'
) {
return exception.toString();
}
return fallbackMessage;
};
export class ErrorHandler {
private static readonly errors: Array<ErrorHandlerError> = [];
@ -18,23 +181,46 @@ export class ErrorHandler {
(error: ErrorHandlerError, metadata: ErrorMetadata) => void
> = [];
public static addException(exception: Error) {
ErrorHandler.addError(Severity.ERROR, exception.message);
public static addException(
exception: unknown,
{
severity = Severity.ERROR,
fallbackMessage,
code,
details,
}: ErrorHandlerExceptionOptions = {}
) {
const runtimeError = exception instanceof RuntimeError ? exception : undefined;
ErrorHandler.addError(severity, getErrorMessage(exception, fallbackMessage), {
code: code ?? runtimeError?.code,
details: {
...(runtimeError?.details ?? {}),
...(details ?? {}),
},
});
}
public static addError(severity: Severity, message: string) {
ErrorHandler.errors.push({ severity, message });
public static addError(
severity: Severity,
message: string,
{ code, details }: ErrorHandlerErrorOptions = {}
) {
const error: ErrorHandlerError = {
severity,
message,
...(code === undefined ? {} : { code }),
...(details === undefined
? {}
: { details: serializeMetadataValue(details) as ErrorMetadata }),
};
ErrorHandler.errors.push(error);
ErrorHandler.onErrorListeners.forEach((listener) =>
listener({ severity, message }, ErrorHandler.metadata)
listener(error, ErrorHandler.metadata)
);
}
public static addMetadata(key: string, value: any) {
const serialized: Record<string, any> = {};
for (const k in value) {
serialized[k] = value[k];
}
ErrorHandler.metadata[key] = serialized;
public static addMetadata(key: string, value: unknown) {
ErrorHandler.metadata[key] = serializeMetadataValue(value);
}
public static addOnErrorListener(