Search docs

Jump between documentation pages.

Streaming responses

DaloyJS ships first-class helpers for two streaming formats that are common in HTTP APIs: Server-Sent Events (SSE) and newline-delimited JSON (NDJSON). Both helpers wrap anAsyncIterable in a backpressure-safe ReadableStream — the underlying iterator is only advanced when the consumer pulls the next chunk, so a slow client cannot cause unbounded memory growth.

They also honor an optional AbortSignal and call iterator.return() when the client disconnects, so any caller-owned resources (DB cursors, upstream fetches, message-queue subscriptions) get released cleanly.

The helpers live in the main barrel and in the /streaming subpath:

ts
import {
  sseStream,
  sseResponse,
  ndjsonStream,
  ndjsonResponse,
} from "@daloyjs/core";

// Or, if you want a tree-shake-friendly subpath:
import { sseStream } from "@daloyjs/core/streaming";

Server-Sent Events (SSE)

Yield either a string (sent as data: …) or an SSEMessage object with any combination of event, id, retry, comment, and data. Multi-line strings are split into one data: line per source line, and CR/LF in event / id values are sanitized.

ts
import { sseStream } from "@daloyjs/core";

app.route({
  method: "GET",
  path: "/events",
  operationId: "events",
  responses: { 200: { description: "SSE stream" } },
  handler: ({ request }) => ({
    status: 200 as const,
    headers: { "content-type": "text/event-stream" },
    body: sseStream(
      async function* () {
        for (let i = 0; i < 5; i++) {
          yield { event: "tick", id: String(i), data: { now: Date.now() } };
          await new Promise((r) => setTimeout(r, 1_000));
        }
      },
      { signal: request.signal, keepAliveMs: 15_000 }
    ),
  }),
});

Use sseResponse(...) when you want a fully-formed Response with the standard SSE headers (text/event-stream, cache-control: no-cache, no-transform, connection: keep-alive, and x-accel-buffering: no) already set:

ts
import { sseResponse } from "@daloyjs/core";

const res = sseResponse(async function* () {
  yield { event: "ping", data: "hi" };
});

Keep-alive comments

Pass keepAliveMs to send a : keep-alive comment frame at a fixed interval. This prevents idle proxies from closing the connection while no events are flowing.

Newline-delimited JSON (NDJSON)

Yield any JSON-serializable value; each value is encoded with JSON.stringify and terminated with a single \n. Strings are emitted as JSON strings, and values that cannot be represented as JSON throw instead of emitting invalid NDJSON.

ts
import { ndjsonStream } from "@daloyjs/core";

app.route({
  method: "GET",
  path: "/exports/users.ndjson",
  operationId: "exportUsers",
  responses: { 200: { description: "NDJSON dump" } },
  handler: ({ request }) => ({
    status: 200 as const,
    headers: { "content-type": "application/x-ndjson" },
    body: ndjsonStream(
      (async function* () {
        for await (const user of db.users.cursor()) {
          yield user;
        }
      })(),
      { signal: request.signal }
    ),
  }),
});

ndjsonResponse(...) builds the same stream with application/x-ndjson headers pre-set.

Backpressure & cancellation

Both helpers use the pull() entry point of ReadableStream — they call iterator.next() exactly once per pull. The runtime decides when to pull: a slow client on a Node socket pulls slowly, a fast Cloudflare consumer pulls quickly. You never need to write throttling code.

When the request is aborted (client disconnects, request timeout fires, explicit AbortController.abort()), the stream is closed and iterator.return() is invoked so a generator's finally block runs and any underlying cursor/socket is released.

Cross-runtime compatibility

The helpers only depend on web-standard ReadableStream and TextEncoder, so the same handler works identically on Node, Bun, Deno, Cloudflare Workers, and Vercel Edge. The DaloyJS response serializer recognizes a ReadableStream body when you set an explicit non-JSON content-type and forwards it to the runtime without buffering.

OpenAPI

OpenAPI 3.1 has no rich schema for streamed event payloads. Document streaming routes with a free-form 200 response (just { description }) and describe the event shape in prose, or attach an example string showing one or two frames.