diff --git a/docs/structured-clone.md b/docs/structured-clone.md index d0a0fbb84..80dd045e6 100644 --- a/docs/structured-clone.md +++ b/docs/structured-clone.md @@ -18,7 +18,7 @@ buffer.byteLength; // 0 — the memory now belongs to `moved` - `options` may be `undefined` or `null` (both mean "no transfer"); anything else must be an object, or a `TypeError` is thrown. - `options.transfer` is a WebIDL sequence: any object with a callable `Symbol.iterator` works (an array, a `Set`, a generator). A non-iterable value — including a string primitive — throws a `TypeError`. -Cloneable: every primitive value except symbols — numbers (including `-0`, `NaN` and the infinities), strings, booleans, `BigInt`, `null` and `undefined`; plain objects and arrays; `Date`, `RegExp`, `Map`, `Set`, `Error`; `Boolean`/`String`/`Number` wrapper objects; `ArrayBuffer`, every typed array and `DataView`. +Cloneable: every primitive value except symbols — numbers (including `-0`, `NaN` and the infinities), strings, booleans, `BigInt`, `null` and `undefined`; plain objects and arrays; `Date`, `RegExp`, `Map`, `Set`, `Error`; `Boolean`/`String`/`Number` wrapper objects; `ArrayBuffer`, every typed array and `DataView`; and `DOMException`, per its Web IDL `[Serializable]` slot — `name`, `message` and `stack` round-trip through `structuredClone` and worker `postMessage`, and object identity within a graph is preserved. The clone preserves the shape of the graph, not just the values: an object referenced twice in the input is a single object referenced twice in the output, and cycles round-trip. Prototypes do not survive — a class instance clones to a plain object with the same own properties. Getters are invoked during cloning and their result is stored as a plain data property. Property insertion order is preserved. diff --git a/test-app/app/src/main/assets/app/shared b/test-app/app/src/main/assets/app/shared index 9cc46c06b..aa7f8cfcc 160000 --- a/test-app/app/src/main/assets/app/shared +++ b/test-app/app/src/main/assets/app/shared @@ -1 +1 @@ -Subproject commit 9cc46c06bc918d849a54d089842f5a42ebfbb6e6 +Subproject commit aa7f8cfcc29ac2945e190fafe1259827ab6ab2e6 diff --git a/test-app/app/src/main/assets/app/tests/domExceptionFirstCloneWorker.js b/test-app/app/src/main/assets/app/tests/domExceptionFirstCloneWorker.js new file mode 100644 index 000000000..256105beb --- /dev/null +++ b/test-app/app/src/main/assets/app/tests/domExceptionFirstCloneWorker.js @@ -0,0 +1,14 @@ +// A fresh isolate: the DOMException constructed inside the getter below is +// the first one this isolate has ever seen, and it is born while the clone +// that carries it is already being written. +var graph = { + get inner() { + return new DOMException("first in this isolate", "AbortError"); + }, +}; +var clone = structuredClone(graph); +postMessage({ + isDomException: clone.inner instanceof DOMException, + name: clone.inner.name, + message: clone.inner.message, +}); diff --git a/test-app/app/src/main/assets/app/tests/testRuntimeImplementedAPIs.js b/test-app/app/src/main/assets/app/tests/testRuntimeImplementedAPIs.js index b93266f48..08b07c8f0 100644 --- a/test-app/app/src/main/assets/app/tests/testRuntimeImplementedAPIs.js +++ b/test-app/app/src/main/assets/app/tests/testRuntimeImplementedAPIs.js @@ -62,6 +62,47 @@ describe("DOMException canary", function () { it("is not reachable as a module from app code", function () { expect(function () { require("internal/dom-exception"); }).toThrow(); }); + + it("serializes through structuredClone on this runtime", function () { + var clone = structuredClone(new DOMException("x", "AbortError")); + expect(clone instanceof DOMException).toBe(true); + expect(clone.name).toBe("AbortError"); + }); + + it("clones the isolate's first DOMException even when a getter creates it mid-clone", function (done) { + var worker = new Worker("./domExceptionFirstCloneWorker.js"); + worker.onmessage = function (event) { + expect(event.data.isDomException).toBe(true); + expect(event.data.name).toBe("AbortError"); + expect(event.data.message).toBe("first in this isolate"); + worker.terminate(); + done(); + }; + worker.onerror = function (event) { + fail("worker error: " + event.message); + worker.terminate(); + done(); + return true; + }; + }); + + // Once an isolate holds a DOMException the serializer claims host objects + // itself, and V8 then stops detecting native wrappers on its own. These are + // the shapes that would silently clone as {} if the claim missed them. + it("still rejects native wrappers once a DOMException exists", function () { + new DOMException("x", "AbortError"); + var wrappers = [new java.lang.Object(), new URL("https://example.com/")]; + for (var i = 0; i < wrappers.length; i++) { + var error; + try { + structuredClone(wrappers[i]); + } catch (e) { + error = e; + } + expect(error instanceof DOMException).toBe(true); + expect(error.name).toBe("DataCloneError"); + } + }); }); describe("CustomEvent canary", function () { diff --git a/test-app/runtime/src/main/cpp/LazyGlobals.cpp b/test-app/runtime/src/main/cpp/LazyGlobals.cpp index c812eebd4..9b174c3ba 100644 --- a/test-app/runtime/src/main/cpp/LazyGlobals.cpp +++ b/test-app/runtime/src/main/cpp/LazyGlobals.cpp @@ -3,6 +3,7 @@ #include "ArgConverter.h" #include "Base64.h" #include "BuiltinLoader.h" +#include "StructuredSerialization.h" #include "TextEncoding.h" using namespace v8; @@ -38,7 +39,7 @@ constexpr LazyGlobalEntry kLazyGlobals[] = { {"TextDecoder", "TextDecoder", TextEncoding::GetExports}, {"atob", "atob", Base64::GetExports}, {"btoa", "btoa", Base64::GetExports}, - {"DOMException", "DOMException", BuiltinExports}, + {"DOMException", "DOMException", serialization::GetDomExceptionExports}, // events.js is an eager builtin (Events::Init), so this row never runs // a file: the read hits the exports cache and only the placement is // lazy. diff --git a/test-app/runtime/src/main/cpp/NsBuiltinModules.cpp b/test-app/runtime/src/main/cpp/NsBuiltinModules.cpp index 467917d93..1e1d2bba3 100644 --- a/test-app/runtime/src/main/cpp/NsBuiltinModules.cpp +++ b/test-app/runtime/src/main/cpp/NsBuiltinModules.cpp @@ -10,6 +10,7 @@ #include "NativeScriptAssert.h" #include "Runtime.h" #include "RuntimeState.h" +#include "StructuredSerialization.h" #include "TextEncoding.h" #include "TraceLog.h" #include "console/Console.h" @@ -58,7 +59,8 @@ constexpr Registration kRegistry[] = { {"node:module", BuiltinId::kNodeModule, nullptr}, {"node:url", BuiltinId::kNodeUrl, nullptr}, {"node:util", BuiltinId::kNodeUtil, nullptr}, - {"internal/dom-exception", BuiltinId::kDomException, nullptr, true}, + {"internal/dom-exception", BuiltinId::kDomException, serialization::DomExceptionBinding, + true}, {"internal/events", BuiltinId::kEvents, nullptr, true}, }; diff --git a/test-app/runtime/src/main/cpp/StructuredSerialization.cpp b/test-app/runtime/src/main/cpp/StructuredSerialization.cpp index 3da795333..75a5c2877 100644 --- a/test-app/runtime/src/main/cpp/StructuredSerialization.cpp +++ b/test-app/runtime/src/main/cpp/StructuredSerialization.cpp @@ -4,12 +4,77 @@ #include "ArgConverter.h" #include "BuiltinLoader.h" +#include "RuntimeState.h" using namespace v8; namespace tns { namespace serialization { +namespace { + +/* + * The private symbol markCloneable stamps on every DOMException instance. + * Private, so app code can neither forge the brand onto an impostor nor strip + * it; per isolate because a worker's instances are branded and checked on its + * own isolate, and only bytes cross between them. Every instance passes + * through markCloneable — deserialization rebuilds via the constructor. + */ +struct DomExceptionBrandState { + Persistent brand; +}; + +Local BrandOf(Isolate* isolate, DomExceptionBrandState* state) { + if (state->brand.IsEmpty()) { + state->brand.Reset(isolate, + Private::New(isolate, ArgConverter::ConvertToV8String( + isolate, "domExceptionCloneable"))); + } + return state->brand.Get(isolate); +} + +// Empty once teardown has begun — callers bail to their fallback. +Local DomExceptionBrand(Isolate* isolate) { + auto* state = RuntimeState::For(isolate); + if (state == nullptr) { + return Local(); + } + return BrandOf(isolate, state); +} + +void MarkCloneableCallback(const FunctionCallbackInfo& info) { + Isolate* isolate = info.GetIsolate(); + if (info.Length() < 1 || !info[0]->IsObject()) { + return; + } + auto* state = RuntimeState::For(isolate); + if (state == nullptr) { + return; + } + Local brand = BrandOf(isolate, state); + (void)info[0].As()->SetPrivate(isolate->GetCurrentContext(), brand, + v8::True(isolate)); +} + +} // namespace + +MaybeLocal DomExceptionBinding(Local context) { + Isolate* isolate = v8::Isolate::GetCurrent(); + Local binding = Object::New(isolate); + Local markCloneable; + if (!v8::Function::New(context, MarkCloneableCallback).ToLocal(&markCloneable) || + !binding->Set(context, ArgConverter::ConvertToV8String(isolate, "markCloneable"), + markCloneable) + .FromMaybe(false)) { + return MaybeLocal(); + } + return binding; +} + +MaybeLocal GetDomExceptionExports(Local context) { + return BuiltinLoader::GetExports(context, BuiltinId::kDomException, DomExceptionBinding); +} + void ThrowDataCloneError(Isolate* isolate, const std::string& message) { Local context = isolate->GetCurrentContext(); @@ -25,8 +90,7 @@ void ThrowDataCloneError(Isolate* isolate, const std::string& message) { TryCatch tc(isolate); Local exports; Local ctor; - if (BuiltinLoader::GetExports(context, BuiltinId::kDomException, nullptr) - .ToLocal(&exports) && + if (GetDomExceptionExports(context).ToLocal(&exports) && exports->Get(context, ArgConverter::ConvertToV8String(isolate, "DOMException")) .ToLocal(&ctor) && ctor->IsFunction()) { @@ -56,24 +120,73 @@ void ThrowDataCloneError(Isolate* isolate, const std::string& message) { namespace { +/* + * Every host object's payload starts with one of these, so the reader can + * dispatch. kHostObjectDegraded carries nothing further; kHostObjectDomException + * carries a uint32 index into the SerializedValue's out-of-band payload list. + * The bytes never outlive the process (structuredClone round-trips in one + * isolate, worker messages cross isolates in the same binary), so the format can + * evolve freely with this file. + */ +constexpr uint32_t kHostObjectDegraded = 0; +constexpr uint32_t kHostObjectDomException = 1; + class SerializerDelegate : public ValueSerializer::Delegate { public: SerializerDelegate(Isolate* isolate, HostObjectPolicy hostObjectPolicy, - std::vector>* sharedBuffers) + std::vector>* sharedBuffers, + std::vector* domExceptions) : isolate_(isolate), hostObjectPolicy_(hostObjectPolicy), - sharedBuffers_(sharedBuffers) {} + sharedBuffers_(sharedBuffers), + domExceptions_(domExceptions), + domExceptionBrand_(DomExceptionBrand(isolate)) {} + + void SetSerializer(ValueSerializer* serializer) { serializer_ = serializer; } void ThrowDataCloneError(Local message) override { serialization::ThrowDataCloneError( isolate_, ArgConverter::ConvertToString(message)); } + /* + * Always claimed: V8 samples this once per ValueSerializer and never again, + * so a gate on "this isolate holds a DOMException" would miss the isolate's + * first instance when a getter constructs it during the very clone that + * carries it. The price is one IsHostObject call per plain JS object in a + * graph, the same Node pays for its JSTransferable protocol. + */ + bool HasCustomHostObject(Isolate* isolate) override { return true; } + + Maybe IsHostObject(Isolate* isolate, Local object) override { + // Claiming custom host objects REPLACES V8's own embedder-field + // detection rather than adding to it, so anything with a native half + // has to be claimed here too — otherwise a Java proxy would be written + // out as a plain object, silently losing the half that mattered. + if (object->InternalFieldCount() > 0) { + return Just(true); + } + if (domExceptionBrand_.IsEmpty()) { + return Just(false); + } + return object->HasPrivate(isolate->GetCurrentContext(), domExceptionBrand_); + } + Maybe WriteHostObject(Isolate* isolate, Local object) override { + // DOMException serializes under both policies: it is [Serializable] in + // the IDL, and it is a plain JS object with no native half to lose. + bool isDomException = false; + if (!domExceptionBrand_.IsEmpty() && + !object->HasPrivate(isolate->GetCurrentContext(), domExceptionBrand_) + .To(&isDomException)) { + return Nothing(); + } + if (isDomException) { + return WriteDomException(isolate, object); + } if (hostObjectPolicy_ == HostObjectPolicy::kDegrade) { - // V8 has already written the kHostObject tag; writing no payload is - // what the zero-byte ReadHostObject below expects, and the value - // surfaces as an empty object. + // Tag only, no payload: the value surfaces as an empty object. + serializer_->WriteUint32(kHostObjectDegraded); return Just(true); } std::string name = @@ -109,21 +222,83 @@ class SerializerDelegate : public ValueSerializer::Delegate { } private: + /* + * Web IDL's DOMException serialization steps (name and message), plus the + * stack, matching Node. The payload travels out-of-band and only an index + * enters the stream: the receiving side must construct instances before + * ReadValue runs, because V8 forbids JS execution during deserialization. + */ + Maybe WriteDomException(Isolate* isolate, Local object) { + Local context = isolate->GetCurrentContext(); + Local name, message, stack; + if (!object->Get(context, ArgConverter::ConvertToV8String(isolate, "name")) + .ToLocal(&name) || + !object->Get(context, ArgConverter::ConvertToV8String(isolate, "message")) + .ToLocal(&message) || + !object->Get(context, ArgConverter::ConvertToV8String(isolate, "stack")) + .ToLocal(&stack)) { + return Nothing(); + } + SerializedValue::DomExceptionPayload payload; + payload.name = ArgConverter::ToString(isolate, name); + payload.message = ArgConverter::ToString(isolate, message); + // The stack can legitimately be absent or tampered into a non-string; + // carry it only when it is the string captureStackTrace left. + payload.hasStack = stack->IsString(); + if (payload.hasStack) { + payload.stack = ArgConverter::ToString(isolate, stack); + } + serializer_->WriteUint32(kHostObjectDomException); + serializer_->WriteUint32(static_cast(domExceptions_->size())); + domExceptions_->push_back(std::move(payload)); + return Just(true); + } + Isolate* isolate_; HostObjectPolicy hostObjectPolicy_; std::vector>* sharedBuffers_; + std::vector* domExceptions_; + // Resolved once per serializer: V8 asks about every object in the graph, + // and each lookup would otherwise re-resolve the state slot and push a + // fresh handle into the caller's scope. + Local domExceptionBrand_; + ValueSerializer* serializer_ = nullptr; }; class DeserializerDelegate : public ValueDeserializer::Delegate { public: - explicit DeserializerDelegate( - const std::vector>* sharedBuffers) - : sharedBuffers_(sharedBuffers) {} + DeserializerDelegate(const std::vector>* sharedBuffers, + const std::vector>* domExceptions) + : sharedBuffers_(sharedBuffers), domExceptions_(domExceptions) {} - // Counterpart of the kDegrade branch: consumes no bytes, so the stream - // stays balanced. Unreachable for a value written under kReject. + void SetDeserializer(ValueDeserializer* deserializer) { + deserializer_ = deserializer; + } + + // No JS may run in here (V8 forbids it during a read); DOMException + // instances were constructed by Deserialize before ReadValue started, and + // this only hands them out. MaybeLocal ReadHostObject(Isolate* isolate) override { - return Object::New(isolate); + uint32_t tag; + if (!deserializer_->ReadUint32(&tag)) { + return MaybeLocal(); + } + switch (tag) { + case kHostObjectDegraded: + // Counterpart of the kDegrade branch: tag only, so the value + // arrives as an empty object. Unreachable for a value written + // under kReject. + return Object::New(isolate); + case kHostObjectDomException: { + uint32_t index; + if (!deserializer_->ReadUint32(&index) || index >= domExceptions_->size()) { + return MaybeLocal(); + } + return (*domExceptions_)[index]; + } + default: + return MaybeLocal(); + } } MaybeLocal GetSharedArrayBufferFromId( @@ -136,6 +311,8 @@ class DeserializerDelegate : public ValueDeserializer::Delegate { private: const std::vector>* sharedBuffers_; + const std::vector>* domExceptions_; + ValueDeserializer* deserializer_ = nullptr; }; /* @@ -207,8 +384,9 @@ Maybe SerializedValue::Serialize(Isolate* isolate, Local context, return Nothing(); } - SerializerDelegate delegate(isolate, hostObjectPolicy, &sharedBuffers_); + SerializerDelegate delegate(isolate, hostObjectPolicy, &sharedBuffers_, &domExceptions_); ValueSerializer serializer(isolate, &delegate); + delegate.SetSerializer(&serializer); for (size_t i = 0; i < transfers.size(); i++) { serializer.TransferArrayBuffer(static_cast(i), transfers[i]); } @@ -258,9 +436,54 @@ MaybeLocal SerializedValue::Deserialize(Isolate* isolate, sharedBuffers.push_back(SharedArrayBuffer::New(isolate, backingStore)); } - DeserializerDelegate delegate(&sharedBuffers); + /* + * Construct every DOMException the payload names before the read begins: + * JS is allowed here and forbidden inside ReadHostObject. Construction goes + * through the real constructor — on a worker isolate that never touched + * DOMException this runs the builtin on demand — so each instance is + * branded again and re-serializes on the next hop. + */ + std::vector> domExceptions; + if (!domExceptions_.empty()) { + Local exports; + Local ctor; + if (!GetDomExceptionExports(context).ToLocal(&exports) || + !exports->Get(context, ArgConverter::ConvertToV8String(isolate, "DOMException")) + .ToLocal(&ctor) || + !ctor->IsFunction()) { + // Only reached with nothing pending (a builtin that cannot load + // during teardown); the caller must see a failure, not an undefined + // result. + if (!isolate->HasPendingException()) { + ThrowDataCloneError(isolate, + "DOMException could not be rebuilt on this isolate."); + } + return MaybeLocal(); + } + Local stackKey = ArgConverter::ConvertToV8String(isolate, "stack"); + for (const DomExceptionPayload& payload : domExceptions_) { + Local args[] = {ArgConverter::ConvertToV8String(isolate, payload.message), + ArgConverter::ConvertToV8String(isolate, payload.name)}; + Local exception; + if (!ctor.As()->NewInstance(context, 2, args).ToLocal(&exception)) { + return MaybeLocal(); + } + // The sender's stack replaces the one captured just now for the + // receiving side's constructor frame, matching Node. + if (payload.hasStack && + !exception->Set(context, stackKey, + ArgConverter::ConvertToV8String(isolate, payload.stack)) + .FromMaybe(false)) { + return MaybeLocal(); + } + domExceptions.push_back(exception); + } + } + + DeserializerDelegate delegate(&sharedBuffers, &domExceptions); ValueDeserializer deserializer(isolate, buffer_.get(), bufferSize_, &delegate); + delegate.SetDeserializer(&deserializer); for (size_t i = 0; i < transferredBuffers_.size(); i++) { deserializer.TransferArrayBuffer( diff --git a/test-app/runtime/src/main/cpp/StructuredSerialization.h b/test-app/runtime/src/main/cpp/StructuredSerialization.h index 43c1c0fbb..ec7f4fb2c 100644 --- a/test-app/runtime/src/main/cpp/StructuredSerialization.h +++ b/test-app/runtime/src/main/cpp/StructuredSerialization.h @@ -34,6 +34,23 @@ enum class HostObjectPolicy { */ void ThrowDataCloneError(v8::Isolate* isolate, const std::string& message); +/* + * The dom-exception builtin's native half: `markCloneable`, which stamps a + * per-isolate private brand on every instance the constructor makes. The brand + * is what the serialization delegates answer IsHostObject from, so DOMException + * travels through structuredClone and worker postMessage (Web IDL + * [Serializable]). Lives here, next to those delegates. + */ +v8::MaybeLocal DomExceptionBinding(v8::Local context); + +/* + * The dom-exception builtin's exports with its binding attached. GetExports + * consults the factory only on the run that populates the cache, so every call + * site for this builtin must go through here — a site passing a different + * factory would win or lose by init order. + */ +v8::MaybeLocal GetDomExceptionExports(v8::Local context); + /* * A value serialized out of one isolate, plus the memory that travels with it. * Serializing and deserializing are separate halves because a worker message @@ -68,6 +85,20 @@ class SerializedValue { v8::MaybeLocal Deserialize(v8::Isolate* isolate, v8::Local context); + /* + * Web IDL's DOMException serialization steps (name, message) plus the + * stack, matching Node. Kept out-of-band because V8 forbids JS while a + * value is being read: Deserialize constructs every instance up front and + * ReadHostObject only hands them out by index (Node's host_objects_ + * design). + */ + struct DomExceptionPayload { + std::string name; + std::string message; + std::string stack; + bool hasStack = false; + }; + private: struct FreeDeleter { void operator()(void* pointer) const { std::free(pointer); } @@ -82,6 +113,9 @@ class SerializedValue { std::vector> transferredBuffers_; // Backing stores shared with — not moved from — the sending isolate. std::vector> sharedBuffers_; + // One entry per distinct DOMException in the graph, in write order (a + // repeated reference is an object id in the stream, not a second entry). + std::vector domExceptions_; }; } // namespace serialization diff --git a/test-app/runtime/src/main/cpp/WorkerWrapper.cpp b/test-app/runtime/src/main/cpp/WorkerWrapper.cpp index 04abc68fd..f53648480 100644 --- a/test-app/runtime/src/main/cpp/WorkerWrapper.cpp +++ b/test-app/runtime/src/main/cpp/WorkerWrapper.cpp @@ -343,13 +343,26 @@ void WorkerWrapper::FireMessageOnParentWorkerObject(int workerId, } Local data; - if (message->Deserialize(isolate, context).ToLocal(&data)) { - auto event = Object::New(isolate); - event->DefineOwnProperty(context, ArgConverter::ConvertToV8String(isolate, "data"), - data, PropertyAttribute::ReadOnly); - Local args[] = {event}; - callback.As()->Call(context, Undefined(isolate), 1, args); + { + // Reading runs JS (a DOMException is rebuilt through its + // constructor), so a failure here must not stay pending on the + // isolate past this callout. + TryCatch tc(isolate); + if (!message->Deserialize(isolate, context).ToLocal(&data)) { + if (!tc.HasTerminated() && tc.HasCaught()) { + DEBUG_WRITE_FORCE("MAIN: worker(id=%d) message could not be read: %s", + workerId, + ArgConverter::ToString(isolate, tc.Exception()).c_str()); + } + return; + } } + + auto event = Object::New(isolate); + event->DefineOwnProperty(context, ArgConverter::ConvertToV8String(isolate, "data"), data, + PropertyAttribute::ReadOnly); + Local args[] = {event}; + callback.As()->Call(context, Undefined(isolate), 1, args); } catch (NativeScriptException& ex) { ex.ReThrowToV8(); } diff --git a/test-app/runtime/src/main/cpp/js/dom-exception.js b/test-app/runtime/src/main/cpp/js/dom-exception.js index e8821d6fa..64eb25754 100644 --- a/test-app/runtime/src/main/cpp/js/dom-exception.js +++ b/test-app/runtime/src/main/cpp/js/dom-exception.js @@ -9,10 +9,11 @@ // constructor through require("internal/dom-exception") at throw time, so // this file never runs in an app that never touches a DOMException. // -// Not implemented: the spec's [Serializable] slot. structuredClone and worker -// postMessage go through v8::ValueSerializer, which has no hook for a plain -// JS class, so a DOMException inside a cloned graph degrades the same way any -// custom Error subclass does. +// [Serializable]: the constructor stamps every instance with a native private +// brand (binding.markCloneable), which the serialization delegates in +// StructuredSerialization.cpp claim through V8's IsHostObject hook — name, +// message and stack travel across structuredClone and worker postMessage, +// Node's JSTransferable approach reduced to the one class. const { ErrorCaptureStackTrace, ErrorPrototype, @@ -21,6 +22,8 @@ const { SymbolToStringTag, } = primordials; +const { markCloneable } = binding; + // Web IDL §4.3.4: the closed table of names with a legacy code. Any name // outside it — including every post-table spec name — has code 0. const nameToCode = { @@ -62,6 +65,7 @@ class DOMException { this.#message = `${message}`; this.#name = `${name}`; ErrorCaptureStackTrace(this, DOMException); + markCloneable(this); } get name() {