mrkeyoor.com_
Sat 08 Aug 22:49 UTC
npmUtilsupdated 08 Aug 2026

cloneable-readable

cloneable-readable wraps one Node.js Readable and lets several consumers receive the complete stream, including consumers attached on later event-loop turns. You create every clone before reading starts, then pipe, resume, or attach data/readable listeners to all branches. The wrapper waits until every declared branch is flowing, preserves object mode, and forwards source errors to the wrapper and clones. It solves a narrow fan-out problem for one-shot streams such as file uploads, request bodies, hashing, and duplicate file writes; it is not a general message bus or a replayable cache.

Verdict

A useful, focused fix when several known consumers must receive one Node Readable from byte zero, especially when attachment is slightly delayed. Do not install it for dynamic subscribers, independent object copies, or any design where one branch may remain idle.

API stability4/5The public surface is only cloneable(stream), stream.clone(), and cloneable.isCloneable(), and the README describes the same lifecycle that the version 3 source implements. The main compatibility event was version 3 moving its sole dependency to readable-stream 4 in 2022. The small surface reduces churn, but the reliance on readable-stream internals such as _readableState means runtime changes can still matter.
Docs3/5The README is short but unusually direct about the critical rule that every clone must start flowing, and it shows delayed piping plus object-mode support. The shipped tests add strong evidence for errors, destruction, cloning a clone, readable events, and asynchronous consumers. The public guide does not explain backpressure, shared object references, source ownership, TypeScript, or ESM, so safe production use requires reading code or tests.
Maintenance3/5Version 3.0.0 was published in June 2022, a real behavior test for asymmetric backpressure landed in September 2023, and a security policy followed in 2024. GitHub reports repository activity in April 2026 and only one open issue or PR, but there has been no npm release since 2022. This looks like a mature small package receiving occasional care, not an actively evolving project with frequent releases.
Ecosystem3/5The package recorded 3,199,590 downloads in the latest npm week and builds on readable-stream 4, so it sits inside the familiar Node stream ecosystem. It has 121 GitHub stars and external @types/cloneable-readable declarations exist. Integration breadth is still narrow: it is CommonJS-only, has no browser story, no framework adapters, and solves one specific fan-out lifecycle rather than general stream composition.

Use it if

  • One non-rewindable Node Readable must feed two or more consumers and every consumer needs every chunk from the beginning
  • Some branches attach a tick or timer later, so piping the source directly to multiple PassThrough streams would risk an early start
  • You need the same fan-out behavior for object-mode streams without configuring each branch by hand
  • You are maintaining CommonJS stream code already built around readable-stream and Node's pipe or pipeline APIs
Skip it if

Setup reality

Installation is one command, npm install cloneable-readable, with readable-stream 4 as its only runtime dependency. There are no native builds, credentials, environment variables, or config files. The setup trap is lifecycle order. Wrap the source, call clone() for every extra branch before any branch is piped, resumed, or given a data/readable listener, and then make every declared branch flow. The implementation counts the wrapper itself as a branch and does not pipe the original source until that count reaches zero. Forgetting one clone does not merely drop that output; it prevents all output. If a branch is no longer needed, destroy that clone so the other branches can proceed. Once flow has begun, clone() throws 'already started', so this is not late subscriber replay. Source errors are forwarded to the wrapper and clones, but destroying the wrapper or an individual clone does not destroy the original source; decide who owns source cleanup and prefer pipeline() for destination error handling. Object mode is copied from the source automatically, but chunk objects are shared references rather than deep clones. TypeScript users need separate declarations such as @types/cloneable-readable and may have to enable esModuleInterop or use require syntax. Version 3 is CommonJS and depends on readable-stream 4, so check that combination if the surrounding application standardizes on Node core streams or ESM-only modules.

Patterns

Send one file stream to two filesduplicate-file-read

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('copy-a.bin')),
  pipeline(copy, fs.createWriteStream('copy-b.bin')),
])

Create copy before either pipeline starts. Calling source.clone() after flow begins throws 'already started'.

Declare every branch before startingcreate-multiple-clones

const cloneable = require('cloneable-readable')

const shared = cloneable(incomingRequest)
const forParser = shared.clone()
const forArchive = shared.clone()

parser.consume(forParser)
archive.write(forArchive)
shared.resume()

The wrapper itself is also a branch. If it has no useful consumer, resume it to drain it after the clones are connected.

Attach a clone on a later tickdelay-one-consumer

const shared = cloneable(source)
const delayed = shared.clone()

shared.pipe(primarySink)

setImmediate(() => {
  delayed.pipe(secondarySink)
})

The original source waits for delayed because that clone was declared first. An undeclared late subscriber cannot be added after reading starts.

Destroy a branch that will not be useddiscard-unused-clone

const shared = cloneable(source)
const optional = shared.clone()

shared.pipe(requiredSink)

if (shouldSaveCopy) {
  optional.pipe(copySink)
} else {
  optional.destroy()
}

Leaving optional untouched stalls the source. Destroying one clone lets the remaining branches proceed and does not destroy the source.

Read one branch with data eventsconsume-data-events

const shared = cloneable(source)
const observer = shared.clone()
let bytes = 0

observer.on('data', (chunk) => {
  bytes += chunk.length
})
observer.on('end', () => console.log({ bytes }))
shared.pipe(destination)

Adding a data listener counts as starting that branch. Attach every clone's consumer before expecting the source to flow.

Pull chunks with the readable eventconsume-readable-events

const shared = cloneable(source)
const copy = shared.clone()

copy.on('readable', function () {
  let chunk
  while ((chunk = this.read()) !== null) {
    inspectChunk(chunk)
  }
})
shared.resume()

A readable listener also marks the clone as active. The main wrapper is resumed because this example does not otherwise consume it.

Clone an object-mode streamfan-out-object-mode

const { Readable } = require('node:stream')
const cloneable = require('cloneable-readable')

const shared = cloneable(Readable.from([
  { id: 1, state: 'new' },
  { id: 2, state: 'ready' },
], { objectMode: true }))
const auditBranch = shared.clone()

shared.on('data', processRecord)
auditBranch.on('data', auditRecord)

Object mode is detected automatically, but both branches receive the same object references. Do not mutate records in either consumer.

Create another branch from a cloneclone-from-clone

const shared = cloneable(source)
const first = shared.clone()
const second = first.clone()

shared.pipe(sinkA)
first.pipe(sinkB)
second.pipe(sinkC)

A clone delegates clone() to the same parent, so all three branches still share one start barrier and one source.

Wrap a stream only when neededavoid-double-wrapping

const cloneable = require('cloneable-readable')

function ensureCloneable(stream) {
  return cloneable.isCloneable(stream) ? stream : cloneable(stream)
}

const shared = ensureCloneable(input)
const copy = shared.clone()

isCloneable() recognizes both the main wrapper and clones created by it. It does not claim that an arbitrary stream can be replayed.

Hash while sending the same bytes elsewherehash-and-upload

const { createHash } = require('node:crypto')
const { pipeline } = require('node:stream/promises')
const cloneable = require('cloneable-readable')

const shared = cloneable(uploadBody)
const hashBranch = shared.clone()
const hash = createHash('sha256').setEncoding('hex')

await Promise.all([
  pipeline(shared, objectStoreSink),
  pipeline(hashBranch, hash),
])
console.log(hash.read())

The slower pipeline governs overall progress through backpressure, so hashing does not turn the upload body into an unbounded in-memory copy.

Observe a source failure on every branchhandle-source-error

const shared = cloneable(source)
const copy = shared.clone()

shared.on('error', (err) => report('primary', err))
copy.on('error', (err) => report('copy', err))

shared.pipe(sinkA)
copy.pipe(sinkB)

The implementation forwards a source error to the wrapper, which then reaches clones. Every unpiped stream still needs an error listener or pipeline owner.

Let pipeline clean up destinationsuse-promise-pipeline

const { pipeline } = require('node:stream/promises')
const cloneable = require('cloneable-readable')

const shared = cloneable(source)
const copy = shared.clone()

const results = await Promise.allSettled([
  pipeline(shared, transformA, sinkA),
  pipeline(copy, transformB, sinkB),
])
for (const result of results) {
  if (result.status === 'rejected') console.error(result.reason)
}

A destination failure on one branch needs an explicit policy for the other branch and original source; cloneable-readable does not own that application-level cleanup decision.

Alternatives

PackageRegistryPick it when
readable-streamnpmYou want the userland Node streams implementation and can build a simple fixed fan-out with PassThrough branches yourself
streamxnpmYou are willing to adopt a different stream implementation for tighter control over backpressure and stream internals
readable-stream-clonenpmYou need another small stream-cloning package and will evaluate its much smaller API and maintenance record yourself