cloneable-readable review
cloneable-readable 3.0.0 wraps one Node Readable in a PassThrough that can feed the same chunks to several consumers. It delays the original source until the wrapper and every clone have been piped, resumed, or given a data or readable listener. That makes it useful when one non-replayable upload or file stream must reach two destinations without reading the source twice. Version 3 moved its sole runtime dependency to readable-stream 4; it did not add a new public API. The package remains CommonJS, has no declarations, and exposes only the wrapper function, clone(), and isCloneable().
cloneable-readable 3.0.0 installed in 0.9 seconds and left 11 packages and 2 MB in our sandbox, but its browser bundle reached 117.6 KB minified. Install it for a Node-only fan-out whose consumers are all known before reading starts; choose buffering or a replayable source when branches can arrive late or retry independently.
We installed it
| Install | ✓ · 0.9s | 11 packages on disk · 2 MB |
| Import | ✓ | ESM import works · require() works · CommonJS package |
| Browser | 35.3 KB | gzipped (117.6 KB minified), bundled with esbuild |
| Types | — | no TypeScript types found |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does cloneable-readable install cleanly?
Yes. In a fresh container with an empty cache, npm install cloneable-readable finished in 0.9s, leaving 11 packages and 2 MB on disk. npm audit reported no known vulnerabilities.
How much does cloneable-readable add to a browser bundle?
35.3 KB gzipped (117.6 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.
Does cloneable-readable work with both ESM and CommonJS?
Yes. Both import 'cloneable-readable' and require('cloneable-readable') worked in Node 22 in our run. The package is published as CommonJS.
Does cloneable-readable include TypeScript types?
No type declarations were found in our install, so TypeScript users need their own declarations.
cloneable-readable or readable-stream: which should you use?
readable-stream: Use it for the userland Node stream implementation itself when you can wire PassThrough branches and their lifecycle directly. cloneable-readable 3.0.0 installed in 0.9 seconds and left 11 packages and 2 MB in our sandbox, but its browser bundle reached 117.6 KB minified.
When should you not use cloneable-readable?
A consumer may join after reading has started. The implementation clears its original-source reference on first flow, and a later clone() call throws already started.
Use it if
- One incoming Readable must feed two or more consumers, and every consumer can be registered before bytes start moving.
- A file, upload, or generated stream cannot be reopened, buffered in full, or requested from its origin a second time.
- Object-mode chunks must be copied to parallel Node stream pipelines while preserving the source's objectMode setting.
- The codebase already uses classic Node streams and can manage backpressure, error listeners, and branch cleanup explicitly.
- A consumer may join after reading has started. The implementation clears its original-source reference on first flow, and a later clone() call throws `already started`.
- One branch may never be consumed. The README says every clone must be piped or resumed before flow starts, so an abandoned branch can leave the whole fan-out waiting.
- You need replay, seeking, or independent retry. cloneable-readable forwards one live sequence and keeps no durable copy that a failed consumer can request again.
- The data belongs in browser code. Our bundle reached 117.6 KB minified and 35.3 KB gzipped because the package brings the readable-stream compatibility layer.
- Your TypeScript policy requires package-owned declarations. Version 3.0.0 ships no TypeScript types, and its small API does not compensate for a strict no-stub rule.
Setup reality
We installed cloneable-readable 3.0.0 in a fresh Node 22 Bookworm sandbox. npm finished in 0.9 seconds, left 11 packages using 2 MB, and reported 0 known vulnerabilities. The package itself is 56 KB unpacked with 1 direct dependency and no peers. Both require() and ESM import worked, although the published entry is CommonJS without an exports map and no TypeScript declarations are included.
There are no credentials or config files. Wrap the untouched Readable first, create every needed clone, and then connect each branch. The wrapper copies objectMode from the source. Options passed to the wrapper configure its PassThrough, but the README documents no queue, replay store, or per-branch retry facility.
Startup is coordinated across branches. A clone counts as ready when it is piped, resumed, destroyed, closed, or receives a data or readable listener. If one created clone is forgotten, the source waits. Once all branches are ready, the original stream is piped and the wrapper discards the reference used to make further clones; clone() after that point throws already started.
Each output is still a live Node stream. A source error is forwarded to the wrapper and clones, while destroying one clone leaves its siblings and source alone. Our browser build produced 117.6 KB minified and 35.3 KB gzipped, so frontend fan-out is better handled with browser-native streams or an application-specific buffer.
Patterns
Write one file stream twice clone-file-stream
const fs = require('node:fs')
const { pipeline } = require('node:stream/promises')
const cloneable = require('cloneable-readable')
const source = cloneable(fs.createReadStream('input.bin'))
const copy = source.clone()
await Promise.all([
pipeline(source, fs.createWriteStream('a.bin')),
pipeline(copy, fs.createWriteStream('b.bin')),
])Create the clone before either pipeline begins. Calling clone() after source starts flowing throws `already started`.
Prepare three branches before flow fan-out-three-ways
const shared = cloneable(input)
const auditBranch = shared.clone()
const archiveBranch = shared.clone()
await Promise.all([
pipeline(shared, primarySink),
pipeline(auditBranch, auditSink),
pipeline(archiveBranch, archiveSink),
])All 3 branches must be piped, resumed, destroyed, or listened to before the original source starts.
Duplicate an object-mode source consume-object-mode
const { Readable } = require('node:stream')
const input = Readable.from([{ id: 1 }, { id: 2 }], { objectMode: true })
const rows = cloneable(input)
const copy = rows.clone()
rows.on('data', saveRow)
copy.on('data', indexRow)Version 3 copies objectMode from the wrapped stream's readable state, so objects are not converted to buffers.
Read a clone in paused mode consume-with-readable-event
const shared = cloneable(input)
const copy = shared.clone()
copy.on('readable', () => {
let chunk
while ((chunk = copy.read()) !== null) inspect(chunk)
})
shared.resume()A readable listener marks that clone as ready. The original wrapper also needs a consumer here, so resume() drains it.
Destroy a branch you no longer need discard-unused-clone
const shared = cloneable(input)
const optional = shared.clone()
if (shouldInspect) optional.pipe(inspector)
else optional.destroy()
shared.pipe(destination)Destroying one clone releases its startup count and does not destroy the source, wrapper, or sibling clones.
Observe errors on every branch handle-source-error
const shared = cloneable(input)
const copy = shared.clone()
shared.on('error', reportPrimaryFailure)
copy.on('error', reportCopyFailure)
shared.pipe(primarySink)
copy.pipe(copySink)The implementation forwards a source error into the wrapper, which then reaches its clones. Add error handling to each consumed stream or use pipeline().
Avoid wrapping an existing clone check-before-wrapping
function ensureCloneable(stream) {
return cloneable.isCloneable(stream) ? stream : cloneable(stream)
}
const shared = ensureCloneable(input)
const copy = shared.clone()isCloneable() returns true for both the main wrapper and clones made from it.
Create another branch from a clone clone-a-clone
const shared = cloneable(input)
const first = shared.clone()
const second = first.clone()
shared.pipe(a)
first.pipe(b)
second.pipe(c)A clone delegates clone() to its parent. Make the full branch set before any branch starts reading.
Load the CommonJS package from ESM use-esm-import
import cloneable from 'cloneable-readable'
import { createReadStream, createWriteStream } from 'node:fs'
const shared = cloneable(createReadStream('input.txt'))
shared.pipe(createWriteStream('output.txt'))ESM import worked in our Node 22 check, but version 3 has no exports map and publishes a CommonJS entry.
Attach a known consumer on a later tick delay-one-consumer
const shared = cloneable(input)
const delayed = shared.clone()
shared.pipe(firstSink)
setImmediate(() => delayed.pipe(secondSink))The source waits for delayed because that clone already exists. A consumer cannot create a new clone after shared has begun flowing.
Hash while writing the same bytes hash-and-store
const { createHash } = require('node:crypto')
const shared = cloneable(input)
const hashBranch = shared.clone()
const hash = createHash('sha256').setEncoding('hex')
await Promise.all([
pipeline(shared, fileSink),
pipeline(hashBranch, hash),
])
console.log(hash.read())Both consumers see the same chunk sequence. A slow destination still participates in normal Node stream backpressure.
Use only a clone and drain the wrapper drain-original-branch
const shared = cloneable(input)
const copy = shared.clone()
copy.pipe(destination)
shared.resume()The wrapper itself counts as a branch. If its output is unwanted, resume() drains it so the clone can start.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| readable-stream | npm | Use it for the userland Node stream implementation itself when you can wire PassThrough branches and their lifecycle directly. |
| through2 | npm | Use it when the job is transforming chunks through a small stream rather than duplicating one source. |
| clone-regexp | npm | Use it only for copying RegExp objects; it is a verified package with a similar name but solves an unrelated cloning problem. |
More utils guides
lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

