ts-procedures 3.3.3 → 3.4.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/README.md +29 -0
- package/build/errors.js +5 -1
- package/build/errors.js.map +1 -1
- package/build/implementations/http/hono-stream/index.d.ts +21 -2
- package/build/implementations/http/hono-stream/index.js +49 -19
- package/build/implementations/http/hono-stream/index.js.map +1 -1
- package/build/implementations/http/hono-stream/index.test.js +234 -7
- package/build/implementations/http/hono-stream/index.test.js.map +1 -1
- package/build/index.d.ts +6 -2
- package/build/index.js +16 -9
- package/build/index.js.map +1 -1
- package/build/index.test.js +122 -0
- package/build/index.test.js.map +1 -1
- package/package.json +1 -1
- package/src/errors.ts +5 -1
- package/src/implementations/http/README.md +2 -1
- package/src/implementations/http/hono-stream/README.md +139 -19
- package/src/implementations/http/hono-stream/index.test.ts +438 -259
- package/src/implementations/http/hono-stream/index.ts +89 -28
- package/src/index.test.ts +166 -0
- package/src/index.ts +51 -22
package/README.md
CHANGED
|
@@ -265,6 +265,35 @@ AJV is configured with:
|
|
|
265
265
|
|
|
266
266
|
**Note:** `schema.params` is validated at runtime. `schema.returnType` is for documentation/introspection only.
|
|
267
267
|
|
|
268
|
+
### Skipping Validation with isPrevalidated
|
|
269
|
+
|
|
270
|
+
When building framework integrations that validate params before calling procedure handlers, you can pass `isPrevalidated: true` in the context to skip duplicate validation:
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
// Framework integration example
|
|
274
|
+
app.post('/rpc/:name', async (req, res) => {
|
|
275
|
+
const procedure = getProcedure(req.params.name)
|
|
276
|
+
|
|
277
|
+
// Validate params at the framework level
|
|
278
|
+
const { errors } = procedure.config.validation?.params?.(req.body)
|
|
279
|
+
if (errors) {
|
|
280
|
+
return res.status(400).json({ errors })
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Call handler with isPrevalidated to skip redundant validation
|
|
284
|
+
const result = await procedure.handler(
|
|
285
|
+
{ ...context, isPrevalidated: true },
|
|
286
|
+
req.body
|
|
287
|
+
)
|
|
288
|
+
res.json(result)
|
|
289
|
+
})
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
This is useful for:
|
|
293
|
+
- Framework integrations (like `HonoStreamAppBuilder`) that validate before starting streams
|
|
294
|
+
- Custom middleware that performs early validation for better error responses
|
|
295
|
+
- Performance optimization when validation has already occurred
|
|
296
|
+
|
|
268
297
|
## Streaming Procedures
|
|
269
298
|
|
|
270
299
|
Streaming procedures use async generators to yield values over time, enabling SSE (Server-Sent Events), HTTP streaming, and real-time data feeds.
|
package/build/errors.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { formatDefinitionInfo } from './stack-utils.js';
|
|
2
|
+
import { kebabCase } from 'es-toolkit/string';
|
|
2
3
|
export class ProcedureError extends Error {
|
|
3
4
|
procedureName;
|
|
4
5
|
message;
|
|
@@ -50,7 +51,10 @@ export class ProcedureValidationError extends ProcedureError {
|
|
|
50
51
|
constructor(procedureName, message, errors,
|
|
51
52
|
// Used for error stack trace details
|
|
52
53
|
definitionInfo) {
|
|
53
|
-
|
|
54
|
+
const readableErrors = errors
|
|
55
|
+
?.map((err) => `- ${kebabCase(err.instancePath).replace('-', '.')} ${err.message}`)
|
|
56
|
+
.join(', ');
|
|
57
|
+
super(procedureName, message + ' ' + readableErrors, { errors }, definitionInfo);
|
|
54
58
|
this.procedureName = procedureName;
|
|
55
59
|
this.errors = errors;
|
|
56
60
|
this.name = 'ProcedureValidationError';
|
package/build/errors.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AACA,OAAO,EAAsC,oBAAoB,EAAE,MAAM,kBAAkB,CAAA;
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,6 +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
|
+
/**
|
|
7
|
+
* Result from onMidStreamError callback.
|
|
8
|
+
* @property value - The value to write to the stream (should match yieldType schema)
|
|
9
|
+
* @property closeStream - Whether to close the stream after writing (defaults to true)
|
|
10
|
+
*/
|
|
11
|
+
export type MidStreamErrorResult = {
|
|
12
|
+
value: unknown;
|
|
13
|
+
closeStream?: boolean;
|
|
14
|
+
};
|
|
6
15
|
export type HonoStreamAppBuilderConfig = {
|
|
7
16
|
/**
|
|
8
17
|
* An existing Hono application instance to use.
|
|
@@ -18,9 +27,19 @@ export type HonoStreamAppBuilderConfig = {
|
|
|
18
27
|
onStreamStart?: (procedure: TStreamProcedureRegistration, c: Context) => void;
|
|
19
28
|
onStreamEnd?: (procedure: TStreamProcedureRegistration, c: Context) => void;
|
|
20
29
|
/**
|
|
21
|
-
*
|
|
30
|
+
* Called for errors BEFORE streaming starts (validation, auth, context resolution).
|
|
31
|
+
* Return value IS used as the HTTP response.
|
|
32
|
+
*/
|
|
33
|
+
onPreStreamError?: (procedure: TStreamProcedureRegistration, c: Context, error: Error) => Response | Promise<Response>;
|
|
34
|
+
/**
|
|
35
|
+
* Called for errors DURING streaming (generator throws).
|
|
36
|
+
* Return value is written to the stream as a yield.
|
|
37
|
+
* Should return a value matching your yieldType schema (e.g., error variant of a union).
|
|
38
|
+
* Return undefined to use default behavior (writes { error: message }).
|
|
39
|
+
*
|
|
40
|
+
* @returns { value, closeStream? } - value to yield, whether to close after (default true)
|
|
22
41
|
*/
|
|
23
|
-
|
|
42
|
+
onMidStreamError?: (procedure: TStreamProcedureRegistration, c: Context, error: Error) => MidStreamErrorResult | undefined;
|
|
24
43
|
};
|
|
25
44
|
/**
|
|
26
45
|
* Builder class for creating a Hono application with streaming RPC routes.
|
|
@@ -2,6 +2,7 @@ import { Hono } from 'hono';
|
|
|
2
2
|
import { streamSSE, streamText } from 'hono/streaming';
|
|
3
3
|
import { kebabCase } from 'es-toolkit/string';
|
|
4
4
|
import { castArray } from 'es-toolkit/compat';
|
|
5
|
+
import { ProcedureValidationError } from '../../../errors.js';
|
|
5
6
|
/**
|
|
6
7
|
* Builder class for creating a Hono application with streaming RPC routes.
|
|
7
8
|
*
|
|
@@ -31,12 +32,6 @@ export class HonoStreamAppBuilder {
|
|
|
31
32
|
await next();
|
|
32
33
|
});
|
|
33
34
|
}
|
|
34
|
-
if (config?.onRequestEnd) {
|
|
35
|
-
this._app.use('*', async (c, next) => {
|
|
36
|
-
await next();
|
|
37
|
-
config.onRequestEnd(c);
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
35
|
}
|
|
41
36
|
/**
|
|
42
37
|
* Generates the stream route path based on the RPC configuration.
|
|
@@ -83,13 +78,23 @@ export class HonoStreamAppBuilder {
|
|
|
83
78
|
createStreamHandler(procedure, factoryContext, streamMode) {
|
|
84
79
|
return async (c) => {
|
|
85
80
|
try {
|
|
86
|
-
const context = typeof factoryContext === 'function'
|
|
87
|
-
? await factoryContext(c)
|
|
88
|
-
: factoryContext;
|
|
81
|
+
const context = typeof factoryContext === 'function' ? await factoryContext(c) : factoryContext;
|
|
89
82
|
// GET: query params, POST: JSON body
|
|
90
83
|
const params = c.req.method === 'GET'
|
|
91
84
|
? Object.fromEntries(new URL(c.req.url).searchParams)
|
|
92
85
|
: await c.req.json().catch(() => ({}));
|
|
86
|
+
// Validate params BEFORE starting the stream
|
|
87
|
+
if (procedure.config.validation?.params) {
|
|
88
|
+
const { errors } = procedure.config.validation.params(params);
|
|
89
|
+
if (errors) {
|
|
90
|
+
const error = new ProcedureValidationError(procedure.name, `Validation error for ${procedure.name}`, errors);
|
|
91
|
+
// Use onPreStreamError if provided
|
|
92
|
+
if (this.config?.onPreStreamError) {
|
|
93
|
+
return this.config.onPreStreamError(procedure, c, error);
|
|
94
|
+
}
|
|
95
|
+
return c.json({ error: error.message }, 400);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
93
98
|
if (this.config?.onStreamStart) {
|
|
94
99
|
this.config.onStreamStart(procedure, c);
|
|
95
100
|
}
|
|
@@ -101,8 +106,9 @@ export class HonoStreamAppBuilder {
|
|
|
101
106
|
}
|
|
102
107
|
}
|
|
103
108
|
catch (error) {
|
|
104
|
-
|
|
105
|
-
|
|
109
|
+
// Use onPreStreamError for context resolution errors
|
|
110
|
+
if (this.config?.onPreStreamError) {
|
|
111
|
+
return this.config.onPreStreamError(procedure, c, error);
|
|
106
112
|
}
|
|
107
113
|
return c.json({ error: error.message }, 500);
|
|
108
114
|
}
|
|
@@ -113,12 +119,13 @@ export class HonoStreamAppBuilder {
|
|
|
113
119
|
*/
|
|
114
120
|
handleSSEStream(procedure, context, params, c) {
|
|
115
121
|
return streamSSE(c, async (stream) => {
|
|
116
|
-
|
|
122
|
+
// Pass isPrevalidated: true since we already validated params in createStreamHandler
|
|
123
|
+
const generator = procedure.handler({ ...context, isPrevalidated: true }, params);
|
|
117
124
|
stream.onAbort(async () => {
|
|
118
125
|
await generator.return(undefined);
|
|
119
126
|
});
|
|
127
|
+
let eventId = 0;
|
|
120
128
|
try {
|
|
121
|
-
let eventId = 0;
|
|
122
129
|
for await (const value of generator) {
|
|
123
130
|
await stream.writeSSE({
|
|
124
131
|
data: JSON.stringify(value),
|
|
@@ -128,16 +135,29 @@ export class HonoStreamAppBuilder {
|
|
|
128
135
|
}
|
|
129
136
|
}
|
|
130
137
|
catch (error) {
|
|
131
|
-
//
|
|
138
|
+
// Get error yield value from callback (onMidStreamError)
|
|
139
|
+
let errorResult;
|
|
140
|
+
if (this.config?.onMidStreamError) {
|
|
141
|
+
errorResult = this.config.onMidStreamError(procedure, c, error);
|
|
142
|
+
}
|
|
143
|
+
// Write error value to stream
|
|
144
|
+
const errorValue = errorResult?.value ?? { error: error.message };
|
|
132
145
|
await stream.writeSSE({
|
|
133
|
-
data: JSON.stringify(
|
|
134
|
-
event
|
|
146
|
+
data: JSON.stringify(errorValue),
|
|
147
|
+
// Use procedure event name if custom value provided, otherwise 'error'
|
|
148
|
+
event: errorResult?.value !== undefined ? procedure.name : 'error',
|
|
149
|
+
id: String(eventId++),
|
|
135
150
|
});
|
|
151
|
+
// closeStream defaults to true if not specified
|
|
152
|
+
// (stream closes naturally after this handler completes)
|
|
136
153
|
}
|
|
137
154
|
finally {
|
|
138
155
|
if (this.config?.onStreamEnd) {
|
|
139
156
|
this.config.onStreamEnd(procedure, c);
|
|
140
157
|
}
|
|
158
|
+
if (this.config?.onRequestEnd) {
|
|
159
|
+
this.config.onRequestEnd(c);
|
|
160
|
+
}
|
|
141
161
|
}
|
|
142
162
|
});
|
|
143
163
|
}
|
|
@@ -146,7 +166,8 @@ export class HonoStreamAppBuilder {
|
|
|
146
166
|
*/
|
|
147
167
|
handleTextStream(procedure, context, params, c) {
|
|
148
168
|
return streamText(c, async (stream) => {
|
|
149
|
-
|
|
169
|
+
// Pass isPrevalidated: true since we already validated params in createStreamHandler
|
|
170
|
+
const generator = procedure.handler({ ...context, isPrevalidated: true }, params);
|
|
150
171
|
stream.onAbort(async () => {
|
|
151
172
|
await generator.return(undefined);
|
|
152
173
|
});
|
|
@@ -156,13 +177,22 @@ export class HonoStreamAppBuilder {
|
|
|
156
177
|
}
|
|
157
178
|
}
|
|
158
179
|
catch (error) {
|
|
159
|
-
//
|
|
160
|
-
|
|
180
|
+
// Get error yield value from callback (onMidStreamError)
|
|
181
|
+
let errorResult;
|
|
182
|
+
if (this.config?.onMidStreamError) {
|
|
183
|
+
errorResult = this.config.onMidStreamError(procedure, c, error);
|
|
184
|
+
}
|
|
185
|
+
// Write error value to stream
|
|
186
|
+
const errorValue = errorResult?.value ?? { error: error.message };
|
|
187
|
+
await stream.writeln(JSON.stringify(errorValue));
|
|
161
188
|
}
|
|
162
189
|
finally {
|
|
163
190
|
if (this.config?.onStreamEnd) {
|
|
164
191
|
this.config.onStreamEnd(procedure, c);
|
|
165
192
|
}
|
|
193
|
+
if (this.config?.onRequestEnd) {
|
|
194
|
+
this.config.onRequestEnd(c);
|
|
195
|
+
}
|
|
166
196
|
}
|
|
167
197
|
});
|
|
168
198
|
}
|
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/implementations/http/hono-stream/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -268,7 +268,8 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
268
268
|
});
|
|
269
269
|
builder.register(RPC, () => ({}));
|
|
270
270
|
const app = builder.build();
|
|
271
|
-
await app.request('/test/test/1');
|
|
271
|
+
const response = await app.request('/test/test/1');
|
|
272
|
+
await response.text(); // Consume stream to trigger onRequestEnd
|
|
272
273
|
expect(onRequestEnd).toHaveBeenCalledTimes(1);
|
|
273
274
|
expect(onRequestEnd.mock.calls[0][0]).toHaveProperty('req');
|
|
274
275
|
});
|
|
@@ -329,6 +330,8 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
329
330
|
expect(order[0]).toBe('request-start');
|
|
330
331
|
// stream-start should be before handler
|
|
331
332
|
expect(order.indexOf('stream-start')).toBeLessThan(order.indexOf('handler'));
|
|
333
|
+
// request-end should be last
|
|
334
|
+
expect(order[order.length - 1]).toBe('request-end');
|
|
332
335
|
});
|
|
333
336
|
});
|
|
334
337
|
// --------------------------------------------------------------------------
|
|
@@ -339,15 +342,26 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
339
342
|
const errorHandler = vi.fn((procedure, c, error) => {
|
|
340
343
|
return c.json({ customError: error.message }, 400);
|
|
341
344
|
});
|
|
342
|
-
const builder = new HonoStreamAppBuilder({
|
|
345
|
+
const builder = new HonoStreamAppBuilder({ onPreStreamError: errorHandler });
|
|
343
346
|
const RPC = Procedures();
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
+
RPC.CreateStream('ValidatedStream', {
|
|
348
|
+
scope: 'validated',
|
|
349
|
+
version: 1,
|
|
350
|
+
schema: {
|
|
351
|
+
params: v.object({ count: v.number() }),
|
|
352
|
+
},
|
|
353
|
+
}, async function* (ctx, params) {
|
|
354
|
+
yield { count: params.count };
|
|
347
355
|
});
|
|
356
|
+
builder.register(RPC, () => ({}));
|
|
348
357
|
const app = builder.build();
|
|
349
|
-
|
|
350
|
-
|
|
358
|
+
const res = await app.request('/validated/validated-stream/1?count=not-a-number');
|
|
359
|
+
expect(res.status).toBe(400);
|
|
360
|
+
const body = await res.json();
|
|
361
|
+
expect(body.customError).toContain('Validation error');
|
|
362
|
+
expect(errorHandler).toHaveBeenCalledTimes(1);
|
|
363
|
+
expect(errorHandler.mock.calls[0][0].name).toBe('ValidatedStream');
|
|
364
|
+
expect(errorHandler.mock.calls[0][2].message).toContain('Validation error');
|
|
351
365
|
});
|
|
352
366
|
test('errors during streaming are sent as error events (SSE mode)', async () => {
|
|
353
367
|
const builder = new HonoStreamAppBuilder();
|
|
@@ -380,6 +394,150 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
380
394
|
// Error is wrapped by Procedures with "Error in streaming handler for {name}" prefix
|
|
381
395
|
expect(JSON.parse(lines[1]).error).toContain('Stream error');
|
|
382
396
|
});
|
|
397
|
+
test('validation errors return 400 by default when no error handler', async () => {
|
|
398
|
+
const builder = new HonoStreamAppBuilder();
|
|
399
|
+
const RPC = Procedures();
|
|
400
|
+
RPC.CreateStream('ValidatedStream', {
|
|
401
|
+
scope: 'validated',
|
|
402
|
+
version: 1,
|
|
403
|
+
schema: {
|
|
404
|
+
params: v.object({ count: v.number() }),
|
|
405
|
+
},
|
|
406
|
+
}, async function* (ctx, params) {
|
|
407
|
+
yield { count: params.count };
|
|
408
|
+
});
|
|
409
|
+
builder.register(RPC, () => ({}));
|
|
410
|
+
const app = builder.build();
|
|
411
|
+
const res = await app.request('/validated/validated-stream/1?count=not-a-number');
|
|
412
|
+
// Default: returns 400 JSON error
|
|
413
|
+
expect(res.status).toBe(400);
|
|
414
|
+
const body = await res.json();
|
|
415
|
+
expect(body.error).toContain('Validation error');
|
|
416
|
+
});
|
|
417
|
+
// Tests for onPreStreamError and onMidStreamError callbacks
|
|
418
|
+
test('onPreStreamError handles validation errors with custom Response', async () => {
|
|
419
|
+
const onPreStreamError = vi.fn((procedure, c, error) => {
|
|
420
|
+
return c.json({ customError: true, procedureName: procedure.name, details: error.message }, 422);
|
|
421
|
+
});
|
|
422
|
+
const builder = new HonoStreamAppBuilder({ onPreStreamError });
|
|
423
|
+
const RPC = Procedures();
|
|
424
|
+
RPC.CreateStream('ValidatedStream', {
|
|
425
|
+
scope: 'validated',
|
|
426
|
+
version: 1,
|
|
427
|
+
schema: {
|
|
428
|
+
params: v.object({ count: v.number() }),
|
|
429
|
+
},
|
|
430
|
+
}, async function* (ctx, params) {
|
|
431
|
+
yield { count: params.count };
|
|
432
|
+
});
|
|
433
|
+
builder.register(RPC, () => ({}));
|
|
434
|
+
const app = builder.build();
|
|
435
|
+
const res = await app.request('/validated/validated-stream/1?count=not-a-number');
|
|
436
|
+
expect(res.status).toBe(422);
|
|
437
|
+
const body = await res.json();
|
|
438
|
+
expect(body.customError).toBe(true);
|
|
439
|
+
expect(body.procedureName).toBe('ValidatedStream');
|
|
440
|
+
expect(body.details).toContain('Validation error');
|
|
441
|
+
expect(onPreStreamError).toHaveBeenCalledTimes(1);
|
|
442
|
+
});
|
|
443
|
+
test('onPreStreamError handles context resolution errors', async () => {
|
|
444
|
+
const onPreStreamError = vi.fn((procedure, c, error) => {
|
|
445
|
+
return c.json({ contextError: error.message }, 401);
|
|
446
|
+
});
|
|
447
|
+
const builder = new HonoStreamAppBuilder({ onPreStreamError });
|
|
448
|
+
const RPC = Procedures();
|
|
449
|
+
RPC.CreateStream('SecureStream', { scope: 'secure', version: 1 }, async function* (ctx) {
|
|
450
|
+
yield { userId: ctx.userId };
|
|
451
|
+
});
|
|
452
|
+
builder.register(RPC, () => {
|
|
453
|
+
throw new Error('Authentication required');
|
|
454
|
+
});
|
|
455
|
+
const app = builder.build();
|
|
456
|
+
const res = await app.request('/secure/secure-stream/1');
|
|
457
|
+
expect(res.status).toBe(401);
|
|
458
|
+
const body = await res.json();
|
|
459
|
+
expect(body.contextError).toBe('Authentication required');
|
|
460
|
+
expect(onPreStreamError).toHaveBeenCalledTimes(1);
|
|
461
|
+
});
|
|
462
|
+
test('onMidStreamError returns custom value written to SSE stream', async () => {
|
|
463
|
+
const onMidStreamError = vi.fn((procedure, c, error) => {
|
|
464
|
+
return {
|
|
465
|
+
value: {
|
|
466
|
+
type: 'error',
|
|
467
|
+
code: 'STREAM_FAILED',
|
|
468
|
+
message: error.message,
|
|
469
|
+
retryable: false,
|
|
470
|
+
},
|
|
471
|
+
closeStream: true,
|
|
472
|
+
};
|
|
473
|
+
});
|
|
474
|
+
const builder = new HonoStreamAppBuilder({ onMidStreamError });
|
|
475
|
+
const RPC = Procedures();
|
|
476
|
+
RPC.CreateStream('ErrorStream', { scope: 'error', version: 1 }, async function* () {
|
|
477
|
+
yield { type: 'data', value: 1 };
|
|
478
|
+
throw new Error('Something broke');
|
|
479
|
+
});
|
|
480
|
+
builder.register(RPC, () => ({}));
|
|
481
|
+
const app = builder.build();
|
|
482
|
+
const res = await app.request('/error/error-stream/1');
|
|
483
|
+
const text = await res.text();
|
|
484
|
+
// First yield should be present
|
|
485
|
+
expect(text).toContain('data: {"type":"data","value":1}');
|
|
486
|
+
// Error should use custom format from onMidStreamError
|
|
487
|
+
expect(text).toContain('data: {"type":"error","code":"STREAM_FAILED"');
|
|
488
|
+
expect(text).toContain('"retryable":false');
|
|
489
|
+
// Event should use procedure name (not 'error') since custom value provided
|
|
490
|
+
expect(text).toContain('event: ErrorStream');
|
|
491
|
+
expect(onMidStreamError).toHaveBeenCalledTimes(1);
|
|
492
|
+
});
|
|
493
|
+
test('onMidStreamError returns custom value written to text stream', async () => {
|
|
494
|
+
const onMidStreamError = vi.fn((procedure, c, error) => {
|
|
495
|
+
return {
|
|
496
|
+
value: { type: 'error', message: error.message },
|
|
497
|
+
};
|
|
498
|
+
});
|
|
499
|
+
const builder = new HonoStreamAppBuilder({
|
|
500
|
+
defaultStreamMode: 'text',
|
|
501
|
+
onMidStreamError,
|
|
502
|
+
});
|
|
503
|
+
const RPC = Procedures();
|
|
504
|
+
RPC.CreateStream('ErrorStream', { scope: 'error', version: 1 }, async function* () {
|
|
505
|
+
yield { type: 'data', value: 'hello' };
|
|
506
|
+
throw new Error('Stream failed');
|
|
507
|
+
});
|
|
508
|
+
builder.register(RPC, () => ({}));
|
|
509
|
+
const app = builder.build();
|
|
510
|
+
const res = await app.request('/error/error-stream/1');
|
|
511
|
+
const text = await res.text();
|
|
512
|
+
const lines = text.trim().split('\n');
|
|
513
|
+
expect(JSON.parse(lines[0])).toEqual({ type: 'data', value: 'hello' });
|
|
514
|
+
// Error message may be wrapped by Procedures with "Error in streaming handler for X - " prefix
|
|
515
|
+
const errorLine = JSON.parse(lines[1]);
|
|
516
|
+
expect(errorLine.type).toBe('error');
|
|
517
|
+
expect(errorLine.message).toContain('Stream failed');
|
|
518
|
+
expect(onMidStreamError).toHaveBeenCalledTimes(1);
|
|
519
|
+
});
|
|
520
|
+
test('onMidStreamError returning undefined falls back to default error format', async () => {
|
|
521
|
+
const onMidStreamError = vi.fn(() => undefined);
|
|
522
|
+
const builder = new HonoStreamAppBuilder({
|
|
523
|
+
defaultStreamMode: 'text',
|
|
524
|
+
onMidStreamError,
|
|
525
|
+
});
|
|
526
|
+
const RPC = Procedures();
|
|
527
|
+
RPC.CreateStream('ErrorStream', { scope: 'error', version: 1 }, async function* () {
|
|
528
|
+
yield { value: 1 };
|
|
529
|
+
throw new Error('Fallback test');
|
|
530
|
+
});
|
|
531
|
+
builder.register(RPC, () => ({}));
|
|
532
|
+
const app = builder.build();
|
|
533
|
+
const res = await app.request('/error/error-stream/1');
|
|
534
|
+
const text = await res.text();
|
|
535
|
+
const lines = text.trim().split('\n');
|
|
536
|
+
expect(JSON.parse(lines[0])).toEqual({ value: 1 });
|
|
537
|
+
// Falls back to default { error: message } format
|
|
538
|
+
expect(JSON.parse(lines[1]).error).toContain('Fallback test');
|
|
539
|
+
expect(onMidStreamError).toHaveBeenCalledTimes(1);
|
|
540
|
+
});
|
|
383
541
|
});
|
|
384
542
|
// --------------------------------------------------------------------------
|
|
385
543
|
// Context Resolution Tests
|
|
@@ -622,6 +780,75 @@ describe('HonoStreamAppBuilder', () => {
|
|
|
622
780
|
});
|
|
623
781
|
});
|
|
624
782
|
// --------------------------------------------------------------------------
|
|
783
|
+
// isPrevalidated Tests
|
|
784
|
+
// --------------------------------------------------------------------------
|
|
785
|
+
describe('isPrevalidated context property', () => {
|
|
786
|
+
test('passes isPrevalidated: true to procedure handler to skip double validation', async () => {
|
|
787
|
+
let receivedIsPrevalidated;
|
|
788
|
+
const builder = new HonoStreamAppBuilder({ defaultStreamMode: 'text' });
|
|
789
|
+
const RPC = Procedures();
|
|
790
|
+
RPC.CreateStream('CheckPrevalidated', { scope: 'check', version: 1 }, async function* (ctx) {
|
|
791
|
+
receivedIsPrevalidated = ctx.isPrevalidated;
|
|
792
|
+
yield { ok: true };
|
|
793
|
+
});
|
|
794
|
+
builder.register(RPC, () => ({}));
|
|
795
|
+
const app = builder.build();
|
|
796
|
+
const res = await app.request('/check/check-prevalidated/1');
|
|
797
|
+
await res.text();
|
|
798
|
+
expect(receivedIsPrevalidated).toBe(true);
|
|
799
|
+
});
|
|
800
|
+
test('valid params work correctly with pre-validation flow', async () => {
|
|
801
|
+
const builder = new HonoStreamAppBuilder({ defaultStreamMode: 'text' });
|
|
802
|
+
const RPC = Procedures();
|
|
803
|
+
RPC.CreateStream('ValidParams', {
|
|
804
|
+
scope: 'valid',
|
|
805
|
+
version: 1,
|
|
806
|
+
schema: {
|
|
807
|
+
params: v.object({ count: v.number() }),
|
|
808
|
+
},
|
|
809
|
+
}, async function* (ctx, params) {
|
|
810
|
+
const count = params.count ?? 0;
|
|
811
|
+
for (let i = 0; i < count; i++) {
|
|
812
|
+
yield { index: i };
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
builder.register(RPC, () => ({}));
|
|
816
|
+
const app = builder.build();
|
|
817
|
+
// With valid params, both HonoStreamAppBuilder validation and procedure validation should work
|
|
818
|
+
const res = await app.request('/valid/valid-params/1?count=2');
|
|
819
|
+
expect(res.status).toBe(200);
|
|
820
|
+
const text = await res.text();
|
|
821
|
+
const lines = text.trim().split('\n');
|
|
822
|
+
expect(lines).toHaveLength(2);
|
|
823
|
+
expect(JSON.parse(lines[0])).toEqual({ index: 0 });
|
|
824
|
+
expect(JSON.parse(lines[1])).toEqual({ index: 1 });
|
|
825
|
+
});
|
|
826
|
+
test('invalid params are caught by HonoStreamAppBuilder before handler runs', async () => {
|
|
827
|
+
let handlerCalled = false;
|
|
828
|
+
const builder = new HonoStreamAppBuilder({ defaultStreamMode: 'text' });
|
|
829
|
+
const RPC = Procedures();
|
|
830
|
+
RPC.CreateStream('InvalidParams', {
|
|
831
|
+
scope: 'invalid',
|
|
832
|
+
version: 1,
|
|
833
|
+
schema: {
|
|
834
|
+
params: v.object({ count: v.number() }),
|
|
835
|
+
},
|
|
836
|
+
}, async function* () {
|
|
837
|
+
handlerCalled = true;
|
|
838
|
+
yield { ok: true };
|
|
839
|
+
});
|
|
840
|
+
builder.register(RPC, () => ({}));
|
|
841
|
+
const app = builder.build();
|
|
842
|
+
// With invalid params, HonoStreamAppBuilder catches the error before streaming starts
|
|
843
|
+
const res = await app.request('/invalid/invalid-params/1?count=not-a-number');
|
|
844
|
+
expect(res.status).toBe(400);
|
|
845
|
+
const body = await res.json();
|
|
846
|
+
expect(body.error).toContain('Validation error');
|
|
847
|
+
// Handler should never be called since validation fails before streaming
|
|
848
|
+
expect(handlerCalled).toBe(false);
|
|
849
|
+
});
|
|
850
|
+
});
|
|
851
|
+
// --------------------------------------------------------------------------
|
|
625
852
|
// Integration Test
|
|
626
853
|
// --------------------------------------------------------------------------
|
|
627
854
|
describe('integration', () => {
|