mrkeyoor.com_
Wed 23 Sept 02:54 UTC
npmUtilsupdated 22 Sept 2026

turbo-stream review

turbo-stream 3.2.1 is a JavaScript-to-JavaScript codec carried by Web ReadableStream. It preserves values that JSON cannot represent directly, including BigInt, Date, Map, Set, special numbers, typed arrays, circular identity, promises, async iterables, files, blobs, form data, and nested streams. A receiver can get the outer object while embedded async values are still arriving. The 3.2.1 release fixes `__proto__` handling and exponential-notation numbers. This is a transport format, not HTTP, validation, or durable storage.

Verdict

Our turbo-stream 3.2.1 browser import measured 14.2 KB minified and 4.8 KB gzipped with 0 runtime dependencies. Choose it for JavaScript peers that need progressive nested values or object identity; prefer JSON or a cross-language codec for ordinary payloads.

We installed it

Lab card: what happened when we installed turbo-streamScreenshot of turbo-stream documentation
Install✓ · 0.7s1 package on disk · 1 MB
ImportESM import works · require() works · CommonJS package with exports map
Browser4.8 KBgzipped (14.2 KB minified), bundled with esbuild
TypesTypeScript types bundled
Known vulns00 critical · 0 high · 0 moderate · 0 low (npm audit)

Answers from our run

Does turbo-stream install cleanly?

Yes. In a fresh container with an empty cache, npm install turbo-stream finished in 0.7s, leaving 1 package and 1 MB on disk. npm audit reported no known vulnerabilities.

How much does turbo-stream add to a browser bundle?

4.8 KB gzipped (14.2 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.

Does turbo-stream work with both ESM and CommonJS?

Yes. Both import 'turbo-stream' and require('turbo-stream') worked in Node 22 in our run. The package is published as CommonJS with an exports map.

Does turbo-stream include TypeScript types?

Yes, type declarations ship inside the package, so no @types install is needed.

turbo-stream or devalue: which should you use?

devalue: Use it for rich JavaScript value serialization without progressive Web Stream delivery. Our turbo-stream 3.2.1 browser import measured 14.2 KB minified and 4.8 KB gzipped with 0 runtime dependencies.

When should you not use turbo-stream?

Other languages need to consume the payload: turbo-stream has no standard cross-language wire format

API stability3/5Version 3.2.1 keeps the public surface centered on `encode`, `decode`, option bags, and paired plugins, which is small enough to wrap and fixture-test. The v3.0.0 release introduced a new encoding format, so textual API stability does not guarantee old payload compatibility. The 3.2.1 fixes for `__proto__` and exponential numbers also show that edge-case wire behavior is still being refined. Pin the major on both ends.
Docs3/5The README lists supported scalar, collection, binary, async, reference, and Web platform values; shows the basic stream round trip; states that ReadableStream is the transport; and publishes a reproducible benchmark command. The documentation site resolves and expands the API. Production details such as byte conversion, AbortSignal wiring, error redaction, plugin mismatch behavior, runtime feature checks, and cross-major fixture testing still require declarations or source reading.
Maintenance4/5GitHub shows a push on 2026-08-19, the same date as the current v3.2.1 release, and the repository is not archived. That release addresses two concrete decoder cases, while 3.2.0 added large-chunk handling through a high-water mark. GitHub reports 3 open issues and pull requests combined. The project is active, though a compact maintainer footprint and protocol-level changes keep it short of a 5.
Ecosystem4/5npm counted 2,743,164 downloads last week. The package provides CommonJS and ESM entry paths, an exports map, bundled declarations, browser compatibility, and no runtime dependencies. Web Streams make it usable across current JavaScript servers and browsers. Its boundary remains JavaScript-specific: custom plugins must match on both ends, and no documented family of non-JavaScript decoders makes the format suitable for a mixed-language public API.

Use it if

  • A server should send an outer object immediately while nested promises or async iterables continue producing values
  • Maps, Sets, Dates, BigInts, typed arrays, circular links, or repeated object identity must survive transport
  • Producer and consumer both run JavaScript with Web Streams and can pin the same turbo-stream major
  • You need a dependency-free codec with CommonJS, ESM, an exports map, and bundled declarations
Skip it if

Setup reality

We installed turbo-stream 3.2.1 in our sandbox in 0.7 seconds. It left 1 package and 1 MB on disk, and npm audit found 0 known vulnerabilities. The package is 184 KB unpacked, with 0 direct dependencies, 0 peer dependencies, and an MIT license. No credentials, native build, service, or config file is required.

The package is CommonJS with an exports map that also provides an ESM import target. require() and ESM import both worked under Node 22, and TypeScript declarations are bundled. Our full browser import measured 14.2 KB minified and 4.8 KB gzipped. The registry declares no Node engine floor, so deployments must separately confirm Web Streams and any payload classes such as File, Blob, or FormData.

encode() produces a Web ReadableStream of strings. HTTP responses and files usually need bytes, so pipe through TextEncoderStream; feed response bytes through TextDecoderStream before decode(). Classic node:stream objects need Web Stream adapters. Embedded promises and iterables stay live after the outer decode resolves, which means late errors must be handled at each await or iteration point. Wire an AbortSignal through to stop production after disconnects.

Errors are redacted by default, and disabling redaction can expose names, messages, and stacks. Objects with toJSON() execute that method during encoding. Custom types require paired plugins deployed with the same tag and field order on both ends. Validate the decoded result at the application boundary. Version 3 changed the encoding format, while 3.2.1 specifically repairs __proto__ and exponential-number cases, so keep producer and consumer majors coordinated and retain fixture tests across upgrades.

Patterns

Encode and decode one rich value round-trip

import { encode, decode } from 'turbo-stream';

const stream = encode({ answer: 42, createdAt: new Date() });
const value = await decode(stream);

`encode()` returns the exact string Web Stream shape accepted by `decode()` before any network byte conversion.

Keep collections and special numbers preserve-types

const input = {
  ids: new Set([1, 2]),
  labels: new Map([[1, 'one']]),
  limit: Infinity,
  counter: 42n
};
const output = await decode(encode(input));

These runtime values survive the round trip; JSON would flatten, reject, or lose several of them.

Retain circular and repeated references preserve-identity

const shared = { name: 'shared' };
const input = { first: shared, second: shared };
input.self = input;
const output = await decode(encode(input));
console.log(output.first === output.second, output.self === output);

Reference identity is encoded, so the format represents graphs rather than only JSON-style trees.

Deliver a nested Promise later stream-promise

const decoded = await decode(encode({
  user: { id: 7 },
  recommendations: fetchRecommendations(7)
}));
renderUser(decoded.user);
renderRecommendations(await decoded.recommendations);

The outer object can arrive first. Handle rejection where the nested Promise is awaited.

Transport an async iterable stream-iterable

const decoded = await decode(encode({ events: eventSource() }));
for await (const event of decoded.events) {
  console.log(event);
}

A producer failure after iteration starts appears during the loop, not in the initial `decode()` result.

Encode strings into an HTTP body send-http

const body = encode(loadDashboard())
  .pipeThrough(new TextEncoderStream());
return new Response(body, {
  headers: { 'content-type': 'text/x-turbo-stream; charset=utf-8' }
});

HTTP bodies carry byte chunks. `TextEncoderStream` converts turbo-stream's string chunks at that boundary.

Decode a fetch response decode-http

const response = await fetch('/api/dashboard');
if (!response.ok || !response.body) throw new Error(`HTTP ${response.status}`);
const data = await decode(response.body.pipeThrough(new TextDecoderStream()));

Check status and content type yourself; `decode()` does not validate the HTTP response.

Stop pending values after disconnect abort-encoding

const controller = new AbortController();
request.signal.addEventListener('abort', () => controller.abort());
const stream = encode(loadDashboard(), { signal: controller.signal });

The signal stops codec work waiting on async values. The underlying database or network calls need their own cancellation wiring.

Choose the serialized error exposure redact-errors

const safe = encode(result);
const publicMessage = encode(result, { redactErrors: 'Request failed' });
const internal = encode(result, { redactErrors: false });

`false` may send an original stack and message. Keep redaction across trust boundaries.

Pair plugins for a custom class custom-type

const encodeMoney = value =>
  value instanceof Money ? ['Money', value.cents, value.currency] : false;
const decodeMoney = (type, cents, currency) =>
  type === 'Money' ? { value: new Money(cents, currency) } : false;

const wire = encode(money, { plugins: [encodeMoney] });
const copy = await decode(wire, { plugins: [decodeMoney] });

Both peers must agree on tag and field order. Deploy plugin changes with the same protocol-version discipline as the core codec.

Alternatives

PackageRegistryPick it when
devaluenpmUse it for rich JavaScript value serialization without progressive Web Stream delivery.
superjsonnpmUse it when JSON plus metadata for common rich values is easier to inspect and transport.
@msgpack/msgpacknpmUse it for a compact binary format with implementations outside JavaScript.

More utils guides

lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.