From 54574432109f353dcb936343d732f406584bce51 Mon Sep 17 00:00:00 2001 From: Chengzhong Wu Date: Wed, 11 Jun 2025 17:11:18 +0100 Subject: [PATCH] lib,src: support DOMException ser-des Added serialization and deserialization support for `DOMException`. Co-Authored-By: jazelly PR-URL: https://github.com/nodejs/node/pull/58649 Fixes: https://github.com/nodejs/node/issues/49181 Reviewed-By: James M Snell Reviewed-By: Ethan Arrowood Reviewed-By: Anna Henningsen Reviewed-By: Jason Zhang --- lib/eslint.config_partial.mjs | 12 +++++ lib/internal/per_context/domexception.js | 35 ++++++++++++++ lib/internal/worker/clone_dom_exception.js | 6 +++ src/api/environment.cc | 37 +++++++++++++- src/node_builtins.cc | 1 + .../test-structuredClone-domexception.js | 48 +++++++++++++++++++ 6 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 lib/internal/worker/clone_dom_exception.js create mode 100644 test/parallel/test-structuredClone-domexception.js diff --git a/lib/eslint.config_partial.mjs b/lib/eslint.config_partial.mjs index 646359807a8..fce886bbc93 100644 --- a/lib/eslint.config_partial.mjs +++ b/lib/eslint.config_partial.mjs @@ -525,4 +525,16 @@ export default [ ], }, }, + { + files: [ + 'lib/internal/per_context/domexception.js', + ], + languageOptions: { + globals: { + // Parameters passed to internal modules. + privateSymbols: 'readonly', + perIsolateSymbols: 'readonly', + }, + }, + }, ]; diff --git a/lib/internal/per_context/domexception.js b/lib/internal/per_context/domexception.js index cf9042fec7c..7d51007bd48 100644 --- a/lib/internal/per_context/domexception.js +++ b/lib/internal/per_context/domexception.js @@ -12,6 +12,18 @@ const { 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(); @@ -50,6 +62,7 @@ const disusedNamesSet = new SafeSet() class DOMException { constructor(message = '', options = 'Error') { + this[transfer_mode_private_symbol] = kCloneable; ErrorCaptureStackTrace(this); if (options && typeof options === 'object') { @@ -76,6 +89,28 @@ class DOMException { } } + [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) { diff --git a/lib/internal/worker/clone_dom_exception.js b/lib/internal/worker/clone_dom_exception.js new file mode 100644 index 00000000000..e1e47a3b988 --- /dev/null +++ b/lib/internal/worker/clone_dom_exception.js @@ -0,0 +1,6 @@ +'use strict'; + +// Delegate to the actual DOMException implementation. +module.exports = { + DOMException: internalBinding('messaging').DOMException, +}; diff --git a/src/api/environment.cc b/src/api/environment.cc index 408c22f8466..3cc0a38cbf6 100644 --- a/src/api/environment.cc +++ b/src/api/environment.cc @@ -767,13 +767,13 @@ MaybeLocal InitializePrivateSymbols(Local context, Context::Scope context_scope(context); Local private_symbols = ObjectTemplate::New(isolate); - Local private_symbols_object; #define V(PropertyName, _) \ private_symbols->Set(isolate, #PropertyName, isolate_data->PropertyName()); PER_ISOLATE_PRIVATE_SYMBOL_PROPERTIES(V) #undef V + Local private_symbols_object; if (!private_symbols->NewInstance(context).ToLocal(&private_symbols_object) || private_symbols_object->SetPrototypeV2(context, Null(isolate)) .IsNothing()) { @@ -783,6 +783,32 @@ MaybeLocal InitializePrivateSymbols(Local context, return scope.Escape(private_symbols_object); } +MaybeLocal InitializePerIsolateSymbols(Local context, + IsolateData* isolate_data) { + CHECK(isolate_data); + Isolate* isolate = context->GetIsolate(); + EscapableHandleScope scope(isolate); + Context::Scope context_scope(context); + + Local per_isolate_symbols = ObjectTemplate::New(isolate); +#define V(PropertyName, _) \ + per_isolate_symbols->Set( \ + isolate, #PropertyName, isolate_data->PropertyName()); + + PER_ISOLATE_SYMBOL_PROPERTIES(V) +#undef V + + Local per_isolate_symbols_object; + if (!per_isolate_symbols->NewInstance(context).ToLocal( + &per_isolate_symbols_object) || + per_isolate_symbols_object->SetPrototypeV2(context, Null(isolate)) + .IsNothing()) { + return MaybeLocal(); + } + + return scope.Escape(per_isolate_symbols_object); +} + Maybe InitializePrimordials(Local context, IsolateData* isolate_data) { // Run per-context JS files. @@ -812,6 +838,12 @@ Maybe InitializePrimordials(Local context, return Nothing(); } + Local per_isolate_symbols; + if (!InitializePerIsolateSymbols(context, isolate_data) + .ToLocal(&per_isolate_symbols)) { + return Nothing(); + } + static const char* context_files[] = {"internal/per_context/primordials", "internal/per_context/domexception", "internal/per_context/messageport", @@ -827,7 +859,8 @@ Maybe InitializePrimordials(Local context, builtin_loader.SetEagerCompile(); for (const char** module = context_files; *module != nullptr; module++) { - Local arguments[] = {exports, primordials, private_symbols}; + Local arguments[] = { + exports, primordials, private_symbols, per_isolate_symbols}; if (builtin_loader .CompileAndCall( diff --git a/src/node_builtins.cc b/src/node_builtins.cc index 6c6c4dd3236..00a4ef69d31 100644 --- a/src/node_builtins.cc +++ b/src/node_builtins.cc @@ -416,6 +416,7 @@ MaybeLocal BuiltinLoader::LookupAndCompile(Local context, FIXED_ONE_BYTE_STRING(isolate, "exports"), FIXED_ONE_BYTE_STRING(isolate, "primordials"), FIXED_ONE_BYTE_STRING(isolate, "privateSymbols"), + FIXED_ONE_BYTE_STRING(isolate, "perIsolateSymbols"), }; } else if (strncmp(id, "internal/main/", strlen("internal/main/")) == 0 || strncmp(id, diff --git a/test/parallel/test-structuredClone-domexception.js b/test/parallel/test-structuredClone-domexception.js new file mode 100644 index 00000000000..4645cc8a002 --- /dev/null +++ b/test/parallel/test-structuredClone-domexception.js @@ -0,0 +1,48 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); + +function assertDOMException(actual, expected) { + assert.strictEqual(actual instanceof DOMException, true); + assert.strictEqual(actual.message, expected.message); + assert.strictEqual(actual.name, expected.name); + assert.strictEqual(actual.code, expected.code); + assert.strictEqual(actual.stack, expected.stack); +} + +{ + // Clone basic DOMException + const e = new DOMException('test'); + const clone = structuredClone(e); + const clone2 = structuredClone(clone); + assertDOMException(clone, e); + assertDOMException(clone2, e); +} + +{ + // Clone a DOMException with a name + const e = new DOMException('test', 'DataCloneError'); + const clone = structuredClone(e); + const clone2 = structuredClone(clone); + assertDOMException(clone, e); + assertDOMException(clone2, e); +} + +{ + // Clone an arbitrary object with a DOMException prototype + const obj = {}; + Object.setPrototypeOf(obj, DOMException.prototype); + const clone = structuredClone(obj); + assert.strictEqual(clone instanceof DOMException, false); +} + +{ + // Transfer a DOMException. DOMExceptions are not transferable. + const e = new DOMException('test'); + assert.throws(() => { + structuredClone(e, { transfer: [e] }); + }, { + name: 'DataCloneError', + }); +}