@trpc/server 11.0.0-rc.746 → 11.0.0-rc.748

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 (35) hide show
  1. package/dist/@trpc/server/index.d.ts +1 -1
  2. package/dist/@trpc/server/index.d.ts.map +1 -1
  3. package/dist/adapters/node-http/nodeHTTPRequestHandler.js +1 -1
  4. package/dist/adapters/node-http/nodeHTTPRequestHandler.mjs +1 -1
  5. package/dist/adapters/ws.js +2 -2
  6. package/dist/adapters/ws.mjs +2 -2
  7. package/dist/bundle-analysis.json +122 -119
  8. package/dist/index.js +4 -3
  9. package/dist/index.mjs +1 -1
  10. package/dist/unstable-core-do-not-import/http/contentType.d.ts +2 -2
  11. package/dist/unstable-core-do-not-import/http/contentType.d.ts.map +1 -1
  12. package/dist/unstable-core-do-not-import/http/contentType.js +12 -10
  13. package/dist/unstable-core-do-not-import/http/contentType.mjs +12 -10
  14. package/dist/unstable-core-do-not-import/http/resolveResponse.js +2 -2
  15. package/dist/unstable-core-do-not-import/http/resolveResponse.mjs +2 -2
  16. package/dist/unstable-core-do-not-import/procedureBuilder.js +1 -0
  17. package/dist/unstable-core-do-not-import/procedureBuilder.mjs +1 -0
  18. package/dist/unstable-core-do-not-import/router.d.ts +20 -3
  19. package/dist/unstable-core-do-not-import/router.d.ts.map +1 -1
  20. package/dist/unstable-core-do-not-import/router.js +102 -6
  21. package/dist/unstable-core-do-not-import/router.mjs +102 -8
  22. package/dist/unstable-core-do-not-import/stream/jsonl.d.ts +3 -9
  23. package/dist/unstable-core-do-not-import/stream/jsonl.d.ts.map +1 -1
  24. package/dist/unstable-core-do-not-import/stream/jsonl.js +24 -33
  25. package/dist/unstable-core-do-not-import/stream/jsonl.mjs +24 -33
  26. package/dist/unstable-core-do-not-import.js +2 -0
  27. package/dist/unstable-core-do-not-import.mjs +1 -1
  28. package/package.json +2 -2
  29. package/src/@trpc/server/index.ts +1 -0
  30. package/src/adapters/ws.ts +1 -1
  31. package/src/unstable-core-do-not-import/http/contentType.ts +48 -42
  32. package/src/unstable-core-do-not-import/http/resolveResponse.ts +2 -2
  33. package/src/unstable-core-do-not-import/procedureBuilder.ts +1 -0
  34. package/src/unstable-core-do-not-import/router.ts +156 -14
  35. package/src/unstable-core-do-not-import/stream/jsonl.ts +28 -40
@@ -13,7 +13,13 @@ import type { ProcedureCallOptions } from './procedureBuilder';
13
13
  import type { AnyRootTypes, RootConfig } from './rootConfig';
14
14
  import { defaultTransformer } from './transformer';
15
15
  import type { MaybePromise, ValueOf } from './types';
16
- import { isFunction, mergeWithoutOverrides, omitPrototype } from './utils';
16
+ import {
17
+ isFunction,
18
+ isObject,
19
+ mergeWithoutOverrides,
20
+ omitPrototype,
21
+ run,
22
+ } from './utils';
17
23
 
18
24
  export interface RouterRecord {
19
25
  [key: string]: AnyProcedure | RouterRecord;
@@ -69,6 +75,58 @@ export type RouterCaller<
69
75
  },
70
76
  ) => DecorateRouterRecord<TRecord>;
71
77
 
78
+ const lazySymbol = Symbol('lazy');
79
+ export type Lazy<TAny> = (() => Promise<TAny>) & { [lazySymbol]: true };
80
+
81
+ type LazyLoader<TAny> = {
82
+ load: () => Promise<void>;
83
+ ref: Lazy<TAny>;
84
+ };
85
+
86
+ /**
87
+ * Lazy load a router
88
+ * @see https://trpc.io/docs/server/merging-routers#lazy-load
89
+ */
90
+ export function lazy<TRouter extends AnyRouter>(
91
+ getRouter: () => Promise<
92
+ | TRouter
93
+ | {
94
+ [key: string]: TRouter;
95
+ }
96
+ >,
97
+ ): Lazy<NoInfer<TRouter>> {
98
+ let cachedPromise: Promise<TRouter> | null = null;
99
+ const lazyGetter = (() => {
100
+ if (!cachedPromise) {
101
+ cachedPromise = run(async (): Promise<TRouter> => {
102
+ const mod = await getRouter();
103
+
104
+ // if the module is a router, return it
105
+ if (isRouter(mod)) {
106
+ return mod;
107
+ }
108
+
109
+ const routers = Object.values(mod);
110
+
111
+ if (routers.length !== 1 || !isRouter(routers[0])) {
112
+ throw new Error(
113
+ "Invalid router module - either define exactly 1 export or return the router directly.\nExample: `experimental_lazy(() => import('./slow.js').then((m) => m.slowRouter))`",
114
+ );
115
+ }
116
+
117
+ return routers[0];
118
+ });
119
+ }
120
+ return cachedPromise;
121
+ }) as Lazy<TRouter>;
122
+ lazyGetter[lazySymbol] = true;
123
+ return lazyGetter;
124
+ }
125
+
126
+ function isLazy<TAny>(input: unknown): input is Lazy<TAny> {
127
+ return typeof input === 'function' && lazySymbol in input;
128
+ }
129
+
72
130
  export interface Router<
73
131
  TRoot extends AnyRootTypes,
74
132
  TRecord extends RouterRecord,
@@ -79,6 +137,7 @@ export interface Router<
79
137
  procedure?: never;
80
138
  procedures: TRecord;
81
139
  record: TRecord;
140
+ lazy: Record<string, LazyLoader<AnyRouter>>;
82
141
  };
83
142
  /**
84
143
  * @deprecated use `t.createCallerFactory(router)` instead
@@ -104,10 +163,10 @@ export type inferRouterError<TRouter extends AnyRouter> =
104
163
  export type inferRouterMeta<TRouter extends AnyRouter> =
105
164
  inferRouterRootTypes<TRouter>['meta'];
106
165
 
107
- function isRouter(
108
- procedureOrRouter: ValueOf<CreateRouterOptions>,
109
- ): procedureOrRouter is AnyRouter {
110
- return procedureOrRouter._def && 'router' in procedureOrRouter._def;
166
+ function isRouter(value: unknown): value is AnyRouter {
167
+ return (
168
+ isObject(value) && isObject(value['_def']) && 'router' in value['_def']
169
+ );
111
170
  }
112
171
 
113
172
  const emptyRouter = {
@@ -138,7 +197,11 @@ const reservedWords = [
138
197
  ];
139
198
 
140
199
  export type CreateRouterOptions = {
141
- [key: string]: AnyProcedure | AnyRouter | CreateRouterOptions;
200
+ [key: string]:
201
+ | AnyProcedure
202
+ | AnyRouter
203
+ | CreateRouterOptions
204
+ | Lazy<AnyRouter>;
142
205
  };
143
206
 
144
207
  export type DecorateCreateRouterOptions<
@@ -149,9 +212,11 @@ export type DecorateCreateRouterOptions<
149
212
  ? $Value
150
213
  : $Value extends Router<any, infer TRecord>
151
214
  ? TRecord
152
- : $Value extends CreateRouterOptions
153
- ? DecorateCreateRouterOptions<$Value>
154
- : never
215
+ : $Value extends Lazy<Router<any, infer TRecord>>
216
+ ? TRecord
217
+ : $Value extends CreateRouterOptions
218
+ ? DecorateCreateRouterOptions<$Value>
219
+ : never
155
220
  : never;
156
221
  };
157
222
 
@@ -175,10 +240,55 @@ export function createRouterFactory<TRoot extends AnyRootTypes>(
175
240
  }
176
241
 
177
242
  const procedures: Record<string, AnyProcedure> = omitPrototype({});
243
+ const lazy: Record<string, LazyLoader<AnyRouter>> = omitPrototype({});
244
+
245
+ function createLazyLoader(opts: {
246
+ ref: Lazy<AnyRouter>;
247
+ path: readonly string[];
248
+ key: string;
249
+ aggregate: RouterRecord;
250
+ }): LazyLoader<AnyRouter> {
251
+ return {
252
+ ref: opts.ref,
253
+ load: async () => {
254
+ const router = await opts.ref();
255
+ const lazyPath = [...opts.path, opts.key];
256
+ const lazyKey = lazyPath.join('.');
257
+
258
+ opts.aggregate[opts.key] = step(router._def.record, lazyPath);
259
+
260
+ delete lazy[lazyKey];
261
+
262
+ // add lazy loaders for nested routers
263
+ for (const [nestedKey, nestedItem] of Object.entries(
264
+ router._def.lazy,
265
+ )) {
266
+ const nestedRouterKey = [...lazyPath, nestedKey].join('.');
267
+
268
+ // console.log('adding lazy', nestedRouterKey);
269
+ lazy[nestedRouterKey] = createLazyLoader({
270
+ ref: nestedItem.ref,
271
+ path: lazyPath,
272
+ key: nestedKey,
273
+ aggregate: opts.aggregate[opts.key] as RouterRecord,
274
+ });
275
+ }
276
+ },
277
+ };
278
+ }
178
279
 
179
280
  function step(from: CreateRouterOptions, path: readonly string[] = []) {
180
281
  const aggregate: RouterRecord = omitPrototype({});
181
282
  for (const [key, item] of Object.entries(from ?? {})) {
283
+ if (isLazy(item)) {
284
+ lazy[[...path, key].join('.')] = createLazyLoader({
285
+ path,
286
+ ref: item,
287
+ key,
288
+ aggregate,
289
+ });
290
+ continue;
291
+ }
182
292
  if (isRouter(item)) {
183
293
  aggregate[key] = step(item._def.record, [...path, key]);
184
294
  continue;
@@ -207,6 +317,7 @@ export function createRouterFactory<TRoot extends AnyRootTypes>(
207
317
  _config: config,
208
318
  router: true,
209
319
  procedures,
320
+ lazy,
210
321
  ...emptyRouter,
211
322
  record,
212
323
  };
@@ -229,17 +340,42 @@ function isProcedure(
229
340
  ): procedureOrRouter is AnyProcedure {
230
341
  return typeof procedureOrRouter === 'function';
231
342
  }
343
+
344
+ export async function getProcedureAtPath(
345
+ _def: AnyRouter['_def'],
346
+ path: string,
347
+ ): Promise<AnyProcedure | null> {
348
+ let procedure = _def.procedures[path];
349
+
350
+ while (!procedure) {
351
+ const key = Object.keys(_def.lazy).find((key) => path.startsWith(key));
352
+ // console.log(`found lazy: ${key ?? 'NOPE'} (fullPath: ${fullPath})`);
353
+
354
+ if (!key) {
355
+ return null;
356
+ }
357
+ // console.log('loading', key, '.......');
358
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
359
+ const lazyRouter = _def.lazy[key]!;
360
+ await lazyRouter.load();
361
+
362
+ procedure = _def.procedures[path];
363
+ }
364
+
365
+ return procedure;
366
+ }
367
+
232
368
  /**
233
369
  * @internal
234
370
  */
235
- export function callProcedure(
371
+ export async function callProcedure(
236
372
  opts: ProcedureCallOptions<unknown> & {
237
- procedures: RouterRecord;
373
+ _def: AnyRouter['_def'];
238
374
  allowMethodOverride?: boolean;
239
375
  },
240
376
  ) {
241
377
  const { type, path } = opts;
242
- const proc = opts.procedures[path];
378
+ const proc = await getProcedureAtPath(opts._def, path);
243
379
  if (
244
380
  !proc ||
245
381
  !isProcedure(proc) ||
@@ -282,10 +418,16 @@ export function createCallerFactory<TRoot extends AnyRootTypes>() {
282
418
  return _def;
283
419
  }
284
420
 
285
- const procedure = _def.procedures[fullPath] as AnyProcedure;
421
+ const procedure = await getProcedureAtPath(_def, fullPath);
286
422
 
287
423
  let ctx: Context | undefined = undefined;
288
424
  try {
425
+ if (!procedure) {
426
+ throw new TRPCError({
427
+ code: 'NOT_FOUND',
428
+ message: `No procedure found on path "${path}"`,
429
+ });
430
+ }
289
431
  ctx = isFunction(ctxOrCallback)
290
432
  ? await Promise.resolve(ctxOrCallback())
291
433
  : ctxOrCallback;
@@ -303,7 +445,7 @@ export function createCallerFactory<TRoot extends AnyRootTypes>() {
303
445
  error: getTRPCErrorFromUnknown(cause),
304
446
  input: args[0],
305
447
  path: fullPath,
306
- type: procedure._def.type,
448
+ type: procedure?._def.type ?? 'unknown',
307
449
  });
308
450
  throw cause;
309
451
  }
@@ -309,13 +309,6 @@ export function jsonlStreamProducer(opts: JSONLProducerOptions) {
309
309
  .pipeThrough(new TextEncoderStream());
310
310
  }
311
311
 
312
- class StreamInterruptedError extends Error {
313
- constructor(cause?: unknown) {
314
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
315
- // @ts-ignore https://github.com/tc39/proposal-error-cause
316
- super('Invalid response or stream interrupted', { cause });
317
- }
318
- }
319
312
  class AsyncError extends Error {
320
313
  constructor(public readonly data: unknown) {
321
314
  super('Received error from server');
@@ -403,10 +396,6 @@ function createConsumerStream<THead>(
403
396
  }),
404
397
  );
405
398
  }
406
- /**
407
- * Represents a chunk of data or stream interruption error that can be enqueued to a controller
408
- */
409
- type ControllerChunk = ChunkData | StreamInterruptedError;
410
399
 
411
400
  /**
412
401
  * Creates a handler for managing stream controllers and their lifecycle
@@ -428,29 +417,19 @@ function createStreamsManager(abortController: AbortController) {
428
417
  * Creates a stream controller
429
418
  */
430
419
  function createStreamController() {
431
- let originalController: ReadableStreamDefaultController<ControllerChunk>;
432
- const stream = new ReadableStream<ControllerChunk>({
420
+ let originalController: ReadableStreamDefaultController<ChunkData>;
421
+ const stream = new ReadableStream<ChunkData>({
433
422
  start(controller) {
434
423
  originalController = controller;
435
424
  },
436
425
  });
437
426
 
438
427
  const streamController = {
439
- enqueue: (v: ControllerChunk) => originalController.enqueue(v),
428
+ enqueue: (v: ChunkData) => originalController.enqueue(v),
440
429
  close: () => {
441
430
  originalController.close();
442
431
 
443
- // mark as closed and remove methods
444
- Object.assign(streamController, {
445
- closed: true,
446
- close: () => {
447
- // noop
448
- },
449
- enqueue: () => {
450
- // noop
451
- },
452
- getReaderResource: null,
453
- });
432
+ clear();
454
433
 
455
434
  if (isEmpty()) {
456
435
  abortController.abort();
@@ -465,7 +444,26 @@ function createStreamsManager(abortController: AbortController) {
465
444
  streamController.close();
466
445
  });
467
446
  },
447
+ error: (reason: unknown) => {
448
+ originalController.error(reason);
449
+ clear();
450
+ },
468
451
  };
452
+ function clear() {
453
+ Object.assign(streamController, {
454
+ closed: true,
455
+ close: () => {
456
+ // noop
457
+ },
458
+ enqueue: () => {
459
+ // noop
460
+ },
461
+ getReaderResource: null,
462
+ error: () => {
463
+ // noop
464
+ },
465
+ });
466
+ }
469
467
 
470
468
  return streamController;
471
469
  }
@@ -486,10 +484,8 @@ function createStreamsManager(abortController: AbortController) {
486
484
  * Cancels all pending controllers and rejects deferred promises
487
485
  */
488
486
  function cancelAll(reason: unknown) {
489
- const error = new StreamInterruptedError(reason);
490
487
  for (const controller of controllerMap.values()) {
491
- controller.enqueue(error);
492
- controller.close();
488
+ controller.error(reason);
493
489
  }
494
490
  }
495
491
 
@@ -541,9 +537,6 @@ export async function jsonlStreamConsumer<THead>(opts: {
541
537
  using reader = controller.getReaderResource();
542
538
 
543
539
  const { value } = await reader.read();
544
- if (value instanceof StreamInterruptedError) {
545
- throw value;
546
- }
547
540
  const [_chunkId, status, data] = value as PromiseChunk;
548
541
  switch (status) {
549
542
  case PROMISE_STATUS_FULFILLED:
@@ -559,9 +552,6 @@ export async function jsonlStreamConsumer<THead>(opts: {
559
552
 
560
553
  while (true) {
561
554
  const { value } = await reader.read();
562
- if (value instanceof StreamInterruptedError) {
563
- throw value;
564
- }
565
555
 
566
556
  const [_chunkId, status, data] = value as IterableChunk;
567
557
 
@@ -598,11 +588,9 @@ export async function jsonlStreamConsumer<THead>(opts: {
598
588
  return data;
599
589
  }
600
590
 
601
- const closeOrAbort = (reason?: unknown) => {
602
- const error = new StreamInterruptedError(reason);
603
-
604
- headDeferred?.reject(error);
605
- streamManager.cancelAll(error);
591
+ const closeOrAbort = (reason: unknown) => {
592
+ headDeferred?.reject(reason);
593
+ streamManager.cancelAll(reason);
606
594
  };
607
595
  source
608
596
  .pipeTo(
@@ -626,7 +614,7 @@ export async function jsonlStreamConsumer<THead>(opts: {
626
614
  const controller = streamManager.getOrCreate(idx);
627
615
  controller.enqueue(chunk);
628
616
  },
629
- close: closeOrAbort,
617
+ close: () => closeOrAbort(new Error('Stream closed')),
630
618
  abort: closeOrAbort,
631
619
  }),
632
620
  {