@trpc/server 11.0.0-rc.366 → 11.0.0-rc.370

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.
@@ -20,6 +20,23 @@ export type WSConnectionHandlerOptions<TRouter extends AnyRouter> = BaseHandlerO
20
20
  export type WSSHandlerOptions<TRouter extends AnyRouter> = WSConnectionHandlerOptions<TRouter> & {
21
21
  wss: ws.WebSocketServer;
22
22
  prefix?: string;
23
+ keepAlive?: {
24
+ /**
25
+ * Enable heartbeat messages
26
+ * @default false
27
+ */
28
+ enabled: boolean;
29
+ /**
30
+ * Heartbeat interval in milliseconds
31
+ * @default 30000
32
+ */
33
+ pingMs?: number;
34
+ /**
35
+ * Terminate the WebSocket if no pong is received after this many milliseconds
36
+ * @default 5000
37
+ */
38
+ pongWaitMs?: number;
39
+ };
23
40
  };
24
41
  export declare function getWSConnectionHandler<TRouter extends AnyRouter>(opts: WSConnectionHandlerOptions<TRouter>): (client: ws.WebSocket, req: IncomingMessage) => Promise<void>;
25
42
  export declare function applyWSSHandler<TRouter extends AnyRouter>(opts: WSSHandlerOptions<TRouter>): {
@@ -1 +1 @@
1
- {"version":3,"file":"ws.d.ts","sourceRoot":"","sources":["../../src/adapters/ws.ts"],"names":[],"mappings":";AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,MAAM,CAAC;AAC5C,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,EACV,SAAS,EACT,qBAAqB,EACrB,kBAAkB,EACnB,MAAM,iBAAiB,CAAC;AAQzB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAY/D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,aAAa,CAAC;AAQlE;;GAEG;AACH,MAAM,MAAM,yBAAyB,GAAG,IAAI,CAC1C,8BAA8B,CAAC,eAAe,EAAE,EAAE,CAAC,SAAS,CAAC,EAC7D,MAAM,CACP,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,kBAAkB,CAAC,OAAO,SAAS,SAAS,IAAI,CAC1D,IAAI,EAAE,yBAAyB,KAC5B,YAAY,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC,CAAC;AAE/C,MAAM,MAAM,0BAA0B,CAAC,OAAO,SAAS,SAAS,IAC9D,kBAAkB,CAAC,OAAO,EAAE,eAAe,CAAC,GAC1C,qBAAqB,CACnB,kBAAkB,CAAC,OAAO,CAAC,EAC3B,kBAAkB,CAAC,OAAO,CAAC,CAC5B,CAAC;AAEN;;GAEG;AACH,MAAM,MAAM,iBAAiB,CAAC,OAAO,SAAS,SAAS,IACrD,0BAA0B,CAAC,OAAO,CAAC,GAAG;IACpC,GAAG,EAAE,EAAE,CAAC,eAAe,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEJ,wBAAgB,sBAAsB,CAAC,OAAO,SAAS,SAAS,EAC9D,IAAI,EAAE,0BAA0B,CAAC,OAAO,CAAC,YAKnB,YAAY,OAAO,eAAe,mBAoPzD;AAED,wBAAgB,eAAe,CAAC,OAAO,SAAS,SAAS,EACvD,IAAI,EAAE,iBAAiB,CAAC,OAAO,CAAC;;EA2BjC"}
1
+ {"version":3,"file":"ws.d.ts","sourceRoot":"","sources":["../../src/adapters/ws.ts"],"names":[],"mappings":";AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,MAAM,CAAC;AAC5C,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,EACV,SAAS,EACT,qBAAqB,EACrB,kBAAkB,EACnB,MAAM,iBAAiB,CAAC;AAQzB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAY/D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,aAAa,CAAC;AAQlE;;GAEG;AACH,MAAM,MAAM,yBAAyB,GAAG,IAAI,CAC1C,8BAA8B,CAAC,eAAe,EAAE,EAAE,CAAC,SAAS,CAAC,EAC7D,MAAM,CACP,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,kBAAkB,CAAC,OAAO,SAAS,SAAS,IAAI,CAC1D,IAAI,EAAE,yBAAyB,KAC5B,YAAY,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC,CAAC;AAE/C,MAAM,MAAM,0BAA0B,CAAC,OAAO,SAAS,SAAS,IAC9D,kBAAkB,CAAC,OAAO,EAAE,eAAe,CAAC,GAC1C,qBAAqB,CACnB,kBAAkB,CAAC,OAAO,CAAC,EAC3B,kBAAkB,CAAC,OAAO,CAAC,CAC5B,CAAC;AAEN;;GAEG;AACH,MAAM,MAAM,iBAAiB,CAAC,OAAO,SAAS,SAAS,IACrD,0BAA0B,CAAC,OAAO,CAAC,GAAG;IACpC,GAAG,EAAE,EAAE,CAAC,eAAe,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE;QACV;;;WAGG;QACH,OAAO,EAAE,OAAO,CAAC;QACjB;;;WAGG;QACH,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB;;;WAGG;QACH,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC;CACH,CAAC;AAEJ,wBAAgB,sBAAsB,CAAC,OAAO,SAAS,SAAS,EAC9D,IAAI,EAAE,0BAA0B,CAAC,OAAO,CAAC,YAKnB,YAAY,OAAO,eAAe,mBAoPzD;AAiCD,wBAAgB,eAAe,CAAC,OAAO,SAAS,SAAS,EACvD,IAAI,EAAE,iBAAiB,CAAC,OAAO,CAAC;;EA+BjC"}
@@ -259,14 +259,43 @@ function getWSConnectionHandler(opts) {
259
259
  await createContextAsync();
260
260
  };
261
261
  }
262
+ /**
263
+ * Handle WebSocket keep-alive messages
264
+ */ function handleKeepAlive(client, pingMs = 30000, pongWaitMs = 5000) {
265
+ let heartbeatTimeout;
266
+ const heartbeatInterval = setInterval(()=>{
267
+ if (client.readyState !== WEBSOCKET_OPEN) {
268
+ return;
269
+ }
270
+ // First we send a ping message and wait for a pong
271
+ client.ping();
272
+ // We set a timeout to close the connection if the pong is not received
273
+ heartbeatTimeout = setTimeout(()=>{
274
+ client.terminate();
275
+ clearInterval(heartbeatInterval);
276
+ }, pongWaitMs);
277
+ }, pingMs).unref();
278
+ // When we receive a pong message, we clear the timeout
279
+ client.on('pong', ()=>{
280
+ heartbeatTimeout && clearTimeout(heartbeatTimeout);
281
+ });
282
+ // If the connection is closed, we clear the interval
283
+ client.on('close', ()=>{
284
+ clearInterval(heartbeatInterval);
285
+ });
286
+ }
262
287
  function applyWSSHandler(opts) {
263
- const { wss , prefix } = opts;
288
+ const { wss , prefix , keepAlive } = opts;
264
289
  const onConnection = getWSConnectionHandler(opts);
265
290
  wss.on('connection', async (client, req)=>{
266
291
  if (prefix && !req.url?.startsWith(prefix)) {
267
292
  return;
268
293
  }
269
294
  await onConnection(client, req);
295
+ if (keepAlive?.enabled) {
296
+ const { pingMs , pongWaitMs } = keepAlive;
297
+ handleKeepAlive(client, pingMs, pongWaitMs);
298
+ }
270
299
  });
271
300
  return {
272
301
  broadcastReconnectNotification: ()=>{
@@ -257,14 +257,43 @@ function getWSConnectionHandler(opts) {
257
257
  await createContextAsync();
258
258
  };
259
259
  }
260
+ /**
261
+ * Handle WebSocket keep-alive messages
262
+ */ function handleKeepAlive(client, pingMs = 30000, pongWaitMs = 5000) {
263
+ let heartbeatTimeout;
264
+ const heartbeatInterval = setInterval(()=>{
265
+ if (client.readyState !== WEBSOCKET_OPEN) {
266
+ return;
267
+ }
268
+ // First we send a ping message and wait for a pong
269
+ client.ping();
270
+ // We set a timeout to close the connection if the pong is not received
271
+ heartbeatTimeout = setTimeout(()=>{
272
+ client.terminate();
273
+ clearInterval(heartbeatInterval);
274
+ }, pongWaitMs);
275
+ }, pingMs).unref();
276
+ // When we receive a pong message, we clear the timeout
277
+ client.on('pong', ()=>{
278
+ heartbeatTimeout && clearTimeout(heartbeatTimeout);
279
+ });
280
+ // If the connection is closed, we clear the interval
281
+ client.on('close', ()=>{
282
+ clearInterval(heartbeatInterval);
283
+ });
284
+ }
260
285
  function applyWSSHandler(opts) {
261
- const { wss , prefix } = opts;
286
+ const { wss , prefix , keepAlive } = opts;
262
287
  const onConnection = getWSConnectionHandler(opts);
263
288
  wss.on('connection', async (client, req)=>{
264
289
  if (prefix && !req.url?.startsWith(prefix)) {
265
290
  return;
266
291
  }
267
292
  await onConnection(client, req);
293
+ if (keepAlive?.enabled) {
294
+ const { pingMs , pongWaitMs } = keepAlive;
295
+ handleKeepAlive(client, pingMs, pongWaitMs);
296
+ }
268
297
  });
269
298
  return {
270
299
  broadcastReconnectNotification: ()=>{
@@ -1,12 +1,12 @@
1
1
  {
2
- "bundleSize": 85905,
3
- "bundleOrigSize": 132659,
4
- "bundleReduction": 35.24,
2
+ "bundleSize": 86976,
3
+ "bundleOrigSize": 134089,
4
+ "bundleReduction": 35.14,
5
5
  "modules": [
6
6
  {
7
7
  "id": "/src/adapters/ws.ts",
8
- "size": 10170,
9
- "origSize": 9440,
8
+ "size": 11241,
9
+ "origSize": 10870,
10
10
  "renderedExports": [
11
11
  "getWSConnectionHandler",
12
12
  "applyWSSHandler"
@@ -15,7 +15,7 @@
15
15
  "dependents": [
16
16
  "/src/adapters/fastify/fastifyTRPCPlugin.ts"
17
17
  ],
18
- "percent": 11.84,
18
+ "percent": 12.92,
19
19
  "reduction": 0
20
20
  },
21
21
  {
@@ -29,7 +29,7 @@
29
29
  "dependents": [
30
30
  "/src/unstable-core-do-not-import.ts"
31
31
  ],
32
- "percent": 11.05,
32
+ "percent": 10.91,
33
33
  "reduction": 4.97
34
34
  },
35
35
  {
@@ -44,7 +44,7 @@
44
44
  "/src/unstable-core-do-not-import.ts",
45
45
  "/src/unstable-core-do-not-import/initTRPC.ts"
46
46
  ],
47
- "percent": 7.36,
47
+ "percent": 7.27,
48
48
  "reduction": 59.52
49
49
  },
50
50
  {
@@ -63,7 +63,7 @@
63
63
  "/src/unstable-core-do-not-import/http/resolveResponse.ts",
64
64
  "/src/unstable-core-do-not-import/initTRPC.ts"
65
65
  ],
66
- "percent": 6.97,
66
+ "percent": 6.88,
67
67
  "reduction": 41.05
68
68
  },
69
69
  {
@@ -78,7 +78,7 @@
78
78
  "/src/unstable-core-do-not-import.ts",
79
79
  "/src/unstable-core-do-not-import/http/resolveResponse.ts"
80
80
  ],
81
- "percent": 6.46,
81
+ "percent": 6.38,
82
82
  "reduction": 0
83
83
  },
84
84
  {
@@ -92,7 +92,7 @@
92
92
  "dependents": [
93
93
  "/src/adapters/aws-lambda/index.ts"
94
94
  ],
95
- "percent": 5.65,
95
+ "percent": 5.58,
96
96
  "reduction": 13.62
97
97
  },
98
98
  {
@@ -109,7 +109,7 @@
109
109
  "/src/observable/index.ts",
110
110
  "/src/observable/operators.ts"
111
111
  ],
112
- "percent": 3.64,
112
+ "percent": 3.6,
113
113
  "reduction": 0.67
114
114
  },
115
115
  {
@@ -123,7 +123,7 @@
123
123
  "dependents": [
124
124
  "/src/adapters/next-app-dir.ts"
125
125
  ],
126
- "percent": 3.55,
126
+ "percent": 3.51,
127
127
  "reduction": 20.12
128
128
  },
129
129
  {
@@ -139,7 +139,7 @@
139
139
  "dependents": [
140
140
  "/src/observable/index.ts"
141
141
  ],
142
- "percent": 3.21,
142
+ "percent": 3.17,
143
143
  "reduction": 0
144
144
  },
145
145
  {
@@ -159,7 +159,7 @@
159
159
  "/src/unstable-core-do-not-import/router.ts",
160
160
  "/src/unstable-core-do-not-import/initTRPC.ts"
161
161
  ],
162
- "percent": 3.19,
162
+ "percent": 3.15,
163
163
  "reduction": 45.94
164
164
  },
165
165
  {
@@ -173,7 +173,7 @@
173
173
  "dependents": [
174
174
  "/src/unstable-core-do-not-import.ts"
175
175
  ],
176
- "percent": 3.09,
176
+ "percent": 3.05,
177
177
  "reduction": 41.46
178
178
  },
179
179
  {
@@ -193,7 +193,7 @@
193
193
  "/src/unstable-core-do-not-import/initTRPC.ts",
194
194
  "/src/unstable-core-do-not-import/procedureBuilder.ts"
195
195
  ],
196
- "percent": 3.06,
196
+ "percent": 3.02,
197
197
  "reduction": 55.5
198
198
  },
199
199
  {
@@ -207,7 +207,7 @@
207
207
  "dependents": [
208
208
  "/src/adapters/fetch/index.ts"
209
209
  ],
210
- "percent": 2.62,
210
+ "percent": 2.59,
211
211
  "reduction": 2.17
212
212
  },
213
213
  {
@@ -223,7 +223,7 @@
223
223
  "/src/unstable-core-do-not-import.ts",
224
224
  "/src/unstable-core-do-not-import/router.ts"
225
225
  ],
226
- "percent": 2.33,
226
+ "percent": 2.3,
227
227
  "reduction": 0
228
228
  },
229
229
  {
@@ -238,7 +238,7 @@
238
238
  "/src/adapters/node-http/index.ts",
239
239
  "/src/adapters/node-http/nodeHTTPRequestHandler.ts"
240
240
  ],
241
- "percent": 2.27,
241
+ "percent": 2.25,
242
242
  "reduction": 20.5
243
243
  },
244
244
  {
@@ -259,7 +259,7 @@
259
259
  "/src/unstable-core-do-not-import/http/contentType.ts",
260
260
  "/src/unstable-core-do-not-import/procedureBuilder.ts"
261
261
  ],
262
- "percent": 2.02,
262
+ "percent": 1.99,
263
263
  "reduction": 19.47
264
264
  },
265
265
  {
@@ -273,7 +273,7 @@
273
273
  "dependents": [
274
274
  "/src/adapters/node-http/index.ts"
275
275
  ],
276
- "percent": 2,
276
+ "percent": 1.98,
277
277
  "reduction": 20.03
278
278
  },
279
279
  {
@@ -285,7 +285,7 @@
285
285
  ],
286
286
  "removedExports": [],
287
287
  "dependents": [],
288
- "percent": 1.84,
288
+ "percent": 1.82,
289
289
  "reduction": 21.04
290
290
  },
291
291
  {
@@ -297,7 +297,7 @@
297
297
  ],
298
298
  "removedExports": [],
299
299
  "dependents": [],
300
- "percent": 1.8,
300
+ "percent": 1.77,
301
301
  "reduction": 26.94
302
302
  },
303
303
  {
@@ -310,7 +310,7 @@
310
310
  ],
311
311
  "removedExports": [],
312
312
  "dependents": [],
313
- "percent": 1.77,
313
+ "percent": 1.75,
314
314
  "reduction": 27.6
315
315
  },
316
316
  {
@@ -324,7 +324,7 @@
324
324
  "dependents": [
325
325
  "/src/adapters/fastify/index.ts"
326
326
  ],
327
- "percent": 1.75,
327
+ "percent": 1.72,
328
328
  "reduction": 34.7
329
329
  },
330
330
  {
@@ -341,7 +341,7 @@
341
341
  "/src/unstable-core-do-not-import/http/resolveResponse.ts",
342
342
  "/src/unstable-core-do-not-import/error/getErrorShape.ts"
343
343
  ],
344
- "percent": 1.47,
344
+ "percent": 1.46,
345
345
  "reduction": 23.5
346
346
  },
347
347
  {
@@ -356,7 +356,7 @@
356
356
  "/src/adapters/fastify/index.ts",
357
357
  "/src/adapters/fastify/fastifyTRPCPlugin.ts"
358
358
  ],
359
- "percent": 1.32,
359
+ "percent": 1.3,
360
360
  "reduction": 47.59
361
361
  },
362
362
  {
@@ -371,7 +371,7 @@
371
371
  "/src/unstable-core-do-not-import.ts",
372
372
  "/src/unstable-core-do-not-import/procedureBuilder.ts"
373
373
  ],
374
- "percent": 1.15,
374
+ "percent": 1.14,
375
375
  "reduction": 56.22
376
376
  },
377
377
  {
@@ -388,7 +388,7 @@
388
388
  "/src/adapters/next-app-dir/nextAppDirCaller.ts",
389
389
  "/src/adapters/next-app-dir/rethrowNextErrors.ts"
390
390
  ],
391
- "percent": 1.15,
391
+ "percent": 1.13,
392
392
  "reduction": 13.65
393
393
  },
394
394
  {
@@ -413,7 +413,7 @@
413
413
  "/src/unstable-core-do-not-import/http/contentType.ts",
414
414
  "/src/unstable-core-do-not-import/procedureBuilder.ts"
415
415
  ],
416
- "percent": 1.09,
416
+ "percent": 1.08,
417
417
  "reduction": 25.69
418
418
  },
419
419
  {
@@ -427,7 +427,7 @@
427
427
  "dependents": [
428
428
  "/src/adapters/next-app-dir/nextAppDirCaller.ts"
429
429
  ],
430
- "percent": 0.95,
430
+ "percent": 0.94,
431
431
  "reduction": 0.97
432
432
  },
433
433
  {
@@ -439,7 +439,7 @@
439
439
  ],
440
440
  "removedExports": [],
441
441
  "dependents": [],
442
- "percent": 0.88,
442
+ "percent": 0.87,
443
443
  "reduction": 66.9
444
444
  },
445
445
  {
@@ -454,7 +454,7 @@
454
454
  "/src/unstable-core-do-not-import.ts",
455
455
  "/src/unstable-core-do-not-import/http/resolveResponse.ts"
456
456
  ],
457
- "percent": 0.78,
457
+ "percent": 0.77,
458
458
  "reduction": 0.6
459
459
  },
460
460
  {
@@ -469,7 +469,7 @@
469
469
  "/src/unstable-core-do-not-import.ts",
470
470
  "/src/unstable-core-do-not-import/http/resolveResponse.ts"
471
471
  ],
472
- "percent": 0.73,
472
+ "percent": 0.72,
473
473
  "reduction": 43.49
474
474
  },
475
475
  {
@@ -482,7 +482,7 @@
482
482
  ],
483
483
  "removedExports": [],
484
484
  "dependents": [],
485
- "percent": 0.61,
485
+ "percent": 0.6,
486
486
  "reduction": 67.09
487
487
  },
488
488
  {
@@ -511,7 +511,7 @@
511
511
  "/src/unstable-core-do-not-import.ts",
512
512
  "/src/unstable-core-do-not-import/initTRPC.ts"
513
513
  ],
514
- "percent": 0.4,
514
+ "percent": 0.39,
515
515
  "reduction": 87.07
516
516
  },
517
517
  {
@@ -523,7 +523,7 @@
523
523
  ],
524
524
  "removedExports": [],
525
525
  "dependents": [],
526
- "percent": 0.33,
526
+ "percent": 0.32,
527
527
  "reduction": 72.92
528
528
  },
529
529
  {
@@ -551,7 +551,7 @@
551
551
  "dependents": [
552
552
  "/src/unstable-core-do-not-import.ts"
553
553
  ],
554
- "percent": 0.26,
554
+ "percent": 0.25,
555
555
  "reduction": 94.55
556
556
  },
557
557
  {
@@ -713,8 +713,8 @@
713
713
  "removedExports": [],
714
714
  "dependents": [
715
715
  "/src/adapters/express.ts",
716
- "/src/adapters/next.ts",
717
716
  "/src/adapters/standalone.ts",
717
+ "/src/adapters/next.ts",
718
718
  "/src/adapters/fastify/fastifyRequestHandler.ts"
719
719
  ],
720
720
  "percent": 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trpc/server",
3
- "version": "11.0.0-rc.366+237dbb3f9",
3
+ "version": "11.0.0-rc.370+1a920ac81",
4
4
  "description": "The tRPC server library",
5
5
  "author": "KATT",
6
6
  "license": "MIT",
@@ -122,7 +122,7 @@
122
122
  "@types/express": "^4.17.17",
123
123
  "@types/hash-sum": "^1.0.0",
124
124
  "@types/node": "^20.10.0",
125
- "@types/react": "^18.3.0",
125
+ "@types/react": "^18.3.1",
126
126
  "@types/react-dom": "^18.3.0",
127
127
  "@types/ws": "^8.2.0",
128
128
  "devalue": "^5.0.0",
@@ -133,8 +133,8 @@
133
133
  "hash-sum": "^2.0.0",
134
134
  "myzod": "^1.3.1",
135
135
  "next": "^14.1.4",
136
- "react": "^18.3.0",
137
- "react-dom": "^18.3.0",
136
+ "react": "^18.3.1",
137
+ "react-dom": "^18.3.1",
138
138
  "rollup": "^4.9.5",
139
139
  "superjson": "^1.12.4",
140
140
  "superstruct": "^1.0.0",
@@ -149,5 +149,5 @@
149
149
  "funding": [
150
150
  "https://trpc.io/sponsor"
151
151
  ],
152
- "gitHead": "237dbb3f9c92836cb097ec07d5eade88286490da"
152
+ "gitHead": "1a920ac8160dacfb6a49e6161f56208859168317"
153
153
  }
@@ -62,6 +62,23 @@ export type WSSHandlerOptions<TRouter extends AnyRouter> =
62
62
  WSConnectionHandlerOptions<TRouter> & {
63
63
  wss: ws.WebSocketServer;
64
64
  prefix?: string;
65
+ keepAlive?: {
66
+ /**
67
+ * Enable heartbeat messages
68
+ * @default false
69
+ */
70
+ enabled: boolean;
71
+ /**
72
+ * Heartbeat interval in milliseconds
73
+ * @default 30000
74
+ */
75
+ pingMs?: number;
76
+ /**
77
+ * Terminate the WebSocket if no pong is received after this many milliseconds
78
+ * @default 5000
79
+ */
80
+ pongWaitMs?: number;
81
+ };
65
82
  };
66
83
 
67
84
  export function getWSConnectionHandler<TRouter extends AnyRouter>(
@@ -316,10 +333,41 @@ export function getWSConnectionHandler<TRouter extends AnyRouter>(
316
333
  };
317
334
  }
318
335
 
336
+ /**
337
+ * Handle WebSocket keep-alive messages
338
+ */
339
+ function handleKeepAlive(
340
+ client: ws.WebSocket,
341
+ pingMs = 30000,
342
+ pongWaitMs = 5000,
343
+ ) {
344
+ let heartbeatTimeout: NodeJS.Timeout | undefined;
345
+ const heartbeatInterval = setInterval(() => {
346
+ if (client.readyState !== WEBSOCKET_OPEN) {
347
+ return;
348
+ }
349
+ // First we send a ping message and wait for a pong
350
+ client.ping();
351
+ // We set a timeout to close the connection if the pong is not received
352
+ heartbeatTimeout = setTimeout(() => {
353
+ client.terminate();
354
+ clearInterval(heartbeatInterval);
355
+ }, pongWaitMs);
356
+ }, pingMs).unref();
357
+ // When we receive a pong message, we clear the timeout
358
+ client.on('pong', () => {
359
+ heartbeatTimeout && clearTimeout(heartbeatTimeout);
360
+ });
361
+ // If the connection is closed, we clear the interval
362
+ client.on('close', () => {
363
+ clearInterval(heartbeatInterval);
364
+ });
365
+ }
366
+
319
367
  export function applyWSSHandler<TRouter extends AnyRouter>(
320
368
  opts: WSSHandlerOptions<TRouter>,
321
369
  ) {
322
- const { wss, prefix } = opts;
370
+ const { wss, prefix, keepAlive } = opts;
323
371
 
324
372
  const onConnection = getWSConnectionHandler(opts);
325
373
  wss.on('connection', async (client, req) => {
@@ -328,6 +376,10 @@ export function applyWSSHandler<TRouter extends AnyRouter>(
328
376
  }
329
377
 
330
378
  await onConnection(client, req);
379
+ if (keepAlive?.enabled) {
380
+ const { pingMs, pongWaitMs } = keepAlive;
381
+ handleKeepAlive(client, pingMs, pongWaitMs);
382
+ }
331
383
  });
332
384
 
333
385
  return {