@trpc/server 11.0.0-rc.528 → 11.0.0-rc.531

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.
@@ -27,17 +27,23 @@ export type WSSHandlerOptions<TRouter extends AnyRouter> = WSConnectionHandlerOp
27
27
  enabled: boolean;
28
28
  /**
29
29
  * Heartbeat interval in milliseconds
30
- * @default 30000
30
+ * @default 30_000
31
31
  */
32
32
  pingMs?: number;
33
33
  /**
34
34
  * Terminate the WebSocket if no pong is received after this many milliseconds
35
- * @default 5000
35
+ * @default 5_000
36
36
  */
37
37
  pongWaitMs?: number;
38
38
  };
39
+ /**
40
+ * Disable responding to ping messages from the client
41
+ * **Not recommended** - this is mainly used for testing
42
+ * @default false
43
+ */
44
+ dangerouslyDisablePong?: boolean;
39
45
  };
40
- export declare function getWSConnectionHandler<TRouter extends AnyRouter>(opts: WSConnectionHandlerOptions<TRouter>): (client: ws.WebSocket, req: IncomingMessage) => Promise<void>;
46
+ export declare function getWSConnectionHandler<TRouter extends AnyRouter>(opts: WSSHandlerOptions<TRouter>): (client: ws.WebSocket, req: IncomingMessage) => Promise<void>;
41
47
  /**
42
48
  * Handle WebSocket keep-alive messages
43
49
  */
@@ -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;AASzB,OAAO,EAAS,KAAK,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AActE,OAAO,EAKL,KAAK,YAAY,EAClB,MAAM,gCAAgC,CAAC;AACxC,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,aAAa,CAAC;AAQlE;;GAEG;AACH,MAAM,MAAM,yBAAyB,GAAG,8BAA8B,CACpE,eAAe,EACf,EAAE,CAAC,SAAS,CACb,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;AAGJ,wBAAgB,sBAAsB,CAAC,OAAO,SAAS,SAAS,EAC9D,IAAI,EAAE,0BAA0B,CAAC,OAAO,CAAC,YAKnB,EAAE,CAAC,SAAS,OAAO,eAAe,mBAwWzD;AAED;;GAEG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,EAAE,CAAC,SAAS,EACpB,MAAM,SAAQ,EACd,UAAU,SAAO,QAuBlB;AAED,wBAAgB,eAAe,CAAC,OAAO,SAAS,SAAS,EACvD,IAAI,EAAE,iBAAiB,CAAC,OAAO,CAAC;;EA+BjC"}
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;AASzB,OAAO,EAAS,KAAK,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AActE,OAAO,EAKL,KAAK,YAAY,EAClB,MAAM,gCAAgC,CAAC;AACxC,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,aAAa,CAAC;AAQlE;;GAEG;AACH,MAAM,MAAM,yBAAyB,GAAG,8BAA8B,CACpE,eAAe,EACf,EAAE,CAAC,SAAS,CACb,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;IACF;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;CAClC,CAAC;AAGJ,wBAAgB,sBAAsB,CAAC,OAAO,SAAS,SAAS,EAC9D,IAAI,EAAE,iBAAiB,CAAC,OAAO,CAAC,YAKV,EAAE,CAAC,SAAS,OAAO,eAAe,mBAsXzD;AAED;;GAEG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,EAAE,CAAC,SAAS,EACpB,MAAM,SAAS,EACf,UAAU,SAAQ,QAiCnB;AAED,wBAAgB,eAAe,CAAC,OAAO,SAAS,SAAS,EACvD,IAAI,EAAE,iBAAiB,CAAC,OAAO,CAAC;;EAwCjC"}
@@ -23,6 +23,10 @@ function getWSConnectionHandler(opts) {
23
23
  return async (client, req)=>{
24
24
  const clientSubscriptions = new Map();
25
25
  const abortController = new AbortController();
26
+ if (opts.keepAlive?.enabled) {
27
+ const { pingMs , pongWaitMs } = opts.keepAlive;
28
+ handleKeepAlive(client, pingMs, pongWaitMs);
29
+ }
26
30
  function respond(untransformedJSON) {
27
31
  client.send(JSON.stringify(transformer.transformTRPCResponse(router$1._def._config, untransformedJSON)));
28
32
  }
@@ -277,13 +281,23 @@ function getWSConnectionHandler(opts) {
277
281
  });
278
282
  }
279
283
  }
280
- client.on('message', async (message)=>{
284
+ client.on('message', async (rawData)=>{
285
+ const msgStr = rawData.toString();
286
+ if (msgStr === 'PONG') {
287
+ return;
288
+ }
289
+ if (msgStr === 'PING') {
290
+ if (!opts.dangerouslyDisablePong) {
291
+ client.send('PONG');
292
+ }
293
+ return;
294
+ }
281
295
  if (ctxPromise === unsetContextPromiseSymbol) {
282
296
  // If the ctxPromise wasn't created immediately, we're expecting the first message to be a TRPCConnectionParamsMessage
283
297
  ctxPromise = createCtxPromise(()=>{
284
298
  let msg;
285
299
  try {
286
- msg = JSON.parse(message.toString());
300
+ msg = JSON.parse(msgStr);
287
301
  if (!utils.isObject(msg)) {
288
302
  throw new Error('Message was not an object');
289
303
  }
@@ -300,7 +314,7 @@ function getWSConnectionHandler(opts) {
300
314
  return;
301
315
  }
302
316
  try {
303
- const msgJSON = JSON.parse(message.toString());
317
+ const msgJSON = JSON.parse(msgStr);
304
318
  const msgs = Array.isArray(msgJSON) ? msgJSON : [
305
319
  msgJSON
306
320
  ];
@@ -346,48 +360,59 @@ function getWSConnectionHandler(opts) {
346
360
  abortController.abort();
347
361
  });
348
362
  if (ctxPromise !== unsetContextPromiseSymbol) {
349
- // prevent unhandled promise rejection errors
350
- await ctxPromise.catch(()=>null);
363
+ await ctxPromise;
351
364
  }
352
365
  };
353
366
  }
354
367
  /**
355
368
  * Handle WebSocket keep-alive messages
356
369
  */ function handleKeepAlive(client, pingMs = 30000, pongWaitMs = 5000) {
357
- let heartbeatTimeout;
358
- const heartbeatInterval = setInterval(()=>{
359
- if (client.readyState !== WEBSOCKET_OPEN) {
360
- return;
361
- }
362
- // First we send a ping message and wait for a pong
363
- client.ping();
364
- // We set a timeout to close the connection if the pong is not received
365
- heartbeatTimeout = setTimeout(()=>{
366
- client.terminate();
367
- clearInterval(heartbeatInterval);
368
- }, pongWaitMs);
369
- }, pingMs).unref();
370
- // When we receive a pong message, we clear the timeout
371
- client.on('pong', ()=>{
372
- heartbeatTimeout && clearTimeout(heartbeatTimeout);
373
- });
374
- // If the connection is closed, we clear the interval
370
+ let timeout = undefined;
371
+ let ping = undefined;
372
+ const schedulePing = ()=>{
373
+ const scheduleTimeout = ()=>{
374
+ timeout = setTimeout(()=>{
375
+ client.terminate();
376
+ }, pongWaitMs);
377
+ };
378
+ ping = setTimeout(()=>{
379
+ client.send('PING');
380
+ scheduleTimeout();
381
+ }, pingMs);
382
+ };
383
+ const onMessage = ()=>{
384
+ clearTimeout(ping);
385
+ clearTimeout(timeout);
386
+ schedulePing();
387
+ };
388
+ client.on('message', onMessage);
375
389
  client.on('close', ()=>{
376
- clearInterval(heartbeatInterval);
390
+ clearTimeout(ping);
391
+ clearTimeout(timeout);
377
392
  });
393
+ schedulePing();
378
394
  }
379
395
  function applyWSSHandler(opts) {
380
- const { wss , prefix , keepAlive } = opts;
381
396
  const onConnection = getWSConnectionHandler(opts);
382
- wss.on('connection', async (client, req)=>{
383
- if (prefix && !req.url?.startsWith(prefix)) {
397
+ opts.wss.on('connection', (client, req)=>{
398
+ if (opts.prefix && !req.url?.startsWith(opts.prefix)) {
384
399
  return;
385
400
  }
386
- await onConnection(client, req);
387
- if (keepAlive?.enabled) {
388
- const { pingMs , pongWaitMs } = keepAlive;
389
- handleKeepAlive(client, pingMs, pongWaitMs);
390
- }
401
+ onConnection(client, req).catch((cause)=>{
402
+ opts.onError?.({
403
+ error: new TRPCError.TRPCError({
404
+ code: 'INTERNAL_SERVER_ERROR',
405
+ cause,
406
+ message: 'Failed to handle WebSocket connection'
407
+ }),
408
+ req: req,
409
+ path: undefined,
410
+ type: 'unknown',
411
+ ctx: undefined,
412
+ input: undefined
413
+ });
414
+ client.close();
415
+ });
391
416
  });
392
417
  return {
393
418
  broadcastReconnectNotification: ()=>{
@@ -396,7 +421,7 @@ function applyWSSHandler(opts) {
396
421
  method: 'reconnect'
397
422
  };
398
423
  const data = JSON.stringify(response);
399
- for (const client of wss.clients){
424
+ for (const client of opts.wss.clients){
400
425
  if (client.readyState === WEBSOCKET_OPEN) {
401
426
  client.send(data);
402
427
  }
@@ -21,6 +21,10 @@ function getWSConnectionHandler(opts) {
21
21
  return async (client, req)=>{
22
22
  const clientSubscriptions = new Map();
23
23
  const abortController = new AbortController();
24
+ if (opts.keepAlive?.enabled) {
25
+ const { pingMs , pongWaitMs } = opts.keepAlive;
26
+ handleKeepAlive(client, pingMs, pongWaitMs);
27
+ }
24
28
  function respond(untransformedJSON) {
25
29
  client.send(JSON.stringify(transformTRPCResponse(router._def._config, untransformedJSON)));
26
30
  }
@@ -275,13 +279,23 @@ function getWSConnectionHandler(opts) {
275
279
  });
276
280
  }
277
281
  }
278
- client.on('message', async (message)=>{
282
+ client.on('message', async (rawData)=>{
283
+ const msgStr = rawData.toString();
284
+ if (msgStr === 'PONG') {
285
+ return;
286
+ }
287
+ if (msgStr === 'PING') {
288
+ if (!opts.dangerouslyDisablePong) {
289
+ client.send('PONG');
290
+ }
291
+ return;
292
+ }
279
293
  if (ctxPromise === unsetContextPromiseSymbol) {
280
294
  // If the ctxPromise wasn't created immediately, we're expecting the first message to be a TRPCConnectionParamsMessage
281
295
  ctxPromise = createCtxPromise(()=>{
282
296
  let msg;
283
297
  try {
284
- msg = JSON.parse(message.toString());
298
+ msg = JSON.parse(msgStr);
285
299
  if (!isObject(msg)) {
286
300
  throw new Error('Message was not an object');
287
301
  }
@@ -298,7 +312,7 @@ function getWSConnectionHandler(opts) {
298
312
  return;
299
313
  }
300
314
  try {
301
- const msgJSON = JSON.parse(message.toString());
315
+ const msgJSON = JSON.parse(msgStr);
302
316
  const msgs = Array.isArray(msgJSON) ? msgJSON : [
303
317
  msgJSON
304
318
  ];
@@ -344,48 +358,59 @@ function getWSConnectionHandler(opts) {
344
358
  abortController.abort();
345
359
  });
346
360
  if (ctxPromise !== unsetContextPromiseSymbol) {
347
- // prevent unhandled promise rejection errors
348
- await ctxPromise.catch(()=>null);
361
+ await ctxPromise;
349
362
  }
350
363
  };
351
364
  }
352
365
  /**
353
366
  * Handle WebSocket keep-alive messages
354
367
  */ function handleKeepAlive(client, pingMs = 30000, pongWaitMs = 5000) {
355
- let heartbeatTimeout;
356
- const heartbeatInterval = setInterval(()=>{
357
- if (client.readyState !== WEBSOCKET_OPEN) {
358
- return;
359
- }
360
- // First we send a ping message and wait for a pong
361
- client.ping();
362
- // We set a timeout to close the connection if the pong is not received
363
- heartbeatTimeout = setTimeout(()=>{
364
- client.terminate();
365
- clearInterval(heartbeatInterval);
366
- }, pongWaitMs);
367
- }, pingMs).unref();
368
- // When we receive a pong message, we clear the timeout
369
- client.on('pong', ()=>{
370
- heartbeatTimeout && clearTimeout(heartbeatTimeout);
371
- });
372
- // If the connection is closed, we clear the interval
368
+ let timeout = undefined;
369
+ let ping = undefined;
370
+ const schedulePing = ()=>{
371
+ const scheduleTimeout = ()=>{
372
+ timeout = setTimeout(()=>{
373
+ client.terminate();
374
+ }, pongWaitMs);
375
+ };
376
+ ping = setTimeout(()=>{
377
+ client.send('PING');
378
+ scheduleTimeout();
379
+ }, pingMs);
380
+ };
381
+ const onMessage = ()=>{
382
+ clearTimeout(ping);
383
+ clearTimeout(timeout);
384
+ schedulePing();
385
+ };
386
+ client.on('message', onMessage);
373
387
  client.on('close', ()=>{
374
- clearInterval(heartbeatInterval);
388
+ clearTimeout(ping);
389
+ clearTimeout(timeout);
375
390
  });
391
+ schedulePing();
376
392
  }
377
393
  function applyWSSHandler(opts) {
378
- const { wss , prefix , keepAlive } = opts;
379
394
  const onConnection = getWSConnectionHandler(opts);
380
- wss.on('connection', async (client, req)=>{
381
- if (prefix && !req.url?.startsWith(prefix)) {
395
+ opts.wss.on('connection', (client, req)=>{
396
+ if (opts.prefix && !req.url?.startsWith(opts.prefix)) {
382
397
  return;
383
398
  }
384
- await onConnection(client, req);
385
- if (keepAlive?.enabled) {
386
- const { pingMs , pongWaitMs } = keepAlive;
387
- handleKeepAlive(client, pingMs, pongWaitMs);
388
- }
399
+ onConnection(client, req).catch((cause)=>{
400
+ opts.onError?.({
401
+ error: new TRPCError({
402
+ code: 'INTERNAL_SERVER_ERROR',
403
+ cause,
404
+ message: 'Failed to handle WebSocket connection'
405
+ }),
406
+ req: req,
407
+ path: undefined,
408
+ type: 'unknown',
409
+ ctx: undefined,
410
+ input: undefined
411
+ });
412
+ client.close();
413
+ });
389
414
  });
390
415
  return {
391
416
  broadcastReconnectNotification: ()=>{
@@ -394,7 +419,7 @@ function applyWSSHandler(opts) {
394
419
  method: 'reconnect'
395
420
  };
396
421
  const data = JSON.stringify(response);
397
- for (const client of wss.clients){
422
+ for (const client of opts.wss.clients){
398
423
  if (client.readyState === WEBSOCKET_OPEN) {
399
424
  client.send(data);
400
425
  }
@@ -1,7 +1,7 @@
1
1
  {
2
- "bundleSize": 135647,
3
- "bundleOrigSize": 185375,
4
- "bundleReduction": 26.83,
2
+ "bundleSize": 136151,
3
+ "bundleOrigSize": 185917,
4
+ "bundleReduction": 26.77,
5
5
  "modules": [
6
6
  {
7
7
  "id": "/src/unstable-core-do-not-import/http/resolveResponse.ts",
@@ -14,7 +14,7 @@
14
14
  "dependents": [
15
15
  "/src/unstable-core-do-not-import.ts"
16
16
  ],
17
- "percent": 13.2,
17
+ "percent": 13.16,
18
18
  "reduction": 0
19
19
  },
20
20
  {
@@ -31,13 +31,13 @@
31
31
  "/src/unstable-core-do-not-import.ts",
32
32
  "/src/unstable-core-do-not-import/http/resolveResponse.ts"
33
33
  ],
34
- "percent": 12.95,
34
+ "percent": 12.9,
35
35
  "reduction": 4.31
36
36
  },
37
37
  {
38
38
  "id": "/src/adapters/ws.ts",
39
- "size": 15782,
40
- "origSize": 14714,
39
+ "size": 16286,
40
+ "origSize": 15256,
41
41
  "renderedExports": [
42
42
  "getWSConnectionHandler",
43
43
  "handleKeepAlive",
@@ -47,7 +47,7 @@
47
47
  "dependents": [
48
48
  "/src/adapters/fastify/fastifyTRPCPlugin.ts"
49
49
  ],
50
- "percent": 11.63,
50
+ "percent": 11.96,
51
51
  "reduction": 0
52
52
  },
53
53
  {
@@ -62,7 +62,7 @@
62
62
  "/src/unstable-core-do-not-import.ts",
63
63
  "/src/unstable-core-do-not-import/http/resolveResponse.ts"
64
64
  ],
65
- "percent": 5.67,
65
+ "percent": 5.65,
66
66
  "reduction": 0
67
67
  },
68
68
  {
@@ -80,7 +80,7 @@
80
80
  "/src/unstable-core-do-not-import.ts",
81
81
  "/src/unstable-core-do-not-import/initTRPC.ts"
82
82
  ],
83
- "percent": 4.73,
83
+ "percent": 4.72,
84
84
  "reduction": 40.18
85
85
  },
86
86
  {
@@ -97,7 +97,7 @@
97
97
  "/src/unstable-core-do-not-import.ts",
98
98
  "/src/unstable-core-do-not-import/http/resolveResponse.ts"
99
99
  ],
100
- "percent": 4.36,
100
+ "percent": 4.34,
101
101
  "reduction": 15.22
102
102
  },
103
103
  {
@@ -112,7 +112,7 @@
112
112
  "/src/unstable-core-do-not-import.ts",
113
113
  "/src/unstable-core-do-not-import/initTRPC.ts"
114
114
  ],
115
- "percent": 4.27,
115
+ "percent": 4.26,
116
116
  "reduction": 63.05
117
117
  },
118
118
  {
@@ -126,7 +126,7 @@
126
126
  "dependents": [
127
127
  "/src/adapters/aws-lambda/index.ts"
128
128
  ],
129
- "percent": 3.99,
129
+ "percent": 3.98,
130
130
  "reduction": 11.38
131
131
  },
132
132
  {
@@ -146,7 +146,7 @@
146
146
  "/src/unstable-core-do-not-import/http/resolveResponse.ts",
147
147
  "/src/observable/operators.ts"
148
148
  ],
149
- "percent": 3.03,
149
+ "percent": 3.02,
150
150
  "reduction": 0
151
151
  },
152
152
  {
@@ -174,7 +174,7 @@
174
174
  "dependents": [
175
175
  "/src/adapters/node-http/index.ts"
176
176
  ],
177
- "percent": 2.11,
177
+ "percent": 2.1,
178
178
  "reduction": 6.07
179
179
  },
180
180
  {
@@ -190,7 +190,7 @@
190
190
  "dependents": [
191
191
  "/src/observable/index.ts"
192
192
  ],
193
- "percent": 2.03,
193
+ "percent": 2.02,
194
194
  "reduction": 0
195
195
  },
196
196
  {
@@ -206,9 +206,9 @@
206
206
  "removedExports": [],
207
207
  "dependents": [
208
208
  "/src/unstable-core-do-not-import.ts",
209
+ "/src/unstable-core-do-not-import/http/resolveResponse.ts",
209
210
  "/src/unstable-core-do-not-import/initTRPC.ts",
210
- "/src/unstable-core-do-not-import/router.ts",
211
- "/src/unstable-core-do-not-import/http/resolveResponse.ts"
211
+ "/src/unstable-core-do-not-import/router.ts"
212
212
  ],
213
213
  "percent": 2.02,
214
214
  "reduction": 45.94
@@ -244,7 +244,7 @@
244
244
  "/src/unstable-core-do-not-import/initTRPC.ts",
245
245
  "/src/unstable-core-do-not-import/procedureBuilder.ts"
246
246
  ],
247
- "percent": 1.94,
247
+ "percent": 1.93,
248
248
  "reduction": 55.77
249
249
  },
250
250
  {
@@ -260,7 +260,7 @@
260
260
  "/src/unstable-core-do-not-import.ts",
261
261
  "/src/unstable-core-do-not-import/router.ts"
262
262
  ],
263
- "percent": 1.72,
263
+ "percent": 1.71,
264
264
  "reduction": 0
265
265
  },
266
266
  {
@@ -289,7 +289,7 @@
289
289
  "/src/adapters/node-http/index.ts",
290
290
  "/src/adapters/node-http/nodeHTTPRequestHandler.ts"
291
291
  ],
292
- "percent": 1.62,
292
+ "percent": 1.61,
293
293
  "reduction": 13.79
294
294
  },
295
295
  {
@@ -332,16 +332,16 @@
332
332
  "removedExports": [],
333
333
  "dependents": [
334
334
  "/src/unstable-core-do-not-import.ts",
335
- "/src/unstable-core-do-not-import/middleware.ts",
336
- "/src/unstable-core-do-not-import/router.ts",
337
335
  "/src/unstable-core-do-not-import/http/parseConnectionParams.ts",
338
336
  "/src/unstable-core-do-not-import/http/resolveResponse.ts",
337
+ "/src/unstable-core-do-not-import/middleware.ts",
338
+ "/src/unstable-core-do-not-import/router.ts",
339
339
  "/src/unstable-core-do-not-import/http/contentType.ts",
340
340
  "/src/unstable-core-do-not-import/procedureBuilder.ts",
341
341
  "/src/unstable-core-do-not-import/stream/jsonl.ts",
342
342
  "/src/unstable-core-do-not-import/stream/sse.ts"
343
343
  ],
344
- "percent": 1.28,
344
+ "percent": 1.27,
345
345
  "reduction": 19.47
346
346
  },
347
347
  {
@@ -366,7 +366,7 @@
366
366
  ],
367
367
  "removedExports": [],
368
368
  "dependents": [],
369
- "percent": 1.19,
369
+ "percent": 1.18,
370
370
  "reduction": 27.47
371
371
  },
372
372
  {
@@ -378,7 +378,7 @@
378
378
  ],
379
379
  "removedExports": [],
380
380
  "dependents": [],
381
- "percent": 1.14,
381
+ "percent": 1.13,
382
382
  "reduction": 26.94
383
383
  },
384
384
  {
@@ -392,8 +392,8 @@
392
392
  "removedExports": [],
393
393
  "dependents": [
394
394
  "/src/unstable-core-do-not-import.ts",
395
- "/src/unstable-core-do-not-import/error/getErrorShape.ts",
396
- "/src/unstable-core-do-not-import/http/resolveResponse.ts"
395
+ "/src/unstable-core-do-not-import/http/resolveResponse.ts",
396
+ "/src/unstable-core-do-not-import/error/getErrorShape.ts"
397
397
  ],
398
398
  "percent": 1.06,
399
399
  "reduction": 16.73
@@ -410,7 +410,7 @@
410
410
  "/src/unstable-core-do-not-import.ts",
411
411
  "/src/unstable-core-do-not-import/procedureBuilder.ts"
412
412
  ],
413
- "percent": 0.96,
413
+ "percent": 0.95,
414
414
  "reduction": 55.21
415
415
  },
416
416
  {
@@ -429,14 +429,14 @@
429
429
  "removedExports": [],
430
430
  "dependents": [
431
431
  "/src/unstable-core-do-not-import.ts",
432
+ "/src/unstable-core-do-not-import/http/getHTTPStatusCode.ts",
433
+ "/src/unstable-core-do-not-import/http/parseConnectionParams.ts",
434
+ "/src/unstable-core-do-not-import/http/resolveResponse.ts",
432
435
  "/src/unstable-core-do-not-import/error/TRPCError.ts",
433
436
  "/src/unstable-core-do-not-import/transformer.ts",
434
437
  "/src/unstable-core-do-not-import/middleware.ts",
435
438
  "/src/unstable-core-do-not-import/router.ts",
436
439
  "/src/unstable-core-do-not-import/rpc/parseTRPCMessage.ts",
437
- "/src/unstable-core-do-not-import/http/getHTTPStatusCode.ts",
438
- "/src/unstable-core-do-not-import/http/parseConnectionParams.ts",
439
- "/src/unstable-core-do-not-import/http/resolveResponse.ts",
440
440
  "/src/unstable-core-do-not-import/http/contentType.ts",
441
441
  "/src/unstable-core-do-not-import/procedureBuilder.ts",
442
442
  "/src/unstable-core-do-not-import/stream/jsonl.ts",
@@ -504,7 +504,7 @@
504
504
  "/src/adapters/next-app-dir/nextAppDirCaller.ts",
505
505
  "/src/adapters/next-app-dir/rethrowNextErrors.ts"
506
506
  ],
507
- "percent": 0.73,
507
+ "percent": 0.72,
508
508
  "reduction": 13.65
509
509
  },
510
510
  {
@@ -726,6 +726,18 @@
726
726
  "percent": 0.04,
727
727
  "reduction": 93.8
728
728
  },
729
+ {
730
+ "id": "/src/http.ts",
731
+ "size": 0,
732
+ "origSize": 37,
733
+ "renderedExports": [],
734
+ "removedExports": [],
735
+ "dependents": [
736
+ "/src/adapters/ws.ts"
737
+ ],
738
+ "percent": 0,
739
+ "reduction": 100
740
+ },
729
741
  {
730
742
  "id": "/src/index.ts",
731
743
  "size": 0,
@@ -747,13 +759,14 @@
747
759
  "reduction": 100
748
760
  },
749
761
  {
750
- "id": "/src/http.ts",
762
+ "id": "/src/unstable-core-do-not-import.ts",
751
763
  "size": 0,
752
- "origSize": 37,
764
+ "origSize": 2306,
753
765
  "renderedExports": [],
754
766
  "removedExports": [],
755
767
  "dependents": [
756
- "/src/adapters/ws.ts"
768
+ "/src/adapters/ws.ts",
769
+ "/src/adapters/next-app-dir/nextAppDirCaller.ts"
757
770
  ],
758
771
  "percent": 0,
759
772
  "reduction": 100
@@ -768,19 +781,6 @@
768
781
  "percent": 0,
769
782
  "reduction": 100
770
783
  },
771
- {
772
- "id": "/src/unstable-core-do-not-import.ts",
773
- "size": 0,
774
- "origSize": 2306,
775
- "renderedExports": [],
776
- "removedExports": [],
777
- "dependents": [
778
- "/src/adapters/ws.ts",
779
- "/src/adapters/next-app-dir/nextAppDirCaller.ts"
780
- ],
781
- "percent": 0,
782
- "reduction": 100
783
- },
784
784
  {
785
785
  "id": "/src/adapters/next-app-dir.ts",
786
786
  "size": 0,
@@ -813,16 +813,6 @@
813
813
  "percent": 0,
814
814
  "reduction": 100
815
815
  },
816
- {
817
- "id": "/src/adapters/fetch/index.ts",
818
- "size": 0,
819
- "origSize": 64,
820
- "renderedExports": [],
821
- "removedExports": [],
822
- "dependents": [],
823
- "percent": 0,
824
- "reduction": 100
825
- },
826
816
  {
827
817
  "id": "/src/adapters/node-http/index.ts",
828
818
  "size": 0,
@@ -837,6 +827,16 @@
837
827
  ],
838
828
  "percent": 0,
839
829
  "reduction": 100
830
+ },
831
+ {
832
+ "id": "/src/adapters/fetch/index.ts",
833
+ "size": 0,
834
+ "origSize": 64,
835
+ "renderedExports": [],
836
+ "removedExports": [],
837
+ "dependents": [],
838
+ "percent": 0,
839
+ "reduction": 100
840
840
  }
841
841
  ],
842
842
  "moduleCount": 56
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trpc/server",
3
- "version": "11.0.0-rc.528+32e6b1285",
3
+ "version": "11.0.0-rc.531+42d5af1b1",
4
4
  "description": "The tRPC server library",
5
5
  "author": "KATT",
6
6
  "license": "MIT",
@@ -149,5 +149,5 @@
149
149
  "funding": [
150
150
  "https://trpc.io/sponsor"
151
151
  ],
152
- "gitHead": "32e6b1285dd844776d323ae23fbea638312d676e"
152
+ "gitHead": "42d5af1b189a1b80b1f128deb81550b1ddc6eeb6"
153
153
  }
@@ -79,20 +79,26 @@ export type WSSHandlerOptions<TRouter extends AnyRouter> =
79
79
  enabled: boolean;
80
80
  /**
81
81
  * Heartbeat interval in milliseconds
82
- * @default 30000
82
+ * @default 30_000
83
83
  */
84
84
  pingMs?: number;
85
85
  /**
86
86
  * Terminate the WebSocket if no pong is received after this many milliseconds
87
- * @default 5000
87
+ * @default 5_000
88
88
  */
89
89
  pongWaitMs?: number;
90
90
  };
91
+ /**
92
+ * Disable responding to ping messages from the client
93
+ * **Not recommended** - this is mainly used for testing
94
+ * @default false
95
+ */
96
+ dangerouslyDisablePong?: boolean;
91
97
  };
92
98
 
93
99
  const unsetContextPromiseSymbol = Symbol('unsetContextPromise');
94
100
  export function getWSConnectionHandler<TRouter extends AnyRouter>(
95
- opts: WSConnectionHandlerOptions<TRouter>,
101
+ opts: WSSHandlerOptions<TRouter>,
96
102
  ) {
97
103
  const { createContext, router } = opts;
98
104
  const { transformer } = router._def._config;
@@ -101,6 +107,11 @@ export function getWSConnectionHandler<TRouter extends AnyRouter>(
101
107
  const clientSubscriptions = new Map<number | string, AbortController>();
102
108
  const abortController = new AbortController();
103
109
 
110
+ if (opts.keepAlive?.enabled) {
111
+ const { pingMs, pongWaitMs } = opts.keepAlive;
112
+ handleKeepAlive(client, pingMs, pongWaitMs);
113
+ }
114
+
104
115
  function respond(untransformedJSON: TRPCResponseMessage) {
105
116
  client.send(
106
117
  JSON.stringify(
@@ -377,13 +388,23 @@ export function getWSConnectionHandler<TRouter extends AnyRouter>(
377
388
  });
378
389
  }
379
390
  }
380
- client.on('message', async (message) => {
391
+ client.on('message', async (rawData) => {
392
+ const msgStr = rawData.toString();
393
+ if (msgStr === 'PONG') {
394
+ return;
395
+ }
396
+ if (msgStr === 'PING') {
397
+ if (!opts.dangerouslyDisablePong) {
398
+ client.send('PONG');
399
+ }
400
+ return;
401
+ }
381
402
  if (ctxPromise === unsetContextPromiseSymbol) {
382
403
  // If the ctxPromise wasn't created immediately, we're expecting the first message to be a TRPCConnectionParamsMessage
383
404
  ctxPromise = createCtxPromise(() => {
384
405
  let msg;
385
406
  try {
386
- msg = JSON.parse(message.toString()) as TRPCConnectionParamsMessage;
407
+ msg = JSON.parse(msgStr) as TRPCConnectionParamsMessage;
387
408
 
388
409
  if (!isObject(msg)) {
389
410
  throw new Error('Message was not an object');
@@ -403,7 +424,7 @@ export function getWSConnectionHandler<TRouter extends AnyRouter>(
403
424
  return;
404
425
  }
405
426
  try {
406
- const msgJSON: unknown = JSON.parse(message.toString());
427
+ const msgJSON: unknown = JSON.parse(msgStr);
407
428
  const msgs: unknown[] = Array.isArray(msgJSON) ? msgJSON : [msgJSON];
408
429
  const promises = msgs
409
430
  .map((raw) => parseTRPCMessage(raw, transformer))
@@ -453,8 +474,7 @@ export function getWSConnectionHandler<TRouter extends AnyRouter>(
453
474
  });
454
475
 
455
476
  if (ctxPromise !== unsetContextPromiseSymbol) {
456
- // prevent unhandled promise rejection errors
457
- await ctxPromise.catch(() => null);
477
+ await ctxPromise;
458
478
  }
459
479
  };
460
480
  }
@@ -464,48 +484,67 @@ export function getWSConnectionHandler<TRouter extends AnyRouter>(
464
484
  */
465
485
  export function handleKeepAlive(
466
486
  client: ws.WebSocket,
467
- pingMs = 30000,
468
- pongWaitMs = 5000,
487
+ pingMs = 30_000,
488
+ pongWaitMs = 5_000,
469
489
  ) {
470
- let heartbeatTimeout: NodeJS.Timeout | undefined;
471
- const heartbeatInterval = setInterval(() => {
472
- if (client.readyState !== WEBSOCKET_OPEN) {
473
- return;
474
- }
475
- // First we send a ping message and wait for a pong
476
- client.ping();
477
- // We set a timeout to close the connection if the pong is not received
478
- heartbeatTimeout = setTimeout(() => {
479
- client.terminate();
480
- clearInterval(heartbeatInterval);
481
- }, pongWaitMs);
482
- }, pingMs).unref();
483
- // When we receive a pong message, we clear the timeout
484
- client.on('pong', () => {
485
- heartbeatTimeout && clearTimeout(heartbeatTimeout);
486
- });
487
- // If the connection is closed, we clear the interval
490
+ let timeout: NodeJS.Timeout | undefined = undefined;
491
+ let ping: NodeJS.Timeout | undefined = undefined;
492
+
493
+ const schedulePing = () => {
494
+ const scheduleTimeout = () => {
495
+ timeout = setTimeout(() => {
496
+ client.terminate();
497
+ }, pongWaitMs);
498
+ };
499
+ ping = setTimeout(() => {
500
+ client.send('PING');
501
+
502
+ scheduleTimeout();
503
+ }, pingMs);
504
+ };
505
+
506
+ const onMessage = () => {
507
+ clearTimeout(ping);
508
+ clearTimeout(timeout);
509
+
510
+ schedulePing();
511
+ };
512
+
513
+ client.on('message', onMessage);
514
+
488
515
  client.on('close', () => {
489
- clearInterval(heartbeatInterval);
516
+ clearTimeout(ping);
517
+ clearTimeout(timeout);
490
518
  });
519
+
520
+ schedulePing();
491
521
  }
492
522
 
493
523
  export function applyWSSHandler<TRouter extends AnyRouter>(
494
524
  opts: WSSHandlerOptions<TRouter>,
495
525
  ) {
496
- const { wss, prefix, keepAlive } = opts;
497
-
498
526
  const onConnection = getWSConnectionHandler(opts);
499
- wss.on('connection', async (client, req) => {
500
- if (prefix && !req.url?.startsWith(prefix)) {
527
+ opts.wss.on('connection', (client, req) => {
528
+ if (opts.prefix && !req.url?.startsWith(opts.prefix)) {
501
529
  return;
502
530
  }
503
531
 
504
- await onConnection(client, req);
505
- if (keepAlive?.enabled) {
506
- const { pingMs, pongWaitMs } = keepAlive;
507
- handleKeepAlive(client, pingMs, pongWaitMs);
508
- }
532
+ onConnection(client, req).catch((cause) => {
533
+ opts.onError?.({
534
+ error: new TRPCError({
535
+ code: 'INTERNAL_SERVER_ERROR',
536
+ cause,
537
+ message: 'Failed to handle WebSocket connection',
538
+ }),
539
+ req: req,
540
+ path: undefined,
541
+ type: 'unknown',
542
+ ctx: undefined,
543
+ input: undefined,
544
+ });
545
+
546
+ client.close();
547
+ });
509
548
  });
510
549
 
511
550
  return {
@@ -515,7 +554,7 @@ export function applyWSSHandler<TRouter extends AnyRouter>(
515
554
  method: 'reconnect',
516
555
  };
517
556
  const data = JSON.stringify(response);
518
- for (const client of wss.clients) {
557
+ for (const client of opts.wss.clients) {
519
558
  if (client.readyState === WEBSOCKET_OPEN) {
520
559
  client.send(data);
521
560
  }