NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
npm · #3446 most downloaded on npm
Chain functions, generators, Node streams, and Web streams into a pipeline with backpressure support.
Last release 8 days ago
26 Sep 2026
Ships unpredictably
gaps range from 8 days to 1.1 years
Some releases are documented
notes for 13 of 40 stable releases
Nothing withdrawn
no release was ever pulled
8 years old
40 releases · first in 2018
Patch release. Three bug fixes. No API changes.
Patch release. Three bug fixes. No API changes.
fun() keeps the outputs of overlapping calls apart. A composed fun() pipeline held one output buffer for all its calls, so calling it again before an asynchronous call settled mixed the two calls' values, and one of them could fail with a TypeError or resolve to null . Each call now collects into its own buffer, the way gen() always did. Reported by oss-security-shopify ; the report was handled as a bug, not a security advisory, since both calls come from the same application. Regression tests in tests/core/test-fun-direct.js .
lines() handles a CRLF split across chunks. When \r ended one chunk and \n started the next, the emitted line kept a trailing \r ; the same text in one chunk did not. The JSONL parser was unaffected ( JSON.parse ignores the whitespace). First direct tests for lines() in tests/core/test-lines.js .
asyncBlockWriter refuses every call after a failed write. After a failed block write, final write, or close, the next call used to reopen the file with 'w' , truncating what had been written, and carry on without an error. Every later call now throws an Error whose cause is the original failure, and the file is kept as it was. New tests in tests/node/test-asyncBlockWriter.js .
fixUtf8Stream() 's input contract is documented: one stream carries either bytes or strings, not both. Its unused internal buffer was removed, with no change in behavior.
The JSONL splitter was audited for rescanning on every chunk and measured linear on Node and Bun, 1 to 16 MB in a single line; bench/jsonl-long-line.js stays as the regression meter.
Dev dependencies bumped ( @types/node , prettier , tape-six ).
One column per quarter.
Patch release. Plugs a file-descriptor leak in asyncBlockWriter on its own write failure. No API changes.
Patch release. Plugs a file-descriptor leak in asyncBlockWriter on its own write failure. No API changes.
Patch release. Fixes a regression introduced in 4.2.3: a failed pipe() could throw a spurious AggregateError carrying the same error twice. No API cha
Patch release. Fixes a regression introduced in 4.2.3: a failed pipe() could throw a spurious AggregateError carrying the same error twice. No API changes.
The flush no longer runs on stop , a throw, or an early break . Finalizing a pipeline that didn't complete is wrong (it re-runs failed work and emits partial tails); the flush is reserved for clean completion. Resource-owning source stages still release inline (a generator source gets it.return() from the executor's abort path, unchanged since 4.2.2). A resource-owning sink that needs release on abort guards its own code with try / catch , or the caller does so in a catch / finally — the exception always propagates intact, never suppressed, so it stays actionable.
The genuine cleanup-double-fault AggregateError in asyncBlockReader / asyncBlockWriter / exec (an I/O op and a separate close() both failing) is unchanged — those combine two genuinely distinct failures and never re-run the work that threw.
Patch release. Resource-cleanup fixes in the functional ( pipe / gen ) path and the asyncBlock* helpers, plus richer error reporting when cleanup itse
Patch release. Resource-cleanup fixes in the functional ( pipe / gen ) path and the asyncBlock* helpers, plus richer error reporting when cleanup itself fails. No API changes.
pipe() flushes in a finally . A stop , throw, or early break in the data pass previously skipped the end-of-input flush, so a flushable sink's final() (e.g. asyncBlockWriter / stringerToFile closing its FileHandle ) never ran — a confirmed file-handle leak (measured +1 fd). The flush now always runs.
asyncBlockWriter / asyncBlockReader always release their handle. A failed final write no longer skips the writer's close() ; a throwing close() in the reader no longer masks the body error. exec.abort no longer silently drops the source generator's cleanup error.
Cleanup double-faults become an AggregateError . When an operation and its cleanup both throw, the two are reported together — .errors in chronological [first, second] order — instead of one masking the other. Wrapping happens only when the primary is a real Error , so gen() 's non- Error cancel sentinel keeps its identity. Single failures still propagate unwrapped (a bare Stop is still a Stop ).
stop / finalValue are documented as supported from generators ( none / many stay pointless there — a generator expresses both natively via yield / yield* ). The rule: issue stop / finalValue , then return . Stream pipelines absorb a stop as a clean end; functional pipelines propagate it (catch for cleanup).
The finalValue auto-exit-generator alternative was benchmarked and rejected (a consistent ~2–3% regression on the generator-stage hot path); the documented rule above is the contract.
Patch release. A correctness fix in the shared executor — source generators are now released when a pipeline ends abnormally, closing a file-handle le
Patch release. A correctness fix in the shared executor — source generators are now released when a pipeline ends abnormally, closing a file-handle leak on file-backed pipelines — plus removal of dead, undocumented runtime exports.
Dead, undocumented runtime exports. None were typed (all absent from the .d.ts surface) or reachable through a documented entry point:
The legacy gen.next async-generator trampoline — gen() has been a push→pull bridge over the shared executor since 4.1.0; the trampoline was retained "for compatibility" but had no callers.
The fun.next / fun.collect / fun.asArray attachments and their named exports — fun.js now exports {fun} only. The collect / asArray functions remain (used internally).
The exec.next / exec.flush property attachments on the internal exec factory — the named next / flush exports (consumed by the four compositors) are unchanged.
…option is accepted, so stream-json 's deprecated JSONL users can migrate by changing only the import specifier. See jsonl § Factory-bundled entries fo…
Patch release. Factory-bundled JSONL entry points — the ergonomic adapter surface stream-json 's JSONL needs to delegate here — plus a bugfix removing checkedParse() , which 4.2.0 exposed by mistake.
Factory-bundled JSONL entries under stream-chain/node/jsonl/ and stream-chain/web/jsonl/ . Each is one factory that carries the substrate adapters as methods, so jsonlParser.asStream(options) / jsonlParser.asWebStream(options) (and the stringer equivalents) work without separately importing the suffixed parserStream / parserWebStream modules.
stream-chain/node/jsonl/parser.js — jsonlParser() returns the gen() chain; .asStream (Node Duplex ) and .asWebStream (Web pair) attached.
stream-chain/web/jsonl/parser.js — browser-safe; .asWebStream only (never pulls in node:stream ).
stream-chain/node/jsonl/stringer.js — jsonlStringer() returns a Node Transform ; .asStream is the factory itself, .asWebStream returns a Web TransformStream .
stream-chain/web/jsonl/stringer.js — browser-safe; returns a Web TransformStream , .asWebStream is itself.
Two curated barrels, stream-chain/node/jsonl and stream-chain/web/jsonl , export {jsonlParser, jsonlStringer} for the substrate (new exports keys back them).
The option/item type names match stream-json 's — JsonlParserOptions , JsonlItem , JsonlStringerOptions — and a no-op checkErrors? option is accepted, so stream-json 's deprecated JSONL users can migrate by changing only the import specifier. See jsonl § Factory-bundled entries for the full surface and a migration table.
The four existing JSONL adapter modules now also export their option types ( ParserOptions , ParserWebStreamOptions , StringerOptions , StringerWebStreamOptions ), previously declared but un-exported.
The bundled entries delegate to the existing adapter modules; no parsing/serialization logic is duplicated. The Web-flavored entries are covered by the browser-safety test (no node:* imports in their transitive graph).
Internal: the per-line JSONL parser was refactored to select its error-handling closure once up front (function-form vs constant errorIndicator , ignoreErrors , default) rather than re-checking per line — behavior is unchanged.
These entries exist primarily to unblock stream-json 's JSONL delegation: with the adapter surface matched, stream-json can repoint its JSONL re-exports here and eventually delete its own JSONL surface as an import-only swap.
Everything else is additive; the only borderline-breaking change is the /core chain no longer iterating a string input character-by-character.
Minor release. New JSONL file-edge components for local-file pipelines, a richer JSONL error-handling API absorbed from stream-json , and a /core chain string-source pass-through fix. Everything else is additive; the only borderline-breaking change is the /core chain no longer iterating a string input character-by-character.
JSONL file-edge composites under stream-chain/jsonl/file/ (Node-only).
parseFile(options) — returns a gen() pipeline shaped as (path) => AsyncGenerator<{key, value}> . Internally composes asyncBlockReader with the standard JSONL parser ; forwards all parser options.
stringerToFile(path, options) — returns a gen() pipeline shaped as (value) => AsyncGenerator<never> . Terminal sink that composes the function-pipeline stringer with asyncBlockWriter ; the writer's flushable final() closes the file handle on flush.
Drop straight into chain([...]) (the chain factory wraps them into one fused asStream internally), or drive directly with pipe(...) + drain(...) for the substrate-free gen path. See jsonl § File-edge composites for the full story and bench/jsonl-file.js for the round-trip numbers.
JSONL errorIndicator API on parser({...}) , parserStream , and parserWebStream . Presence-checked ( 'errorIndicator' in options ), so errorIndicator: undefined is meaningful and distinct from "not set":
errorIndicator: undefined — drop bad lines without bumping the line counter (sequential keys).
errorIndicator: null (or any constant) — emit the value in place of the failed line, counter bumps.
errorIndicator: (error, input, reviver) => unknown — function form; return value replaces the line, undefined return drops without bumping.
The legacy ignoreErrors: true shortcut is preserved unchanged for backwards compatibility (drops but the counter bumps every line → gappy keys). errorIndicator wins when both are set.
Raw jsonlParser(options?) — per-line factory without the fixUtf8Stream -> lines input front. For callers whose chunks already arrive line-aligned. Parallels stream-json's jsonParser raw export.
Standalone checkedParse(input, reviver?, errorIndicator?) — single-line parser. Behaves exactly like JSON.parse(input, reviver) unless errorIndicator is passed, in which case parse failures invoke the function form (or return the constant) instead of throwing. arguments.length < 3 is what distinguishes "omitted-indicator throws" from "explicit undefined catches".
Function-pipeline JSONL stringer at src/jsonl/stringer.js . Flushable; canonical building block alongside the existing Transform / TransformStream variants. Same options as stringerStream .
Supporting helpers in src/utils/ :
pipe(...stages) — one-shot single-value driver for a gen pipeline. Runs g(value) then g(none) so flushable sinks close. Substrate-free.
drain(asyncIter) — awaits an async iterable, returns its last yielded value. Standard way to await a pipe(...)(value) whose output you don't otherwise want.
asyncBlockReader(options?) — Node-only (path) => AsyncGenerator<string> over fs/promises.open + StringDecoder('utf8') .
asyncBlockWriter(path, options?) — Node-only flushable that buffers and writes fixed-size blocks via fileHandle.write , closing the handle on flush.
/core chain string-source iteration footgun. chain([...])('input.json') previously walked 'i','n','p',... into the first stage because strings carry a Symbol.iterator and /core 's outer for await (const v of input) yield* g(v) happily iterated them. The driver now passes strings through as a single value, and the same check carves out other non-iterables (numbers, booleans, plain objects, …) so they don't throw on for await either. Arrays, generators, async iterables, Map , Set still iterate as before; null / undefined still yield empty. Borderline-breaking — anyone relying on per-character iteration of a string source loses it (the explicit replacement is [...'hello'] ). Surfaced 2026-05-28 wiring stream-json's parseFile .
Empty JSONL lines now dropped silently across all parser variants. Was: JSON.parse('') threw without ignoreErrors set. This is part of the errorIndicator work and applies uniformly to parser / parserStream / parserWebStream regardless of which error-handling option (if any) is set.
Bench. bench/jsonl-file.js (50k JSONL rows fixture, nano-bench driven) compares the new file-edge components against the equivalent fs.createReadStream + parserStream + ... + stringerStream + fs.createWriteStream arrangement. Round-trip is ~40% faster; parse-with-in-pipeline-work is ~10% faster; parse-and-drain-externally is ~20% slower (the per-token gen async-bridge cost — same lesson as the sink-placement principle stream-json saw in its 2026-05-28 file-edge bench). See the bench header for which shape applies to which workload.
stream-json proxy unblocked. The errorIndicator / checkedParse API absorption makes stream-json's src/core/jsonl/parser.js a candidate to collapse to a proxy re-export of stream-chain's parser plus a couple of aliases. The stream-json side ships when its peerdep is bumped to ^4.2.0 (separate session).
Helpers duplicated for now. pipe , drain , asyncBlockReader , and asyncBlockWriter are intentionally mirrored between stream-chain and stream-json so each package stays self-contained. A future stream-json cleanup will collapse its copies into re-exports from stream-chain.
Docs lead. README gained a new "JSONL" section with a chain() -based round-trip example, wiki/jsonl.md grew a "File-edge composites" section, wiki/utils.md grew "Driving gen pipelines" and "Block I/O (Node-only)" sections, wiki/benchmarks.md was rewritten to match the actual bench/ contents, and wiki/Home.md index links the new helpers and components.
Patch release. A performance release: the four compositors now share one sync-when-possible executor, and a memory-growth bug on backpressured many()
Patch release. A performance release: the four compositors now share one sync-when-possible executor, and a memory-growth bug on backpressured many() expansion is fixed. No public API change and no behavior change — the full test suite passes identically before and after.
Unified exec engine ( src/exec.js ). gen() , fun() , asStream() , and asWebStream() previously each ran their own fused-pipeline executor; the Node and Web wrappers used an async function applyFns that returned a Promise on every call and await ed between every push. They now share a single sync-when-possible, value-or-promise executor : it threads a value through the function-list and emits via a push callback, staying fully synchronous until the first real promise (an async stage, a thenable value, or a backpressuring push) appears — then it suspends and resumes. Synchronous pipelines no longer pay a per-item microtask. exec is internal (not exported).
gen() is now a push→pull bridge over the shared executor rather than a recursive async function* ; the legacy trampoline is retained as gen.next for compatibility but is no longer used internally.
The bounded-queue guarantee is unchanged: the executor honors the push return value, so when an enqueue backpressures it suspends at that push and the readable queue stays at hwm + 1 regardless of how many values one input produces.
Executor-layer microbenchmarks (synthetic, nano-bench): asStream ≈ 26% faster, asWebStream ≈ 10% faster than the old applyFns path; exec is the fastest of the in-process fun / gen / exec trio.
End-to-end against stream-json via npm link : real createReadStream file pipelines 20–26% faster (json-filter, json-transform, json-count); a chunk-size sweep that previously blew up on whole-document input is now flat across chunk sizes.
Minor release. Web Streams parity across the utility surface. Every Node-substrate primitive now has a Web counterpart with matching contract; substra
Minor release. Web Streams parity across the utility surface. Every Node-substrate primitive now has a Web counterpart with matching contract; substrate-agnostic helpers ( dataSource , stream type guards) are exposed on every subpath.
readableWebStreamFrom(iterable | options) in src/utils/readableWebStreamFrom.js . Web Streams counterpart to readableFrom() . Accepts a plain iterable, async iterable, iterator, 0-ary function (sync or async), or {iterable, strategy?} options object. Returns a ReadableStream . Per-item backpressure via desiredSize / pull() . Speaks the chain protocol on the producer side (returned none / null / undefined is skipped, stop terminates, many([...]) fans out, finalValue(v) is unwrapped). For the plain iterable case the platform's ReadableStream.from() is enough; reach for readableWebStreamFrom when you need function-source, promise-resolution, or chain-protocol awareness.
reduceWebStream(reducer, initial) / reduceWebStream(options) in src/utils/reduceWebStream.js . Web Streams counterpart to reduceStream() . Returns {writable, result, accumulator} — write into writable , await result for the final accumulator (resolves on clean close, rejects on abort or reducer error), or read the running value any time via the accumulator getter. Reducer is called with this bound to the return object so this.accumulator works inside (parity with reduceStream ).
parserWebStream(options?) in src/jsonl/parserWebStream.js . Web Streams counterpart to parserStream . One-liner over asWebStream(parser({...})) . Same reviver / ignoreErrors contract. Emitted records are {key, value} where key is the zero-based input line index.
stringerWebStream(options?) in src/jsonl/stringerWebStream.js . Web Streams counterpart to stringerStream . Implemented as a TransformStream because JSON.stringify is synchronous — the substrate's flush() callback handles the suffix/ emptyValue emit, and there's no value in routing terminal text-emission through applyFns 's per-chunk Promise allocation (stringer sits downstream of user code, not between intermediate function stages).
dataSource(fn) on /web and /core . Moved to src/dataSource.js (substrate-agnostic). Re-exported from all three subpath entries ( stream-chain , stream-chain/web , stream-chain/core ) and attached as chain.dataSource on each.
Node stream type guards in defs.js . isReadableNodeStream , isWritableNodeStream , isDuplexNodeStream joined the Web guards. Shape-based (no node:stream import), so callable from any substrate without pulling Node Streams into a browser bundle. Closes the symmetry with the Web guards (which had been exported since 4.0.0).
Modernization sweep: options && options.X → options?.X across the source tree (20 sites, 10 files). For paired fallback chains, switched || to ?? only after per-site analysis confirmed the left-side type was object-or-undefined ( QueuingStrategy , reviver-or-function). For initial-accumulator presence checks ( 'initial' in options ), kept the explicit in operator — ?? would coalesce explicit null accumulators to the default, losing the semantic distinction.
reduceStream / reduceWebStream cleanup. Dropped a band-aid 'initial' in options && options.initial !== undefined check that worked around a normalization clobber bug. The wrap step now only writes initial into options when it was actually passed positionally, and a clean 'initial' in options presence check stands on its own. Both substrates aligned.
Stream type guards consolidated in defs.js . Node guards moved from inline definitions in src/index.js into defs.js alongside the Web guards. src/index.js now destructures them from the defs namespace like the Web guards.
dataSource extracted to its own module. Was inline in src/index.js . Now src/dataSource.js + .d.ts , parallel to gen.js / fun.js .
Wiki Node↔Web mapping table added to wiki/utils.md and wiki/jsonl.md headers. wiki/defs.md gained a "Stream type guards" section covering both substrates.
Test count: 322 tests / ~620 asserts on Node (up from 282 / 562 in 4.0.2). Browser bucket: 176 tests (up from 137). New suites:
tests/web/test-readableWebStreamFrom.js and tests/web/test-readableWebStreamFrom-lifecycle.js — smoke, protocol features, backpressure, cancel-halts-pump, error propagation.
tests/web/test-reduceWebStream.js — positional + options forms, sync + async reducers, abort, reducer-throws, this.accumulator Node-parity, live accumulator getter.
tests/web/test-jsonl-parserWebStream.js — smoke, chunked multi-byte boundaries, bad-JSON propagation, ignoreErrors drops bad lines, reviver, roundtrip via stringerWebStream.
tests/web/test-jsonl-stringerWebStream.js and tests/web/test-jsonl-stringerWebStream-lifecycle.js — smoke, custom-separator edges, abort, cancel, replacer-throws, empty-stream-flush.
tests/core/test-dataSource.js — substrate-agnostic dataSource behavior, per-subpath re-export.
tests/node/test-jsonl-parserStream.js — added ignoreErrors test that pins the silently-drops-bad-lines contract on the Node side.
All four runtimes green: Node 26 / Bun 1.3.14 / Deno 2.7 / headless Chromium via tape-six-playwright . ts-check + js-check + Prettier clean.
Patch release. Restores fixUtf8Stream() portability across runtimes that don't expose node:string_decoder .
Patch release. Restores fixUtf8Stream() portability across runtimes that don't expose node:string_decoder .
fixUtf8Stream() no longer breaks browser consumers. 4.0.1's import {StringDecoder} from 'node:string_decoder' at module top crashed every browser ESM consumer of a fixUtf8Stream user on load — before any test or code ran. The bare node: specifier fails resolution in browsers, with no try/catch surface above.
Fix: src/utils/fixUtf8Stream.js now defaults to a TextDecoder -backed factory (WHATWG standard, available in every supported runtime) and, on Node specifically, asynchronously upgrades to StringDecoder via import('node:string_decoder').then(...) . Bun and Deno stay on TextDecoder per benchmark (Bun: roughly tied; Deno's TextDecoder actually beats its node-compat StringDecoder ). Runtime detection uses the same idiom as tape-six 's utils/timer.js .
No top-level await — tests/node/test-cjs.cjs (require-of-ESM interop) stays green; an earlier TLA shape broke it across Node, Bun, and Deno during exploration.
Decision data: bench/decoder-stringdecoder-vs-textdecoder.js (kept in-repo). Node 26: StringDecoder 2–4× faster on the decoder hot path; Bun 1.3.14: wash; Deno 2.7: TextDecoder beats StringDecoder via node-compat.
src/utils/fixUtf8Stream.d.ts now types the chunk parameter as string | Uint8Array | typeof none (was string | Buffer | typeof none ). On Node, Buffer extends Uint8Array , so existing Node consumers still type-check. The /// <reference types="node" /> directive is gone — the file no longer depends on Node-only types.
Fix: import {StringDecoder} from 'node:string_decoder' . ESM hygiene anyway — the bare specifier has been deprecated in favor of node: prefixes since…
Patch release. Two cross-runtime compatibility fixes surfaced during a multi-runtime verification pass (Node 26, Bun 1.3.14, Deno 2.7, headless Chromium via tape-six-playwright ).
asWebStream() crash on Bun ≤1.3.14. src/asWebStream.js listens to controller.signal inside the WritableStream sink's start(controller) callback to wake a backpressured write when the writer aborts — without it, an aborted writer with a pending drain would deadlock. The signal is per WHATWG Streams §4.5.2 and works on Node 26 and Deno 2.7, but Bun 1.3.14's WritableStreamDefaultController returns undefined for .signal , causing every asWebStream instantiation on Bun to throw TypeError: undefined is not an object (evaluating 'controller.signal.addEventListener') .
Fix: optional-chained the listener ( controller.signal?.addEventListener(...) ). Bun loses the abort-wakeup safety net until upstream lands the fix — a consumer that aborts a writer with a pending backpressured write would deadlock, rare in practice. Normal write/close/cancel paths are unaffected.
Upstream fix in flight: oven-sh/bun#31156 / PR #31157 . The PR cites stream-chain v4 by name as the motivating downstream case and includes a regression test labeled "unblocks a pending write() from an abort listener — the stream-chain v4 use case." When the PR ships in Bun 1.3.15+, the optional chaining becomes a no-op and the abort-wakeup safety net is restored across all four runtimes.
fixUtf8Stream() bare string_decoder import. src/utils/fixUtf8Stream.js did import {StringDecoder} from 'string_decoder' . Node and Bun resolve this to the built-in fine, but Deno requires the explicit node: prefix for any built-in module import ( TypeError: Import "string_decoder" not a dependency ). Fix: import {StringDecoder} from 'node:string_decoder' . ESM hygiene anyway — the bare specifier has been deprecated in favor of node: prefixes since Node 16.
The test directory is now organized by execution environment, mirroring tape-six 's configuration schema :
tests/core/ — substrate-agnostic tests (use the runChain(transducers, input) helper from tests/web-helpers.js , which drives a /web chain internally). Runs in browser AND CLI.
tests/web/ — Web Streams substrate. Runs in browser AND CLI.
tests/node/ — Node Streams substrate. Runs only in CLI.
Configured via tape6.tests (core + web) and tape6.cli (node) in package.json . The split lets the same test set drive Node 26, Bun 1.3.14, Deno 2.7, and headless Chromium without per-runtime patterns in the npm scripts.
npm run test:browser drives headless Chromium through tape-six-playwright , auto-starting tape6-server on port 55555 (env-overridable to avoid the default 3000 collision). Runs the browser-safe subset: 113 tests / 166 asserts covering asWebStream , /web chain, webStreamPuller , and defs in a real browser engine.
Two Bun-specific feature-probe skips were added in this release — tests/web/test-asWebStream-lifecycle.js and tests/web/test-webStreamPuller.js each detect the relevant Bun spec gaps at test start and t.skipTest(...) rather than failing. They auto-unskip when Bun ships the matching fixes, reported as skipped: 2 in npm run test:bun output for visibility.
All four runtimes green:
Runtime tests / asserts Status
Node 26 258 / 534 all pass
Bun 1.3.14 258 / 532 all pass (2 skipped via feature-probe)
Deno 2.7 258 / 534 all pass
Browser (Chromium) 113 / 166 all pass
Also covers Ubuntu 26.04: Playwright's manifest stops at ubuntu24.04, so the npm install postinstall fails to download Chromium on fresh 26.04. Workaround documented in AGENTS.md : npm install --ignore-scripts then PLAYWRIGHT_HOST_PLATFORM_OVERRIDE=ubuntu24.04-x64 npx playwright install chromium . Install-time only; runtime needs no env.
Major release. Substantial substrate and API expansion; small but breaking adjustments for CJS callers and existing consumers. See the dedicated Migra
Major release. Substantial substrate and API expansion; small but breaking adjustments for CJS callers and existing consumers. See the dedicated Migration guide for a step-by-step upgrade walkthrough.
ESM-only distribution. The package is "type": "module" . The 3.x fallback that exposed chain as a callable from require('stream-chain') is gone — CJS callers now destructure: const {chain} = require('stream-chain') . Was the only required source change for most callers.
Node 22+ floor. Drops Node 16, 18, 20. Current supported majors are 22, 24, 26 (latest minor of each).
Per-item backpressure semantic shift. When a function returns many(...) or a generator yields multiple values, the runtime now awaits the drain signal between every push. Queue stays at hwm + 1 regardless of expansion factor. Cost: ~40% throughput hit on synchronous-heavy pipelines (the applyFns path is now async end-to-end). Benefit: bounded memory under unbounded expansion. No source change needed.
Generators-yield-plain convention. Generators (sync function* and async async function* ) must yield plain values only — not none , stop , many(...) , or finalValue(...) . 3.x was inconsistent about handling these from generator output; 4.x is explicit that the behavior is undefined.
The default stream-chain entry resolves to stream-chain/node (the canonical Node Streams runtime). Two new substrate-specific subpaths:
stream-chain/web — a native Web Streams chain. Returns {readable, writable} pairs built on ReadableStream / WritableStream . No node:stream interop layer — browser-safe and works in any environment with the Web Streams substrate.
stream-chain/core — substrate-free composition. Returns a callable async-iterable factory: (input?) => AsyncGenerator<R> . Useful for gen() / fun() composition without dragging in either substrate.
Existing 3.x code keeps working without source changes — the split is purely additive.
Web Streams counterpart to asStream() . Returns a native {readable, writable} duplex pair (NOT a TransformStream — its transform() callback can't suspend mid-call for per-item drain). Per-item backpressure via a custom pull() callback that wakes a pending-drain Promise. Options accept Web Streams' standard QueuingStrategy shape: {strategy?, readableStrategy?, writableStrategy?} . Lifecycle matches TransformStream : reader.cancel(reason) propagates to the writable as an error; writer.abort(reason) errors both sides and unblocks pending backpressure; user-function errors propagate to both sides. See asWebStream() .
isReadableWebStream , isWritableWebStream , isDuplexWebStream consolidated into stream-chain/defs.js as the single canonical source. Re-exported from stream-chain (and chain.X ) and stream-chain/web .
makeStreamPuller(readable) wraps a Node Readable as a non-destructive async iterator (thin facade over readable.iterator({destroyOnReturn: false}) ). Preserves the original 'error' value (no AbortError wrapping), synthesizes Error('Premature close') on destroy-without-end, and breaks out of for await without destroying the source.
makeWebStreamPuller(readable) wraps a Web ReadableStream similarly (built on stream[Symbol.asyncIterator]({preventCancel: true}) ) plus a cancel(reason) extension method — the iterator-protocol return() can't carry a cancel reason cleanly.
Both intended for downstream consumers (stream-join, stream-sorting) that need original-error preservation. Both implement [Symbol.asyncIterator] directly so for await works without ceremony. Live at stream-chain/utils/streamPuller.js and stream-chain/utils/webStreamPuller.js . See makeStreamPuller() and makeWebStreamPuller() .
Five distinct issues found and fixed during the post-merge cleanup pass. Each ships with regression tests in tests/test-asStream-lifecycle.js and tests/test-asWebStream-lifecycle.js (257 / 534 asserts total, up from 237 / 513 in 3.6.3):
asWebStream writer.abort() deadlock when a sink write was awaiting backpressure. Spec serializes sink callbacks, so the abort sink couldn't fire to unblock the pending drain. Fix: listen to controller.signal in start() (the signal aborts before sink callbacks settle).
asWebStream reader.cancel() left writes silently failing. Should match TransformStream: propagate the cancel reason to the writable as an error so the producer learns. As a side effect, mid-chain errors in /web chains now reach the producer.
asWebStream double-close on the readable controller after cancel. Idempotent closeReadable / errorReadable helpers introduced.
asWebStream user-function errors weren't propagating to the readable. Downstream pipeTo 'd stages would hang waiting for input. Fix: errorReadable(error) in write / close sink catch blocks before rethrowing.
asStream stream.destroy() deadlock while in-flight _write was awaiting paused . Node doesn't signal in-flight writes either. Fix: custom destroy(err, callback) sink in the Duplex options that calls resume() .
isNodeStream defensive helper in src/index.js removed — no longer needed once web-stream guards drop the !isNodeStream check (Node and Web streams have non-overlapping method sets).
asStream.js and asWebStream.js simplified: dropped the inline "is plain value" fast-path (sync-when-possible processValue covers it), dropped duplicated stop-handler boilerplate, hoisted helpers ( unblockDrain , closeReadable , errorReadable , absorbStop ). Net: asStream.js 271 → 248 lines, asWebStream.js 302 → 282 lines.
Many -array iteration in the slow path no longer allocates an array iterator — walks the values directly with promise-chaining only when needed.
Tests renamed from .mjs / .mts to .js / .ts (the package is now "type": "module" , so the explicit extensions are redundant).
New lifecycle test files: tests/test-asStream-lifecycle.js , tests/test-asWebStream-lifecycle.js , tests/test-streamPuller.js , tests/test-webStreamPuller.js .
New wiki pages: asWebStream , makeStreamPuller , makeWebStreamPuller , Migration-V3-to-V4 .
Existing pages updated for 4.x: Home (4.x banner, subpath section), chain() , asStream() , defs (generator-yields-plain convention), highWaterMark , utils , fun() (rewritten with memory-caveat + history rationale).
AI-facing docs ( AGENTS.md , ARCHITECTURE.md , llms.txt , llms-full.txt ) fully rewritten for 4.x.
Special values in generators (don't yield none / many(...) ; stop / finalValue(...) supported) — documented in wiki/defs.md § Special values in generators .
If a module declares export default X , it must also declare export {X} for the same value. Fleet-wide rule, documented as topics/esm-default-export-with-named-mirror and slice 17 of the fleet-conventions-bundle .
TypeScript inference fixes for nested and cross-type pipe compositions. Pure type-system patch — no runtime changes. Two distinct bugs found via stric
TypeScript inference fixes for nested and cross-type pipe compositions. Pure type-system patch — no runtime changes. Two distinct bugs found via strict probes of the existing 3.6.2 typings:
Ret<ChainOutput<W, R>> and Arg0<ChainOutput<W, R>> were collapsing to any because ChainOutput<W, R> = Omit<Duplex, ...> & ChainOutputExtensions<R> is structurally assignable to Duplex , so the broad F extends Readable | Transform | Duplex ? any branch in both Arg0 and Ret matched before any chain-specific check. W was also not structurally present (the alias parameter was unused in the body).
Fix: mirror the TypedDuplex phantom pattern. Added __streamTypeW(): W and __streamTypeR(): R phantom methods to ChainOutputExtensions (now parameterized by both W and R ); added F extends ChainOutput<infer W, any> / F extends ChainOutput<any, infer R> as the first branch in Arg0 and Ret so chain-of-chain composition recovers both type parameters.
fun(...fns) returns (arg) => Many<R> | Promise<Many<R>> . When that function appeared inside another fun , gen , or chain , the outer's Ret ran OutputType<F> over the union — and UnpackReturnType checked ReturnType<F> extends Promise<unknown> against the whole union. The check failed on the Many<R> arm, the unwrap fell through unchanged, and the inner Promise<Many<R>> leaked downstream as R | Promise<Many<R>> . For fun-of-fun the pollution compounded to Many<R | Promise<Many<R>>> | Promise<Many<R | Promise<Many<R>>>> .
Fix: rewrite UnpackReturnType as a thin shim over a naked-T inner alias that distributes over union members and recurses on Promise<infer U> . Same form ( AsyncGenerator -> Generator -> Promise-recurse -> T ) handles Promise<Promise<X>> and the union arms uniformly.
Every pipe-like API ( chain , fun , gen , asStream ) is now exercised in every structurally-permitted position (first / middle / last) of every other pipe-like API. New tests cover:
Nested-position guards: chain([fn, X, fn]) , fun(fn, X, fn) , gen(fn, X, fn) for X in {fun, gen, asStream, nested chain} .
First-position guards: chain output as first item, fun(fun-out, fn) , gen(gen-out, fn) , plus the cross-type pairings.
Threading verification: a deliberately-mismatched-middle repro confirmed TypeScript pinpoints the mismatch on the middle item via the ChainList / FnList Ret<F1, I> walk.
Typings test count: 56 → 68 ( tests/test-typings-chain.mts + tests/test-typings-gen-fun.mts ).
Tarball now includes AGENTS.md and ARCHITECTURE.md alongside the existing llms.txt / llms-full.txt per the tarball AI-docs convention — useful for AI tools in downstream projects discovering the package via npm install .
@types/node ^25.6.0 -> ^25.7.0 , nano-benchmark ^1.0.15 -> ^1.0.16 , tape-six ^1.8.0 -> ^1.9.0 , tape-six-proc ^1.2.8 -> ^1.2.9 .
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Your coding agent can read these notes before it upgrades. Set up the MCP server →