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

streaming-utils

v0.1.0

Published

Streaming TransformStream utilities built on Web Standards. Base64, encryption, compression, hashing, and more -- without buffering into memory.

Readme

streaming-utils

Streaming TransformStream utilities built on Web Standards. Process data through pipelines -- base64, encryption, compression, hashing, and more -- without buffering into memory.

Works in Cloudflare Workers, Deno, Bun, Node.js, and browsers.

Install

npm install streaming-utils

Quick Start

Every utility is a factory function that returns a standard TransformStream. Compose them with the native .pipeThrough() API:

import { base64Encode, compress } from 'streaming-utils';

const encoded = readableStream
  .pipeThrough(compress('gzip'))
  .pipeThrough(base64Encode());

Or use the optional StreamUtils wrapper for a chainable API with convenient constructors and consumers:

import { StreamUtils, compress, base64Encode } from 'streaming-utils';

const result = await StreamUtils.fromResponse(response)
  .pipeThrough(compress('gzip'))
  .pipeThrough(base64Encode())
  .toReadableStream();

API

StreamUtils (optional wrapper)

A chainable wrapper around ReadableStream. Totally optional -- every transform works standalone with native streams.

import { StreamUtils } from 'streaming-utils';

// Constructors
StreamUtils.from(readableStream)
StreamUtils.fromString('hello')
StreamUtils.fromBytes(uint8Array)
StreamUtils.fromResponse(response)

// Chaining
stream.pipeThrough(transform)   // returns new StreamUtils
stream.pipeTo(writableStream)   // returns Promise<void>

// Consumers
await stream.toBytes()          // Uint8Array
await stream.toText()           // string (UTF-8)
stream.toReadableStream()       // ReadableStream
stream.toResponse(init?)        // Response
stream.tee()                    // [StreamUtils, StreamUtils]

Base64

import { base64Encode, base64Decode } from 'streaming-utils';

// Uint8Array → string (base64)
readableStream.pipeThrough(base64Encode());

// string (base64) → Uint8Array
base64Stream.pipeThrough(base64Decode());

Byte-level variants that keep the entire pipeline as Uint8Array:

import { base64EncodeBytes, base64DecodeBytes } from 'streaming-utils';

// Uint8Array → Uint8Array (UTF-8 encoded base64)
readableStream.pipeThrough(base64EncodeBytes());

// Uint8Array (UTF-8 encoded base64) → Uint8Array
readableStream.pipeThrough(base64DecodeBytes());

Encryption

AES-GCM (authenticated, buffered)

AES-GCM produces an auth tag over the full plaintext, so it buffers the entire stream before encrypting. The IV is automatically prepended to the output.

import { aesGcmEncrypt, aesGcmDecrypt } from 'streaming-utils';

const key = await crypto.subtle.generateKey(
  { name: 'AES-GCM', length: 256 },
  true,
  ['encrypt', 'decrypt'],
);

// Encrypt -- IV is prepended to output
const encrypted = readableStream.pipeThrough(aesGcmEncrypt({ key }));

// Decrypt -- reads IV from first 12 bytes
const decrypted = encryptedStream.pipeThrough(aesGcmDecrypt({ key }));

You can also pass a raw Uint8Array as the key, and optionally provide your own iv or additionalData.

AES-CTR (streaming)

AES-CTR encrypts chunk-by-chunk and handles 16-byte block alignment automatically. Suitable for large data.

import { aesCtrEncrypt, aesCtrDecrypt } from 'streaming-utils';

const key = await crypto.subtle.generateKey(
  { name: 'AES-CTR', length: 256 },
  true,
  ['encrypt', 'decrypt'],
);
const counter = crypto.getRandomValues(new Uint8Array(16));

const encrypted = readableStream.pipeThrough(aesCtrEncrypt({ key, counter }));
const decrypted = encryptedStream.pipeThrough(aesCtrDecrypt({ key, counter }));

Note: AES-CTR does not provide authentication. Pair with an HMAC if you need integrity.

Compression

Wraps the built-in CompressionStream/DecompressionStream APIs.

import { compress, decompress } from 'streaming-utils';

// gzip, deflate, or deflate-raw
const compressed = readableStream.pipeThrough(compress('gzip'));
const decompressed = compressedStream.pipeThrough(decompress('gzip'));

Hashing

Computes a digest over the entire stream using Web Crypto.

import { hash } from 'streaming-utils';

// SHA-256 as Uint8Array (default)
const digest = readableStream.pipeThrough(hash());

// SHA-256 as hex string
const hexDigest = readableStream.pipeThrough(hash({ output: 'hex' }));

// SHA-512
const sha512 = readableStream.pipeThrough(hash({ algorithm: 'SHA-512' }));

Supported algorithms: SHA-1, SHA-256, SHA-384, SHA-512.

Text

import { textEncode, textDecode } from 'streaming-utils';

// string → Uint8Array
stringStream.pipeThrough(textEncode());

// Uint8Array → string (handles multi-byte splits across chunks)
byteStream.pipeThrough(textDecode());

Hex

import { hexEncode, hexDecode } from 'streaming-utils';

// Uint8Array → string (lowercase hex)
readableStream.pipeThrough(hexEncode());

// string (hex) → Uint8Array
hexStream.pipeThrough(hexDecode());

NDJSON

import { ndjsonParse, ndjsonSerialize, textDecode } from 'streaming-utils';

// Parse newline-delimited JSON
const objects = byteStream
  .pipeThrough(textDecode())
  .pipeThrough(ndjsonParse());

// Serialize objects to NDJSON
const ndjson = objectStream.pipeThrough(ndjsonSerialize());

Chunking Utilities

import { fixedChunkSize, limitBytes, tap } from 'streaming-utils';

// Re-chunk into fixed-size blocks (last chunk may be smaller)
readableStream.pipeThrough(fixedChunkSize(65536));

// Truncate stream after N bytes
readableStream.pipeThrough(limitBytes(1024 * 1024));

// Observe bytes passing through without modifying them
readableStream.pipeThrough(
  tap((totalBytes) => console.log(`${totalBytes} bytes so far`))
);

Node.js Stream Interop

Separate entry point so non-Node runtimes never import it:

import { fromNodeReadable, toNodeReadable, fromNodeWritable } from 'streaming-utils/node';
import { createReadStream, createWriteStream } from 'fs';

// Node Readable → Web ReadableStream
const webStream = fromNodeReadable(createReadStream('input.txt'));

// Web ReadableStream → Node Readable
const nodeStream = toNodeReadable(webReadableStream);

// Node Writable → Web WritableStream
const webWritable = fromNodeWritable(createWriteStream('output.txt'));
await readableStream.pipeTo(webWritable);

Uses Readable.toWeb() / Readable.fromWeb() when available (Node 17+), with a manual fallback for older versions.

Composing Transforms

The power is in composition. Every transform is just a TransformStream, so you chain them with .pipeThrough():

import {
  StreamUtils,
  compress,
  base64EncodeBytes,
  aesCtrEncrypt,
  tap,
} from 'streaming-utils';

const key = /* ... */;
const counter = crypto.getRandomValues(new Uint8Array(16));

const result = await StreamUtils.fromResponse(await fetch('https://example.com/data'))
  .pipeThrough(tap((n) => console.log(`Downloaded ${n} bytes`)))
  .pipeThrough(compress('gzip'))
  .pipeThrough(aesCtrEncrypt({ key, counter }))
  .pipeThrough(base64EncodeBytes())
  .toBytes();

Runtime Support

Tested across four runtimes on every change:

| Runtime | Command | |---|---| | Node.js | npm run test:node | | Cloudflare Workers (workerd) | npm run test:workers | | Bun | npm run test:bun | | Deno | npm run test:deno | | All | npm run test:all |

License

MIT