MCPcopy Index your code
hub / github.com/WinterTC55/iter-streams

github.com/WinterTC55/iter-streams @main

Chat with this repo
repository ↗ · DeepWiki ↗ · + Follow
1,060 symbols 2,615 edges 81 files 246 documented · 23%
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

Redesigning the Web Streams API

Date: January 2026

This is a prototype of an alternative streams API and implementation designed to address fundamental ergonomics and performance flaws in the Web Streams API model.

This is a work in progress and should not be considered a final design. The API and implementation are evolving as we explore design tradeoffs and gather feedback.

DO NOT USE THIS API IN PRODUCTION — it is not stable and may change significantly.

Why

The WHATWG Streams Standard[^1] ("Web streams") provides a foundation for streaming data on the web platform, but suffers from significant usability issues stemming from its design predating async iteration and from attempting to serve too many use cases with a single complex abstraction. This document critiques Web Streams and presents design principles for a simpler, iterable-based alternative.

A complete reference implementation is available in this repository demonstrating these principles. See DESIGN.md for the API design and API.md for the complete API reference.

Table of Contents

  1. Design Principles for Improvement
  2. Detailed Design and Reference Implementation
  3. Benchmarks

1. Design Principles for Improvement

The new streams API follows these principles:

P1: Streams Are Just Iterables

No custom Stream class with hidden state. Streams are AsyncIterable<Uint8Array[]> or Iterable<Uint8Array[]> - standard JavaScript iteration protocols. The for await...of syntax is the idiomatic way to consume a stream.

// Streams are standard async iterables
for await (const chunks of readable) {
  for (const chunk of chunks) {
    process(chunk);
  }
}

P2: Transforms Are Pull-Through

Transforms only execute when the consumer pulls. No eager evaluation, no hidden buffering. Data flows on-demand from source through transforms to consumer.

// Nothing executes until iteration begins
const output = Stream.pull(source, compress, encrypt);

// Transforms execute as we iterate
for await (const chunks of output) { /* ... */ }

P3: Explicit Backpressure

Strict by default, reject on overflow. All buffering has explicit limits with configurable overflow policies:

  • 'strict' (default) - Writes reject when buffer full
  • 'block' - Writes wait until space available
  • 'drop-oldest' - Drop oldest buffered chunks to make room
  • 'drop-newest' - Drop incoming chunks when buffer full
const { writer, readable } = Stream.push({
  highWaterMark: 10,
  backpressure: 'drop-oldest'
});

Pull-streams implement inherent backpressure by only producing data when the consumer requests it.

Push-streams enforce strictly following backpressure policies on writes by default.

P4: Batched Chunks

Iterables yield Uint8Array[] (arrays of chunks) to amortize async overhead. This batching reduces promise creation per-chunk:

for await (const chunks of readable) {  // chunks is Uint8Array[]
  for (const chunk of chunks) {         // chunk is Uint8Array
    process(chunk);
  }
}

Writers accept Uint8Array[] for batched/vectorized writes:

await writer.writev([chunk1, chunk2, chunk3]); // Write multiple chunks at once

P5: Explicit Multi-Consumer

No built-in tee() with hidden unbounded buffers. Instead, explicit multi-consumer patterns:

  • Stream.broadcast() - Push model with writer pushing to all consumers
  • Stream.share() - Pull model with shared source pulled on demand

Both require explicit buffer limits and backpressure policies:

// Share with explicit buffer management
const shared = Stream.share(source, {
  highWaterMark: 100,
  backpressure: 'strict'
});
const consumer1 = shared.pull();
const consumer2 = shared.pull(decompress);

For both models, a single cursor-based queue manages data flow to multiple consumers, ensuring predictable memory usage.

P6: Clean Sync/Async Separation

Complete parallel sync versions for CPU-bound workloads. No ambiguity about which path executes:

Async Sync
Stream.pull() Stream.pullSync()
Stream.pipeTo() Stream.pipeToSync()
Stream.bytes() Stream.bytesSync()
Stream.text() Stream.textSync()
Stream.share() Stream.shareSync()

The design allows for a synchronous fast path when all components are synchronous, eliminating unnecessary promise overhead.

P7: Non-Negative desiredSize

Unlike Web Streams, desiredSize is always >= 0 or null (closed). The API enforces strict backpressure semantics - no negative values indicating "over capacity."

P8: Bytes Only

The API deals exclusively with bytes (Uint8Array). Strings are UTF-8 encoded automatically. No "value streams" - use async iterables directly for streaming arbitrary JS values.

P9: Chunk-Oriented, Not Byte-Oriented

Operations work on chunks (contiguous byte sequences) rather than individual bytes. No BYOB (bring your own buffer), no min/max read sizes, no partial fill handling.

Approach Overhead Complexity
Byte-oriented Per-byte processing, frequent allocations High (BYOB requests, partial fills)
Chunk-oriented Per-chunk processing, batch allocations Low (simple iteration over chunks)

P10: No Transfer/Detach Semantics

The API does not automatically transfer or detach buffers. Use ArrayBuffer.transfer() explicitly when needed. This keeps the API simple and predictable while allowing developers to opt into transfer semantics when performance requires it.

P11: No forced/hidden promise chains

Implementers may optimize away promise chains when operations complete synchronously. The API does not mandate promise creation in all cases, allowing for efficient synchronous fast paths.


2. Detailed Design and Reference Implementation

A complete reference implementation is available in this repository demonstrating the API design principles above. The implementation is tested (194 tests) and benchmarked against Web Streams.

Documentation

Document Description
DESIGN.md Comprehensive API design document covering push streams, pull pipelines, transforms, consumers, multi-consumer patterns, and protocol extensibility
API.md Complete API reference for the reference implementation
USAGE.md Guide to using the API with code examples
REQUIREMENTS.md Detailed list of testable assertions
MIGRATION.md Guide for migrating from Web Streams API to this API
MIGRATION-NODEJS.md Guide for migrating from Node.js streams to this API
DESIGN-TRADEOFFS.md Discussion of design tradeoffs and rationale
COMPLETENESS-ANALYSIS.md Analysis of feature completeness
TRANSFER-INTEGRATION.md Design discussion: Transfer Protocol integration for ownership semantics

Code

Resource Description
src/ TypeScript source code (types.ts, push.ts, from.ts, pull.ts, consumers.ts, broadcast.ts, share.ts)
samples/ Sample files demonstrating API usage patterns
benchmarks/ Benchmark suites comparing performance with Web Streams

Installing from npm

npm install new-streams

Running the Reference Implementation

# Install dependencies
npm install

# Build
npm run build

# Run tests (194 tests)
npm test

# Run samples
npx tsx samples/01-basic-creation.ts

# Run benchmarks
npm run benchmark

# Run html samples/benchmark server
npm run html-samples

3. Benchmarks

Note that these numbers are illustrative and will vary by environment and implementation status. They are provided here to demonstrate the performance characteristics of the new streams API compared to Web Streams and Node.js streams and should not be taken as definitive.

No assertion is made that these benchmarks represent real-world workloads. There is no intention to claim that the new streams API is universally faster than any specific Web streams or Node.js streams implementation in all scenarios. The benchmarks were run on a MacBook Pro (M1 Pro, 16GB RAM) using Node.js v24.x.

``` ────────────────────────────────────────────────────────────────────── Running: 01-throughput.ts ──────────────────────────────────────────────────────────────────────

Benchmark: Raw Throughput Measuring data flow speed through streams Comparing: New Streams vs Web Streams vs Node.js Streams New API uses batched iteration (Uint8Array[]) for amortized overhead (minimum 20 samples, 3 seconds per test)

Running: Large chunks... Running: Medium chunks... Running: Small chunks... Running: Tiny chunks... Running: Async iteration... Running: Generator source...

================================================================================================================================== BENCHMARK RESULTS (higher throughput = better) ================================================================================================================================== Scenario | New Stream | Web Stream | Node Stream | New vs Web | New vs Node ---------------------------------+--------------------+--------------------+--------------------+----------------------+--------------------- Large chunks (64KB x 500) | 4.87 GB/s | 6.03 GB/s | 5.83 GB/s | ~same | ~same Medium chunks (8KB x 2000) | 5.72 GB/s | 4.72 GB/s | 3.83 GB/s | ~same | ~same Small chunks (1KB x 5000) | 4.64 GB/s | 2.09 GB/s | 779.10 MB/s | 2.22x faster | 5.96x faster Tiny chunks (100B x 10000) | 1.18 GB/s | 275.86 MB/s | 124.15 MB/s | 4.28x faster | 9.50x faster Async iteration (8KB x 1000) | 310.78 GB/s | 19.05 GB/s | 12.39 GB/s | 16.31x faster | 25.08x faster Generator source (8KB x 1000) | 19.71 GB/s | 14.29 GB/s | 9.88 GB/s | ~same | 1.99x faster ==================================================================================================================================

New Stream vs Web Stream: 3 faster, 0 slower, 3 within noise New Stream vs Node Stream: 4 faster, 0 slower, 2 within noise Samples per benchmark: 100

────────────────────────────────────────────────────────────────────── Running: 02-push-streams.ts ──────────────────────────────────────────────────────────────────────

Benchmark: Push Stream Performance Measuring concurrent write/read patterns Comparing: New Streams vs Web Streams vs Node.js Streams (minimum 15-20 samples, 3 seconds per test)

Running: Concurrent push (medium chunks)... Running: Many small writes... Running: Batch writes... Running: Push + async iteration...

================================================================================================================================== BENCHMARK RESULTS (higher throughput = better) ================================================================================================================================== Scenario | New Stream | Web Stream | Node Stream | New vs Web | New vs Node ---------------------------------+--------------------+--------------------+--------------------+----------------------+--------------------- Concurrent push (4KB x 1000) | 174.92 MB/s | 181.45 MB/s | 166.52 MB/s | ~same | ~same Many small writes (64B x 10000) | 111.66 MB/s | 85.41 MB/s | 77.15 MB/s | 1.31x faster | ~same Batch writes (512B x 20 x 200) | 137.98 MB/s | 145.09 MB/s | 136.07 MB/s | ~same | ~same Push + async iter (2KB x 1000) | 162.86 MB/s | 166.54 MB/s | 162.72 MB/s | ~same | ~same ==================================================================================================================================

New Stream vs Web Stream: 1 faster, 0 slower, 3 within noise New Stream vs Node Stream: 0 faster, 0 slower, 4 within noise Samples per benchmark: 100

────────────────────────────────────────────────────────────────────── Running: 03-transforms.ts ──────────────────────────────────────────────────────────────────────

Benchmark: Transform Performance Measuring data transformation speed Comparing: New Streams vs Web Streams vs Node.js Streams (minimum 15-20 samples, 3 seconds per test)

Running: Identity transform... Running: XOR transform... Running: Expanding transform... Running: Chained transforms... Running: Async transform...

================================================================================================================================== BENCHMARK RESULTS (higher throughput = better) =======================================================================================================

Extension points exported contracts — how you extend this code

ToStreamable (Interface)
(no doc) [6 implementers]
src/types.ts
PendingBroadcastWrite (Interface)
Pending write waiting for buffer space
src/broadcast.ts
PendingWrite (Interface)
Pending write waiting for buffer space (strict and block policies)
src/push.ts
EncryptionParams (Interface)
(no doc)
samples/15-encryption.ts
ExecutionContext (Interface)
(no doc)
samples/cloudflare-worker/src/index.ts
AsyncConsumerState (Interface)
(no doc)
src/share.ts
ThreeWayComparison (Interface)
(no doc)
benchmarks/08-pipeline-sync.ts
ThreeWayComparison (Interface)
(no doc)
benchmarks/09-sync-async-comparison.ts

Core symbols most depended-on inside this repo

push
called by 528
src/types.ts
benchmark
called by 238
benchmarks/utils.ts
write
called by 197
src/types.ts
pull
called by 182
src/types.ts
end
called by 154
src/types.ts
writeSync
called by 149
src/types.ts
section
called by 145
samples/util.js
close
called by 135
src/types.ts

Shape

Function 733
Method 214
Interface 59
Class 54

Languages

TypeScript100%

Modules by API surface

src/types.ts69 symbols
src/broadcast.ts55 symbols
src/push.ts51 symbols
src/share.ts41 symbols
src/from.test.ts35 symbols
src/pull.js32 symbols
benchmarks/utils.ts26 symbols
src/pull.ts25 symbols
benchmarks/21-memory-sustained.ts25 symbols
benchmarks/10-advanced-features.ts25 symbols
src/from.js24 symbols
samples/01-basic-creation.ts24 symbols

For agents

$ claude mcp add iter-streams \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact