mrkeyoor.com_
Wed 23 Sept 00:32 UTC
npmUtilsupdated 22 Sept 2026

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().

Verdict

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

Lab card: what happened when we installed cloneable-readableScreenshot of cloneable-readable documentation
Install✓ · 0.9s11 packages on disk · 2 MB
ImportESM import works · require() works · CommonJS package
Browser35.3 KBgzipped (117.6 KB minified), bundled with esbuild
Typesno TypeScript types found
Known vulns00 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.

API stability4/5The public surface in version 3.0.0 is still three pieces: call cloneable(stream), call clone() before flow, and test wrappers with isCloneable(). The v3 release changed readable-stream from major 3 to major 4 rather than redesigning those calls. Stability has a hard boundary, though: the code reads the source's private _readableState.objectMode field and deliberately throws `already started` when clone() is called after consumption begins.
Docs3/5The README gives one complete file-stream example, states the all-clones-must-flow rule in capital letters, confirms objectMode handling, and lists both public methods. It does not explain backpressure among branches, memory behavior, TypeScript setup, ESM interop, or the different destroy paths found in the source tests. The repository test file is the clearest reference for delayed consumers, errors, nested clones, and abandoned branches.
Maintenance3/5Version 3.0.0 was published on June 29, 2022, and its release notes mainly record the readable-stream 4 dependency update. GitHub reports a push on April 28, 2026, 120 stars, and 1 open issue or pull request; the repository is not archived. That recent repository activity is useful, but no newer npm release demonstrates that compatibility changes have reached users since the 2022 major.
Ecosystem3/5npm counted 3,273,369 downloads in the latest completed week, so cloneable-readable remains present in many dependency graphs. It works with Node Readable streams, accepts objectMode sources, and uses readable-stream 4 internally. Integration stops at that stream contract: there are no bundled TypeScript declarations, no exports map, no browser-oriented entry, and no adapters for Web Streams or observable libraries.

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.
Skip it if

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

PackageRegistryPick it when
readable-streamnpmUse it for the userland Node stream implementation itself when you can wire PassThrough branches and their lifecycle directly.
through2npmUse it when the job is transforming chunks through a small stream rather than duplicating one source.
clone-regexpnpmUse 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.