ts-procedures 4.0.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/implementations/http/hono-stream/index.d.ts +3 -3
- package/build/implementations/http/hono-stream/index.js +36 -5
- package/build/implementations/http/hono-stream/index.js.map +1 -1
- package/build/implementations/http/hono-stream/index.test.js +191 -47
- package/build/implementations/http/hono-stream/index.test.js.map +1 -1
- package/package.json +1 -1
- package/src/implementations/http/hono-stream/README.md +58 -31
- package/src/implementations/http/hono-stream/index.test.ts +234 -47
- package/src/implementations/http/hono-stream/index.ts +43 -9
|
@@ -3,15 +3,15 @@ import { TStreamProcedureRegistration } from '../../../index.js';
|
|
|
3
3
|
import { ExtractConfig, ExtractContext, ProceduresFactory, RPCConfig } from '../../types.js';
|
|
4
4
|
import { StreamHttpRouteDoc, StreamMode } from './types.js';
|
|
5
5
|
export type { StreamHttpRouteDoc, StreamMode };
|
|
6
|
-
export type
|
|
7
|
-
data: string | unknown;
|
|
6
|
+
export type SSEOptions = {
|
|
8
7
|
event?: string;
|
|
9
8
|
id?: string;
|
|
10
9
|
retry?: number;
|
|
11
10
|
};
|
|
11
|
+
export declare function sse<T extends object>(data: T, options?: SSEOptions): T;
|
|
12
12
|
/**
|
|
13
13
|
* Result from onMidStreamError callback.
|
|
14
|
-
* @property data - The data to write
|
|
14
|
+
* @property data - The data to write as the SSE `data:` field content (should match yieldType schema)
|
|
15
15
|
* @property event - Optional SSE event name (defaults to procedure name if data provided, 'error' otherwise)
|
|
16
16
|
* @property id - Optional SSE event id (auto-incremented if not provided)
|
|
17
17
|
* @property closeStream - Whether to close the stream after writing (defaults to true)
|
|
@@ -3,6 +3,17 @@ import { streamSSE, streamText } from 'hono/streaming';
|
|
|
3
3
|
import { kebabCase } from 'es-toolkit/string';
|
|
4
4
|
import { castArray } from 'es-toolkit/compat';
|
|
5
5
|
import { ProcedureValidationError } from '../../../errors.js';
|
|
6
|
+
const sseMetadata = new WeakMap();
|
|
7
|
+
export function sse(data, options) {
|
|
8
|
+
sseMetadata.set(data, options ?? {});
|
|
9
|
+
return data;
|
|
10
|
+
}
|
|
11
|
+
function getSSEMeta(value) {
|
|
12
|
+
if (typeof value === 'object' && value !== null) {
|
|
13
|
+
return sseMetadata.get(value);
|
|
14
|
+
}
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
6
17
|
/**
|
|
7
18
|
* Builder class for creating a Hono application with streaming RPC routes.
|
|
8
19
|
*
|
|
@@ -128,11 +139,17 @@ export class HonoStreamAppBuilder {
|
|
|
128
139
|
try {
|
|
129
140
|
for await (const value of generator) {
|
|
130
141
|
const currentId = eventId++;
|
|
142
|
+
const meta = getSSEMeta(value);
|
|
143
|
+
const data = typeof value === 'string'
|
|
144
|
+
? value
|
|
145
|
+
: value != null
|
|
146
|
+
? JSON.stringify(value)
|
|
147
|
+
: '';
|
|
131
148
|
await stream.writeSSE({
|
|
132
|
-
data
|
|
133
|
-
event:
|
|
134
|
-
id:
|
|
135
|
-
...(
|
|
149
|
+
data,
|
|
150
|
+
event: meta?.event ?? procedure.name,
|
|
151
|
+
id: meta?.id ?? String(currentId),
|
|
152
|
+
...(meta?.retry !== undefined && { retry: meta.retry }),
|
|
136
153
|
});
|
|
137
154
|
}
|
|
138
155
|
}
|
|
@@ -232,7 +249,21 @@ export class HonoStreamAppBuilder {
|
|
|
232
249
|
if (config.schema?.params) {
|
|
233
250
|
jsonSchema.params = config.schema.params;
|
|
234
251
|
}
|
|
235
|
-
if (
|
|
252
|
+
if (streamMode === 'sse') {
|
|
253
|
+
jsonSchema.yieldType = {
|
|
254
|
+
type: 'object',
|
|
255
|
+
description: 'SSE message envelope. The data field contains the procedure yield value.',
|
|
256
|
+
required: ['data', 'event', 'id'],
|
|
257
|
+
properties: {
|
|
258
|
+
data: config.schema?.yieldType ?? {},
|
|
259
|
+
event: { type: 'string' },
|
|
260
|
+
id: { type: 'string' },
|
|
261
|
+
retry: { type: 'number' },
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
else if (config.schema?.yieldType) {
|
|
266
|
+
// Text mode: pass through as-is
|
|
236
267
|
jsonSchema.yieldType = config.schema.yieldType;
|
|
237
268
|
}
|
|
238
269
|
if (config.schema?.returnType) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/implementations/http/hono-stream/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAW,MAAM,MAAM,CAAA;AACpC,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AACtD,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAC7C,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAI7C,OAAO,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/implementations/http/hono-stream/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-empty-object-type, @typescript-eslint/explicit-function-return-type */
|
|
1
2
|
import { describe, expect, test, vi, beforeEach } from 'vitest';
|
|
2
3
|
import { Hono } from 'hono';
|
|
3
4
|
import { v } from 'suretype';
|
|
4
5
|
import { Procedures } from '../../../index.js';
|
|
5
|
-
import { HonoStreamAppBuilder } from './index.js';
|
|
6
|
+
import { HonoStreamAppBuilder, sse } from './index.js';
|
|
6
7
|
/**
|
|
7
8
|
* HonoStreamAppBuilder Test Suite
|
|
8
9
|
*
|
|
@@ -17,9 +18,9 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
17
18
|
test('creates default Hono app', async () => {
|
|
18
19
|
const builder = new HonoStreamAppBuilder();
|
|
19
20
|
const RPC = Procedures();
|
|
20
|
-
RPC.CreateStream('StreamMessages', { scope: 'messages', version: 1 }, async function* (
|
|
21
|
+
RPC.CreateStream('StreamMessages', { scope: 'messages', version: 1 }, async function* () {
|
|
21
22
|
yield { message: 'hello' };
|
|
22
|
-
yield {
|
|
23
|
+
yield { message: 'world' };
|
|
23
24
|
});
|
|
24
25
|
builder.register(RPC, () => ({ userId: '123' }));
|
|
25
26
|
const app = builder.build();
|
|
@@ -35,7 +36,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
35
36
|
const builder = new HonoStreamAppBuilder({ app: customApp });
|
|
36
37
|
const RPC = Procedures();
|
|
37
38
|
RPC.CreateStream('StreamData', { scope: 'data', version: 1 }, async function* () {
|
|
38
|
-
yield { data:
|
|
39
|
+
yield { data: 1 };
|
|
39
40
|
});
|
|
40
41
|
builder.register(RPC, () => ({ userId: '123' }));
|
|
41
42
|
const app = builder.build();
|
|
@@ -67,9 +68,9 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
67
68
|
const builder = new HonoStreamAppBuilder();
|
|
68
69
|
const RPC = Procedures();
|
|
69
70
|
RPC.CreateStream('Counter', { scope: 'counter', version: 1 }, async function* () {
|
|
70
|
-
yield {
|
|
71
|
-
yield {
|
|
72
|
-
yield {
|
|
71
|
+
yield { count: 1 };
|
|
72
|
+
yield { count: 2 };
|
|
73
|
+
yield { count: 3 };
|
|
73
74
|
});
|
|
74
75
|
builder.register(RPC, () => ({}));
|
|
75
76
|
const app = builder.build();
|
|
@@ -89,7 +90,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
89
90
|
const builder = new HonoStreamAppBuilder();
|
|
90
91
|
const RPC = Procedures();
|
|
91
92
|
RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
92
|
-
yield {
|
|
93
|
+
yield { ok: true };
|
|
93
94
|
});
|
|
94
95
|
builder.register(RPC, () => ({}));
|
|
95
96
|
const app = builder.build();
|
|
@@ -100,7 +101,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
100
101
|
const builder = new HonoStreamAppBuilder({ defaultStreamMode: 'sse' });
|
|
101
102
|
const RPC = Procedures();
|
|
102
103
|
RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
103
|
-
yield {
|
|
104
|
+
yield { ok: true };
|
|
104
105
|
});
|
|
105
106
|
builder.register(RPC, () => ({}));
|
|
106
107
|
const app = builder.build();
|
|
@@ -152,7 +153,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
152
153
|
const builder = new HonoStreamAppBuilder();
|
|
153
154
|
const RPC = Procedures();
|
|
154
155
|
RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
155
|
-
yield {
|
|
156
|
+
yield { method: 'works' };
|
|
156
157
|
});
|
|
157
158
|
builder.register(RPC, () => ({}));
|
|
158
159
|
const app = builder.build();
|
|
@@ -163,7 +164,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
163
164
|
const builder = new HonoStreamAppBuilder();
|
|
164
165
|
const RPC = Procedures();
|
|
165
166
|
RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
166
|
-
yield {
|
|
167
|
+
yield { method: 'works' };
|
|
167
168
|
});
|
|
168
169
|
builder.register(RPC, () => ({}));
|
|
169
170
|
const app = builder.build();
|
|
@@ -213,7 +214,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
213
214
|
const builder = new HonoStreamAppBuilder({ pathPrefix: '/api/v1' });
|
|
214
215
|
const RPC = Procedures();
|
|
215
216
|
RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
216
|
-
yield {
|
|
217
|
+
yield { ok: true };
|
|
217
218
|
});
|
|
218
219
|
builder.register(RPC, () => ({}));
|
|
219
220
|
const app = builder.build();
|
|
@@ -224,7 +225,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
224
225
|
const builder = new HonoStreamAppBuilder({ pathPrefix: 'custom' });
|
|
225
226
|
const RPC = Procedures();
|
|
226
227
|
RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
227
|
-
yield {
|
|
228
|
+
yield { ok: true };
|
|
228
229
|
});
|
|
229
230
|
builder.register(RPC, () => ({}));
|
|
230
231
|
const app = builder.build();
|
|
@@ -235,7 +236,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
235
236
|
const builder = new HonoStreamAppBuilder({ pathPrefix: '/api' });
|
|
236
237
|
const RPC = Procedures();
|
|
237
238
|
RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
238
|
-
yield {
|
|
239
|
+
yield {};
|
|
239
240
|
});
|
|
240
241
|
builder.register(RPC, () => ({}));
|
|
241
242
|
builder.build();
|
|
@@ -251,7 +252,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
251
252
|
const builder = new HonoStreamAppBuilder({ onRequestStart });
|
|
252
253
|
const RPC = Procedures();
|
|
253
254
|
RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
254
|
-
yield {
|
|
255
|
+
yield { ok: true };
|
|
255
256
|
});
|
|
256
257
|
builder.register(RPC, () => ({}));
|
|
257
258
|
const app = builder.build();
|
|
@@ -264,7 +265,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
264
265
|
const builder = new HonoStreamAppBuilder({ onRequestEnd });
|
|
265
266
|
const RPC = Procedures();
|
|
266
267
|
RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
267
|
-
yield {
|
|
268
|
+
yield { ok: true };
|
|
268
269
|
});
|
|
269
270
|
builder.register(RPC, () => ({}));
|
|
270
271
|
const app = builder.build();
|
|
@@ -278,7 +279,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
278
279
|
const builder = new HonoStreamAppBuilder({ onStreamStart });
|
|
279
280
|
const RPC = Procedures();
|
|
280
281
|
RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
281
|
-
yield {
|
|
282
|
+
yield { ok: true };
|
|
282
283
|
});
|
|
283
284
|
builder.register(RPC, () => ({}));
|
|
284
285
|
const app = builder.build();
|
|
@@ -291,7 +292,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
291
292
|
const builder = new HonoStreamAppBuilder({ onStreamEnd });
|
|
292
293
|
const RPC = Procedures();
|
|
293
294
|
RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
294
|
-
yield {
|
|
295
|
+
yield { ok: true };
|
|
295
296
|
});
|
|
296
297
|
builder.register(RPC, () => ({}));
|
|
297
298
|
const app = builder.build();
|
|
@@ -312,7 +313,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
312
313
|
const RPC = Procedures();
|
|
313
314
|
RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
314
315
|
order.push('handler');
|
|
315
|
-
yield {
|
|
316
|
+
yield { ok: true };
|
|
316
317
|
});
|
|
317
318
|
builder.register(RPC, () => ({}));
|
|
318
319
|
const app = builder.build();
|
|
@@ -351,7 +352,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
351
352
|
params: v.object({ count: v.number() }),
|
|
352
353
|
},
|
|
353
354
|
}, async function* (ctx, params) {
|
|
354
|
-
yield {
|
|
355
|
+
yield { count: params.count };
|
|
355
356
|
});
|
|
356
357
|
builder.register(RPC, () => ({}));
|
|
357
358
|
const app = builder.build();
|
|
@@ -367,7 +368,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
367
368
|
const builder = new HonoStreamAppBuilder();
|
|
368
369
|
const RPC = Procedures();
|
|
369
370
|
RPC.CreateStream('ErrorStream', { scope: 'error', version: 1 }, async function* () {
|
|
370
|
-
yield {
|
|
371
|
+
yield { count: 1 };
|
|
371
372
|
throw new Error('Stream error');
|
|
372
373
|
});
|
|
373
374
|
builder.register(RPC, () => ({}));
|
|
@@ -404,7 +405,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
404
405
|
params: v.object({ count: v.number() }),
|
|
405
406
|
},
|
|
406
407
|
}, async function* (ctx, params) {
|
|
407
|
-
yield {
|
|
408
|
+
yield { count: params.count };
|
|
408
409
|
});
|
|
409
410
|
builder.register(RPC, () => ({}));
|
|
410
411
|
const app = builder.build();
|
|
@@ -428,7 +429,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
428
429
|
params: v.object({ count: v.number() }),
|
|
429
430
|
},
|
|
430
431
|
}, async function* (ctx, params) {
|
|
431
|
-
yield {
|
|
432
|
+
yield { count: params.count };
|
|
432
433
|
});
|
|
433
434
|
builder.register(RPC, () => ({}));
|
|
434
435
|
const app = builder.build();
|
|
@@ -447,7 +448,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
447
448
|
const builder = new HonoStreamAppBuilder({ onPreStreamError });
|
|
448
449
|
const RPC = Procedures();
|
|
449
450
|
RPC.CreateStream('SecureStream', { scope: 'secure', version: 1 }, async function* (ctx) {
|
|
450
|
-
yield {
|
|
451
|
+
yield { userId: ctx.userId };
|
|
451
452
|
});
|
|
452
453
|
builder.register(RPC, () => {
|
|
453
454
|
throw new Error('Authentication required');
|
|
@@ -474,7 +475,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
474
475
|
const builder = new HonoStreamAppBuilder({ onMidStreamError });
|
|
475
476
|
const RPC = Procedures();
|
|
476
477
|
RPC.CreateStream('ErrorStream', { scope: 'error', version: 1 }, async function* () {
|
|
477
|
-
yield {
|
|
478
|
+
yield { type: 'data', value: 1 };
|
|
478
479
|
throw new Error('Something broke');
|
|
479
480
|
});
|
|
480
481
|
builder.register(RPC, () => ({}));
|
|
@@ -609,8 +610,73 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
609
610
|
expect(doc.methods).toEqual(['get', 'post']);
|
|
610
611
|
expect(doc.streamMode).toBe('sse');
|
|
611
612
|
expect(doc.jsonSchema.params).toBeDefined();
|
|
612
|
-
expect(doc.jsonSchema.yieldType).toBeDefined();
|
|
613
613
|
expect(doc.jsonSchema.returnType).toBeDefined();
|
|
614
|
+
// yieldType is nested under SSE envelope's data property
|
|
615
|
+
const yt = doc.jsonSchema.yieldType;
|
|
616
|
+
expect(yt.description).toBe('SSE message envelope. The data field contains the procedure yield value.');
|
|
617
|
+
expect(yt.required).toEqual(['data', 'event', 'id']);
|
|
618
|
+
expect(yt.properties.event).toEqual({ type: 'string' });
|
|
619
|
+
expect(yt.properties.id).toEqual({ type: 'string' });
|
|
620
|
+
expect(yt.properties.retry).toEqual({ type: 'number' });
|
|
621
|
+
// Developer's yieldType is nested under data
|
|
622
|
+
expect(yt.properties.data.properties.message).toBeDefined();
|
|
623
|
+
});
|
|
624
|
+
test('SSE mode generates SSE envelope even when no yieldType is defined', () => {
|
|
625
|
+
const builder = new HonoStreamAppBuilder();
|
|
626
|
+
const RPC = Procedures();
|
|
627
|
+
RPC.CreateStream('NoYield', { scope: 'test', version: 1 }, async function* () {
|
|
628
|
+
yield {};
|
|
629
|
+
});
|
|
630
|
+
builder.register(RPC, () => ({}));
|
|
631
|
+
builder.build();
|
|
632
|
+
const yt = builder.docs[0].jsonSchema.yieldType;
|
|
633
|
+
expect(yt).toBeDefined();
|
|
634
|
+
expect(yt.type).toBe('object');
|
|
635
|
+
expect(yt.description).toBe('SSE message envelope. The data field contains the procedure yield value.');
|
|
636
|
+
expect(yt.required).toEqual(['data', 'event', 'id']);
|
|
637
|
+
// data is empty schema when no yieldType defined
|
|
638
|
+
expect(yt.properties.data).toEqual({});
|
|
639
|
+
expect(yt.properties.event).toEqual({ type: 'string' });
|
|
640
|
+
expect(yt.properties.id).toEqual({ type: 'string' });
|
|
641
|
+
expect(yt.properties.retry).toEqual({ type: 'number' });
|
|
642
|
+
});
|
|
643
|
+
test('text mode passes yieldType through as-is', () => {
|
|
644
|
+
const yieldSchema = v.object({ chunk: v.string() });
|
|
645
|
+
const builder = new HonoStreamAppBuilder({ defaultStreamMode: 'text' });
|
|
646
|
+
const RPC = Procedures();
|
|
647
|
+
RPC.CreateStream('TextStream', { scope: 'test', version: 1, schema: { yieldType: yieldSchema } }, async function* () {
|
|
648
|
+
yield { chunk: 'hi' };
|
|
649
|
+
});
|
|
650
|
+
builder.register(RPC, () => ({}));
|
|
651
|
+
builder.build();
|
|
652
|
+
const yt = builder.docs[0].jsonSchema.yieldType;
|
|
653
|
+
expect(yt).toBeDefined();
|
|
654
|
+
// Text mode should NOT have SSE envelope fields injected
|
|
655
|
+
expect(yt.properties?.event).toBeUndefined();
|
|
656
|
+
expect(yt.properties?.id).toBeUndefined();
|
|
657
|
+
expect(yt.properties?.retry).toBeUndefined();
|
|
658
|
+
});
|
|
659
|
+
test('yieldType with id property does not collide with SSE id field', () => {
|
|
660
|
+
// User's yieldType has an `id` field (number) — this should be nested under
|
|
661
|
+
// the SSE envelope's `data` property, not collide with the SSE `id` (string)
|
|
662
|
+
const yieldSchema = v.object({
|
|
663
|
+
id: v.number(),
|
|
664
|
+
message: v.string(),
|
|
665
|
+
});
|
|
666
|
+
const builder = new HonoStreamAppBuilder();
|
|
667
|
+
const RPC = Procedures();
|
|
668
|
+
RPC.CreateStream('Notifications', { scope: 'test', version: 1, schema: { yieldType: yieldSchema } }, async function* () {
|
|
669
|
+
yield { id: 42, message: 'hello' };
|
|
670
|
+
});
|
|
671
|
+
builder.register(RPC, () => ({}));
|
|
672
|
+
builder.build();
|
|
673
|
+
const yt = builder.docs[0].jsonSchema.yieldType;
|
|
674
|
+
expect(yt.required).toEqual(['data', 'event', 'id']);
|
|
675
|
+
// SSE envelope id is a string
|
|
676
|
+
expect(yt.properties.id).toEqual({ type: 'string' });
|
|
677
|
+
// User's id (number) is safely nested under data
|
|
678
|
+
expect(yt.properties.data.properties.id.type).toBe('number');
|
|
679
|
+
expect(yt.properties.data.properties.message.type).toBe('string');
|
|
614
680
|
});
|
|
615
681
|
test('streamMode is recorded in docs', () => {
|
|
616
682
|
const builder = new HonoStreamAppBuilder({ defaultStreamMode: 'text' });
|
|
@@ -646,7 +712,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
646
712
|
}));
|
|
647
713
|
// Streaming procedure (should be registered)
|
|
648
714
|
RPC.CreateStream('Stream', { scope: 'stream', version: 1 }, async function* () {
|
|
649
|
-
yield {
|
|
715
|
+
yield { ok: true };
|
|
650
716
|
});
|
|
651
717
|
builder.register(RPC, () => ({}));
|
|
652
718
|
const app = builder.build();
|
|
@@ -669,10 +735,10 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
669
735
|
const builder = new HonoStreamAppBuilder();
|
|
670
736
|
const RPC = Procedures();
|
|
671
737
|
RPC.CreateStream('StreamEvents', { scope: 'events', version: 1 }, async function* () {
|
|
672
|
-
yield {
|
|
738
|
+
yield {};
|
|
673
739
|
});
|
|
674
740
|
builder.register(RPC, () => ({}), {
|
|
675
|
-
extendProcedureDoc: ({
|
|
741
|
+
extendProcedureDoc: ({ procedure }) => ({
|
|
676
742
|
summary: `Stream events endpoint`,
|
|
677
743
|
tags: ['events'],
|
|
678
744
|
operationId: procedure.name,
|
|
@@ -688,7 +754,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
688
754
|
const builder = new HonoStreamAppBuilder();
|
|
689
755
|
const RPC = Procedures();
|
|
690
756
|
RPC.CreateStream('Test', { scope: 'test', version: 1 }, async function* () {
|
|
691
|
-
yield {
|
|
757
|
+
yield {};
|
|
692
758
|
});
|
|
693
759
|
builder.register(RPC, () => ({}), {
|
|
694
760
|
extendProcedureDoc: () => ({
|
|
@@ -738,7 +804,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
738
804
|
const SSERPC = Procedures();
|
|
739
805
|
const TextRPC = Procedures();
|
|
740
806
|
SSERPC.CreateStream('SSEStream', { scope: 'sse', version: 1 }, async function* () {
|
|
741
|
-
yield {
|
|
807
|
+
yield { mode: 'sse' };
|
|
742
808
|
});
|
|
743
809
|
TextRPC.CreateStream('TextStream', { scope: 'text', version: 1 }, async function* () {
|
|
744
810
|
yield { mode: 'text' };
|
|
@@ -789,7 +855,7 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
789
855
|
const RPC = Procedures();
|
|
790
856
|
RPC.CreateStream('CheckPrevalidated', { scope: 'check', version: 1 }, async function* (ctx) {
|
|
791
857
|
receivedIsPrevalidated = ctx.isPrevalidated;
|
|
792
|
-
yield {
|
|
858
|
+
yield { ok: true };
|
|
793
859
|
});
|
|
794
860
|
builder.register(RPC, () => ({}));
|
|
795
861
|
const app = builder.build();
|
|
@@ -852,12 +918,12 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
852
918
|
// SSE Yield Shape Tests
|
|
853
919
|
// --------------------------------------------------------------------------
|
|
854
920
|
describe('SSE yield shape', () => {
|
|
855
|
-
test('custom event names
|
|
921
|
+
test('custom event names via sse() helper', async () => {
|
|
856
922
|
const builder = new HonoStreamAppBuilder();
|
|
857
923
|
const RPC = Procedures();
|
|
858
924
|
RPC.CreateStream('Events', { scope: 'events', version: 1 }, async function* () {
|
|
859
|
-
yield {
|
|
860
|
-
yield {
|
|
925
|
+
yield sse({ type: 'user_joined' }, { event: 'join' });
|
|
926
|
+
yield sse({ type: 'message' }, { event: 'chat' });
|
|
861
927
|
});
|
|
862
928
|
builder.register(RPC, () => ({}));
|
|
863
929
|
const app = builder.build();
|
|
@@ -867,12 +933,12 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
867
933
|
expect(text).toContain('event: chat');
|
|
868
934
|
expect(text).not.toContain('event: Events');
|
|
869
935
|
});
|
|
870
|
-
test('custom id
|
|
936
|
+
test('custom id via sse() helper', async () => {
|
|
871
937
|
const builder = new HonoStreamAppBuilder();
|
|
872
938
|
const RPC = Procedures();
|
|
873
939
|
RPC.CreateStream('Events', { scope: 'events', version: 1 }, async function* () {
|
|
874
|
-
yield {
|
|
875
|
-
yield {
|
|
940
|
+
yield sse({ msg: 'first' }, { id: 'msg-001' });
|
|
941
|
+
yield sse({ msg: 'second' }, { id: 'msg-002' });
|
|
876
942
|
});
|
|
877
943
|
builder.register(RPC, () => ({}));
|
|
878
944
|
const app = builder.build();
|
|
@@ -885,8 +951,8 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
885
951
|
const builder = new HonoStreamAppBuilder();
|
|
886
952
|
const RPC = Procedures();
|
|
887
953
|
RPC.CreateStream('Events', { scope: 'events', version: 1 }, async function* () {
|
|
888
|
-
yield
|
|
889
|
-
yield {
|
|
954
|
+
yield 'already a string';
|
|
955
|
+
yield { needs: 'stringify' };
|
|
890
956
|
});
|
|
891
957
|
builder.register(RPC, () => ({}));
|
|
892
958
|
const app = builder.build();
|
|
@@ -901,9 +967,9 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
901
967
|
const builder = new HonoStreamAppBuilder();
|
|
902
968
|
const RPC = Procedures();
|
|
903
969
|
RPC.CreateStream('MyProcedure', { scope: 'test', version: 1 }, async function* () {
|
|
904
|
-
yield {
|
|
905
|
-
yield {
|
|
906
|
-
yield {
|
|
970
|
+
yield { value: 1 };
|
|
971
|
+
yield sse({ value: 2 }, { event: 'custom' });
|
|
972
|
+
yield { value: 3 };
|
|
907
973
|
});
|
|
908
974
|
builder.register(RPC, () => ({}));
|
|
909
975
|
const app = builder.build();
|
|
@@ -920,6 +986,84 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
920
986
|
});
|
|
921
987
|
});
|
|
922
988
|
// --------------------------------------------------------------------------
|
|
989
|
+
// sse() Helper Tests
|
|
990
|
+
// --------------------------------------------------------------------------
|
|
991
|
+
describe('sse() helper', () => {
|
|
992
|
+
test('tagged yields with custom event/id/retry', async () => {
|
|
993
|
+
const builder = new HonoStreamAppBuilder();
|
|
994
|
+
const RPC = Procedures();
|
|
995
|
+
RPC.CreateStream('Tagged', { scope: 'tagged', version: 1 }, async function* () {
|
|
996
|
+
yield sse({ count: 1 }, { event: 'tick', id: 'evt-1', retry: 5000 });
|
|
997
|
+
});
|
|
998
|
+
builder.register(RPC, () => ({}));
|
|
999
|
+
const app = builder.build();
|
|
1000
|
+
const res = await app.request('/tagged/tagged/1');
|
|
1001
|
+
const text = await res.text();
|
|
1002
|
+
expect(text).toContain('event: tick');
|
|
1003
|
+
expect(text).toContain('id: evt-1');
|
|
1004
|
+
expect(text).toContain('retry: 5000');
|
|
1005
|
+
expect(text).toContain('data: {"count":1}');
|
|
1006
|
+
});
|
|
1007
|
+
test('plain domain objects use procedure name and auto-incremented id', async () => {
|
|
1008
|
+
const builder = new HonoStreamAppBuilder();
|
|
1009
|
+
const RPC = Procedures();
|
|
1010
|
+
RPC.CreateStream('Plain', { scope: 'plain', version: 1 }, async function* () {
|
|
1011
|
+
yield { a: 1 };
|
|
1012
|
+
yield { a: 2 };
|
|
1013
|
+
});
|
|
1014
|
+
builder.register(RPC, () => ({}));
|
|
1015
|
+
const app = builder.build();
|
|
1016
|
+
const res = await app.request('/plain/plain/1');
|
|
1017
|
+
const text = await res.text();
|
|
1018
|
+
const messages = text.split('\n\n').filter(Boolean);
|
|
1019
|
+
expect(messages[0]).toContain('event: Plain');
|
|
1020
|
+
expect(messages[0]).toContain('id: 0');
|
|
1021
|
+
expect(messages[0]).toContain('data: {"a":1}');
|
|
1022
|
+
expect(messages[1]).toContain('event: Plain');
|
|
1023
|
+
expect(messages[1]).toContain('id: 1');
|
|
1024
|
+
expect(messages[1]).toContain('data: {"a":2}');
|
|
1025
|
+
});
|
|
1026
|
+
test('sse() metadata is invisible in text mode', async () => {
|
|
1027
|
+
const builder = new HonoStreamAppBuilder({ defaultStreamMode: 'text' });
|
|
1028
|
+
const RPC = Procedures();
|
|
1029
|
+
RPC.CreateStream('TextTagged', { scope: 'text', version: 1 }, async function* () {
|
|
1030
|
+
yield sse({ count: 1 }, { event: 'tick' });
|
|
1031
|
+
yield { count: 2 };
|
|
1032
|
+
});
|
|
1033
|
+
builder.register(RPC, () => ({}));
|
|
1034
|
+
const app = builder.build();
|
|
1035
|
+
const res = await app.request('/text/text-tagged/1');
|
|
1036
|
+
const text = await res.text();
|
|
1037
|
+
const lines = text.trim().split('\n');
|
|
1038
|
+
// Text mode just JSON-stringifies — sse() metadata is not visible
|
|
1039
|
+
expect(JSON.parse(lines[0])).toEqual({ count: 1 });
|
|
1040
|
+
expect(JSON.parse(lines[1])).toEqual({ count: 2 });
|
|
1041
|
+
});
|
|
1042
|
+
test('sse() with partial options', async () => {
|
|
1043
|
+
const builder = new HonoStreamAppBuilder();
|
|
1044
|
+
const RPC = Procedures();
|
|
1045
|
+
RPC.CreateStream('Partial', { scope: 'partial', version: 1 }, async function* () {
|
|
1046
|
+
yield sse({ v: 1 }, { event: 'custom' });
|
|
1047
|
+
yield sse({ v: 2 }, { id: 'my-id' });
|
|
1048
|
+
yield sse({ v: 3 });
|
|
1049
|
+
});
|
|
1050
|
+
builder.register(RPC, () => ({}));
|
|
1051
|
+
const app = builder.build();
|
|
1052
|
+
const res = await app.request('/partial/partial/1');
|
|
1053
|
+
const text = await res.text();
|
|
1054
|
+
const messages = text.split('\n\n').filter(Boolean);
|
|
1055
|
+
// First: custom event, auto id
|
|
1056
|
+
expect(messages[0]).toContain('event: custom');
|
|
1057
|
+
expect(messages[0]).toContain('id: 0');
|
|
1058
|
+
// Second: default event, custom id
|
|
1059
|
+
expect(messages[1]).toContain('event: Partial');
|
|
1060
|
+
expect(messages[1]).toContain('id: my-id');
|
|
1061
|
+
// Third: sse() with no options — same as plain object (defaults)
|
|
1062
|
+
expect(messages[2]).toContain('event: Partial');
|
|
1063
|
+
expect(messages[2]).toContain('id: 2');
|
|
1064
|
+
});
|
|
1065
|
+
});
|
|
1066
|
+
// --------------------------------------------------------------------------
|
|
923
1067
|
// Integration Test
|
|
924
1068
|
// --------------------------------------------------------------------------
|
|
925
1069
|
describe('integration', () => {
|
|
@@ -947,8 +1091,8 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
947
1091
|
defaultStreamMode: 'text',
|
|
948
1092
|
onRequestStart: () => events.push('request-start'),
|
|
949
1093
|
onRequestEnd: () => events.push('request-end'),
|
|
950
|
-
onStreamStart: (
|
|
951
|
-
onStreamEnd: (
|
|
1094
|
+
onStreamStart: () => events.push('stream-start'),
|
|
1095
|
+
onStreamEnd: () => events.push('stream-end'),
|
|
952
1096
|
});
|
|
953
1097
|
builder.register(RPC, (c) => ({
|
|
954
1098
|
userId: c.req.header('x-user-id') || 'anonymous',
|
|
@@ -970,8 +1114,8 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
970
1114
|
expect(JSON.parse(lines[1])).toEqual({ id: 2, message: 'Notification 2 for user-123' });
|
|
971
1115
|
// Verify hooks were called
|
|
972
1116
|
expect(events).toContain('request-start');
|
|
973
|
-
expect(events).toContain('stream-start
|
|
974
|
-
expect(events).toContain('stream-end
|
|
1117
|
+
expect(events).toContain('stream-start');
|
|
1118
|
+
expect(events).toContain('stream-end');
|
|
975
1119
|
expect(events).toContain('request-end');
|
|
976
1120
|
});
|
|
977
1121
|
});
|