Streaming responses
Use managed typed iterators or native SSE, text, and byte streams.
Return a sequence of values when a response should arrive incrementally instead of as one completed JSON object.
Choose managed streaming
Declare output: streamOf(itemSchema) and return an async generator. The iterator is lazy,
single-consumer, schema-validates each item, and retains invocation scope until completion or
cancellation. The queue is bounded to 32 items and 4 MiB; a fully encoded item over 1 MiB fails
immediately.
/** Supplies the report route with ten validated stream items, stopping when its invocation signal aborts. */
import { defineFunction, streamOf } from "@relkit/app/functions";
import { z } from "@relkit/app/schema";
const streamOrderReport = defineFunction({
input: z.object({ reportId: z.string().min(1) }),
output: streamOf(z.object({ completed: z.number().int(), total: z.number().int() })),
handler: async function* (_input, context) {
for (let completed = 1; completed <= 10; completed += 1) {
// if (completed > 1) {
// await Bun.sleep(1_000);
// }
if (context.signal.aborted) return;
yield { completed, total: completed };
}
},
});
export default streamOrderReport;
Use from the frontend
Expose that function through a route before pulling its contract:
/** Maps a report path identifier to the order stream and exposes its validated items as server-sent events. */
import { defineRoute, http } from "@relkit/app/routes";
import orders from "../../../services/orders/service.js";
export const GET = defineRoute({
target: orders.streamOrderReport,
request: http.input({ reportId: http.path("reportId") }),
client: { operation: "query" },
stream: { format: "sse" },
});
After frontend setup and pulling the commerce contract, render
this component under the provider. The GET /reports/:reportId route exposes the streaming function:
"use client";
/** Consumes the commerce report route as a cancellable typed stream inside the client provider. */
import { useRelkitClient, useStream } from "@relkit/client/react";
/** Starts one report explicitly and displays its validated progress items.
* @returns Start/cancel controls and visible progress or stream failure. */
export function Report() {
const { status } = useRelkitClient();
const report = useStream("GET /reports/:reportId");
return (
<section>
<button
disabled={status !== "ready"}
onClick={() => void report.start({ reportId: "daily" })}
>
Run report
</button>
<button onClick={() => report.cancel()}>Cancel report</button>
<p role="status">Completed: {report.items.map((item) => item.completed).join(", ")}</p>
{report.error && <p role="alert">{String(report.error)}</p>}
</section>
);
}
streamOf supplies the item type, so report.items exposes each item’s completed field.
start({ reportId }) begins the request; cancel() releases it. Use the domain input even when
REST places a field in the URL. Streaming operations are excluded from finite useRoute queries.
Reference: useStream.
Continue with stream lifecycle and failures.
Choose a native format
Managed oRPC iterators preserve typed completion and failure. Native SSE emits relkit-item and an
explicit relkit-complete or sanitized relkit-error. Native text and bytes abort on a post-header
application failure; because EOF cannot always distinguish failure, use managed streaming or SSE
when callers need an explicit terminal result. Existing raw Hono routes remain the escape hatch.