Skip to content

feat: add DOMException and CustomEvent as lazy globals - #2027

Merged
NathanWalker merged 1 commit into
mainfrom
feat/dom-exception
Aug 25, 2026
Merged

feat: add DOMException and CustomEvent as lazy globals#2027
NathanWalker merged 1 commit into
mainfrom
feat/dom-exception

Conversation

@edusperoni

@edusperoni edusperoni commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Mirrors NativeScript/ios#452 — the follow-up to #2026 promised there ("a follow-up PR will introduce DOMException and upgrade these plus AbortSignal's reasons"): DOMException and CustomEvent, both behind the lazy-global tier.

DOMException

New lazy builtin dom-exception.js (Web IDL §4.3), byte-identical with the iOS copy:

  • Class grafted onto Error.prototypeinstanceof Error holds and Error.prototype.toString renders name: message — with name/message/code as branded, enumerable prototype accessors (private fields double as the Web IDL brand check), the full legacy code table, the 25 constants on interface object and prototype, @@toStringTag, and stack capture.
  • Placed by LazyGlobals on first read; until then nothing runs or allocates.

Internal require tier

Sibling builtins construct DOMExceptions lazily via a new internal-only specifier tier: kRegistry rows flagged internalOnly resolve through the require builtins receive and nowhere else — the module system refuses them (GetModule guard for ES imports; the CommonJS path was already prefix-gated by IsBuiltinScheme), and a canary test pins that app code cannot name them. This is the Node internal-module idiom the js README had planned.

All five existing stand-in throw sites now produce real DOMExceptions, with the builtin required at first throw so a clean path never runs it:

  • abort-signal.js — default abort ("AbortError") and timeout ("TimeoutError") reasons
  • performance.js — SyntaxError / InvalidModificationError failures
  • structured-clone.js — transfer-list DataCloneError
  • base64.js — atob/btoa InvalidCharacterError
  • StructuredSerialization.cpp — the native serializer's DataCloneError, built through the same exports cache (the name-patched-Error shape kept as a teardown fallback)

internals parameter removed

With the tier in place, the interim internals object (introduced with AbortSignal in #2025) had exactly two users left, and 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), and a consumer resolves the capability explicitly at the require — a cache hit for consumers of eager producers, an on-demand run otherwise — so it can never observe a missing key the way the shared object allowed.

CustomEvent

Defined in events.js next to the Event it extends (same ES5 idiom), 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.

Not implemented

The spec's [Serializable] slot for DOMException: structuredClone/worker postMessage go through v8::ValueSerializer, which has no hook for a plain JS class, so a DOMException inside a cloned graph degrades like any custom Error subclass.

Tests

  • Shared suites (NativeScript/common-runtime-tests-app@9cc46c0, submodule bumped, wired into mainpage.js): self-gating DOMException and CustomEvent suites that skip with a visible pending spec where the APIs are absent, plus integration specs — gated per collaborating API — asserting AbortSignal reasons, atob failures and structuredClone failures are real DOMExceptions.
  • Unguarded canaries in testRuntimeImplementedAPIs.js so this runtime regressing the globals fails instead of skipping, plus the app-code-cannot-require-internal pin.
  • Full suite (runtestsAndVerifyResults, arm64 API 33): 1203 specs, 0 failures.

Summary by CodeRabbit

  • New Features

    • Added standards-compatible DOMException support, including legacy properties and error codes.
    • Added CustomEvent support with read-only event details.
    • Improved abort, encoding, performance, and structured-cloning errors to use appropriate DOMException types.
  • Documentation

    • Updated runtime documentation to describe DOMException behavior and structured-cloning errors.
  • Tests

    • Added coverage verifying DOMException and CustomEvent availability, inheritance, and error behavior.

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.
@coderabbitai

coderabbitai Bot commented Aug 25, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d00e6bcf-e04f-4748-a586-a7238f3819d0

📥 Commits

Reviewing files that changed from the base of the PR and between f69b684 and ebb100d.

📒 Files selected for processing (28)
  • docs/README.md
  • docs/abort-signal.md
  • docs/performance.md
  • docs/structured-clone.md
  • eslint.config.mjs
  • test-app/app/src/main/assets/app/mainpage.js
  • test-app/app/src/main/assets/app/shared
  • test-app/app/src/main/assets/app/tests/testRuntimeImplementedAPIs.js
  • test-app/runtime/CMakeLists.txt
  • test-app/runtime/src/main/cpp/BuiltinLoader.cpp
  • test-app/runtime/src/main/cpp/BuiltinLoader.h
  • test-app/runtime/src/main/cpp/Events.cpp
  • test-app/runtime/src/main/cpp/LazyGlobals.cpp
  • test-app/runtime/src/main/cpp/LazyGlobals.h
  • test-app/runtime/src/main/cpp/NsBuiltinModules.cpp
  • test-app/runtime/src/main/cpp/NsBuiltinModules.h
  • test-app/runtime/src/main/cpp/StructuredSerialization.cpp
  • test-app/runtime/src/main/cpp/StructuredSerialization.h
  • test-app/runtime/src/main/cpp/js/README.md
  • test-app/runtime/src/main/cpp/js/abort-signal.js
  • test-app/runtime/src/main/cpp/js/base64.js
  • test-app/runtime/src/main/cpp/js/dom-exception.js
  • test-app/runtime/src/main/cpp/js/error-events.js
  • test-app/runtime/src/main/cpp/js/events.js
  • test-app/runtime/src/main/cpp/js/performance.js
  • test-app/runtime/src/main/cpp/js/primordials.js
  • test-app/runtime/src/main/cpp/js/structured-clone.js
  • test-app/runtime/src/main/cpp/js/text-encoding.js

Included review availability: Your plan provides up to 4 included reviews per hour; 3 remain after this review.


📝 Walkthrough

Walkthrough

The runtime replaces the per-runtime internals channel with internal-only builtin modules. It adds DOMException and CustomEvent, updates related error paths, wires lazy globals and event exports, and adds application canaries and documentation updates.

Changes

Runtime builtin architecture

Layer / File(s) Summary
Internal builtin module contract
test-app/runtime/src/main/cpp/BuiltinLoader.*, test-app/runtime/src/main/cpp/NsBuiltinModules.*, test-app/runtime/src/main/cpp/js/README.md, eslint.config.mjs, test-app/runtime/src/main/cpp/js/*
Builtin wrappers no longer receive internals. Builtins can resolve internal/... modules through privileged require calls.
DOMException and event builtin exports
test-app/runtime/CMakeLists.txt, test-app/runtime/src/main/cpp/js/dom-exception.js, test-app/runtime/src/main/cpp/js/events.js, test-app/runtime/src/main/cpp/js/primordials.js, test-app/runtime/src/main/cpp/Events.cpp, test-app/runtime/src/main/cpp/LazyGlobals.*
The runtime adds a Web IDL-shaped DOMException, adds CustomEvent, exports event-system capabilities, and registers both constructors as runtime globals.
DOMException error paths
test-app/runtime/src/main/cpp/js/abort-signal.js, base64.js, performance.js, structured-clone.js, error-events.js, test-app/runtime/src/main/cpp/StructuredSerialization.*
Abort, timeout, base64, performance, and structured-clone failures construct DOMExceptions through internal builtin loading. Listener error reporting uses the internal events module.
Runtime validation and documentation
test-app/app/src/main/assets/app/mainpage.js, test-app/app/src/main/assets/app/tests/testRuntimeImplementedAPIs.js, test-app/app/src/main/assets/app/shared, docs/*
The test application runs DOMException and CustomEvent canaries. Documentation describes DOMException errors and the internal events hook.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: ⚪ Minimal · up to ebb10

The PR adds lazy DOMException and CustomEvent globals and updates related error paths; no actionable merge-blocking risk remains after normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant Runtime
  participant BuiltinLoader
  participant InternalModules
  participant DOMException
  Runtime->>BuiltinLoader: Request builtin exports
  BuiltinLoader->>InternalModules: Resolve internal/dom-exception
  InternalModules->>DOMException: Load and cache constructor
  DOMException-->>Runtime: Return DOMException constructor
  Runtime->>DOMException: Construct named exception
  DOMException-->>Runtime: Throw DOMException
Loading

Suggested reviewers: nathanwalker

Poem

A rabbit checks the exception gate

DOMException arrives in proper state
Internal paths hide hooks from sight
CustomEvent joins the event flight
Tests hop through the runtime bright

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.21% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 33 functions across 21 files. (7 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the primary changes: adding DOMException and CustomEvent as lazy globals.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 21.21% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 33 functions across 21 files. (7 skipped: 7 unsupported.)

✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@NathanWalker
NathanWalker merged commit 4e7f207 into main Aug 25, 2026
8 checks passed
@NathanWalker
NathanWalker deleted the feat/dom-exception branch August 25, 2026 22:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants