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 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
- const finalTypedReq = {
347
- ...expressReq,
348
- params: parsedParams,
349
- query: parsedQuery,
350
- body: parsedBody,
351
- ctx: expressReq.ctx,
352
- headers: expressReq.headers,
353
- cookies: expressReq.cookies,
354
- ip: expressReq.ip,
355
- ips: expressReq.ips,
356
- hostname: expressReq.hostname,
357
- protocol: expressReq.protocol,
358
- secure: expressReq.secure,
359
- xhr: expressReq.xhr,
360
- fresh: expressReq.fresh,
361
- stale: expressReq.stale,
362
- subdomains: expressReq.subdomains,
363
- path: expressReq.path,
364
- originalUrl: expressReq.originalUrl,
365
- baseUrl: expressReq.baseUrl,
366
- url: expressReq.url,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-typed-api",
3
- "version": "0.2.20",
3
+ "version": "0.2.21",
4
4
  "description": "A lightweight, type-safe RPC library for TypeScript with Zod validation",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
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
- const finalTypedReq = {
405
- ...expressReq,
406
- params: parsedParams,
407
- query: parsedQuery,
408
- body: parsedBody,
409
- ctx: (expressReq as any).ctx,
410
- headers: expressReq.headers,
411
- cookies: expressReq.cookies,
412
- ip: expressReq.ip,
413
- ips: expressReq.ips,
414
- hostname: expressReq.hostname,
415
- protocol: expressReq.protocol,
416
- secure: expressReq.secure,
417
- xhr: expressReq.xhr,
418
- fresh: expressReq.fresh,
419
- stale: expressReq.stale,
420
- subdomains: expressReq.subdomains,
421
- path: expressReq.path,
422
- originalUrl: expressReq.originalUrl,
423
- baseUrl: expressReq.baseUrl,
424
- url: expressReq.url,
425
- } as TypedRequest<TDef, typeof currentDomain, typeof currentRouteKey>;
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: {
@@ -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`);