@trpc/server 11.0.0-alpha-tmp-subscription-connection-state.488 → 11.0.0-alpha-tmp-subscription-connection-state.489

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.
Files changed (53) hide show
  1. package/dist/@trpc/server/index.d.ts +5 -1
  2. package/dist/@trpc/server/index.d.ts.map +1 -1
  3. package/dist/adapters/ws.d.ts.map +1 -1
  4. package/dist/adapters/ws.js +36 -6
  5. package/dist/adapters/ws.mjs +37 -7
  6. package/dist/bundle-analysis.json +102 -87
  7. package/dist/index.js +3 -2
  8. package/dist/index.mjs +1 -1
  9. package/dist/unstable-core-do-not-import/clientish/serialize.d.ts +1 -1
  10. package/dist/unstable-core-do-not-import/clientish/serialize.d.ts.map +1 -1
  11. package/dist/unstable-core-do-not-import/http/resolveResponse.d.ts.map +1 -1
  12. package/dist/unstable-core-do-not-import/http/resolveResponse.js +13 -2
  13. package/dist/unstable-core-do-not-import/http/resolveResponse.mjs +13 -2
  14. package/dist/unstable-core-do-not-import/procedureBuilder.d.ts +2 -2
  15. package/dist/unstable-core-do-not-import/procedureBuilder.d.ts.map +1 -1
  16. package/dist/unstable-core-do-not-import/rootConfig.d.ts +1 -1
  17. package/dist/unstable-core-do-not-import/rootConfig.d.ts.map +1 -1
  18. package/dist/unstable-core-do-not-import/rpc/envelopes.d.ts +8 -0
  19. package/dist/unstable-core-do-not-import/rpc/envelopes.d.ts.map +1 -1
  20. package/dist/unstable-core-do-not-import/rpc/parseTRPCMessage.d.ts.map +1 -1
  21. package/dist/unstable-core-do-not-import/rpc/parseTRPCMessage.js +6 -2
  22. package/dist/unstable-core-do-not-import/rpc/parseTRPCMessage.mjs +6 -2
  23. package/dist/unstable-core-do-not-import/stream/jsonl.d.ts +7 -3
  24. package/dist/unstable-core-do-not-import/stream/jsonl.d.ts.map +1 -1
  25. package/dist/unstable-core-do-not-import/stream/jsonl.js +5 -4
  26. package/dist/unstable-core-do-not-import/stream/jsonl.mjs +5 -4
  27. package/dist/unstable-core-do-not-import/stream/sse.d.ts +12 -30
  28. package/dist/unstable-core-do-not-import/stream/sse.d.ts.map +1 -1
  29. package/dist/unstable-core-do-not-import/stream/sse.js +29 -23
  30. package/dist/unstable-core-do-not-import/stream/sse.mjs +30 -22
  31. package/dist/unstable-core-do-not-import/stream/tracked.d.ts +31 -0
  32. package/dist/unstable-core-do-not-import/stream/tracked.d.ts.map +1 -0
  33. package/dist/unstable-core-do-not-import/stream/tracked.js +29 -0
  34. package/dist/unstable-core-do-not-import/stream/tracked.mjs +25 -0
  35. package/dist/unstable-core-do-not-import/transformer.d.ts +1 -0
  36. package/dist/unstable-core-do-not-import/transformer.d.ts.map +1 -1
  37. package/dist/unstable-core-do-not-import.d.ts +1 -0
  38. package/dist/unstable-core-do-not-import.d.ts.map +1 -1
  39. package/dist/unstable-core-do-not-import.js +4 -2
  40. package/dist/unstable-core-do-not-import.mjs +2 -1
  41. package/package.json +7 -7
  42. package/src/@trpc/server/index.ts +4 -0
  43. package/src/adapters/ws.ts +42 -6
  44. package/src/unstable-core-do-not-import/clientish/serialize.ts +1 -1
  45. package/src/unstable-core-do-not-import/http/resolveResponse.ts +13 -1
  46. package/src/unstable-core-do-not-import/procedureBuilder.ts +2 -2
  47. package/src/unstable-core-do-not-import/rootConfig.ts +4 -1
  48. package/src/unstable-core-do-not-import/rpc/envelopes.ts +18 -2
  49. package/src/unstable-core-do-not-import/rpc/parseTRPCMessage.ts +5 -1
  50. package/src/unstable-core-do-not-import/stream/jsonl.ts +14 -6
  51. package/src/unstable-core-do-not-import/stream/sse.ts +59 -68
  52. package/src/unstable-core-do-not-import/stream/tracked.ts +55 -0
  53. package/src/unstable-core-do-not-import.ts +1 -0
@@ -34,6 +34,7 @@ export * from './unstable-core-do-not-import/router';
34
34
  export * from './unstable-core-do-not-import/rpc';
35
35
  export * from './unstable-core-do-not-import/stream/jsonl';
36
36
  export * from './unstable-core-do-not-import/stream/sse';
37
+ export * from './unstable-core-do-not-import/stream/tracked';
37
38
  export * from './unstable-core-do-not-import/stream/utils/createDeferred';
38
39
  export * from './unstable-core-do-not-import/transformer';
39
40
  export * from './unstable-core-do-not-import/types';
@@ -1 +1 @@
1
- {"version":3,"file":"unstable-core-do-not-import.d.ts","sourceRoot":"","sources":["../src/unstable-core-do-not-import.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,cAAc,mDAAmD,CAAC;AAClE,cAAc,oDAAoD,CAAC;AACnE,cAAc,mDAAmD,CAAC;AAClE,cAAc,2CAA2C,CAAC;AAC1D,cAAc,+CAA+C,CAAC;AAC9D,cAAc,mDAAmD,CAAC;AAClE,cAAc,+CAA+C,CAAC;AAC9D,cAAc,yDAAyD,CAAC;AACxE,cAAc,gDAAgD,CAAC;AAC/D,cAAc,uDAAuD,CAAC;AACtE,cAAc,qDAAqD,CAAC;AACpE,cAAc,sDAAsD,CAAC;AACrE,cAAc,0DAA0D,CAAC;AACzE,cAAc,oDAAoD,CAAC;AACnE,cAAc,0CAA0C,CAAC;AACzD,cAAc,0CAA0C,CAAC;AACzD,cAAc,wCAAwC,CAAC;AACvD,cAAc,0CAA0C,CAAC;AACzD,cAAc,sCAAsC,CAAC;AACrD,cAAc,yCAAyC,CAAC;AACxD,cAAc,gDAAgD,CAAC;AAC/D,cAAc,0CAA0C,CAAC;AACzD,cAAc,sCAAsC,CAAC;AACrD,cAAc,mCAAmC,CAAC;AAClD,cAAc,4CAA4C,CAAC;AAC3D,cAAc,0CAA0C,CAAC;AACzD,cAAc,2DAA2D,CAAC;AAC1E,cAAc,2CAA2C,CAAC;AAC1D,cAAc,qCAAqC,CAAC;AACpD,cAAc,qCAAqC,CAAC"}
1
+ {"version":3,"file":"unstable-core-do-not-import.d.ts","sourceRoot":"","sources":["../src/unstable-core-do-not-import.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,cAAc,mDAAmD,CAAC;AAClE,cAAc,oDAAoD,CAAC;AACnE,cAAc,mDAAmD,CAAC;AAClE,cAAc,2CAA2C,CAAC;AAC1D,cAAc,+CAA+C,CAAC;AAC9D,cAAc,mDAAmD,CAAC;AAClE,cAAc,+CAA+C,CAAC;AAC9D,cAAc,yDAAyD,CAAC;AACxE,cAAc,gDAAgD,CAAC;AAC/D,cAAc,uDAAuD,CAAC;AACtE,cAAc,qDAAqD,CAAC;AACpE,cAAc,sDAAsD,CAAC;AACrE,cAAc,0DAA0D,CAAC;AACzE,cAAc,oDAAoD,CAAC;AACnE,cAAc,0CAA0C,CAAC;AACzD,cAAc,0CAA0C,CAAC;AACzD,cAAc,wCAAwC,CAAC;AACvD,cAAc,0CAA0C,CAAC;AACzD,cAAc,sCAAsC,CAAC;AACrD,cAAc,yCAAyC,CAAC;AACxD,cAAc,gDAAgD,CAAC;AAC/D,cAAc,0CAA0C,CAAC;AACzD,cAAc,sCAAsC,CAAC;AACrD,cAAc,mCAAmC,CAAC;AAClD,cAAc,4CAA4C,CAAC;AAC3D,cAAc,0CAA0C,CAAC;AACzD,cAAc,8CAA8C,CAAC;AAC7D,cAAc,2DAA2D,CAAC;AAC1E,cAAc,2CAA2C,CAAC;AAC1D,cAAc,qCAAqC,CAAC;AACpD,cAAc,qCAAqC,CAAC"}
@@ -23,6 +23,7 @@ var codes = require('./unstable-core-do-not-import/rpc/codes.js');
23
23
  var parseTRPCMessage = require('./unstable-core-do-not-import/rpc/parseTRPCMessage.js');
24
24
  var jsonl = require('./unstable-core-do-not-import/stream/jsonl.js');
25
25
  var sse = require('./unstable-core-do-not-import/stream/sse.js');
26
+ var tracked = require('./unstable-core-do-not-import/stream/tracked.js');
26
27
  var createDeferred = require('./unstable-core-do-not-import/stream/utils/createDeferred.js');
27
28
  var transformer = require('./unstable-core-do-not-import/transformer.js');
28
29
  var types = require('./unstable-core-do-not-import/types.js');
@@ -67,11 +68,12 @@ exports.parseTRPCMessage = parseTRPCMessage.parseTRPCMessage;
67
68
  exports.isPromise = jsonl.isPromise;
68
69
  exports.jsonlStreamConsumer = jsonl.jsonlStreamConsumer;
69
70
  exports.jsonlStreamProducer = jsonl.jsonlStreamProducer;
70
- exports.isSSEMessageEnvelope = sse.isSSEMessageEnvelope;
71
- exports.sse = sse.sse;
72
71
  exports.sseHeaders = sse.sseHeaders;
73
72
  exports.sseStreamConsumer = sse.sseStreamConsumer;
74
73
  exports.sseStreamProducer = sse.sseStreamProducer;
74
+ exports.isTrackedEnvelope = tracked.isTrackedEnvelope;
75
+ exports.sse = tracked.sse;
76
+ exports.tracked = tracked.tracked;
75
77
  exports.createDeferred = createDeferred.createDeferred;
76
78
  exports.createTimeoutPromise = createDeferred.createTimeoutPromise;
77
79
  exports.defaultTransformer = transformer.defaultTransformer;
@@ -20,7 +20,8 @@ export { callProcedure, createCallerFactory, createRouterFactory, mergeRouters }
20
20
  export { TRPC_ERROR_CODES_BY_KEY, TRPC_ERROR_CODES_BY_NUMBER } from './unstable-core-do-not-import/rpc/codes.mjs';
21
21
  export { parseTRPCMessage } from './unstable-core-do-not-import/rpc/parseTRPCMessage.mjs';
22
22
  export { isPromise, jsonlStreamConsumer, jsonlStreamProducer } from './unstable-core-do-not-import/stream/jsonl.mjs';
23
- export { isSSEMessageEnvelope, sse, sseHeaders, sseStreamConsumer, sseStreamProducer } from './unstable-core-do-not-import/stream/sse.mjs';
23
+ export { sseHeaders, sseStreamConsumer, sseStreamProducer } from './unstable-core-do-not-import/stream/sse.mjs';
24
+ export { isTrackedEnvelope, sse, tracked } from './unstable-core-do-not-import/stream/tracked.mjs';
24
25
  export { createDeferred, createTimeoutPromise } from './unstable-core-do-not-import/stream/utils/createDeferred.mjs';
25
26
  export { defaultTransformer, getDataTransformer, transformResult, transformTRPCResponse } from './unstable-core-do-not-import/transformer.mjs';
26
27
  export { ERROR_SYMBOL } from './unstable-core-do-not-import/types.mjs';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trpc/server",
3
- "version": "11.0.0-alpha-tmp-subscription-connection-state.488+70f6f6f44",
3
+ "version": "11.0.0-alpha-tmp-subscription-connection-state.489+04c141d1b",
4
4
  "description": "The tRPC server library",
5
5
  "author": "KATT",
6
6
  "license": "MIT",
@@ -117,7 +117,7 @@
117
117
  },
118
118
  "devDependencies": {
119
119
  "@fastify/websocket": "^10.0.1",
120
- "@tanstack/react-query": "^5.49.2",
120
+ "@tanstack/react-query": "^5.51.11",
121
121
  "@types/aws-lambda": "^8.10.137",
122
122
  "@types/express": "^4.17.17",
123
123
  "@types/hash-sum": "^1.0.0",
@@ -126,13 +126,13 @@
126
126
  "@types/react-dom": "^18.3.0",
127
127
  "@types/ws": "^8.2.0",
128
128
  "devalue": "^5.0.0",
129
- "eslint": "^8.56.0",
129
+ "eslint": "^8.57.0",
130
130
  "express": "^4.17.1",
131
131
  "fastify": "^4.16.0",
132
132
  "fastify-plugin": "^4.5.0",
133
133
  "hash-sum": "^2.0.0",
134
134
  "myzod": "^1.3.1",
135
- "next": "^14.1.4",
135
+ "next": "^14.2.5",
136
136
  "react": "^18.3.1",
137
137
  "react-dom": "^18.3.1",
138
138
  "rollup": "^4.9.5",
@@ -140,8 +140,8 @@
140
140
  "superstruct": "^2.0.0",
141
141
  "tslib": "^2.5.0",
142
142
  "tsx": "^4.0.0",
143
- "typescript": "^5.5.0",
144
- "valibot": "^0.36.0",
143
+ "typescript": "^5.5.3",
144
+ "valibot": "^0.37.0",
145
145
  "ws": "^8.0.0",
146
146
  "yup": "^1.0.0",
147
147
  "zod": "^3.0.0"
@@ -149,5 +149,5 @@
149
149
  "funding": [
150
150
  "https://trpc.io/sponsor"
151
151
  ],
152
- "gitHead": "70f6f6f44439bcb258420096517570e8369b3d69"
152
+ "gitHead": "04c141d1b4bb50631826aa956a874ba1d363f492"
153
153
  }
@@ -37,7 +37,11 @@ export {
37
37
  type QueryProcedure as TRPCQueryProcedure,
38
38
  type SubscriptionProcedure as TRPCSubscriptionProcedure,
39
39
  type TRPCBuilder,
40
+ /**
41
+ * @deprecated use `tracked(id, data)` instead
42
+ */
40
43
  sse,
44
+ tracked,
41
45
  } from '../../unstable-core-do-not-import';
42
46
 
43
47
  export type {
@@ -21,6 +21,7 @@ import type {
21
21
  TRPCConnectionParamsMessage,
22
22
  TRPCReconnectNotification,
23
23
  TRPCResponseMessage,
24
+ TRPCResultMessage,
24
25
  } from '../@trpc/server/rpc';
25
26
  import { parseConnectionParamsFromUnknown } from '../http';
26
27
  import { isObservable } from '../observable';
@@ -29,6 +30,7 @@ import { observableToAsyncIterable } from '../observable/observable';
29
30
  import {
30
31
  isAsyncIterable,
31
32
  isObject,
33
+ isTrackedEnvelope,
32
34
  run,
33
35
  type MaybePromise,
34
36
  } from '../unstable-core-do-not-import';
@@ -171,6 +173,7 @@ export function getWSConnectionHandler<TRouter extends AnyRouter>(
171
173
 
172
174
  async function handleRequest(msg: TRPCClientOutgoingMessage) {
173
175
  const { id, jsonrpc } = msg;
176
+
174
177
  /* istanbul ignore next -- @preserve */
175
178
  if (id === null) {
176
179
  throw new TRPCError({
@@ -182,9 +185,22 @@ export function getWSConnectionHandler<TRouter extends AnyRouter>(
182
185
  clientSubscriptions.get(id)?.abort();
183
186
  return;
184
187
  }
185
- const { path, input } = msg.params;
188
+ const { path, lastEventId } = msg.params;
189
+ let { input } = msg.params;
186
190
  const type = msg.method;
187
191
  try {
192
+ if (lastEventId !== undefined) {
193
+ if (isObject(input)) {
194
+ input = {
195
+ ...input,
196
+ lastEventId: lastEventId,
197
+ };
198
+ } else {
199
+ input ??= {
200
+ lastEventId: lastEventId,
201
+ };
202
+ }
203
+ }
188
204
  await ctxPromise; // asserts context has been set
189
205
 
190
206
  const result = await callProcedure({
@@ -195,7 +211,16 @@ export function getWSConnectionHandler<TRouter extends AnyRouter>(
195
211
  type,
196
212
  });
197
213
 
214
+ const isIterableResult =
215
+ isAsyncIterable(result) || isObservable(result);
216
+
198
217
  if (type !== 'subscription') {
218
+ if (isIterableResult) {
219
+ throw new TRPCError({
220
+ code: 'UNSUPPORTED_MEDIA_TYPE',
221
+ message: `Cannot return an async iterable or observable from a ${type} procedure with WebSockets`,
222
+ });
223
+ }
199
224
  // send the value as data if the method is not a subscription
200
225
  respond({
201
226
  id,
@@ -208,7 +233,7 @@ export function getWSConnectionHandler<TRouter extends AnyRouter>(
208
233
  return;
209
234
  }
210
235
 
211
- if (!isObservable(result) && !isAsyncIterable(result)) {
236
+ if (!isIterableResult) {
212
237
  throw new TRPCError({
213
238
  message: `Subscription ${path} did not return an observable or a AsyncGenerator`,
214
239
  code: 'INTERNAL_SERVER_ERROR',
@@ -277,13 +302,24 @@ export function getWSConnectionHandler<TRouter extends AnyRouter>(
277
302
  break;
278
303
  }
279
304
 
305
+ const result: TRPCResultMessage<unknown>['result'] = {
306
+ type: 'data',
307
+ data: next.value,
308
+ };
309
+
310
+ if (isTrackedEnvelope(next.value)) {
311
+ const [id, data] = next.value;
312
+ result.id = id;
313
+ result.data = {
314
+ id,
315
+ data,
316
+ };
317
+ }
318
+
280
319
  respond({
281
320
  id,
282
321
  jsonrpc,
283
- result: {
284
- type: 'data',
285
- data: next.value,
286
- },
322
+ result,
287
323
  });
288
324
  }
289
325
 
@@ -26,7 +26,7 @@ type IsRecord<T extends object> = keyof WithoutIndexSignature<T> extends never
26
26
  export type Serialize<T> =
27
27
  IsAny<T> extends true ? any :
28
28
  unknown extends T ? unknown :
29
- T extends AsyncIterable<infer U> ? AsyncIterable<Serialize<U>> :
29
+ T extends AsyncGenerator<infer $T, infer $Return, infer $Next> ? AsyncGenerator<Serialize<$T>, Serialize<$Return>, Serialize<$Next>> :
30
30
  T extends JsonReturnable ? T :
31
31
  T extends Map<any, any> | Set<any> ? object :
32
32
  T extends NonJsonPrimitive ? never :
@@ -380,9 +380,21 @@ export async function resolveResponse<TRouter extends AnyRouter>(
380
380
  ? observableToAsyncIterable(data)
381
381
  : data;
382
382
  const stream = sseStreamProducer({
383
- ...router._def._config.experimental?.sseSubscriptions,
383
+ ...config.experimental?.sseSubscriptions,
384
384
  data: dataAsIterable,
385
385
  serialize: (v) => config.transformer.output.serialize(v),
386
+ formatError(errorOpts) {
387
+ const shape = getErrorShape({
388
+ config,
389
+ ctx,
390
+ error: getTRPCErrorFromUnknown(errorOpts.error),
391
+ input: call?.result(),
392
+ path: call?.path,
393
+ type: call?.procedure?._def.type ?? 'unknown',
394
+ });
395
+
396
+ return shape;
397
+ },
386
398
  });
387
399
  for (const [key, value] of Object.entries(sseHeaders)) {
388
400
  headers.set(key, value);
@@ -23,7 +23,7 @@ import type {
23
23
  QueryProcedure,
24
24
  SubscriptionProcedure,
25
25
  } from './procedure';
26
- import type { inferSSEOutput } from './stream/sse';
26
+ import type { inferTrackedOutput } from './stream/tracked';
27
27
  import type {
28
28
  GetRawInputFn,
29
29
  MaybePromise,
@@ -47,7 +47,7 @@ type DefaultValue<TValue, TFallback> = TValue extends UnsetMarker
47
47
  type inferSubscriptionOutput<TOutput> = TOutput extends AsyncIterable<
48
48
  infer $Output
49
49
  >
50
- ? inferSSEOutput<$Output>
50
+ ? inferTrackedOutput<$Output>
51
51
  : inferObservableValue<TOutput>;
52
52
 
53
53
  export type CallerOverride<TContext> = (opts: {
@@ -80,7 +80,10 @@ export interface RootConfig<TTypes extends RootTypes> {
80
80
  * @default true
81
81
  */
82
82
  enabled?: boolean;
83
- } & Omit<SSEStreamProducerOptions, 'maxDepth' | 'data' | 'serialize'>;
83
+ } & Pick<
84
+ SSEStreamProducerOptions,
85
+ 'ping' | 'emitAndEndImmediately' | 'maxDurationMs'
86
+ >;
84
87
  };
85
88
  }
86
89
 
@@ -49,7 +49,17 @@ export namespace JSONRPC2 {
49
49
  /////////////////////////// HTTP envelopes ///////////////////////
50
50
 
51
51
  export interface TRPCRequest
52
- extends JSONRPC2.Request<ProcedureType, { path: string; input: unknown }> {}
52
+ extends JSONRPC2.Request<
53
+ ProcedureType,
54
+ {
55
+ path: string;
56
+ input: unknown;
57
+ /**
58
+ * The last event id that the client received
59
+ */
60
+ lastEventId?: string;
61
+ }
62
+ > {}
53
63
 
54
64
  export interface TRPCResult<TData = unknown> {
55
65
  data: TData;
@@ -101,7 +111,13 @@ export interface TRPCResultMessage<TData>
101
111
  extends JSONRPC2.ResultResponse<
102
112
  | { type: 'started'; data?: never }
103
113
  | { type: 'stopped'; data?: never }
104
- | (TRPCResult<TData> & { type: 'data' })
114
+ | (TRPCResult<TData> & {
115
+ type: 'data';
116
+ /**
117
+ * The id of the message to keep track of in case of a reconnect
118
+ */
119
+ id?: string;
120
+ })
105
121
  > {}
106
122
 
107
123
  export type TRPCResponseMessage<
@@ -67,9 +67,12 @@ export function parseTRPCMessage(
67
67
  }
68
68
  assertIsProcedureType(method);
69
69
  assertIsObject(params);
70
- const { input: rawInput, path } = params;
70
+ const { input: rawInput, path, lastEventId } = params;
71
71
 
72
72
  assertIsString(path);
73
+ if (lastEventId !== undefined) {
74
+ assertIsString(lastEventId);
75
+ }
73
76
 
74
77
  const input = transformer.input.deserialize(rawInput);
75
78
 
@@ -80,6 +83,7 @@ export function parseTRPCMessage(
80
83
  params: {
81
84
  input,
82
85
  path,
86
+ lastEventId,
83
87
  },
84
88
  };
85
89
  }
@@ -30,8 +30,8 @@ type PROMISE_STATUS_FULFILLED = typeof PROMISE_STATUS_FULFILLED;
30
30
  const PROMISE_STATUS_REJECTED = 1;
31
31
  type PROMISE_STATUS_REJECTED = typeof PROMISE_STATUS_REJECTED;
32
32
 
33
- const ASYNC_ITERABLE_STATUS_DONE = 0;
34
- type ASYNC_ITERABLE_STATUS_DONE = typeof ASYNC_ITERABLE_STATUS_DONE;
33
+ const ASYNC_ITERABLE_STATUS_RETURN = 0;
34
+ type ASYNC_ITERABLE_STATUS_RETURN = typeof ASYNC_ITERABLE_STATUS_RETURN;
35
35
  const ASYNC_ITERABLE_STATUS_VALUE = 1;
36
36
  type ASYNC_ITERABLE_STATUS_VALUE = typeof ASYNC_ITERABLE_STATUS_VALUE;
37
37
  const ASYNC_ITERABLE_STATUS_ERROR = 2;
@@ -70,7 +70,11 @@ type PromiseChunk =
70
70
  ]
71
71
  | [chunkIndex: ChunkIndex, status: PROMISE_STATUS_REJECTED, error: unknown];
72
72
  type IterableChunk =
73
- | [chunkIndex: ChunkIndex, status: ASYNC_ITERABLE_STATUS_DONE]
73
+ | [
74
+ chunkIndex: ChunkIndex,
75
+ status: ASYNC_ITERABLE_STATUS_RETURN,
76
+ value: DehydratedValue,
77
+ ]
74
78
  | [
75
79
  chunkIndex: ChunkIndex,
76
80
  status: ASYNC_ITERABLE_STATUS_VALUE,
@@ -204,7 +208,11 @@ function createBatchStreamProducer(opts: ProducerOptions) {
204
208
  break;
205
209
  }
206
210
  if (next.done) {
207
- stream.controller.enqueue([idx, ASYNC_ITERABLE_STATUS_DONE]);
211
+ stream.controller.enqueue([
212
+ idx,
213
+ ASYNC_ITERABLE_STATUS_RETURN,
214
+ dehydrate(next.value, path),
215
+ ]);
208
216
  break;
209
217
  }
210
218
  stream.controller.enqueue([
@@ -541,12 +549,12 @@ export async function jsonlStreamConsumer<THead>(opts: {
541
549
  done: false,
542
550
  value: hydrate(data),
543
551
  };
544
- case ASYNC_ITERABLE_STATUS_DONE:
552
+ case ASYNC_ITERABLE_STATUS_RETURN:
545
553
  controllers.delete(chunkId);
546
554
  maybeAbort();
547
555
  return {
548
556
  done: true,
549
- value: undefined,
557
+ value: hydrate(data),
550
558
  };
551
559
  case ASYNC_ITERABLE_STATUS_ERROR:
552
560
  controllers.delete(chunkId);
@@ -1,59 +1,14 @@
1
1
  import { getTRPCErrorFromUnknown } from '../error/TRPCError';
2
- import type { ValidateShape } from '../types';
3
2
  import { run } from '../utils';
4
3
  import type { ConsumerOnError } from './jsonl';
4
+ import type { inferTrackedOutput } from './tracked';
5
+ import { isTrackedEnvelope } from './tracked';
5
6
  import { createTimeoutPromise } from './utils/createDeferred';
6
7
  import { createReadableStream } from './utils/createReadableStream';
7
8
 
8
9
  type Serialize = (value: any) => any;
9
10
  type Deserialize = (value: any) => any;
10
11
 
11
- /**
12
- * Server-sent Event Message
13
- * @see https://html.spec.whatwg.org/multipage/server-sent-events.html
14
- * @public
15
- */
16
- export interface SSEMessage {
17
- /**
18
- * The data field of the message - this can be anything
19
- */
20
- data: unknown;
21
- /**
22
- * The id for this message
23
- * Passing this id will allow the client to resume the connection from this point if the connection is lost
24
- * @see https://html.spec.whatwg.org/multipage/server-sent-events.html#the-last-event-id-header
25
- */
26
- id: string;
27
- }
28
-
29
- const sseSymbol = Symbol('SSEMessageEnvelope');
30
- export type SSEMessageEnvelope<TData> = [typeof sseSymbol, TData];
31
-
32
- /**
33
- * Produce a typed server-sent event message
34
- */
35
- export function sse<TData extends SSEMessage>(
36
- event: ValidateShape<TData, SSEMessage>,
37
- ): SSEMessageEnvelope<TData> {
38
- if (event.id === '') {
39
- // This could be removed by using different event names for `yield sse(x)`-emitted events and `yield y`-emitted events
40
- throw new Error(
41
- '`id` must not be an empty string as empty string is the same as not setting the id at all',
42
- );
43
- }
44
- return [sseSymbol, event as TData];
45
- }
46
-
47
- export function isSSEMessageEnvelope<TData extends SSEMessage>(
48
- value: unknown,
49
- ): value is SSEMessageEnvelope<TData> {
50
- return Array.isArray(value) && value[0] === sseSymbol;
51
- }
52
-
53
- export type SerializedSSEvent = Omit<SSEMessage, 'data'> & {
54
- data?: string;
55
- };
56
-
57
12
  /**
58
13
  * @internal
59
14
  */
@@ -87,14 +42,17 @@ export interface SSEStreamProducerOptions {
87
42
  * @default false
88
43
  */
89
44
  emitAndEndImmediately?: boolean;
45
+ formatError?: (opts: { error: unknown }) => unknown;
90
46
  }
91
47
 
92
- type SSEvent = Partial<
93
- SSEMessage & {
94
- comment: string;
95
- event: string;
96
- }
97
- >;
48
+ const SERIALIZED_ERROR_EVENT = 'serialized-error';
49
+
50
+ type SSEvent = Partial<{
51
+ id: string;
52
+ data: unknown;
53
+ comment: string;
54
+ event: string;
55
+ }>;
98
56
  /**
99
57
  *
100
58
  * @see https://html.spec.whatwg.org/multipage/server-sent-events.html
@@ -151,7 +109,13 @@ export function sseStreamProducer(opts: SSEStreamProducerOptions) {
151
109
  }
152
110
 
153
111
  if (next instanceof Error) {
154
- stream.controller.error(next);
112
+ const data = opts.formatError
113
+ ? opts.formatError({ error: next })
114
+ : null;
115
+ stream.controller.enqueue({
116
+ event: SERIALIZED_ERROR_EVENT,
117
+ data: JSON.stringify(serialize(data)),
118
+ });
155
119
  break;
156
120
  }
157
121
  if (next.done) {
@@ -160,8 +124,11 @@ export function sseStreamProducer(opts: SSEStreamProducerOptions) {
160
124
 
161
125
  const value = next.value;
162
126
 
163
- const chunk: SSEvent = isSSEMessageEnvelope(value)
164
- ? { ...value[1] }
127
+ const chunk: SSEvent = isTrackedEnvelope(value)
128
+ ? {
129
+ id: value[0],
130
+ data: value[1],
131
+ }
165
132
  : {
166
133
  data: value,
167
134
  };
@@ -207,41 +174,65 @@ export function sseStreamProducer(opts: SSEStreamProducerOptions) {
207
174
  }),
208
175
  );
209
176
  }
210
- export type inferSSEOutput<TData> = TData extends SSEMessageEnvelope<
211
- infer $Data
212
- >
213
- ? $Data
214
- : TData;
177
+
178
+ type ConsumerStreamResult<TData> =
179
+ | {
180
+ ok: true;
181
+ data: inferTrackedOutput<TData>;
182
+ }
183
+ | {
184
+ ok: false;
185
+ error: unknown;
186
+ };
187
+
215
188
  /**
216
189
  * @see https://html.spec.whatwg.org/multipage/server-sent-events.html
217
190
  */
218
-
219
191
  export function sseStreamConsumer<TData>(opts: {
220
192
  from: EventSource;
221
193
  onError?: ConsumerOnError;
222
194
  deserialize?: Deserialize;
223
- }): AsyncIterable<inferSSEOutput<TData>> {
195
+ }): AsyncIterable<ConsumerStreamResult<TData>> {
224
196
  const { deserialize = (v) => v } = opts;
225
197
  const eventSource = opts.from;
226
198
 
227
199
  const stream = createReadableStream<MessageEvent>();
228
200
 
229
- const transform = new TransformStream<MessageEvent, inferSSEOutput<TData>>({
201
+ const transform = new TransformStream<
202
+ MessageEvent,
203
+ ConsumerStreamResult<TData>
204
+ >({
230
205
  async transform(chunk, controller) {
231
- const def: Partial<SSEMessage> = {
232
- data: deserialize(JSON.parse(chunk.data)),
206
+ const data = deserialize(JSON.parse(chunk.data));
207
+ if (chunk.type === SERIALIZED_ERROR_EVENT) {
208
+ controller.enqueue({
209
+ ok: false,
210
+ error: data,
211
+ });
212
+ return;
213
+ }
214
+ // console.debug('transforming', chunk.type, chunk.data);
215
+ const def: SSEvent = {
216
+ data,
233
217
  };
234
218
 
235
219
  if (chunk.lastEventId) {
236
220
  def.id = chunk.lastEventId;
237
221
  }
238
- controller.enqueue(def as inferSSEOutput<TData>);
222
+
223
+ controller.enqueue({
224
+ ok: true,
225
+ data: def as inferTrackedOutput<TData>,
226
+ });
239
227
  },
240
228
  });
241
229
 
242
230
  eventSource.addEventListener('message', (msg) => {
243
231
  stream.controller.enqueue(msg);
244
232
  });
233
+ eventSource.addEventListener(SERIALIZED_ERROR_EVENT, (msg) => {
234
+ stream.controller.enqueue(msg);
235
+ });
245
236
  eventSource.addEventListener('error', (cause) => {
246
237
  if (eventSource.readyState === EventSource.CLOSED) {
247
238
  stream.controller.error(cause);
@@ -253,7 +244,7 @@ export function sseStreamConsumer<TData>(opts: {
253
244
  [Symbol.asyncIterator]() {
254
245
  const reader = readable.getReader();
255
246
 
256
- const iterator: AsyncIterator<inferSSEOutput<TData>> = {
247
+ const iterator: AsyncIterator<ConsumerStreamResult<TData>> = {
257
248
  async next() {
258
249
  const value = await reader.read();
259
250
  if (value.done) {
@@ -0,0 +1,55 @@
1
+ const trackedSymbol = Symbol('TrackedEnvelope');
2
+
3
+ type TrackedId = string & {
4
+ __brand: 'TrackedId';
5
+ };
6
+ export type TrackedEnvelope<TData> = [TrackedId, TData, typeof trackedSymbol];
7
+
8
+ type Tracked<TData> = {
9
+ /**
10
+ * The id of the message to keep track of in case the connection gets lost
11
+ */
12
+ id: string;
13
+ /**
14
+ * The data field of the message - this can be anything
15
+ */
16
+ data: TData;
17
+ };
18
+ /**
19
+ * Produce a typed server-sent event message
20
+ * @deprecated use `tracked(id, data)` instead
21
+ */
22
+ export function sse<TData>(event: {
23
+ id: string;
24
+ data: TData;
25
+ }): TrackedEnvelope<TData> {
26
+ return tracked(event.id, event.data);
27
+ }
28
+
29
+ export function isTrackedEnvelope<TData>(
30
+ value: unknown,
31
+ ): value is TrackedEnvelope<TData> {
32
+ return Array.isArray(value) && value[2] === trackedSymbol;
33
+ }
34
+
35
+ /**
36
+ * Automatically track an event so that it can be resumed from a given id if the connection is lost
37
+ */
38
+ export function tracked<TData>(
39
+ id: string,
40
+ data: TData,
41
+ ): TrackedEnvelope<TData> {
42
+ if (id === '') {
43
+ // This limitation could be removed by using different SSE event names / channels for tracked event and non-tracked event
44
+ throw new Error(
45
+ '`id` must not be an empty string as empty string is the same as not setting the id at all',
46
+ );
47
+ }
48
+ return [id as TrackedId, data, trackedSymbol];
49
+ }
50
+
51
+ export type inferTrackedOutput<TData> = TData extends TrackedEnvelope<
52
+ infer $Data
53
+ >
54
+ ? Tracked<$Data>
55
+ : TData;
@@ -34,6 +34,7 @@ export * from './unstable-core-do-not-import/router';
34
34
  export * from './unstable-core-do-not-import/rpc';
35
35
  export * from './unstable-core-do-not-import/stream/jsonl';
36
36
  export * from './unstable-core-do-not-import/stream/sse';
37
+ export * from './unstable-core-do-not-import/stream/tracked';
37
38
  export * from './unstable-core-do-not-import/stream/utils/createDeferred';
38
39
  export * from './unstable-core-do-not-import/transformer';
39
40
  export * from './unstable-core-do-not-import/types';