Skip to content

Commit ebb100d

Browse files
committed
feat: add DOMException and CustomEvent as lazy globals
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.
1 parent f69b684 commit ebb100d

28 files changed

Lines changed: 440 additions & 176 deletions

docs/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@
1212
(`AbortController`, `AbortSignal` with the `abort`/`timeout`/`any` statics)
1313
layered on the runtime's `EventTarget`, the GC contract (weak timers and
1414
`any()` links, listener-driven persistence), and the `DOMException`
15-
stand-in (name-patched `Error` reasons).
15+
reasons.
1616
- [TextEncoder / TextDecoder and atob / btoa](text-encoding.md) — the WHATWG
1717
encoding and base64 globals (`TextEncoder`, `TextDecoder`, `atob`, `btoa`),
1818
the supported encodings with their label sets, streaming decode semantics,
1919
and the lazy-global tier that runs their builtins only on first use.
2020
- [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.
21-
- [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`.
21+
- [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.
2222
- [Implementing additional Chrome DevTools protocol Domains](extending-inspector.md)
2323

2424
## Knowledge

docs/abort-signal.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,20 +50,20 @@ be dropped.
5050
accounting comes from an internal symbol-keyed hook the events builtin
5151
calls from every listener-list mutation path (add, remove, and `once`
5252
removal during dispatch); the key travels only through the builtin-only
53-
`internals` object (see `test-app/runtime/src/main/cpp/js/README.md`) and
54-
never reaches app code, so the accounting cannot be bypassed via a
55-
captured `EventTarget.prototype.addEventListener`.
53+
`require("internal/events")` tier (see
54+
`test-app/runtime/src/main/cpp/js/README.md`) and never reaches app code,
55+
so the accounting cannot be bypassed via a captured
56+
`EventTarget.prototype.addEventListener`.
5657

5758
Entries leave the persistent set on abort, on the last abort-listener
5859
removal, or when a composite loses its last source.
5960

61+
Default reasons are real `DOMException`s — `"AbortError"` for a plain abort,
62+
`"TimeoutError"` for `timeout()` — so both `reason.name` and
63+
`instanceof DOMException` checks work.
64+
6065
## Deviations from Node / the web
6166

62-
- **No `DOMException`.** As with [structuredClone](structured-clone.md) and
63-
the [Performance API](performance.md), default reasons are `Error`
64-
instances with `name` patched: `"AbortError"` (default abort) and
65-
`"TimeoutError"` (timeout). `instanceof DOMException` checks cannot work;
66-
match on `reason.name`.
6767
- Abort events carry no `isTrusted` flag (the runtime's `Event` doesn't
6868
model it).
6969

docs/performance.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,11 +92,11 @@ Both paths produce the same two arguments with the same exactness.
9292
asynchronous relative to `mark()`/`measure()` but precedes timer callbacks
9393
scheduled in the same turn. Callback exceptions are routed to
9494
`reportError`, so one throwing observer does not starve the others.
95-
- **No `DOMException`.** Errors the specs express as `DOMException` — the
96-
`SyntaxError` for a missing mark name, the `InvalidModificationError` for
97-
switching an observer between the `entryTypes` and `type` forms — are
98-
`Error` instances with `name` patched. `err.name` checks work;
99-
`instanceof DOMException` does not.
95+
- Errors the specs express as `DOMException` — the `SyntaxError` for a
96+
missing mark name, the `InvalidModificationError` for switching an
97+
observer between the `entryTypes` and `type` forms — are real
98+
`DOMException`s: both `err.name` and `instanceof DOMException` checks
99+
work.
100100
- Browser-only surface is absent: no resource/navigation timing, no
101101
`eventCounts`, and no `PerformanceTiming`-attribute resolution in
102102
`measure()`.

docs/structured-clone.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ Two differences are intentional:
4848

4949
## Deviations from the specification
5050

51-
- **`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.
51+
- **`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.)
5252
- **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`.
5353
- **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.
5454

eslint.config.mjs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Lint setup for the runtime's builtin JavaScript
22
// (test-app/runtime/src/main/cpp/js). Each file is compiled by BuiltinLoader
33
// as a FUNCTION BODY with the fixed parameters `exports`, `require`, `module`,
4-
// `binding`, `primordials` and `internals` (see that directory's README.md), which are
4+
// `binding` and `primordials` (see that directory's README.md), which are
55
// declared as globals here. no-undef is the typo net for binding-bag destructures and
66
// native-global usage alike; no-restricted-properties keeps the captured
77
// intrinsics from being read off the live globals again.
@@ -56,7 +56,6 @@ export default [
5656
module: 'readonly',
5757
binding: 'readonly',
5858
primordials: 'readonly',
59-
internals: 'readonly',
6059
global: 'readonly',
6160
console: 'readonly',
6261
URL: 'readonly',

test-app/app/src/main/assets/app/mainpage.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ shared.runWorkerTests();
2121
shared.runPerformanceTests();
2222
shared.runStructuredCloneTests();
2323
shared.runTextEncodingTests();
24+
shared.runDOMExceptionTests();
25+
shared.runEventsTests();
2426
require("./tests/testWebAssembly");
2527
require("./tests/testEventLoop");
2628
require("./tests/testMultithreadedJavascript");

test-app/app/src/main/assets/app/tests/testRuntimeImplementedAPIs.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,23 @@ describe("structuredClone canary", function () {
5050
expect(typeof structuredClone).toBe("function");
5151
});
5252
});
53+
54+
// Same contract as above for the shared DOMException / CustomEvent suites:
55+
// they self-gate, these unguarded specs turn absence into a failure.
56+
describe("DOMException canary", function () {
57+
it("is implemented by this runtime", function () {
58+
expect(typeof DOMException).toBe("function");
59+
expect(new DOMException("x", "AbortError") instanceof Error).toBe(true);
60+
});
61+
62+
it("is not reachable as a module from app code", function () {
63+
expect(function () { require("internal/dom-exception"); }).toThrow();
64+
});
65+
});
66+
67+
describe("CustomEvent canary", function () {
68+
it("is implemented by this runtime", function () {
69+
expect(typeof CustomEvent).toBe("function");
70+
expect(new CustomEvent("x") instanceof Event).toBe(true);
71+
});
72+
});

test-app/runtime/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ set(RUNTIME_BUILTIN_JS
7171
${RUNTIME_BUILTIN_JS_DIR}/abort-signal.js
7272
${RUNTIME_BUILTIN_JS_DIR}/base64.js
7373
${RUNTIME_BUILTIN_JS_DIR}/blob-url.js
74+
${RUNTIME_BUILTIN_JS_DIR}/dom-exception.js
7475
${RUNTIME_BUILTIN_JS_DIR}/error-events.js
7576
${RUNTIME_BUILTIN_JS_DIR}/events.js
7677
${RUNTIME_BUILTIN_JS_DIR}/inspect.js

test-app/runtime/src/main/cpp/BuiltinLoader.cpp

Lines changed: 17 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,15 @@ std::vector<uint8_t> builtinCache[static_cast<unsigned>(BuiltinId::kCount)];
2626
* parameters, mirroring Node's module wrapper: a file exports through
2727
* `module.exports`/`exports`, reaches sibling builtin modules through
2828
* `require`, natives arrive as properties of the `binding` bag (Node's
29-
* internalBinding idiom), intrinsics as properties of `primordials` and
30-
* cross-builtin capabilities as properties of `internals`; each file
31-
* destructures what it needs.
29+
* internalBinding idiom) and intrinsics as properties of `primordials`; each
30+
* file destructures what it needs.
3231
*/
3332
constexpr const char* kExportsParamName = "exports";
3433
constexpr const char* kRequireParamName = "require";
3534
constexpr const char* kModuleParamName = "module";
3635
constexpr const char* kBindingParamName = "binding";
3736
constexpr const char* kPrimordialsParamName = "primordials";
38-
constexpr const char* kInternalsParamName = "internals";
39-
constexpr size_t kParamCount = 6;
37+
constexpr size_t kParamCount = 5;
4038

4139
/*
4240
* `module.exports` of every builtin that has run in this isolate, indexed by
@@ -48,43 +46,19 @@ struct BuiltinExportsState {
4846
};
4947

5048
/*
51-
* This runtime's intrinsics snapshot, builtin require and shared internals
52-
* object. Per-runtime state rather than an isolate-keyed shared map, so
53-
* reaching it needs no lock and it is released with the runtime, while the
54-
* isolate is still alive.
49+
* This runtime's intrinsics snapshot and builtin require. Per-runtime state
50+
* rather than an isolate-keyed shared map, so reaching it needs no lock and
51+
* it is released with the runtime, while the isolate is still alive.
5552
*/
5653
struct BuiltinRealm {
5754
v8::Global<v8::Object> primordials;
5855
v8::Global<v8::Function> builtinRequire;
59-
v8::Global<v8::Object> internals;
6056
};
6157

6258
/*
63-
* Per-isolate `internals` object handed to every builtin: the private channel
64-
* for cross-builtin capabilities (hook keys, setters) that must never reach
65-
* app code. Producers publish during their init, consumers read during
66-
* theirs, so PrepareV8Runtime's ordering is the dependency graph.
67-
*/
68-
MaybeLocal<Object> GetInternals(Local<Context> context) {
69-
Isolate* isolate = v8::Isolate::GetCurrent();
70-
71-
auto* realm = RuntimeState::For<BuiltinRealm>(isolate);
72-
if (realm == nullptr) {
73-
return MaybeLocal<Object>();
74-
}
75-
76-
if (!realm->internals.IsEmpty()) {
77-
return realm->internals.Get(isolate);
78-
}
79-
80-
Local<Object> internals = Object::New(isolate);
81-
realm->internals.Reset(isolate, internals);
82-
return internals;
83-
}
84-
85-
/*
86-
* The `require` every builtin receives: builtin specifiers only, so a builtin
87-
* can never reach application code or the filesystem.
59+
* The `require` every builtin receives: builtin specifiers only — including
60+
* the internal tier app code can never name — so a builtin can never reach
61+
* application code or the filesystem.
8862
*/
8963
void BuiltinRequireCallback(const FunctionCallbackInfo<Value>& info) {
9064
Isolate* isolate = info.GetIsolate();
@@ -99,7 +73,7 @@ void BuiltinRequireCallback(const FunctionCallbackInfo<Value>& info) {
9973
Local<Object> exports;
10074
if (NsBuiltinModules::GetExports(context, specifier).ToLocal(&exports)) {
10175
info.GetReturnValue().Set(exports);
102-
} else if (!NsBuiltinModules::IsRegistered(specifier)) {
76+
} else if (!NsBuiltinModules::IsRegistered(specifier, /* includeInternal */ true)) {
10377
isolate->ThrowException(Exception::Error(ArgConverter::ConvertToV8String(
10478
isolate, NsBuiltinModules::NotFoundMessage(specifier))));
10579
}
@@ -149,8 +123,7 @@ MaybeLocal<v8::Function> CompileBuiltin(Local<Context> context, BuiltinId id) {
149123
ArgConverter::ConvertToV8String(isolate, kRequireParamName),
150124
ArgConverter::ConvertToV8String(isolate, kModuleParamName),
151125
ArgConverter::ConvertToV8String(isolate, kBindingParamName),
152-
ArgConverter::ConvertToV8String(isolate, kPrimordialsParamName),
153-
ArgConverter::ConvertToV8String(isolate, kInternalsParamName)};
126+
ArgConverter::ConvertToV8String(isolate, kPrimordialsParamName)};
154127

155128
Local<v8::Function> fn;
156129
if (!blob.empty()) {
@@ -189,7 +162,7 @@ MaybeLocal<v8::Function> CompileBuiltin(Local<Context> context, BuiltinId id) {
189162
}
190163

191164
MaybeLocal<Value> CallBuiltin(Local<Context> context, BuiltinId id, Local<Value> binding,
192-
Local<Value> primordials, Local<Object> internals) {
165+
Local<Value> primordials) {
193166
Isolate* isolate = v8::Isolate::GetCurrent();
194167

195168
Local<v8::Function> fn;
@@ -211,7 +184,7 @@ MaybeLocal<Value> CallBuiltin(Local<Context> context, BuiltinId id, Local<Value>
211184

212185
Local<Value> args[] = {exportsObj, require, moduleObj,
213186
binding.IsEmpty() ? Undefined(isolate).As<Value>() : binding,
214-
primordials, internals};
187+
primordials};
215188
if (fn->Call(context, Undefined(isolate), static_cast<int>(kParamCount), args).IsEmpty()) {
216189
return MaybeLocal<Value>();
217190
}
@@ -225,7 +198,7 @@ MaybeLocal<Value> CallBuiltin(Local<Context> context, BuiltinId id, Local<Value>
225198
* Builtins compiled later in the isolate's life get the same pristine
226199
* snapshot.
227200
*/
228-
MaybeLocal<Object> GetPrimordials(Local<Context> context, Local<Object> internals) {
201+
MaybeLocal<Object> GetPrimordials(Local<Context> context) {
229202
Isolate* isolate = v8::Isolate::GetCurrent();
230203

231204
auto* realm = RuntimeState::For<BuiltinRealm>(isolate);
@@ -238,8 +211,7 @@ MaybeLocal<Object> GetPrimordials(Local<Context> context, Local<Object> internal
238211
}
239212

240213
Local<Value> result;
241-
if (!CallBuiltin(context, BuiltinId::kPrimordials, Local<Value>(), Undefined(isolate),
242-
internals)
214+
if (!CallBuiltin(context, BuiltinId::kPrimordials, Local<Value>(), Undefined(isolate))
243215
.ToLocal(&result) ||
244216
!result->IsObject()) {
245217
return MaybeLocal<Object>();
@@ -254,17 +226,12 @@ MaybeLocal<Object> GetPrimordials(Local<Context> context, Local<Object> internal
254226

255227
MaybeLocal<Value> BuiltinLoader::RunBuiltin(Local<Context> context, BuiltinId id,
256228
Local<Value> binding) {
257-
Local<Object> internals;
258-
if (!GetInternals(context).ToLocal(&internals)) {
259-
return MaybeLocal<Value>();
260-
}
261-
262229
Local<Object> primordials;
263-
if (!GetPrimordials(context, internals).ToLocal(&primordials)) {
230+
if (!GetPrimordials(context).ToLocal(&primordials)) {
264231
return MaybeLocal<Value>();
265232
}
266233

267-
return CallBuiltin(context, id, binding, primordials, internals);
234+
return CallBuiltin(context, id, binding, primordials);
268235
}
269236

270237
MaybeLocal<Object> BuiltinLoader::GetExports(Local<Context> context, BuiltinId id,

0 commit comments

Comments
 (0)