@@ -2,17 +2,34 @@ import type { HattipHandler } from '@hattip/core'
22import { createClient , createRouter } from 'rouzer'
33import * as http from 'rouzer/http'
44import * as ndjson from 'rouzer/ndjson'
5+ import { z } from 'zod'
56
67type Event = {
78 id : number
89 message : string
910}
1011
12+ const EventFilter = z . object ( {
13+ names : z . array ( z . string ( ) ) ,
14+ where : z . array (
15+ z . object ( {
16+ path : z . string ( ) ,
17+ equals : z . string ( ) ,
18+ } )
19+ ) ,
20+ } )
21+
1122export const events = http . get ( 'events' , {
1223 response : ndjson . $type < Event > ( ) ,
1324} )
1425
15- export const routes = { events }
26+ // NDJSON responses work for POST routes with ordinary JSON body schemas too.
27+ export const stream = http . post ( 'events/stream' , {
28+ body : EventFilter ,
29+ response : ndjson . $type < Event > ( ) ,
30+ } )
31+
32+ export const routes = { events, stream }
1633
1734/**
1835 * Tiny Hattip adapter used only to keep this example self-contained. Real apps
@@ -46,6 +63,17 @@ async function collect<T>(source: AsyncIterable<T>) {
4663 return values
4764}
4865
66+ async function readFirst < T > ( source : AsyncIterable < T > ) {
67+ const iterator = source [ Symbol . asyncIterator ] ( )
68+ try {
69+ return ( await iterator . next ( ) ) . value
70+ } finally {
71+ // Closing the client iterator cancels the response body. For Rouzer NDJSON
72+ // routes, that cancellation reaches the server source iterator's return().
73+ await iterator . return ?.( )
74+ }
75+ }
76+
4977export async function runNdjsonStreamExample ( ) {
5078 const handler = createRouter ( {
5179 basePath : 'api/' ,
@@ -55,6 +83,14 @@ export async function runNdjsonStreamExample() {
5583 yield { id : 1 , message : 'ready' }
5684 yield { id : 2 , message : 'done' }
5785 } ,
86+ async * stream ( { body } ) {
87+ // The POST body was parsed and validated before the stream starts.
88+ yield {
89+ id : 1 ,
90+ message : `${ body . names [ 0 ] } for ${ body . where [ 0 ] ?. equals } ` ,
91+ }
92+ yield { id : 2 , message : 'done' }
93+ } ,
5894 } )
5995
6096 const client = createClient ( {
@@ -64,5 +100,16 @@ export async function runNdjsonStreamExample() {
64100 fetch : createLocalFetch ( handler ) ,
65101 } )
66102
67- return collect ( await client . events ( ) )
103+ const allEvents = await collect ( await client . events ( ) )
104+
105+ // This call sends a JSON body, receives an AsyncIterable, and then stops after
106+ // one event. Request signals can also be used to cancel long-lived streams.
107+ const firstMatchingEvent = await readFirst (
108+ await client . stream ( {
109+ names : [ 'session.message' ] ,
110+ where : [ { path : 'id' , equals : 'ses_123' } ] ,
111+ } )
112+ )
113+
114+ return { allEvents, firstMatchingEvent }
68115}
0 commit comments