trpc-uwebsockets 11.0.4 → 11.1.2

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.
@@ -1,56 +1,25 @@
1
- /* eslint-disable @typescript-eslint/no-unused-vars */
2
- import type {
3
- HTTPHeaders,
4
- TRPCLink,
5
- WebSocketClientOptions,
6
- } from '@trpc/client';
7
- import {
8
- createTRPCClient,
9
- createWSClient,
10
- httpBatchLink,
11
- TRPCClientError,
12
- httpBatchStreamLink,
13
- httpSubscriptionLink,
14
- wsLink,
15
- } from '@trpc/client';
1
+ import { TRPCClientError } from '@trpc/client';
16
2
  import { initTRPC, TRPCError } from '@trpc/server';
17
3
  import { observable } from '@trpc/server/observable';
18
- import { EventSourcePolyfill } from 'event-source-polyfill';
19
4
  import { EventEmitter } from 'events';
20
- import uWs from 'uWebSockets.js';
21
- import {
22
- afterEach,
23
- beforeEach,
24
- describe,
25
- expect,
26
- expectTypeOf,
27
- test,
28
- vi,
29
- } from 'vitest';
30
- import { z } from 'zod';
31
5
 
32
- import {
33
- type CreateContextOptions,
34
- applyRequestHandler,
35
- CreateHandlerOptions,
36
- } from './requestHandler';
37
- import { applyWebsocketHandler } from './websockets';
6
+ import { afterEach, describe, expect, expectTypeOf, test, vi } from 'vitest';
7
+ import { z } from 'zod';
8
+ import { testFactory } from './___testHelpers';
38
9
 
39
- const config = {
40
- prefix: '/trpc',
41
- };
10
+ import { type CreateContextOptions } from './requestHandler';
11
+ import { TRPCRequestInfo } from '@trpc/server/unstable-core-do-not-import';
42
12
 
43
13
  async function createContext({ req, res, info, client }: CreateContextOptions) {
44
14
  const user = { name: req.headers.get('username') || 'anonymous' };
45
15
 
46
16
  await new Promise((resolve) => setTimeout(resolve, 10));
47
17
 
48
- if (req.headers.has('throw')) {
18
+ if (info.url?.searchParams.get('throw')) {
49
19
  throw new TRPCError({
50
20
  code: 'BAD_REQUEST',
51
- message: req.headers.get('throw')!,
21
+ message: info.url!.searchParams.get('throw')!,
52
22
  });
53
- // throw new Error(req.headers.get('throw')!);
54
23
  }
55
24
 
56
25
  // for websocket context throwing after connection
@@ -61,15 +30,6 @@ async function createContext({ req, res, info, client }: CreateContextOptions) {
61
30
  });
62
31
  }
63
32
 
64
- // for websocket context throwing during connection
65
- const url = new URL(req.url);
66
- if (url.searchParams.has('throw')) {
67
- throw new TRPCError({
68
- code: 'BAD_REQUEST',
69
- message: url.searchParams.get('throw')!,
70
- });
71
- }
72
-
73
33
  // test writing headers inside the context
74
34
  if (info.type === 'query') {
75
35
  res.cork(() => {
@@ -79,8 +39,6 @@ async function createContext({ req, res, info, client }: CreateContextOptions) {
79
39
  return { req, res, user, info, client };
80
40
  }
81
41
 
82
- type Context = Awaited<ReturnType<typeof createContext>>;
83
-
84
42
  interface Message {
85
43
  id: string;
86
44
  }
@@ -90,7 +48,7 @@ function createAppRouter() {
90
48
  const onNewMessageSubscription = vi.fn();
91
49
  const onSubscriptionEnded = vi.fn();
92
50
 
93
- const t = initTRPC.context<Context>().create();
51
+ const t = initTRPC.create();
94
52
  const router = t.router;
95
53
  const publicProcedure = t.procedure;
96
54
 
@@ -110,7 +68,7 @@ function createAppRouter() {
110
68
  .nullish()
111
69
  )
112
70
  .query(({ input, ctx }) => ({
113
- text: `hello ${input?.username ?? ctx.user?.name ?? 'world'}`,
71
+ text: `hello ${input?.username ?? (ctx as any).user?.name ?? 'world'}`,
114
72
  })),
115
73
  helloMutation: publicProcedure
116
74
  .input(z.string())
@@ -126,7 +84,7 @@ function createAppRouter() {
126
84
  })
127
85
  )
128
86
  .mutation(async ({ input, ctx }) => {
129
- if (ctx.user.name === 'anonymous') {
87
+ if ((ctx as any).user.name === 'anonymous') {
130
88
  return { error: 'Unauthorized user' };
131
89
  }
132
90
  const { id, data } = input;
@@ -148,10 +106,12 @@ function createAppRouter() {
148
106
  return sub;
149
107
  }),
150
108
  pubSub: publicProcedure.input(z.null()).subscription(({ ctx }) => {
151
- if (!ctx.client) {
109
+ // TODO: fix all these
110
+ const anyContext = ctx as any;
111
+ if (!anyContext.client) {
152
112
  throw new Error('assertion: client should never be null in websockets');
153
113
  }
154
- const client = ctx.client!;
114
+ const client = anyContext.client!;
155
115
  // for some reason client.publish does not work...
156
116
  // currently okay is false, not sure why uWs behaves this way
157
117
  // publishing from the server works though
@@ -186,7 +146,7 @@ function createAppRouter() {
186
146
  }),
187
147
  request: router({
188
148
  info: publicProcedure.query(({ ctx }) => {
189
- return ctx.info;
149
+ return (ctx as any).info as TRPCRequestInfo;
190
150
  }),
191
151
  }),
192
152
  deferred: publicProcedure
@@ -222,7 +182,10 @@ function createAppRouter() {
222
182
  return;
223
183
  }
224
184
  if (input.throw === i) {
225
- throw new Error(i.toString());
185
+ throw new TRPCError({
186
+ code: 'BAD_REQUEST',
187
+ message: i.toString(),
188
+ });
226
189
  }
227
190
  yield i;
228
191
  }
@@ -232,249 +195,35 @@ function createAppRouter() {
232
195
  return { appRouter, ee, onNewMessageSubscription, onSubscriptionEnded };
233
196
  }
234
197
 
235
- type CreateAppRouter = Awaited<ReturnType<typeof createAppRouter>>;
236
- type AppRouter = CreateAppRouter['appRouter'];
237
-
238
- interface ServerOptions {
239
- appRouter: AppRouter;
240
- }
241
-
242
- function createServer(opts: ServerOptions) {
243
- const instance = uWs.App();
244
-
245
- const router = opts.appRouter;
246
-
247
- applyRequestHandler(instance, {
248
- prefix: config.prefix,
249
- ssl: false,
250
- trpcOptions: {
251
- router,
252
- createContext,
253
- onError(data) {
254
- // console.error('trpc error', data);
255
- },
256
- responseMeta({ ctx, paths, type, errors }) {
257
- return {
258
- headers: {
259
- 'Access-Control-Allow-Origin': '*',
260
- },
261
- };
262
- },
263
- } satisfies CreateHandlerOptions<AppRouter>['trpcOptions'],
264
- });
265
- applyWebsocketHandler(instance, {
266
- prefix: config.prefix,
267
- router,
268
- createContext,
269
- });
270
-
271
- instance.get('/hello', async (res, req) => {
272
- res.end('Hello world');
273
- });
274
- instance.post('/hello', async (res, req) => {
275
- res.end(JSON.stringify({ hello: 'POST', body: 'TODO, why?' }));
276
- });
198
+ let stopFn = () => {};
277
199
 
278
- instance.ws('/ws', {
279
- message: (client, rawMsg) => {
280
- client.send(rawMsg);
281
- },
282
- });
283
-
284
- instance.ws('/pubsub', {
285
- open: (client) => {
286
- const ok = client.subscribe('topic');
287
-
288
- if (!ok) {
289
- throw new Error("assertion: failed to subscribe to 'topic'");
290
- }
291
- },
292
- });
293
-
294
- instance.any('/*', (res, req) => {
295
- res.writeStatus('404 NOT FOUND');
296
- res.end();
297
- });
200
+ afterEach(() => {
201
+ stopFn();
202
+ });
298
203
 
299
- let socket: uWs.us_listen_socket | false | null = null;
204
+ function defaultFactory(config?: {
205
+ createContext?: (opts: CreateContextOptions) => Promise<any>;
206
+ }) {
207
+ const router = createAppRouter();
300
208
 
301
- instance.listen('0.0.0.0', 0, (token) => {
302
- socket = token;
209
+ const factoryVal = testFactory({
210
+ appRouter: router.appRouter,
211
+ createContext: config?.createContext ?? createContext,
303
212
  });
304
213
 
305
- if (!socket) {
306
- throw new Error('could not make a socket');
307
- }
308
-
309
- const port = uWs.us_socket_local_port(socket);
214
+ stopFn = factoryVal.stop;
310
215
 
311
216
  return {
312
- stop() {
313
- if (!socket) {
314
- throw new Error('could not close socket as socket is already closed');
315
- }
316
- uWs.us_listen_socket_close(socket);
317
- socket = null;
318
- },
319
- port,
320
- instance,
217
+ ...router,
218
+ ...factoryVal,
321
219
  };
322
220
  }
323
221
 
324
- const orderedResults: number[] = [];
325
- const linkSpy: TRPCLink<AppRouter> = () => {
326
- // here we just got initialized in the app - this happens once per app
327
- // useful for storing cache for instance
328
- return ({ next, op }) => {
329
- // this is when passing the result to the next link
330
- // each link needs to return an observable which propagates results
331
- return observable((observer) => {
332
- const unsubscribe = next(op).subscribe({
333
- next(value) {
334
- orderedResults.push(value.result.data as number);
335
- observer.next(value);
336
- },
337
- error: observer.error,
338
- });
339
- return unsubscribe;
340
- });
341
- };
342
- };
343
-
344
- interface ClientOptions {
345
- wsClientOptions?: Omit<WebSocketClientOptions, 'url'> | undefined;
346
- queryParams?: Record<string, string> | undefined;
347
- headers?: HTTPHeaders | undefined;
348
- port: number;
349
- }
350
-
351
- function toQueryString(queryParams: Record<string, string>) {
352
- const raw = new URLSearchParams(queryParams).toString();
353
- return raw.length == 0 ? '' : `?${raw}`;
354
- }
355
-
356
- function createClientWs(opts: ClientOptions) {
357
- const qs = toQueryString(opts.queryParams ?? {});
358
- const host = `localhost:${opts.port}${config.prefix}${qs}`;
359
- const wsClient = createWSClient({
360
- retryDelayMs: () => 99999, // never retry by default
361
- ...opts.wsClientOptions,
362
- url: `ws://${host}`,
363
- });
364
- const client = createTRPCClient<AppRouter>({
365
- links: [linkSpy, wsLink({ client: wsClient })],
366
- });
367
-
368
- return { client, wsClient };
369
- }
370
-
371
- function createClientBatchStream(opts: ClientOptions) {
372
- const qs = toQueryString(opts.queryParams ?? {});
373
- const host = `localhost:${opts.port}${config.prefix}${qs}`;
374
- const client = createTRPCClient<AppRouter>({
375
- links: [
376
- linkSpy,
377
- httpBatchStreamLink({
378
- url: `http://${host}`,
379
- headers: opts.headers,
380
- }),
381
- ],
382
- });
383
-
384
- return { client };
385
- }
386
-
387
- function createClientBatch(opts: ClientOptions) {
388
- const qs = toQueryString(opts.queryParams ?? {});
389
- const host = `localhost:${opts.port}${config.prefix}${qs}`;
390
- const client = createTRPCClient<AppRouter>({
391
- links: [
392
- httpBatchLink({
393
- url: `http://${host}`,
394
- headers: opts.headers,
395
- }),
396
- ],
397
- });
398
-
399
- return { client };
400
- }
401
-
402
- function createClientSSE(opts: ClientOptions) {
403
- const qs = toQueryString(opts.queryParams ?? {});
404
- const host = `localhost:${opts.port}${config.prefix}${qs}`;
405
- const client = createTRPCClient<AppRouter>({
406
- links: [
407
- // loggerLink(),
408
- httpSubscriptionLink({
409
- url: `http://${host}`,
410
- connectionParams: opts.wsClientOptions?.connectionParams,
411
- // ponyfill EventSource
412
- EventSource: EventSourcePolyfill as any,
413
- }),
414
- ],
415
- });
416
- return { client };
417
- }
418
- type ClientType = 'batchStream' | 'batch' | 'sse';
419
- async function createApp(serverOptions?: Partial<ServerOptions> | undefined) {
420
- const { appRouter, ee } = createAppRouter();
421
- const { instance, port, stop } = createServer({
422
- ...(serverOptions ?? {}),
423
- appRouter,
424
- });
425
-
426
- return {
427
- server: instance,
428
- stop,
429
- getClient(
430
- clientType: ClientType,
431
- clientOptions?: Partial<ClientOptions> | undefined
432
- ) {
433
- switch (clientType) {
434
- case 'batchStream':
435
- return createClientBatchStream({
436
- ...clientOptions,
437
- port,
438
- });
439
- case 'batch':
440
- return createClientBatch({
441
- ...clientOptions,
442
- port,
443
- });
444
- case 'sse':
445
- return createClientSSE({
446
- ...clientOptions,
447
- port,
448
- });
449
- default:
450
- throw new Error('unknown client');
451
- }
452
- },
453
- getClientWs(clientOptions?: Partial<ClientOptions> | undefined) {
454
- return createClientWs({
455
- ...clientOptions,
456
- port,
457
- });
458
- },
459
- ee,
460
- port,
461
- opts: serverOptions,
462
- };
463
- }
464
-
465
- let app: Awaited<ReturnType<typeof createApp>>;
466
- beforeEach(async () => {
467
- orderedResults.length = 0;
468
- app = await createApp();
469
- });
470
-
471
- afterEach(() => {
472
- app.stop();
473
- });
474
-
475
222
  describe('server', () => {
476
223
  test('fetch GET smoke', async () => {
477
- const res = await fetch(`http://localhost:${app.port}/hello`, {
224
+ const ctx = defaultFactory();
225
+
226
+ const res = await ctx.fetch(`/hello`, {
478
227
  method: 'GET',
479
228
  headers: {
480
229
  Accept: 'application/json',
@@ -485,39 +234,23 @@ describe('server', () => {
485
234
  expect(await res.text()).toEqual('Hello world');
486
235
  });
487
236
 
488
- test('response meta', async () => {
489
- const res = await fetch(
490
- `http://localhost:${app.port}/trpc/ping?input=${encodeURI('{}')}`
491
- );
237
+ test('forwards response meta', async () => {
238
+ const ctx = defaultFactory();
239
+ const res = await ctx.fetch(`/trpc/ping?input=${encodeURI('{}')}`, {});
492
240
  expect(res.status).toEqual(200);
493
241
  expect(res.headers.get('Access-Control-Allow-Origin')).toEqual('*'); // from the meta
494
242
  expect(res.headers.get('x-test')).toEqual('true'); // from the context
495
243
  });
496
244
 
497
245
  test('query', async () => {
498
- {
499
- // batch stream
500
- const { client } = app.getClient('batchStream');
501
- expect(await client.ping.query()).toMatchInlineSnapshot(`"pong"`);
502
- expect(await client.hello.query()).toMatchInlineSnapshot(`
503
- Object {
504
- "text": "hello anonymous",
505
- }
506
- `);
507
- expect(
508
- await client.hello.query({
509
- username: 'test',
510
- })
511
- ).toMatchInlineSnapshot(`
512
- Object {
513
- "text": "hello test",
514
- }
515
- `);
516
- }
517
-
518
- {
519
- // batch
520
- const { client } = app.getClient('batch');
246
+ const ctx = defaultFactory();
247
+ const clients = [
248
+ ctx.clientBatchStream().client,
249
+ ctx.clientBatch().client,
250
+ ctx.clientWs().client,
251
+ ];
252
+
253
+ for (const client of clients) {
521
254
  expect(await client.ping.query()).toMatchInlineSnapshot(`"pong"`);
522
255
  expect(await client.hello.query()).toMatchInlineSnapshot(`
523
256
  Object {
@@ -537,23 +270,14 @@ describe('server', () => {
537
270
  });
538
271
 
539
272
  test('mutation', async () => {
540
- {
541
- // batch stream
542
- const { client } = app.getClient('batchStream');
543
- expect(
544
- await client.editPost.mutate({
545
- id: '42',
546
- data: { title: 'new_title', text: 'new_text' },
547
- })
548
- ).toMatchInlineSnapshot(`
549
- Object {
550
- "error": "Unauthorized user",
551
- }
552
- `);
553
- }
554
- {
555
- // batch
556
- const { client } = app.getClient('batch');
273
+ const ctx = defaultFactory();
274
+ const clients = [
275
+ ctx.clientBatchStream().client,
276
+ ctx.clientBatch().client,
277
+ ctx.clientWs().client,
278
+ ];
279
+
280
+ for (const client of clients) {
557
281
  expect(
558
282
  await client.editPost.mutate({
559
283
  id: '42',
@@ -567,33 +291,102 @@ describe('server', () => {
567
291
  }
568
292
  });
569
293
 
570
- test('streaming', async () => {
571
- const { client } = app.getClient('batchStream');
572
- const results = await Promise.all([
573
- client.deferred.query({ wait: 3 }),
574
- client.deferred.query({ wait: 1 }),
575
- client.deferred.query({ wait: 2 }),
576
- ]);
577
- expect(results).toEqual([3, 1, 2]);
578
- expect(orderedResults).toEqual([1, 2, 3]);
579
- });
294
+ test('subscription', { timeout: 5000 }, async () => {
295
+ const ctx = defaultFactory();
296
+ const ee = ctx.ee;
297
+ const clients = [ctx.clientSse().client, ctx.clientWs().client];
580
298
 
581
- test('batched requests in body work correctly', async () => {
582
- {
583
- // batch stream
584
- const { client } = app.getClient('batchStream');
299
+ for (const client of clients) {
300
+ ee.once('subscription:created', () => {
301
+ setTimeout(() => {
302
+ ee.emit('server:msg', {
303
+ id: '1',
304
+ });
305
+ ee.emit('server:msg', {
306
+ id: '2',
307
+ });
308
+ });
309
+ });
310
+ const onStartedMock = vi.fn();
311
+ const onDataMock = vi.fn();
312
+ const sub = client.onMessage.subscribe('onMessage', {
313
+ onStarted: onStartedMock,
314
+ onData(data) {
315
+ expectTypeOf(data).not.toBeAny();
316
+ expectTypeOf(data).toMatchTypeOf<Message>();
317
+ onDataMock(data);
318
+ },
319
+ });
585
320
 
586
- const res = await Promise.all([
587
- client.helloMutation.mutate('world'),
588
- client.helloMutation.mutate('KATT'),
321
+ await vi.waitFor(
322
+ () => {
323
+ expect(onStartedMock).toHaveBeenCalledTimes(1);
324
+ expect(onDataMock).toHaveBeenCalledTimes(2);
325
+ },
326
+ { timeout: 3000 }
327
+ );
328
+
329
+ ee.emit('server:msg', {
330
+ id: '3',
331
+ });
332
+
333
+ await vi.waitFor(() => {
334
+ expect(onDataMock).toHaveBeenCalledTimes(3);
335
+ });
336
+
337
+ expect(onDataMock.mock.calls).toMatchInlineSnapshot(`
338
+ Array [
339
+ Array [
340
+ Object {
341
+ "id": "1",
342
+ },
343
+ ],
344
+ Array [
345
+ Object {
346
+ "id": "2",
347
+ },
348
+ ],
349
+ Array [
350
+ Object {
351
+ "id": "3",
352
+ },
353
+ ],
354
+ ]
355
+ `);
356
+
357
+ sub.unsubscribe();
358
+
359
+ await vi.waitFor(() => {
360
+ expect(ee.listenerCount('server:msg')).toBe(0);
361
+ expect(ee.listenerCount('server:error')).toBe(0);
362
+ });
363
+ }
364
+ });
365
+
366
+ test('streaming', async () => {
367
+ const ctx = defaultFactory();
368
+ const tests = [ctx.clientBatchStream(), ctx.clientWs()];
369
+
370
+ for (const { client, orderedResults } of tests) {
371
+ const results = await Promise.all([
372
+ client.deferred.query({ wait: 3 }),
373
+ client.deferred.query({ wait: 1 }),
374
+ client.deferred.query({ wait: 2 }),
589
375
  ]);
590
- expect(res).toEqual(['hello world', 'hello KATT']);
376
+ expect(results).toEqual([3, 1, 2]);
377
+ expect(orderedResults).toEqual([1, 2, 3]);
591
378
  }
379
+ });
592
380
 
593
- {
594
- //batch
595
- const { client } = app.getClient('batch');
381
+ test('batched requests', async () => {
382
+ const ctx = defaultFactory();
383
+ const clients = [
384
+ ctx.clientBatchStream().client,
385
+ ctx.clientBatch().client,
386
+ ctx.clientWs().client,
387
+ ];
596
388
 
389
+ for (const client of clients) {
597
390
  const res = await Promise.all([
598
391
  client.helloMutation.mutate('world'),
599
392
  client.helloMutation.mutate('KATT'),
@@ -602,105 +395,84 @@ describe('server', () => {
602
395
  }
603
396
  });
604
397
 
605
- test('handles throwing procedure', async () => {
606
- // batch stream
607
- const { client } = app.getClient('batchStream');
608
- await expect(
609
- client.throw.query('expected_procedure_error')
610
- ).rejects.toThrowErrorMatchingInlineSnapshot(
611
- `[TRPCClientError: expected_procedure_error]`
612
- );
613
- });
614
-
615
398
  test('handles throwing context', async () => {
616
- const { client } = app.getClient('batchStream', {
617
- headers: {
399
+ const ctx = defaultFactory();
400
+ const clientOpts = {
401
+ queryParams: {
618
402
  throw: 'expected_context_error',
619
403
  },
620
- });
404
+ };
405
+ const clients = [
406
+ ctx.clientBatchStream(clientOpts).client,
407
+ ctx.clientBatch(clientOpts).client,
408
+ ];
409
+
410
+ for (const client of clients) {
411
+ await expect(
412
+ client.echo.query('hii')
413
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
414
+ `[TRPCClientError: expected_context_error]`
415
+ );
416
+ }
417
+
418
+ // websocket context resolution failure hides the message
419
+ const wsClient = ctx.clientWs(clientOpts).client;
621
420
  await expect(
622
- client.echo.query('hii')
421
+ wsClient.echo.query('hii')
623
422
  ).rejects.toThrowErrorMatchingInlineSnapshot(
624
- `[TRPCClientError: expected_context_error]`
423
+ `[TRPCClientError: Unknown error]`
625
424
  );
626
425
  });
627
426
 
628
- // TODO: test failure of context as in v10
629
-
630
- test('subscription - sse', { timeout: 5000 }, async () => {
631
- const { client } = app.getClient('sse');
632
-
633
- app.ee.once('subscription:created', () => {
634
- setTimeout(() => {
635
- app.ee.emit('server:msg', {
636
- id: '1',
637
- });
638
- app.ee.emit('server:msg', {
639
- id: '2',
640
- });
641
- });
642
- });
643
-
644
- const onStartedMock = vi.fn();
645
- const onDataMock = vi.fn();
646
- const sub = client.onMessage.subscribe('onMessage', {
647
- onStarted: onStartedMock,
648
- onData(data) {
649
- expectTypeOf(data).not.toBeAny();
650
- expectTypeOf(data).toMatchTypeOf<Message>();
651
- onDataMock(data);
652
- },
653
- });
654
-
655
- await vi.waitFor(
656
- () => {
657
- expect(onStartedMock).toHaveBeenCalledTimes(1);
658
- expect(onDataMock).toHaveBeenCalledTimes(2);
659
- },
660
- { timeout: 3000 }
661
- );
662
-
663
- app.ee.emit('server:msg', {
664
- id: '3',
665
- });
666
-
667
- await vi.waitFor(() => {
668
- expect(onDataMock).toHaveBeenCalledTimes(3);
669
- });
427
+ test('handles throwing procedure', async () => {
428
+ const ctx = defaultFactory();
429
+ const clients = [
430
+ ctx.clientBatchStream().client,
431
+ ctx.clientBatch().client,
432
+ ctx.clientWs().client,
433
+ ];
434
+
435
+ for (const client of clients) {
436
+ await expect(
437
+ client.throw.query('expected_procedure_error')
438
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
439
+ `[TRPCClientError: expected_procedure_error]`
440
+ );
441
+ expect(ctx.onServerErrorSpy).toHaveBeenCalled();
442
+ }
443
+ });
670
444
 
671
- expect(onDataMock.mock.calls).toMatchInlineSnapshot(`
672
- Array [
673
- Array [
674
- Object {
675
- "id": "1",
676
- },
677
- ],
678
- Array [
679
- Object {
680
- "id": "2",
681
- },
682
- ],
683
- Array [
684
- Object {
685
- "id": "3",
445
+ test('handles throwing subscription', async () => {
446
+ const ctx = defaultFactory();
447
+ const clients = [ctx.clientWs(), ctx.clientSse()];
448
+
449
+ for (const { client, orderedResults } of clients) {
450
+ let error: TRPCClientError<any> | null = null;
451
+ client.count.subscribe(
452
+ {
453
+ from: 0,
454
+ count: 10,
455
+ throw: 5,
456
+ },
457
+ {
458
+ onError(err) {
459
+ error = err;
686
460
  },
687
- ],
688
- ]
689
- `);
690
-
691
- sub.unsubscribe();
692
-
693
- await vi.waitFor(() => {
694
- expect(app.ee.listenerCount('server:msg')).toBe(0);
695
- expect(app.ee.listenerCount('server:error')).toBe(0);
696
- });
461
+ }
462
+ );
463
+ await vi.waitFor(() => {
464
+ expect(orderedResults).toEqual([0, 1, 2, 3, 4]);
465
+ expect(ctx.onServerErrorSpy).toHaveBeenCalled();
466
+ expect(error!.message).toEqual(new TRPCClientError('5').message);
467
+ });
468
+ }
697
469
  });
698
470
  });
699
471
 
700
472
  describe('websocket', () => {
701
473
  test('does not bind other websocket connection', async () => {
702
- const client = new WebSocket(`ws://localhost:${app.port}/ws`);
703
-
474
+ const ctx = defaultFactory();
475
+ const client = new WebSocket(`ws://localhost:${ctx.port}/ws`);
704
476
  await new Promise<void>((resolve, reject) => {
705
477
  client.onopen = () => {
706
478
  client.send('hello');
@@ -708,113 +480,47 @@ describe('websocket', () => {
708
480
  };
709
481
  client.onerror = reject;
710
482
  });
711
-
712
483
  const promise = new Promise<string>((resolve) => {
713
484
  client.onmessage = (msg) => {
714
485
  return resolve(msg.data);
715
486
  };
716
487
  });
717
-
718
488
  const message = await promise;
719
-
720
489
  expect(message.toString()).toBe('hello');
721
-
722
490
  client.close();
723
491
  });
724
492
 
725
- test('basic functionality', async () => {
726
- const { client } = app.getClientWs();
493
+ test('properly passes context', async () => {
494
+ const contextDone = vi.fn();
495
+ const ctx = defaultFactory({
496
+ createContext: async ({ req, res, info, client }) => {
497
+ expect(client != null);
498
+ expect(res != null);
499
+ expect(client).equals(res);
727
500
 
728
- app.ee.once('subscription:created', () => {
729
- setTimeout(() => {
730
- app.ee.emit('server:msg', {
731
- id: '1',
732
- });
733
- app.ee.emit('server:msg', {
734
- id: '2',
735
- });
736
- });
737
- });
738
-
739
- const onStartedMock = vi.fn();
740
- const onDataMock = vi.fn();
741
- const sub = client.onMessage.subscribe('onMessage', {
742
- onStarted: onStartedMock,
743
- onData(data) {
744
- expectTypeOf(data).not.toBeAny();
745
- expectTypeOf(data).toMatchTypeOf<Message>();
746
- onDataMock(data);
747
- },
748
- });
501
+ // can still get the removeAddress from res
502
+ // hopefully this stays constant in ci?
503
+ const ip = new TextDecoder().decode(res.getRemoteAddressAsText());
504
+ expect(ip).equals('127.0.0.1');
749
505
 
750
- await vi.waitFor(() => {
751
- expect(onStartedMock).toHaveBeenCalledTimes(1);
752
- expect(onDataMock).toHaveBeenCalledTimes(2);
753
- });
506
+ // but other res-like features are not possible
507
+ expect('writeHeader' in res).equals(false);
754
508
 
755
- app.ee.emit('server:msg', {
756
- id: '3',
757
- });
509
+ contextDone();
758
510
 
759
- await vi.waitFor(() => {
760
- expect(onDataMock).toHaveBeenCalledTimes(3);
761
- });
762
-
763
- expect(onDataMock.mock.calls).toMatchInlineSnapshot(`
764
- Array [
765
- Array [
766
- Object {
767
- "id": "1",
768
- },
769
- ],
770
- Array [
771
- Object {
772
- "id": "2",
773
- },
774
- ],
775
- Array [
776
- Object {
777
- "id": "3",
778
- },
779
- ],
780
- ]
781
- `);
782
-
783
- sub.unsubscribe();
784
-
785
- await vi.waitFor(() => {
786
- expect(app.ee.listenerCount('server:msg')).toBe(0);
787
- expect(app.ee.listenerCount('server:error')).toBe(0);
788
- });
789
- });
790
-
791
- test('handles throwing procedure', async () => {
792
- const { client } = app.getClientWs();
793
-
794
- let error: any = null;
795
-
796
- client.count.subscribe(
797
- {
798
- from: 0,
799
- count: 10,
800
- throw: 5,
511
+ return { req, res, info, client };
801
512
  },
802
- {
803
- onError(err) {
804
- error = err;
805
- },
806
- }
807
- );
808
-
809
- await vi.waitFor(() => {
810
- expect(error.message).toEqual(new TRPCClientError('5').message);
811
513
  });
514
+ const client = ctx.clientWs().client;
515
+ await client.echo.query('testing');
516
+
517
+ expect(contextDone).toHaveBeenCalledOnce();
812
518
  });
813
519
 
814
520
  test('handles throwing context with connection params', async () => {
521
+ const server = defaultFactory();
815
522
  let closeCode: number | undefined = undefined;
816
-
817
- const { client } = app.getClientWs({
523
+ const { client } = server.clientWs({
818
524
  wsClientOptions: {
819
525
  connectionParams: {
820
526
  throw: 'expected_context_error',
@@ -828,28 +534,20 @@ describe('websocket', () => {
828
534
  },
829
535
  });
830
536
 
831
- try {
832
- await client.echo.query('hi');
833
- } catch (err) {
834
- //
835
- }
836
- // client.count.subscribe(
837
- // {
838
- // from: 0,
839
- // count: 10,
840
- // },
841
- // {}
842
- // );
537
+ await expect(
538
+ client.echo.query('hi')
539
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
540
+ `[TRPCClientError: Unknown error]`
541
+ );
843
542
 
844
543
  await vi.waitFor(() => {
845
544
  expect(closeCode).toEqual(1008);
846
545
  });
847
546
  });
848
-
849
547
  test('handles throwing context without connection params', async () => {
548
+ const server = defaultFactory();
850
549
  let closeCode: number | undefined = undefined;
851
-
852
- const { client } = app.getClientWs({
550
+ const { client } = server.clientWs({
853
551
  queryParams: {
854
552
  throw: 'expected_context_error',
855
553
  },
@@ -860,12 +558,10 @@ describe('websocket', () => {
860
558
  },
861
559
  });
862
560
 
863
- client.count.subscribe(
864
- {
865
- from: 0,
866
- count: 10,
867
- },
868
- {}
561
+ await expect(
562
+ client.echo.query('hi')
563
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
564
+ `[TRPCClientError: Unknown error]`
869
565
  );
870
566
 
871
567
  await vi.waitFor(() => {
@@ -873,78 +569,31 @@ describe('websocket', () => {
873
569
  });
874
570
  });
875
571
 
876
- // for some reason client.publish does not work, even though it should
877
- test('uWebsockets pubsub', { skip: true }, async () => {
878
- const clientWs = new WebSocket(`ws://localhost:${app.port}/pubsub`);
879
-
880
- const messages: string[] = [];
881
- let connected = false;
882
- clientWs.onmessage = (ev) => {
883
- console.log('triggered onmessage', ev.data);
884
- messages.push(JSON.parse(ev.data));
885
- };
886
- clientWs.onerror = () => {
887
- throw new Error('client had an error');
888
- };
889
-
890
- clientWs.onopen = () => {
891
- connected = true;
892
- };
893
-
894
- await vi.waitFor(() => {
895
- expect(connected).toBe(true);
896
- });
897
-
898
- expect(app.server.numSubscribers('topic')).toBe(1);
899
-
900
- const { client } = app.getClientWs();
901
- const sub = client.pubSub.subscribe(null, {
902
- onData(data) {
903
- console.log('trpc onMessage', data);
904
-
905
- expectTypeOf(data).not.toBeAny();
906
- expectTypeOf(data).toMatchTypeOf<Message>();
907
- messages.push(data.id);
908
- },
909
- });
910
-
911
- await vi.waitFor(() => {
912
- expect(messages).toEqual(['created', 'message', 'ended']);
913
- });
914
-
915
- sub.unsubscribe();
916
- clientWs.close();
917
- });
918
-
919
572
  test('keep alive', async () => {
573
+ const ctx = defaultFactory();
920
574
  let closeCode: number | undefined = undefined;
921
-
922
- const { client, wsClient } = app.getClientWs({
575
+ const { client, wsClient } = ctx.clientWs({
923
576
  wsClientOptions: {
924
577
  onClose(cause) {
925
578
  closeCode = cause?.code;
926
579
  },
927
580
  keepAlive: {
928
581
  enabled: true,
929
- intervalMs: 200,
930
- pongTimeoutMs: 400,
582
+ intervalMs: 100,
583
+ pongTimeoutMs: 200,
931
584
  },
932
585
  },
933
586
  });
934
-
935
587
  const { unsubscribe } = client.waitForMs.subscribe(2_000, {});
936
-
937
588
  await vi.waitFor(() => {
938
589
  expect(wsClient.connection!.state).toBe('open');
939
590
  });
940
-
941
591
  let pongCount = 0;
942
592
  wsClient.connection!.ws!.addEventListener('message', (ev) => {
943
593
  if (ev.data === 'PONG') {
944
594
  pongCount++;
945
595
  }
946
596
  });
947
-
948
597
  await vi.waitFor(
949
598
  () => {
950
599
  expect(pongCount).toEqual(4);
@@ -954,13 +603,44 @@ describe('websocket', () => {
954
603
  timeout: 2_000,
955
604
  }
956
605
  );
957
-
958
606
  unsubscribe();
959
-
960
607
  wsClient.close();
961
-
962
608
  await vi.waitFor(() => {
963
609
  expect(closeCode).toEqual(1005);
964
610
  });
965
611
  });
612
+ // // for some reason client.publish does not work, even though it should
613
+ // test('uWebsockets pubsub', { skip: true }, async () => {
614
+ // const clientWs = new WebSocket(`ws://localhost:${app.port}/pubsub`);
615
+ // const messages: string[] = [];
616
+ // let connected = false;
617
+ // clientWs.onmessage = (ev) => {
618
+ // console.log('triggered onmessage', ev.data);
619
+ // messages.push(JSON.parse(ev.data));
620
+ // };
621
+ // clientWs.onerror = () => {
622
+ // throw new Error('client had an error');
623
+ // };
624
+ // clientWs.onopen = () => {
625
+ // connected = true;
626
+ // };
627
+ // await vi.waitFor(() => {
628
+ // expect(connected).toBe(true);
629
+ // });
630
+ // expect(app.server.numSubscribers('topic')).toBe(1);
631
+ // const { client } = app.clientWs();
632
+ // const sub = client.pubSub.subscribe(null, {
633
+ // onData(data) {
634
+ // console.log('trpc onMessage', data);
635
+ // expectTypeOf(data).not.toBeAny();
636
+ // expectTypeOf(data).toMatchTypeOf<Message>();
637
+ // messages.push(data.id);
638
+ // },
639
+ // });
640
+ // await vi.waitFor(() => {
641
+ // expect(messages).toEqual(['created', 'message', 'ended']);
642
+ // });
643
+ // sub.unsubscribe();
644
+ // clientWs.close();
645
+ // });
966
646
  });