Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/structured-clone.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Original file line number Diff line number Diff line change
@@ -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,
});
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down
3 changes: 2 additions & 1 deletion test-app/runtime/src/main/cpp/LazyGlobals.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "ArgConverter.h"
#include "Base64.h"
#include "BuiltinLoader.h"
#include "StructuredSerialization.h"
#include "TextEncoding.h"

using namespace v8;
Expand Down Expand Up @@ -38,7 +39,7 @@ constexpr LazyGlobalEntry kLazyGlobals[] = {
{"TextDecoder", "TextDecoder", TextEncoding::GetExports},
{"atob", "atob", Base64::GetExports},
{"btoa", "btoa", Base64::GetExports},
{"DOMException", "DOMException", BuiltinExports<BuiltinId::kDomException>},
{"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.
Expand Down
4 changes: 3 additions & 1 deletion test-app/runtime/src/main/cpp/NsBuiltinModules.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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},
};

Expand Down
Loading