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
1 change: 1 addition & 0 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
- [`node:tty`](./interop/nodejs-builtins/supported-modules/tty.md)
- [`node:url`](./interop/nodejs-builtins/supported-modules/url.md)
- [`node:util`](./interop/nodejs-builtins/supported-modules/util.md)
- [`node:v8`](./interop/nodejs-builtins/supported-modules/v8.md)
- [`node:vfs`](./interop/nodejs-builtins/supported-modules/vfs.md)
- [`node:vm`](./interop/nodejs-builtins/supported-modules/vm.md)
- [`node:wasi`](./interop/nodejs-builtins/supported-modules/wasi.md)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ compatibility limits. Related submodules share their parent API page. See the
| [`node:tty`](./tty.md) | Node 24.20 `isatty`, `ReadStream` and `WriteStream` over the host process's descriptors through an explicit host capability; denied by default. |
| [`node:url`](./url.md) | Node 24 URL, URLSearchParams, URLPattern, domain and file conversions; relative file paths use optional WASI environment imports. |
| [`node:util`](./util.md), [`node:util/types`](./util.md) | Portable Node 24 utilities, sharing assertion equality and console formatting. No WIT capability; see the API page for engine and process restrictions. |
| [`node:v8`](./v8.md) | Native V8 serialization, host heap diagnostics and profiling through an explicit provider; guest engine hooks are unsupported. |
| [`node:vfs`](./vfs.md) | Node 26 memory VFS, default-denied Node passthrough, and WASI filesystem storage with configurable preopen placement. |
| [`node:vm`](./vm.md) | Same-context script evaluation and function compilation in the guest. No WIT capability; separate realms, native caches and VM modules are unsupported. |
| [`node:wasi`](./wasi.md) | Node 24.19 `WASI` construction over an explicit host capability, denied by default; `start()` and `initialize()` refuse because a component cannot instantiate a nested module. |
Expand Down
109 changes: 109 additions & 0 deletions docs/src/interop/nodejs-builtins/supported-modules/v8.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# `node:v8`

Jco provides the Node 24.20.0 `node:v8` export surface. Native operations use an
explicit `jco:node/v8@0.1.0` capability and are denied by default.

## Enable the Node provider

Application source keeps ordinary Node imports:

```js
import { serialize, deserialize, getHeapStatistics } from 'node:v8';

const bytes = serialize({ message: 'hello', count: 42n });
console.log(deserialize(bytes));
console.log(getHeapStatistics());
```

Bundle with Jco's Node builtin support, then explicitly grant the host capability:

```sh
jco componentize app.js --bundle --wit app.wit -o app.wasm
jco transpile app.wasm -o out \
--map 'jco:node/v8@0.1.0=@bytecodealliance/jco-std/wasi/0.2.x/node/24.x.x/v8/host/node'
```

Importing the module does not access the provider. Without an explicit mapping,
native operations throw `ERR_JCO_V8_ADAPTER_REQUIRED`. The portable adapter can
also be imported directly from jco-std alongside ordinary Node builtins.

## Host diagnostics and controls

The provider delegates these operations to public `node:v8` APIs:

- `cachedDataVersionTag`, heap, heap-space, code and C++ heap statistics;
- `getHeapSnapshot`, `writeHeapSnapshot`, and `setHeapSnapshotNearHeapLimit`;
- `GCProfiler` and `startCpuProfile`;
- `setFlagsFromString`, `takeCoverage`, and `stopCoverage`.

These operations inspect or affect the **Node host V8 isolate**. They do not
inspect the QuickJS or SpiderMonkey guest heap. Snapshot paths, coverage settings,
and V8 flags belong to the host process. The cached-data tag describes host V8;
it cannot establish compatibility with guest compiled code.

`getHeapSnapshot` returns a portable byte-mode `Readable`. The provider gathers
the synchronous native snapshot before copying it into the component, so this
requires additional memory and does not provide incremental host streaming.
Profiles stop and release their native resource on `stop()` or disposal. Repeated
stops return `undefined`, matching the pinned runtime.

`startCpuProfile` requires a Node host that provides that API. On older hosts,
including Node 22, it throws `ERR_JCO_UNSUPPORTED_NODE_API`. Other V8 operations
remain available when supported by the host.

## Serialization

`serialize` and `deserialize` use the native V8 binary format. `Serializer`,
`Deserializer`, `DefaultSerializer`, and `DefaultDeserializer` support headers,
values, unsigned integers, doubles, raw bytes and wire-format inspection. Successive
value writes and reads preserve object identity through the native serializer.

The shared component transport supports primitives, bigint, special numbers,
cyclic plain records and arrays, Map, Set, Date, RegExp, ArrayBuffer, DataView,
and the integer/float typed arrays available in both engines. The default
serializer also preserves Buffer branding and visible bytes.

The transport rejects accessors, custom prototypes, Error objects, Float16Array,
shared memory and native objects with explicit errors. Functions and symbols
cannot be serialized. Serializer subclasses, custom native serialization hooks,
and `transferArrayBuffer` registrations are unsupported across the component
boundary. The corresponding entry points throw `ERR_JCO_UNSUPPORTED_NODE_API`.
No JSON substitute is returned as a V8 serialization buffer.

Buffers cross WIT by value: `readRawBytes` returns a copy rather than a view into
the caller's original input. Deserialization takes a snapshot of the input bytes
at construction. Changes to the input afterwards are not visible to the native
reader. Native format versions are controlled by the selected host Node release.

`releaseBuffer()` releases a writer's native resource; a subsequent write reopens
it. Serializer and deserializer objects additionally expose `Symbol.dispose` for
deterministic cleanup of abandoned writers and completed readers. The convenience
functions clean up their resources automatically.

## Guest engine restrictions

`promiseHooks`, `queryObjects`, and `isStringOneByteRepresentation` throw
`ERR_JCO_UNSUPPORTED_NODE_API`: guest promises, constructors and string storage
cannot be inspected through a host V8 call.

`startupSnapshot.isBuildingSnapshot()` returns `false`. The three startup callback
registration methods throw `ERR_NOT_BUILDING_SNAPSHOT`, matching ordinary Node
execution outside its snapshot builder. Componentization does not run Node's
startup-snapshot callbacks.

## Implementation

The compatibility target is [Node v24.20.0](https://nodejs.org/download/release/v24.20.0/docs/api/v8.html),
source commit `71b8b174857e25106d39b61a9e6f30d927da8b01`. Public declarations are
reconciled with `@types/node` 24.13.3 and do not require consumer Node types.

Native V8 owns binary serialization and diagnostics. Jco reuses
its shared errors, validation, Buffer, Readable, and the graph transport extracted
from the worker-threads implementation. Workers retain their existing cloning
policy; V8 opts into Buffer preservation and persistent reference sessions.

The unenv V8 module uses mock statistics and inert serializers, so it is not used.
The codec audit also considered `@ungap/structured-clone`, devalue, flatted and the
existing cluster JSON transport. The shared worker codec already preserves the
required backing-buffer relationships and clone-marking policy; V8 adds session
identity without replacing the worker format.
19 changes: 19 additions & 0 deletions packages/jco-std/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,25 @@
"types": "./dist/wasi/0.2.x/node/24.x.x/wasi-host-node.d.ts",
"node": "./dist/wasi/0.2.x/node/24.x.x/wasi-host-node.js"
},
"./wasi/0.2.x/node/24.x.x/v8": {
"types": "./dist/wasi/0.2.x/node/24.x.x/v8.d.ts",
"browser": "./dist/wasi/0.2.x/node/24.x.x/v8.js",
"default": "./dist/wasi/0.2.x/node/24.x.x/v8.js"
},
"./wasi/0.2.x/node/24.x.x/v8/core": {
"types": "./dist/wasi/0.2.x/node/24.x.x/v8/core.d.ts",
"browser": "./dist/wasi/0.2.x/node/24.x.x/v8/core.js",
"default": "./dist/wasi/0.2.x/node/24.x.x/v8/core.js"
},
"./wasi/0.2.x/node/24.x.x/v8/host": {
"types": "./dist/wasi/0.2.x/node/24.x.x/v8-host.d.ts",
"browser": "./dist/wasi/0.2.x/node/24.x.x/v8-host.js",
"default": "./dist/wasi/0.2.x/node/24.x.x/v8-host.js"
},
"./wasi/0.2.x/node/24.x.x/v8/host/node": {
"types": "./dist/wasi/0.2.x/node/24.x.x/v8-host-node.d.ts",
"node": "./dist/wasi/0.2.x/node/24.x.x/v8-host-node.js"
},
"./wasi/0.2.x/node/26.x.x/vfs": {
"types": "./dist/wasi/0.2.x/node/26.x.x/vfs.d.ts",
"browser": "./dist/wasi/0.2.x/node/26.x.x/vfs.js",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* Shared bounded graph transport extracted from worker-threads/codec.ts.
* Worker defaults preserve its clone-mark policy and Uint8Array treatment of Buffer.
* V8 opts into Buffer branding and persistent identities between native serdes calls.
* The graph is transport only; it is never returned as a V8 serialization buffer.
*/
import { createEncoder } from "./structured-value/encode.js";
import { objectLike } from "./structured-value/types.js";
import type { CodecOptions, MessageCodec } from "./structured-value/types.js";

export { decodeMessage } from "./structured-value/decode.js";

export type { CodecOptions, MessageCodec, DecodeSession } from "./structured-value/types.js";

export function createMessageCodec(options: CodecOptions = {}): MessageCodec {
const uncloneable = new WeakSet<object>();
const untransferable = new WeakSet<object>();

return {
encode: createEncoder(options, uncloneable),

markAsUncloneable(value: unknown): void {
// Node's clone flag applies to ordinary objects, not built-in value serializers.
if (
objectLike(value) &&
!Array.isArray(value) &&
!ArrayBuffer.isView(value) &&
!(value instanceof ArrayBuffer) &&
!(value instanceof Map) &&
!(value instanceof Set) &&
!(value instanceof Date) &&
!(value instanceof RegExp)
) {
uncloneable.add(value);
}
},

markAsUntransferable(value: unknown): void {
if (objectLike(value)) {
untransferable.add(value);
}
},

isMarkedAsUntransferable(value: unknown): boolean {
return objectLike(value) && untransferable.has(value);
},
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { Buffer } from "node:buffer";
import { viewConstructors } from "./types.js";
import type { EncodedValue, Graph, DecodeSession } from "./types.js";

/** Decode only the private graph representation emitted by the paired adapter. */
export function decodeMessage(source: string, session?: DecodeSession): unknown {
const graph: EncodedValue[] | Graph = JSON.parse(source);
const nodes = Array.isArray(graph) ? graph : graph.nodes;
const root = Array.isArray(graph) ? 0 : graph.root;
const values = session?.values ?? [];
const start = values.length;

for (let index = start; index < nodes.length; index++) {
values.push(allocate(nodes[index]));
}

// Buffers must exist before constructing views; object references are filled last.
for (let index = start; index < nodes.length; index++) {
const node = nodes[index];

if (node.type === "view") {
values[index] = restoreView(node, values);
}
}

for (let index = start; index < nodes.length; index++) {
fillReferences(nodes[index], values[index], values);
}

return values[root];
}

function allocate(node: EncodedValue): unknown {
switch (node.type) {
case "value":
return node.value;

case "undefined":
case "view":
return undefined;

case "number":
return Number(node.value);

case "bigint":
return BigInt(node.value);

case "date":
return new Date(Number(node.value));

case "array":
return new Array(node.length);

case "object":
return {};

case "map":
return new Map<unknown, unknown>();

case "set":
return new Set<unknown>();

case "buffer":
return Uint8Array.from(node.bytes).buffer;

case "node-buffer":
return Buffer.from(node.bytes);

case "regexp":
return new RegExp(node.source, node.flags);
}
}

function restoreView(
node: Extract<EncodedValue, { type: "view" }>,
values: unknown[],
): ArrayBufferView {
// The paired encoder references an ArrayBuffer node, allocated in the first pass.
const buffer = values[node.buffer] as ArrayBuffer;

if (node.name === "DataView") {
return new DataView(buffer, node.offset, node.length);
}

const Constructor = viewConstructors[node.name as keyof typeof viewConstructors];

return new Constructor(buffer, node.offset, node.length / Constructor.BYTES_PER_ELEMENT);
}

function fillReferences(node: EncodedValue, target: unknown, values: unknown[]): void {
if (node.type === "object" || node.type === "array") {
for (const [key, reference] of node.entries) {
Object.defineProperty(target, key, {
value: values[reference],
enumerable: true,
configurable: true,
writable: true,
});
}
} else if (node.type === "map") {
// The allocation pass creates the collection corresponding to this node tag.
for (const [key, value] of node.entries) {
(target as Map<unknown, unknown>).set(values[key], values[value]);
}
} else if (node.type === "set") {
for (const value of node.entries) {
(target as Set<unknown>).add(values[value]);
}
}
}
Loading
Loading