summaryrefslogtreecommitdiff
path: root/ext/js/core.js
diff options
context:
space:
mode:
Diffstat (limited to 'ext/js/core.js')
-rw-r--r--ext/js/core.js345
1 files changed, 173 insertions, 172 deletions
diff --git a/ext/js/core.js b/ext/js/core.js
index fb164795..5c03b44b 100644
--- a/ext/js/core.js
+++ b/ext/js/core.js
@@ -16,54 +16,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-/**
- * Converts an `Error` object to a serializable JSON object.
- * @param {*} error An error object to convert.
- * @returns {{name: string, message: string, stack: string, data?: *}|{value: *, hasValue: boolean}} A simple object which can be serialized by `JSON.stringify()`.
- */
-export function serializeError(error) {
- try {
- if (typeof error === 'object' && error !== null) {
- const result = {
- name: error.name,
- message: error.message,
- stack: error.stack
- };
- if (Object.prototype.hasOwnProperty.call(error, 'data')) {
- result.data = error.data;
- }
- return result;
- }
- } catch (e) {
- // NOP
- }
- return {
- value: error,
- hasValue: true
- };
-}
-
-/**
- * Converts a serialized erorr into a standard `Error` object.
- * @param {{name: string, message: string, stack: string, data?: *}|{value: *, hasValue: boolean}} serializedError A simple object which was initially generated by serializeError.
- * @returns {Error|*} A new `Error` instance.
- */
-export function deserializeError(serializedError) {
- if (serializedError.hasValue) {
- return serializedError.value;
- }
- const error = new Error(serializedError.message);
- error.name = serializedError.name;
- error.stack = serializedError.stack;
- if (Object.prototype.hasOwnProperty.call(serializedError, 'data')) {
- error.data = serializedError.data;
- }
- return error;
-}
+import {ExtensionError} from './core/extension-error.js';
/**
* Checks whether a given value is a non-array object.
- * @param {*} value The value to check.
+ * @param {unknown} value The value to check.
* @returns {boolean} `true` if the value is an object and not an array, `false` otherwise.
*/
export function isObject(value) {
@@ -91,14 +48,20 @@ export function stringReverse(string) {
/**
* Creates a deep clone of an object or value. This is similar to `JSON.parse(JSON.stringify(value))`.
- * @param {*} value The value to clone.
- * @returns {*} A new clone of the value.
+ * @template T
+ * @param {T} value The value to clone.
+ * @returns {T} A new clone of the value.
* @throws An error if the value is circular and cannot be cloned.
*/
export const clone = (() => {
- // eslint-disable-next-line no-shadow
+ /**
+ * @template T
+ * @param {T} value
+ * @returns {T}
+ */
+ // eslint-disable-next-line no-shadow, @typescript-eslint/no-shadow
function clone(value) {
- if (value === null) { return null; }
+ if (value === null) { return /** @type {T} */ (null); }
switch (typeof value) {
case 'boolean':
case 'number':
@@ -112,8 +75,15 @@ export const clone = (() => {
}
}
+ /**
+ * @template [T=unknown]
+ * @param {T} value
+ * @param {Set<unknown>} visited
+ * @returns {T}
+ * @throws {Error}
+ */
function cloneInternal(value, visited) {
- if (value === null) { return null; }
+ if (value === null) { return /** @type {T} */ (null); }
switch (typeof value) {
case 'boolean':
case 'number':
@@ -122,13 +92,23 @@ export const clone = (() => {
case 'symbol':
case 'undefined':
return value;
- case 'function':
- return cloneObject(value, visited);
case 'object':
- return Array.isArray(value) ? cloneArray(value, visited) : cloneObject(value, visited);
+ return /** @type {T} */ (
+ Array.isArray(value) ?
+ cloneArray(value, visited) :
+ cloneObject(/** @type {import('core').SerializableObject} */ (value), visited)
+ );
+ default:
+ throw new Error(`Cannot clone object of type ${typeof value}`);
}
}
+ /**
+ * @param {unknown[]} value
+ * @param {Set<unknown>} visited
+ * @returns {unknown[]}
+ * @throws {Error}
+ */
function cloneArray(value, visited) {
if (visited.has(value)) { throw new Error('Circular'); }
try {
@@ -143,10 +123,17 @@ export const clone = (() => {
}
}
+ /**
+ * @param {import('core').SerializableObject} value
+ * @param {Set<unknown>} visited
+ * @returns {import('core').SerializableObject}
+ * @throws {Error}
+ */
function cloneObject(value, visited) {
if (visited.has(value)) { throw new Error('Circular'); }
try {
visited.add(value);
+ /** @type {import('core').SerializableObject} */
const result = {};
for (const key in value) {
if (Object.prototype.hasOwnProperty.call(value, key)) {
@@ -164,12 +151,17 @@ export const clone = (() => {
/**
* Checks if an object or value is deeply equal to another object or value.
- * @param {*} value1 The first value to check.
- * @param {*} value2 The second value to check.
+ * @param {unknown} value1 The first value to check.
+ * @param {unknown} value2 The second value to check.
* @returns {boolean} `true` if the values are the same object, or deeply equal without cycles. `false` otherwise.
*/
export const deepEqual = (() => {
- // eslint-disable-next-line no-shadow
+ /**
+ * @param {unknown} value1
+ * @param {unknown} value2
+ * @returns {boolean}
+ */
+ // eslint-disable-next-line no-shadow, @typescript-eslint/no-shadow
function deepEqual(value1, value2) {
if (value1 === value2) { return true; }
@@ -185,6 +177,12 @@ export const deepEqual = (() => {
}
}
+ /**
+ * @param {unknown} value1
+ * @param {unknown} value2
+ * @param {Set<unknown>} visited1
+ * @returns {boolean}
+ */
function deepEqualInternal(value1, value2, visited1) {
if (value1 === value2) { return true; }
@@ -200,13 +198,23 @@ export const deepEqual = (() => {
if (array !== Array.isArray(value2)) { return false; }
if (visited1.has(value1)) { return false; }
visited1.add(value1);
- return array ? areArraysEqual(value1, value2, visited1) : areObjectsEqual(value1, value2, visited1);
+ return (
+ array ?
+ areArraysEqual(/** @type {unknown[]} */ (value1), /** @type {unknown[]} */ (value2), visited1) :
+ areObjectsEqual(/** @type {import('core').UnknownObject} */ (value1), /** @type {import('core').UnknownObject} */ (value2), visited1)
+ );
}
default:
return false;
}
}
+ /**
+ * @param {import('core').UnknownObject} value1
+ * @param {import('core').UnknownObject} value2
+ * @param {Set<unknown>} visited1
+ * @returns {boolean}
+ */
function areObjectsEqual(value1, value2, visited1) {
const keys1 = Object.keys(value1);
const keys2 = Object.keys(value2);
@@ -220,6 +228,12 @@ export const deepEqual = (() => {
return true;
}
+ /**
+ * @param {unknown[]} value1
+ * @param {unknown[]} value2
+ * @param {Set<unknown>} visited1
+ * @returns {boolean}
+ */
function areArraysEqual(value1, value2, visited1) {
const length = value1.length;
if (length !== value2.length) { return false; }
@@ -251,76 +265,55 @@ export function generateId(length) {
/**
* Creates an unresolved promise that can be resolved later, outside the promise's executor function.
- * @returns {{promise: Promise, resolve: Function, reject: Function}} An object `{promise, resolve, reject}`, containing the promise and the resolve/reject functions.
+ * @template T
+ * @returns {import('core').DeferredPromiseDetails<T>} An object `{promise, resolve, reject}`, containing the promise and the resolve/reject functions.
*/
export function deferPromise() {
+ /** @type {((value: T) => void)|undefined} */
let resolve;
+ /** @type {((reason?: import('core').RejectionReason) => void)|undefined} */
let reject;
const promise = new Promise((resolve2, reject2) => {
resolve = resolve2;
reject = reject2;
});
- return {promise, resolve, reject};
+ return {
+ promise,
+ resolve: /** @type {(value: T) => void} */ (resolve),
+ reject: /** @type {(reason?: import('core').RejectionReason) => void} */ (reject)
+ };
}
/**
* Creates a promise that is resolved after a set delay.
* @param {number} delay How many milliseconds until the promise should be resolved. If 0, the promise is immediately resolved.
- * @param {*} [resolveValue] The value returned when the promise is resolved.
- * @returns {Promise} A promise with two additional properties: `resolve` and `reject`, which can be used to complete the promise early.
+ * @returns {Promise<void>} A promise with two additional properties: `resolve` and `reject`, which can be used to complete the promise early.
*/
-export function promiseTimeout(delay, resolveValue) {
- if (delay <= 0) {
- const promise = Promise.resolve(resolveValue);
- promise.resolve = () => {}; // NOP
- promise.reject = () => {}; // NOP
- return promise;
- }
-
- let timer = null;
- let {promise, resolve, reject} = deferPromise();
-
- const complete = (callback, value) => {
- if (callback === null) { return; }
- if (timer !== null) {
- clearTimeout(timer);
- timer = null;
- }
- resolve = null;
- reject = null;
- callback(value);
- };
-
- const resolveWrapper = (value) => complete(resolve, value);
- const rejectWrapper = (value) => complete(reject, value);
-
- timer = setTimeout(() => {
- timer = null;
- resolveWrapper(resolveValue);
- }, delay);
-
- promise.resolve = resolveWrapper;
- promise.reject = rejectWrapper;
-
- return promise;
+export function promiseTimeout(delay) {
+ return delay <= 0 ? Promise.resolve() : new Promise((resolve) => { setTimeout(resolve, delay); });
}
/**
* Creates a promise that will resolve after the next animation frame, using `requestAnimationFrame`.
* @param {number} [timeout] A maximum duration (in milliseconds) to wait until the promise resolves. If null or omitted, no timeout is used.
- * @returns {Promise<{time: number, timeout: number}>} A promise that is resolved with `{time, timeout}`, where `time` is the timestamp from `requestAnimationFrame`,
+ * @returns {Promise<{time: number, timeout: boolean}>} A promise that is resolved with `{time, timeout}`, where `time` is the timestamp from `requestAnimationFrame`,
* and `timeout` is a boolean indicating whether the cause was a timeout or not.
* @throws The promise throws an error if animation is not supported in this context, such as in a service worker.
*/
-export function promiseAnimationFrame(timeout=null) {
+export function promiseAnimationFrame(timeout) {
return new Promise((resolve, reject) => {
if (typeof cancelAnimationFrame !== 'function' || typeof requestAnimationFrame !== 'function') {
reject(new Error('Animation not supported in this context'));
return;
}
+ /** @type {?import('core').Timeout} */
let timer = null;
+ /** @type {?number} */
let frameRequest = null;
+ /**
+ * @param {number} time
+ */
const onFrame = (time) => {
frameRequest = null;
if (timer !== null) {
@@ -350,14 +343,12 @@ export function promiseAnimationFrame(timeout=null) {
/**
* Invokes a standard message handler. This function is used to react and respond
* to communication messages within the extension.
- * @param {object} details Details about how to handle messages.
- * @param {Function} details.handler A handler function which is passed `params` and `...extraArgs` as arguments.
- * @param {boolean|string} details.async Whether or not the handler is async or not. Values include `false`, `true`, or `'dynamic'`.
- * When the value is `'dynamic'`, the handler should return an object of the format `{async: boolean, result: any}`.
- * @param {object} params Information which was passed with the original message.
- * @param {Function} callback A callback function which is invoked after the handler has completed. The value passed
+ * @template {import('core').SafeAny} TParams
+ * @param {import('core').MessageHandlerDetails} details Details about how to handle messages.
+ * @param {TParams} params Information which was passed with the original message.
+ * @param {(response: import('core').Response) => void} callback A callback function which is invoked after the handler has completed. The value passed
* to the function is in the format:
- * - `{result: any}` if the handler invoked successfully.
+ * - `{result: unknown}` if the handler invoked successfully.
* - `{error: object}` if the handler thew an error. The error is serialized.
* @param {...*} extraArgs Additional arguments which are passed to the `handler` function.
* @returns {boolean} `true` if the function is invoked asynchronously, `false` otherwise.
@@ -369,9 +360,9 @@ export function invokeMessageHandler({handler, async}, params, callback, ...extr
({async, result: promiseOrResult} = promiseOrResult);
}
if (async) {
- promiseOrResult.then(
+ /** @type {Promise<any>} */ (promiseOrResult).then(
(result) => { callback({result}); },
- (error) => { callback({error: serializeError(error)}); }
+ (error) => { callback({error: ExtensionError.serialize(error)}); }
);
return true;
} else {
@@ -379,12 +370,13 @@ export function invokeMessageHandler({handler, async}, params, callback, ...extr
return false;
}
} catch (error) {
- callback({error: serializeError(error)});
+ callback({error: ExtensionError.serialize(error)});
return false;
}
}
/**
+ * @template {string} TEventName
* Base class controls basic event dispatching.
*/
export class EventDispatcher {
@@ -392,13 +384,14 @@ export class EventDispatcher {
* Creates a new instance.
*/
constructor() {
+ /** @type {Map<string, ((details: import('core').SafeAny) => void)[]>} */
this._eventMap = new Map();
}
/**
* Triggers an event with the given name and specified argument.
- * @param {string} eventName The string representing the event's name.
- * @param {*} [details] The argument passed to the callback functions.
+ * @param {TEventName} eventName The string representing the event's name.
+ * @param {unknown} [details] The argument passed to the callback functions.
* @returns {boolean} `true` if any callbacks were registered, `false` otherwise.
*/
trigger(eventName, details) {
@@ -413,8 +406,8 @@ export class EventDispatcher {
/**
* Adds a single event listener to a specific event.
- * @param {string} eventName The string representing the event's name.
- * @param {Function} callback The event listener callback to add.
+ * @param {TEventName} eventName The string representing the event's name.
+ * @param {(details: import('core').SafeAny) => void} callback The event listener callback to add.
*/
on(eventName, callback) {
let callbacks = this._eventMap.get(eventName);
@@ -427,8 +420,8 @@ export class EventDispatcher {
/**
* Removes a single event listener from a specific event.
- * @param {string} eventName The string representing the event's name.
- * @param {Function} callback The event listener callback to add.
+ * @param {TEventName} eventName The string representing the event's name.
+ * @param {(details: import('core').SafeAny) => void} callback The event listener callback to add.
* @returns {boolean} `true` if the callback was removed, `false` otherwise.
*/
off(eventName, callback) {
@@ -450,7 +443,7 @@ export class EventDispatcher {
/**
* Checks if an event has any listeners.
- * @param {string} eventName The string representing the event's name.
+ * @param {TEventName} eventName The string representing the event's name.
* @returns {boolean} `true` if the event has listeners, `false` otherwise.
*/
hasListeners(eventName) {
@@ -467,6 +460,7 @@ export class EventListenerCollection {
* Creates a new instance.
*/
constructor() {
+ /** @type {import('event-listener-collection').EventListenerDetails[]} */
this._eventListeners = [];
}
@@ -479,50 +473,40 @@ export class EventListenerCollection {
}
/**
- * Adds an event listener of a generic type.
- * @param {string} type The type of event listener, which can be 'addEventListener', 'addListener', or 'on'.
- * @param {object} object The object to add the event listener to.
- * @param {...*} args The argument array passed to the object's event listener adding function.
- * @returns {void}
- * @throws An error if type is not an expected value.
- */
- addGeneric(type, object, ...args) {
- switch (type) {
- case 'addEventListener': return this.addEventListener(object, ...args);
- case 'addListener': return this.addListener(object, ...args);
- case 'on': return this.on(object, ...args);
- default: throw new Error(`Invalid type: ${type}`);
- }
- }
-
- /**
* Adds an event listener using `object.addEventListener`. The listener will later be removed using `object.removeEventListener`.
- * @param {object} object The object to add the event listener to.
- * @param {...*} args The argument array passed to the `addEventListener`/`removeEventListener` functions.
+ * @param {import('event-listener-collection').EventTarget} target The object to add the event listener to.
+ * @param {string} type The name of the event.
+ * @param {EventListener | EventListenerObject | import('event-listener-collection').EventListenerFunction} listener The callback listener.
+ * @param {AddEventListenerOptions | boolean} [options] Options for the event.
*/
- addEventListener(object, ...args) {
- object.addEventListener(...args);
- this._eventListeners.push(['removeEventListener', object, ...args]);
+ addEventListener(target, type, listener, options) {
+ target.addEventListener(type, listener, options);
+ this._eventListeners.push({type: 'removeEventListener', target, eventName: type, listener, options});
}
/**
* Adds an event listener using `object.addListener`. The listener will later be removed using `object.removeListener`.
- * @param {object} object The object to add the event listener to.
- * @param {...*} args The argument array passed to the `addListener`/`removeListener` function.
+ * @template {import('event-listener-collection').EventListenerFunction} TCallback
+ * @template [TArgs=unknown]
+ * @param {import('event-listener-collection').ExtensionEvent<TCallback, TArgs>} target The object to add the event listener to.
+ * @param {TCallback} callback The callback.
+ * @param {TArgs[]} args The extra argument array passed to the `addListener`/`removeListener` function.
*/
- addListener(object, ...args) {
- object.addListener(...args);
- this._eventListeners.push(['removeListener', object, ...args]);
+ addListener(target, callback, ...args) {
+ target.addListener(callback, ...args);
+ this._eventListeners.push({type: 'removeListener', target, callback, args});
}
/**
* Adds an event listener using `object.on`. The listener will later be removed using `object.off`.
- * @param {object} object The object to add the event listener to.
- * @param {...*} args The argument array passed to the `on`/`off` function.
+ * @template {string} TEventName
+ * @param {EventDispatcher<TEventName>} target The object to add the event listener to.
+ * @param {TEventName} eventName The string representing the event's name.
+ * @param {(details: import('core').SafeAny) => void} callback The event listener callback to add.
*/
- on(object, ...args) {
- object.on(...args);
- this._eventListeners.push(['off', object, ...args]);
+ on(target, eventName, callback) {
+ target.on(eventName, callback);
+ this._eventListeners.push({type: 'off', eventName, target, callback});
}
/**
@@ -530,16 +514,16 @@ export class EventListenerCollection {
*/
removeAllEventListeners() {
if (this._eventListeners.length === 0) { return; }
- for (const [removeFunctionName, object, ...args] of this._eventListeners) {
- switch (removeFunctionName) {
+ for (const item of this._eventListeners) {
+ switch (item.type) {
case 'removeEventListener':
- object.removeEventListener(...args);
+ item.target.removeEventListener(item.eventName, item.listener, item.options);
break;
case 'removeListener':
- object.removeListener(...args);
+ item.target.removeListener(item.callback, ...item.args);
break;
case 'off':
- object.off(...args);
+ item.target.off(item.eventName, item.callback);
break;
}
}
@@ -550,23 +534,28 @@ export class EventListenerCollection {
/**
* Class representing a generic value with an override stack.
* Changes can be observed by listening to the 'change' event.
+ * @template T
+ * @augments EventDispatcher<import('dynamic-property').EventType>
*/
export class DynamicProperty extends EventDispatcher {
/**
* Creates a new instance with the specified value.
- * @param {*} value The value to assign.
+ * @param {T} value The value to assign.
*/
constructor(value) {
super();
+ /** @type {T} */
this._value = value;
+ /** @type {T} */
this._defaultValue = value;
+ /** @type {{value: T, priority: number, token: string}[]} */
this._overrides = [];
}
/**
* Gets the default value for the property, which is assigned to the
* public value property when no overrides are present.
- * @type {*}
+ * @type {T}
*/
get defaultValue() {
return this._defaultValue;
@@ -576,7 +565,7 @@ export class DynamicProperty extends EventDispatcher {
* Assigns the default value for the property. If no overrides are present
* and if the value is different than the current default value,
* the 'change' event will be triggered.
- * @param {*} value The value to assign.
+ * @param {T} value The value to assign.
*/
set defaultValue(value) {
this._defaultValue = value;
@@ -585,7 +574,7 @@ export class DynamicProperty extends EventDispatcher {
/**
* Gets the current value for the property, taking any overrides into account.
- * @type {*}
+ * @type {T}
*/
get value() {
return this._value;
@@ -593,7 +582,7 @@ export class DynamicProperty extends EventDispatcher {
/**
* Gets the number of overrides added to the property.
- * @type {*}
+ * @type {number}
*/
get overrideCount() {
return this._overrides.length;
@@ -606,9 +595,9 @@ export class DynamicProperty extends EventDispatcher {
* If the newly added override has the highest priority of all overrides
* and if the override value is different from the current value,
* the 'change' event will be fired.
- * @param {*} value The override value to assign.
+ * @param {T} value The override value to assign.
* @param {number} [priority] The priority value to use, as a number.
- * @returns {string} A string token which can be passed to the clearOverride function
+ * @returns {import('core').TokenString} A string token which can be passed to the clearOverride function
* to remove the override.
*/
setOverride(value, priority=0) {
@@ -627,7 +616,7 @@ export class DynamicProperty extends EventDispatcher {
* Removes a specific override value. If the removed override
* had the highest priority, and the new value is different from
* the previous value, the 'change' event will be fired.
- * @param {string} token The token for the corresponding override which is to be removed.
+ * @param {import('core').TokenString} token The token for the corresponding override which is to be removed.
* @returns {boolean} `true` if an override was returned, `false` otherwise.
*/
clearOverride(token) {
@@ -649,13 +638,14 @@ export class DynamicProperty extends EventDispatcher {
const value = this._overrides.length > 0 ? this._overrides[0].value : this._defaultValue;
if (this._value === value) { return; }
this._value = value;
- this.trigger('change', {value});
+ this.trigger('change', /** @type {import('dynamic-property').ChangeEventDetails<T>} */ ({value}));
}
}
/**
* This class handles logging of messages to the console and triggering
* an event for log calls.
+ * @augments EventDispatcher<import('log').LoggerEventType>
*/
export class Logger extends EventDispatcher {
/**
@@ -663,7 +653,8 @@ export class Logger extends EventDispatcher {
*/
constructor() {
super();
- this._extensionName = 'Yomichan';
+ /** @type {string} */
+ this._extensionName = 'Yomitan';
try {
const {name, version} = chrome.runtime.getManifest();
this._extensionName = `${name} ${version}`;
@@ -674,13 +665,13 @@ export class Logger extends EventDispatcher {
/**
* Logs a generic error. This will trigger the 'log' event with the same arguments as the function invocation.
- * @param {Error|object|*} error The error to log. This is typically an `Error` or `Error`-like object.
- * @param {string} level The level to log at. Values include `'info'`, `'debug'`, `'warn'`, and `'error'`.
+ * @param {unknown} error The error to log. This is typically an `Error` or `Error`-like object.
+ * @param {import('log').LogLevel} level The level to log at. Values include `'info'`, `'debug'`, `'warn'`, and `'error'`.
* Other values will be logged at a non-error level.
- * @param {?object} [context] An optional context object for the error which should typically include a `url` field.
+ * @param {?import('log').LogContext} [context] An optional context object for the error which should typically include a `url` field.
*/
log(error, level, context=null) {
- if (!isObject(context)) {
+ if (typeof context !== 'object' || context === null) {
context = {url: location.href};
}
@@ -689,7 +680,11 @@ export class Logger extends EventDispatcher {
if (typeof error === 'string') {
errorString = error;
} else {
- errorString = error.toString();
+ errorString = (
+ typeof error === 'object' && error !== null ?
+ error.toString() :
+ `${error}`
+ );
if (/^\[object \w+\]$/.test(errorString)) {
errorString = JSON.stringify(error);
}
@@ -700,14 +695,20 @@ export class Logger extends EventDispatcher {
let errorStack;
try {
- errorStack = (typeof error.stack === 'string' ? error.stack.trimRight() : '');
+ errorStack = (
+ error instanceof Error ?
+ (typeof error.stack === 'string' ? error.stack.trimEnd() : '') :
+ ''
+ );
} catch (e) {
errorStack = '';
}
let errorData;
try {
- errorData = error.data;
+ if (error instanceof ExtensionError) {
+ errorData = error.data;
+ }
} catch (e) {
// NOP
}
@@ -739,8 +740,8 @@ export class Logger extends EventDispatcher {
/**
* Logs a warning. This function invokes `log` internally.
- * @param {Error|object|*} error The error to log. This is typically an `Error` or `Error`-like object.
- * @param {?object} context An optional context object for the error which should typically include a `url` field.
+ * @param {unknown} error The error to log. This is typically an `Error` or `Error`-like object.
+ * @param {?import('log').LogContext} context An optional context object for the error which should typically include a `url` field.
*/
warn(error, context=null) {
this.log(error, 'warn', context);
@@ -748,8 +749,8 @@ export class Logger extends EventDispatcher {
/**
* Logs an error. This function invokes `log` internally.
- * @param {Error|object|*} error The error to log. This is typically an `Error` or `Error`-like object.
- * @param {?object} context An optional context object for the error which should typically include a `url` field.
+ * @param {unknown} error The error to log. This is typically an `Error` or `Error`-like object.
+ * @param {?import('log').LogContext} context An optional context object for the error which should typically include a `url` field.
*/
error(error, context=null) {
this.log(error, 'error', context);