-
Notifications
You must be signed in to change notification settings - Fork 0
Concepts: streaming
double-meh streams over Web streams (ReadableStream / WritableStream) in both directions —
the same API in the browser and on the CLI, and it composes directly with
stream-chain. There is no node:stream code; bridge
to Node streams with Readable.fromWeb / Readable.toWeb when you need to.
Streaming has two independent axes:
| Axis | How you turn it on |
|---|---|
| Response (read the body as a stream) | the stream: true option, or the io.stream namespace |
| Request (send a stream as the body) | pass a ReadableStream / Blob / File as the body — no flag |
They compose freely: one axis each on a write = stream up and down (that is what the io.stream
write duplexes give you).
{stream: true} makes the pipeline hand back the raw response.body instead of parsing it:
const body = await io.get(url, null, {stream: true}); // a ReadableStreamio.stream.get is the GET-only sugar for that — no null placeholder:
const body = await io.stream.get(url); // ≡ io.get(url, null, {stream: true})
const rows = await io.stream.get(url, {page: 2}); // query still composesio.stream is a return-shape namespace alongside io.full. get is its only read verb:
HEAD/OPTIONS/DELETE have no body to stream, and write verbs take the body positionally, so
io.post(url, data, {stream: true}) already streams the response with no placeholder. Its write
verbs — io.stream.put/post/patch — are the request/response duplexes below.
track and cache stand down for streamed responses (a stream is
single-consumer).
Pass a Web ReadableStream (or Blob / File / FormData) as the body and it streams up — the
fetch transport sets duplex: 'half' automatically, and
retry stands down (a stream cannot be replayed):
await io.put(url, fileStream, {as: 'octet'}); // Content-Type application/octet-streamA streamed body carries no inherent Content-Type (unlike a Blob), so set one — the terse way is the
as shorthand ({as: 'ndjson'}, or a full type {as: 'application/x-ndjson'}). See
Requests & options.
Request-body streaming for writes lives on the same namespace — io.stream.put /
io.stream.post / io.stream.patch (url, options?). Each returns a Web duplex synchronously,
so it drops straight into a stream-chain pipe as a node:
import io from 'double-meh';
const up = io.stream.put('https://example.com/data', {as: 'ndjson'});
// up.writable → the request body streams up (duplex:'half')
// up.readable → the streamed response body
// up.response → a promise for the envelope (status/headers), resolved at headers-time-
No
dataarg — the body arrives through the pipe, so the 2nd positional isoptions. -
Non-2xx errors
up.readableand rejectsup.response; pass{ignoreBadStatus: true}to stream the error-response body through instead. - Response streaming is intrinsic — a duplex never takes
{stream: true}. - There is no read-verb duplex (GET has no body to stream — use
io.stream.get).
io.put accepts a stream-chain chain (or any {readable} duplex) directly and uses its .readable
as the body — so you can terminate a pipeline into an upload:
const pipe = chain([source, transform, jsonlStringer()]);
await io.put(url, pipe, {as: 'ndjson'}); // or io.put(url, pipe.readable, …)Byte streams are the floor; two helpers consume framed streams directly:
-
io.records.get/post— a lazy async iterable of parsed JSONL /json-seqrecords (for await (const record of io.records.get(url))). -
io.sse— Server-Sent Events with EventSource-style reconnects, riding the pipeline (auth headers, POST, inspectors on every reconnect).
They cover consuming simply. Record processing pipelines and the encode/upload side are
stream-chain's job (below) — double-meh deliberately duplicates none of it.
Import from stream-chain/web (its Web-streams build) and stream-chain/web/jsonl (the JSONL
factories). io.stream.get is a pipe source; io.stream.put is a mid-pipe duplex (upload,
then the server's streamed response flows on):
import {chain} from 'stream-chain/web';
import {jsonlParser, jsonlStringer} from 'stream-chain/web/jsonl';
import io from 'double-meh';
const pipe = chain([
await io.stream.get(inputUrl), // source: streamed GET
jsonlParser(), // bytes → records
record => ((record.ts = Date.now()), record),
jsonlStringer(), // records → bytes
io.stream.put(outputUrl, {as: 'ndjson'}) // duplex: upload, response streams back
]);More recipes: Cookbook: streaming pipelines.
Guides
Concepts
Services
Modules
- fetch transport
- keys & URLs
- helpers
- records
- sse
- storage backends
- compression encoders
- Service Worker integration
Cookbook