Skip to content

Commit 4e7f207

Browse files
authored
feat: add DOMException and CustomEvent as lazy globals (#2027)
1 parent f69b684 commit 4e7f207

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)