ferrijs-std 0.2.4

Node and web standard library for the ferrijs QuickJS runtime: WHATWG Streams, Events, AbortController, Buffer, crypto, fs, os, url, zlib and the capability model they enforce (partly derived from awslabs/llrt, Apache-2.0).
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
# ferridriver-jsstd

Vendored subset of [awslabs/llrt](https://github.com/awslabs/llrt) (Apache
License 2.0), providing the WHATWG Streams implementation, the `node:os`
module, and the pieces they depend on for the ferridriver QuickJS runtime.

Upstream: `0.9.0-beta`, re-synced against `awslabs/llrt@0a10758` (main,
2026-09-06). Every module is taken from that one commit.

| upstream crate     | module here  |
| ------------------ | ------------ |
| `llrt_utils`       | `utils`      |
| `llrt_context`     | `context`    |
| `llrt_exceptions`  | `exceptions` |
| `llrt_events`      | `events`     |
| `llrt_abort`       | `abort`      |
| `llrt_encoding`    | `encoding`   |
| `llrt_buffer`      | `buffer`     |
| `llrt_json`        | `json`       |
| `llrt_crypto`      | `crypto`     |
| `llrt_os`          | `os`         |
| `llrt_fs`          | `fs`         |
| `llrt_path`        | `pathutil`   |
| `llrt_stream_web`  | `stream_web` |
| `llrt_zlib`        | `zlib`       |
| `llrt_compression` | `compression`|
| `llrt_string_decoder` | `string_decoder` |
| `llrt_perf_hooks`  | `perf_hooks` |
| `llrt_tty`         | `tty`        |
| `llrt_navigator`   | `navigator`  |
| `llrt_url`         | `url`        |
| `llrt_util`        | `text`       |
| `llrt_test`        | `test` (dev) |

`llrt_stream` is NOT vendored, and not because we chose against it: llrt has
no node `stream` module. `llrt_stream` registers no `ModuleDef` and answers
no specifier — it is an internal trait library (`Readable` / `Writable` /
`SteamEvents`) that llrt's own `fs`, `net` and `child_process` implement.
`llrt_stream_web` is the module, and it is `stream/web`. There is likewise
no `querystring` anywhere in llrt.

The rest of llrt — its hyper/fetch stack, timers, console — is deliberately
not vendored: ferridriver has its own, over `reqwest`. `fs` is vendored
because it IS Node's `fs`, which is what a suite expects; only the Rust
path helpers of `llrt_path` come with it (`pathutil`) — the `path` MODULE
stays ferridriver's. `os` is vendored
because ferridriver has nothing equivalent and the module is pure host
introspection with no overlap with the automation stack. From `llrt_util`
only the four text codecs are taken (`TextEncoder`, `TextDecoder` and their
stream forms); its `format` / `inherits` / `inspect` are `node::util`'s,
which are richer.

## What a host installs

Two entry points, and nothing else to remember:

- `jsstd::init(ctx)` — every global this crate provides: `DOMException`,
  `Event` / `EventTarget`, `AbortController` / `AbortSignal`, the Streams
  surface, `Buffer` / `Blob` / `File`, `crypto`, `TextEncoder` /
  `TextDecoder` (+ their stream forms), `URL` / `URLSearchParams`, `atob` /
  `btoa`, `structuredClone` and `performance`.
- `jsstd::modules::modules()` — every Node / web MODULE it serves, each
  entry carrying its specifiers, the `ModuleDef` the ES loader declares,
  and the object `require()` returns: `path`, `buffer`, `os`, `util`,
  `events`, `assert` (+ `/strict`), `url`, `process`, `timers` (+
  `/promises`), `crypto`, `zlib`, `string_decoder`, `perf_hooks`, `tty`,
  `stream/web`. The host merges that list into its own loader,
  its `require` table and its bundler's external list, so the three
  cannot drift apart.

## `src/node/` — ferridriver-authored

Not everything Node exposes has a usable upstream in llrt. `llrt_util` is
`TextEncoder`/`TextDecoder` plus `format` and `inherits` (no `promisify`, no
`inspect`, no `types`), and `llrt_assert` is a single `ok`. Those modules are
written here instead, under `src/node/`, so the runtime still has exactly one
implementation of each surface:

| module | why it is ours |
| ------ | -------------- |
| `node::inspect` | The `util.inspect` / `util.format` renderer, moved out of `ferridriver-script`'s `console` so `console.log`, `util.format` and `util.inspect` cannot drift apart |
| `node::deep_equal` | Structural equality for `util.isDeepStrictEqual` (and `assert.deepStrictEqual` when it lands) |
| `node::util` | The `util` module |
| `node::assert` | The `assert` module (upstream `llrt_assert` is a single `ok`) |
| `node::process` | A sandbox-safe `process` — inert identity and timing, with `env` and `cwd()` supplied by the host — and its module form |
| `node::timers` | The module form of `web::timers`, plus `timers/promises` |
| `node::path` | The `path` module, moved out of `ferridriver-script`'s `node_compat` |
| `node::bytes` | The one JS-value-to-`Vec<u8>` walk: `BufferSource`, `Buffer`, byte arrays, encoded strings. `crypto`, the compression streams, `Buffer.from` and `setInputFiles` all read through it — there were three separate walks before |

`src/node/` carries its own `rustfmt.toml` re-enabling formatting (the crate
disables it for the vendored subtree) and follows the repo's house style. It
is compiled under this crate's relaxed lints because pedantic's
`needless_pass_by_value` cannot be satisfied by an rquickjs callback, which
must take owned JS values.

## `src/web/` — ferridriver-authored

Web-platform globals llrt has no upstream for. Same formatting rules as
`src/node/`.

| module | what it is |
| ------ | ---------- |
| `web` (`mod.rs`) | `atob` / `btoa` (the WHATWG forgiving-base64 algorithm, which `base64::STANDARD` does not implement), `structuredClone`, and `performance.now()` / `timeOrigin` over a monotonic base. `llrt_buffer`'s module form reads `atob` / `btoa` off the globals, so installing them here is what makes `require('buffer').atob` resolve |
| `web::form_data` | `FormData`. It holds entries and nothing else: a host serializes them with its own multipart writer and hands parsed bodies back through `from_entries` |
| `web::compression` | `CompressionStream` / `DecompressionStream` (gzip, deflate, deflate-raw) over the vendored `TransformStream` |
| `web::timers` | `setTimeout` / `setInterval` / `clearTimeout` / `clearInterval` / `setImmediate` / `queueMicrotask`. NOT installed by `init`: a host supplies a `CallbackPolicy` so ambient state (ferridriver carries an `allow.net` grant) survives from arming the timer to running the callback |
| `web::blob_bytes` | The bytes-and-type read of a `Blob` / `File` value |
| `web::js_iterator` | The live-iterator protocol object `FormData`'s `entries` / `keys` / `values` return |

## Keeping it re-syncable

Sources are kept byte-close to upstream, including upstream's 4-space
formatting, so a re-sync against a newer llrt stays a mechanical diff. The
crate therefore does **not** inherit the workspace lints (see its
`Cargo.toml`), and `cargo fmt` must not be pointed at it.

Re-sync recipe (from a checkout of llrt):

```sh
for m in utils:libs/llrt_utils context:libs/llrt_context \
         zlib:modules/llrt_zlib string_decoder:modules/llrt_string_decoder \
         perf_hooks:modules/llrt_perf_hooks tty:modules/llrt_tty \
         navigator:modules/llrt_navigator compression:libs/llrt_compression \
         encoding:libs/llrt_encoding exceptions:modules/llrt_exceptions \
         events:modules/llrt_events abort:modules/llrt_abort \
         os:modules/llrt_os buffer:modules/llrt_buffer \
         fs:modules/llrt_fs \
         json:libs/llrt_json crypto:modules/llrt_crypto \
         url:modules/llrt_url \
         stream_web:modules/llrt_stream_web; do
  name="${m%%:*}"; path="${m##*:}"
  cp -R "$LLRT/$path/src" "src/$name" && mv "src/$name/lib.rs" "src/$name/mod.rs"
done
# `text` takes only llrt_util's four codec files; its own mod.rs stays.
for f in text_encoder text_decoder text_encoder_stream text_decoder_stream; do
  cp "$LLRT/modules/llrt_util/src/$f.rs" "src/text/$f.rs"
done
# per-module first, then the cross-crate rewrite (BSD sed has no \b — use perl)
for name in utils context encoding exceptions events abort os buffer json crypto url text stream_web \
            zlib string_decoder perf_hooks tty navigator compression; do
  find "src/$name" -name '*.rs' | while read -r f; do
    perl -pi -e "s/\bcrate::/crate::${name}::/g" "$f"
  done
done
find src -name '*.rs' | while read -r f; do
  perl -pi -e 's/\bllrt_([a-z_]+)/crate::$1/g' "$f"
done
```

Then re-apply the local deltas below.

## Local deltas

Everything here is a fix or a visibility widening, never a behaviour change
for ferridriver's convenience. Upstream candidates.

0. **Upstream regressions we do NOT take.** Still true at the 2026-09-06 main sync:
   upstream still ships the two transform-stream bugs listed in deltas 2
   and 3 below — and has since changed
   `transform_stream_error_writable_and_unblock_write` to take `_e` and
   ignore it, moving further from the spec. A future re-sync must keep
   OUR versions of `stream_web/transform/{controller,stream}.rs`,
   `stream_web/writable/mod.rs` and the visibility widenings; taking
   upstream wholesale reintroduces a hung `read()` and a `JS_FreeRuntime`
   assertion at teardown.

1. **`abort/abort_signal.rs`** — the `sleep-tokio` arm imports
   `CtxExtension` from `llrt_utils::ctx`, where it does not exist; it lives
   in `llrt_context`. Repointed at `crate::context`. (Upstream only builds
   the default `sleep-timers` arm, which is why this never surfaced there.)

2. **`stream_web/transform/controller.rs`**   `TransformStreamDefaultControllerPerformTransform` was missing spec step
   3: reacting to the transform promise's rejection by erroring the stream.
   A `transform()` that threw left both sides live, so a pending
   `reader.read()` never settled.

3. **`stream_web/transform/controller.rs`**   `TransformStreamErrorWritableAndUnblockWrite` was missing
   `WritableStreamDefaultControllerErrorIfNeeded`, so an errored transform
   left its writable in the `"writable"` state with an unresolved write
   request. Added `stream_web::writable::writable_stream_error_if_needed`
   for it.

4. **Visibility widenings only**`SizeAlgorithm` / `SizeValue` /
   `SizeFunction` / `NativeSizeFunction` from `pub(super)` to `pub(crate)`,
   and `writable_stream_default_controller_error` to `pub(crate)`. Upstream
   these were crate-visible because each module was its own crate; nesting
   them under one crate narrowed them below what their own public API needs.

5. **Feature gates**`sleep-tokio` is on by default. `sleep-timers` is
   deliberately *not* a Cargo feature (the timers module is not vendored, so
   `--all-features` would otherwise enable an arm that cannot compile); it is
   declared to rustc as a known-but-never-set cfg via `check-cfg` in
   `Cargo.toml`, which keeps the upstream `cfg` arms compiling out silently.

7. **`rquickjs` `half` feature** — enabled because the synced
   `utils/bytes.rs` and `stream_web/readable/byob_reader.rs` handle
   `Float16Array`. Without it `PredefinedAtom::Float16Array` does not
   exist and `f16` has no `TypedArrayItem` impl.

6. **Tests**`abort::abort_signal::tests::test_abort_signal` is no longer
   gated on `sleep-timers`, so it covers the `sleep-tokio` path we build.
   Two regression tests were added to `stream_web/transform/tests.rs` for
   deltas 2 and 3.

8. **`os/mod.rs` — no Windows arm.** `llrt_os`'s `windows.rs` is not
   vendored: ferridriver targets macOS and Linux, and that arm needs four
   Windows-only dependencies (`whoami`, `windows-registry`,
   `windows-result`, `windows-version`).

9. **`os/unix.rs``getpwuid_r` instead of the `users` crate.** Upstream
   read the login name and shell through `users` 0.11, which has been
   unmaintained since 2021, and at the 2026-09-06 sync moved to `uzers`
   0.12, the maintained fork. The replacement calls `getpwuid_r` directly —
   the same call either crate makes — including its ERANGE grow-the-buffer
   protocol, so the one-line upstream swap is not taken and neither crate
   is a dependency here.

10. **`os/statistics.rs` — real CPU times.** Upstream returns
    `times: { user: 0, nice: 0, sys: 0, idle: 0, irq: 0 }` for every CPU,
    with the comment "cannot be obtained at this time". sysinfo does not
    expose them, but the kernel does: `/proc/stat` on Linux and
    `host_processor_info` on macOS, which is where libuv reads them for
    Node. Ticks are converted to milliseconds through `_SC_CLK_TCK`.
    Darwin does not account interrupt time, so `irq` stays 0 there — as
    it does in libuv.

11. **`os/mod.rs``fill()` / `os_object()` split.** Upstream fills the
    module's default export inline inside `evaluate`. ferridriver serves
    every native module twice, as an ES module and as a synchronous
    `require()` namespace, and its loader requires both to read from one
    place, so the body moved into a function.

12. **`os` feature gates.** Upstream's `system` / `statistics` / `network`
    features are declared here too (all on by default) so the `#[cfg]`
    arms stay exactly as upstream wrote them.

## Known gaps against Node

- `os.constants` (signal, errno, priority and dlopen tables) is not
  implemented upstream and is not added here.
- `networkInterfaces()` marks link-local and multicast addresses
  `internal: true`; Node marks only loopback interfaces internal.

13. **`buffer/blob.rs` and `buffer/file.rs` — three fixes.** Both files
    ARE vendored and `init` defines both classes (an earlier version of
    this note said neither was taken). `Blob::stream`
    copies the bytes out before building the pull closure: upstream
    captures the JS `ArrayBuffer` in a native closure, a cycle the
    collector cannot see, which trips `JS_FreeRuntime`'s
    `list_empty(&rt->gc_obj_list)` assertion at teardown.
    `File::from_bytes` goes through `Blob::from_bytes` rather than
    `into_js`, which made a JS array of numbers that `new Blob([...])`
    stringified (`File.from_bytes(b"hi")` read back as `"104105"`).
    And `buffer/mod.rs` chains `File.prototype` to `Blob.prototype` after
    defining both classes, since rquickjs classes do not inherit and
    upstream leaves `file instanceof Blob` false.

14. **`buffer/class.rs`** is upstream's `buffer.rs`, renamed. A `buffer`
    module inside a `buffer` module trips `clippy::module_inception`,
    which is on by default and the repo's gate runs `-D warnings`.

15. **`buffer/mod.rs``equals` and `toJSON`.** Node defines both on
    `Buffer.prototype`; upstream defines neither, and the hand-written
    class this vendoring replaced had both, so not adding them would be a
    regression. Added after `set_prototype` rather than inside the
    vendored file, so `class.rs` stays a mechanical diff. Upstream
    candidates.

16. **`llrt_encoding`'s build script is not vendored.** It only calls
    `llrt_build::set_nightly_cfg()`; this repo pins stable. As of the
    2026-09-06 sync upstream has dropped its last `rust_nightly` arm
    (`bytes_to_utf16_string` now uses the stable `as_chunks`), so no
    vendored file reads either cfg; `rust_nightly` and `nightly` stay
    declared as known-but-never-set cfgs in `Cargo.toml` so a future
    upstream arm compiles out silently rather than warning.

## Known gaps against Node — `Buffer`

`Buffer` is a real `Uint8Array` subclass, so every typed-array method
works and index access reads bytes. Missing against Node: the
string-aware overrides of `includes` / `indexOf` / `lastIndexOf` / `fill`
(the `Uint8Array` versions are inherited, so they take byte values, not
strings), `swap16` / `swap32` / `swap64`, `compare`, and `Buffer.poolSize`.

17. **`crypto/provider/{ring,openssl,graviola}.rs` are not vendored.**
    Only the pure-Rust provider (`crypto-rust`, upstream's own default) is
    taken; the other three back-ends would each add a system dependency.
    Their feature names are declared as known-but-unset cfgs. Upstream's
    `_modern-webcrypto` marker (ML-DSA, ML-KEM, the hybrid KEMs,
    ChaCha20-Poly1305, SHA-3 / cSHAKE / TurboSHAKE, `supports`,
    `getPublicKey`, `encapsulate*` / `decapsulate*`) is declared and on:
    every upstream provider enables it, and its back-ends (`ml-dsa`,
    `ml-kem`, `chacha20poly1305`, `sha3`, `shake`, `cshake`, `keccak`,
    `sponge-cursor`, `ctutils`) are all pure Rust. `provider/modern.rs`
    is provider-independent upstream and is vendored as is.

18. **`crypto` / `json` macro imports.** `iterable_enum` and `str_enum` are
    `#[macro_export]`ed, so they live at the crate root rather than under
    `utils` — the import lines are repointed at `crate::`.

19. **Hash crates keep their `oid` feature.** `sha1` / `sha2` / `md-5` are
    taken with `oid` (and `aes-gcm` with `hazmat`): PKCS#1 v1.5 signing
    needs `AssociatedOid`, and WebCrypto allows 32- and 64-bit GCM tags,
    which are gated behind those features in the 0.11 releases.

20. **`url/url_search_params.rs` — a non-string, non-object init.**
    Upstream ignores it, so `new URLSearchParams(null)` and
    `new URLSearchParams(42)` both build an EMPTY query. WebIDL's init
    union is not nullable, so anything that is neither a sequence nor a
    record converts to USVString: the queries are `null` and `42`, which
    is what every browser engine produces. Only `undefined` (the argument
    omitted) means empty.

23. **`encoding/mod.rs``windows-1252` / `latin1` / `ascii` are real.**
    Upstream folds `Windows1252` into the UTF-8 arm in every direction,
    so `new TextDecoder('windows-1252').decode([0xE9])` answered U+FFFD
    and `Buffer.from('é', 'latin1')` produced the two UTF-8 bytes rather
    than one. Worse, one label map served both consumers, which cannot
    be right: Node's `latin1` is ISO-8859-1 and its `ascii` masks the
    high bit, while the WHATWG Encoding Standard maps BOTH labels to
    `windows-1252`. There are now two maps — `Encoder::from_str` for
    Buffer, `Encoder::from_web_label` for `TextDecoder` — and three
    single-byte variants (`Windows1252` with the real 0x80-0x9F index,
    `Latin1`, `Ascii`) implemented in both directions.

22. **`url/mod.rs``fileURLToPath` decodes and validates.** Upstream
    strips the `file://` prefix and hands the rest to `PathBuf`: the
    scheme is never checked, a host is silently swallowed, a query or
    fragment stays in the path, and percent-escapes are NOT decoded, so
    `file:///tmp/a%20b.txt` names a file whose name literally contains
    `%20`. Node checks the scheme, refuses a host it cannot address
    locally, drops query and fragment, decodes the escapes and refuses
    an ENCODED separator (which would otherwise change which file is
    named).

21. **`url/url_class.rs``urlToHttpOptions` matches Node's shape.**
    Upstream reports `port` as a STRING, omits `search` / `hash` when
    they are empty, keeps the brackets on an IPv6 `hostname`, and joins
    the raw percent-encoded credentials into `auth`. Node reports a
    numeric port, always sets `search` and `hash`, hands over a bare IPv6
    host (what a socket connect takes) and `decodeURIComponent`s the
    credentials. `URL::inner_url` was upstream's only reader for the
    omitted-hash branch and goes with it; the `percent-encoding` dep is
    for the credential decode (`url` keeps that crate private).

24. **`fs/mod.rs``existsSync`.** Node has it and a large share of real
    code calls it; upstream ships neither it nor a callback API, so
    without it the only way to ask whether a file is there is to catch a
    `stat` rejection.

25. **`fs/mod.rs` — one namespace per VM.** Upstream builds a fresh
    exports object per module evaluation. Node answers the SAME object
    for `require("fs")`, `require("node:fs")` and `fs.promises` vs
    `require("fs/promises")`, so the namespaces are built once per
    context and remembered; the `fs` global is that object too. Without
    it, identity comparisons are false and a caller who patches a method
    patches a copy nobody else sees.

26. **`zlib` — the codec back-ends taken.** Upstream's default is
    `compression-c`, which links a system zlib-ng, brotli and zstd. This
    crate takes the pure-Rust back-end wherever one exists
    (`flate2/rust_backend`, which is also what `CompressionStream`
    already used, and the `brotli` crate) and `zstd-c` for zstd, which
    upstream's own manifest says has no pure-Rust implementation. The
    `zstd` crate vendors and compiles the C rather than needing one
    installed, so the build still has no system dependency — the same
    posture as `rquickjs-sys` compiling QuickJS. All six upstream feature
    names are declared, so its `#[cfg]` arms stay exactly as written.

27. **`compression/streaming.rs` is not vendored.** Its only consumer
    upstream is `llrt_fetch`'s response decoder, and this crate does not
    vendor `llrt_fetch` (ferridriver's `fetch` is its own, over
    `reqwest`). Carrying it would be dead code that also trips
    `clippy::large_enum_variant``StreamingDecoder`'s zstd variant is
    ~336 bytes larger than the next — and boxing a variant to satisfy a
    lint in code nothing calls is worse than not taking the file. Same
    reasoning as `buffer/blob.rs`, `crypto/provider/ring.rs` and
    `os/windows.rs`.

28. **`zlib/codec.rs` and `string_decoder/decoder.rs`** are upstream's
    `zlib.rs` and `string_decoder.rs`, renamed. A module with its
    parent's name trips `clippy::module_inception`, which is on by
    default and the repo's gate runs `-D warnings`. Same fix as delta 14.

29. **`zlib/{brotli,codec,zstd}.rs` — macro imports.** `define_sync_function`
    and `define_cb_function` are `#[macro_export]`ed, so they live at the
    crate root rather than under `zlib`. The `use super::{...}` lines are
    split: the two macros come from `crate::`, the plain items still from
    `super::`. Same cause as delta 18.

30. **`perf_hooks` — the module only, not the global.** Upstream's `init`
    installs its own `Performance` class on `globalThis`, and taking it
    would be a regression: `Performance::now` reads
    `llrt_utils::time::now_nanos`, which is `SystemTime::now()` — the
    WALL clock. High Resolution Time exists to give a monotonic reading,
    so upstream's `now()` steps backwards whenever the system clock
    does, and `saturating_sub` clamps that to `0` rather than surfacing
    it. `web::init`'s reads `Instant::elapsed`, which is monotonic by
    construction. Upstream also leaves `origin_nanos()` at `0` until a
    host calls `time::init()`, and an unset origin makes `now()` return
    the whole Unix epoch in milliseconds.

    Upstream's MODULE body only reads `globalThis.performance` and
    re-exports it, so dropping `init` (and `performance.rs` with it)
    leaves `perf_hooks` serving ferridriver's own.

    The two things upstream's class had over the old plain object —
    `toJSON()` and being a real class instance — are in `web::performance`
    now, along with the User Timing and Performance Timeline surface
    neither side had. See "`performance`" below.

31. **`navigator` — this runtime's name.** Upstream hardcodes
    `userAgent` to `llrt <version>`. Shipping that verbatim would answer
    every user-agent sniffer with the wrong runtime. It reads
    `ferridriver/<version>` here, which is the shape Node 21+ uses.

    Worth knowing before relying on it: a `navigator` global is still how
    some libraries decide they are in a browser, the mirror image of the
    `process.versions.node` check. It is Node parity, not a browser
    claim, but a package that misroutes on it is misrouting for this
    reason.

32. **`stream/web` registered as a specifier.** The implementation was
    vendored from the start but only ever installed as globals, so
    `import { ReadableStream } from 'node:stream/web'` — Node's own name
    for it, and how a library reaches it without assuming a browser —
    resolved to nothing. `modules.rs` now serves it, reading the classes
    back off the globals so there is still one implementation.

## `performance`

`web::performance` is ferridriver's, not vendored, and covers three
specs rather than the `now()` / `timeOrigin` pair it started as:

- **High Resolution Time**`now()`, `timeOrigin`, `toJSON()`.
  `now()` reads `Instant::elapsed`; the wall clock appears exactly once,
  as `timeOrigin`, which is what the monotonic readings are relative to.
- **User Timing**`mark()`, `measure()`, `clearMarks()`,
  `clearMeasures()`, and the `PerformanceMark` / `PerformanceMeasure`
  classes with their `detail`. `measure` implements all three overloads
  (bare name, start-mark, options bag) and refuses the two combinations
  the spec calls out: an options bag together with a trailing `endMark`,
  and `start` + `end` + `duration` all at once.
- **Performance Timeline**`getEntries()`, `getEntriesByName()`,
  `getEntriesByType()`, sorted chronologically by `startTime` rather
  than by insertion, because `mark(name, { startTime })` can backdate an
  entry. The sort is stable, so entries sharing a `startTime` keep the
  order they were recorded in.

`PerformanceMark` and `PerformanceMeasure` chain their prototype to
`PerformanceEntry`, so `mark instanceof PerformanceEntry` holds.
Constructibility follows the IDL: `PerformanceMark` takes
`(name, options)` and does NOT buffer (only `performance.mark()`
records), while `PerformanceEntry`, `PerformanceMeasure` and
`Performance` throw `Illegal constructor`.

`performance.now()` and `process.hrtime()` count from ONE base
(`performance::monotonic_base`), so the two are correlatable the way
Node's are — it derives both from a single libuv hrtime. Two separate
`Instant::now()` calls would put a constant, invisible skew between
them; a test asserts they agree within a millisecond.

Not implemented: `PerformanceObserver` (it needs a task-queue hook this
runtime has no equivalent of), the resource and navigation entry types
(no document), Node's `eventLoopUtilization` / `nodeTiming`, and a
buffer size limit — nothing evicts, so a program marking in a hot loop
grows the buffer until it calls `clearMarks`.

## `Intl` is absent, and llrt cannot fill it

QuickJS-ng as vendored by `rquickjs-sys` contains no ECMA-402 at all —
no `Intl` object, and no build flag that would add one. `react-intl` and
anything like it fails with `Intl is not defined`.

`llrt_intl` does not close this. It is `Intl.DateTimeFormat` plus
`supportedValuesOf` and `Date.prototype.toLocaleString`, aimed at
timezone support, and it carries a `jiff` dependency and ~3k lines of
bundled CLDR data. There is no `NumberFormat`, `PluralRules` or
`Collator` — which is the part a formatting library actually reaches
for.

Until that changes, a suite that needs `Intl` supplies a JS polyfill
through `[bundler.alias]`, which keeps the choice (and its weight) in
the extension that needs it.

33. **`crypto/subtle/digest.rs` — a synchronous validation failure is
    kept.** Upstream validates the algorithm before returning the
    future, so the exception is thrown while `subtle_digest` is still
    returning `Ok(future)`; by the time the future runs, the pending
    exception is gone and the promise rejects with an uninitialized
    value (`typeof e === "unknown"`). The thrown value is now taken with
    `ctx.catch()` at that point and re-thrown inside the future, where
    the rejection is built. Upstream candidate.