@replit/river 0.8.0 → 0.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.
Files changed (57) hide show
  1. package/README.md +2 -0
  2. package/dist/__tests__/bandwidth.bench.js +1 -1
  3. package/dist/__tests__/e2e.test.js +119 -5
  4. package/dist/__tests__/fixtures/cleanup.d.ts +12 -0
  5. package/dist/__tests__/fixtures/cleanup.d.ts.map +1 -0
  6. package/dist/__tests__/fixtures/cleanup.js +39 -0
  7. package/dist/__tests__/fixtures/services.d.ts +72 -0
  8. package/dist/__tests__/fixtures/services.d.ts.map +1 -1
  9. package/dist/__tests__/fixtures/services.js +55 -0
  10. package/dist/__tests__/handler.test.js +35 -2
  11. package/dist/__tests__/invariants.test.d.ts +2 -0
  12. package/dist/__tests__/invariants.test.d.ts.map +1 -0
  13. package/dist/__tests__/invariants.test.js +136 -0
  14. package/dist/__tests__/serialize.test.js +40 -0
  15. package/dist/__tests__/typescript-stress.test.d.ts +392 -196
  16. package/dist/__tests__/typescript-stress.test.d.ts.map +1 -1
  17. package/dist/__tests__/typescript-stress.test.js +13 -3
  18. package/dist/router/builder.d.ts +49 -11
  19. package/dist/router/builder.d.ts.map +1 -1
  20. package/dist/router/builder.js +8 -2
  21. package/dist/router/client.d.ts +18 -2
  22. package/dist/router/client.d.ts.map +1 -1
  23. package/dist/router/client.js +52 -11
  24. package/dist/router/index.d.ts +1 -1
  25. package/dist/router/index.d.ts.map +1 -1
  26. package/dist/router/server.d.ts +15 -1
  27. package/dist/router/server.d.ts.map +1 -1
  28. package/dist/router/server.js +106 -44
  29. package/dist/transport/events.d.ts +19 -0
  30. package/dist/transport/events.d.ts.map +1 -0
  31. package/dist/transport/events.js +26 -0
  32. package/dist/transport/impls/stdio/stdio.d.ts +0 -4
  33. package/dist/transport/impls/stdio/stdio.d.ts.map +1 -1
  34. package/dist/transport/impls/stdio/stdio.js +0 -5
  35. package/dist/transport/impls/stdio/stdio.test.js +5 -0
  36. package/dist/transport/impls/ws/client.d.ts.map +1 -1
  37. package/dist/transport/impls/ws/client.js +2 -2
  38. package/dist/transport/impls/ws/connection.d.ts.map +1 -1
  39. package/dist/transport/impls/ws/connection.js +2 -1
  40. package/dist/transport/impls/ws/server.d.ts +0 -2
  41. package/dist/transport/impls/ws/server.d.ts.map +1 -1
  42. package/dist/transport/impls/ws/server.js +4 -9
  43. package/dist/transport/impls/ws/ws.test.js +30 -10
  44. package/dist/transport/index.d.ts +3 -3
  45. package/dist/transport/index.d.ts.map +1 -1
  46. package/dist/transport/index.js +3 -3
  47. package/dist/transport/message.d.ts +10 -0
  48. package/dist/transport/message.d.ts.map +1 -1
  49. package/dist/transport/message.js +15 -0
  50. package/dist/transport/transport.d.ts +48 -19
  51. package/dist/transport/transport.d.ts.map +1 -1
  52. package/dist/transport/transport.js +60 -27
  53. package/dist/util/testHelpers.d.ts +58 -7
  54. package/dist/util/testHelpers.d.ts.map +1 -1
  55. package/dist/util/testHelpers.js +133 -3
  56. package/package.json +12 -13
  57. /package/dist/__tests__/{largePayload.json → fixtures/largePayload.json} +0 -0
package/README.md CHANGED
@@ -8,6 +8,8 @@ It's like tRPC but...
8
8
  - with Result types and error handling
9
9
  - over WebSockets
10
10
 
11
+ To use River, you must be on least Typescript 5 with `"moduleResolution": "bundler"`.
12
+
11
13
  ## Developing
12
14
 
13
15
  [![Run on Repl.it](https://replit.com/badge/github/replit/river)](https://replit.com/new/github/replit/river)
@@ -1,7 +1,7 @@
1
1
  import http from 'http';
2
2
  import { assert, bench, describe } from 'vitest';
3
3
  import { createWebSocketServer, createWsTransports, onServerReady, } from '../util/testHelpers';
4
- import largePayload from './largePayload.json';
4
+ import largePayload from './fixtures/largePayload.json';
5
5
  import { TestServiceConstructor } from './fixtures/services';
6
6
  import { createServer } from '../router/server';
7
7
  import { createClient } from '../router/client';
@@ -3,20 +3,19 @@ import { createLocalWebSocketClient, createWebSocketServer, createWsTransports,
3
3
  import { createServer } from '../router/server';
4
4
  import { createClient } from '../router/client';
5
5
  import http from 'http';
6
- import { BinaryFileServiceConstructor, DIV_BY_ZERO, FallibleServiceConstructor, OrderingServiceConstructor, STREAM_ERROR, SubscribableServiceConstructor, TestServiceConstructor, } from './fixtures/services';
6
+ import { BinaryFileServiceConstructor, DIV_BY_ZERO, FallibleServiceConstructor, OrderingServiceConstructor, STREAM_ERROR, SubscribableServiceConstructor, UploadableServiceConstructor, TestServiceConstructor, } from './fixtures/services';
7
7
  import { UNCAUGHT_ERROR } from '../router/result';
8
8
  import { codecs } from '../codec/codec.test';
9
9
  import { WebSocketClientTransport } from '../transport/impls/ws/client';
10
10
  import { WebSocketServerTransport } from '../transport/impls/ws/server';
11
+ import { testFinishesCleanly } from './fixtures/cleanup';
11
12
  describe.each(codecs)('client <-> server integration test ($name codec)', async ({ codec }) => {
12
13
  const httpServer = http.createServer();
13
14
  const port = await onServerReady(httpServer);
14
15
  const webSocketServer = await createWebSocketServer(httpServer);
15
16
  const getTransports = () => createWsTransports(port, webSocketServer, codec);
16
17
  afterAll(() => {
17
- webSocketServer.clients.forEach((socket) => {
18
- socket.close();
19
- });
18
+ webSocketServer.close();
20
19
  httpServer.close();
21
20
  });
22
21
  test('rpc', async () => {
@@ -27,6 +26,11 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
27
26
  const result = await client.test.add.rpc({ n: 3 });
28
27
  assert(result.ok);
29
28
  expect(result.payload).toStrictEqual({ result: 3 });
29
+ await testFinishesCleanly({
30
+ clientTransports: [clientTransport],
31
+ serverTransport,
32
+ server,
33
+ });
30
34
  });
31
35
  test('fallible rpc', async () => {
32
36
  const [clientTransport, serverTransport] = getTransports();
@@ -45,6 +49,11 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
45
49
  test: 'abc',
46
50
  },
47
51
  });
52
+ await testFinishesCleanly({
53
+ clientTransports: [clientTransport],
54
+ serverTransport,
55
+ server,
56
+ });
48
57
  });
49
58
  test('rpc with binary (uint8array)', async () => {
50
59
  const [clientTransport, serverTransport] = getTransports();
@@ -55,6 +64,11 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
55
64
  assert(result.ok);
56
65
  assert(result.payload.contents instanceof Uint8Array);
57
66
  expect(new TextDecoder().decode(result.payload.contents)).toStrictEqual('contents for file test.py');
67
+ await testFinishesCleanly({
68
+ clientTransports: [clientTransport],
69
+ serverTransport,
70
+ server,
71
+ });
58
72
  });
59
73
  test('stream', async () => {
60
74
  const [clientTransport, serverTransport] = getTransports();
@@ -65,6 +79,7 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
65
79
  input.push({ msg: 'abc', ignore: false });
66
80
  input.push({ msg: 'def', ignore: true });
67
81
  input.push({ msg: 'ghi', ignore: false });
82
+ input.push({ msg: 'end', ignore: false, end: true });
68
83
  input.end();
69
84
  const result1 = await iterNext(output);
70
85
  assert(result1.ok);
@@ -72,7 +87,43 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
72
87
  const result2 = await iterNext(output);
73
88
  assert(result2.ok);
74
89
  expect(result2.payload).toStrictEqual({ response: 'ghi' });
90
+ const result3 = await iterNext(output);
91
+ assert(result3.ok);
92
+ expect(result3.payload).toStrictEqual({ response: 'end' });
93
+ // after the server stream is ended, the client stream should be ended too
94
+ const result4 = await output.next();
95
+ assert(result4.done);
75
96
  close();
97
+ await testFinishesCleanly({
98
+ clientTransports: [clientTransport],
99
+ serverTransport,
100
+ server,
101
+ });
102
+ });
103
+ test('stream with init message', async () => {
104
+ const [clientTransport, serverTransport] = getTransports();
105
+ const serviceDefs = { test: TestServiceConstructor() };
106
+ const server = await createServer(serverTransport, serviceDefs);
107
+ const client = createClient(clientTransport);
108
+ const [input, output, close] = await client.test.echoWithPrefix.stream({
109
+ prefix: 'test',
110
+ });
111
+ input.push({ msg: 'abc', ignore: false });
112
+ input.push({ msg: 'def', ignore: true });
113
+ input.push({ msg: 'ghi', ignore: false });
114
+ input.end();
115
+ const result1 = await iterNext(output);
116
+ assert(result1.ok);
117
+ expect(result1.payload).toStrictEqual({ response: 'test abc' });
118
+ const result2 = await iterNext(output);
119
+ assert(result2.ok);
120
+ expect(result2.payload).toStrictEqual({ response: 'test ghi' });
121
+ close();
122
+ await testFinishesCleanly({
123
+ clientTransports: [clientTransport],
124
+ serverTransport,
125
+ server,
126
+ });
76
127
  });
77
128
  test('fallible stream', async () => {
78
129
  const [clientTransport, serverTransport] = getTransports();
@@ -96,6 +147,11 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
96
147
  message: 'some message',
97
148
  });
98
149
  close();
150
+ await testFinishesCleanly({
151
+ clientTransports: [clientTransport],
152
+ serverTransport,
153
+ server,
154
+ });
99
155
  });
100
156
  test('subscription', async () => {
101
157
  const options = { codec };
@@ -132,6 +188,49 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
132
188
  expect(result.payload).toStrictEqual({ result: 4 });
133
189
  close1();
134
190
  close2();
191
+ await testFinishesCleanly({
192
+ clientTransports: [client1Transport, client2Transport],
193
+ serverTransport,
194
+ server,
195
+ });
196
+ });
197
+ test('upload', async () => {
198
+ const [clientTransport, serverTransport] = getTransports();
199
+ const serviceDefs = { uploadable: UploadableServiceConstructor() };
200
+ const server = await createServer(serverTransport, serviceDefs);
201
+ const client = createClient(clientTransport);
202
+ const [addStream, addResult] = await client.uploadable.addMultiple.upload();
203
+ addStream.push({ n: 1 });
204
+ addStream.push({ n: 2 });
205
+ addStream.end();
206
+ const result = await addResult;
207
+ assert(result.ok);
208
+ expect(result.payload).toStrictEqual({ result: 3 });
209
+ await testFinishesCleanly({
210
+ clientTransports: [clientTransport],
211
+ serverTransport,
212
+ server,
213
+ });
214
+ });
215
+ test('upload with init message', async () => {
216
+ const [clientTransport, serverTransport] = getTransports();
217
+ const serviceDefs = { uploadable: UploadableServiceConstructor() };
218
+ const server = await createServer(serverTransport, serviceDefs);
219
+ const client = createClient(clientTransport);
220
+ const [addStream, addResult] = await client.uploadable.addMultipleWithPrefix.upload({
221
+ prefix: 'test',
222
+ });
223
+ addStream.push({ n: 1 });
224
+ addStream.push({ n: 2 });
225
+ addStream.end();
226
+ const result = await addResult;
227
+ assert(result.ok);
228
+ expect(result.payload).toStrictEqual({ result: 'test 3' });
229
+ await testFinishesCleanly({
230
+ clientTransports: [clientTransport],
231
+ serverTransport,
232
+ server,
233
+ });
135
234
  });
136
235
  test('message order is preserved in the face of disconnects', async () => {
137
236
  const [clientTransport, serverTransport] = getTransports();
@@ -153,7 +252,12 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
153
252
  }
154
253
  const res = await client.test.getAll.rpc({});
155
254
  assert(res.ok);
156
- return expect(res.payload.msgs).toStrictEqual(expected);
255
+ expect(res.payload.msgs).toStrictEqual(expected);
256
+ await testFinishesCleanly({
257
+ clientTransports: [clientTransport],
258
+ serverTransport,
259
+ server,
260
+ });
157
261
  });
158
262
  const CONCURRENCY = 10;
159
263
  test('concurrent rpcs', async () => {
@@ -170,6 +274,11 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
170
274
  assert(result.ok);
171
275
  expect(result.payload).toStrictEqual({ n: i });
172
276
  }
277
+ await testFinishesCleanly({
278
+ clientTransports: [clientTransport],
279
+ serverTransport,
280
+ server,
281
+ });
173
282
  });
174
283
  test('concurrent streams', async () => {
175
284
  const [clientTransport, serverTransport] = getTransports();
@@ -198,5 +307,10 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
198
307
  const [_input, _output, close] = openStreams[i];
199
308
  close();
200
309
  }
310
+ await testFinishesCleanly({
311
+ clientTransports: [clientTransport],
312
+ serverTransport,
313
+ server,
314
+ });
201
315
  });
202
316
  });
@@ -0,0 +1,12 @@
1
+ import { Connection, Transport } from '../../transport';
2
+ import { Server } from '../../router';
3
+ export declare function ensureTransportIsClean(t: Transport<Connection>): Promise<void>;
4
+ export declare function waitUntil<T>(valueGetter: () => T, expected: T, message?: string): Promise<boolean>;
5
+ export declare function ensureTransportQueuesAreEventuallyEmpty(t: Transport<Connection>): Promise<void>;
6
+ export declare function ensureServerIsClean(s: Server<unknown>): Promise<boolean>;
7
+ export declare function testFinishesCleanly({ clientTransports, serverTransport, server, }: Partial<{
8
+ clientTransports: Array<Transport<Connection>>;
9
+ serverTransport: Transport<Connection>;
10
+ server: Server<unknown>;
11
+ }>): Promise<void>;
12
+ //# sourceMappingURL=cleanup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cleanup.d.ts","sourceRoot":"","sources":["../../../__tests__/fixtures/cleanup.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAOtC,wBAAsB,sBAAsB,CAAC,CAAC,EAAE,SAAS,CAAC,UAAU,CAAC,iBAapE;AAED,wBAAsB,SAAS,CAAC,CAAC,EAC/B,WAAW,EAAE,MAAM,CAAC,EACpB,QAAQ,EAAE,CAAC,EACX,OAAO,CAAC,EAAE,MAAM,oBAOjB;AAED,wBAAsB,uCAAuC,CAC3D,CAAC,EAAE,SAAS,CAAC,UAAU,CAAC,iBAazB;AAED,wBAAsB,mBAAmB,CAAC,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,oBAM3D;AAED,wBAAsB,mBAAmB,CAAC,EACxC,gBAAgB,EAChB,eAAe,EACf,MAAM,GACP,EAAE,OAAO,CAAC;IACT,gBAAgB,EAAE,KAAK,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC,CAAC;IAC/C,eAAe,EAAE,SAAS,CAAC,UAAU,CAAC,CAAC;IACvC,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;CACzB,CAAC,iBAgBD"}
@@ -0,0 +1,39 @@
1
+ import { expect, vi } from 'vitest';
2
+ const waitUntilOptions = {
3
+ timeout: 250,
4
+ interval: 5, // check every 5ms
5
+ };
6
+ export async function ensureTransportIsClean(t) {
7
+ expect(t.state, `transport ${t.clientId} should be closed after the test`).to.not.equal('open');
8
+ expect(t.connections, `transport ${t.clientId} should not have open connections after the test`).toStrictEqual(new Map());
9
+ expect(t.eventDispatcher.numberOfListeners('message'), `transport ${t.clientId} should not have open message handlers after the test`).equal(0);
10
+ }
11
+ export async function waitUntil(valueGetter, expected, message) {
12
+ return vi
13
+ .waitUntil(() => valueGetter() === expected, waitUntilOptions)
14
+ .finally(() => {
15
+ expect(valueGetter(), message).toEqual(expected);
16
+ });
17
+ }
18
+ export async function ensureTransportQueuesAreEventuallyEmpty(t) {
19
+ await waitUntil(() => t.sendQueue.size, 0, `transport ${t.clientId} should not have any messages waiting to send after the test`);
20
+ await waitUntil(() => t.sendBuffer.size, 0, `transport ${t.clientId} should not have any un-acked messages after the test`);
21
+ }
22
+ export async function ensureServerIsClean(s) {
23
+ return waitUntil(() => s.streams.size, 0, `server should not have any open streams after the test`);
24
+ }
25
+ export async function testFinishesCleanly({ clientTransports, serverTransport, server, }) {
26
+ if (clientTransports) {
27
+ await Promise.all(clientTransports.map((t) => t.close()));
28
+ await Promise.all(clientTransports.map(ensureTransportIsClean));
29
+ }
30
+ // server sits on top of server transport so we clean it up first
31
+ if (server) {
32
+ await ensureServerIsClean(server);
33
+ await server.close();
34
+ }
35
+ if (serverTransport) {
36
+ await serverTransport.close();
37
+ await ensureTransportIsClean(serverTransport);
38
+ }
39
+ }
@@ -2,6 +2,7 @@ import { Observable } from './observable';
2
2
  export declare const EchoRequest: import("@sinclair/typebox").TObject<{
3
3
  msg: import("@sinclair/typebox").TString;
4
4
  ignore: import("@sinclair/typebox").TBoolean;
5
+ end: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
5
6
  }>;
6
7
  export declare const EchoResponse: import("@sinclair/typebox").TObject<{
7
8
  response: import("@sinclair/typebox").TString;
@@ -34,6 +35,7 @@ export declare const TestServiceConstructor: () => {
34
35
  input: import("@sinclair/typebox").TObject<{
35
36
  msg: import("@sinclair/typebox").TString;
36
37
  ignore: import("@sinclair/typebox").TBoolean;
38
+ end: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
37
39
  }>;
38
40
  output: import("@sinclair/typebox").TObject<{
39
41
  response: import("@sinclair/typebox").TString;
@@ -42,6 +44,34 @@ export declare const TestServiceConstructor: () => {
42
44
  handler: (context: import("../../router").ServiceContextWithState<{
43
45
  count: number;
44
46
  }>, input: AsyncIterable<import("../../transport/message").TransportMessage<{
47
+ end?: boolean | undefined;
48
+ msg: string;
49
+ ignore: boolean;
50
+ }>>, output: import("it-pushable").Pushable<import("../../transport/message").TransportMessage<import("../../router/result").Result<{
51
+ response: string;
52
+ }, never>>, void, unknown>) => Promise<void>;
53
+ type: "stream";
54
+ };
55
+ } & {
56
+ echoWithPrefix: {
57
+ init: import("@sinclair/typebox").TObject<{
58
+ prefix: import("@sinclair/typebox").TString;
59
+ }>;
60
+ input: import("@sinclair/typebox").TObject<{
61
+ msg: import("@sinclair/typebox").TString;
62
+ ignore: import("@sinclair/typebox").TBoolean;
63
+ end: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
64
+ }>;
65
+ output: import("@sinclair/typebox").TObject<{
66
+ response: import("@sinclair/typebox").TString;
67
+ }>;
68
+ errors: import("@sinclair/typebox").TNever;
69
+ handler: (context: import("../../router").ServiceContextWithState<{
70
+ count: number;
71
+ }>, init: import("../../transport/message").TransportMessage<{
72
+ prefix: string;
73
+ }>, input: AsyncIterable<import("../../transport/message").TransportMessage<{
74
+ end?: boolean | undefined;
45
75
  msg: string;
46
76
  ignore: boolean;
47
77
  }>>, output: import("it-pushable").Pushable<import("../../transport/message").TransportMessage<import("../../router/result").Result<{
@@ -213,4 +243,46 @@ export declare const SubscribableServiceConstructor: () => {
213
243
  };
214
244
  };
215
245
  };
246
+ export declare const UploadableServiceConstructor: () => {
247
+ name: "uploadable";
248
+ state: {};
249
+ procedures: {
250
+ addMultiple: {
251
+ input: import("@sinclair/typebox").TObject<{
252
+ n: import("@sinclair/typebox").TNumber;
253
+ }>;
254
+ output: import("@sinclair/typebox").TObject<{
255
+ result: import("@sinclair/typebox").TNumber;
256
+ }>;
257
+ errors: import("@sinclair/typebox").TNever;
258
+ handler: (context: import("../../router").ServiceContextWithState<{}>, input: AsyncIterable<import("../../transport/message").TransportMessage<{
259
+ n: number;
260
+ }>>) => Promise<import("../../transport/message").TransportMessage<import("../../router/result").Result<{
261
+ result: number;
262
+ }, never>>>;
263
+ type: "upload";
264
+ };
265
+ } & {
266
+ addMultipleWithPrefix: {
267
+ init: import("@sinclair/typebox").TObject<{
268
+ prefix: import("@sinclair/typebox").TString;
269
+ }>;
270
+ input: import("@sinclair/typebox").TObject<{
271
+ n: import("@sinclair/typebox").TNumber;
272
+ }>;
273
+ output: import("@sinclair/typebox").TObject<{
274
+ result: import("@sinclair/typebox").TString;
275
+ }>;
276
+ errors: import("@sinclair/typebox").TNever;
277
+ handler: (context: import("../../router").ServiceContextWithState<{}>, init: import("../../transport/message").TransportMessage<{
278
+ prefix: string;
279
+ }>, input: AsyncIterable<import("../../transport/message").TransportMessage<{
280
+ n: number;
281
+ }>>) => Promise<import("../../transport/message").TransportMessage<import("../../router/result").Result<{
282
+ result: string;
283
+ }, never>>>;
284
+ type: "upload";
285
+ };
286
+ };
287
+ };
216
288
  //# sourceMappingURL=services.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"services.d.ts","sourceRoot":"","sources":["../../../__tests__/fixtures/services.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1C,eAAO,MAAM,WAAW;;;EAGtB,CAAC;AACH,eAAO,MAAM,YAAY;;EAA2C,CAAC;AAErE,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8BpB,CAAC;AAEhB,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyBxB,CAAC;AAEhB,eAAO,MAAM,4BAA4B;;;;;;;;;;;;;;;;;;;;CAc1B,CAAC;AAEhB,eAAO,MAAM,WAAW,gBAAgB,CAAC;AACzC,eAAO,MAAM,YAAY,iBAAiB,CAAC;AAC3C,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiExB,CAAC;AAEhB,eAAO,MAAM,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2B5B,CAAC"}
1
+ {"version":3,"file":"services.d.ts","sourceRoot":"","sources":["../../../__tests__/fixtures/services.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1C,eAAO,MAAM,WAAW;;;;EAItB,CAAC;AACH,eAAO,MAAM,YAAY;;EAA2C,CAAC;AAErE,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmDpB,CAAC;AAEhB,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyBxB,CAAC;AAEhB,eAAO,MAAM,4BAA4B;;;;;;;;;;;;;;;;;;;;CAc1B,CAAC;AAEhB,eAAO,MAAM,WAAW,gBAAgB,CAAC;AACzC,eAAO,MAAM,YAAY,iBAAiB,CAAC;AAC3C,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiExB,CAAC;AAEhB,eAAO,MAAM,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2B5B,CAAC;AAEhB,eAAO,MAAM,4BAA4B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuC1B,CAAC"}
@@ -6,6 +6,7 @@ import { Observable } from './observable';
6
6
  export const EchoRequest = Type.Object({
7
7
  msg: Type.String(),
8
8
  ignore: Type.Boolean(),
9
+ end: Type.Optional(Type.Boolean()),
9
10
  });
10
11
  export const EchoResponse = Type.Object({ response: Type.String() });
11
12
  export const TestServiceConstructor = () => ServiceBuilder.create('test')
@@ -34,6 +35,24 @@ export const TestServiceConstructor = () => ServiceBuilder.create('test')
34
35
  if (!req.ignore) {
35
36
  returnStream.push(reply(msg, Ok({ response: req.msg })));
36
37
  }
38
+ if (req.end) {
39
+ returnStream.end();
40
+ }
41
+ }
42
+ },
43
+ })
44
+ .defineProcedure('echoWithPrefix', {
45
+ type: 'stream',
46
+ init: Type.Object({ prefix: Type.String() }),
47
+ input: EchoRequest,
48
+ output: EchoResponse,
49
+ errors: Type.Never(),
50
+ async handler(_ctx, init, msgStream, returnStream) {
51
+ for await (const msg of msgStream) {
52
+ const req = msg.payload;
53
+ if (!req.ignore) {
54
+ returnStream.push(reply(msg, Ok({ response: `${init.payload.prefix} ${req.msg}` })));
55
+ }
37
56
  }
38
57
  },
39
58
  })
@@ -167,3 +186,39 @@ export const SubscribableServiceConstructor = () => ServiceBuilder.create('subsc
167
186
  },
168
187
  })
169
188
  .finalize();
189
+ export const UploadableServiceConstructor = () => ServiceBuilder.create('uploadable')
190
+ .initialState({})
191
+ .defineProcedure('addMultiple', {
192
+ type: 'upload',
193
+ input: Type.Object({ n: Type.Number() }),
194
+ output: Type.Object({ result: Type.Number() }),
195
+ errors: Type.Never(),
196
+ async handler(_ctx, msgStream) {
197
+ let result = 0;
198
+ let lastMsg;
199
+ for await (const msg of msgStream) {
200
+ const { n } = msg.payload;
201
+ result += n;
202
+ lastMsg = msg;
203
+ }
204
+ return reply(lastMsg, Ok({ result: result }));
205
+ },
206
+ })
207
+ .defineProcedure('addMultipleWithPrefix', {
208
+ type: 'upload',
209
+ init: Type.Object({ prefix: Type.String() }),
210
+ input: Type.Object({ n: Type.Number() }),
211
+ output: Type.Object({ result: Type.String() }),
212
+ errors: Type.Never(),
213
+ async handler(_ctx, init, msgStream) {
214
+ let result = 0;
215
+ let lastMsg;
216
+ for await (const msg of msgStream) {
217
+ const { n } = msg.payload;
218
+ result += n;
219
+ lastMsg = msg;
220
+ }
221
+ return reply(lastMsg, Ok({ result: init.payload.prefix + ' ' + result }));
222
+ },
223
+ })
224
+ .finalize();
@@ -1,6 +1,6 @@
1
- import { asClientRpc, asClientStream, asClientSubscription, iterNext, } from '../util/testHelpers';
1
+ import { asClientRpc, asClientStream, asClientStreamWithInitialization, asClientSubscription, asClientUpload, asClientUploadWithInitialization, iterNext, } from '../util/testHelpers';
2
2
  import { assert, describe, expect, test } from 'vitest';
3
- import { DIV_BY_ZERO, FallibleServiceConstructor, STREAM_ERROR, SubscribableServiceConstructor, TestServiceConstructor, } from './fixtures/services';
3
+ import { DIV_BY_ZERO, FallibleServiceConstructor, STREAM_ERROR, SubscribableServiceConstructor, UploadableServiceConstructor, TestServiceConstructor, } from './fixtures/services';
4
4
  import { UNCAUGHT_ERROR } from '../router/result';
5
5
  import { Observable } from './fixtures/observable';
6
6
  describe('server-side test', () => {
@@ -48,6 +48,20 @@ describe('server-side test', () => {
48
48
  expect(result2.payload).toStrictEqual({ response: 'ghi' });
49
49
  expect(output.readableLength).toBe(0);
50
50
  });
51
+ test('stream with initialization', async () => {
52
+ const [input, output] = asClientStreamWithInitialization(initialState, service.procedures.echoWithPrefix, { prefix: 'test' });
53
+ input.push({ msg: 'abc', ignore: false });
54
+ input.push({ msg: 'def', ignore: true });
55
+ input.push({ msg: 'ghi', ignore: false });
56
+ input.end();
57
+ const result1 = await iterNext(output);
58
+ assert(result1 && result1.ok);
59
+ expect(result1.payload).toStrictEqual({ response: 'test abc' });
60
+ const result2 = await iterNext(output);
61
+ assert(result2 && result2.ok);
62
+ expect(result2.payload).toStrictEqual({ response: 'test ghi' });
63
+ expect(output.readableLength).toBe(0);
64
+ });
51
65
  test('fallible stream', async () => {
52
66
  const service = FallibleServiceConstructor();
53
67
  const [input, output] = asClientStream({}, service.procedures.echo);
@@ -85,4 +99,23 @@ describe('server-side test', () => {
85
99
  assert(streamResult2 && streamResult1.ok);
86
100
  expect(streamResult2.payload).toStrictEqual({ result: 3 });
87
101
  });
102
+ test('uploads', async () => {
103
+ const service = UploadableServiceConstructor();
104
+ const [input, result] = asClientUpload({}, service.procedures.addMultiple);
105
+ input.push({ n: 1 });
106
+ input.push({ n: 2 });
107
+ input.end();
108
+ expect(await result).toStrictEqual({ ok: true, payload: { result: 3 } });
109
+ });
110
+ test('uploads with initialization', async () => {
111
+ const service = UploadableServiceConstructor();
112
+ const [input, result] = asClientUploadWithInitialization({}, service.procedures.addMultipleWithPrefix, { prefix: 'test' });
113
+ input.push({ n: 1 });
114
+ input.push({ n: 2 });
115
+ input.end();
116
+ expect(await result).toStrictEqual({
117
+ ok: true,
118
+ payload: { result: 'test 3' },
119
+ });
120
+ });
88
121
  });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=invariants.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invariants.test.d.ts","sourceRoot":"","sources":["../../__tests__/invariants.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,136 @@
1
+ import { afterAll, assert, describe, expect, test } from 'vitest';
2
+ import http from 'http';
3
+ import { createWebSocketServer, createWsTransports, iterNext, onServerReady, } from '../util/testHelpers';
4
+ import { SubscribableServiceConstructor, TestServiceConstructor, } from './fixtures/services';
5
+ import { createClient, createServer } from '../router';
6
+ import { ensureServerIsClean, ensureTransportQueuesAreEventuallyEmpty, waitUntil, } from './fixtures/cleanup';
7
+ describe('procedures should leave no trace after finishing', async () => {
8
+ const httpServer = http.createServer();
9
+ const port = await onServerReady(httpServer);
10
+ const webSocketServer = await createWebSocketServer(httpServer);
11
+ const getTransports = () => createWsTransports(port, webSocketServer);
12
+ afterAll(() => {
13
+ webSocketServer.close();
14
+ httpServer.close();
15
+ });
16
+ test('closing a transport from the client cleans up connection on the server', async () => {
17
+ const [clientTransport, serverTransport] = getTransports();
18
+ const serviceDefs = { test: TestServiceConstructor() };
19
+ const server = await createServer(serverTransport, serviceDefs);
20
+ const client = createClient(clientTransport);
21
+ expect(clientTransport.connections.size).toEqual(0);
22
+ expect(serverTransport.connections.size).toEqual(0);
23
+ // start procedure
24
+ await client.test.add.rpc({ n: 3 });
25
+ // end procedure
26
+ expect(clientTransport.connections.size).toEqual(1);
27
+ expect(serverTransport.connections.size).toEqual(1);
28
+ // should be back to 0 connections after client closes
29
+ clientTransport.close();
30
+ expect(clientTransport.connections.size).toEqual(0);
31
+ await waitUntil(() => serverTransport.connections.size, 0, 'server should cleanup connection after client closes');
32
+ });
33
+ test('closing a transport from the server cleans up connection on the client', async () => {
34
+ const [clientTransport, serverTransport] = getTransports();
35
+ const serviceDefs = { test: TestServiceConstructor() };
36
+ const server = await createServer(serverTransport, serviceDefs);
37
+ const client = createClient(clientTransport);
38
+ expect(clientTransport.connections.size).toEqual(0);
39
+ expect(serverTransport.connections.size).toEqual(0);
40
+ // start procedure
41
+ await client.test.add.rpc({ n: 3 });
42
+ // end procedure
43
+ expect(clientTransport.connections.size).toEqual(1);
44
+ expect(serverTransport.connections.size).toEqual(1);
45
+ // should be back to 0 connections after client closes
46
+ serverTransport.close();
47
+ expect(serverTransport.connections.size).toEqual(0);
48
+ await waitUntil(() => clientTransport.connections.size, 0, 'client should cleanup connection after server closes');
49
+ });
50
+ test('rpc', async () => {
51
+ const [clientTransport, serverTransport] = getTransports();
52
+ const serviceDefs = { test: TestServiceConstructor() };
53
+ const server = await createServer(serverTransport, serviceDefs);
54
+ const client = createClient(clientTransport);
55
+ let serverListeners = serverTransport.eventDispatcher.numberOfListeners('message');
56
+ let clientListeners = clientTransport.eventDispatcher.numberOfListeners('message');
57
+ // start procedure
58
+ await client.test.add.rpc({ n: 3 });
59
+ // end procedure
60
+ // number of message handlers shouldn't increase after rpc
61
+ expect(serverTransport.eventDispatcher.numberOfListeners('message')).toEqual(serverListeners);
62
+ expect(clientTransport.eventDispatcher.numberOfListeners('message')).toEqual(clientListeners);
63
+ // check number of connections
64
+ expect(serverTransport.connections.size).toEqual(1);
65
+ expect(clientTransport.connections.size).toEqual(1);
66
+ await ensureTransportQueuesAreEventuallyEmpty(clientTransport);
67
+ await ensureTransportQueuesAreEventuallyEmpty(serverTransport);
68
+ // ensure we have no streams left on the server
69
+ await ensureServerIsClean(server);
70
+ });
71
+ test('stream', async () => {
72
+ const [clientTransport, serverTransport] = getTransports();
73
+ const serviceDefs = { test: TestServiceConstructor() };
74
+ const server = await createServer(serverTransport, serviceDefs);
75
+ const client = createClient(clientTransport);
76
+ let serverListeners = serverTransport.eventDispatcher.numberOfListeners('message');
77
+ let clientListeners = clientTransport.eventDispatcher.numberOfListeners('message');
78
+ // start procedure
79
+ const [input, output, close] = await client.test.echo.stream();
80
+ input.push({ msg: '1', ignore: false });
81
+ input.push({ msg: '2', ignore: false, end: true });
82
+ input.end();
83
+ const result1 = await iterNext(output);
84
+ assert(result1.ok);
85
+ expect(result1.payload).toStrictEqual({ response: '1' });
86
+ const result2 = await iterNext(output);
87
+ assert(result2.ok);
88
+ expect(result2.payload).toStrictEqual({ response: '2' });
89
+ // ensure we exactly have one stream even after we send multiple messages
90
+ expect(server.streams.size).toEqual(1);
91
+ const result3 = await output.next();
92
+ assert(result3.done);
93
+ close();
94
+ // end procedure
95
+ // number of message handlers shouldn't increase after stream ends
96
+ expect(serverTransport.eventDispatcher.numberOfListeners('message')).toEqual(serverListeners);
97
+ expect(clientTransport.eventDispatcher.numberOfListeners('message')).toEqual(clientListeners);
98
+ // check number of connections
99
+ expect(serverTransport.connections.size).toEqual(1);
100
+ expect(clientTransport.connections.size).toEqual(1);
101
+ await ensureTransportQueuesAreEventuallyEmpty(clientTransport);
102
+ await ensureTransportQueuesAreEventuallyEmpty(serverTransport);
103
+ // ensure we have no streams left on the server
104
+ await ensureServerIsClean(server);
105
+ });
106
+ test('subscription', async () => {
107
+ const [clientTransport, serverTransport] = getTransports();
108
+ const serviceDefs = { test: SubscribableServiceConstructor() };
109
+ const server = await createServer(serverTransport, serviceDefs);
110
+ const client = createClient(clientTransport);
111
+ let serverListeners = serverTransport.eventDispatcher.numberOfListeners('message');
112
+ let clientListeners = clientTransport.eventDispatcher.numberOfListeners('message');
113
+ // start procedure
114
+ const [subscription, close] = await client.test.value.subscribe({});
115
+ let result = await iterNext(subscription);
116
+ assert(result.ok);
117
+ expect(result.payload).toStrictEqual({ result: 0 });
118
+ const add1 = await client.test.add.rpc({ n: 1 });
119
+ assert(add1.ok);
120
+ result = await iterNext(subscription);
121
+ assert(result.ok);
122
+ expect(result.payload).toStrictEqual({ result: 1 });
123
+ close();
124
+ // end procedure
125
+ // number of message handlers shouldn't increase after stream ends
126
+ expect(serverTransport.eventDispatcher.numberOfListeners('message')).toEqual(serverListeners);
127
+ expect(clientTransport.eventDispatcher.numberOfListeners('message')).toEqual(clientListeners);
128
+ // check number of connections
129
+ expect(serverTransport.connections.size).toEqual(1);
130
+ expect(clientTransport.connections.size).toEqual(1);
131
+ await ensureTransportQueuesAreEventuallyEmpty(clientTransport);
132
+ await ensureTransportQueuesAreEventuallyEmpty(serverTransport);
133
+ // ensure we have no streams left on the server
134
+ await ensureServerIsClean(server);
135
+ });
136
+ });