/** * @license * Copyright 2022-2024 Matter.js Authors * SPDX-License-Identifier: Apache-2.0 */ import { errorOf } from "./util/Error.js"; const inspect = Symbol.for("nodejs.util.inspect.custom"); /** * Error base class for all errors thrown by this library. */ export class MatterError extends Error { /** * Convert the error to formatted text. * * Matter encodes errors with modern JS features including {@link Error#cause} and {@link AggregateError#errors} * subfields. You can use this function to ensure all error details are presented regardless of environment. */ format(format: "plain" | "ansi" | "html" = "plain", indents = 0) { let formatterFor = MatterError.formatterFor; if (typeof formatterFor !== "function") { formatterFor = MatterError.defaultFormatterFactory; } let formatter = formatterFor(format); if (typeof formatter !== "function") { formatter = fallbackFormatter; } let result = formatter(this, indents); if (typeof result !== "string") { result = `${result}`; } return result as string; } /** * Node.js-style object inspection. * * Node's default inspection only prevents two levels of depth which may hide critical information. It's also * considerably more verbose than native matter.js formatting. We therefore offer this custom implementation. * * Note that this conforms to Node's API but is not dependent on Node. */ [inspect](depth: number, inspectionOptions?: { colors?: boolean }) { const formatterFor = MatterError.formatterFor; if (typeof formatterFor !== "function") { return this; } const format = formatterFor(inspectionOptions?.colors ? "ansi" : "plain"); if (typeof format !== "function") { return this; } return format(this, depth); } /** * Rethrow an error unless it is an instance of this class. */ static accept(this: new (...args: any[]) => T, error: unknown): asserts error is T { if (error instanceof this) { return; } throw error; } /** * Rethrow an error if it is an instance of this class. */ static reject(error: unknown): void { if (error instanceof this) { throw error; } } /** * The fallback formatter factory. This produces a limited plaintext formatter. */ static defaultFormatterFactory = () => fallbackFormatter; /** * The error formatter factory. The default formatter is replaced by Matter.js in ./Format.ts. */ static formatterFor: (formatName: string) => (value: unknown, indents?: number) => unknown = MatterError.defaultFormatterFactory; // TODO - this is probably correct; MatterAggregateError should be typeof MatterError. Need to diagnose some test // breakage before enabling though // static [Symbol.hasInstance](instance: unknown) { // if (instance instanceof MatterAggregateError) { // return true; // } // return Error[Symbol.hasInstance](instance); // } } /** * Error thrown when a Platform specific implementation was not added and so a provider (Network, Time, Crypto, etc) * is not available. */ export class NoProviderError extends MatterError {} /** * Error thrown when an internal error occurs like unexpected cases or missing data that should be there. Please * report such errors. */ export class InternalError extends MatterError {} /** Error thrown when a feature is not implemented yet. Please report such errors. */ export class NotImplementedError extends InternalError {} /** Error thrown when an unexpected case in the matter flow is encountered. Please report such errors. */ export class MatterFlowError extends MatterError {} /** Error thrown when an unexpected data is encountered. Please report such errors. */ export class UnexpectedDataError extends MatterError {} /** * Error thrown if most likely an implementation error is detected. Please check and correct your implementation and * provided data. if you are sure your code is correct please report the issue. */ export class ImplementationError extends MatterError {} /** * Thrown for write attempts against immutable data. */ export class ReadOnlyError extends ImplementationError { constructor(message = "This view is read-only") { super(message); } } /** * Thrown for errors that have multiple underlying causes. */ export class MatterAggregateError extends AggregateError { constructor(causes: Iterable, message?: string) { causes = [...causes].map(errorOf); super(causes, message); } [inspect] = MatterError.prototype[inspect]; format = MatterError.prototype.format; // TODO - see comment on MatterError. If that one is correct this is incorrect static [Symbol.hasInstance](instance: unknown) { if (instance instanceof MatterError) { return true; } return AggregateError[Symbol.hasInstance](instance); } } /** * It's never reasonable to fail to present error information so we include this rudimentary fallback error formatter. */ function fallbackFormatter(value: unknown, indents = 0) { if (value === undefined || value === null) { return `${value}`; } function formatOne(value: unknown, indents: number, messagePrefix: string) { const { message, stack, cause, errors } = value as { message?: unknown; stack?: unknown; cause?: unknown; errors?: unknown; }; let indent; if (typeof indents !== "number" || indents < 0) { indent = ""; } else { indent = " ".repeat(indents); } const buffer = [`${indent}${messagePrefix}${message ?? "(unknown error)"}`]; if (stack !== undefined && stack !== null) { const frames = stack.toString().split("\n"); frames.shift(); buffer.push(...frames.map(f => `${indent} ${f.trim()}`)); } if (cause !== undefined) { buffer.push(formatOne(cause, indents, "Caused by: ")); } if (typeof (errors as Iterable | undefined)?.[Symbol.iterator] === "function") { let causeNumber = 0; for (const error of errors as Iterable) { buffer.push(formatOne(error, indents + 1, `Cause #${causeNumber++}: `)); } } return buffer.join("\n"); } return formatOne(value, indents, ""); }