ts-typed-api 0.2.20 → 0.2.21
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/dist/handler.js +31 -22
- package/dist/hono-cloudflare-workers.js +3 -1
- package/dist/router.d.ts +1 -0
- package/examples/simple/definitions.ts +14 -0
- package/package.json +1 -1
- package/src/handler.ts +33 -22
- package/src/hono-cloudflare-workers.ts +3 -1
- package/src/router.ts +2 -0
- package/tests/setup.ts +19 -0
- package/tests/simple-api.test.ts +56 -0
package/dist/handler.js
CHANGED
|
@@ -343,28 +343,37 @@ middlewares) {
|
|
|
343
343
|
? routeDefinition.body.parse(expressReq.body)
|
|
344
344
|
: expressReq.body;
|
|
345
345
|
// Construct TypedRequest using TDef, currentDomain, currentRouteKey
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
346
|
+
// Create a new object that inherits from expressReq to preserve prototype methods like .on()
|
|
347
|
+
const finalTypedReq = Object.create(expressReq, {
|
|
348
|
+
// Core parsed/validated properties
|
|
349
|
+
params: { value: parsedParams, writable: true, enumerable: true, configurable: true },
|
|
350
|
+
query: { value: parsedQuery, writable: true, enumerable: true, configurable: true },
|
|
351
|
+
body: { value: parsedBody, writable: true, enumerable: true, configurable: true },
|
|
352
|
+
ctx: { value: expressReq.ctx, writable: true, enumerable: true, configurable: true },
|
|
353
|
+
// Unified API for client disconnection
|
|
354
|
+
onClose: {
|
|
355
|
+
value: (callback) => expressReq.on('close', callback),
|
|
356
|
+
writable: false,
|
|
357
|
+
enumerable: true,
|
|
358
|
+
configurable: false
|
|
359
|
+
},
|
|
360
|
+
// Restore original Express request properties for full compatibility
|
|
361
|
+
headers: { value: expressReq.headers, writable: false, enumerable: true, configurable: false },
|
|
362
|
+
cookies: { value: expressReq.cookies, writable: false, enumerable: true, configurable: false },
|
|
363
|
+
ip: { value: expressReq.ip, writable: false, enumerable: true, configurable: false },
|
|
364
|
+
ips: { value: expressReq.ips, writable: false, enumerable: true, configurable: false },
|
|
365
|
+
hostname: { value: expressReq.hostname, writable: false, enumerable: true, configurable: false },
|
|
366
|
+
protocol: { value: expressReq.protocol, writable: false, enumerable: true, configurable: false },
|
|
367
|
+
secure: { value: expressReq.secure, writable: false, enumerable: true, configurable: false },
|
|
368
|
+
xhr: { value: expressReq.xhr, writable: false, enumerable: true, configurable: false },
|
|
369
|
+
fresh: { value: expressReq.fresh, writable: false, enumerable: true, configurable: false },
|
|
370
|
+
stale: { value: expressReq.stale, writable: false, enumerable: true, configurable: false },
|
|
371
|
+
subdomains: { value: expressReq.subdomains, writable: false, enumerable: true, configurable: false },
|
|
372
|
+
path: { value: expressReq.path, writable: false, enumerable: true, configurable: false },
|
|
373
|
+
originalUrl: { value: expressReq.originalUrl, writable: false, enumerable: true, configurable: false },
|
|
374
|
+
baseUrl: { value: expressReq.baseUrl, writable: false, enumerable: true, configurable: false },
|
|
375
|
+
url: { value: expressReq.url, writable: false, enumerable: true, configurable: false },
|
|
376
|
+
});
|
|
368
377
|
// Augment expressRes with the .respond and .setHeader methods, using TDef
|
|
369
378
|
const typedExpressRes = expressRes;
|
|
370
379
|
typedExpressRes.respond = (status, dataForResponse) => {
|
|
@@ -389,7 +389,9 @@ function registerHonoRouteHandlers(app, apiDefinition, routeHandlers, middleware
|
|
|
389
389
|
ip: c.req.header('CF-Connecting-IP') || '127.0.0.1',
|
|
390
390
|
method: c.req.method,
|
|
391
391
|
path: c.req.path,
|
|
392
|
-
originalUrl: c.req.url
|
|
392
|
+
originalUrl: c.req.url,
|
|
393
|
+
// Hono doesn't support long-lived connections, so onClose is undefined
|
|
394
|
+
onClose: undefined
|
|
393
395
|
};
|
|
394
396
|
const fakeRes = {
|
|
395
397
|
respond: c.respond,
|
package/dist/router.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ P extends ApiParams<TDef, TDomain, TRouteKey> = ApiParams<TDef, TDomain, TRouteK
|
|
|
9
9
|
[fieldname: string]: File[];
|
|
10
10
|
};
|
|
11
11
|
ctx?: Ctx;
|
|
12
|
+
onClose?: (callback: () => void) => void;
|
|
12
13
|
};
|
|
13
14
|
type ResponseDataForStatus<TDef extends ApiDefinitionSchema, TDomain extends keyof TDef['endpoints'], TRouteName extends keyof TDef['endpoints'][TDomain], TStatus extends keyof TDef['endpoints'][TDomain][TRouteName]['responses'] & number> = InferDataFromUnifiedResponse<TDef['endpoints'][TDomain][TRouteName]['responses'][TStatus]>;
|
|
14
15
|
type RespondFunction<TDef extends ApiDefinitionSchema, TDomain extends keyof TDef['endpoints'], TRouteName extends keyof TDef['endpoints'][TDomain]> = <TStatusLocal extends keyof TDef['endpoints'][TDomain][TRouteName]['responses'] & number>(status: TStatusLocal, data: ResponseDataForStatus<TDef, TDomain, TRouteName, TStatusLocal>) => void;
|
|
@@ -77,6 +77,20 @@ export const PublicApiDefinition = CreateApiDefinition({
|
|
|
77
77
|
200: z.string() // Raw SSE data
|
|
78
78
|
})
|
|
79
79
|
},
|
|
80
|
+
disconnectTest: {
|
|
81
|
+
method: 'GET',
|
|
82
|
+
path: '/disconnect-test',
|
|
83
|
+
description: 'Endpoint for testing client disconnection handling',
|
|
84
|
+
query: z.object({
|
|
85
|
+
delay: z.number().int().min(100).max(5000).optional().default(1000)
|
|
86
|
+
}),
|
|
87
|
+
responses: CreateResponses({
|
|
88
|
+
200: z.object({
|
|
89
|
+
message: z.string(),
|
|
90
|
+
disconnected: z.boolean()
|
|
91
|
+
}),
|
|
92
|
+
})
|
|
93
|
+
},
|
|
80
94
|
}
|
|
81
95
|
}
|
|
82
96
|
})
|
package/package.json
CHANGED
package/src/handler.ts
CHANGED
|
@@ -401,28 +401,39 @@ export function registerRouteHandlers<TDef extends ApiDefinitionSchema>(
|
|
|
401
401
|
: expressReq.body;
|
|
402
402
|
|
|
403
403
|
// Construct TypedRequest using TDef, currentDomain, currentRouteKey
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
404
|
+
// Create a new object that inherits from expressReq to preserve prototype methods like .on()
|
|
405
|
+
const finalTypedReq = Object.create(expressReq, {
|
|
406
|
+
// Core parsed/validated properties
|
|
407
|
+
params: { value: parsedParams, writable: true, enumerable: true, configurable: true },
|
|
408
|
+
query: { value: parsedQuery, writable: true, enumerable: true, configurable: true },
|
|
409
|
+
body: { value: parsedBody, writable: true, enumerable: true, configurable: true },
|
|
410
|
+
ctx: { value: (expressReq as any).ctx, writable: true, enumerable: true, configurable: true },
|
|
411
|
+
|
|
412
|
+
// Unified API for client disconnection
|
|
413
|
+
onClose: {
|
|
414
|
+
value: (callback: () => void) => expressReq.on('close', callback),
|
|
415
|
+
writable: false,
|
|
416
|
+
enumerable: true,
|
|
417
|
+
configurable: false
|
|
418
|
+
},
|
|
419
|
+
|
|
420
|
+
// Restore original Express request properties for full compatibility
|
|
421
|
+
headers: { value: expressReq.headers, writable: false, enumerable: true, configurable: false },
|
|
422
|
+
cookies: { value: expressReq.cookies, writable: false, enumerable: true, configurable: false },
|
|
423
|
+
ip: { value: expressReq.ip, writable: false, enumerable: true, configurable: false },
|
|
424
|
+
ips: { value: expressReq.ips, writable: false, enumerable: true, configurable: false },
|
|
425
|
+
hostname: { value: expressReq.hostname, writable: false, enumerable: true, configurable: false },
|
|
426
|
+
protocol: { value: expressReq.protocol, writable: false, enumerable: true, configurable: false },
|
|
427
|
+
secure: { value: expressReq.secure, writable: false, enumerable: true, configurable: false },
|
|
428
|
+
xhr: { value: expressReq.xhr, writable: false, enumerable: true, configurable: false },
|
|
429
|
+
fresh: { value: expressReq.fresh, writable: false, enumerable: true, configurable: false },
|
|
430
|
+
stale: { value: expressReq.stale, writable: false, enumerable: true, configurable: false },
|
|
431
|
+
subdomains: { value: expressReq.subdomains, writable: false, enumerable: true, configurable: false },
|
|
432
|
+
path: { value: expressReq.path, writable: false, enumerable: true, configurable: false },
|
|
433
|
+
originalUrl: { value: expressReq.originalUrl, writable: false, enumerable: true, configurable: false },
|
|
434
|
+
baseUrl: { value: expressReq.baseUrl, writable: false, enumerable: true, configurable: false },
|
|
435
|
+
url: { value: expressReq.url, writable: false, enumerable: true, configurable: false },
|
|
436
|
+
}) as TypedRequest<TDef, typeof currentDomain, typeof currentRouteKey>;
|
|
426
437
|
|
|
427
438
|
// Augment expressRes with the .respond and .setHeader methods, using TDef
|
|
428
439
|
const typedExpressRes = expressRes as TypedResponse<TDef, typeof currentDomain, typeof currentRouteKey>;
|
|
@@ -471,7 +471,9 @@ export function registerHonoRouteHandlers<
|
|
|
471
471
|
ip: c.req.header('CF-Connecting-IP') || '127.0.0.1',
|
|
472
472
|
method: c.req.method,
|
|
473
473
|
path: c.req.path,
|
|
474
|
-
originalUrl: c.req.url
|
|
474
|
+
originalUrl: c.req.url,
|
|
475
|
+
// Hono doesn't support long-lived connections, so onClose is undefined
|
|
476
|
+
onClose: undefined
|
|
475
477
|
} as TypedRequest<TDef, typeof currentDomain, typeof currentRouteKey>;
|
|
476
478
|
|
|
477
479
|
const fakeRes = {
|
package/src/router.ts
CHANGED
|
@@ -27,6 +27,8 @@ export type TypedRequest<
|
|
|
27
27
|
files?: File[] | { [fieldname: string]: File[] };
|
|
28
28
|
// Add typed context object for carrying data between middlewares and handlers
|
|
29
29
|
ctx?: Ctx;
|
|
30
|
+
// Unified client disconnection handler (Express only, undefined for Hono)
|
|
31
|
+
onClose?: (callback: () => void) => void;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
// --- Enhanced TypedResponse with res.respond, now generic over TDef ---
|
package/tests/setup.ts
CHANGED
|
@@ -50,6 +50,25 @@ const simplePublicHandlers = {
|
|
|
50
50
|
|
|
51
51
|
// Close the stream
|
|
52
52
|
res.endStream();
|
|
53
|
+
},
|
|
54
|
+
disconnectTest: async (req: any, res: any) => {
|
|
55
|
+
const delay = req.query?.delay || 1000;
|
|
56
|
+
let disconnected = false;
|
|
57
|
+
|
|
58
|
+
// Set up disconnection handler using unified API
|
|
59
|
+
req.onClose?.(() => {
|
|
60
|
+
console.log('Client disconnected during disconnect-test');
|
|
61
|
+
disconnected = true;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Simulate long-running operation
|
|
65
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
66
|
+
|
|
67
|
+
// Check if client disconnected during the delay
|
|
68
|
+
res.respond(200, {
|
|
69
|
+
message: disconnected ? 'Client disconnected' : 'Operation completed',
|
|
70
|
+
disconnected
|
|
71
|
+
});
|
|
53
72
|
}
|
|
54
73
|
},
|
|
55
74
|
status: {
|
package/tests/simple-api.test.ts
CHANGED
|
@@ -251,6 +251,62 @@ describe.each([
|
|
|
251
251
|
return data ? JSON.parse(data) : null;
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
+
test('should handle client disconnection with req.onClose', async () => {
|
|
255
|
+
// Test that the endpoint works normally when client doesn't disconnect
|
|
256
|
+
const result = await client.callApi('common', 'disconnectTest', {
|
|
257
|
+
query: { delay: 100 }
|
|
258
|
+
}, {
|
|
259
|
+
200: ({ data }) => {
|
|
260
|
+
expect(data.message).toBe('Operation completed');
|
|
261
|
+
expect(data.disconnected).toBe(false);
|
|
262
|
+
return data;
|
|
263
|
+
},
|
|
264
|
+
422: ({ error }) => {
|
|
265
|
+
throw new Error(`Validation error: ${JSON.stringify(error)}`);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
expect(result.disconnected).toBe(false);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test('should detect client disconnection during request processing', async () => {
|
|
273
|
+
if (serverName === 'Hono') {
|
|
274
|
+
// Hono doesn't support disconnection detection, so onClose is undefined
|
|
275
|
+
// The endpoint completes successfully but doesn't detect disconnections
|
|
276
|
+
const response = await fetch(`${baseUrl}/api/v1/public/disconnect-test?delay=100`);
|
|
277
|
+
expect(response.status).toBe(200);
|
|
278
|
+
const data = await response.json();
|
|
279
|
+
expect(data.data.disconnected).toBe(false); // No disconnection detected
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Use fetch directly with AbortController to simulate client disconnection
|
|
284
|
+
const controller = new AbortController();
|
|
285
|
+
const signal = controller.signal;
|
|
286
|
+
|
|
287
|
+
// Start the request
|
|
288
|
+
const fetchPromise = fetch(`${baseUrl}/api/v1/public/disconnect-test?delay=500`, {
|
|
289
|
+
signal,
|
|
290
|
+
headers: { 'Accept': 'application/json' }
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// Abort the request after a short delay (before the server finishes)
|
|
294
|
+
setTimeout(() => {
|
|
295
|
+
controller.abort();
|
|
296
|
+
}, 200);
|
|
297
|
+
|
|
298
|
+
// The request should be aborted (node-fetch uses different error messages)
|
|
299
|
+
await expect(fetchPromise).rejects.toThrow(/aborted/i);
|
|
300
|
+
|
|
301
|
+
// Give the server a moment to process the disconnection
|
|
302
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
303
|
+
|
|
304
|
+
// Note: In a real test environment, we would need a way to verify
|
|
305
|
+
// that the close handler was called on the server side.
|
|
306
|
+
// For this test, we're mainly verifying that the endpoint exists
|
|
307
|
+
// and that client disconnection doesn't crash the server.
|
|
308
|
+
});
|
|
309
|
+
|
|
254
310
|
test('generateUrl should return correct URL for ping', () => {
|
|
255
311
|
const url = client.generateUrl('common', 'ping');
|
|
256
312
|
expect(url).toBe(`${baseUrl}/api/v1/public/ping`);
|