Node pushes Buffer slices of its output buffer, so a chunk from a zlib
stream converts to its text. The runtime pushed plain Uint8Array
storage: String(chunk), template literals and chunk + "" printed the
bytes comma-separated. 5370 and 5371 check the conversions and
Buffer.isBuffer.
5337 ordered the concurrent map() callbacks with 20 ms and 5 ms timers, so
a slow machine changed which callback ended first, in Node as well. The
callbacks now resume on setImmediate, and "a" waits for a promise that
"c" resolves, which keeps the same order and output.
- Every mbedTLS call in the cipher runtime is checked. A failed update(),
final() or setAAD() returns no output and retires the context, so later
calls throw and no tag is returned or accepted. An update past the
AES-GCM limit of 2^36 - 32 bytes per message throws Node's "Trying to
add data in unsupported state" error; it used to return zeroed output
and leave the tag covering only the earlier data.
- chacha20-poly1305 decryption without setAuthTag() fails in final()
again. Node 24.22 skips the tag check when authTagLength is not passed
either and returns unverified plaintext; limitations.mdx records the
difference.
- A runtime unit test moves contexts next to the GCM message and AAD
limits and checks that the failure is permanent; errors.test.ts covers
decryption without a tag.
The generated localeCompare table now agrees with Node on spellings that
tie at the primary and secondary levels. "l·" and "L·" collate as one
unit, equal to U+0140 and U+013F, through a contraction table. Precomposed
characters take their decompositions' elements after the case weights are
assigned, so U+0130 equals I + U+0307. Ligatures whose spelled-out letters
tie with them (ij and ij) get the case weight Node places them at among
their first letter's case forms, and ligatures with their own secondary
(Æ) keep their own case. The generator's check now also compares every
covered character's decompositions, case forms and ligature spellings in
four contexts. A per-thread class table lets the lazy primary comparison
find characters that need element lists with one lookup.
util.stripVTControlCharacters ran Node's ANSI pattern through the
regex replace loop, which tried the OSC branch's lazy scan at every
introducer. Text with many unterminated OSC introducers took quadratic
time (about 200 s for 100,000 of them).
The function now scans once and gives the same result as Node 24.22's
pattern: the OSC branch ends at the first terminator after ESC ], and
that position is cached across introducers; the CSI branch resolves its
first match without backtracking. A removal that brings lone surrogates
together keeps canonical storage.
Corpus programs 5470-5474 cover OSC and CSI forms, seeded random input,
long unterminated runs and surrogates, and strip-vt-linear.test.ts checks
that 400,000 unterminated introducers finish quickly.
Hash.update and Hmac.update accept every Buffer input encoding and any
TypedArray or DataView. Hex input of odd length throws Node's
ERR_INVALID_ARG_VALUE, an encoding Node does not know reads as UTF-8, and
data of other types throws ERR_INVALID_ARG_TYPE. digest() returns a
string for every Buffer encoding and a Buffer for a falsy or unknown one,
and a repeated Hmac.digest() returns Node's empty result.
createHash and hash.copy take options.outputLength for the SHAKE and
KECCAK-KMAC digests, with Node 24.22's validation and its
ERR_OSSL_EVP_NOT_XOF_OR_INVALID_LENGTH error, including the library and
reason properties, for other digests. crypto.hash takes an output
encoding or an options object with outputEncoding (including "buffer")
and outputLength. The functionName and customization options are
validated and then refused with Node 24.22's errors. createHmac takes
TypedArray, DataView and ArrayBuffer keys and options.encoding for string
keys, and checks them in Node's order.
Calls whose encodings and options the compiler can resolve keep their
typed fast paths. Other forms go through runtime entry points that apply
Node's checks and return a string or Buffer, checked at the call's type.
A literal digest encoding for which Node returns a Buffer is refused
when the declared result is a string.
The digest compression functions start on a cache line, so their speed
no longer depends on the size of unrelated runtime code.
setEncoding() converted the buffered entries once, but Readable.from's
lazy reader, the operators and compose added later object-mode entries in
the kind they arrived in, so a Buffer entry could be read as a string.
Every insertion now passes the entry's kind: Buffer entries decode through
the stream's decoder, entries that would mix strings and Buffers are
refused, and the buffer insert checks the kind. setEncoding() on an ended
stream also flushes the decoder's partial tail, as in Node 24.
A class instance reached through an untyped or interface-typed value
printed as a plain object ({ r: 1 }); it now prints with its class's name
(Circle { r: 1 }), including subclasses and class expressions. Values
whose static class has subclasses, which were refused, print through the
same view. Maps and Sets held in untyped values printed as an empty
string; they now print as Map(n) { k => v } and Set(n) { v }.
A number or string seen as unknown lives in a dyn box. Allocating one
cleared the whole value union, although both kinds set a single payload
word, and releasing one went through the destroy queue, the generic dispose
switch and the weak-dispose hook before the node returned to the free list.
Number and string boxes now skip the union clear, and their last release
returns them to the free list directly. They hold at most one string, are
never cycle candidates and cannot be weak-collection keys, so the skipped
steps have nothing to do for them.
createHash, createHmac, crypto.hash, pbkdf2, pbkdf2Sync, hkdf and hkdfSync
accept every digest crypto.getHashes() lists in Node 24: SHA-512/224,
SHA-512/256, SHA-256/192, SHA3-224 to SHA3-512, Keccak-224 to Keccak-512,
SHAKE128, SHAKE256, KECCAK-KMAC-128, KECCAK-KMAC-256, RIPEMD-160,
BLAKE2b-512, BLAKE2s-256, SM3 and MD5-SHA1. The new runtime unit
scr_hash.c implements Keccak, RIPEMD-160, BLAKE2b, BLAKE2s and SM3, and
is registered in both runtime pack matrices and the C-compile path.
Names resolve as Node 24.22 resolves them: case-insensitively, with
OpenSSL's provider aliases and object identifiers, ending at the first
NUL, and with trailing spaces allowed except on the RSA signature
aliases. "dss1" and "DSS1" are SHA-1, and createHash and crypto.hash
accept "dss1" in any case. The compiler no longer refuses literal digest
names outside SHA-1/SHA-2 and MD5; unknown names throw Node's errors at
run time.
SHAKE and KMAC digests use their default output lengths. createHmac
rejects them with Node's OpenSSL error, pbkdf2 with "xof digests not
allowed", and hkdf with "Deriving bits failed". pbkdf2 with a key length
of 0 now fails with "Deriving bits failed" as in Node 24.22, and the
callback forms pass these failures to the callback.
PBKDF2 computes the keyed HMAC states once instead of on every
iteration, and digest contexts are cleared with memset and a compiler
barrier instead of a byte loop. The Keccak permutation, the RIPEMD-160
and BLAKE2 compression functions and the SM3 rounds are unrolled with
constant indices.
Node converts a string chunk with Buffer.from(chunk, encoding) and
returns Buffer.concat for read(n), so every byte chunk user code
receives from a non-object-mode stream is a Buffer. The runtime made
these chunks as plain Uint8Array storage. A typed write(), transform()
or 'data' callback then printed String(chunk), `${chunk}` and
chunk + "" as comma-separated bytes (97,98) instead of the text, and
read(n) and read() after unshift(string) returned non-Buffer values to
untyped code.
The chunks the stream creates now carry the Buffer brand: write() and
push() of a string (with or without an encoding), transform output given
as a string (the callback or this.push), unshift() of a string, and the
concatenation read(n) returns.
Corpus 5445 (typed callbacks, pipeline and stream/promises pipeline)
and 5446 (untyped code, Buffer.isBuffer at each boundary) check this
against Node.
String.prototype.localeCompare keeps its results and gets fast paths for
the common cases. Byte-equal strings compare equal at once. ASCII strings
compare their primary weights in one pass from a per-thread table and
read the secondary and tertiary weights only when every primary is equal.
Latin text whose marks carry no primary weight compares primaries
lazily, decoding only until the first difference. Other text, and every
tie at the primary level, uses the existing element lists, which no
longer zero their inline buffers or check capacity per element. The
backend lends both strings to the call instead of retaining them.
- chacha20-poly1305 refuses non-empty AAD once data has been processed
(ERR_CRYPTO_INVALID_STATE); empty AAD is still accepted.
- chacha20-poly1305 decryption without setAuthTag() succeeds when no
authTagLength was passed, and is verified when one was.
- Changing the output encoding of update() or final() throws
ERR_INVALID_ARG_VALUE instead of ERR_INTERNAL_ASSERTION.
- The ctsMode and xtsStandard options are validated in Node's order;
the implemented ciphers report ERR_CRYPTO_UNSUPPORTED_OPERATION.
- The list of known but unimplemented cipher names follows Node 24.22's
getCiphers(), so the CBC-HMAC names are unknown ciphers.
Array.prototype.sort and toSorted with a comparator now run V8's
TimSort (array-sort.tq) step for step in the runtime, in the generated
sort for booleans and unions with undefined, and in the checked-value
sort. The comparator is called as f(x, y) < 0 in Node's argument order,
so a comparator sees Node's calls in Node's order and an inconsistent
comparator yields Node's permutation. Undefined values, including a
union's undefined arm, are set aside before sorting and never reach the
comparator, and a checked comparator's result converts with ToNumber.
Structured inputs take fewer comparator calls than the previous merge
sort. Each thread reuses one sort buffer of up to 1 MiB.
Blob and File (globals and node:buffer exports) compile to native
handles in the new scr_blob.c, following Node 24.22's
lib/internal/blob.js and file.js. A Blob is a window over a shared,
immutable byte store plus the byte length of each part. Sources (strings,
typed arrays, DataView, ArrayBuffer, Buffer, other Blobs, Set and Map)
convert as a Web IDL sequence, with each non-buffer part converted to a
string, before the options are read, and are copied when the Blob is
built. The constructors throw Node's Web IDL TypeErrors and its
ERR_INVALID_ARG_TYPE, ERR_INVALID_ARG_VALUE and ERR_MISSING_ARGS errors,
and read and convert their options in Node's order. The type is
lowercased, or cleared when it holds a character outside U+0020..U+007E.
endings: "native" rewrites CRLF, CR and LF to the platform's line break.
File takes its name with lone surrogates replaced and lastModified as a
number (Date.now() when absent).
slice() converts its offsets as a [Clamp] long long (rounded half to
even) and keeps the parts Node's data queue keeps. text(), bytes() and
arrayBuffer() settle on the same microtask turn as in Node, which depends
on the part count. util.inspect prints Node's `Blob { size, type }` and
`File { ..., name, lastModified }` forms, with `[Blob]` past the depth
limit. String() and toString() give the class tag, JSON.stringify gives
{}, and structuredClone returns a new Blob or File over the same bytes.
instanceof works for both spellings. stream(), textStream() and the
Object.prototype members other than toString() fail at compile time.
Non-array iterables other than Set and Map throw SC2020.
Three handle-table additions support this: a structuredClone hook, a
JSON `{}` flag, and a bracketed past-depth inspect flag. scr_blob.c is an
optional unit selected by dynAsync, because the dynamic dispatch path
boxes its reads as dyn promises through scr_async_dyn.c.
Corpus 5285-5290 and a node-types fixture check the behavior against
Node.
In TypeScript, Object.values over a checked-dynamic value whose result is
only iterated with for-of, indexed, or passed to every, some, findIndex,
forEach, join or reduce now returns a native vector of the checked values.
The program never observes the array itself, so the checked-dynamic array
and, for for-of, the dynamic iterator protocol are skipped. Typed records
fill the vector through their record ops table and plain data objects
from their entries in own-key order. Other uses, and JavaScript files,
keep the checked-dynamic array.
Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw and Unzip, from
createGzip() and the other factories or new zlib.Gzip(options), are
runtime Transform streams over zlib.
- scr_zlib.c gains a step engine that follows Node's ZlibContext: lazy
init, Unzip's gzip-magic detection, the dictionary retry, Gunzip's
multi-member loop, and Node's error messages, codes and errno.
- scr_zlib_stream.c, linked when a program uses zlib and streams, runs
each write as engine steps on the worker pool with Node's output
chunking, the finish flush from 'prefinish', read-side backpressure,
flush(), params(), close(), reset() and bytesWritten, and validates
options in Node's order.
- The fallback declarations add the classes, factories and a
literal-typed ZlibConstants; zlib.constants and the module-level Z_*
copies bake as literals. Brotli streams and their factories are refused
by name.
Corpus programs 5370-5377 cover round trips, chunking and event order,
errors, options, flush and params, stream/promises, constants and
backpressure.
A record built from unchecked element reads can hold undefined in a field
the checker calls present. Such a record kept its widened layout only in
an inferred binding: an annotated parameter or return, or an unannotated
return, converted it to the declared layout with a checked narrow, which
throws a TypeError for a missing element where Node passes undefined
through.
Record values that flow between bindings and the parameters and results
of functions and methods whose every call is visible now form flow
classes. A class takes the widened layout, with the union of its members'
optional fields, when it is closed: each value entering it is a fresh
object literal or comes from a member, each use of a member reads or
writes its fields, passes it to another member or only inspects it, and
every member declares the same shape. Members then share one layout, so
no conversion runs at the boundary and the record keeps its identity.
A class that reaches other storage keeps the declared layout for every
member, including the inferred bindings and the record parameters the
analysis widened before. Converting there copied the object, so writes
through a parameter or a pushed record did not reach the caller's object;
such a literal now converts once where it is created.
Interface dispatch now unboxes a receiver it has matched by key with one
load and no reference count change, and keeps its checked fallback out of
line so the key tests inline at each call site. A loop over an array of
interface values lends each element to a dispatch whose implementing
methods cannot release it; the fallback, which can run any code, takes
its own reference to the receiver.
The Readable operators (map, filter, forEach, toArray, some, find, every,
flatMap, drop, take, reduce) run over string and Buffer chunk streams with
Node's ordering, errors, and concurrency, highWaterMark and signal options.
Readable.from reads arrays lazily and accepts the objectMode and
highWaterMark options, and its JavaScript form follows from.js's checks.
stream.compose returns a Duplex over a pipeline of stream instances.
pipeline now follows pipeline.js: per-stream destroyers and eos watchers,
and the callback on a tick once every watcher finished. 'finish' waits for
a pending _final or _flush callback, and a failed synchronous write emits
'error' one tick later, as in Node.
The schema reader scanned every member name with the general string scan,
compared it with memcmp, and re-entered the recursive reader for every
value. The expected next member's name is now compared directly against
the text and its closing quote when the name has at most eight bytes and
none that JSON text escapes; other keys keep the general scan and compare
names with scr_key_equal. Number, boolean and string members and elements
are read in the record and array loops; records and arrays still recurse.
The native link feature records in the self-hosting tests list cipher,
and the library profile test uses crypto.createSign for its remediation
example now that createCipheriv is supported.
Assignments to returnValue and cancelBubble on a typed Event subclass
receiver lower in value position as well as in statements: an
initializer (`const r = (ev.returnValue = false)`), a call argument, and
a chained assignment write the accessor through event.set and yield the
assigned value. The short-circuit forms (&&=, ||=, ??=) read the
accessor through event.get and write it only on the branch that assigns.
The receiver now evaluates before the assigned value, as in Node.
OutgoingMessage now corks the message for a tick like Node 24.22, so
chunked writes in one tick (and a corked end(chunk)) go out as one chunk,
for server responses and client requests. Fetch accepts a connection
header that is a comma list of tokens, as Undici 7.29.1 does. The
crypto.constants declaration matches the Node 24.22 table, with a pin test.
The emitted JSON writers read each array element through scr_arr_state
and scr_arr_get_ref, retaining and releasing it, and appended every
bracket, comma and key through a runtime call. Arrays whose indices below
the length all hold values now serialize from their slots
(scr_arr_dense_slots) with borrowed elements; other arrays keep the
element walk. Fixed text is stored inline when it fits the buffer's spare
capacity, booleans write their literal inline, and numbers are formatted
directly into the buffer.
Arrays whose elements are class-backed interface values are native arrays
of the element capsules, so iteration, indexing and most array methods no
longer go through the checked-dynamic array, and method calls receive a
receiver they can dispatch. Methods without a native form for these
elements run on the array's live dynamic view.
A read of an interface property signature has the property's static type
and reads the field of an implementing class directly. Accessors, object
literals and JavaScript classes keep the checked property read.
Pin Node v24.22.0 (commit f14cd3aa7bf9, Undici 7.29.1, OpenSSL 3.5.8) in
.node-version, Dockerfile.sandbox, the docs matrix fallback, the
compatibility pin, the fetch profile, and process.version. A node-pin test
checks that these pins agree. The Node compatibility snapshot is
regenerated from the v24.22.0 API docs; the new stream_iter chapter maps
to node:stream/iter, and the SuiteContext rows no longer match the
TestContext evidence.
Runtime and compiler changes follow Node 24.22 behavior:
- fs.read, fs.readSync, and FileHandle.read validate the position before
the zero-length return.
- fs.statfs results include frsize; Windows reads
FileFsFullSizeInformation and reports bfree and bavail separately.
- fs.utimes, futimes, and lutimes keep nanosecond precision.
- ServerResponse sets writableFinished where 'finish' is emitted and runs
end() callbacks before the 'finish' listeners while the socket is still
attached; a request read to 'end' closes before its response.
- socket.write(string, 'buffer') throws ERR_UNKNOWN_ENCODING.
- URL protocol-setter rejections keep the current URL, the host setter
no longer leaves a "/." escape when it adds a port to a "//" path, and
new URLSearchParams(null) parses "null".
- util.stripVTControlCharacters uses Node 24.22's ANSI pattern,
util.styleText accepts #RGB and #RRGGBB colors, and
util.types.isDataView has its JS name and length.
- structuredClone and AbortSignal.any use the "cannot be converted to
sequence" message.
- crypto.getCiphers, getHashes, and constants match Node 24.22.
- dns.lookup, net.connect, and tls.connect throw ERR_INVALID_ARG_VALUE for
hosts with null bytes.
- fs.readFile validates options.buffer and refuses its valid forms;
httpValidation and certificateCompression refuse by name.
- url.domainToASCII, url.domainToUnicode, and url.parse hostnames follow
ada 4.0: ASCII domains are lowercased without checking their xn--
labels, the RFC 5893 rule checks how LTR labels end and how RTL labels
start, a Punycode label whose decoded form begins with "xn--" fails, and
ToUnicode decodes only labels that pass the ToASCII checks. The url
legacy test cases are regenerated with Node 24.22.
Corpus programs 5275-5279 cover the new behavior. 1640, 2283, 2599, and
3136 are updated where they pinned Node 24.15 output.
Typed and JavaScript classes can extend the global EventTarget and Event.
%EventTarget and %Event are runtime-provided hierarchy roots like the
EventEmitter: a subclass instance embeds a prefix pointing at the engine
state that super() creates (the target's listener core, the event's
state), and the backend's emitter prefix handling becomes a table of
runtime prefixes that covers all three bases.
The instances' checked-dynamic capsules use the runtime RC adapters, so
the engine recognizes them: a target instance is its listeners' `this`
and its events' target, and an Event subclass instance is the event its
listeners receive, with its own fields and methods. The inherited
methods and accessors lower directly on typed receivers (including
super.dispatchEvent) and answer through the class property dispatch and
the runtime's capsule hooks for checked-dynamic values, with Node's
util.inspect output, @@toStringTag and leak-warning class name. Members
read as values are the prototype's functions, also for plain handles.
A target keeps its own capsule in its state, so repeated dispatches reuse
it; the state and an event's target are traced, so the collector
reclaims targets whose listeners capture them. Subclass members that
spell an inherited member are refused, since the engine would never call
or read them.
Listener objects that are class instances now work: when a program can
register EventTarget listeners and declares a class with a handleEvent
method, %main installs a compiled `listener.handleEvent` reader that the
engine uses at every dispatch, so the class's own, inherited or
overridden method runs with the instance as `this`, as in Node. An
unannotated arrow in an object literal whose contextual type maps to a
checked-dynamic slot or to no lowered type (an EventTarget subclass's
listener union) no longer adopts the void callback slot's return type,
so `{ handleEvent: (e) => n++ }` passed as a listener keeps the result
its record field promises.