npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

readable-stream-builder

v1.0.0

Published

Simple utility for sequentially constructing a Readable stream from sync and async sources.

Readme

readable-stream-builder

Coverage npm version TypeScript Node Bun License: MIT

Basic utility to create a Readable stream from other strings or streams. Under the hood, this just uses an async generator function with a little bit of logic, and returns a Readable that can be piped into a Response (or whatever else).

Why?

When dealing with server-side rendering, stitching together the final HTML can often be awkward, ugly, and brittle. Many times it can lead to mixed async data fetching, string concatenation, and response-handling logic. That leads to several pain points:

  • Hard-to-follow control flow: data fetching patterns and string concatenation patterns tend to influence each other in bad ways.
  • Tight coupling to Response: many solutions write directly to the response, scattering I/O logic across rendering code.

readable-stream-builder exists to make composition of streamed HTML (or any streamed text) simple and explicit. It accepts a mixed list of sources — plain strings, Node Readable streams, promises that resolve to either a string or Readable stream. It also accepts factory functions (which can be sync or async) that ultimately resolve to a string or Readable stream.

These sources can be passed in during instantiation, added later with the push() method, or a mix of both.

The build() method returns a single Readable that you can feed to server Response. Behind the scenes, this method kicks off the chain of sources asyncronously, but resolves them in order to the Readable stream that was returned.

Benefits:

  • Compose synchronously and asynchronously without manual orchestration.
  • Keep most of your render code server and framework agnostic by interacting with the stream builder instead of the response.
  • Keep rendering logic declarative and local to components.
  • Stream content as it becomes available to reduce latency and memory usage.

Minimal example:

import { Readable } from 'stream';
import { createReadStream } from 'fs';
import { ReadablePromiseStreamBuilder } from 'readable-stream-builder';

// ...
const handler = () => {
    const parts = [
        '<!doctype html>',
        '<html>',
        '<head>',
        async () => {
            const title = await fetchTitle();

            return `<title>${title}</title>`
        },
        '</head>',
        '<body>',
        '<div id="root">',
        () => createReadStream('./dummy.txt', { encoding: 'utf8' }),
        '</div>',
        '</body>',
        '</html>'
    ];

    const builder = new ReadablePromiseStreamBuilder(parts);

    return new Response(
        builder.build(),
        { headers: { 'content-type': 'text/html; charset=utf-8' } }
    );
}

Installation

# pnpm
pnpm add readable-stream-builder

# bun
bun add readable-stream-builder

# npm
npm install readable-stream-builder

# yarn
yarn add readable-stream-builder

Usage

Import

import { ReadablePromiseStreamBuilder } from 'readable-stream-builder';

API

  • new ReadablePromiseStreamBuilder(sources?: StreamSource[]) — create a builder with an optional initial list of sources.
  • push(...sources: StreamSource[]) — add more sources after construction.
  • build() — returns a Node Readable that yields content from each source in order.

StreamSource may be any of:

  • string
  • Readable (Node stream)
  • Promise<string> or Promise<Readable>
  • a factory function that returns any of the above (can be async)

This makes it easy to mix static strings, async-rendered fragments, and file/stream content without manual orchestration.

Notes:

  • Use async factory functions to fetch data just-in-time; the builder will resolve them in order.
  • When including file streams (e.g., createReadStream()), the file's chunks are forwarded to the resulting Readable.
  • Keep fragments small and composable to get the most benefit from streaming (reduced latency, lower peak memory).

Examples

Simple strings, promises, non-blocking data fetch, and inline render call

// fetch-style handler (Bun / Cloudflare Workers / edge runtimes that support `Response`)
function handler() {
    // ... other routing logic hidden for simplicity

    // fetch title early, but don't wait here
    const titlePromise = fetchTitle();

    const parts = [
        '<!doctype html>',
        '<html>',
        titlePromise.then((title) => `<head><title>${title}</title></head>`), // non-blocking promise chain
        '<body>',
        renderAppHtml(), // sync or async render
        '</body>',
        '</html>'
    ];

    const builder = new ReadablePromiseStreamBuilder(parts);

    return new Response(builder.build(), { headers: { 'content-type': 'text/html; charset=utf-8' } });
}

Simple strings and lazy render with factory function

import { ReadablePromiseStreamBuilder } from 'readable-stream-builder';
// ... imports

// Node.js http server with stream piped to the response
createServer((req, res) => {
    // ... other routing logic hidden for simplicity

    const builder = new ReadablePromiseStreamBuilder();
    builder.push('<!doctype html><html><body>');
    builder.push(async () => {
        const { html } = await renderApp();

        return html;
    });
    builder.push('</body></html>');

    const stream = builder.build();

    res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
    stream.pipe(res);
}).listen(3000);

Basic HTML template with React renderToPipeableStream and suspense render

import { ReadablePromiseStreamBuilder } from 'readable-stream-builder';
// ... imports

// fetch-style handler (Bun / Cloudflare Workers / edge runtimes that support `Response`)
function handler() {
    // ... other routing logic hidden for simplicity

    const builder = new ReadablePromiseStreamBuilder();
    const [openingHtml, closingHtml] = htmlTemplate.split('<!--ssr-outlet-->');

    builder.push(openingHtml);

    // create a passthrough stream to receive the piped React stream
    const appStream = new PassThrough();

    builder.push(appStream, closingHtml);

    // renderToPipeableStream doesn't return an actual stream
    const { pipe } = renderToPipeableStream(<App />, {
        // onShellReady is called when rendering is complete for any content above the first Suspense boundary
        onShellReady() {
            pipe(appStream);
        },
        onShellError(error) {
            appStream.write('<div>gone fishing</div>');
            appStream.end();
        }
    });

    return new Response(builder.build(), { headers: { 'content-type': 'text/html; charset=utf-8' } });
};

Conditional stream with rendered head and lazy rendered body

import { ReadablePromiseStreamBuilder } from 'readable-stream-builder';
// ... imports

// Node.js http server with stream piped to the response
createServer((req, res) => {
    // ... other routing logic hidden for simplicity

    // don't block setup with async call
    const routeResult = verifyUserRequest(req);

    const builder = new ReadablePromiseStreamBuilder(['<!doctype html><html>']);
    builder.push(async () => {
        const { html } = await renderHead();

        return html;
    });
    builder.push(async () => {
        const { html } = await renderApp();

        return html;
    });
    builder.push('</html>');

    // block response until we know we are good to continue
    const { status } = await routeResult;

    if (status === 'OK') {
        // pre-checks are good, start the response
        res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
        builder.build().pipe(res);
    }
    else {
        // pre-checks failed, don't invoke the stream builder
        res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' });
        res.end();
    }
    
}).listen(3000);