node/lib/internal/per_context/domexception.js
Filip Skokan b328bf74bd
lib,src: implement QuotaExceededError as DOMException-derived interface
Implement QuotaExceededError as a DOMException-derived interface per the
WebIDL specification update.

QuotaExceededError is now a proper constructor exposed as a global
[Exposed=*] interface that extends DOMException with optional `quota`
and `requested` attributes (both nullable doubles, defaulting to null).

The constructor validates that quota and requested are finite,
non-negative, and that requested is not less than quota when both are
provided.

QuotaExceededError is [Serializable] and supports structuredClone,
preserving the quota and requested values across the serialization
boundary.

Callers updated:
- crypto.getRandomValues() now throws a QuotaExceededError instance
- WebStorage (C++) now constructs QuotaExceededError directly

Refs: https://redirect.github.com/whatwg/webidl/pull/1465
Fixes: https://github.com/nodejs/node/issues/58987
PR-URL: https://github.com/nodejs/node/pull/62293

Reviewed-By: Mattias Buelens <mattias@buelens.com>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
2026-03-19 14:41:50 +01:00

296 lines
9.2 KiB
JavaScript

'use strict';
const {
Error,
ErrorPrototype,
NumberIsFinite,
NumberIsNaN,
ObjectDefineProperties,
ObjectDefineProperty,
ObjectSetPrototypeOf,
RangeError,
SafeMap,
SafeSet,
SafeWeakMap,
SymbolToStringTag,
TypeError,
} = primordials;
const {
transfer_mode_private_symbol,
} = privateSymbols;
const {
messaging_clone_symbol,
messaging_deserialize_symbol,
} = perIsolateSymbols;
/**
* Maps to BaseObject::TransferMode::kCloneable
*/
const kCloneable = 2;
function throwInvalidThisError(Base, type) {
const err = new Base();
const key = 'ERR_INVALID_THIS';
ObjectDefineProperties(err, {
message: {
__proto__: null,
value: `Value of "this" must be of ${type}`,
enumerable: false,
writable: true,
configurable: true,
},
toString: {
__proto__: null,
value() {
return `${this.name} [${key}]: ${this.message}`;
},
enumerable: false,
writable: true,
configurable: true,
},
});
err.code = key;
throw err;
}
const internalsMap = new SafeWeakMap();
const nameToCodeMap = new SafeMap();
// These were removed from the error names table.
// See https://github.com/heycam/webidl/pull/946.
const disusedNamesSet = new SafeSet()
.add('DOMStringSizeError')
.add('NoDataAllowedError')
.add('ValidationError');
// The DOMException WebIDL interface defines that:
// - ObjectGetPrototypeOf(DOMException) === Function.
// - ObjectGetPrototypeOf(DOMException.prototype) === Error.prototype.
// Thus, we can not simply use the pattern of `class DOMException extends Error` and call
// `super()` to construct an object. The `super` in `super()` call in the constructor will
// be resolved to `Function`, instead of `Error`. Use the trick of return overriding to
// create an object with the `[[ErrorData]]` internal slot.
// Ref: https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#sec-getsuperconstructor
class DOMException {
constructor(message = '', options = 'Error') {
// Invokes the Error constructor to create an object with the [[ErrorData]]
// internal slot.
// eslint-disable-next-line no-restricted-syntax
const self = new Error();
// Use `new.target.prototype` to support DOMException subclasses.
ObjectSetPrototypeOf(self, new.target.prototype);
self[transfer_mode_private_symbol] = kCloneable;
if (options && typeof options === 'object') {
const { name } = options;
internalsMap.set(self, {
message: `${message}`,
name: `${name}`,
});
if ('cause' in options) {
ObjectDefineProperty(self, 'cause', {
__proto__: null,
value: options.cause,
configurable: true,
writable: true,
enumerable: false,
});
}
} else {
internalsMap.set(self, {
message: `${message}`,
name: `${options}`,
});
}
// Return the error object as the return overriding of the constructor.
// eslint-disable-next-line no-constructor-return
return self;
}
[messaging_clone_symbol]() {
// See serialization steps in https://webidl.spec.whatwg.org/#dom-domexception-domexception
const internals = internalsMap.get(this);
return {
data: {
message: internals.message,
name: internals.name,
stack: this.stack,
},
deserializeInfo: 'internal/worker/clone_dom_exception:DOMException',
};
}
[messaging_deserialize_symbol](data) {
// See deserialization steps in https://webidl.spec.whatwg.org/#dom-domexception-domexception
internalsMap.set(this, {
message: data.message,
name: data.name,
});
this.stack = data.stack;
}
get name() {
const internals = internalsMap.get(this);
if (internals === undefined) {
throwInvalidThisError(TypeError, 'DOMException');
}
return internals.name;
}
get message() {
const internals = internalsMap.get(this);
if (internals === undefined) {
throwInvalidThisError(TypeError, 'DOMException');
}
return internals.message;
}
get code() {
const internals = internalsMap.get(this);
if (internals === undefined) {
throwInvalidThisError(TypeError, 'DOMException');
}
if (disusedNamesSet.has(internals.name)) {
return 0;
}
const code = nameToCodeMap.get(internals.name);
return code === undefined ? 0 : code;
}
}
const DOMExceptionPrototype = DOMException.prototype;
ObjectSetPrototypeOf(DOMExceptionPrototype, ErrorPrototype);
ObjectDefineProperties(DOMExceptionPrototype, {
[SymbolToStringTag]: { __proto__: null, configurable: true, value: 'DOMException' },
name: { __proto__: null, enumerable: true, configurable: true },
message: { __proto__: null, enumerable: true, configurable: true },
code: { __proto__: null, enumerable: true, configurable: true },
});
for (const { 0: name, 1: codeName, 2: value } of [
['IndexSizeError', 'INDEX_SIZE_ERR', 1],
['DOMStringSizeError', 'DOMSTRING_SIZE_ERR', 2],
['HierarchyRequestError', 'HIERARCHY_REQUEST_ERR', 3],
['WrongDocumentError', 'WRONG_DOCUMENT_ERR', 4],
['InvalidCharacterError', 'INVALID_CHARACTER_ERR', 5],
['NoDataAllowedError', 'NO_DATA_ALLOWED_ERR', 6],
['NoModificationAllowedError', 'NO_MODIFICATION_ALLOWED_ERR', 7],
['NotFoundError', 'NOT_FOUND_ERR', 8],
['NotSupportedError', 'NOT_SUPPORTED_ERR', 9],
['InUseAttributeError', 'INUSE_ATTRIBUTE_ERR', 10],
['InvalidStateError', 'INVALID_STATE_ERR', 11],
['SyntaxError', 'SYNTAX_ERR', 12],
['InvalidModificationError', 'INVALID_MODIFICATION_ERR', 13],
['NamespaceError', 'NAMESPACE_ERR', 14],
['InvalidAccessError', 'INVALID_ACCESS_ERR', 15],
['ValidationError', 'VALIDATION_ERR', 16],
['TypeMismatchError', 'TYPE_MISMATCH_ERR', 17],
['SecurityError', 'SECURITY_ERR', 18],
['NetworkError', 'NETWORK_ERR', 19],
['AbortError', 'ABORT_ERR', 20],
['URLMismatchError', 'URL_MISMATCH_ERR', 21],
['QuotaExceededError', 'QUOTA_EXCEEDED_ERR', 22],
['TimeoutError', 'TIMEOUT_ERR', 23],
['InvalidNodeTypeError', 'INVALID_NODE_TYPE_ERR', 24],
['DataCloneError', 'DATA_CLONE_ERR', 25],
// There are some more error names, but since they don't have codes assigned,
// we don't need to care about them.
]) {
const desc = { enumerable: true, value };
ObjectDefineProperty(DOMException, codeName, desc);
ObjectDefineProperty(DOMExceptionPrototype, codeName, desc);
nameToCodeMap.set(name, value);
}
exports.DOMException = DOMException;
// https://webidl.spec.whatwg.org/#quotaexceedederror
class QuotaExceededError extends DOMException {
#quota;
#requested;
constructor(message = '', options = { __proto__: null }) {
super(message, 'QuotaExceededError');
this[transfer_mode_private_symbol] = kCloneable;
let quota = null;
let requested = null;
if (options !== null && options !== undefined) {
if ('quota' in options) {
quota = +options.quota;
if (!NumberIsFinite(quota)) {
// eslint-disable-next-line no-restricted-syntax
throw new TypeError(
`Cannot convert options.quota to a double: the value is ${NumberIsNaN(quota) ? 'NaN' : 'Infinity'}`,
);
}
if (quota < 0) {
// eslint-disable-next-line no-restricted-syntax
throw new RangeError('options.quota must not be negative');
}
}
if ('requested' in options) {
requested = +options.requested;
if (!NumberIsFinite(requested)) {
// eslint-disable-next-line no-restricted-syntax
throw new TypeError(
`Cannot convert options.requested to a double: the value is ${NumberIsNaN(requested) ? 'NaN' : 'Infinity'}`,
);
}
if (requested < 0) {
// eslint-disable-next-line no-restricted-syntax
throw new RangeError('options.requested must not be negative');
}
}
}
if (quota !== null && requested !== null && requested < quota) {
// eslint-disable-next-line no-restricted-syntax
throw new RangeError('options.requested must not be less than options.quota');
}
this.#quota = quota;
this.#requested = requested;
}
[messaging_clone_symbol]() {
const domExceptionClone = DOMExceptionPrototype[messaging_clone_symbol].call(this);
domExceptionClone.data.quota = this.#quota;
domExceptionClone.data.requested = this.#requested;
domExceptionClone.deserializeInfo = 'internal/worker/clone_dom_exception:QuotaExceededError';
return domExceptionClone;
}
[messaging_deserialize_symbol](data) {
DOMExceptionPrototype[messaging_deserialize_symbol].call(this, data);
this.#quota = data.quota;
this.#requested = data.requested;
}
get quota() {
if (!(#quota in this)) {
throwInvalidThisError(TypeError, 'QuotaExceededError');
}
return this.#quota;
}
get requested() {
if (!(#requested in this)) {
throwInvalidThisError(TypeError, 'QuotaExceededError');
}
return this.#requested;
}
}
ObjectDefineProperties(QuotaExceededError.prototype, {
[SymbolToStringTag]: { __proto__: null, configurable: true, value: 'QuotaExceededError' },
quota: { __proto__: null, enumerable: true, configurable: true },
requested: { __proto__: null, enumerable: true, configurable: true },
});
exports.QuotaExceededError = QuotaExceededError;