@trpc/server 11.8.2-canary.3 → 11.9.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.
@@ -10,8 +10,9 @@ require('../initTRPC--HrEu8sH.cjs');
10
10
  require('../http-DXy3XyhL.cjs');
11
11
  require('../node-http-D0T_XJ9C.cjs');
12
12
  require('../observable-BVzLuBs6.cjs');
13
- const require_ws = require('../ws-B16sko8C.cjs');
13
+ const require_ws = require('../ws-DpjAOm3h.cjs');
14
14
 
15
15
  exports.applyWSSHandler = require_ws.applyWSSHandler;
16
16
  exports.getWSConnectionHandler = require_ws.getWSConnectionHandler;
17
- exports.handleKeepAlive = require_ws.handleKeepAlive;
17
+ exports.handleKeepAlive = require_ws.handleKeepAlive;
18
+ exports.jsonEncoder = require_ws.jsonEncoder;
@@ -5,8 +5,34 @@ import { NodeHTTPCreateContextFnOptions } from "../index.d--uNJFzUS.cjs";
5
5
  import { IncomingMessage } from "http";
6
6
  import ws from "ws";
7
7
 
8
- //#region src/adapters/ws.d.ts
8
+ //#region src/adapters/wsEncoder.d.ts
9
9
 
10
+ /**
11
+ * Encoder for WebSocket wire format.
12
+ * Encodes outgoing messages and decodes incoming messages.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * const customEncoder: Encoder = {
17
+ * encode: (data) => myFormat.stringify(data),
18
+ * decode: (data) => myFormat.parse(data),
19
+ * };
20
+ * ```
21
+ */
22
+ interface Encoder {
23
+ /** Encode data for transmission over the wire */
24
+ encode(data: unknown): string | Uint8Array;
25
+ /** Decode data received from the wire */
26
+ decode(data: string | ArrayBuffer | Uint8Array): unknown;
27
+ }
28
+ /**
29
+ * Default JSON encoder - used when no encoder is specified.
30
+ * This maintains backwards compatibility with existing behavior.
31
+ */
32
+ declare const jsonEncoder: Encoder;
33
+ //# sourceMappingURL=wsEncoder.d.ts.map
34
+ //#endregion
35
+ //#region src/adapters/ws.d.ts
10
36
  /**
11
37
  * @public
12
38
  */
@@ -45,6 +71,11 @@ type WSSHandlerOptions<TRouter extends AnyRouter> = WSConnectionHandlerOptions<T
45
71
  * @default false
46
72
  */
47
73
  dangerouslyDisablePong?: boolean;
74
+ /**
75
+ * Custom encoder for wire encoding (e.g. custom binary formats)
76
+ * @default jsonEncoder
77
+ */
78
+ experimental_encoder?: Encoder;
48
79
  };
49
80
  declare function getWSConnectionHandler<TRouter extends AnyRouter>(opts: WSSHandlerOptions<TRouter>): (client: ws.WebSocket, req: IncomingMessage) => void;
50
81
  /**
@@ -54,8 +85,6 @@ declare function handleKeepAlive(client: ws.WebSocket, pingMs?: number, pongWait
54
85
  declare function applyWSSHandler<TRouter extends AnyRouter>(opts: WSSHandlerOptions<TRouter>): {
55
86
  broadcastReconnectNotification: () => void;
56
87
  };
57
- //# sourceMappingURL=ws.d.ts.map
58
-
59
88
  //#endregion
60
- export { CreateWSSContextFn, CreateWSSContextFnOptions, WSConnectionHandlerOptions, WSSHandlerOptions, applyWSSHandler, getWSConnectionHandler, handleKeepAlive };
89
+ export { CreateWSSContextFn, CreateWSSContextFnOptions, Encoder, WSConnectionHandlerOptions, WSSHandlerOptions, applyWSSHandler, getWSConnectionHandler, handleKeepAlive, jsonEncoder };
61
90
  //# sourceMappingURL=ws.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ws.d.cts","names":[],"sources":["../../src/adapters/ws.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;AAmDA;AAAqC,KAAzB,yBAAA,GAA4B,8BAAH,CACnC,eADmC,EAEnC,EAAA,CAAG,SAFgC,CAAA;;;;AAAiC,KAQ1D,kBAR0D,CAAA,gBAQvB,SARuB,CAAA,GAAA,CAAA,IAAA,EAS9D,yBAT8D,EAAA,GAUjE,YAViE,CAUpD,kBAVoD,CAUjC,OAViC,CAAA,CAAA;AAQ1D,KAIA,0BAJkB,CAAA,gBAIyB,SAJzB,CAAA,GAK5B,kBAL4B,CAKT,OALS,EAKA,eALA,CAAA,GAM1B,qBAN0B,CAOxB,kBAPwB,CAOL,OAPK,CAAA,EAQxB,kBARwB,CAQL,OARK,CAAA,CAAA;;;;AAEO,KAYzB,iBAZyB,CAAA,gBAYS,SAZT,CAAA,GAanC,0BAbmC,CAaR,OAbQ,CAAA,GAAA;EAAO,GAA1B,EAcT,EAAA,CAAG,eAdM;EAAkB,MAA/B,CAAA,EAAA,MAAA;EAAY,SAAA,CAAA,EAAA;IAEL;;;;IACkB,OAAA,EAAA,OAAA;IAA5B;;;;IAGI,MAAA,CAAA,EAAA,MAAA;IAFF;AAAqB;AAQzB;;IAA8C,UAAA,CAAA,EAAA,MAAA;EAAS,CAAA;EACnB;;AACT;AA2B3B;;EAAsC,sBAAiB,CAAA,EAAA,OAAA;CAAS;AACxD,iBADQ,sBACR,CAAA,gBAD+C,SAC/C,CAAA,CAAA,IAAA,EAAA,iBAAA,CAAkB,OAAlB,CAAA,CAAA,EAAA,CAAA,MAAA,EAKU,EAAA,CAAG,SALb,EAAA,GAAA,EAK6B,eAL7B,EAAA,GAAA,IAAA;;;AAK4C;AA8apC,iBAAA,eAAA,CACH,MAAS,EAAZ,EAAA,CAAG,SAAS,EAAA,MAAA,CAAA,EAAA,MAAA,EAAA,UAAA,CAAA,EAAA,MAAA,CAAA,EAAA,IAAA;AAqCN,iBAAA,eAAe,CAAA,gBAAiB,SAAjB,CAAA,CAAA,IAAA,EACvB,iBADuB,CACL,OADK,CAAA,CAAA,EAAA;EAAA,8BAAA,EAAA,GAAA,GAAA,IAAA;CAAA"}
1
+ {"version":3,"file":"ws.d.cts","names":[],"sources":["../../src/adapters/wsEncoder.ts","../../src/adapters/ws.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;;;AAYA;;;;AAIsC,UAJrB,OAAA,CAIqB;EAAU;EAOnC,MAAA,CAAA,IAAA,EAAA,OAWZ,CAAA,EAAA,MAXyB,GATQ,UAoBjC;;wBAlBuB,cAAc;;ACqCtC;;;;AAAwC,cD9B3B,WC8B2B,ED9Bd,OC8Bc;AAA8B;;;;ADzCtE;;AAEkC,KCuCtB,yBAAA,GAA4B,8BDvCN,CCwChC,eDxCgC,ECyChC,EAAA,CAAG,SDzC6B,CAAA;;;AAEc;AAOnC,KCsCD,kBDtCc,CAAA,gBCsCqB,SD3B9C,CAAA,GAAA,CAAA,IAAA,EC4BO,yBD5BP,EAAA,GC6BI,YD7BJ,CC6BiB,kBD7BjB,CC6BoC,OD7BpC,CAAA,CAAA;KC+BW,2CAA2C,aACrD,mBAAmB,SAAS,mBAC1B,sBACE,mBAAmB,UACnB,mBAAmB;;;AAhBzB;AAAqC,KAsBzB,iBAtByB,CAAA,gBAsBS,SAtBT,CAAA,GAuBnC,0BAvBmC,CAuBR,OAvBQ,CAAA,GAAA;EAAA,GACnC,EAuBO,EAAA,CAAG,eAvBV;EAAe,MACZ,CAAA,EAAA,MAAA;EAAS,SAF0B,CAAA,EAAA;IAA8B;AAQtE;;;IACQ,OAAA,EAAA,OAAA;IAC6B;;;AAApB;IAEL,MAAA,CAAA,EAAA,MAAA;IAA0B;;;;IACpC,UAAA,CAAA,EAAA,MAAA;EAAkB,CAAA;EAEY;;;;AADP;EAQb,sBAAiB,CAAA,EAAA,OAAA;EAAA;;;;EACD,oBAChB,CAAA,EA6Be,OA7Bf;CAAe;AA6BO,iBAGlB,sBAHkB,CAAA,gBAGqB,SAHrB,CAAA,CAAA,IAAA,EAI1B,iBAJ0B,CAIR,OAJQ,CAAA,CAAA,EAAA,CAAA,MAAA,EAUhB,EAAA,CAAG,SAVa,EAAA,GAAA,EAUG,eAVH,EAAA,GAAA,IAAA;AAGlC;;;AAC0B,iBAgdV,eAAA,CAhdU,MAAA,EAidhB,EAAA,CAAG,SAjda,EAAA,MAAA,CAAA,EAAA,MAAA,EAAA,UAAA,CAAA,EAAA,MAAA,CAAA,EAAA,IAAA;AAAlB,iBAsfQ,eAtfR,CAAA,gBAsfwC,SAtfxC,CAAA,CAAA,IAAA,EAufA,iBAvfA,CAufkB,OAvflB,CAAA,CAAA,EAAA;EAAiB,8BAMJ,EAAA,GAAA,GAAA,IAAA;CAAS"}
@@ -5,8 +5,34 @@ import { NodeHTTPCreateContextFnOptions } from "../index.d-D7vhS0-R.mjs";
5
5
  import { IncomingMessage } from "http";
6
6
  import ws from "ws";
7
7
 
8
- //#region src/adapters/ws.d.ts
8
+ //#region src/adapters/wsEncoder.d.ts
9
9
 
10
+ /**
11
+ * Encoder for WebSocket wire format.
12
+ * Encodes outgoing messages and decodes incoming messages.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * const customEncoder: Encoder = {
17
+ * encode: (data) => myFormat.stringify(data),
18
+ * decode: (data) => myFormat.parse(data),
19
+ * };
20
+ * ```
21
+ */
22
+ interface Encoder {
23
+ /** Encode data for transmission over the wire */
24
+ encode(data: unknown): string | Uint8Array;
25
+ /** Decode data received from the wire */
26
+ decode(data: string | ArrayBuffer | Uint8Array): unknown;
27
+ }
28
+ /**
29
+ * Default JSON encoder - used when no encoder is specified.
30
+ * This maintains backwards compatibility with existing behavior.
31
+ */
32
+ declare const jsonEncoder: Encoder;
33
+ //# sourceMappingURL=wsEncoder.d.ts.map
34
+ //#endregion
35
+ //#region src/adapters/ws.d.ts
10
36
  /**
11
37
  * @public
12
38
  */
@@ -45,6 +71,11 @@ type WSSHandlerOptions<TRouter extends AnyRouter> = WSConnectionHandlerOptions<T
45
71
  * @default false
46
72
  */
47
73
  dangerouslyDisablePong?: boolean;
74
+ /**
75
+ * Custom encoder for wire encoding (e.g. custom binary formats)
76
+ * @default jsonEncoder
77
+ */
78
+ experimental_encoder?: Encoder;
48
79
  };
49
80
  declare function getWSConnectionHandler<TRouter extends AnyRouter>(opts: WSSHandlerOptions<TRouter>): (client: ws.WebSocket, req: IncomingMessage) => void;
50
81
  /**
@@ -54,8 +85,6 @@ declare function handleKeepAlive(client: ws.WebSocket, pingMs?: number, pongWait
54
85
  declare function applyWSSHandler<TRouter extends AnyRouter>(opts: WSSHandlerOptions<TRouter>): {
55
86
  broadcastReconnectNotification: () => void;
56
87
  };
57
- //# sourceMappingURL=ws.d.ts.map
58
-
59
88
  //#endregion
60
- export { CreateWSSContextFn, CreateWSSContextFnOptions, WSConnectionHandlerOptions, WSSHandlerOptions, applyWSSHandler, getWSConnectionHandler, handleKeepAlive };
89
+ export { CreateWSSContextFn, CreateWSSContextFnOptions, Encoder, WSConnectionHandlerOptions, WSSHandlerOptions, applyWSSHandler, getWSConnectionHandler, handleKeepAlive, jsonEncoder };
61
90
  //# sourceMappingURL=ws.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ws.d.mts","names":[],"sources":["../../src/adapters/ws.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;AAmDA;AAAqC,KAAzB,yBAAA,GAA4B,8BAAH,CACnC,eADmC,EAEnC,EAAA,CAAG,SAFgC,CAAA;;;;AAAiC,KAQ1D,kBAR0D,CAAA,gBAQvB,SARuB,CAAA,GAAA,CAAA,IAAA,EAS9D,yBAT8D,EAAA,GAUjE,YAViE,CAUpD,kBAVoD,CAUjC,OAViC,CAAA,CAAA;AAQ1D,KAIA,0BAJkB,CAAA,gBAIyB,SAJzB,CAAA,GAK5B,kBAL4B,CAKT,OALS,EAKA,eALA,CAAA,GAM1B,qBAN0B,CAOxB,kBAPwB,CAOL,OAPK,CAAA,EAQxB,kBARwB,CAQL,OARK,CAAA,CAAA;;;;AAEO,KAYzB,iBAZyB,CAAA,gBAYS,SAZT,CAAA,GAanC,0BAbmC,CAaR,OAbQ,CAAA,GAAA;EAAO,GAA1B,EAcT,EAAA,CAAG,eAdM;EAAkB,MAA/B,CAAA,EAAA,MAAA;EAAY,SAAA,CAAA,EAAA;IAEL;;;;IACkB,OAAA,EAAA,OAAA;IAA5B;;;;IAGI,MAAA,CAAA,EAAA,MAAA;IAFF;AAAqB;AAQzB;;IAA8C,UAAA,CAAA,EAAA,MAAA;EAAS,CAAA;EACnB;;AACT;AA2B3B;;EAAsC,sBAAiB,CAAA,EAAA,OAAA;CAAS;AACxD,iBADQ,sBACR,CAAA,gBAD+C,SAC/C,CAAA,CAAA,IAAA,EAAA,iBAAA,CAAkB,OAAlB,CAAA,CAAA,EAAA,CAAA,MAAA,EAKU,EAAA,CAAG,SALb,EAAA,GAAA,EAK6B,eAL7B,EAAA,GAAA,IAAA;;;AAK4C;AA8apC,iBAAA,eAAA,CACH,MAAS,EAAZ,EAAA,CAAG,SAAS,EAAA,MAAA,CAAA,EAAA,MAAA,EAAA,UAAA,CAAA,EAAA,MAAA,CAAA,EAAA,IAAA;AAqCN,iBAAA,eAAe,CAAA,gBAAiB,SAAjB,CAAA,CAAA,IAAA,EACvB,iBADuB,CACL,OADK,CAAA,CAAA,EAAA;EAAA,8BAAA,EAAA,GAAA,GAAA,IAAA;CAAA"}
1
+ {"version":3,"file":"ws.d.mts","names":[],"sources":["../../src/adapters/wsEncoder.ts","../../src/adapters/ws.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;;;AAYA;;;;AAIsC,UAJrB,OAAA,CAIqB;EAAU;EAOnC,MAAA,CAAA,IAAA,EAAA,OAWZ,CAAA,EAAA,MAXyB,GATQ,UAoBjC;;wBAlBuB,cAAc;;ACqCtC;;;;AAAwC,cD9B3B,WC8B2B,ED9Bd,OC8Bc;AAA8B;;;;ADzCtE;;AAEkC,KCuCtB,yBAAA,GAA4B,8BDvCN,CCwChC,eDxCgC,ECyChC,EAAA,CAAG,SDzC6B,CAAA;;;AAEc;AAOnC,KCsCD,kBDtCc,CAAA,gBCsCqB,SD3B9C,CAAA,GAAA,CAAA,IAAA,EC4BO,yBD5BP,EAAA,GC6BI,YD7BJ,CC6BiB,kBD7BjB,CC6BoC,OD7BpC,CAAA,CAAA;KC+BW,2CAA2C,aACrD,mBAAmB,SAAS,mBAC1B,sBACE,mBAAmB,UACnB,mBAAmB;;;AAhBzB;AAAqC,KAsBzB,iBAtByB,CAAA,gBAsBS,SAtBT,CAAA,GAuBnC,0BAvBmC,CAuBR,OAvBQ,CAAA,GAAA;EAAA,GACnC,EAuBO,EAAA,CAAG,eAvBV;EAAe,MACZ,CAAA,EAAA,MAAA;EAAS,SAF0B,CAAA,EAAA;IAA8B;AAQtE;;;IACQ,OAAA,EAAA,OAAA;IAC6B;;;AAApB;IAEL,MAAA,CAAA,EAAA,MAAA;IAA0B;;;;IACpC,UAAA,CAAA,EAAA,MAAA;EAAkB,CAAA;EAEY;;;;AADP;EAQb,sBAAiB,CAAA,EAAA,OAAA;EAAA;;;;EACD,oBAChB,CAAA,EA6Be,OA7Bf;CAAe;AA6BO,iBAGlB,sBAHkB,CAAA,gBAGqB,SAHrB,CAAA,CAAA,IAAA,EAI1B,iBAJ0B,CAIR,OAJQ,CAAA,CAAA,EAAA,CAAA,MAAA,EAUhB,EAAA,CAAG,SAVa,EAAA,GAAA,EAUG,eAVH,EAAA,GAAA,IAAA;AAGlC;;;AAC0B,iBAgdV,eAAA,CAhdU,MAAA,EAidhB,EAAA,CAAG,SAjda,EAAA,MAAA,CAAA,EAAA,MAAA,EAAA,UAAA,CAAA,EAAA,MAAA,CAAA,EAAA,IAAA;AAAlB,iBAsfQ,eAtfR,CAAA,gBAsfwC,SAtfxC,CAAA,CAAA,IAAA,EAufA,iBAvfA,CAufkB,OAvflB,CAAA,CAAA,EAAA;EAAiB,8BAMJ,EAAA,GAAA,GAAA,IAAA;CAAS"}
@@ -10,6 +10,6 @@ import "../initTRPC-T5bbc89W.mjs";
10
10
  import "../http-CWyjOa1l.mjs";
11
11
  import "../node-http-pD5xpNfK.mjs";
12
12
  import "../observable-CUiPknO-.mjs";
13
- import { applyWSSHandler, getWSConnectionHandler, handleKeepAlive } from "../ws-Dl6Y2YNA.mjs";
13
+ import { applyWSSHandler, getWSConnectionHandler, handleKeepAlive, jsonEncoder } from "../ws-DCeuK64x.mjs";
14
14
 
15
- export { applyWSSHandler, getWSConnectionHandler, handleKeepAlive };
15
+ export { applyWSSHandler, getWSConnectionHandler, handleKeepAlive, jsonEncoder };
@@ -6,6 +6,20 @@ import { Unpromise, iteratorResource, parseConnectionParamsFromUnknown, require_
6
6
  import { isObservable, observableToAsyncIterable } from "./observable-UMO3vUa_.mjs";
7
7
  import { createURL } from "./node-http-pD5xpNfK.mjs";
8
8
 
9
+ //#region src/adapters/wsEncoder.ts
10
+ /**
11
+ * Default JSON encoder - used when no encoder is specified.
12
+ * This maintains backwards compatibility with existing behavior.
13
+ */
14
+ const jsonEncoder = {
15
+ encode: (data) => JSON.stringify(data),
16
+ decode: (data) => {
17
+ if (typeof data !== "string") throw new Error("jsonEncoder received binary data. JSON uses text frames. Use a binary encoder for binary data.");
18
+ return JSON.parse(data);
19
+ }
20
+ };
21
+
22
+ //#endregion
9
23
  //#region src/adapters/ws.ts
10
24
  var import_objectSpread2 = __toESM(require_objectSpread2(), 1);
11
25
  var import_usingCtx = __toESM(require_usingCtx(), 1);
@@ -15,8 +29,10 @@ var import_usingCtx = __toESM(require_usingCtx(), 1);
15
29
  */
16
30
  const WEBSOCKET_OPEN = 1;
17
31
  function getWSConnectionHandler(opts) {
32
+ var _opts$experimental_en;
18
33
  const { createContext, router } = opts;
19
34
  const { transformer } = router._def._config;
35
+ const encoder = (_opts$experimental_en = opts.experimental_encoder) !== null && _opts$experimental_en !== void 0 ? _opts$experimental_en : jsonEncoder;
20
36
  return (client, req) => {
21
37
  var _opts$keepAlive;
22
38
  const clientSubscriptions = /* @__PURE__ */ new Map();
@@ -26,7 +42,7 @@ function getWSConnectionHandler(opts) {
26
42
  handleKeepAlive(client, pingMs, pongWaitMs);
27
43
  }
28
44
  function respond(untransformedJSON) {
29
- client.send(JSON.stringify(transformTRPCResponse(router._def._config, untransformedJSON)));
45
+ client.send(encoder.encode(transformTRPCResponse(router._def._config, untransformedJSON)));
30
46
  }
31
47
  async function createCtxPromise(getConnectionParams) {
32
48
  try {
@@ -300,18 +316,39 @@ function getWSConnectionHandler(opts) {
300
316
  });
301
317
  });
302
318
  }
303
- client.on("message", (rawData) => {
304
- const msgStr = rawData.toString();
305
- if (msgStr === "PONG") return;
306
- if (msgStr === "PING") {
307
- if (!opts.dangerouslyDisablePong) client.send("PONG");
319
+ client.on("message", (rawData, isBinary) => {
320
+ if (!isBinary) {
321
+ const msgStr = rawData.toString();
322
+ if (msgStr === "PONG") return;
323
+ if (msgStr === "PING") {
324
+ if (!opts.dangerouslyDisablePong) client.send("PONG");
325
+ return;
326
+ }
327
+ }
328
+ if (!Buffer.isBuffer(rawData)) {
329
+ const error = new TRPCError({
330
+ code: "UNPROCESSABLE_CONTENT",
331
+ message: "Unexpected WebSocket message format"
332
+ });
333
+ respond({
334
+ id: null,
335
+ error: getErrorShape({
336
+ config: router._def._config,
337
+ error,
338
+ type: "unknown",
339
+ path: void 0,
340
+ input: void 0,
341
+ ctx
342
+ })
343
+ });
308
344
  return;
309
345
  }
346
+ const data = isBinary ? rawData : rawData.toString("utf8");
310
347
  if (!ctxPromise) {
311
348
  ctxPromise = createCtxPromise(() => {
312
349
  let msg;
313
350
  try {
314
- msg = JSON.parse(msgStr);
351
+ msg = encoder.decode(data);
315
352
  if (!isObject(msg)) throw new Error("Message was not an object");
316
353
  } catch (cause) {
317
354
  throw new TRPCError({
@@ -327,7 +364,7 @@ function getWSConnectionHandler(opts) {
327
364
  }
328
365
  const parsedMsgs = run(() => {
329
366
  try {
330
- const msgJSON = JSON.parse(msgStr);
367
+ const msgJSON = encoder.decode(data);
331
368
  const msgs = Array.isArray(msgJSON) ? msgJSON : [msgJSON];
332
369
  return msgs.map((raw) => parseTRPCMessage(raw, transformer));
333
370
  } catch (cause) {
@@ -399,6 +436,8 @@ function handleKeepAlive(client, pingMs = 3e4, pongWaitMs = 5e3) {
399
436
  schedulePing();
400
437
  }
401
438
  function applyWSSHandler(opts) {
439
+ var _opts$experimental_en2;
440
+ const encoder = (_opts$experimental_en2 = opts.experimental_encoder) !== null && _opts$experimental_en2 !== void 0 ? _opts$experimental_en2 : jsonEncoder;
402
441
  const onConnection = getWSConnectionHandler(opts);
403
442
  opts.wss.on("connection", (client, req) => {
404
443
  var _req$url;
@@ -410,11 +449,11 @@ function applyWSSHandler(opts) {
410
449
  id: null,
411
450
  method: "reconnect"
412
451
  };
413
- const data = JSON.stringify(response);
452
+ const data = encoder.encode(response);
414
453
  for (const client of opts.wss.clients) if (client.readyState === WEBSOCKET_OPEN) client.send(data);
415
454
  } };
416
455
  }
417
456
 
418
457
  //#endregion
419
- export { applyWSSHandler, getWSConnectionHandler, handleKeepAlive };
420
- //# sourceMappingURL=ws-Dl6Y2YNA.mjs.map
458
+ export { applyWSSHandler, getWSConnectionHandler, handleKeepAlive, jsonEncoder };
459
+ //# sourceMappingURL=ws-DCeuK64x.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws-DCeuK64x.mjs","names":["jsonEncoder: Encoder","opts: WSSHandlerOptions<TRouter>","client: ws.WebSocket","req: IncomingMessage","untransformedJSON: TRPCResponseMessage","getConnectionParams: () => TRPCRequestInfo['connectionParams']","ctx: Context | undefined","msg: TRPCClientOutgoingMessage","abortController","next:\n | null\n | TRPCError\n | Awaited<\n typeof abortPromise | ReturnType<(typeof iterator)['next']>\n >","result: null | TRPCResultMessage<unknown>['result']","id","data: string | Uint8Array","msgJSON: unknown","msgs: unknown[]","timeout: NodeJS.Timeout | undefined","ping: NodeJS.Timeout | undefined","response: TRPCReconnectNotification"],"sources":["../src/adapters/wsEncoder.ts","../src/adapters/ws.ts"],"sourcesContent":["/**\n * Encoder for WebSocket wire format.\n * Encodes outgoing messages and decodes incoming messages.\n *\n * @example\n * ```ts\n * const customEncoder: Encoder = {\n * encode: (data) => myFormat.stringify(data),\n * decode: (data) => myFormat.parse(data),\n * };\n * ```\n */\nexport interface Encoder {\n /** Encode data for transmission over the wire */\n encode(data: unknown): string | Uint8Array;\n /** Decode data received from the wire */\n decode(data: string | ArrayBuffer | Uint8Array): unknown;\n}\n\n/**\n * Default JSON encoder - used when no encoder is specified.\n * This maintains backwards compatibility with existing behavior.\n */\nexport const jsonEncoder: Encoder = {\n encode: (data) => JSON.stringify(data),\n decode: (data) => {\n if (typeof data !== 'string') {\n throw new Error(\n 'jsonEncoder received binary data. JSON uses text frames. ' +\n 'Use a binary encoder for binary data.',\n );\n }\n return JSON.parse(data);\n },\n};\n","import type { IncomingMessage } from 'http';\nimport type ws from 'ws';\nimport type {\n AnyRouter,\n CreateContextCallback,\n inferRouterContext,\n} from '../@trpc/server';\nimport {\n callTRPCProcedure,\n getErrorShape,\n getTRPCErrorFromUnknown,\n transformTRPCResponse,\n TRPCError,\n} from '../@trpc/server';\nimport type { TRPCRequestInfo } from '../@trpc/server/http';\nimport { type BaseHandlerOptions } from '../@trpc/server/http';\nimport { parseTRPCMessage } from '../@trpc/server/rpc';\n// @trpc/server/rpc\nimport type {\n TRPCClientOutgoingMessage,\n TRPCConnectionParamsMessage,\n TRPCReconnectNotification,\n TRPCResponseMessage,\n TRPCResultMessage,\n} from '../@trpc/server/rpc';\nimport { parseConnectionParamsFromUnknown } from '../http';\nimport { isObservable, observableToAsyncIterable } from '../observable';\n// eslint-disable-next-line no-restricted-imports\nimport {\n isAsyncIterable,\n isObject,\n isTrackedEnvelope,\n run,\n type MaybePromise,\n} from '../unstable-core-do-not-import';\n// eslint-disable-next-line no-restricted-imports\nimport type { Result } from '../unstable-core-do-not-import';\n// eslint-disable-next-line no-restricted-imports\nimport { iteratorResource } from '../unstable-core-do-not-import/stream/utils/asyncIterable';\nimport { Unpromise } from '../vendor/unpromise';\nimport { createURL, type NodeHTTPCreateContextFnOptions } from './node-http';\nimport type { Encoder } from './wsEncoder';\nimport { jsonEncoder } from './wsEncoder';\n\n/**\n * Importing ws causes a build error\n * @see https://github.com/trpc/trpc/pull/5279\n */\nconst WEBSOCKET_OPEN = 1; /* ws.WebSocket.OPEN */\n\n/**\n * @public\n */\nexport type CreateWSSContextFnOptions = NodeHTTPCreateContextFnOptions<\n IncomingMessage,\n ws.WebSocket\n>;\n\n/**\n * @public\n */\nexport type CreateWSSContextFn<TRouter extends AnyRouter> = (\n opts: CreateWSSContextFnOptions,\n) => MaybePromise<inferRouterContext<TRouter>>;\n\nexport type WSConnectionHandlerOptions<TRouter extends AnyRouter> =\n BaseHandlerOptions<TRouter, IncomingMessage> &\n CreateContextCallback<\n inferRouterContext<TRouter>,\n CreateWSSContextFn<TRouter>\n >;\n\n/**\n * Web socket server handler\n */\nexport type WSSHandlerOptions<TRouter extends AnyRouter> =\n WSConnectionHandlerOptions<TRouter> & {\n wss: ws.WebSocketServer;\n prefix?: string;\n keepAlive?: {\n /**\n * Enable heartbeat messages\n * @default false\n */\n enabled: boolean;\n /**\n * Heartbeat interval in milliseconds\n * @default 30_000\n */\n pingMs?: number;\n /**\n * Terminate the WebSocket if no pong is received after this many milliseconds\n * @default 5_000\n */\n pongWaitMs?: number;\n };\n /**\n * Disable responding to ping messages from the client\n * **Not recommended** - this is mainly used for testing\n * @default false\n */\n dangerouslyDisablePong?: boolean;\n /**\n * Custom encoder for wire encoding (e.g. custom binary formats)\n * @default jsonEncoder\n */\n experimental_encoder?: Encoder;\n };\n\nexport function getWSConnectionHandler<TRouter extends AnyRouter>(\n opts: WSSHandlerOptions<TRouter>,\n) {\n const { createContext, router } = opts;\n const { transformer } = router._def._config;\n const encoder = opts.experimental_encoder ?? jsonEncoder;\n\n return (client: ws.WebSocket, req: IncomingMessage) => {\n type Context = inferRouterContext<TRouter>;\n type ContextResult = Result<Context>;\n\n const clientSubscriptions = new Map<number | string, AbortController>();\n const abortController = new AbortController();\n\n if (opts.keepAlive?.enabled) {\n const { pingMs, pongWaitMs } = opts.keepAlive;\n handleKeepAlive(client, pingMs, pongWaitMs);\n }\n\n function respond(untransformedJSON: TRPCResponseMessage) {\n client.send(\n encoder.encode(\n transformTRPCResponse(router._def._config, untransformedJSON),\n ),\n );\n }\n\n async function createCtxPromise(\n getConnectionParams: () => TRPCRequestInfo['connectionParams'],\n ): Promise<ContextResult> {\n try {\n return await run(async (): Promise<ContextResult> => {\n ctx = await createContext?.({\n req,\n res: client,\n info: {\n connectionParams: getConnectionParams(),\n calls: [],\n isBatchCall: false,\n accept: null,\n type: 'unknown',\n signal: abortController.signal,\n url: null,\n },\n });\n\n return {\n ok: true,\n value: ctx,\n };\n });\n } catch (cause) {\n const error = getTRPCErrorFromUnknown(cause);\n opts.onError?.({\n error,\n path: undefined,\n type: 'unknown',\n ctx,\n req,\n input: undefined,\n });\n respond({\n id: null,\n error: getErrorShape({\n config: router._def._config,\n error,\n type: 'unknown',\n path: undefined,\n input: undefined,\n ctx,\n }),\n });\n\n // close in next tick\n (globalThis.setImmediate ?? globalThis.setTimeout)(() => {\n client.close();\n });\n return {\n ok: false,\n error,\n };\n }\n }\n\n let ctx: Context | undefined = undefined;\n\n /**\n * promise for initializing the context\n *\n * - the context promise will be created immediately on connection if no connectionParams are expected\n * - if connection params are expected, they will be created once received\n */\n let ctxPromise =\n createURL(req).searchParams.get('connectionParams') === '1'\n ? null\n : createCtxPromise(() => null);\n\n function handleRequest(msg: TRPCClientOutgoingMessage) {\n const { id, jsonrpc } = msg;\n\n if (id === null) {\n const error = getTRPCErrorFromUnknown(\n new TRPCError({\n code: 'PARSE_ERROR',\n message: '`id` is required',\n }),\n );\n opts.onError?.({\n error,\n path: undefined,\n type: 'unknown',\n ctx,\n req,\n input: undefined,\n });\n respond({\n id,\n jsonrpc,\n error: getErrorShape({\n config: router._def._config,\n error,\n type: 'unknown',\n path: undefined,\n input: undefined,\n ctx,\n }),\n });\n return;\n }\n if (msg.method === 'subscription.stop') {\n clientSubscriptions.get(id)?.abort();\n return;\n }\n const { path, lastEventId } = msg.params;\n let { input } = msg.params;\n const type = msg.method;\n\n if (lastEventId !== undefined) {\n if (isObject(input)) {\n input = {\n ...input,\n lastEventId: lastEventId,\n };\n } else {\n input ??= {\n lastEventId: lastEventId,\n };\n }\n }\n run(async () => {\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n const res = await ctxPromise!; // asserts context has been set\n if (!res.ok) {\n throw res.error;\n }\n\n const abortController = new AbortController();\n const result = await callTRPCProcedure({\n router,\n path,\n getRawInput: async () => input,\n ctx,\n type,\n signal: abortController.signal,\n });\n\n const isIterableResult =\n isAsyncIterable(result) || isObservable(result);\n\n if (type !== 'subscription') {\n if (isIterableResult) {\n throw new TRPCError({\n code: 'UNSUPPORTED_MEDIA_TYPE',\n message: `Cannot return an async iterable or observable from a ${type} procedure with WebSockets`,\n });\n }\n // send the value as data if the method is not a subscription\n respond({\n id,\n jsonrpc,\n result: {\n type: 'data',\n data: result,\n },\n });\n return;\n }\n\n if (!isIterableResult) {\n throw new TRPCError({\n message: `Subscription ${path} did not return an observable or a AsyncGenerator`,\n code: 'INTERNAL_SERVER_ERROR',\n });\n }\n\n /* istanbul ignore next -- @preserve */\n if (client.readyState !== WEBSOCKET_OPEN) {\n // if the client got disconnected whilst initializing the subscription\n // no need to send stopped message if the client is disconnected\n\n return;\n }\n\n /* istanbul ignore next -- @preserve */\n if (clientSubscriptions.has(id)) {\n // duplicate request ids for client\n\n throw new TRPCError({\n message: `Duplicate id ${id}`,\n code: 'BAD_REQUEST',\n });\n }\n\n const iterable = isObservable(result)\n ? observableToAsyncIterable(result, abortController.signal)\n : result;\n\n run(async () => {\n await using iterator = iteratorResource(iterable);\n\n const abortPromise = new Promise<'abort'>((resolve) => {\n abortController.signal.onabort = () => resolve('abort');\n });\n // We need those declarations outside the loop for garbage collection reasons. If they\n // were declared inside, they would not be freed until the next value is present.\n let next:\n | null\n | TRPCError\n | Awaited<\n typeof abortPromise | ReturnType<(typeof iterator)['next']>\n >;\n let result: null | TRPCResultMessage<unknown>['result'];\n\n while (true) {\n next = await Unpromise.race([\n iterator.next().catch(getTRPCErrorFromUnknown),\n abortPromise,\n ]);\n\n if (next === 'abort') {\n await iterator.return?.();\n break;\n }\n if (next instanceof Error) {\n const error = getTRPCErrorFromUnknown(next);\n opts.onError?.({ error, path, type, ctx, req, input });\n respond({\n id,\n jsonrpc,\n error: getErrorShape({\n config: router._def._config,\n error,\n type,\n path,\n input,\n ctx,\n }),\n });\n break;\n }\n if (next.done) {\n break;\n }\n\n result = {\n type: 'data',\n data: next.value,\n };\n\n if (isTrackedEnvelope(next.value)) {\n const [id, data] = next.value;\n result.id = id;\n result.data = {\n id,\n data,\n };\n }\n\n respond({\n id,\n jsonrpc,\n result,\n });\n\n // free up references for garbage collection\n next = null;\n result = null;\n }\n\n respond({\n id,\n jsonrpc,\n result: {\n type: 'stopped',\n },\n });\n clientSubscriptions.delete(id);\n }).catch((cause) => {\n const error = getTRPCErrorFromUnknown(cause);\n opts.onError?.({ error, path, type, ctx, req, input });\n respond({\n id,\n jsonrpc,\n error: getErrorShape({\n config: router._def._config,\n error,\n type,\n path,\n input,\n ctx,\n }),\n });\n abortController.abort();\n });\n clientSubscriptions.set(id, abortController);\n\n respond({\n id,\n jsonrpc,\n result: {\n type: 'started',\n },\n });\n }).catch((cause) => {\n // procedure threw an error\n const error = getTRPCErrorFromUnknown(cause);\n opts.onError?.({ error, path, type, ctx, req, input });\n respond({\n id,\n jsonrpc,\n error: getErrorShape({\n config: router._def._config,\n error,\n type,\n path,\n input,\n ctx,\n }),\n });\n });\n }\n client.on('message', (rawData, isBinary) => {\n // Handle PING/PONG as text regardless of encoder\n if (!isBinary) {\n // eslint-disable-next-line @typescript-eslint/no-base-to-string\n const msgStr = rawData.toString();\n if (msgStr === 'PONG') {\n return;\n }\n if (msgStr === 'PING') {\n if (!opts.dangerouslyDisablePong) {\n client.send('PONG');\n }\n return;\n }\n }\n\n // Convert rawData to a format our encoder accepts\n // ws gives us Buffer for both text and binary frames\n if (!Buffer.isBuffer(rawData)) {\n const error = new TRPCError({\n code: 'UNPROCESSABLE_CONTENT',\n message: 'Unexpected WebSocket message format',\n });\n respond({\n id: null,\n error: getErrorShape({\n config: router._def._config,\n error,\n type: 'unknown',\n path: undefined,\n input: undefined,\n ctx,\n }),\n });\n return;\n }\n const data: string | Uint8Array = isBinary\n ? rawData\n : rawData.toString('utf8');\n\n if (!ctxPromise) {\n // If the ctxPromise wasn't created immediately, we're expecting the first message to be a TRPCConnectionParamsMessage\n ctxPromise = createCtxPromise(() => {\n let msg;\n try {\n msg = encoder.decode(data) as TRPCConnectionParamsMessage;\n\n if (!isObject(msg)) {\n throw new Error('Message was not an object');\n }\n } catch (cause) {\n throw new TRPCError({\n code: 'PARSE_ERROR',\n message: `Malformed TRPCConnectionParamsMessage`,\n cause,\n });\n }\n\n const connectionParams = parseConnectionParamsFromUnknown(msg.data);\n\n return connectionParams;\n });\n return;\n }\n\n const parsedMsgs = run(() => {\n try {\n const msgJSON: unknown = encoder.decode(data);\n const msgs: unknown[] = Array.isArray(msgJSON) ? msgJSON : [msgJSON];\n\n return msgs.map((raw) => parseTRPCMessage(raw, transformer));\n } catch (cause) {\n const error = new TRPCError({\n code: 'PARSE_ERROR',\n cause,\n });\n\n respond({\n id: null,\n error: getErrorShape({\n config: router._def._config,\n error,\n type: 'unknown',\n path: undefined,\n input: undefined,\n ctx,\n }),\n });\n\n return [];\n }\n });\n\n parsedMsgs.map(handleRequest);\n });\n\n // WebSocket errors should be handled, as otherwise unhandled exceptions will crash Node.js.\n // This line was introduced after the following error brought down production systems:\n // \"RangeError: Invalid WebSocket frame: RSV2 and RSV3 must be clear\"\n // Here is the relevant discussion: https://github.com/websockets/ws/issues/1354#issuecomment-774616962\n client.on('error', (cause) => {\n opts.onError?.({\n ctx,\n error: getTRPCErrorFromUnknown(cause),\n input: undefined,\n path: undefined,\n type: 'unknown',\n req,\n });\n });\n\n client.once('close', () => {\n for (const sub of clientSubscriptions.values()) {\n sub.abort();\n }\n clientSubscriptions.clear();\n abortController.abort();\n });\n };\n}\n\n/**\n * Handle WebSocket keep-alive messages\n */\nexport function handleKeepAlive(\n client: ws.WebSocket,\n pingMs = 30_000,\n pongWaitMs = 5_000,\n) {\n let timeout: NodeJS.Timeout | undefined = undefined;\n let ping: NodeJS.Timeout | undefined = undefined;\n\n const schedulePing = () => {\n const scheduleTimeout = () => {\n timeout = setTimeout(() => {\n client.terminate();\n }, pongWaitMs) as any;\n };\n ping = setTimeout(() => {\n client.send('PING');\n\n scheduleTimeout();\n }, pingMs) as any;\n };\n\n const onMessage = () => {\n clearTimeout(ping);\n clearTimeout(timeout);\n\n schedulePing();\n };\n\n client.on('message', onMessage);\n\n client.on('close', () => {\n clearTimeout(ping);\n clearTimeout(timeout);\n });\n\n schedulePing();\n}\n\nexport function applyWSSHandler<TRouter extends AnyRouter>(\n opts: WSSHandlerOptions<TRouter>,\n) {\n const encoder = opts.experimental_encoder ?? jsonEncoder;\n const onConnection = getWSConnectionHandler(opts);\n opts.wss.on('connection', (client, req) => {\n if (opts.prefix && !req.url?.startsWith(opts.prefix)) {\n return;\n }\n\n onConnection(client, req);\n });\n\n return {\n broadcastReconnectNotification: () => {\n const response: TRPCReconnectNotification = {\n id: null,\n method: 'reconnect',\n };\n const data = encoder.encode(response);\n for (const client of opts.wss.clients) {\n if (client.readyState === WEBSOCKET_OPEN) {\n client.send(data);\n }\n }\n },\n };\n}\n\nexport type { Encoder } from './wsEncoder';\nexport { jsonEncoder } from './wsEncoder';\n"],"mappings":"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"}
@@ -6,6 +6,20 @@ const require_resolveResponse = require('./resolveResponse-ByfQ6olt.cjs');
6
6
  const require_observable = require('./observable-B1Nk6r1H.cjs');
7
7
  const require_node_http = require('./node-http-D0T_XJ9C.cjs');
8
8
 
9
+ //#region src/adapters/wsEncoder.ts
10
+ /**
11
+ * Default JSON encoder - used when no encoder is specified.
12
+ * This maintains backwards compatibility with existing behavior.
13
+ */
14
+ const jsonEncoder = {
15
+ encode: (data) => JSON.stringify(data),
16
+ decode: (data) => {
17
+ if (typeof data !== "string") throw new Error("jsonEncoder received binary data. JSON uses text frames. Use a binary encoder for binary data.");
18
+ return JSON.parse(data);
19
+ }
20
+ };
21
+
22
+ //#endregion
9
23
  //#region src/adapters/ws.ts
10
24
  var import_objectSpread2 = require_getErrorShape.__toESM(require_getErrorShape.require_objectSpread2(), 1);
11
25
  var import_usingCtx = require_getErrorShape.__toESM(require_resolveResponse.require_usingCtx(), 1);
@@ -15,8 +29,10 @@ var import_usingCtx = require_getErrorShape.__toESM(require_resolveResponse.requ
15
29
  */
16
30
  const WEBSOCKET_OPEN = 1;
17
31
  function getWSConnectionHandler(opts) {
32
+ var _opts$experimental_en;
18
33
  const { createContext, router } = opts;
19
34
  const { transformer } = router._def._config;
35
+ const encoder = (_opts$experimental_en = opts.experimental_encoder) !== null && _opts$experimental_en !== void 0 ? _opts$experimental_en : jsonEncoder;
20
36
  return (client, req) => {
21
37
  var _opts$keepAlive;
22
38
  const clientSubscriptions = /* @__PURE__ */ new Map();
@@ -26,7 +42,7 @@ function getWSConnectionHandler(opts) {
26
42
  handleKeepAlive(client, pingMs, pongWaitMs);
27
43
  }
28
44
  function respond(untransformedJSON) {
29
- client.send(JSON.stringify(require_tracked.transformTRPCResponse(router._def._config, untransformedJSON)));
45
+ client.send(encoder.encode(require_tracked.transformTRPCResponse(router._def._config, untransformedJSON)));
30
46
  }
31
47
  async function createCtxPromise(getConnectionParams) {
32
48
  try {
@@ -300,18 +316,39 @@ function getWSConnectionHandler(opts) {
300
316
  });
301
317
  });
302
318
  }
303
- client.on("message", (rawData) => {
304
- const msgStr = rawData.toString();
305
- if (msgStr === "PONG") return;
306
- if (msgStr === "PING") {
307
- if (!opts.dangerouslyDisablePong) client.send("PONG");
319
+ client.on("message", (rawData, isBinary) => {
320
+ if (!isBinary) {
321
+ const msgStr = rawData.toString();
322
+ if (msgStr === "PONG") return;
323
+ if (msgStr === "PING") {
324
+ if (!opts.dangerouslyDisablePong) client.send("PONG");
325
+ return;
326
+ }
327
+ }
328
+ if (!Buffer.isBuffer(rawData)) {
329
+ const error = new require_tracked.TRPCError({
330
+ code: "UNPROCESSABLE_CONTENT",
331
+ message: "Unexpected WebSocket message format"
332
+ });
333
+ respond({
334
+ id: null,
335
+ error: require_getErrorShape.getErrorShape({
336
+ config: router._def._config,
337
+ error,
338
+ type: "unknown",
339
+ path: void 0,
340
+ input: void 0,
341
+ ctx
342
+ })
343
+ });
308
344
  return;
309
345
  }
346
+ const data = isBinary ? rawData : rawData.toString("utf8");
310
347
  if (!ctxPromise) {
311
348
  ctxPromise = createCtxPromise(() => {
312
349
  let msg;
313
350
  try {
314
- msg = JSON.parse(msgStr);
351
+ msg = encoder.decode(data);
315
352
  if (!require_codes.isObject(msg)) throw new Error("Message was not an object");
316
353
  } catch (cause) {
317
354
  throw new require_tracked.TRPCError({
@@ -327,7 +364,7 @@ function getWSConnectionHandler(opts) {
327
364
  }
328
365
  const parsedMsgs = require_codes.run(() => {
329
366
  try {
330
- const msgJSON = JSON.parse(msgStr);
367
+ const msgJSON = encoder.decode(data);
331
368
  const msgs = Array.isArray(msgJSON) ? msgJSON : [msgJSON];
332
369
  return msgs.map((raw) => require_parseTRPCMessage.parseTRPCMessage(raw, transformer));
333
370
  } catch (cause) {
@@ -399,6 +436,8 @@ function handleKeepAlive(client, pingMs = 3e4, pongWaitMs = 5e3) {
399
436
  schedulePing();
400
437
  }
401
438
  function applyWSSHandler(opts) {
439
+ var _opts$experimental_en2;
440
+ const encoder = (_opts$experimental_en2 = opts.experimental_encoder) !== null && _opts$experimental_en2 !== void 0 ? _opts$experimental_en2 : jsonEncoder;
402
441
  const onConnection = getWSConnectionHandler(opts);
403
442
  opts.wss.on("connection", (client, req) => {
404
443
  var _req$url;
@@ -410,7 +449,7 @@ function applyWSSHandler(opts) {
410
449
  id: null,
411
450
  method: "reconnect"
412
451
  };
413
- const data = JSON.stringify(response);
452
+ const data = encoder.encode(response);
414
453
  for (const client of opts.wss.clients) if (client.readyState === WEBSOCKET_OPEN) client.send(data);
415
454
  } };
416
455
  }
@@ -433,4 +472,10 @@ Object.defineProperty(exports, 'handleKeepAlive', {
433
472
  get: function () {
434
473
  return handleKeepAlive;
435
474
  }
475
+ });
476
+ Object.defineProperty(exports, 'jsonEncoder', {
477
+ enumerable: true,
478
+ get: function () {
479
+ return jsonEncoder;
480
+ }
436
481
  });
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@trpc/server",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "11.8.2-canary.3+f48ed5c18",
5
+ "version": "11.9.0",
6
6
  "description": "The tRPC server library",
7
7
  "author": "KATT",
8
8
  "license": "MIT",
@@ -194,7 +194,7 @@
194
194
  },
195
195
  "devDependencies": {
196
196
  "@fastify/websocket": "^11.0.0",
197
- "@oxc-project/runtime": "0.105.0",
197
+ "@oxc-project/runtime": "0.110.0",
198
198
  "@tanstack/react-query": "^5.80.3",
199
199
  "@types/aws-lambda": "^8.10.149",
200
200
  "@types/express": "^5.0.0",
@@ -228,5 +228,5 @@
228
228
  "peerDependencies": {
229
229
  "typescript": ">=5.7.2"
230
230
  },
231
- "gitHead": "f48ed5c18121bca0808f59d8f6c6d6d141c5e1f0"
231
+ "gitHead": "3beb5067de117bad47fcbd8ce4fbcd73817e872a"
232
232
  }
@@ -39,6 +39,8 @@ import type { Result } from '../unstable-core-do-not-import';
39
39
  import { iteratorResource } from '../unstable-core-do-not-import/stream/utils/asyncIterable';
40
40
  import { Unpromise } from '../vendor/unpromise';
41
41
  import { createURL, type NodeHTTPCreateContextFnOptions } from './node-http';
42
+ import type { Encoder } from './wsEncoder';
43
+ import { jsonEncoder } from './wsEncoder';
42
44
 
43
45
  /**
44
46
  * Importing ws causes a build error
@@ -98,6 +100,11 @@ export type WSSHandlerOptions<TRouter extends AnyRouter> =
98
100
  * @default false
99
101
  */
100
102
  dangerouslyDisablePong?: boolean;
103
+ /**
104
+ * Custom encoder for wire encoding (e.g. custom binary formats)
105
+ * @default jsonEncoder
106
+ */
107
+ experimental_encoder?: Encoder;
101
108
  };
102
109
 
103
110
  export function getWSConnectionHandler<TRouter extends AnyRouter>(
@@ -105,6 +112,7 @@ export function getWSConnectionHandler<TRouter extends AnyRouter>(
105
112
  ) {
106
113
  const { createContext, router } = opts;
107
114
  const { transformer } = router._def._config;
115
+ const encoder = opts.experimental_encoder ?? jsonEncoder;
108
116
 
109
117
  return (client: ws.WebSocket, req: IncomingMessage) => {
110
118
  type Context = inferRouterContext<TRouter>;
@@ -120,7 +128,7 @@ export function getWSConnectionHandler<TRouter extends AnyRouter>(
120
128
 
121
129
  function respond(untransformedJSON: TRPCResponseMessage) {
122
130
  client.send(
123
- JSON.stringify(
131
+ encoder.encode(
124
132
  transformTRPCResponse(router._def._config, untransformedJSON),
125
133
  ),
126
134
  );
@@ -440,24 +448,52 @@ export function getWSConnectionHandler<TRouter extends AnyRouter>(
440
448
  });
441
449
  });
442
450
  }
443
- client.on('message', (rawData) => {
444
- // eslint-disable-next-line @typescript-eslint/no-base-to-string
445
- const msgStr = rawData.toString();
446
- if (msgStr === 'PONG') {
447
- return;
448
- }
449
- if (msgStr === 'PING') {
450
- if (!opts.dangerouslyDisablePong) {
451
- client.send('PONG');
451
+ client.on('message', (rawData, isBinary) => {
452
+ // Handle PING/PONG as text regardless of encoder
453
+ if (!isBinary) {
454
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
455
+ const msgStr = rawData.toString();
456
+ if (msgStr === 'PONG') {
457
+ return;
458
+ }
459
+ if (msgStr === 'PING') {
460
+ if (!opts.dangerouslyDisablePong) {
461
+ client.send('PONG');
462
+ }
463
+ return;
452
464
  }
465
+ }
466
+
467
+ // Convert rawData to a format our encoder accepts
468
+ // ws gives us Buffer for both text and binary frames
469
+ if (!Buffer.isBuffer(rawData)) {
470
+ const error = new TRPCError({
471
+ code: 'UNPROCESSABLE_CONTENT',
472
+ message: 'Unexpected WebSocket message format',
473
+ });
474
+ respond({
475
+ id: null,
476
+ error: getErrorShape({
477
+ config: router._def._config,
478
+ error,
479
+ type: 'unknown',
480
+ path: undefined,
481
+ input: undefined,
482
+ ctx,
483
+ }),
484
+ });
453
485
  return;
454
486
  }
487
+ const data: string | Uint8Array = isBinary
488
+ ? rawData
489
+ : rawData.toString('utf8');
490
+
455
491
  if (!ctxPromise) {
456
492
  // If the ctxPromise wasn't created immediately, we're expecting the first message to be a TRPCConnectionParamsMessage
457
493
  ctxPromise = createCtxPromise(() => {
458
494
  let msg;
459
495
  try {
460
- msg = JSON.parse(msgStr) as TRPCConnectionParamsMessage;
496
+ msg = encoder.decode(data) as TRPCConnectionParamsMessage;
461
497
 
462
498
  if (!isObject(msg)) {
463
499
  throw new Error('Message was not an object');
@@ -479,7 +515,7 @@ export function getWSConnectionHandler<TRouter extends AnyRouter>(
479
515
 
480
516
  const parsedMsgs = run(() => {
481
517
  try {
482
- const msgJSON: unknown = JSON.parse(msgStr);
518
+ const msgJSON: unknown = encoder.decode(data);
483
519
  const msgs: unknown[] = Array.isArray(msgJSON) ? msgJSON : [msgJSON];
484
520
 
485
521
  return msgs.map((raw) => parseTRPCMessage(raw, transformer));
@@ -577,6 +613,7 @@ export function handleKeepAlive(
577
613
  export function applyWSSHandler<TRouter extends AnyRouter>(
578
614
  opts: WSSHandlerOptions<TRouter>,
579
615
  ) {
616
+ const encoder = opts.experimental_encoder ?? jsonEncoder;
580
617
  const onConnection = getWSConnectionHandler(opts);
581
618
  opts.wss.on('connection', (client, req) => {
582
619
  if (opts.prefix && !req.url?.startsWith(opts.prefix)) {
@@ -592,7 +629,7 @@ export function applyWSSHandler<TRouter extends AnyRouter>(
592
629
  id: null,
593
630
  method: 'reconnect',
594
631
  };
595
- const data = JSON.stringify(response);
632
+ const data = encoder.encode(response);
596
633
  for (const client of opts.wss.clients) {
597
634
  if (client.readyState === WEBSOCKET_OPEN) {
598
635
  client.send(data);
@@ -601,3 +638,6 @@ export function applyWSSHandler<TRouter extends AnyRouter>(
601
638
  },
602
639
  };
603
640
  }
641
+
642
+ export type { Encoder } from './wsEncoder';
643
+ export { jsonEncoder } from './wsEncoder';