peek-stream
peek-stream is a small CommonJS utility that buffers the first line of a Node stream, hands that sample to your callback, and then swaps in the Transform stream you choose. The sample is replayed into the selected parser, so downstream consumers still see the complete input. It is mainly useful for sniffing a line-oriented format such as CSV versus newline-delimited JSON without reading the whole source into memory. It uses duplexify and through2 to present one stable duplex stream while its actual parser is selected at runtime.
peek-stream solves one awkward classic Node-stream problem in very little code, and it remains reasonable inside an old pipeline that already depends on it. For new code, its 2018 release, undocumented option, callback-only API, and absent types make a small purpose-built Transform easier to own.
Use it if
- You consume a Node byte stream whose parser can be selected reliably from its first line
- You need to preserve streaming and backpressure instead of buffering an entire upload before format detection
- Your existing pipeline is CommonJS and already uses classic through2-style Node streams
- Parser selection may be asynchronous and fits the package's error-first swap callback
- You need active maintenance: version 1.1.3 was published in March 2018, the repository's latest code commit is from that release, and newer pushes only reflect unmerged pull requests
- Your format signature can occur after 65,535 bytes or spans multiple records; the default sample stops at the first line or at that fixed byte limit
- You need a custom record delimiter: the source recognizes only byte 10 as newline, optionally handling a preceding carriage return, and the newline option is still undocumented in an open issue
- You are building an ESM or TypeScript-first codebase: the package exposes one CommonJS file and ships no type declarations or export map
- The input is security-sensitive and an oversized first record should be rejected by default: strict is off, so reaching maxBuffer selects a parser instead of throwing unless you opt in
Setup reality
Install with npm install peek-stream and require it from CommonJS. There are no peer dependencies, native modules, credentials, or config files, but the package brings buffer-from, duplexify, and through2. You must also install every parser that your selector can return. The selector receives a Buffer containing the first line without its newline; it must call swap(error, parser), where parser is a readable and writable stream. peek-stream writes the sampled bytes and any overflow into that parser for you, so writing the sample yourself duplicates data. Selection can be asynchronous, but the incoming side remains pending until swap is called, and forgetting that callback stalls the pipeline. By default the library waits for LF, buffers at most 65,535 bytes, and then selects using the accumulated bytes even if no newline arrived. Set strict: true if a missing newline at the byte limit or at end-of-stream should be an error. A source-only newline: false option makes it select after maxBuffer rather than by line, but the README does not document it. Object-mode chunks bypass byte accumulation and cause selection immediately on the first non-string, non-Buffer value. Always attach an error handler or use stream.pipeline, because a selector error and a strict-mode framing error are emitted through the returned duplex. The package has no built-in format detection; ambiguous commas, byte-order marks, comments, and malformed first records are your responsibility.
Patterns
Choose between CSV and newline-delimited JSONselect-csv-or-json
const peek = require('peek-stream');
const csv = require('csv-parser');
const ldjson = require('ldjson-stream');
const parser = peek((sample, swap) => {
const line = sample.toString('utf8');
if (line.trimStart().startsWith('{')) return swap(null, ldjson());
if (line.includes(',')) return swap(null, csv());
swap(new Error('unsupported input format'));
});
source.pipe(parser).on('data', console.log);peek-stream does no detection itself. Order tests from most specific to least specific because a JSON string can also contain commas.
Propagate source, selector, and destination errorsuse-stream-pipeline
const { pipeline } = require('node:stream');
pipeline(source, parser, destination, (err) => {
if (err) console.error('parse failed:', err.message);
});pipeline is safer than a bare pipe chain because selector errors and downstream failures reach one completion callback.
Cap the bytes used for detectionlimit-first-line
const parser = peek({ maxBuffer: 8192 }, (sample, swap) => {
swap(null, chooseParser(sample));
});Without strict mode, reaching maxBuffer triggers selection with the bytes collected so far even when no newline was found.
Reject an oversized or unterminated first linereject-missing-newline
const parser = peek(
{ maxBuffer: 8192, strict: true },
(sample, swap) => swap(null, chooseParser(sample))
);strict also errors when the input ends before any newline, even if the buffered content is shorter than maxBuffer.
Inspect a fixed-size prefix instead of a linepeek-fixed-prefix
const parser = peek(
{ maxBuffer: 16, newline: false },
(prefix, swap) => swap(null, parserForMagicBytes(prefix))
);newline: false exists in version 1.1.3 source but is missing from the README. Selection waits until maxBuffer bytes or end-of-stream.
Choose a parser asynchronouslyselect-parser-async
const parser = peek((sample, swap) => {
lookupParser(sample).then(
(selected) => swap(null, selected),
(err) => swap(err)
);
});Input remains blocked until swap is called exactly once. Convert promise rejection to swap(err) or the pipeline can hang.
Fail closed on an unknown samplereject-unknown-format
const parser = peek((sample, swap) => {
const magic = sample.subarray(0, 4).toString('hex');
if (magic === '504b0304') return swap(null, unzipParser());
swap(new Error(`unsupported magic bytes: ${magic}`));
});Passing an error emits it from the returned duplex. Do not silently select a permissive fallback for untrusted formats.
Let peek-stream replay the inspected bytespreserve-sampled-data
const parser = peek((sample, swap) => {
console.log('detected from', sample.length, 'bytes');
swap(null, createParser());
// Do not call createParser().write(sample) here.
});The library writes sample and overflow to the selected parser after swap. Manually forwarding either chunk duplicates the beginning of the input.
Reject an empty stream explicitlyhandle-empty-input
const parser = peek((sample, swap) => {
if (sample.length === 0) return swap(new Error('empty input'));
swap(null, chooseParser(sample));
});With strict disabled, end-of-stream calls the selector with an empty Buffer. A detector should decide whether that is valid.
Select immediately from an object-mode chunkroute-object-stream
const parser = peek((firstObject, swap) => {
const selected = firstObject.type === 'event'
? eventTransform()
: fallbackTransform();
swap(null, selected);
});
objectSource.pipe(parser).pipe(objectDestination);Any first chunk that is neither a Buffer nor a string bypasses newline and byte-limit logic. The selected stream must support the same object-mode data.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| through2 | npm | You can implement the small amount of prefix buffering yourself and want direct control over stream behavior |
| streamx | npm | You want a maintained stream implementation for a new high-throughput pipeline and can write your own detector |
| readable-stream | npm | You need a userland copy of Node stream primitives with broad runtime compatibility rather than dynamic parser swapping |