Skip to content
Draft
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 @@ -19,6 +19,11 @@
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` `DOMException` on failure.
- [Messaging and `node:worker_threads`](worker-threads.md) — `MessagePort`,
`MessageChannel`, `BroadcastChannel` and `MessageEvent`, the
`node:worker_threads` real-vs-shim table and its documented deviations,
the strong-until-closed port lifetime, HTML port enabling, and the
transfer support matrix with its `DataCloneError` messages.
- [Implementing additional Chrome DevTools protocol Domains](extending-inspector.md)

## Knowledge
Expand Down
53 changes: 32 additions & 21 deletions docs/ns-builtin-modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -380,14 +380,17 @@ npm packages that require Node builtins by their prefixed names can run
unmodified where a shim exists:

- A shim implements a documented **subset** of the corresponding Node module's
API, backed by `ns:` modules. Unimplemented members are simply absent
API, backed by the runtime's own modules. Unimplemented members are simply absent
(so `typeof util.promisify === "function"` feature-checks behave
correctly); they are never present-but-throwing.
correctly); they are never present-but-throwing. The one exception is a
member whose silent absence would read as a delivery bug rather than as a
missing feature — it may be present and throw, and the table below names
every such member.
- **One source file per specifier.** A shim is its own module that consumes
the `ns:` module it adapts through the internal require, and it owns *all*
the adaptation — argument shapes, option names, aliases, anything that has
to track Node. A standard `ns:` module never contains compatibility code
and never knows a shim exists.
the module it adapts through the internal require, and it owns *all* the
adaptation — argument shapes, option names, aliases, anything that has to
track Node. A standard `ns:` module never contains compatibility code and
never knows a shim exists.
- Shims are **lazy**: a shim's source is only evaluated when its specifier is
first resolved, so an app that never touches the `node:` scheme never pays
for one.
Expand All @@ -411,6 +414,7 @@ unmodified where a shim exists:
| `node:util` | `inspect`, `format`, `TextEncoder`, `TextDecoder` | Re-exports `ns:util`'s members unchanged (`nodeUtil.inspect === nsUtil.inspect`) from a **distinct, separately frozen module object**. `TextEncoder`/`TextDecoder` are the globals of those names, as they are in Node. Documented as partial. |
| `node:url` | `fileURLToPath`, `pathToFileURL` | Node-strict converters between `file:` URLs and paths. Documented as partial — no `URL`/`URLSearchParams` re-exports (both are globals), no legacy `url.parse`/`format`/`resolve`. |
| `node:module` | `createRequire` | Re-exports `ns:module`'s `createRequire` unchanged from a **distinct, separately frozen module object**. `createPumpingRequire` is deliberately absent: it has no Node counterpart, so code written against this shim keeps running on Node. `require.resolve`/`.cache`/`.main` are not implemented, and neither is any other `node:module` member (`Module`, `builtinModules`, `isBuiltin`, `register`, `syncBuiltinESMExports`). Documented as partial. |
| `node:worker_threads` | the messaging and thread surface — see [worker-threads.md](worker-threads.md) | The channel half (`MessagePort`, `MessageChannel`, `BroadcastChannel`, `receiveMessageOnPort`) is the real implementation, the same objects the globals of those names hold; the thread half is a bridge over the runtime's own `Worker`. It has no `ns:` counterpart — the surface tracks Node's, so there is nothing for a standard module to own. The one place it breaks the absent-not-throwing rule below is deliberate: `postMessageToThread` and `moveMessagePortToContext` are present and throw an `Error` naming themselves, because silently missing thread-addressed messaging reads as a delivery bug rather than as an unsupported call. Documented as partial. |

`node:url`'s parsing goes through the URL intrinsic, so `file://localhost/x` is
accepted (the URL spec folds a `localhost` authority to none) while any other
Expand Down Expand Up @@ -719,21 +723,28 @@ shims are built on, so it is normative: both runtimes provide it.
Android-only) note in between.
- Internal runtime machinery must never be reachable through the scheme.

That last rule holds because public modules and internal builtins are **two
separate loading paths**, not one registry with a per-entry flag:

- The **public registry** is a table mapping specifier → builtin, and it is the
only thing the `ns:`/`node:` resolver consults. A specifier absent from it
does not resolve, full stop. Today it holds six entries: `ns:module`,
`ns:runtime`, `ns:util`, `node:module`, `node:url`, `node:util`.
- **Internal builtins** (the intrinsics snapshot, the require factory, the
console formatter, and so on) are invoked directly from their own native call
sites. They are never named in the public registry, so there is no specifier
that could reach them and nothing to mark private.

Adding an internal builtin therefore cannot accidentally expose it; exposing
one is an explicit registry entry, which is also the change this document has
to describe.
That last rule holds because every registry row carries its tier, and the two
resolvers read the same table differently:

- The **`ns:`/`node:` resolver** — the app-facing one, behind `require()`,
`import` and `import()` — serves only rows *not* marked internal-only. An
internal-only specifier fails exactly as a name absent from the table does.
Seven rows are public today: `ns:module`, `ns:runtime`, `ns:util`,
`node:module`, `node:url`, `node:util`, `node:worker_threads`.
- The **internal require** builtins receive (previous section) is the only
thing that can name an internal-only row. Five rows are marked that way:
`internal/broadcast-channel`, `internal/dom-exception`, `internal/events`,
`internal/message-channel`, `internal/message-event`. Their exports carry
capabilities app code must not hold — listener-accounting hook keys, the
error-reporter setter, base classes that must be the runtime's own rather
than whatever a global currently names.
- Builtins with **no row at all** (the intrinsics snapshot, the require
factory, the console formatter) are invoked straight from their native call
sites. There is no specifier that could reach them and nothing to mark.

So a builtin is unreachable from app code unless a registry row says
otherwise, and exposing one means editing that row's tier — which is also the
change this document has to describe.

## Source-text modules: deliberately not supported

Expand Down
14 changes: 8 additions & 6 deletions docs/structured-clone.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# structuredClone

The runtime exposes the WHATWG [`structuredClone(value, options)`](https://html.spec.whatwg.org/multipage/structured-data.html#dom-structuredclone) global. It performs a deep, structure-preserving copy of `value` using V8's structured clone serializer — the same one worker `postMessage` uses — optionally taking ownership of `ArrayBuffer`s named in `options.transfer`.
The runtime exposes the WHATWG [`structuredClone(value, options)`](https://html.spec.whatwg.org/multipage/structured-data.html#dom-structuredclone) global. It performs a deep, structure-preserving copy of `value` using V8's structured clone serializer — the same one worker `postMessage` uses — optionally taking ownership of the `ArrayBuffer`s and `MessagePort`s named in `options.transfer`.

```js
const clone = structuredClone({ when: new Date(), tags: new Set(["a"]) });
Expand All @@ -12,7 +12,7 @@ buffer.byteLength; // 0 — the memory now belongs to `moved`

## Surface

`structuredClone(value)` returns a clone of `value`. `structuredClone(value, { transfer })` additionally transfers every `ArrayBuffer` in `transfer`.
`structuredClone(value)` returns a clone of `value`. `structuredClone(value, { transfer })` additionally transfers every `ArrayBuffer` and `MessagePort` in `transfer`.

- `value` is required; calling with no arguments throws a `TypeError`.
- `options` may be `undefined` or `null` (both mean "no transfer"); anything else must be an object, or a `TypeError` is thrown.
Expand All @@ -24,14 +24,16 @@ The clone preserves the shape of the graph, not just the values: an object refer

`SharedArrayBuffer` is **shared, not copied**: the clone is a second `SharedArrayBuffer` over the same memory, so writes through either are visible through the other.

Not cloneable — each throws (see the deviations below): functions, symbols, `WeakMap`/`WeakSet`/`WeakRef`, `Promise`, and every native/interop object (Java proxies and the objects the metadata layer hands out), which have no serialized form.
Not cloneable — each throws (see the deviations below): functions, symbols, `WeakMap`/`WeakSet`/`WeakRef`, `Promise`, and every native/interop object (Java proxies and the objects the metadata layer hands out), which have no serialized form. A `MessagePort` is transferable but never cloneable, so one found in the graph has to be in the transfer list.

## Transfer semantics

Listed buffers are validated before anything is serialized: each entry must be an `ArrayBuffer`, must not already be detached, must be detachable, and must appear at most once. A violation throws before the source buffers are touched, so a rejected call never leaves a half-transferred graph behind.
The list is validated before anything is serialized: each entry must be an `ArrayBuffer` or a `MessagePort`, must not already be detached (an `ArrayBuffer` must additionally be detachable), and must appear at most once. A violation throws before the sources are touched, and nothing is detached or handed over until the whole graph has serialized successfully — a rejected call never leaves a half-transferred graph behind. The guarantee covers transfer state only: serializing the graph runs user getters, and a getter's own side effects (closing a listed port, say) are not rolled back — a port closed that way makes the call fail, already closed.

On success the memory changes hands rather than being copied: the source buffer is detached (`byteLength` becomes 0, and every typed array over it becomes zero-length) and the clone receives the original backing store. A transferred buffer need not appear inside `value` at all; a buffer reached through a typed array in `value` is transferred as a unit, so the cloned view sees the original bytes.

A transferred `MessagePort` is closed as a handle on this side while its queue and its channel membership move to the clone. Unlike a buffer, a port that *is* reachable in `value` must also be listed — an unlisted one is a `DataCloneError`, since a copied port would be a port to nowhere. [worker-threads.md](worker-threads.md) has the full transfer matrix and the exact `DataCloneError` messages.

## Worker `postMessage`

`structuredClone` and worker `postMessage` run on the same serialization core, so everything above — which types clone, graph identity, cycles, `SharedArrayBuffer` sharing — holds for messages too. `postMessage` takes the same transfer list as a second argument:
Expand All @@ -44,12 +46,12 @@ worker.postMessage({ pixels: buffer }, [buffer]); // buffer is detached here,
Two differences are intentional:

- **The transfer list must be an array.** Omitting it, or passing `undefined` or `null`, means "transfer nothing"; every other non-array value is a `TypeError`. The WebIDL iterable-to-sequence conversion that lets `structuredClone` take a `Set` or any iterable lives in the JavaScript wrapper around `structuredClone`; `postMessage` is native all the way down and has no such wrapper.
- **Host objects degrade instead of throwing.** Posting a native/interop object delivers an empty object to the receiver rather than raising a `DataCloneError`. This is long-standing shipped behavior, and app code relies on it; `structuredClone`, being new, follows the spec and rejects. The asymmetry is encoded in exactly one place — the `HostObjectPolicy` enum in `test-app/runtime/src/main/cpp/StructuredSerialization.h` — and unifying the two on rejection is a breaking change that needs the iOS runtime to move at the same time.
- **Host objects degrade instead of throwing.** Posting a native/interop object delivers an empty object to the receiver rather than raising a `DataCloneError`. This is long-standing shipped behavior, and app code relies on it; `structuredClone`, being new, follows the spec and rejects. The asymmetry is encoded in exactly one place — the `HostObjectPolicy` enum in `test-app/runtime/src/main/cpp/StructuredSerialization.h` — and unifying the two on rejection is a breaking change that needs the iOS runtime to move at the same time. `MessagePort` is outside the leniency: a port is rejected or transferred, never degraded, because an empty object in the receiver would strand its sibling.

## Deviations from the specification

- **`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`.
- **Only `ArrayBuffer` and `MessagePort` are transferable.** The spec's other transferable types — `ImageBitmap`, `ReadableStream` and friends — do not exist here, and neither do the runtime's own native/interop wrapper objects, which have no serialized form. Anything else in the transfer list is a `DataCloneError`. Port transfer has rules of its own (a port may not travel on itself, a port in the graph must be listed); [worker-threads.md](worker-threads.md) has the full matrix and the exact messages.
- **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.

`SharedArrayBuffer` follows the spec: it is shared rather than copied, and it is not transferable (listing one throws a `DataCloneError`).
Loading
Loading