From ebb100d1ddbb8a2f52d36e6e7ebf90c12e5a47db Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Tue, 25 Aug 2026 18:42:05 -0300 Subject: [PATCH] feat: add DOMException and CustomEvent as lazy globals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port of NativeScript/ios#452, the next two items of the web-globals plan, both behind the lazy-global tier. DOMException (Web IDL §4.3) is a new lazy builtin (dom-exception.js, shared verbatim with iOS): a class grafted onto Error.prototype with branded enumerable name/message/code prototype accessors, the full legacy code table, the 25 constants on interface object and prototype, @@toStringTag and stack capture. LazyGlobals places it on first read; until then nothing runs or allocates. Sibling builtins construct DOMExceptions through a new internal-only specifier tier: kRegistry rows flagged internalOnly resolve through the require builtins receive and nowhere else (the module system refuses them, and a canary pins that app code cannot name them). All five stand-in throw sites now produce real DOMExceptions, required at first throw so a clean path never runs the builtin: abort-signal.js (AbortError/TimeoutError reasons), performance.js (SyntaxError/InvalidModificationError), structured-clone.js and StructuredSerialization.cpp (DataCloneError, the native serializer keeping the name-patched-Error shape as a teardown fallback), and base64.js (InvalidCharacterError). With the tier in place the interim `internals` wrapper parameter had exactly two users left; both moved into events.js's exports behind internal/events (kListenerChanged for abort-signal's GC accounting, setListenerErrorReporter for error-events). The builtin wrapper is back to Node's five parameters (exports, require, module, binding, primordials). CustomEvent (DOM §2.4) is defined in events.js next to the Event it extends, exported rather than installed: Events::Init now runs the file through BuiltinLoader::GetExports and reads the backing EventTarget from the exports bag, so the lazy CustomEvent row is a cache hit — only the placement is deferred. Tests: shared submodule bumped to 9cc46c06 (self-gating DOMException and CustomEvent suites plus integration specs), both suites wired into mainpage.js, and unguarded canaries added so this runtime regressing the globals fails instead of skipping. Full suite: 1203 specs, 0 failures on arm64 API 33. --- docs/README.md | 4 +- docs/abort-signal.md | 16 +- docs/performance.md | 10 +- docs/structured-clone.md | 2 +- eslint.config.mjs | 3 +- test-app/app/src/main/assets/app/mainpage.js | 2 + test-app/app/src/main/assets/app/shared | 2 +- .../app/tests/testRuntimeImplementedAPIs.js | 20 +++ test-app/runtime/CMakeLists.txt | 1 + .../runtime/src/main/cpp/BuiltinLoader.cpp | 67 +++----- test-app/runtime/src/main/cpp/BuiltinLoader.h | 16 +- test-app/runtime/src/main/cpp/Events.cpp | 24 ++- test-app/runtime/src/main/cpp/LazyGlobals.cpp | 15 ++ test-app/runtime/src/main/cpp/LazyGlobals.h | 5 +- .../runtime/src/main/cpp/NsBuiltinModules.cpp | 30 +++- .../runtime/src/main/cpp/NsBuiltinModules.h | 9 +- .../src/main/cpp/StructuredSerialization.cpp | 32 ++++ .../src/main/cpp/StructuredSerialization.h | 6 +- test-app/runtime/src/main/cpp/js/README.md | 48 +++--- .../runtime/src/main/cpp/js/abort-signal.js | 32 ++-- test-app/runtime/src/main/cpp/js/base64.js | 22 +-- .../runtime/src/main/cpp/js/dom-exception.js | 144 ++++++++++++++++++ .../runtime/src/main/cpp/js/error-events.js | 2 +- test-app/runtime/src/main/cpp/js/events.js | 51 ++++++- .../runtime/src/main/cpp/js/performance.js | 24 +-- .../runtime/src/main/cpp/js/primordials.js | 3 + .../src/main/cpp/js/structured-clone.js | 22 +-- .../runtime/src/main/cpp/js/text-encoding.js | 4 +- 28 files changed, 440 insertions(+), 176 deletions(-) create mode 100644 test-app/runtime/src/main/cpp/js/dom-exception.js 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