Assertion failures throw an assert.AssertionError with Node 24.22's
generatedMessage, code, actual, expected, operator and diff properties,
in Node's key order. new AssertionError(options) validates its options
and builds Node's message from operator, actual and expected.
AssertionError type annotations, instanceof and assert.throws(fn,
AssertionError) compile.
Failure paths record actual, expected and operator before they throw;
passing assertions do no extra work. The deep forms lower through a
per-type function that returns the verdict, so the operands stay call
temporaries. assert.equal and notEqual inspect their operands only when
they fail.
Values without a checked-dynamic form, and values whose type can be
cyclic, record undefined.
scr_runtime.h now includes 48 module headers (scr_core.h, scr_rc.h,
scr_string.h, scr_dyn.h, scr_http.h, ...) in dependency order and holds no
declarations. Each module header holds whole sections of the former file,
includes the headers whose names it uses, and compiles on its own.
Declarations, macros, struct layouts and inline bodies are unchanged. Five
blocks move to join their module: the class-hierarchy vtable header (into
scr_error.h, after the string core), the node:timers/promises pair (into
scr_async.h), the JSON.parse entry points and the JSON output buffer (into
scr_json.h, after the dyn declarations) and the EventTarget block (after
scr_json.h). No moved block uses or declares a name that a block it moves
past uses or declares. The string methods keep their position after the
class objects in scr_string_methods.h, because their struct ScrDyn
parameters rely on the file-scope struct ScrDyn declaration there.
The runtime ABI scan reads scr_runtime.h with its module headers expanded
in include order. A runtime test compiles every module header on its own.
scripts/runtime-header-hunks.mjs retargets a diff of the single-file
header onto the module headers.
The single-pass ASCII comparison now reads both strings a word at a time
when the covered ASCII bytes are TAB..CR and 0x20..0x7E, as the generated
table has them. A word of bytes 0x20..0x7E needs no per-byte coverage
check, equal words have equal primaries, and the rest of both strings is
validated by words after the first differing primary. A non-ASCII byte
answers at once that the strings need the general path; a word with a
tab, line break or another control goes to the per-byte pass. Runs
shorter than a word are read as the eight bytes ending at their last
byte, which stay inside the string object because the header precedes
the data.
The byte-equality check moves after the primary pass on the ASCII path,
where it only runs when every primary ties, and the scan and element-list
path moves out of line, so the ASCII path no longer sets up the element
buffers' stack frame.
scr_exc_alert, the process-wide exception alert of worker executables on
targets without the ELF per-thread pending word, was a tentative
definition. That branch compiles only for Mach-O, COFF and WASI, so the
macOS runtime build emitted it as a COMMON symbol, which library
localization keeps global, and the COMMON-symbol test failed on macOS
while passing on Linux.
Initialize it explicitly, and have the test also compile every runtime
source for aarch64-macos through zig cc when zig is on PATH and the host
is not macOS, so declarations in __APPLE__ branches are checked on Linux
too.
crypto.createSecretKey and generateKey/generateKeySync for "hmac" and
"aes" return SecretKeyObject values, native dyn handles over a copy of
the key bytes. The type and symmetricKeySize getters, export() as a
Buffer or a JWK, equals(), instanceof KeyObject, String(),
JSON.stringify and util.inspect follow Node 24.22, as do the argument
checks, their order and their errors. createHmac and hkdf/hkdfSync
accept these keys. toCryptoKey() and KeyObject.from() need WebCrypto
values and are refused with SC2020.
Dyn handle ops gain a @@toStringTag that differs from the class name
and a flag for views with no properties, which util.inspect prints at
any depth. KeyObject method calls dispatch through dynInvoke.
The localeCompare table now covers U+0363-U+036F, U+00BC-U+00BE and
U+2044 FRACTION SLASH, so strings containing them follow Node's collation
instead of code-point order. A combining Latin letter is its letter's
primary at the letter's secondary, with a tertiary weight between the
letter's case forms where Node places it, and the combining class it
shares with the diacritics it reorders with. A vulgar fraction is the
elements of its compatibility decomposition (1, U+2044, 4), each with the
tertiary weight Node places the fraction at among that position's forms,
so it orders after both 1⁄4 and ¹⁄4. The generator's check adds the
fractions' slash and superscript spellings and the combining letters
around each diacritic. SC6003 and the limitations page describe the new
coverage.
The generated lookup indexes one dense table for every code point below
U+0370, with the gaps between ranges marked uncovered, and checks the
ranges above it (now U+2044) after that, so a Latin lookup is one
comparison instead of up to four.
crypto.timingSafeEqual accepts every TypedArray, DataView, Buffer,
ArrayBuffer and SharedArrayBuffer and compares their bytes. Values of
other types throw Node 24.22's ERR_INVALID_ARG_TYPE for buf1 and then
buf2, before the byte lengths are compared. Views of every element kind
keep the typed call; other arguments take the checks at run time.
randomFillSync accepts every TypedArray, DataView, Buffer, ArrayBuffer and
SharedArrayBuffer and returns its argument at its own type. Offsets and
sizes count elements and are checked in bytes with Node 24.22's
ERR_OUT_OF_RANGE and ERR_INVALID_ARG_TYPE errors. randomFill fills the
same buffers and calls back in a later macrotask, or synchronously when
nothing is filled, as Node does.
randomInt takes a callback in both forms, and its errors carry Node's
received values and the max or "max - min" label. Values come from a
6 KiB cache, as in Node, so a callback runs on the next tick while the
cache has data and after an asynchronous refill when it is empty.
randomUUID and the new randomUUIDv7 validate options.disableEntropyCache
and draw from a 128-UUID batch unless it is set.
A non-u8 view passed to randomFillSync no longer needs a Uint8Array
type, and the bytes callback adapters pass views of any element kind
and dyn values through.
scr_lib.c calls the digest functions in scr_hash.c since #1006, and sort.test.ts copied array.test.ts's source list from before that change, so the test binary failed to link (undefined scr_ripemd160_init and others). The list now matches array.test.ts's.
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.
The gallop searches in the comparator sort held their search bounds in
long, which is 32 bits on Windows x64, so a run of more than 2^31 slots
gave a truncated bound and the merges could copy outside the array. The
bounds are now ptrdiff_t; every other index, length, offset, hint, run
length and run-stack entry in the sort was already size_t. Each gallop
result is checked against its run length (0 <= k <= length) before any
merge uses it, and a result outside the run is an internal error rather
than a copy.
A new runtime unit test calls gallop and the merge step, through test-only
entry points built with SCR_SORT_TEST, at indices past 2^32 and over runs
of more than 2^32 slots in a sparse mapping, and checks that an
out-of-range gallop result traps.
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.