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
70 changes: 68 additions & 2 deletions docs/src/interop/nodejs-builtins.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ is planned.
| `node:perf_hooks` | `@bytecodealliance/jco-std/wasi/0.2.x/node/24.x.x/perf-hooks` | Portable timing and observers; native telemetry throws. Runtime requirements are described below. |
| `node:readline`, `node:readline/promises` | `@bytecodealliance/jco-std/wasi/0.2.x/node/24.x.x/readline` and `/readline/promises` | Node 24.20 line parsing, questions, terminal editing and cursor actions over supplied streams. No WIT capability. |
| `node:repl` | `@bytecodealliance/jco-std/wasi/0.2.x/node/24.x.x/repl` | Node 24.20 REPL over the readline port and supplied streams; `useGlobal: true` only, bundles acorn -- see below. No WIT capability. |
| `node:timers`, `node:timers/promises` | `@bytecodealliance/jco-std/wasi/0.2.x/node/24.x.x/timers` and `/timers/promises` | Node 24 timer handles and promise timers over engine task scheduling; see runtime limits below. |
| `node:string_decoder` | `@bytecodealliance/jco-std/wasi/0.2.x/node/24.x.x/string-decoder` | Guest-local streaming decoder for Node 24. Requires no WIT capability. |
| `node:domain` | _(refused)_ | Deprecated upstream in its entirety. Resolves so the failure explains itself; every use throws `ERR_JCO_UNSUPPORTED_DEPRECATED_NODE_API`. |
| `node:ffi` | `@bytecodealliance/jco-std/wasi/0.2.x/node/26.x.x/ffi` | **Node 26 only.** Native calls and host memory over an explicit host capability; denied by default. Callbacks and guest-buffer addresses are refused -- see below. |
Expand Down Expand Up @@ -1596,7 +1597,6 @@ the module or upstream project.

| Modules | Why they are not enabled yet |
| ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `node:timers/promises` | A component-aware timer/event-loop integration is needed for delays, cancellation, and abort signals. |
| `node:trace_events` | The fallbacks preserve useful shapes, but tracing is synthetic or no-op without runtime integration. |
| `node:url` | There is substantial Node-derived code, but its eager `node:path` dependency adds a WASI environment requirement even for global-only URL use, and its namespace combines modern and legacy APIs that need separate policy. |

Expand All @@ -1608,7 +1608,7 @@ set of coordinated shims:

`node:crypto`, `node:dgram`, `node:http2`,
`node:perf_hooks`, `node:repl`, `node:stream`,
`node:stream/promises`, `node:stream/web`, `node:timers`,
`node:stream/promises`, `node:stream/web`,
`node:tls`, `node:util`, `node:util/types`, `node:v8`, `node:vm`, `node:wasi`,
`node:worker_threads`, and `node:zlib`.

Expand Down Expand Up @@ -1706,3 +1706,69 @@ Marks, measures, resource entries within the buffer limit, and synchronous funct
timing work there. Observer subscription, event listeners and resource buffer
overflow fail explicitly; `PerformanceObserver.supportedEntryTypes` is empty.
StarlingMonkey supports these runtime facilities and the observer APIs.

## Timers

`node:timers` and `node:timers/promises` target Node.js 24.20.0. Keep ordinary
imports in application code and bundle them with `jco componentize --bundle`:

```js
import { setTimeout, clearTimeout } from "node:timers";
import { setTimeout as delay, scheduler } from "node:timers/promises";

const pending = setTimeout(() => console.log("later"), 100);
pending.refresh();
clearTimeout(pending);
await delay(10, "ready");
await scheduler.yield();
```

### Timer handles and promises

The callback module supplies timeout, interval and immediate scheduling and
cancellation. Timeouts support `refresh()`, numeric/string cancellation IDs,
`close()` and `Symbol.dispose`. Immediates support cancellation and disposal.
`close()` remains functional because Node 24 marks it legacy, not deprecated.
Removed exports such as `enroll` and `active` are not reintroduced.

The promise module supplies delays, immediates, interval async iterators and the
scheduler singleton. It shares identity with `timers.promises` and the callback
functions' custom promisify hooks. Abort rejects with `AbortError`, `ABORT_ERR`
and the signal's reason as `cause`; interval iterators retain ticks while the
consumer is busy and release the timer when the loop breaks.

### Engine requirements

No additional WIT import or host mapping is required by the adapter. The component
engine supplies the task scheduler and its underlying clocks. StarlingMonkey
supports scheduling; the current QuickJS backend lacks task timers, so scheduling
throws `ERR_JCO_UNSUPPORTED_NODE_API` (or rejects for promise APIs). Imports and
argument validation remain usable without timers.

`setImmediate` uses the runtime's native implementation when present, otherwise
a zero-delay timer task. Nested immediates run in later tasks, but a Web engine
cannot reproduce libuv's I/O/check phase ordering. The adapter does not replace Web
globals: imported functions return Node-style handles while the engine's global
timer functions retain their native identities and return types. Cancel imported
timers with the imported cancellation functions or their handle methods.

`ref()`, `unref()` and `hasRef()` track handle state and forward liveness changes
when runtime handles support them. Active `unref()` and `{ ref: false }` throw or
reject explicitly on engines with numeric Web timer handles, including
StarlingMonkey. A failed promise setup cancels its timer. Native Node timer
handles support these operations when using jco-std directly in Node.

Node's private async-hook instrumentation, delay warnings, native inspection and
private abort-listener protection against `stopImmediatePropagation()` are not
ported. Abort handling uses the engine's public event API. Direct jco-std imports
can coexist with native Node builtins, but their timer handles and cancellation
registries are separate.

### Implementation source

The TypeScript adaptation follows Node's `lib/timers.js`, `lib/internal/timers.js`
and `lib/timers/promises.js` at commit
`71b8b174857e25106d39b61a9e6f30d927da8b01`, with retained MIT notices. Engine timers
replace Node's native queue. The audited unenv 2.0.0-rc.24 implementation was not
selected: its promise delays resolve immediately, interval promises yield only
once, and fallback handles lack the required lifecycle semantics.
11 changes: 11 additions & 0 deletions packages/jco-std/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ build NodeJS programs as components.
| `wasi/0.2.x/node/24.x.x/os` | `node:os` guest adapter over an explicit host capability |
| `wasi/0.2.x/node/24.x.x/path` | `node:path` adapter, Node 24 on WASI p2 |
| `wasi/0.2.x/node/24.x.x/string-decoder` | Guest-local `node:string_decoder` implementation for Node 24 |
| `wasi/0.2.x/node/24.x.x/timers` | Node 24 callback timers over engine scheduling |
| `wasi/0.2.x/node/24.x.x/timers/promises` | Promise timers, abortable interval iterators and scheduler |
| `wasi/0.2.x/node/24.x.x/domain` | `node:domain`, deprecated upstream: every use throws |
| `wasi/0.2.x/node/24.x.x/async-hooks` | `node:async_hooks` guest adapter, Node 24, synchronous scopes only |
| `wasi/0.2.x/node/24.x.x/diagnostics-channel` | `node:diagnostics_channel` guest adapter, Node 24 |
Expand Down Expand Up @@ -849,6 +851,15 @@ Unix-domain listeners, arbitrary custom transports, and HTTP/1.1 `Upgrade: h2c`.
represents individual requests, not observable Node HTTP/2 sessions or arbitrary
inbound servers.

### Node timers

Jco bundles `node:timers` and `node:timers/promises` imports using this package's
Node 24 adaptation. See the [timer compatibility documentation](../../docs/src/interop/nodejs-builtins.md#timers)
for usage and engine requirements. StarlingMonkey supplies task timers; QuickJS
currently rejects scheduling. Active `unref()` and `{ ref: false }` require runtime
handles with liveness control. Direct adapters can coexist with native Node
builtins, with separate timer handles and cancellation registries.

# License

This project is licensed under the Apache 2.0 license with the LLVM exception.
Expand Down
10 changes: 10 additions & 0 deletions packages/jco-std/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,16 @@
"./wasi/0.2.x/node/24.x.x/tty/host/node": {
"types": "./dist/wasi/0.2.x/node/24.x.x/tty-host-node.d.ts",
"node": "./dist/wasi/0.2.x/node/24.x.x/tty-host-node.js"
},
"./wasi/0.2.x/node/24.x.x/timers": {
"types": "./dist/wasi/0.2.x/node/24.x.x/timers.d.ts",
"browser": "./dist/wasi/0.2.x/node/24.x.x/timers.js",
"default": "./dist/wasi/0.2.x/node/24.x.x/timers.js"
},
"./wasi/0.2.x/node/24.x.x/timers/promises": {
"types": "./dist/wasi/0.2.x/node/24.x.x/timers-promises.d.ts",
"browser": "./dist/wasi/0.2.x/node/24.x.x/timers-promises.js",
"default": "./dist/wasi/0.2.x/node/24.x.x/timers-promises.js"
}
},
"scripts": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/** Promise timers share the callback timer core and its runtime requirements. */
export * from "./timers/promises.js";
export { default } from "./timers/promises.js";
3 changes: 3 additions & 0 deletions packages/jco-std/src/wasi/0.2.x/node/24.x.x/timers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/** Node 24 timers over the component engine's task scheduler. */
export * from "./timers/index.js";
export { default } from "./timers/index.js";
81 changes: 81 additions & 0 deletions packages/jco-std/src/wasi/0.2.x/node/24.x.x/timers/callbacks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* Adapted from nodejs/node lib/timers.js, v24.20.0,
* commit 71b8b174857e25106d39b61a9e6f30d927da8b01 (MIT).
* TypeScript and engine task timers replace primordials and native scheduling.
* No native async hooks, process warnings, or private abort event flags.
*/
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.

import { validateFunction } from "../errors/core.js";
import { Timeout, Immediate, cancelTimeout } from "./handles.js";

export function setTimeout<T extends unknown[]>(
callback: (...args: T) => void,
delay?: number,
...args: T
): Timeout {
validateFunction(callback, "callback");
return new Timeout(
function () {
Reflect.apply(callback, this, args);
},
delay,
false,
);
}

export function setInterval<T extends unknown[]>(
callback: (...args: T) => void,
delay?: number,
...args: T
): Timeout {
validateFunction(callback, "callback");
return new Timeout(
function () {
Reflect.apply(callback, this, args);
},
delay,
true,
);
}

export function setImmediate<T extends unknown[]>(
callback: (...args: T) => void,
...args: T
): Immediate {
validateFunction(callback, "callback");
return new Immediate(function () {
Reflect.apply(callback, this, args);
});
}

export function clearTimeout(timer: Timeout | number | string | undefined): void {
cancelTimeout(timer);
}
export function clearInterval(timer: Timeout | number | string | undefined): void {
cancelTimeout(timer);
}
export function clearImmediate(immediate: Immediate | undefined): void {
if (immediate instanceof Immediate) {
immediate[Symbol.dispose]();
}
}
Loading
Loading