@replit/river 0.7.2 → 0.8.1

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 (56) hide show
  1. package/README.md +2 -0
  2. package/dist/__tests__/bandwidth.bench.js +6 -6
  3. package/dist/__tests__/e2e.test.js +112 -23
  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/observable.d.ts +26 -0
  8. package/dist/__tests__/fixtures/observable.d.ts.map +1 -0
  9. package/dist/__tests__/fixtures/observable.js +38 -0
  10. package/dist/__tests__/fixtures/observable.test.d.ts +2 -0
  11. package/dist/__tests__/fixtures/observable.test.d.ts.map +1 -0
  12. package/dist/__tests__/fixtures/observable.test.js +39 -0
  13. package/dist/__tests__/{fixtures.d.ts → fixtures/services.d.ts} +61 -18
  14. package/dist/__tests__/fixtures/services.d.ts.map +1 -0
  15. package/dist/__tests__/{fixtures.js → fixtures/services.js} +35 -3
  16. package/dist/__tests__/handler.test.js +24 -7
  17. package/dist/__tests__/invariants.test.d.ts +2 -0
  18. package/dist/__tests__/invariants.test.d.ts.map +1 -0
  19. package/dist/__tests__/invariants.test.js +136 -0
  20. package/dist/__tests__/serialize.test.js +2 -1
  21. package/dist/router/builder.d.ts +10 -4
  22. package/dist/router/builder.d.ts.map +1 -1
  23. package/dist/router/client.d.ts +14 -5
  24. package/dist/router/client.d.ts.map +1 -1
  25. package/dist/router/client.js +42 -11
  26. package/dist/router/server.d.ts +14 -0
  27. package/dist/router/server.d.ts.map +1 -1
  28. package/dist/router/server.js +85 -48
  29. package/dist/transport/impls/stdio/stdio.d.ts +0 -4
  30. package/dist/transport/impls/stdio/stdio.d.ts.map +1 -1
  31. package/dist/transport/impls/stdio/stdio.js +0 -5
  32. package/dist/transport/impls/stdio/stdio.test.js +6 -1
  33. package/dist/transport/impls/ws/client.d.ts.map +1 -1
  34. package/dist/transport/impls/ws/client.js +2 -2
  35. package/dist/transport/impls/ws/connection.d.ts.map +1 -1
  36. package/dist/transport/impls/ws/connection.js +2 -1
  37. package/dist/transport/impls/ws/server.d.ts +0 -2
  38. package/dist/transport/impls/ws/server.d.ts.map +1 -1
  39. package/dist/transport/impls/ws/server.js +4 -9
  40. package/dist/transport/impls/ws/ws.test.js +31 -11
  41. package/dist/transport/index.d.ts +3 -3
  42. package/dist/transport/index.d.ts.map +1 -1
  43. package/dist/transport/index.js +1 -1
  44. package/dist/transport/message.d.ts +10 -0
  45. package/dist/transport/message.d.ts.map +1 -1
  46. package/dist/transport/message.js +15 -0
  47. package/dist/transport/transport.d.ts +37 -11
  48. package/dist/transport/transport.d.ts.map +1 -1
  49. package/dist/transport/transport.js +37 -13
  50. package/dist/{testUtils.d.ts → util/testHelpers.d.ts} +27 -8
  51. package/dist/util/testHelpers.d.ts.map +1 -0
  52. package/dist/{testUtils.js → util/testHelpers.js} +51 -5
  53. package/package.json +3 -3
  54. package/dist/__tests__/fixtures.d.ts.map +0 -1
  55. package/dist/testUtils.d.ts.map +0 -1
  56. /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,8 +1,8 @@
1
1
  import http from 'http';
2
2
  import { assert, bench, describe } from 'vitest';
3
- import { createWebSocketServer, createWsTransports, onServerReady, } from '../testUtils';
4
- import largePayload from './largePayload.json';
5
- import { TestServiceConstructor } from './fixtures';
3
+ import { createWebSocketServer, createWsTransports, onServerReady, } from '../util/testHelpers';
4
+ import largePayload from './fixtures/largePayload.json';
5
+ import { TestServiceConstructor } from './fixtures/services';
6
6
  import { createServer } from '../router/server';
7
7
  import { createClient } from '../router/client';
8
8
  import { StupidlyLargeService } from './typescript-stress.test';
@@ -57,10 +57,10 @@ describe('simple router level bandwidth', async () => {
57
57
  const server = await createServer(serverTransport, serviceDefs);
58
58
  const client = createClient(clientTransport);
59
59
  bench('rpc (wait for response)', async () => {
60
- const result = await client.test.add({ n: 1 });
60
+ const result = await client.test.add.rpc({ n: 1 });
61
61
  assert(result.ok);
62
62
  }, { time: BENCH_DURATION });
63
- const [input, output] = await client.test.echo();
63
+ const [input, output] = await client.test.echo.stream();
64
64
  bench('stream (wait for response)', async () => {
65
65
  input.push({ msg: 'abc', ignore: false });
66
66
  const result = await output.next();
@@ -84,7 +84,7 @@ describe('complex (50 procedures) router level bandwidth', async () => {
84
84
  const server = await createServer(serverTransport, serviceDefs);
85
85
  const client = createClient(clientTransport);
86
86
  bench('rpc (wait for response)', async () => {
87
- const result = await client.b.f35({ a: 1 });
87
+ const result = await client.b.f35.rpc({ a: 1 });
88
88
  assert(result.ok);
89
89
  }, { time: BENCH_DURATION });
90
90
  });
@@ -1,20 +1,21 @@
1
1
  import { afterAll, assert, describe, expect, test } from 'vitest';
2
- import { createWebSocketServer, createWsTransports, onServerReady, } from '../testUtils';
2
+ import { createLocalWebSocketClient, createWebSocketServer, createWsTransports, iterNext, onServerReady, } from '../util/testHelpers';
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, TestServiceConstructor, } from './fixtures';
6
+ import { BinaryFileServiceConstructor, DIV_BY_ZERO, FallibleServiceConstructor, OrderingServiceConstructor, STREAM_ERROR, SubscribableServiceConstructor, TestServiceConstructor, } from './fixtures/services';
7
7
  import { UNCAUGHT_ERROR } from '../router/result';
8
8
  import { codecs } from '../codec/codec.test';
9
+ import { WebSocketClientTransport } from '../transport/impls/ws/client';
10
+ import { WebSocketServerTransport } from '../transport/impls/ws/server';
11
+ import { testFinishesCleanly } from './fixtures/cleanup';
9
12
  describe.each(codecs)('client <-> server integration test ($name codec)', async ({ codec }) => {
10
13
  const httpServer = http.createServer();
11
14
  const port = await onServerReady(httpServer);
12
15
  const webSocketServer = await createWebSocketServer(httpServer);
13
16
  const getTransports = () => createWsTransports(port, webSocketServer, codec);
14
17
  afterAll(() => {
15
- webSocketServer.clients.forEach((socket) => {
16
- socket.close();
17
- });
18
+ webSocketServer.close();
18
19
  httpServer.close();
19
20
  });
20
21
  test('rpc', async () => {
@@ -22,19 +23,24 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
22
23
  const serviceDefs = { test: TestServiceConstructor() };
23
24
  const server = await createServer(serverTransport, serviceDefs);
24
25
  const client = createClient(clientTransport);
25
- const result = await client.test.add({ n: 3 });
26
+ const result = await client.test.add.rpc({ n: 3 });
26
27
  assert(result.ok);
27
28
  expect(result.payload).toStrictEqual({ result: 3 });
29
+ await testFinishesCleanly({
30
+ clientTransports: [clientTransport],
31
+ serverTransport,
32
+ server,
33
+ });
28
34
  });
29
35
  test('fallible rpc', async () => {
30
36
  const [clientTransport, serverTransport] = getTransports();
31
37
  const serviceDefs = { test: FallibleServiceConstructor() };
32
38
  const server = await createServer(serverTransport, serviceDefs);
33
39
  const client = createClient(clientTransport);
34
- const result = await client.test.divide({ a: 10, b: 2 });
40
+ const result = await client.test.divide.rpc({ a: 10, b: 2 });
35
41
  assert(result.ok);
36
42
  expect(result.payload).toStrictEqual({ result: 5 });
37
- const result2 = await client.test.divide({ a: 10, b: 0 });
43
+ const result2 = await client.test.divide.rpc({ a: 10, b: 0 });
38
44
  assert(!result2.ok);
39
45
  expect(result2.payload).toStrictEqual({
40
46
  code: DIV_BY_ZERO,
@@ -43,57 +49,125 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
43
49
  test: 'abc',
44
50
  },
45
51
  });
52
+ await testFinishesCleanly({
53
+ clientTransports: [clientTransport],
54
+ serverTransport,
55
+ server,
56
+ });
46
57
  });
47
58
  test('rpc with binary (uint8array)', async () => {
48
59
  const [clientTransport, serverTransport] = getTransports();
49
60
  const serviceDefs = { test: BinaryFileServiceConstructor() };
50
61
  const server = await createServer(serverTransport, serviceDefs);
51
62
  const client = createClient(clientTransport);
52
- const result = await client.test.getFile({ file: 'test.py' });
63
+ const result = await client.test.getFile.rpc({ file: 'test.py' });
53
64
  assert(result.ok);
54
65
  assert(result.payload.contents instanceof Uint8Array);
55
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
+ });
56
72
  });
57
73
  test('stream', async () => {
58
74
  const [clientTransport, serverTransport] = getTransports();
59
75
  const serviceDefs = { test: TestServiceConstructor() };
60
76
  const server = await createServer(serverTransport, serviceDefs);
61
77
  const client = createClient(clientTransport);
62
- const [input, output, close] = await client.test.echo();
78
+ const [input, output, close] = await client.test.echo.stream();
63
79
  input.push({ msg: 'abc', ignore: false });
64
80
  input.push({ msg: 'def', ignore: true });
65
81
  input.push({ msg: 'ghi', ignore: false });
82
+ input.push({ msg: 'end', ignore: false, end: true });
66
83
  input.end();
67
- const result1 = await output.next().then((res) => res.value);
84
+ const result1 = await iterNext(output);
68
85
  assert(result1.ok);
69
86
  expect(result1.payload).toStrictEqual({ response: 'abc' });
70
- const result2 = await output.next().then((res) => res.value);
87
+ const result2 = await iterNext(output);
71
88
  assert(result2.ok);
72
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);
73
96
  close();
97
+ await testFinishesCleanly({
98
+ clientTransports: [clientTransport],
99
+ serverTransport,
100
+ server,
101
+ });
74
102
  });
75
103
  test('fallible stream', async () => {
76
104
  const [clientTransport, serverTransport] = getTransports();
77
105
  const serviceDefs = { test: FallibleServiceConstructor() };
78
106
  const server = await createServer(serverTransport, serviceDefs);
79
107
  const client = createClient(clientTransport);
80
- const [input, output, close] = await client.test.echo();
108
+ const [input, output, close] = await client.test.echo.stream();
81
109
  input.push({ msg: 'abc', throwResult: false, throwError: false });
82
- const result1 = await output.next().then((res) => res.value);
110
+ const result1 = await iterNext(output);
83
111
  assert(result1 && result1.ok);
84
112
  expect(result1.payload).toStrictEqual({ response: 'abc' });
85
113
  input.push({ msg: 'def', throwResult: true, throwError: false });
86
- const result2 = await output.next().then((res) => res.value);
114
+ const result2 = await iterNext(output);
87
115
  assert(result2 && !result2.ok);
88
116
  expect(result2.payload.code).toStrictEqual(STREAM_ERROR);
89
117
  input.push({ msg: 'ghi', throwResult: false, throwError: true });
90
- const result3 = await output.next().then((res) => res.value);
118
+ const result3 = await iterNext(output);
91
119
  assert(result3 && !result3.ok);
92
120
  expect(result3.payload).toStrictEqual({
93
121
  code: UNCAUGHT_ERROR,
94
122
  message: 'some message',
95
123
  });
96
124
  close();
125
+ await testFinishesCleanly({
126
+ clientTransports: [clientTransport],
127
+ serverTransport,
128
+ server,
129
+ });
130
+ });
131
+ test('subscription', async () => {
132
+ const options = { codec };
133
+ const serverTransport = new WebSocketServerTransport(webSocketServer, 'SERVER', options);
134
+ const client1Transport = new WebSocketClientTransport(() => createLocalWebSocketClient(port), 'client1', 'SERVER', options);
135
+ const client2Transport = new WebSocketClientTransport(() => createLocalWebSocketClient(port), 'client2', 'SERVER', options);
136
+ const serviceDefs = { test: SubscribableServiceConstructor() };
137
+ const server = await createServer(serverTransport, serviceDefs);
138
+ const client1 = createClient(client1Transport);
139
+ const client2 = createClient(client2Transport);
140
+ const [subscription1, close1] = await client1.test.value.subscribe({});
141
+ let result = await iterNext(subscription1);
142
+ assert(result.ok);
143
+ expect(result.payload).toStrictEqual({ result: 0 });
144
+ const [subscription2, close2] = await client2.test.value.subscribe({});
145
+ result = await iterNext(subscription2);
146
+ assert(result.ok);
147
+ expect(result.payload).toStrictEqual({ result: 0 });
148
+ const add1 = await client1.test.add.rpc({ n: 1 });
149
+ assert(add1.ok);
150
+ result = await iterNext(subscription1);
151
+ assert(result.ok);
152
+ expect(result.payload).toStrictEqual({ result: 1 });
153
+ result = await iterNext(subscription2);
154
+ assert(result.ok);
155
+ expect(result.payload).toStrictEqual({ result: 1 });
156
+ const add2 = await client2.test.add.rpc({ n: 3 });
157
+ assert(add2.ok);
158
+ result = await iterNext(subscription1);
159
+ assert(result.ok);
160
+ expect(result.payload).toStrictEqual({ result: 4 });
161
+ result = await iterNext(subscription2);
162
+ assert(result.ok);
163
+ expect(result.payload).toStrictEqual({ result: 4 });
164
+ close1();
165
+ close2();
166
+ await testFinishesCleanly({
167
+ clientTransports: [client1Transport, client2Transport],
168
+ serverTransport,
169
+ server,
170
+ });
97
171
  });
98
172
  test('message order is preserved in the face of disconnects', async () => {
99
173
  const [clientTransport, serverTransport] = getTransports();
@@ -109,13 +183,18 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
109
183
  if (i == 42) {
110
184
  clientTransport.connections.forEach((conn) => conn.ws.terminate());
111
185
  }
112
- await client.test.add({
186
+ await client.test.add.rpc({
113
187
  n: i,
114
188
  });
115
189
  }
116
- const res = await client.test.getAll({});
190
+ const res = await client.test.getAll.rpc({});
117
191
  assert(res.ok);
118
- return expect(res.payload.msgs).toStrictEqual(expected);
192
+ expect(res.payload.msgs).toStrictEqual(expected);
193
+ await testFinishesCleanly({
194
+ clientTransports: [clientTransport],
195
+ serverTransport,
196
+ server,
197
+ });
119
198
  });
120
199
  const CONCURRENCY = 10;
121
200
  test('concurrent rpcs', async () => {
@@ -125,13 +204,18 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
125
204
  const client = createClient(clientTransport);
126
205
  const promises = [];
127
206
  for (let i = 0; i < CONCURRENCY; i++) {
128
- promises.push(client.test.add({ n: i }));
207
+ promises.push(client.test.add.rpc({ n: i }));
129
208
  }
130
209
  for (let i = 0; i < CONCURRENCY; i++) {
131
210
  const result = await promises[i];
132
211
  assert(result.ok);
133
212
  expect(result.payload).toStrictEqual({ n: i });
134
213
  }
214
+ await testFinishesCleanly({
215
+ clientTransports: [clientTransport],
216
+ serverTransport,
217
+ server,
218
+ });
135
219
  });
136
220
  test('concurrent streams', async () => {
137
221
  const [clientTransport, serverTransport] = getTransports();
@@ -140,7 +224,7 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
140
224
  const client = createClient(clientTransport);
141
225
  const openStreams = [];
142
226
  for (let i = 0; i < CONCURRENCY; i++) {
143
- const streamHandle = await client.test.echo();
227
+ const streamHandle = await client.test.echo.stream();
144
228
  const input = streamHandle[0];
145
229
  input.push({ msg: `${i}-1`, ignore: false });
146
230
  input.push({ msg: `${i}-2`, ignore: false });
@@ -148,10 +232,10 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
148
232
  }
149
233
  for (let i = 0; i < CONCURRENCY; i++) {
150
234
  const output = openStreams[i][1];
151
- const result1 = await output.next().then((res) => res.value);
235
+ const result1 = await iterNext(output);
152
236
  assert(result1.ok);
153
237
  expect(result1.payload).toStrictEqual({ response: `${i}-1` });
154
- const result2 = await output.next().then((res) => res.value);
238
+ const result2 = await iterNext(output);
155
239
  assert(result2.ok);
156
240
  expect(result2.payload).toStrictEqual({ response: `${i}-2` });
157
241
  }
@@ -160,5 +244,10 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
160
244
  const [_input, _output, close] = openStreams[i];
161
245
  close();
162
246
  }
247
+ await testFinishesCleanly({
248
+ clientTransports: [clientTransport],
249
+ serverTransport,
250
+ server,
251
+ });
163
252
  });
164
253
  });
@@ -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.messageHandlers, `transport ${t.clientId} should not have open message handlers after the test`).toStrictEqual(new Set());
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
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Represents an observable value that can be subscribed to for changes.
3
+ * This should only be used in tests
4
+ * @template T - The type of the value being observed.
5
+ */
6
+ export declare class Observable<T> {
7
+ value: T;
8
+ private listeners;
9
+ constructor(initialValue: T);
10
+ /**
11
+ * Gets the current value of the observable.
12
+ */
13
+ get(): T;
14
+ /**
15
+ * Sets the current value of the observable. All listeners will get an update with this value.
16
+ * @param newValue - The new value to set.
17
+ */
18
+ set(tx: (preValue: T) => T): void;
19
+ /**
20
+ * Subscribes to changes in the observable value.
21
+ * @param listener - A callback function that will be called when the value changes.
22
+ * @returns A function that can be called to unsubscribe from further notifications.
23
+ */
24
+ observe(listener: (val: T) => void): () => boolean;
25
+ }
26
+ //# sourceMappingURL=observable.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observable.d.ts","sourceRoot":"","sources":["../../../__tests__/fixtures/observable.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,qBAAa,UAAU,CAAC,CAAC;IACvB,KAAK,EAAE,CAAC,CAAC;IACT,OAAO,CAAC,SAAS,CAAwB;gBAE7B,YAAY,EAAE,CAAC;IAK3B;;OAEG;IACH,GAAG;IAIH;;;OAGG;IACH,GAAG,CAAC,EAAE,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC;IAM1B;;;;OAIG;IACH,OAAO,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,IAAI;CAKnC"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Represents an observable value that can be subscribed to for changes.
3
+ * This should only be used in tests
4
+ * @template T - The type of the value being observed.
5
+ */
6
+ export class Observable {
7
+ value;
8
+ listeners;
9
+ constructor(initialValue) {
10
+ this.value = initialValue;
11
+ this.listeners = new Set();
12
+ }
13
+ /**
14
+ * Gets the current value of the observable.
15
+ */
16
+ get() {
17
+ return this.value;
18
+ }
19
+ /**
20
+ * Sets the current value of the observable. All listeners will get an update with this value.
21
+ * @param newValue - The new value to set.
22
+ */
23
+ set(tx) {
24
+ const newValue = tx(this.value);
25
+ this.value = newValue;
26
+ this.listeners.forEach((listener) => listener(newValue));
27
+ }
28
+ /**
29
+ * Subscribes to changes in the observable value.
30
+ * @param listener - A callback function that will be called when the value changes.
31
+ * @returns A function that can be called to unsubscribe from further notifications.
32
+ */
33
+ observe(listener) {
34
+ this.listeners.add(listener);
35
+ listener(this.get());
36
+ return () => this.listeners.delete(listener);
37
+ }
38
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=observable.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observable.test.d.ts","sourceRoot":"","sources":["../../../__tests__/fixtures/observable.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,39 @@
1
+ import { Observable } from './observable';
2
+ import { describe, expect, test, vitest } from 'vitest';
3
+ describe('Observable', () => {
4
+ test('should set initial value correctly', () => {
5
+ const initialValue = 10;
6
+ const observable = new Observable(initialValue);
7
+ expect(observable.value).toBe(initialValue);
8
+ });
9
+ test('should update value correctly', () => {
10
+ const observable = new Observable(10);
11
+ const newValue = 20;
12
+ observable.set(() => newValue);
13
+ expect(observable.value).toBe(newValue);
14
+ });
15
+ test('should notify listeners when value changes', () => {
16
+ const observable = new Observable(10);
17
+ const listener = vitest.fn();
18
+ observable.observe(listener);
19
+ expect(listener).toHaveBeenCalledTimes(1);
20
+ const newValue = 20;
21
+ observable.set(() => newValue);
22
+ expect(listener).toHaveBeenCalledTimes(2);
23
+ expect(listener).toHaveBeenCalledWith(newValue);
24
+ });
25
+ test('should unsubscribe from notifications', () => {
26
+ const observable = new Observable(10);
27
+ const listener = vitest.fn();
28
+ const unsubscribe = observable.observe(listener);
29
+ expect(listener).toHaveBeenCalledTimes(1);
30
+ const newValue = 20;
31
+ observable.set(() => newValue);
32
+ expect(listener).toHaveBeenCalledTimes(2);
33
+ expect(listener).toHaveBeenCalledWith(newValue);
34
+ unsubscribe();
35
+ const anotherValue = 30;
36
+ observable.set(() => anotherValue);
37
+ expect(listener).toHaveBeenCalledTimes(2); // should not be called again after unsubscribing
38
+ });
39
+ });
@@ -1,6 +1,8 @@
1
+ import { Observable } from './observable';
1
2
  export declare const EchoRequest: import("@sinclair/typebox").TObject<{
2
3
  msg: import("@sinclair/typebox").TString;
3
4
  ignore: import("@sinclair/typebox").TBoolean;
5
+ end: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
4
6
  }>;
5
7
  export declare const EchoResponse: import("@sinclair/typebox").TObject<{
6
8
  response: import("@sinclair/typebox").TString;
@@ -19,11 +21,11 @@ export declare const TestServiceConstructor: () => {
19
21
  result: import("@sinclair/typebox").TNumber;
20
22
  }>;
21
23
  errors: import("@sinclair/typebox").TNever;
22
- handler: (context: import("../router").ServiceContextWithState<{
24
+ handler: (context: import("../../router").ServiceContextWithState<{
23
25
  count: number;
24
- }>, input: import("../transport/message").TransportMessage<{
26
+ }>, input: import("../../transport/message").TransportMessage<{
25
27
  n: number;
26
- }>) => Promise<import("../transport/message").TransportMessage<import("../router/result").Result<{
28
+ }>) => Promise<import("../../transport/message").TransportMessage<import("../../router/result").Result<{
27
29
  result: number;
28
30
  }, never>>>;
29
31
  type: "rpc";
@@ -33,17 +35,19 @@ export declare const TestServiceConstructor: () => {
33
35
  input: import("@sinclair/typebox").TObject<{
34
36
  msg: import("@sinclair/typebox").TString;
35
37
  ignore: import("@sinclair/typebox").TBoolean;
38
+ end: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
36
39
  }>;
37
40
  output: import("@sinclair/typebox").TObject<{
38
41
  response: import("@sinclair/typebox").TString;
39
42
  }>;
40
43
  errors: import("@sinclair/typebox").TNever;
41
- handler: (context: import("../router").ServiceContextWithState<{
44
+ handler: (context: import("../../router").ServiceContextWithState<{
42
45
  count: number;
43
- }>, input: AsyncIterable<import("../transport/message").TransportMessage<{
46
+ }>, input: AsyncIterable<import("../../transport/message").TransportMessage<{
47
+ end?: boolean | undefined;
44
48
  msg: string;
45
49
  ignore: boolean;
46
- }>>, output: import("it-pushable").Pushable<import("../transport/message").TransportMessage<import("../router/result").Result<{
50
+ }>>, output: import("it-pushable").Pushable<import("../../transport/message").TransportMessage<import("../../router/result").Result<{
47
51
  response: string;
48
52
  }, never>>, void, unknown>) => Promise<void>;
49
53
  type: "stream";
@@ -64,11 +68,11 @@ export declare const OrderingServiceConstructor: () => {
64
68
  n: import("@sinclair/typebox").TNumber;
65
69
  }>;
66
70
  errors: import("@sinclair/typebox").TNever;
67
- handler: (context: import("../router").ServiceContextWithState<{
71
+ handler: (context: import("../../router").ServiceContextWithState<{
68
72
  msgs: number[];
69
- }>, input: import("../transport/message").TransportMessage<{
73
+ }>, input: import("../../transport/message").TransportMessage<{
70
74
  n: number;
71
- }>) => Promise<import("../transport/message").TransportMessage<import("../router/result").Result<{
75
+ }>) => Promise<import("../../transport/message").TransportMessage<import("../../router/result").Result<{
72
76
  n: number;
73
77
  }, never>>>;
74
78
  type: "rpc";
@@ -80,9 +84,9 @@ export declare const OrderingServiceConstructor: () => {
80
84
  msgs: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TNumber>;
81
85
  }>;
82
86
  errors: import("@sinclair/typebox").TNever;
83
- handler: (context: import("../router").ServiceContextWithState<{
87
+ handler: (context: import("../../router").ServiceContextWithState<{
84
88
  msgs: number[];
85
- }>, input: import("../transport/message").TransportMessage<{}>) => Promise<import("../transport/message").TransportMessage<import("../router/result").Result<{
89
+ }>, input: import("../../transport/message").TransportMessage<{}>) => Promise<import("../../transport/message").TransportMessage<import("../../router/result").Result<{
86
90
  msgs: number[];
87
91
  }, never>>>;
88
92
  type: "rpc";
@@ -101,9 +105,9 @@ export declare const BinaryFileServiceConstructor: () => {
101
105
  contents: import("@sinclair/typebox").TUint8Array;
102
106
  }>;
103
107
  errors: import("@sinclair/typebox").TNever;
104
- handler: (context: import("../router").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{
108
+ handler: (context: import("../../router").ServiceContextWithState<{}>, input: import("../../transport/message").TransportMessage<{
105
109
  file: string;
106
- }>) => Promise<import("../transport/message").TransportMessage<import("../router/result").Result<{
110
+ }>) => Promise<import("../../transport/message").TransportMessage<import("../../router/result").Result<{
107
111
  contents: Uint8Array;
108
112
  }, never>>>;
109
113
  type: "rpc";
@@ -131,10 +135,10 @@ export declare const FallibleServiceConstructor: () => {
131
135
  test: import("@sinclair/typebox").TString;
132
136
  }>;
133
137
  }>;
134
- handler: (context: import("../router").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{
138
+ handler: (context: import("../../router").ServiceContextWithState<{}>, input: import("../../transport/message").TransportMessage<{
135
139
  a: number;
136
140
  b: number;
137
- }>) => Promise<import("../transport/message").TransportMessage<import("../router/result").Result<{
141
+ }>) => Promise<import("../../transport/message").TransportMessage<import("../../router/result").Result<{
138
142
  result: number;
139
143
  }, {
140
144
  message: string;
@@ -159,11 +163,11 @@ export declare const FallibleServiceConstructor: () => {
159
163
  code: import("@sinclair/typebox").TLiteral<"STREAM_ERROR">;
160
164
  message: import("@sinclair/typebox").TString;
161
165
  }>;
162
- handler: (context: import("../router").ServiceContextWithState<{}>, input: AsyncIterable<import("../transport/message").TransportMessage<{
166
+ handler: (context: import("../../router").ServiceContextWithState<{}>, input: AsyncIterable<import("../../transport/message").TransportMessage<{
163
167
  msg: string;
164
168
  throwResult: boolean;
165
169
  throwError: boolean;
166
- }>>, output: import("it-pushable").Pushable<import("../transport/message").TransportMessage<import("../router/result").Result<{
170
+ }>>, output: import("it-pushable").Pushable<import("../../transport/message").TransportMessage<import("../../router/result").Result<{
167
171
  response: string;
168
172
  }, {
169
173
  message: string;
@@ -173,4 +177,43 @@ export declare const FallibleServiceConstructor: () => {
173
177
  };
174
178
  };
175
179
  };
176
- //# sourceMappingURL=fixtures.d.ts.map
180
+ export declare const SubscribableServiceConstructor: () => {
181
+ name: "subscribable";
182
+ state: {
183
+ count: Observable<number>;
184
+ };
185
+ procedures: {
186
+ add: {
187
+ input: import("@sinclair/typebox").TObject<{
188
+ n: import("@sinclair/typebox").TNumber;
189
+ }>;
190
+ output: import("@sinclair/typebox").TObject<{
191
+ result: import("@sinclair/typebox").TNumber;
192
+ }>;
193
+ errors: import("@sinclair/typebox").TNever;
194
+ handler: (context: import("../../router").ServiceContextWithState<{
195
+ count: Observable<number>;
196
+ }>, input: import("../../transport/message").TransportMessage<{
197
+ n: number;
198
+ }>) => Promise<import("../../transport/message").TransportMessage<import("../../router/result").Result<{
199
+ result: number;
200
+ }, never>>>;
201
+ type: "rpc";
202
+ };
203
+ } & {
204
+ value: {
205
+ input: import("@sinclair/typebox").TObject<{}>;
206
+ output: import("@sinclair/typebox").TObject<{
207
+ result: import("@sinclair/typebox").TNumber;
208
+ }>;
209
+ errors: import("@sinclair/typebox").TNever;
210
+ handler: (context: import("../../router").ServiceContextWithState<{
211
+ count: Observable<number>;
212
+ }>, input: import("../../transport/message").TransportMessage<{}>, output: import("it-pushable").Pushable<import("../../transport/message").TransportMessage<import("../../router/result").Result<{
213
+ result: number;
214
+ }, never>>, void, unknown>) => Promise<void>;
215
+ type: "subscription";
216
+ };
217
+ };
218
+ };
219
+ //# sourceMappingURL=services.d.ts.map
@@ -0,0 +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;;;;EAItB,CAAC;AACH,eAAO,MAAM,YAAY;;EAA2C,CAAC;AAErE,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkCpB,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"}