Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
timing, performance timeline with `PerformanceObserver`), per-isolate time
origins for workers, the native clock hook that future `requestAnimationFrame`
work must share, and the documented spec deviations.
- [AbortController / AbortSignal](abort-signal.md) — the DOM abort primitives
(`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).
- [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`.
- [Implementing additional Chrome DevTools protocol Domains](extending-inspector.md)
Expand Down
72 changes: 72 additions & 0 deletions docs/abort-signal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# AbortController / AbortSignal

The runtime installs the DOM Standard's abort primitives as globals in every
isolate (main and workers): `AbortController`, and `AbortSignal` with the
`abort`, `timeout` and `any` statics. The implementation is
`test-app/runtime/src/main/cpp/js/abort-signal.js`, evaluated during `Events::Init`
right after the `Event`/`EventTarget` builtin it is layered on, so the
interfaces exist before any user code runs. `AbortSignal` extends the
runtime's `EventTarget`; `new AbortSignal()` throws `TypeError: Illegal
constructor` — instances come from a controller or one of the statics.

## Surface

- `new AbortController()` — `controller.signal` (stable identity) and
`controller.abort(reason?)`.
- `signal.aborted`, `signal.reason`, `signal.throwIfAborted()`, and the
`abort` event (`addEventListener("abort", …)` or the `onabort` handler
attribute with HTML event-handler semantics).
- `AbortSignal.abort(reason?)` — an already-aborted signal; no event fires.
- `AbortSignal.timeout(delay)` — aborts with a `TimeoutError`-named reason
after `delay` ms. `delay` must be an integer in `[0, 2^32 − 1]`
(`TypeError` for non-numbers, `RangeError` otherwise), matching Node's
validation.
- `AbortSignal.any(signals)` — a composite signal that aborts with the first
source's reason. Accepts any iterable whose members are all `AbortSignal`s
(`TypeError` otherwise). Composites are flattened: `any([any([a]), b])`
follows `a` and `b` directly. Per spec, every affected signal's
`aborted`/`reason` flips before the first `abort` event fires.

## GC contract

The implementation is GC-transparent the way Node's is: internal references
never keep an unobservable signal alive, and never let an observable abort
be dropped.

- A `timeout()` timer closes over a `WeakRef`, so a signal nobody can
observe is collectable before it fires; a `FinalizationRegistry` cancels
the pending native timer when that happens.
- `any()` links are `WeakRef`s in both directions (source → dependent and
dependent → source), with prune registries clearing dead entries — so
per-request composites never accumulate on a long-lived source, and a
collected source leaves its composites' source lists (a composite whose
sources are all gone can never abort and stops being retained).
- Weakness alone would silently drop the abort of a signal that is
listened-to but otherwise unreachable, so a strong `gcPersistentSignals`
set holds exactly the signals whose abort someone can still observe: live
timeout signals and live non-empty composites while they have `abort`
listeners (`onabort` counts — it registers a real listener), plus timeout
sources a composite follows, until their timer fires. The listener
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`.

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

## 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).

Listener errors during the abort dispatch go through the runtime's standard
listener-error pipeline (see [error handling](error-handling.md)); a throwing
listener never prevents the remaining listeners from running.
6 changes: 4 additions & 2 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -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` and `primordials` (see that directory's README.md), which are
// `binding`, `primordials` and `internals` (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.
Expand All @@ -16,6 +16,7 @@ const capturedStatics = [
['ArrayBuffer', 'isView', 'ArrayBufferIsView'],
['JSON', 'stringify', 'JSONStringify'],
['Number', 'isFinite', 'NumberIsFinite'],
['Number', 'isInteger', 'NumberIsInteger'],
['Number', 'isNaN', 'NumberIsNaN'],
['Number', 'parseFloat', 'NumberParseFloat'],
['Number', 'parseInt', 'NumberParseInt'],
Expand All @@ -31,7 +32,7 @@ const capturedStatics = [

// Captured constructors. A destructure from `primordials` shadows the global,
// so these only fire on the unguarded reference.
const restrictedGlobals = ['Date', 'Map', 'Number', 'Proxy', 'Set', 'String', 'TypeError'].map((name) => ({
const restrictedGlobals = ['Date', 'FinalizationRegistry', 'Map', 'Number', 'Proxy', 'RangeError', 'Set', 'String', 'TypeError', 'WeakRef'].map((name) => ({
name,
message: `Destructure ${name} from primordials — builtins must not read intrinsics off globals user code can replace.`,
}));
Expand All @@ -55,6 +56,7 @@ export default [
module: 'readonly',
binding: 'readonly',
primordials: 'readonly',
internals: 'readonly',
global: 'readonly',
console: 'readonly',
URL: 'readonly',
Expand Down
2 changes: 2 additions & 0 deletions test-app/app/src/main/assets/app/mainpage.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ require('./tests/testURLSearchParamsImpl.js');
require('./tests/testQueueMicrotask');
require('./tests/testErrorEvents');
require('./tests/testUnhandledRejections');
// AbortController/AbortSignal (abort/timeout/any) on top of EventTarget
require('./tests/testAbortSignal');
require('./tests/testEscapeException');
require('./tests/testUncaughtErrorPolicy');
// Runtime builtins keep working when app code replaces the intrinsics they use
Expand Down
Loading