LGTM
This commit is contained in:
parent
b1acdff594
commit
4e92913925
8 changed files with 734 additions and 115 deletions
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in a new issue