diff --git a/docs/README.md b/docs/README.md index 0d38473ea..cd72dffbe 100644 --- a/docs/README.md +++ b/docs/README.md @@ -12,13 +12,13 @@ (`AbortController`, `AbortSignal` with the `abort`/`timeout`/`any` statics) layered on the runtime's `EventTarget`, the GC contract (weak timers and `any()` links, listener-driven persistence), and the `DOMException` - stand-in (name-patched `Error` reasons). + reasons. - [TextEncoder / TextDecoder and atob / btoa](text-encoding.md) — the WHATWG encoding and base64 globals (`TextEncoder`, `TextDecoder`, `atob`, `btoa`), the supported encodings with their label sets, streaming decode semantics, and the lazy-global tier that runs their builtins only on first use. - [Error handling](error-handling.md) — global `error`/`unhandledrejection` events, `reportError`, catching Java exceptions in JS (`error.nativeException`), forwarding JS throws to Java callers (`interop.escapeException`), JS stacks on Java exceptions (`com.tns.JavaScriptStackTrace`), configuration flags, and crash-reporter integration. -- [structuredClone](structured-clone.md) — the WHATWG `structuredClone(value, { transfer })` global: what clones, how graph identity and cycles are preserved, `ArrayBuffer` transfer, and the `DataCloneError`-named `Error` that stands in for `DOMException`. +- [structuredClone](structured-clone.md) — the WHATWG `structuredClone(value, { transfer })` global: what clones, how graph identity and cycles are preserved, `ArrayBuffer` transfer, and the `DataCloneError` `DOMException` on failure. - [Implementing additional Chrome DevTools protocol Domains](extending-inspector.md) ## Knowledge diff --git a/docs/abort-signal.md b/docs/abort-signal.md index 1ed543e94..dc863d69d 100644 --- a/docs/abort-signal.md +++ b/docs/abort-signal.md @@ -50,20 +50,20 @@ be dropped. accounting comes from an internal symbol-keyed hook the events builtin calls from every listener-list mutation path (add, remove, and `once` removal during dispatch); the key travels only through the builtin-only - `internals` object (see `test-app/runtime/src/main/cpp/js/README.md`) and - never reaches app code, so the accounting cannot be bypassed via a - captured `EventTarget.prototype.addEventListener`. + `require("internal/events")` tier (see + `test-app/runtime/src/main/cpp/js/README.md`) and never reaches app code, + so the accounting cannot be bypassed via a captured + `EventTarget.prototype.addEventListener`. Entries leave the persistent set on abort, on the last abort-listener removal, or when a composite loses its last source. +Default reasons are real `DOMException`s — `"AbortError"` for a plain abort, +`"TimeoutError"` for `timeout()` — so both `reason.name` and +`instanceof DOMException` checks work. + ## Deviations from Node / the web -- **No `DOMException`.** As with [structuredClone](structured-clone.md) and - the [Performance API](performance.md), default reasons are `Error` - instances with `name` patched: `"AbortError"` (default abort) and - `"TimeoutError"` (timeout). `instanceof DOMException` checks cannot work; - match on `reason.name`. - Abort events carry no `isTrusted` flag (the runtime's `Event` doesn't model it). diff --git a/docs/performance.md b/docs/performance.md index 09da26515..b01eb7c7f 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -92,11 +92,11 @@ Both paths produce the same two arguments with the same exactness. asynchronous relative to `mark()`/`measure()` but precedes timer callbacks scheduled in the same turn. Callback exceptions are routed to `reportError`, so one throwing observer does not starve the others. -- **No `DOMException`.** Errors the specs express as `DOMException` — the - `SyntaxError` for a missing mark name, the `InvalidModificationError` for - switching an observer between the `entryTypes` and `type` forms — are - `Error` instances with `name` patched. `err.name` checks work; - `instanceof DOMException` does not. +- Errors the specs express as `DOMException` — the `SyntaxError` for a + missing mark name, the `InvalidModificationError` for switching an + observer between the `entryTypes` and `type` forms — are real + `DOMException`s: both `err.name` and `instanceof DOMException` checks + work. - Browser-only surface is absent: no resource/navigation timing, no `eventCounts`, and no `PerformanceTiming`-attribute resolution in `measure()`. diff --git a/docs/structured-clone.md b/docs/structured-clone.md index 4f7877518..d0a0fbb84 100644 --- a/docs/structured-clone.md +++ b/docs/structured-clone.md @@ -48,7 +48,7 @@ Two differences are intentional: ## Deviations from the specification -- **`DataCloneError` is an `Error`, not a `DOMException`.** This runtime has no `DOMException`, so failures throw an `Error` whose `name` is set to `"DataCloneError"`. Detect failures with `e.name === "DataCloneError"`; `instanceof DOMException` cannot work. +- **`DataCloneError` is a `DOMException`.** Failures throw a `DOMException` named `"DataCloneError"`, from the JS argument checks and the native serializer alike, so both `e.name === "DataCloneError"` and `instanceof DOMException` detect them. (The serializer falls back to a `DataCloneError`-named `Error` only when the builtin can no longer run, e.g. during isolate teardown.) - **Only `ArrayBuffer` is transferable.** The spec's other transferable types — `MessagePort`, `ImageBitmap`, `ReadableStream` and friends — do not exist here. A non-`ArrayBuffer` in the transfer list is a `DataCloneError`. - **Host objects are not cloneable by `structuredClone`.** The spec leaves platform objects to each host; here every native/interop wrapper is rejected with a `DataCloneError`, because a JavaScript copy detached from its native counterpart would be a wrapper around nothing. Worker `postMessage` deliberately differs — see above. diff --git a/eslint.config.mjs b/eslint.config.mjs index a1bd9d493..d7d9cbf61 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,7 +1,7 @@ // Lint setup for the runtime's builtin JavaScript // (test-app/runtime/src/main/cpp/js). Each file is compiled by BuiltinLoader // as a FUNCTION BODY with the fixed parameters `exports`, `require`, `module`, -// `binding`, `primordials` and `internals` (see that directory's README.md), which are +// `binding` and `primordials` (see that directory's README.md), which are // declared as globals here. no-undef is the typo net for binding-bag destructures and // native-global usage alike; no-restricted-properties keeps the captured // intrinsics from being read off the live globals again. @@ -56,7 +56,6 @@ export default [ module: 'readonly', binding: 'readonly', primordials: 'readonly', - internals: 'readonly', global: 'readonly', console: 'readonly', URL: 'readonly', diff --git a/test-app/app/src/main/assets/app/mainpage.js b/test-app/app/src/main/assets/app/mainpage.js index 068a7a782..1d5d9a027 100644 --- a/test-app/app/src/main/assets/app/mainpage.js +++ b/test-app/app/src/main/assets/app/mainpage.js @@ -21,6 +21,8 @@ shared.runWorkerTests(); shared.runPerformanceTests(); shared.runStructuredCloneTests(); shared.runTextEncodingTests(); +shared.runDOMExceptionTests(); +shared.runEventsTests(); require("./tests/testWebAssembly"); require("./tests/testEventLoop"); require("./tests/testMultithreadedJavascript"); diff --git a/test-app/app/src/main/assets/app/shared b/test-app/app/src/main/assets/app/shared index 364cba6f2..9cc46c06b 160000 --- a/test-app/app/src/main/assets/app/shared +++ b/test-app/app/src/main/assets/app/shared @@ -1 +1 @@ -Subproject commit 364cba6f26f540a47e3c62a9029135218851f5a1 +Subproject commit 9cc46c06bc918d849a54d089842f5a42ebfbb6e6 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 d1f08f365..b93266f48 100644 --- a/test-app/app/src/main/assets/app/tests/testRuntimeImplementedAPIs.js +++ b/test-app/app/src/main/assets/app/tests/testRuntimeImplementedAPIs.js @@ -50,3 +50,23 @@ describe("structuredClone canary", function () { expect(typeof structuredClone).toBe("function"); }); }); + +// Same contract as above for the shared DOMException / CustomEvent suites: +// they self-gate, these unguarded specs turn absence into a failure. +describe("DOMException canary", function () { + it("is implemented by this runtime", function () { + expect(typeof DOMException).toBe("function"); + expect(new DOMException("x", "AbortError") instanceof Error).toBe(true); + }); + + it("is not reachable as a module from app code", function () { + expect(function () { require("internal/dom-exception"); }).toThrow(); + }); +}); + +describe("CustomEvent canary", function () { + it("is implemented by this runtime", function () { + expect(typeof CustomEvent).toBe("function"); + expect(new CustomEvent("x") instanceof Event).toBe(true); + }); +}); diff --git a/test-app/runtime/CMakeLists.txt b/test-app/runtime/CMakeLists.txt index 69536da1f..f638b92a1 100644 --- a/test-app/runtime/CMakeLists.txt +++ b/test-app/runtime/CMakeLists.txt @@ -71,6 +71,7 @@ set(RUNTIME_BUILTIN_JS ${RUNTIME_BUILTIN_JS_DIR}/abort-signal.js ${RUNTIME_BUILTIN_JS_DIR}/base64.js ${RUNTIME_BUILTIN_JS_DIR}/blob-url.js + ${RUNTIME_BUILTIN_JS_DIR}/dom-exception.js ${RUNTIME_BUILTIN_JS_DIR}/error-events.js ${RUNTIME_BUILTIN_JS_DIR}/events.js ${RUNTIME_BUILTIN_JS_DIR}/inspect.js diff --git a/test-app/runtime/src/main/cpp/BuiltinLoader.cpp b/test-app/runtime/src/main/cpp/BuiltinLoader.cpp index eae4ddd6c..290378a51 100644 --- a/test-app/runtime/src/main/cpp/BuiltinLoader.cpp +++ b/test-app/runtime/src/main/cpp/BuiltinLoader.cpp @@ -26,17 +26,15 @@ std::vector builtinCache[static_cast(BuiltinId::kCount)]; * parameters, mirroring Node's module wrapper: a file exports through * `module.exports`/`exports`, reaches sibling builtin modules through * `require`, natives arrive as properties of the `binding` bag (Node's - * internalBinding idiom), intrinsics as properties of `primordials` and - * cross-builtin capabilities as properties of `internals`; each file - * destructures what it needs. + * internalBinding idiom) and intrinsics as properties of `primordials`; each + * file destructures what it needs. */ constexpr const char* kExportsParamName = "exports"; constexpr const char* kRequireParamName = "require"; constexpr const char* kModuleParamName = "module"; constexpr const char* kBindingParamName = "binding"; constexpr const char* kPrimordialsParamName = "primordials"; -constexpr const char* kInternalsParamName = "internals"; -constexpr size_t kParamCount = 6; +constexpr size_t kParamCount = 5; /* * `module.exports` of every builtin that has run in this isolate, indexed by @@ -48,43 +46,19 @@ struct BuiltinExportsState { }; /* - * This runtime's intrinsics snapshot, builtin require and shared internals - * object. Per-runtime state rather than an isolate-keyed shared map, so - * reaching it needs no lock and it is released with the runtime, while the - * isolate is still alive. + * This runtime's intrinsics snapshot and builtin require. Per-runtime state + * rather than an isolate-keyed shared map, so reaching it needs no lock and + * it is released with the runtime, while the isolate is still alive. */ struct BuiltinRealm { v8::Global primordials; v8::Global builtinRequire; - v8::Global internals; }; /* - * Per-isolate `internals` object handed to every builtin: the private channel - * for cross-builtin capabilities (hook keys, setters) that must never reach - * app code. Producers publish during their init, consumers read during - * theirs, so PrepareV8Runtime's ordering is the dependency graph. - */ -MaybeLocal GetInternals(Local context) { - Isolate* isolate = v8::Isolate::GetCurrent(); - - auto* realm = RuntimeState::For(isolate); - if (realm == nullptr) { - return MaybeLocal(); - } - - if (!realm->internals.IsEmpty()) { - return realm->internals.Get(isolate); - } - - Local internals = Object::New(isolate); - realm->internals.Reset(isolate, internals); - return internals; -} - -/* - * The `require` every builtin receives: builtin specifiers only, so a builtin - * can never reach application code or the filesystem. + * The `require` every builtin receives: builtin specifiers only — including + * the internal tier app code can never name — so a builtin can never reach + * application code or the filesystem. */ void BuiltinRequireCallback(const FunctionCallbackInfo& info) { Isolate* isolate = info.GetIsolate(); @@ -99,7 +73,7 @@ void BuiltinRequireCallback(const FunctionCallbackInfo& info) { Local exports; if (NsBuiltinModules::GetExports(context, specifier).ToLocal(&exports)) { info.GetReturnValue().Set(exports); - } else if (!NsBuiltinModules::IsRegistered(specifier)) { + } else if (!NsBuiltinModules::IsRegistered(specifier, /* includeInternal */ true)) { isolate->ThrowException(Exception::Error(ArgConverter::ConvertToV8String( isolate, NsBuiltinModules::NotFoundMessage(specifier)))); } @@ -149,8 +123,7 @@ MaybeLocal CompileBuiltin(Local context, BuiltinId id) { ArgConverter::ConvertToV8String(isolate, kRequireParamName), ArgConverter::ConvertToV8String(isolate, kModuleParamName), ArgConverter::ConvertToV8String(isolate, kBindingParamName), - ArgConverter::ConvertToV8String(isolate, kPrimordialsParamName), - ArgConverter::ConvertToV8String(isolate, kInternalsParamName)}; + ArgConverter::ConvertToV8String(isolate, kPrimordialsParamName)}; Local fn; if (!blob.empty()) { @@ -189,7 +162,7 @@ MaybeLocal CompileBuiltin(Local context, BuiltinId id) { } MaybeLocal CallBuiltin(Local context, BuiltinId id, Local binding, - Local primordials, Local internals) { + Local primordials) { Isolate* isolate = v8::Isolate::GetCurrent(); Local fn; @@ -211,7 +184,7 @@ MaybeLocal CallBuiltin(Local context, BuiltinId id, Local Local args[] = {exportsObj, require, moduleObj, binding.IsEmpty() ? Undefined(isolate).As() : binding, - primordials, internals}; + primordials}; if (fn->Call(context, Undefined(isolate), static_cast(kParamCount), args).IsEmpty()) { return MaybeLocal(); } @@ -225,7 +198,7 @@ MaybeLocal CallBuiltin(Local context, BuiltinId id, Local * Builtins compiled later in the isolate's life get the same pristine * snapshot. */ -MaybeLocal GetPrimordials(Local context, Local internals) { +MaybeLocal GetPrimordials(Local context) { Isolate* isolate = v8::Isolate::GetCurrent(); auto* realm = RuntimeState::For(isolate); @@ -238,8 +211,7 @@ MaybeLocal GetPrimordials(Local context, Local internal } Local result; - if (!CallBuiltin(context, BuiltinId::kPrimordials, Local(), Undefined(isolate), - internals) + if (!CallBuiltin(context, BuiltinId::kPrimordials, Local(), Undefined(isolate)) .ToLocal(&result) || !result->IsObject()) { return MaybeLocal(); @@ -254,17 +226,12 @@ MaybeLocal GetPrimordials(Local context, Local internal MaybeLocal BuiltinLoader::RunBuiltin(Local context, BuiltinId id, Local binding) { - Local internals; - if (!GetInternals(context).ToLocal(&internals)) { - return MaybeLocal(); - } - Local primordials; - if (!GetPrimordials(context, internals).ToLocal(&primordials)) { + if (!GetPrimordials(context).ToLocal(&primordials)) { return MaybeLocal(); } - return CallBuiltin(context, id, binding, primordials, internals); + return CallBuiltin(context, id, binding, primordials); } MaybeLocal BuiltinLoader::GetExports(Local context, BuiltinId id, diff --git a/test-app/runtime/src/main/cpp/BuiltinLoader.h b/test-app/runtime/src/main/cpp/BuiltinLoader.h index 5db89ac63..e55c58f9a 100644 --- a/test-app/runtime/src/main/cpp/BuiltinLoader.h +++ b/test-app/runtime/src/main/cpp/BuiltinLoader.h @@ -18,15 +18,13 @@ class BuiltinLoader { /* * Compiles the builtin identified by id as a function body with the fixed * parameters `exports`, `require`, `module`, `binding` (Node's module - * wrapper plus its internalBinding idiom), `primordials` and `internals`, - * calls it with the given bag of natives (or undefined when omitted), this - * isolate's frozen intrinsics snapshot, and the isolate's shared internals - * object, and returns the resulting `module.exports`. `require` reaches - * the builtin modules (NsBuiltinModules) and nothing else. `internals` is - * one plain object per isolate handed identically to every builtin and - * never exposed anywhere app code can reach: the channel for - * cross-builtin capabilities (see the js README; interim until a - * Node-style private internal-module tier exists). + * wrapper plus its internalBinding idiom) and `primordials`, calls it + * with the given bag of natives (or undefined when omitted) and this + * isolate's frozen intrinsics snapshot, and returns the resulting + * `module.exports`. `require` reaches the builtin modules + * (NsBuiltinModules) — including the internal-only tier, which is also + * how builtins hand each other capabilities app code must not see — and + * nothing else. * The snapshot is produced by the kPrimordials builtin on first use * and cached per isolate, so it is taken before any user code can replace * a global. diff --git a/test-app/runtime/src/main/cpp/Events.cpp b/test-app/runtime/src/main/cpp/Events.cpp index 4602b2e3e..bfce5872b 100644 --- a/test-app/runtime/src/main/cpp/Events.cpp +++ b/test-app/runtime/src/main/cpp/Events.cpp @@ -1,5 +1,6 @@ #include "Events.h" +#include "ArgConverter.h" #include "BuiltinLoader.h" #include "NativeScriptException.h" #include "Runtime.h" @@ -9,24 +10,35 @@ using namespace tns; using namespace v8; void Events::Init(Local context) { + // The builtin installs Event/EventTarget and the global EventTarget + // methods; its exports carry the internal EventTarget instance backing the + // global (cached here so native dispatch survives app code overwriting + // globalThis.dispatchEvent) and the CustomEvent interface the lazy-global + // tier places. Run through GetExports so that tier's read shares this run. auto isolate = v8::Isolate::GetCurrent(); auto runtime = Runtime::TryGetRuntime(isolate); if (runtime == nullptr) { throw NativeScriptException("Events::Init: no runtime for isolate"); } - Local result; - if (!BuiltinLoader::RunBuiltin(context, BuiltinId::kEvents).ToLocal(&result) || - !result->IsObject()) { + Local exports; + if (!BuiltinLoader::GetExports(context, BuiltinId::kEvents, nullptr).ToLocal(&exports)) { + throw NativeScriptException("Events::Init: the event-primitives bootstrap failed"); + } + + Local globalEventTarget; + if (!exports->Get(context, ArgConverter::ConvertToV8String(isolate, "globalEventTarget")) + .ToLocal(&globalEventTarget) || + !globalEventTarget->IsObject()) { throw NativeScriptException("Events::Init: the event-primitives bootstrap did not return the backing target"); } - runtime->GlobalEventTarget().Reset(isolate, result.As()); + runtime->GlobalEventTarget().Reset(isolate, globalEventTarget.As()); // AbortController/AbortSignal (internal/abort-signal.js) build directly on // the event primitives installed above; the listener-mutation hook key for - // its GC-liveness accounting arrives through the shared `internals` - // parameter, published by the events builtin. + // its GC-liveness accounting comes from the events builtin's exports, via + // require("internal/events"). Local abortResult; if (!BuiltinLoader::RunBuiltin(context, BuiltinId::kAbortSignal).ToLocal(&abortResult)) { throw NativeScriptException("Events::Init: the abort-signal bootstrap failed"); diff --git a/test-app/runtime/src/main/cpp/LazyGlobals.cpp b/test-app/runtime/src/main/cpp/LazyGlobals.cpp index 5fbe09b48..c812eebd4 100644 --- a/test-app/runtime/src/main/cpp/LazyGlobals.cpp +++ b/test-app/runtime/src/main/cpp/LazyGlobals.cpp @@ -2,6 +2,7 @@ #include "ArgConverter.h" #include "Base64.h" +#include "BuiltinLoader.h" #include "TextEncoding.h" using namespace v8; @@ -23,11 +24,25 @@ struct LazyGlobalEntry { ExportsAccessor exports; }; +/* + * Exports accessor for a builtin with no natives of its own; modules with a + * binding (TextEncoding, Base64) own a hand-written accessor instead. + */ +template +MaybeLocal BuiltinExports(Local context) { + return BuiltinLoader::GetExports(context, id, nullptr); +} + constexpr LazyGlobalEntry kLazyGlobals[] = { {"TextEncoder", "TextEncoder", TextEncoding::GetExports}, {"TextDecoder", "TextDecoder", TextEncoding::GetExports}, {"atob", "atob", Base64::GetExports}, {"btoa", "btoa", Base64::GetExports}, + {"DOMException", "DOMException", BuiltinExports}, + // 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. + {"CustomEvent", "CustomEvent", BuiltinExports}, }; void LazyGlobalGetter(Local property, diff --git a/test-app/runtime/src/main/cpp/LazyGlobals.h b/test-app/runtime/src/main/cpp/LazyGlobals.h index e93383761..2349180d7 100644 --- a/test-app/runtime/src/main/cpp/LazyGlobals.h +++ b/test-app/runtime/src/main/cpp/LazyGlobals.h @@ -15,8 +15,9 @@ namespace tns { * with a plain data property so later reads cost nothing. * * A builtin behind this tier runs at an arbitrary point in the isolate's life - * rather than during init, so it may only consume `internals` keys published - * by eager builtins (see src/main/cpp/js/README.md). + * rather than during init, so anything it needs from a sibling builtin must + * come through `require` or its `binding`, never from init order (see + * src/main/cpp/js/README.md). */ class LazyGlobals { public: diff --git a/test-app/runtime/src/main/cpp/NsBuiltinModules.cpp b/test-app/runtime/src/main/cpp/NsBuiltinModules.cpp index 60be4a93b..467917d93 100644 --- a/test-app/runtime/src/main/cpp/NsBuiltinModules.cpp +++ b/test-app/runtime/src/main/cpp/NsBuiltinModules.cpp @@ -37,13 +37,19 @@ struct Registration { * `node:` shim, which reaches its `ns:` module through require instead). */ BuiltinLoader::BindingFactory binding; + /* + * The internal tier (src/main/cpp/js/README.md): resolvable only through + * the require builtins receive, never through the module system, so the + * file's exports can carry capabilities app code must not reach. + */ + bool internalOnly = false; }; /* - * The v1 registry (docs/ns-builtin-modules.md). One specifier, one source - * file: a `node:` shim is its own builtin that requires the `ns:` module it - * adapts, so the two module objects stay distinct and the standard module - * never carries compatibility code. + * The v1 registry (docs/ns-builtin-modules.md) plus the internal tier. One + * specifier, one source file: a `node:` shim is its own builtin that requires + * the `ns:` module it adapts, so the two module objects stay distinct and the + * standard module never carries compatibility code. */ constexpr Registration kRegistry[] = { {"ns:module", BuiltinId::kNsModule, NsModuleBinding}, @@ -52,6 +58,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/events", BuiltinId::kEvents, nullptr, true}, }; constexpr const char* kDebugKey = "debug"; @@ -303,8 +311,9 @@ bool NsBuiltinModules::IsNsScheme(const std::string& specifier) { return HasPrefix(specifier, kNsPrefix); } -bool NsBuiltinModules::IsRegistered(const std::string& specifier) { - return Find(specifier) != nullptr; +bool NsBuiltinModules::IsRegistered(const std::string& specifier, bool includeInternal) { + const Registration* registration = Find(specifier); + return registration != nullptr && (includeInternal || !registration->internalOnly); } std::string NsBuiltinModules::NotFoundMessage(const std::string& specifier) { @@ -357,6 +366,15 @@ MaybeLocal NsBuiltinModules::GetExports(Local context, MaybeLocal NsBuiltinModules::GetModule(Local context, const std::string& specifier) { + /* + * The module system is the app-facing entry point; the internal tier only + * exists behind the require builtins receive. + */ + const Registration* registration = Find(specifier); + if (registration == nullptr || registration->internalOnly) { + return MaybeLocal(); + } + Isolate* isolate = v8::Isolate::GetCurrent(); RealmState* realmState = GetRealm(isolate); if (realmState == nullptr) { diff --git a/test-app/runtime/src/main/cpp/NsBuiltinModules.h b/test-app/runtime/src/main/cpp/NsBuiltinModules.h index d9d757c55..0b0067388 100644 --- a/test-app/runtime/src/main/cpp/NsBuiltinModules.h +++ b/test-app/runtime/src/main/cpp/NsBuiltinModules.h @@ -28,8 +28,13 @@ class NsBuiltinModules { */ static bool IsNsScheme(const std::string& specifier); - // Whether a module of that name exists. - static bool IsRegistered(const std::string& specifier); + /* + * Whether the specifier names a registered builtin module. Internal-tier + * rows (see kRegistry) only count when includeInternal is set, which is + * the builtin require's privilege; every app-facing caller keeps the + * default. + */ + static bool IsRegistered(const std::string& specifier, bool includeInternal = false); /* * Frozen exports object of a registered specifier. Empty when the diff --git a/test-app/runtime/src/main/cpp/StructuredSerialization.cpp b/test-app/runtime/src/main/cpp/StructuredSerialization.cpp index b3fb4fac9..3da795333 100644 --- a/test-app/runtime/src/main/cpp/StructuredSerialization.cpp +++ b/test-app/runtime/src/main/cpp/StructuredSerialization.cpp @@ -3,6 +3,7 @@ #include "NativeScriptAssert.h" #include "ArgConverter.h" +#include "BuiltinLoader.h" using namespace v8; @@ -11,6 +12,37 @@ namespace serialization { void ThrowDataCloneError(Isolate* isolate, const std::string& message) { Local context = isolate->GetCurrentContext(); + + /* + * The spec's DataCloneError is a DOMException; build it through the + * builtin's exports cache so native and JS throw sites produce the same + * class. Delegates may call into JS here — V8 allows it, and Node's + * serializer delegates do the same. The fallback covers a builtin that + * can no longer run (isolate teardown, broken realm). + */ + Local domException; + { + TryCatch tc(isolate); + Local exports; + Local ctor; + if (BuiltinLoader::GetExports(context, BuiltinId::kDomException, nullptr) + .ToLocal(&exports) && + exports->Get(context, ArgConverter::ConvertToV8String(isolate, "DOMException")) + .ToLocal(&ctor) && + ctor->IsFunction()) { + Local args[] = {ArgConverter::ConvertToV8String(isolate, message), + ArgConverter::ConvertToV8String(isolate, "DataCloneError")}; + Local instance; + if (ctor.As()->NewInstance(context, 2, args).ToLocal(&instance)) { + domException = instance; + } + } + } + if (!domException.IsEmpty()) { + isolate->ThrowException(domException); + return; + } + Local error = Exception::Error(ArgConverter::ConvertToV8String(isolate, message)); bool success = diff --git a/test-app/runtime/src/main/cpp/StructuredSerialization.h b/test-app/runtime/src/main/cpp/StructuredSerialization.h index fac8d20de..43c1c0fbb 100644 --- a/test-app/runtime/src/main/cpp/StructuredSerialization.h +++ b/test-app/runtime/src/main/cpp/StructuredSerialization.h @@ -28,9 +28,9 @@ enum class HostObjectPolicy { }; /* - * Throws the runtime's DataCloneError. There is no DOMException here, so it is - * an Error carrying that name — the shape the shared cross-runtime suite - * detects clone failures by. + * Throws a "DataCloneError" DOMException, the same class the JS half of + * structuredClone raises. Falls back to a "DataCloneError"-named Error when + * the dom-exception builtin cannot run. */ void ThrowDataCloneError(v8::Isolate* isolate, const std::string& message); diff --git a/test-app/runtime/src/main/cpp/js/README.md b/test-app/runtime/src/main/cpp/js/README.md index 4d3ef822e..8e0b5f5a0 100644 --- a/test-app/runtime/src/main/cpp/js/README.md +++ b/test-app/runtime/src/main/cpp/js/README.md @@ -9,8 +9,8 @@ build time a CMake custom command runs `tools/js2c.mjs`, which embeds them into ## Contract (Node's module wrapper + internalBinding idiom) Every file is compiled as a **function body** via `v8::ScriptCompiler::CompileFunction` -with the fixed parameters `exports`, `require`, `module`, `binding`, -`primordials` and `internals`: +with the fixed parameters `exports`, `require`, `module`, `binding` and +`primordials`: ```js const { someNative, anotherNative } = binding; @@ -27,20 +27,21 @@ module.exports = somethingTheCallSiteNeeds; `No such built-in module: `. It is how a `node:` shim consumes the `ns:` module it adapts, and it materializes that module on first use. Requiring a module that is still loading throws rather than recursing. + It also resolves the **internal tier** (`internal/events`, + `internal/dom-exception`): registry rows marked internal-only in + `NsBuiltinModules.cpp` that the module system refuses, so only builtins can + name them — Node's internal-module idiom. This is the one channel for + cross-builtin capabilities that must never leak to app code (the + `kListenerChanged` hook key abort-signal.js takes from events.js, the + `setListenerErrorReporter` setter error-events.js calls): the producer puts + the capability in its `module.exports`, the consumer requires it. + `require("internal/…")` at first use runs the file through the shared + exports cache — for a consumer of an eager producer that is a cache hit, + and a miss runs the producer on demand. A consumer can therefore never + observe a missing capability: it gets the exports, or the require throws + (circular require, or the producer failing to run). - `primordials` is the frozen intrinsics snapshot built by `primordials.js` (see below), the same object for every builtin in an isolate. -- `internals` is one plain per-isolate object handed identically to every - builtin and reachable from nowhere else — the private channel for - cross-builtin capabilities that must never leak to app code (the - `kListenerChanged` hook key events.js publishes for abort-signal.js, the - `setListenerErrorReporter` setter error-events.js calls). Producers - publish during their init, consumers read during theirs, so the - `PrepareV8Runtime` ordering is the dependency graph; a missing key fails - loudly at init, not at first use. **Interim mechanism**: if cross-builtin - needs outgrow one shared object (many producers, lazy consumers), migrate - to a Node-style private internal-module tier — `require("internal/…")` - resolved for builtins only, never through the public `ns:`/`node:` - registry — and fold `internals` into it. - **`module.exports` is the export channel** — whatever it holds when the file finishes is what `RunBuiltin` hands back to C++ (used for factory functions and init results). Both CommonJS styles work: replace the whole export with @@ -78,17 +79,20 @@ the property with a plain data property. That cache is the same one the `ns:`/`node:` module registry uses, so a module re-exporting a lazy builtin's interfaces (`ns:util`'s `TextEncoder`) hands out the objects the globals hold, in either access order. Until then nothing of it exists — no compile, no run, -no allocation. `text-encoding.js` (`TextEncoder`/`TextDecoder`) and -`base64.js` (`atob`/`btoa`) are the current ones; new globals join by adding a -row to `kLazyGlobals`. +no allocation. `text-encoding.js` (`TextEncoder`/`TextDecoder`), `base64.js` +(`atob`/`btoa`) and `dom-exception.js` (`DOMException`) are the current ones; +new globals join by adding a row to `kLazyGlobals`. + +An **eager** file can also feed the tier: `events.js` (eager, `Events::Init`) +exports `CustomEvent`, and the `CustomEvent` row reads it through the same +exports cache — the run happened at init, so only the placement is lazy. The two extra rules a lazy builtin lives by: -- **It runs at an arbitrary point in the isolate's life, not at init.** The - `internals` channel is therefore off limits: its producers publish during - their own init, and a consumer that reads a key it does not find fails at - first use instead of loudly at boot. Anything a lazy builtin needs from - another builtin has to come through `require` or its `binding`. +- **It runs at an arbitrary point in the isolate's life, not at init.** + Anything it needs from another builtin has to come through `require` + (including the internal tier) or its `binding` — never from init-order + assumptions. - **It must not install anything on `globalThis`.** The C++ tier owns placement; a file that self-installs would have to run to do it, which is the thing being avoided. diff --git a/test-app/runtime/src/main/cpp/js/abort-signal.js b/test-app/runtime/src/main/cpp/js/abort-signal.js index bc31851b3..2936e0d66 100644 --- a/test-app/runtime/src/main/cpp/js/abort-signal.js +++ b/test-app/runtime/src/main/cpp/js/abort-signal.js @@ -16,15 +16,13 @@ // fires from every listener-list mutation path and cannot be bypassed // from app code. // -// Deliberate deviation from Node: no DOMException in this runtime — the -// default abort and timeout reasons are Error instances with `name` patched -// ("AbortError" / "TimeoutError"), the same stand-in performance.js and -// structured-clone.js use. +// The default abort and timeout reasons are DOMExceptions ("AbortError" / +// "TimeoutError"), required from the internal tier on first use so an app +// that never aborts never runs the dom-exception builtin. const { ArrayPrototypeIndexOf, ArrayPrototypePush, ArrayPrototypeSplice, - Error, FinalizationRegistry, FinalizationRegistryPrototypeRegister, FinalizationRegistryPrototypeUnregister, @@ -54,23 +52,31 @@ const dispatchEvent = EventTarget.prototype.dispatchEvent; const addEventListener = EventTarget.prototype.addEventListener; const removeEventListener = EventTarget.prototype.removeEventListener; // Published by events.js: the symbol under which EventTargetImpl looks up -// the listener-mutation hook. -const kListenerChanged = internals.kListenerChanged; +// the listener-mutation hook. events.js already ran (Events::Init), so this +// require is a cache hit; a miss would run it on demand rather than fail. +const { kListenerChanged } = require("internal/events"); // Construction token: AbortSignal instances come only from the factories in // this module (the controller, and the abort/timeout/any statics). const kInternal = {}; +let DOMException; +function getDOMException() { + if (DOMException === undefined) { + ({ DOMException } = require("internal/dom-exception")); + } + return DOMException; +} + function abortError() { - const e = new Error("This operation was aborted"); - e.name = "AbortError"; - return e; + return new (getDOMException())("This operation was aborted", "AbortError"); } function timeoutError() { - const e = new Error("The operation was aborted due to timeout"); - e.name = "TimeoutError"; - return e; + return new (getDOMException())( + "The operation was aborted due to timeout", + "TimeoutError" + ); } // The strong holds described in the header. Entries leave on abort, on the diff --git a/test-app/runtime/src/main/cpp/js/base64.js b/test-app/runtime/src/main/cpp/js/base64.js index 3fa075423..70bb423c5 100644 --- a/test-app/runtime/src/main/cpp/js/base64.js +++ b/test-app/runtime/src/main/cpp/js/base64.js @@ -4,21 +4,23 @@ // // This file exports the two functions instead of installing them; the C++ // lazy-global tier (LazyGlobals) places them and is what runs this file, on -// the first read of either name. Nothing here may depend on a builtin that -// runs after it, so `internals` is off limits — see the README. +// the first read of either name. Anything needed from a sibling builtin +// comes through `require` or `binding`, never init order — see the README. // -// Deliberate deviation from the spec: no DOMException in this runtime, so the -// failure is an Error with `name` patched to "InvalidCharacterError", the same -// stand-in abort-signal.js and performance.js use. The native ops answer null -// on failure rather than throwing, so that shape stays here. -const { Error, TypeError } = primordials; +// The native ops answer null on failure rather than throwing, so the +// exception shape — an "InvalidCharacterError" DOMException, per the HTML +// spec — stays here, required on first failure so well-formed input never +// runs the dom-exception builtin. +const { TypeError } = primordials; const { atob: decodeBase64, btoa: encodeBase64 } = binding; +let DOMException; function invalidCharacterError() { - const e = new Error("Invalid character"); - e.name = "InvalidCharacterError"; - return e; + if (DOMException === undefined) { + ({ DOMException } = require("internal/dom-exception")); + } + return new DOMException("Invalid character", "InvalidCharacterError"); } function btoa(data) { diff --git a/test-app/runtime/src/main/cpp/js/dom-exception.js b/test-app/runtime/src/main/cpp/js/dom-exception.js new file mode 100644 index 000000000..e8821d6fa --- /dev/null +++ b/test-app/runtime/src/main/cpp/js/dom-exception.js @@ -0,0 +1,144 @@ +"use strict"; +// DOMException (Web IDL Standard §4.3), the error type the web platform uses +// for named failures. Modeled on Node's per-context implementation: a plain +// class whose prototype is grafted onto Error.prototype, with the readonly +// name/message/code attributes as branded prototype getters (the private +// fields double as the Web IDL brand check). +// +// Lazy builtin: LazyGlobals places the global, and sibling builtins reach the +// 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. +const { + ErrorCaptureStackTrace, + ErrorPrototype, + ObjectDefineProperty, + ObjectSetPrototypeOf, + SymbolToStringTag, +} = primordials; + +// 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 = { + __proto__: null, + IndexSizeError: 1, + DOMStringSizeError: 2, + HierarchyRequestError: 3, + WrongDocumentError: 4, + InvalidCharacterError: 5, + NoDataAllowedError: 6, + NoModificationAllowedError: 7, + NotFoundError: 8, + NotSupportedError: 9, + InUseAttributeError: 10, + InvalidStateError: 11, + SyntaxError: 12, + InvalidModificationError: 13, + NamespaceError: 14, + InvalidAccessError: 15, + ValidationError: 16, + TypeMismatchError: 17, + SecurityError: 18, + NetworkError: 19, + AbortError: 20, + URLMismatchError: 21, + QuotaExceededError: 22, + TimeoutError: 23, + InvalidNodeTypeError: 24, + DataCloneError: 25, +}; + +class DOMException { + #name; + #message; + + // The `= ""` / `= "Error"` defaults also give the constructor the arity the + // IDL requires (0: both arguments optional). + constructor(message = "", name = "Error") { + this.#message = `${message}`; + this.#name = `${name}`; + ErrorCaptureStackTrace(this, DOMException); + } + + get name() { + return this.#name; + } + + get message() { + return this.#message; + } + + get code() { + const code = nameToCode[this.#name]; + return code === undefined ? 0 : code; + } +} + +// Web IDL inheritance: DOMException.prototype's parent is Error.prototype, so +// instanceof Error holds and Error.prototype.toString renders +// "name: message" through the getters above. +ObjectSetPrototypeOf(DOMException.prototype, ErrorPrototype); + +// Class getters are non-enumerable; the IDL attributes are enumerable. +for (const key of ["name", "message", "code"]) { + ObjectDefineProperty(DOMException.prototype, key, { + __proto__: null, + enumerable: true, + }); +} + +ObjectDefineProperty(DOMException.prototype, SymbolToStringTag, { + __proto__: null, + configurable: true, + value: "DOMException", +}); + +// The legacy code constants, on the interface object and on the prototype +// (Web IDL §3.7.5: both, { writable: false, enumerable: true, +// configurable: false }). +const constants = { + __proto__: null, + INDEX_SIZE_ERR: 1, + DOMSTRING_SIZE_ERR: 2, + HIERARCHY_REQUEST_ERR: 3, + WRONG_DOCUMENT_ERR: 4, + INVALID_CHARACTER_ERR: 5, + NO_DATA_ALLOWED_ERR: 6, + NO_MODIFICATION_ALLOWED_ERR: 7, + NOT_FOUND_ERR: 8, + NOT_SUPPORTED_ERR: 9, + INUSE_ATTRIBUTE_ERR: 10, + INVALID_STATE_ERR: 11, + SYNTAX_ERR: 12, + INVALID_MODIFICATION_ERR: 13, + NAMESPACE_ERR: 14, + INVALID_ACCESS_ERR: 15, + VALIDATION_ERR: 16, + TYPE_MISMATCH_ERR: 17, + SECURITY_ERR: 18, + NETWORK_ERR: 19, + ABORT_ERR: 20, + URL_MISMATCH_ERR: 21, + QUOTA_EXCEEDED_ERR: 22, + TIMEOUT_ERR: 23, + INVALID_NODE_TYPE_ERR: 24, + DATA_CLONE_ERR: 25, +}; + +for (const key in constants) { + const descriptor = { + __proto__: null, + value: constants[key], + writable: false, + enumerable: true, + configurable: false, + }; + ObjectDefineProperty(DOMException, key, descriptor); + ObjectDefineProperty(DOMException.prototype, key, descriptor); +} + +module.exports = { DOMException }; diff --git a/test-app/runtime/src/main/cpp/js/error-events.js b/test-app/runtime/src/main/cpp/js/error-events.js index 6f4e3e844..c1d9f6539 100644 --- a/test-app/runtime/src/main/cpp/js/error-events.js +++ b/test-app/runtime/src/main/cpp/js/error-events.js @@ -29,7 +29,7 @@ PromiseRejectionEvent.prototype.constructor = PromiseRejectionEvent; // A listener that throws must not stop other listeners: route the thrown // value to the native fatal tail instead of ever recursively dispatching // another `error` event from inside dispatch. -internals.setListenerErrorReporter(function (e) { +require("internal/events").setListenerErrorReporter(function (e) { try { nativeReportFatal(e, (e && e.stack) || ""); } catch (ignored) {} }); diff --git a/test-app/runtime/src/main/cpp/js/events.js b/test-app/runtime/src/main/cpp/js/events.js index 4430b2656..7cb43312d 100644 --- a/test-app/runtime/src/main/cpp/js/events.js +++ b/test-app/runtime/src/main/cpp/js/events.js @@ -6,6 +6,7 @@ const { ArrayPrototypeSplice, FunctionPrototypeCall, ObjectCreate, + ObjectDefineProperty, String, } = primordials; var g = globalThis; @@ -34,21 +35,20 @@ Event.prototype.stopImmediatePropagation = function () { // A listener that throws must not stop other listeners: route the thrown // value to the native fatal tail instead of ever recursively dispatching // another `error` event from inside dispatch. The error-events layer -// installs the real reporter via internals.setListenerErrorReporter (before -// any user code runs); until then a thrown listener is swallowed. +// installs the real reporter through this file's exports (before any user +// code runs); until then a thrown listener is swallowed. var reportListenerError = function (e) {}; -internals.setListenerErrorReporter = function (fn) { +function setListenerErrorReporter(fn) { reportListenerError = fn; -}; +} // Internal listener-mutation hook. A target (in practice: AbortSignal, on // its prototype) may carry a function under this symbol; it is called with // (target, type, newCount) from every path that changes a listener list — // add, remove, and the once-splice inside dispatch. The key travels only -// through `internals`, so the accounting cannot be bypassed the way an -// overridable addEventListener could. +// through require("internal/events"), so the accounting cannot be bypassed +// the way an overridable addEventListener could. var kListenerChanged = Symbol("listenerChanged"); -internals.kListenerChanged = kListenerChanged; function notifyListenerChanged(target, type, count) { var hook = target[kListenerChanged]; if (hook !== undefined) { hook(target, type, count); } @@ -146,4 +146,39 @@ EventTarget.prototype.dispatchEvent = EventTargetImpl.prototype.dispatchEvent; g.Event = Event; g.EventTarget = EventTarget; -module.exports = globalTarget; +// CustomEvent (DOM Standard §2.4): Event carrying an app-supplied `detail`. +// Defined here so it extends the same Event the globals hold, but NOT +// installed eagerly — the lazy-global tier (LazyGlobals) places it from this +// file's exports on the first read of the name, sharing the init-time run +// through the exports cache. +function CustomEvent(type, opts) { + FunctionPrototypeCall(Event, this, type, opts); + opts = opts || {}; + // `detail` is readonly in the IDL, unlike the base Event's mutable-by-need + // fields (target/defaultPrevented change during dispatch). + ObjectDefineProperty(this, "detail", { + value: opts.detail !== undefined ? opts.detail : null, + writable: false, + enumerable: true, + configurable: true, + }); +} +CustomEvent.prototype = ObjectCreate(Event.prototype); +ObjectDefineProperty(CustomEvent.prototype, "constructor", { + value: CustomEvent, + writable: true, + configurable: true, +}); + +// Consumed by Events::Init, the lazy-global tier (CustomEvent), and sibling +// builtins via require("internal/events"). The module system refuses that +// specifier, so the capabilities here never reach app code. +module.exports = { + // The EventTarget instance backing the global listener methods; Events::Init + // caches it so native dispatch survives app code overwriting + // globalThis.dispatchEvent. + globalEventTarget: globalTarget, + CustomEvent: CustomEvent, + kListenerChanged: kListenerChanged, + setListenerErrorReporter: setListenerErrorReporter, +}; diff --git a/test-app/runtime/src/main/cpp/js/performance.js b/test-app/runtime/src/main/cpp/js/performance.js index 9059f251a..84a7621d4 100644 --- a/test-app/runtime/src/main/cpp/js/performance.js +++ b/test-app/runtime/src/main/cpp/js/performance.js @@ -7,13 +7,10 @@ // else is portable JS, kept in sync with the iOS runtime's copy of this file, // which runs against the same bag. // -// Deliberate deviations from the specs: -// - Observer callbacks are delivered from a microtask rather than a queued -// task. Delivery is still asynchronous relative to mark()/measure(), but it -// precedes timer callbacks scheduled in the same turn. -// - Failures the specs express as DOMException (SyntaxError, -// InvalidModificationError) are Error instances with `name` patched — -// DOMException does not exist in this runtime. +// Deliberate deviation from the specs: observer callbacks are delivered from +// a microtask rather than a queued task. Delivery is still asynchronous +// relative to mark()/measure(), but it precedes timer callbacks scheduled in +// the same turn. const { now, timeOrigin } = binding; const { ArrayPrototypeIndexOf, @@ -21,7 +18,6 @@ const { ArrayPrototypeSlice, ArrayPrototypeSort, ArrayPrototypeSplice, - Error, FunctionPrototypeCall, Number, NumberIsFinite, @@ -60,11 +56,15 @@ function illegalConstructor() { return new TypeError("Illegal constructor"); } -// SyntaxError / InvalidModificationError stand-in (see header). +// The specs' SyntaxError / InvalidModificationError / DataCloneError +// failures, required from the internal tier on first use so the +// dom-exception builtin only ever runs on a throw. +let DOMException; function domException(message, name) { - const e = new Error(message); - e.name = name; - return e; + if (DOMException === undefined) { + ({ DOMException } = require("internal/dom-exception")); + } + return new DOMException(message, name); } // WebIDL sequence conversion: only an object with a callable diff --git a/test-app/runtime/src/main/cpp/js/primordials.js b/test-app/runtime/src/main/cpp/js/primordials.js index cb5c81a73..51b5600b6 100644 --- a/test-app/runtime/src/main/cpp/js/primordials.js +++ b/test-app/runtime/src/main/cpp/js/primordials.js @@ -39,10 +39,12 @@ const intrinsics = { SymbolToStringTag: Symbol.toStringTag, // Namespaces / prototypes. + ErrorPrototype: Error.prototype, ObjectPrototype: Object.prototype, // Statics. ArrayBufferIsView: ArrayBuffer.isView, + ErrorCaptureStackTrace: Error.captureStackTrace, ArrayIsArray: Array.isArray, decodeURIComponent, JSONStringify: JSON.stringify, @@ -59,6 +61,7 @@ const intrinsics = { ObjectGetPrototypeOf: Object.getPrototypeOf, ObjectIs: Object.is, ObjectKeys: Object.keys, + ObjectSetPrototypeOf: Object.setPrototypeOf, // Instance methods, uncurried. ArrayPrototypeForEach: uncurryThis(Array.prototype.forEach), diff --git a/test-app/runtime/src/main/cpp/js/structured-clone.js b/test-app/runtime/src/main/cpp/js/structured-clone.js index 3b3243f43..ff25ec285 100644 --- a/test-app/runtime/src/main/cpp/js/structured-clone.js +++ b/test-app/runtime/src/main/cpp/js/structured-clone.js @@ -4,19 +4,17 @@ // WebIDL sequence handling for `transfer`; the clone itself is native // (v8::ValueSerializer round-tripped in this isolate). // -// Deviations from the HTML spec, both forced by the platform: -// - There is no DOMException here, so a clone failure throws an Error whose -// `name` is "DataCloneError" (same shape as the native-exception errors in -// docs/error-handling.md). `instanceof DOMException` checks cannot work. -// - Only ArrayBuffers are transferable. MessagePort, ImageBitmap and the -// native/interop wrapper objects have no serialization form in this runtime, -// so they are rejected rather than half-supported. +// Deviation from the HTML spec, forced by the platform: only ArrayBuffers are +// transferable. MessagePort, ImageBitmap and the native/interop wrapper +// objects have no serialization form in this runtime, so they are rejected +// rather than half-supported. Clone failures are "DataCloneError" +// DOMExceptions, from here and from the native serializer alike +// (StructuredSerialization.cpp builds the same class). const { clone } = binding; const { ArrayBufferPrototypeGetByteLength, ArrayPrototypePush, - Error, FunctionPrototypeCall, SymbolIterator, TypeError, @@ -24,10 +22,12 @@ const { var g = globalThis; +let DOMException; function dataCloneError(message) { - var e = new Error(message); - e.name = "DataCloneError"; - return e; + if (DOMException === undefined) { + ({ DOMException } = require("internal/dom-exception")); + } + return new DOMException(message, "DataCloneError"); } // Brand check through the captured byteLength getter: it is the one thing only diff --git a/test-app/runtime/src/main/cpp/js/text-encoding.js b/test-app/runtime/src/main/cpp/js/text-encoding.js index 0bf080119..12751af1d 100644 --- a/test-app/runtime/src/main/cpp/js/text-encoding.js +++ b/test-app/runtime/src/main/cpp/js/text-encoding.js @@ -4,8 +4,8 @@ // // This file exports the two interfaces instead of installing them; the C++ // lazy-global tier (LazyGlobals) places them and is what runs this file, on -// the first read of either name. Nothing here may depend on a builtin that -// runs after it, so `internals` is off limits — see the README. +// the first read of either name. Anything needed from a sibling builtin +// comes through `require` or `binding`, never init order — see the README. // // The supported encodings (utf-8, utf-16le, utf-16be, windows-1252) with // their complete label sets, the decoders and the UTF-8 encoder all live in