Streams
Two stream worlds live here. The WHATWG classes (ReadableStream/WritableStream/TransformStream, plus Compression/Decompression) run on the promise/microtask runtime alone — no fibers or libcurl for in-memory pipelines. Node's stream module (Readable/Writable/Duplex/Transform/PassThrough, pipeline/finished, duplexPair) is Node v24's own implementation ported to TypeScript and compiled with the program, so its event order follows Node's. The recurring divergences: a synchronously-throwing pull/start/write of a web stream propagates to the caller instead of erroring the stream, an array/tuple-chunk closed-stream read still yields a zero-shaped value rather than undefined, an omitted error argument to a Node stream callback arrives as null rather than undefined, and fs/http hand out their own stream handles rather than instances of the Node classes.
ReadableStream
Works, with caveatsA pull/push source with backpressure: enqueue via a controller in start/pull, consume with for await, .getReader().read(), or by piping. ReadableStream.from() builds one from an array. Runs on the promise/microtask runtime alone — no fibers or libcurl for in-memory pipelines.
ReturnsReadableStream<T> — A readable stream.
let n = 0
const numbers = new ReadableStream<number>({
pull: (controller) => {
n = n + 1
if (n > 5) { controller.close() } else { controller.enqueue(n * n) }
}
}, { highWaterMark: 1 })
for await (const square of numbers) { console.log("square:", square) }- No BYOB readers / byte controllers (
type: 'bytes'rejected at compile time) — byte data flows asUint8Arraychunks through default readers ReadableStream.from()accepts arrays only, not async iterables- Source callbacks must be arrow/function-expression/method-shorthand literals; a cancel callback can't take a reason parameter
read()on a closed stream whose chunk type is an **array/tuple** still yields the chunk's zero-shaped value, notundefined(an array slot has no spare absent state); a scalar/string/object chunk reads a realundefined, andcontroller.desiredSizeisnullonce erroredreleaseLock()leaves queued pending reads pending instead of rejecting themvalues()returns a reader usable infor awaitonly (no standalone.next())- In the no-fiber tier,
awaitof an already-settledread()skips the microtask drain, so a pending pull can run one tick later than real JS - A synchronously-throwing pull/start propagates to the caller instead of erroring the stream
tee()forwards the last cancelling branch's reason instead of composing both; a signal abort ofpipeTotakes effect at the next chunk boundary
WritableStream
Works, with caveatsA backpressured sink: .write() queues a chunk (each write carries its own promise), .close()/.abort() finish it, and ready re-arms on backpressure flips. The sink's write/close/abort callbacks drive delivery.
ReturnsWritableStream<T> — A writable stream.
const printed: string[] = []
const sink = new WritableStream<string>({
write: (chunk) => { printed.push(chunk) }
})
await source.pipeThrough(upper).pipeTo(sink)releaseLock()doesn't reject a pending-write view- A synchronously-throwing sink write propagates instead of erroring the stream (an async rejection is handled per spec)
- A sink abort callback can't take a reason parameter; no transformer/sink
startpromise-vs-write interlock beyond the started flag
TransformStream
Works, with caveatsA writable-in / readable-out pair whose transform callback maps chunks from one side to the other, running when the readable side has capacity (else parking one chunk). Compose it with pipeThrough. Writable HWM defaults to 1, readable HWM to 0.
ReturnsTransformStream<I, O> — A transform stream.
const upper = new TransformStream<string, string>({
transform: (chunk, controller) => { controller.enqueue(chunk.toUpperCase()) }
})
await source.pipeThrough(upper).pipeTo(sink)- No transformer
startcallback - Cancelling the readable side doesn't error the writable side
- An identity transform (no callback) requires matching I/O chunk representations
CompressionStream / DecompressionStream
Works, with caveatsTransformStreams backed by native zlib: gzip/deflate/deflate-raw compress and decompress a Uint8Array chunk stream. Inflate auto-detects gzip/zlib wrappers, and -lz is linked only when one of these is actually constructed.
ReturnsTransformStream — A zlib-backed transform stream.
const [rawBranch, decodeBranch] = source.pipeThrough(new CompressionStream("gzip")).tee()
for await (const chunk of decodeBranch.pipeThrough(new DecompressionStream("gzip"))) {
out = out + dec.decode(chunk)
}- The format argument must be a string literal ("gzip", "deflate", "deflate-raw") — a runtime-computed format string is a compile error
- Chunks are
Uint8Arrayonly (no ArrayBuffer/DataView BufferSource variants)
Blob.stream()
Works, with caveatsReturns a ReadableStream<Uint8Array> over the blob's bytes — one owned-copy chunk then closed (an empty blob yields no chunk). Consumable via for await or .getReader().
ReturnsReadableStream<Uint8Array> — A readable stream of the blob's bytes.
- Delivers the blob's bytes as a **single**
Uint8Arraychunk (the whole blob), not incrementally chunked as a real byte source would be
stream.Readable / .Writable / .Duplex / .Transform / .PassThrough
Works, with caveatsNode's stream classes, ported from Node v24's own implementation to TypeScript and compiled with the program: the same states, event order and process.nextTick deferrals. The options form binds this to the stream, subclasses override _read/_write/_transform/_final/_destroy, chunks are Buffers (or strings with setEncoding, or any value in object mode), and Readable.from/.toWeb/.fromWeb bridge to iterables, async iterables and web streams. The readable operators (map with concurrency, filter, flatMap, drop, take, forEach, toArray, some, every, find, reduce), the signal option and addAbortSignal, Duplex.from and compose are Node's own.
ReturnsReadable | Writable | Transform | PassThrough — A Node stream.
class Greeter extends Readable {
words: string[] = ["kalimera", "kosme", "apo", "thessaloniki"]
i: number = 0
constructor() { super({ objectMode: true }) }
_read() {
if (this.i >= this.words.length) { this.push(null) }
else { this.push(this.words[this.i]); this.i = this.i + 1 }
}
}- An omitted error argument reaches a callback declared
(err?: Error | null)asnull; Node passesundefined - The fs streams and the HTTP server's
req/resare separate runtime handles, not instances of these classes Readable.toWebhas no pull backpressure;Duplex.toWeb/fromWebare missing- Not ported:
Readable.wrap,isReadable/isErrored/setDefaultHighWaterMark - A method replaced on an instance (
this._read = fn) is not supported; pass it in the options
stream.pipeline() / .finished()
ExactThe callback forms: pipeline(a, ...b, cb) pipes each stream into the next and calls cb once the last has finished, or with the first error, destroying every stream; a stage may be an async generator function, the source any iterable, and the last stage may return a promise, with { signal, end } options; finished(s, [options,] cb) calls cb when the stream has ended, finished and closed, and returns a function removing its listeners. Their promise twins live in stream/promises.
ReturnsStream | () => void — pipeline returns the last stream; finished returns its cleanup function.
finished(echo, (err) => { console.log("echo finished, clean:", !err) })
pipeline(src, new PassThrough(), sink, (err) => { console.log("pipeline done", err ? err.message : "ok") })stream/promises
ExactThe promise-returning pipeline and finished, over the callback forms: pipeline resolves once the last stream has finished, finished once the stream has ended and closed; either rejects with the stream's error.
ReturnsPromise<void> — Resolves on completion, rejects on error.
import { pipeline, finished } from 'stream/promises'
await pipeline(numbers, square, sink)
await finished(words)stream.duplexPair()
ExactTwo Duplex instances wired to each other: what one side writes, the other reads. A write completes when the peer reads it, and end() finishes once the peer has ended, as Node's DuplexSide does. Returned as a two-element array.
Returns[Duplex, Duplex] — A pair of cross-wired Duplex streams.
const [clientSide, serverSide] = duplexPair()
serverSide.on("data", (d) => { console.log("server saw:", d.toString()) })
clientSide.end("hello over the pair")