@replit/river 0.7.2 → 0.8.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.
@@ -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';
3
+ import { createWebSocketServer, createWsTransports, onServerReady, } from '../util/testHelpers';
4
4
  import largePayload from './largePayload.json';
5
- import { TestServiceConstructor } from './fixtures';
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,11 +1,13 @@
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';
9
11
  describe.each(codecs)('client <-> server integration test ($name codec)', async ({ codec }) => {
10
12
  const httpServer = http.createServer();
11
13
  const port = await onServerReady(httpServer);
@@ -22,7 +24,7 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
22
24
  const serviceDefs = { test: TestServiceConstructor() };
23
25
  const server = await createServer(serverTransport, serviceDefs);
24
26
  const client = createClient(clientTransport);
25
- const result = await client.test.add({ n: 3 });
27
+ const result = await client.test.add.rpc({ n: 3 });
26
28
  assert(result.ok);
27
29
  expect(result.payload).toStrictEqual({ result: 3 });
28
30
  });
@@ -31,10 +33,10 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
31
33
  const serviceDefs = { test: FallibleServiceConstructor() };
32
34
  const server = await createServer(serverTransport, serviceDefs);
33
35
  const client = createClient(clientTransport);
34
- const result = await client.test.divide({ a: 10, b: 2 });
36
+ const result = await client.test.divide.rpc({ a: 10, b: 2 });
35
37
  assert(result.ok);
36
38
  expect(result.payload).toStrictEqual({ result: 5 });
37
- const result2 = await client.test.divide({ a: 10, b: 0 });
39
+ const result2 = await client.test.divide.rpc({ a: 10, b: 0 });
38
40
  assert(!result2.ok);
39
41
  expect(result2.payload).toStrictEqual({
40
42
  code: DIV_BY_ZERO,
@@ -49,7 +51,7 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
49
51
  const serviceDefs = { test: BinaryFileServiceConstructor() };
50
52
  const server = await createServer(serverTransport, serviceDefs);
51
53
  const client = createClient(clientTransport);
52
- const result = await client.test.getFile({ file: 'test.py' });
54
+ const result = await client.test.getFile.rpc({ file: 'test.py' });
53
55
  assert(result.ok);
54
56
  assert(result.payload.contents instanceof Uint8Array);
55
57
  expect(new TextDecoder().decode(result.payload.contents)).toStrictEqual('contents for file test.py');
@@ -59,15 +61,15 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
59
61
  const serviceDefs = { test: TestServiceConstructor() };
60
62
  const server = await createServer(serverTransport, serviceDefs);
61
63
  const client = createClient(clientTransport);
62
- const [input, output, close] = await client.test.echo();
64
+ const [input, output, close] = await client.test.echo.stream();
63
65
  input.push({ msg: 'abc', ignore: false });
64
66
  input.push({ msg: 'def', ignore: true });
65
67
  input.push({ msg: 'ghi', ignore: false });
66
68
  input.end();
67
- const result1 = await output.next().then((res) => res.value);
69
+ const result1 = await iterNext(output);
68
70
  assert(result1.ok);
69
71
  expect(result1.payload).toStrictEqual({ response: 'abc' });
70
- const result2 = await output.next().then((res) => res.value);
72
+ const result2 = await iterNext(output);
71
73
  assert(result2.ok);
72
74
  expect(result2.payload).toStrictEqual({ response: 'ghi' });
73
75
  close();
@@ -77,17 +79,17 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
77
79
  const serviceDefs = { test: FallibleServiceConstructor() };
78
80
  const server = await createServer(serverTransport, serviceDefs);
79
81
  const client = createClient(clientTransport);
80
- const [input, output, close] = await client.test.echo();
82
+ const [input, output, close] = await client.test.echo.stream();
81
83
  input.push({ msg: 'abc', throwResult: false, throwError: false });
82
- const result1 = await output.next().then((res) => res.value);
84
+ const result1 = await iterNext(output);
83
85
  assert(result1 && result1.ok);
84
86
  expect(result1.payload).toStrictEqual({ response: 'abc' });
85
87
  input.push({ msg: 'def', throwResult: true, throwError: false });
86
- const result2 = await output.next().then((res) => res.value);
88
+ const result2 = await iterNext(output);
87
89
  assert(result2 && !result2.ok);
88
90
  expect(result2.payload.code).toStrictEqual(STREAM_ERROR);
89
91
  input.push({ msg: 'ghi', throwResult: false, throwError: true });
90
- const result3 = await output.next().then((res) => res.value);
92
+ const result3 = await iterNext(output);
91
93
  assert(result3 && !result3.ok);
92
94
  expect(result3.payload).toStrictEqual({
93
95
  code: UNCAUGHT_ERROR,
@@ -95,6 +97,42 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
95
97
  });
96
98
  close();
97
99
  });
100
+ test('subscription', async () => {
101
+ const options = { codec };
102
+ const serverTransport = new WebSocketServerTransport(webSocketServer, 'SERVER', options);
103
+ const client1Transport = new WebSocketClientTransport(() => createLocalWebSocketClient(port), 'client1', 'SERVER', options);
104
+ const client2Transport = new WebSocketClientTransport(() => createLocalWebSocketClient(port), 'client2', 'SERVER', options);
105
+ const serviceDefs = { test: SubscribableServiceConstructor() };
106
+ const server = await createServer(serverTransport, serviceDefs);
107
+ const client1 = createClient(client1Transport);
108
+ const client2 = createClient(client2Transport);
109
+ const [subscription1, close1] = await client1.test.value.subscribe({});
110
+ let result = await iterNext(subscription1);
111
+ assert(result.ok);
112
+ expect(result.payload).toStrictEqual({ result: 0 });
113
+ const [subscription2, close2] = await client2.test.value.subscribe({});
114
+ result = await iterNext(subscription2);
115
+ assert(result.ok);
116
+ expect(result.payload).toStrictEqual({ result: 0 });
117
+ const add1 = await client1.test.add.rpc({ n: 1 });
118
+ assert(add1.ok);
119
+ result = await iterNext(subscription1);
120
+ assert(result.ok);
121
+ expect(result.payload).toStrictEqual({ result: 1 });
122
+ result = await iterNext(subscription2);
123
+ assert(result.ok);
124
+ expect(result.payload).toStrictEqual({ result: 1 });
125
+ const add2 = await client2.test.add.rpc({ n: 3 });
126
+ assert(add2.ok);
127
+ result = await iterNext(subscription1);
128
+ assert(result.ok);
129
+ expect(result.payload).toStrictEqual({ result: 4 });
130
+ result = await iterNext(subscription2);
131
+ assert(result.ok);
132
+ expect(result.payload).toStrictEqual({ result: 4 });
133
+ close1();
134
+ close2();
135
+ });
98
136
  test('message order is preserved in the face of disconnects', async () => {
99
137
  const [clientTransport, serverTransport] = getTransports();
100
138
  const serviceDefs = { test: OrderingServiceConstructor() };
@@ -109,11 +147,11 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
109
147
  if (i == 42) {
110
148
  clientTransport.connections.forEach((conn) => conn.ws.terminate());
111
149
  }
112
- await client.test.add({
150
+ await client.test.add.rpc({
113
151
  n: i,
114
152
  });
115
153
  }
116
- const res = await client.test.getAll({});
154
+ const res = await client.test.getAll.rpc({});
117
155
  assert(res.ok);
118
156
  return expect(res.payload.msgs).toStrictEqual(expected);
119
157
  });
@@ -125,7 +163,7 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
125
163
  const client = createClient(clientTransport);
126
164
  const promises = [];
127
165
  for (let i = 0; i < CONCURRENCY; i++) {
128
- promises.push(client.test.add({ n: i }));
166
+ promises.push(client.test.add.rpc({ n: i }));
129
167
  }
130
168
  for (let i = 0; i < CONCURRENCY; i++) {
131
169
  const result = await promises[i];
@@ -140,7 +178,7 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
140
178
  const client = createClient(clientTransport);
141
179
  const openStreams = [];
142
180
  for (let i = 0; i < CONCURRENCY; i++) {
143
- const streamHandle = await client.test.echo();
181
+ const streamHandle = await client.test.echo.stream();
144
182
  const input = streamHandle[0];
145
183
  input.push({ msg: `${i}-1`, ignore: false });
146
184
  input.push({ msg: `${i}-2`, ignore: false });
@@ -148,10 +186,10 @@ describe.each(codecs)('client <-> server integration test ($name codec)', async
148
186
  }
149
187
  for (let i = 0; i < CONCURRENCY; i++) {
150
188
  const output = openStreams[i][1];
151
- const result1 = await output.next().then((res) => res.value);
189
+ const result1 = await iterNext(output);
152
190
  assert(result1.ok);
153
191
  expect(result1.payload).toStrictEqual({ response: `${i}-1` });
154
- const result2 = await output.next().then((res) => res.value);
192
+ const result2 = await iterNext(output);
155
193
  assert(result2.ok);
156
194
  expect(result2.payload).toStrictEqual({ response: `${i}-2` });
157
195
  }
@@ -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,3 +1,4 @@
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;
@@ -19,11 +20,11 @@ export declare const TestServiceConstructor: () => {
19
20
  result: import("@sinclair/typebox").TNumber;
20
21
  }>;
21
22
  errors: import("@sinclair/typebox").TNever;
22
- handler: (context: import("../router").ServiceContextWithState<{
23
+ handler: (context: import("../../router").ServiceContextWithState<{
23
24
  count: number;
24
- }>, input: import("../transport/message").TransportMessage<{
25
+ }>, input: import("../../transport/message").TransportMessage<{
25
26
  n: number;
26
- }>) => Promise<import("../transport/message").TransportMessage<import("../router/result").Result<{
27
+ }>) => Promise<import("../../transport/message").TransportMessage<import("../../router/result").Result<{
27
28
  result: number;
28
29
  }, never>>>;
29
30
  type: "rpc";
@@ -38,12 +39,12 @@ export declare const TestServiceConstructor: () => {
38
39
  response: import("@sinclair/typebox").TString;
39
40
  }>;
40
41
  errors: import("@sinclair/typebox").TNever;
41
- handler: (context: import("../router").ServiceContextWithState<{
42
+ handler: (context: import("../../router").ServiceContextWithState<{
42
43
  count: number;
43
- }>, input: AsyncIterable<import("../transport/message").TransportMessage<{
44
+ }>, input: AsyncIterable<import("../../transport/message").TransportMessage<{
44
45
  msg: string;
45
46
  ignore: boolean;
46
- }>>, output: import("it-pushable").Pushable<import("../transport/message").TransportMessage<import("../router/result").Result<{
47
+ }>>, output: import("it-pushable").Pushable<import("../../transport/message").TransportMessage<import("../../router/result").Result<{
47
48
  response: string;
48
49
  }, never>>, void, unknown>) => Promise<void>;
49
50
  type: "stream";
@@ -64,11 +65,11 @@ export declare const OrderingServiceConstructor: () => {
64
65
  n: import("@sinclair/typebox").TNumber;
65
66
  }>;
66
67
  errors: import("@sinclair/typebox").TNever;
67
- handler: (context: import("../router").ServiceContextWithState<{
68
+ handler: (context: import("../../router").ServiceContextWithState<{
68
69
  msgs: number[];
69
- }>, input: import("../transport/message").TransportMessage<{
70
+ }>, input: import("../../transport/message").TransportMessage<{
70
71
  n: number;
71
- }>) => Promise<import("../transport/message").TransportMessage<import("../router/result").Result<{
72
+ }>) => Promise<import("../../transport/message").TransportMessage<import("../../router/result").Result<{
72
73
  n: number;
73
74
  }, never>>>;
74
75
  type: "rpc";
@@ -80,9 +81,9 @@ export declare const OrderingServiceConstructor: () => {
80
81
  msgs: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TNumber>;
81
82
  }>;
82
83
  errors: import("@sinclair/typebox").TNever;
83
- handler: (context: import("../router").ServiceContextWithState<{
84
+ handler: (context: import("../../router").ServiceContextWithState<{
84
85
  msgs: number[];
85
- }>, input: import("../transport/message").TransportMessage<{}>) => Promise<import("../transport/message").TransportMessage<import("../router/result").Result<{
86
+ }>, input: import("../../transport/message").TransportMessage<{}>) => Promise<import("../../transport/message").TransportMessage<import("../../router/result").Result<{
86
87
  msgs: number[];
87
88
  }, never>>>;
88
89
  type: "rpc";
@@ -101,9 +102,9 @@ export declare const BinaryFileServiceConstructor: () => {
101
102
  contents: import("@sinclair/typebox").TUint8Array;
102
103
  }>;
103
104
  errors: import("@sinclair/typebox").TNever;
104
- handler: (context: import("../router").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{
105
+ handler: (context: import("../../router").ServiceContextWithState<{}>, input: import("../../transport/message").TransportMessage<{
105
106
  file: string;
106
- }>) => Promise<import("../transport/message").TransportMessage<import("../router/result").Result<{
107
+ }>) => Promise<import("../../transport/message").TransportMessage<import("../../router/result").Result<{
107
108
  contents: Uint8Array;
108
109
  }, never>>>;
109
110
  type: "rpc";
@@ -131,10 +132,10 @@ export declare const FallibleServiceConstructor: () => {
131
132
  test: import("@sinclair/typebox").TString;
132
133
  }>;
133
134
  }>;
134
- handler: (context: import("../router").ServiceContextWithState<{}>, input: import("../transport/message").TransportMessage<{
135
+ handler: (context: import("../../router").ServiceContextWithState<{}>, input: import("../../transport/message").TransportMessage<{
135
136
  a: number;
136
137
  b: number;
137
- }>) => Promise<import("../transport/message").TransportMessage<import("../router/result").Result<{
138
+ }>) => Promise<import("../../transport/message").TransportMessage<import("../../router/result").Result<{
138
139
  result: number;
139
140
  }, {
140
141
  message: string;
@@ -159,11 +160,11 @@ export declare const FallibleServiceConstructor: () => {
159
160
  code: import("@sinclair/typebox").TLiteral<"STREAM_ERROR">;
160
161
  message: import("@sinclair/typebox").TString;
161
162
  }>;
162
- handler: (context: import("../router").ServiceContextWithState<{}>, input: AsyncIterable<import("../transport/message").TransportMessage<{
163
+ handler: (context: import("../../router").ServiceContextWithState<{}>, input: AsyncIterable<import("../../transport/message").TransportMessage<{
163
164
  msg: string;
164
165
  throwResult: boolean;
165
166
  throwError: boolean;
166
- }>>, output: import("it-pushable").Pushable<import("../transport/message").TransportMessage<import("../router/result").Result<{
167
+ }>>, output: import("it-pushable").Pushable<import("../../transport/message").TransportMessage<import("../../router/result").Result<{
167
168
  response: string;
168
169
  }, {
169
170
  message: string;
@@ -173,4 +174,43 @@ export declare const FallibleServiceConstructor: () => {
173
174
  };
174
175
  };
175
176
  };
176
- //# sourceMappingURL=fixtures.d.ts.map
177
+ export declare const SubscribableServiceConstructor: () => {
178
+ name: "subscribable";
179
+ state: {
180
+ count: Observable<number>;
181
+ };
182
+ procedures: {
183
+ add: {
184
+ input: import("@sinclair/typebox").TObject<{
185
+ n: import("@sinclair/typebox").TNumber;
186
+ }>;
187
+ output: import("@sinclair/typebox").TObject<{
188
+ result: import("@sinclair/typebox").TNumber;
189
+ }>;
190
+ errors: import("@sinclair/typebox").TNever;
191
+ handler: (context: import("../../router").ServiceContextWithState<{
192
+ count: Observable<number>;
193
+ }>, input: import("../../transport/message").TransportMessage<{
194
+ n: number;
195
+ }>) => Promise<import("../../transport/message").TransportMessage<import("../../router/result").Result<{
196
+ result: number;
197
+ }, never>>>;
198
+ type: "rpc";
199
+ };
200
+ } & {
201
+ value: {
202
+ input: import("@sinclair/typebox").TObject<{}>;
203
+ output: import("@sinclair/typebox").TObject<{
204
+ result: import("@sinclair/typebox").TNumber;
205
+ }>;
206
+ errors: import("@sinclair/typebox").TNever;
207
+ handler: (context: import("../../router").ServiceContextWithState<{
208
+ count: Observable<number>;
209
+ }>, input: import("../../transport/message").TransportMessage<{}>, output: import("it-pushable").Pushable<import("../../transport/message").TransportMessage<import("../../router/result").Result<{
210
+ result: number;
211
+ }, never>>, void, unknown>) => Promise<void>;
212
+ type: "subscription";
213
+ };
214
+ };
215
+ };
216
+ //# 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;;;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,7 +1,8 @@
1
1
  import { Type } from '@sinclair/typebox';
2
- import { ServiceBuilder } from '../router/builder';
3
- import { reply } from '../transport/message';
4
- import { Err, Ok } from '../router/result';
2
+ import { ServiceBuilder } from '../../router/builder';
3
+ import { reply } from '../../transport/message';
4
+ import { Err, Ok } from '../../router/result';
5
+ import { Observable } from './observable';
5
6
  export const EchoRequest = Type.Object({
6
7
  msg: Type.String(),
7
8
  ignore: Type.Boolean(),
@@ -139,3 +140,30 @@ export const FallibleServiceConstructor = () => ServiceBuilder.create('fallible'
139
140
  },
140
141
  })
141
142
  .finalize();
143
+ export const SubscribableServiceConstructor = () => ServiceBuilder.create('subscribable')
144
+ .initialState({
145
+ count: new Observable(0),
146
+ })
147
+ .defineProcedure('add', {
148
+ type: 'rpc',
149
+ input: Type.Object({ n: Type.Number() }),
150
+ output: Type.Object({ result: Type.Number() }),
151
+ errors: Type.Never(),
152
+ async handler(ctx, msg) {
153
+ const { n } = msg.payload;
154
+ ctx.state.count.set((prev) => prev + n);
155
+ return reply(msg, Ok({ result: ctx.state.count.get() }));
156
+ },
157
+ })
158
+ .defineProcedure('value', {
159
+ type: 'subscription',
160
+ input: Type.Object({}),
161
+ output: Type.Object({ result: Type.Number() }),
162
+ errors: Type.Never(),
163
+ async handler(ctx, msg, returnStream) {
164
+ ctx.state.count.observe((count) => {
165
+ returnStream.push(reply(msg, Ok({ result: count })));
166
+ });
167
+ },
168
+ })
169
+ .finalize();
@@ -1,7 +1,8 @@
1
- import { asClientRpc, asClientStream } from '../testUtils';
1
+ import { asClientRpc, asClientStream, asClientSubscription, iterNext, } from '../util/testHelpers';
2
2
  import { assert, describe, expect, test } from 'vitest';
3
- import { DIV_BY_ZERO, FallibleServiceConstructor, STREAM_ERROR, TestServiceConstructor, } from './fixtures';
3
+ import { DIV_BY_ZERO, FallibleServiceConstructor, STREAM_ERROR, SubscribableServiceConstructor, TestServiceConstructor, } from './fixtures/services';
4
4
  import { UNCAUGHT_ERROR } from '../router/result';
5
+ import { Observable } from './fixtures/observable';
5
6
  describe('server-side test', () => {
6
7
  const service = TestServiceConstructor();
7
8
  const initialState = { count: 0 };
@@ -39,10 +40,10 @@ describe('server-side test', () => {
39
40
  input.push({ msg: 'def', ignore: true });
40
41
  input.push({ msg: 'ghi', ignore: false });
41
42
  input.end();
42
- const result1 = await output.next().then((res) => res.value);
43
+ const result1 = await iterNext(output);
43
44
  assert(result1 && result1.ok);
44
45
  expect(result1.payload).toStrictEqual({ response: 'abc' });
45
- const result2 = await output.next().then((res) => res.value);
46
+ const result2 = await iterNext(output);
46
47
  assert(result2 && result2.ok);
47
48
  expect(result2.payload).toStrictEqual({ response: 'ghi' });
48
49
  expect(output.readableLength).toBe(0);
@@ -51,15 +52,15 @@ describe('server-side test', () => {
51
52
  const service = FallibleServiceConstructor();
52
53
  const [input, output] = asClientStream({}, service.procedures.echo);
53
54
  input.push({ msg: 'abc', throwResult: false, throwError: false });
54
- const result1 = await output.next().then((res) => res.value);
55
+ const result1 = await iterNext(output);
55
56
  assert(result1 && result1.ok);
56
57
  expect(result1.payload).toStrictEqual({ response: 'abc' });
57
58
  input.push({ msg: 'def', throwResult: true, throwError: false });
58
- const result2 = await output.next().then((res) => res.value);
59
+ const result2 = await iterNext(output);
59
60
  assert(result2 && !result2.ok);
60
61
  expect(result2.payload.code).toStrictEqual(STREAM_ERROR);
61
62
  input.push({ msg: 'ghi', throwResult: false, throwError: true });
62
- const result3 = await output.next().then((res) => res.value);
63
+ const result3 = await iterNext(output);
63
64
  assert(result3 && !result3.ok);
64
65
  expect(result3.payload).toStrictEqual({
65
66
  code: UNCAUGHT_ERROR,
@@ -68,4 +69,20 @@ describe('server-side test', () => {
68
69
  input.end();
69
70
  expect(output.readableLength).toBe(0);
70
71
  });
72
+ test('subscriptions', async () => {
73
+ const service = SubscribableServiceConstructor();
74
+ const state = { count: new Observable(0) };
75
+ const add = asClientRpc(state, service.procedures.add);
76
+ const subscribe = asClientSubscription(state, service.procedures.value);
77
+ const stream = await subscribe({});
78
+ const streamResult1 = await iterNext(stream);
79
+ assert(streamResult1 && streamResult1.ok);
80
+ expect(streamResult1.payload).toStrictEqual({ result: 0 });
81
+ const result = await add({ n: 3 });
82
+ assert(result.ok);
83
+ expect(result.payload).toStrictEqual({ result: 3 });
84
+ const streamResult2 = await iterNext(stream);
85
+ assert(streamResult2 && streamResult1.ok);
86
+ expect(streamResult2.payload).toStrictEqual({ result: 3 });
87
+ });
71
88
  });
@@ -1,6 +1,6 @@
1
1
  import { expect, describe, test } from 'vitest';
2
2
  import { serializeService } from '../router/builder';
3
- import { BinaryFileServiceConstructor, FallibleServiceConstructor, TestServiceConstructor, } from './fixtures';
3
+ import { BinaryFileServiceConstructor, FallibleServiceConstructor, TestServiceConstructor, } from './fixtures/services';
4
4
  describe('serialize service to jsonschema', () => {
5
5
  test('serialize basic service', () => {
6
6
  const service = TestServiceConstructor();
@@ -6,7 +6,7 @@ import { Result, RiverError, RiverUncaughtSchema } from './result';
6
6
  /**
7
7
  * The valid {@link Procedure} types.
8
8
  */
9
- export type ValidProcType = 'stream' | 'rpc';
9
+ export type ValidProcType = 'rpc' | 'stream' | 'subscription';
10
10
  /**
11
11
  * A generic procedure listing where the keys are the names of the procedures
12
12
  * and the values are the {@link Procedure} definitions. This is not meant to
@@ -65,7 +65,7 @@ export type ProcType<S extends AnyService, ProcName extends keyof S['procedures'
65
65
  /**
66
66
  * Defines a Procedure type that can be either an RPC or a stream procedure.
67
67
  * @template State - The TypeBox schema of the state object.
68
- * @template Ty - The type of the procedure, either 'rpc' or 'stream'.
68
+ * @template Ty - The type of the procedure.
69
69
  * @template I - The TypeBox schema of the input object.
70
70
  * @template O - The TypeBox schema of the output object.
71
71
  */
@@ -75,13 +75,19 @@ export type Procedure<State extends object | unknown, Ty extends ValidProcType,
75
75
  errors: E;
76
76
  handler: (context: ServiceContextWithState<State>, input: TransportMessage<Static<I>>) => Promise<TransportMessage<Result<Static<O>, Static<E>>>>;
77
77
  type: Ty;
78
- } : {
78
+ } : Ty extends 'stream' ? {
79
79
  input: I;
80
80
  output: O;
81
81
  errors: E;
82
82
  handler: (context: ServiceContextWithState<State>, input: AsyncIterable<TransportMessage<Static<I>>>, output: Pushable<TransportMessage<Result<Static<O>, Static<E>>>>) => Promise<void>;
83
83
  type: Ty;
84
- };
84
+ } : Ty extends 'subscription' ? {
85
+ input: I;
86
+ output: O;
87
+ errors: E;
88
+ handler: (context: ServiceContextWithState<State>, input: TransportMessage<Static<I>>, output: Pushable<TransportMessage<Result<Static<O>, Static<E>>>>) => Promise<void>;
89
+ type: Ty;
90
+ } : never;
85
91
  export type AnyProcedure = Procedure<object, ValidProcType, TObject, TObject, RiverError>;
86
92
  /**
87
93
  * A builder class for creating River Services.
@@ -1 +1 @@
1
- {"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../../router/builder.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../../router/builder.ts"],"names":[],"mappings":"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"}
@@ -12,11 +12,20 @@ type AsyncIter<T> = AsyncGenerator<T, T, unknown>;
12
12
  * @template Router - The type of the Router.
13
13
  */
14
14
  type ServiceClient<Router extends AnyService> = {
15
- [ProcName in keyof Router['procedures']]: ProcType<Router, ProcName> extends 'rpc' ? (input: Static<ProcInput<Router, ProcName>>) => Promise<Result<Static<ProcOutput<Router, ProcName>>, Static<ProcErrors<Router, ProcName>>>> : () => Promise<[
16
- Pushable<Static<ProcInput<Router, ProcName>>>,
17
- AsyncIter<Result<Static<ProcOutput<Router, ProcName>>, Static<ProcErrors<Router, ProcName>>>>,
18
- () => void
19
- ]>;
15
+ [ProcName in keyof Router['procedures']]: ProcType<Router, ProcName> extends 'rpc' ? {
16
+ rpc: (input: Static<ProcInput<Router, ProcName>>) => Promise<Result<Static<ProcOutput<Router, ProcName>>, Static<ProcErrors<Router, ProcName>>>>;
17
+ } : ProcType<Router, ProcName> extends 'stream' ? {
18
+ stream: () => Promise<[
19
+ Pushable<Static<ProcInput<Router, ProcName>>>,
20
+ AsyncIter<Result<Static<ProcOutput<Router, ProcName>>, Static<ProcErrors<Router, ProcName>>>>,
21
+ () => void
22
+ ]>;
23
+ } : ProcType<Router, ProcName> extends 'subscription' ? {
24
+ subscribe: (input: Static<ProcInput<Router, ProcName>>) => Promise<[
25
+ AsyncIter<Result<Static<ProcOutput<Router, ProcName>>, Static<ProcErrors<Router, ProcName>>>>,
26
+ () => void
27
+ ]>;
28
+ } : never;
20
29
  };
21
30
  /**
22
31
  * Defines a type that represents a client for a server with a set of services.
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../router/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EACL,UAAU,EACV,UAAU,EACV,SAAS,EACT,UAAU,EACV,QAAQ,EACT,MAAM,WAAW,CAAC;AAEnB,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAIL,iBAAiB,EAElB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAG3C,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAGlC,KAAK,SAAS,CAAC,CAAC,IAAI,cAAc,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC;AAElD;;;;GAIG;AACH,KAAK,aAAa,CAAC,MAAM,SAAS,UAAU,IAAI;KAC7C,QAAQ,IAAI,MAAM,MAAM,CAAC,YAAY,CAAC,GAAG,QAAQ,CAChD,MAAM,EACN,QAAQ,CACT,SAAS,KAAK,GAEX,CACE,KAAK,EAAE,MAAM,CAAC,SAAS,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,KACvC,OAAO,CACV,MAAM,CACJ,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,EACpC,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CACrC,CACF,GAED,MAAM,OAAO,CACX;QACE,QAAQ,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;QAC7C,SAAS,CACP,MAAM,CACJ,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,EACpC,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CACrC,CACF;QACD,MAAM,IAAI;KACX,CACF;CACN,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,YAAY,CAAC,GAAG,SAAS,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,IAAI;KACxE,OAAO,IAAI,MAAM,GAAG,CAAC,UAAU,CAAC,GAAG,aAAa,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,OAAO,CAAC,CAAC;CAC5E,CAAC;AAgCF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,YAAY,8DACZ,UAAU,UAAU,CAAC,aACtB,iBAAiB,sBAkFA,CAAC"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../router/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EACL,UAAU,EACV,UAAU,EACV,SAAS,EACT,UAAU,EACV,QAAQ,EACT,MAAM,WAAW,CAAC;AAEnB,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAIL,iBAAiB,EAElB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAG3C,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAGlC,KAAK,SAAS,CAAC,CAAC,IAAI,cAAc,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC;AAElD;;;;GAIG;AACH,KAAK,aAAa,CAAC,MAAM,SAAS,UAAU,IAAI;KAC7C,QAAQ,IAAI,MAAM,MAAM,CAAC,YAAY,CAAC,GAAG,QAAQ,CAChD,MAAM,EACN,QAAQ,CACT,SAAS,KAAK,GACX;QACE,GAAG,EAAE,CACH,KAAK,EAAE,MAAM,CAAC,SAAS,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,KACvC,OAAO,CACV,MAAM,CACJ,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,EACpC,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CACrC,CACF,CAAC;KACH,GACD,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,SAAS,QAAQ,GAC3C;QACE,MAAM,EAAE,MAAM,OAAO,CACnB;YACE,QAAQ,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;YAC7C,SAAS,CACP,MAAM,CACJ,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,EACpC,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CACrC,CACF;YACD,MAAM,IAAI;SACX,CACF,CAAC;KACH,GACD,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,SAAS,cAAc,GACjD;QACE,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,SAAS,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,KAAK,OAAO,CAChE;YACE,SAAS,CACP,MAAM,CACJ,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,EACpC,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CACrC,CACF;YACD,MAAM,IAAI;SACX,CACF,CAAC;KACH,GACD,KAAK;CACV,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,YAAY,CAAC,GAAG,SAAS,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,IAAI;KACxE,OAAO,IAAI,MAAM,GAAG,CAAC,UAAU,CAAC,GAAG,aAAa,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,OAAO,CAAC,CAAC;CAC5E,CAAC;AAgCF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,YAAY,8DACZ,UAAU,UAAU,CAAC,aACtB,iBAAiB,sBAmHA,CAAC"}
@@ -35,7 +35,10 @@ function _createRecursiveProxy(callback, path) {
35
35
  * @returns The client for the server.
36
36
  */
37
37
  export const createClient = (transport, serverId = 'SERVER') => _createRecursiveProxy(async (opts) => {
38
- const [serviceName, procName] = [...opts.path];
38
+ const [serviceName, procName, procType] = [...opts.path];
39
+ if (!(serviceName && procName && procType)) {
40
+ throw new Error('invalid river call, ensure the service and procedure you are calling exists');
41
+ }
39
42
  const [input] = opts.args;
40
43
  const streamId = nanoid();
41
44
  function belongsToSameStream(msg) {
@@ -43,8 +46,7 @@ export const createClient = (transport, serverId = 'SERVER') => _createRecursive
43
46
  msg.procedureName === procName &&
44
47
  msg.streamId === streamId);
45
48
  }
46
- if (input === undefined) {
47
- // stream case (stream methods are called with zero arguments)
49
+ if (procType === 'stream') {
48
50
  const inputStream = pushable({ objectMode: true });
49
51
  const outputStream = pushable({ objectMode: true });
50
52
  // input -> transport
@@ -73,8 +75,7 @@ export const createClient = (transport, serverId = 'SERVER') => _createRecursive
73
75
  };
74
76
  return [inputStream, outputStream, closeHandler];
75
77
  }
76
- else {
77
- // rpc case
78
+ else if (procType === 'rpc') {
78
79
  const m = msg(transport.clientId, serverId, serviceName, procName, streamId, input);
79
80
  // rpc is a stream open + close
80
81
  m.controlFlags |=
@@ -82,4 +83,25 @@ export const createClient = (transport, serverId = 'SERVER') => _createRecursive
82
83
  transport.send(m);
83
84
  return waitForMessage(transport, belongsToSameStream);
84
85
  }
86
+ else if (procType === 'subscribe') {
87
+ const m = msg(transport.clientId, serverId, serviceName, procName, streamId, input);
88
+ m.controlFlags |= 2 /* ControlFlags.StreamOpenBit */;
89
+ transport.send(m);
90
+ // transport -> output
91
+ const outputStream = pushable({ objectMode: true });
92
+ const listener = (msg) => {
93
+ if (belongsToSameStream(msg)) {
94
+ outputStream.push(msg.payload);
95
+ }
96
+ };
97
+ transport.addMessageListener(listener);
98
+ const closeHandler = () => {
99
+ outputStream.end();
100
+ transport.removeMessageListener(listener);
101
+ };
102
+ return [outputStream, closeHandler];
103
+ }
104
+ else {
105
+ throw new Error(`invalid river call, unknown procedure type ${procType}`);
106
+ }
85
107
  }, []);
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../router/server.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EAAgB,UAAU,EAAE,MAAM,WAAW,CAAC;AAWrD,OAAO,EAAE,cAAc,EAA2B,MAAM,WAAW,CAAC;AAWpE;;;GAGG;AACH,MAAM,WAAW,MAAM,CAAC,QAAQ;IAC9B,QAAQ,EAAE,QAAQ,CAAC;IACnB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAWD;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,EAC5E,SAAS,EAAE,SAAS,CAAC,UAAU,CAAC,EAChC,QAAQ,EAAE,QAAQ,EAClB,eAAe,CAAC,EAAE,IAAI,CAAC,cAAc,EAAE,OAAO,CAAC,GAC9C,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAqJ3B"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../router/server.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EAAgB,UAAU,EAAE,MAAM,WAAW,CAAC;AAWrD,OAAO,EAAE,cAAc,EAA2B,MAAM,WAAW,CAAC;AAWpE;;;GAGG;AACH,MAAM,WAAW,MAAM,CAAC,QAAQ;IAC9B,QAAQ,EAAE,QAAQ,CAAC;IACnB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAWD;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,EAC5E,SAAS,EAAE,SAAS,CAAC,UAAU,CAAC,EAChC,QAAQ,EAAE,QAAQ,EAClB,eAAe,CAAC,EAAE,IAAI,CAAC,cAAc,EAAE,OAAO,CAAC,GAC9C,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CA0K3B"}
@@ -44,7 +44,8 @@ export async function createServer(transport, services, extendedContext) {
44
44
  return;
45
45
  }
46
46
  const procedure = service.procedures[msg.procedureName];
47
- if (isStreamOpen(msg.controlFlags)) {
47
+ const streamIdx = `${msg.serviceName}.${msg.procedureName}:${msg.streamId}`;
48
+ if (isStreamOpen(msg.controlFlags) && !streamMap.has(streamIdx)) {
48
49
  const incoming = pushable({ objectMode: true });
49
50
  const outgoing = pushable({ objectMode: true });
50
51
  const openPromises = [
@@ -71,24 +72,40 @@ export async function createServer(transport, services, extendedContext) {
71
72
  }
72
73
  else if (procedure.type === 'rpc') {
73
74
  openPromises.push((async () => {
74
- for await (const inputMessage of incoming) {
75
- try {
76
- const outputMessage = await procedure.handler(serviceContext, inputMessage);
77
- outgoing.push(outputMessage);
78
- }
79
- catch (err) {
80
- errorHandler(err);
81
- }
75
+ const inputMessage = await incoming.next();
76
+ if (inputMessage.done) {
77
+ return;
78
+ }
79
+ try {
80
+ const outputMessage = await procedure.handler(serviceContext, inputMessage.value);
81
+ outgoing.push(outputMessage);
82
+ }
83
+ catch (err) {
84
+ errorHandler(err);
85
+ }
86
+ })());
87
+ }
88
+ else if (procedure.type === 'subscription') {
89
+ openPromises.push((async () => {
90
+ const inputMessage = await incoming.next();
91
+ if (inputMessage.done) {
92
+ return;
93
+ }
94
+ try {
95
+ await procedure.handler(serviceContext, inputMessage.value, outgoing);
96
+ }
97
+ catch (err) {
98
+ errorHandler(err);
82
99
  }
83
100
  })());
84
101
  }
85
- streamMap.set(`${msg.serviceName}.${msg.procedureName}:${msg.streamId}`, {
102
+ streamMap.set(streamIdx, {
86
103
  incoming,
87
104
  outgoing,
88
105
  openPromises,
89
106
  });
90
107
  }
91
- const procStream = streamMap.get(`${msg.serviceName}.${msg.procedureName}:${msg.streamId}`);
108
+ const procStream = streamMap.get(streamIdx);
92
109
  if (!procStream) {
93
110
  log?.warn(`${transport.clientId} -- couldn't find a matching procedure stream for ${msg.serviceName}.${msg.procedureName}:${msg.streamId}`);
94
111
  return;
@@ -2,7 +2,7 @@ import { describe, test, expect } from 'vitest';
2
2
  import stream from 'node:stream';
3
3
  import { StdioTransport } from './stdio';
4
4
  import { waitForMessage } from '../..';
5
- import { payloadToTransportMessage } from '../../../testUtils';
5
+ import { payloadToTransportMessage } from '../../../util/testHelpers';
6
6
  describe('sending and receiving across node streams works', () => {
7
7
  test('basic send/receive', async () => {
8
8
  const clientToServer = new stream.PassThrough();
@@ -1,6 +1,6 @@
1
1
  import http from 'http';
2
2
  import { describe, test, expect, afterAll } from 'vitest';
3
- import { createWebSocketServer, createWsTransports, createDummyTransportMessage, onServerReady, createLocalWebSocketClient, } from '../../../testUtils';
3
+ import { createWebSocketServer, createWsTransports, createDummyTransportMessage, onServerReady, createLocalWebSocketClient, } from '../../../util/testHelpers';
4
4
  import { msg, waitForMessage } from '../..';
5
5
  import { WebSocketServerTransport } from './server';
6
6
  import { WebSocketClientTransport } from './client';
@@ -2,14 +2,14 @@
2
2
  import WebSocket from 'isomorphic-ws';
3
3
  import { WebSocketServer } from 'ws';
4
4
  import http from 'http';
5
- import { WebSocketClientTransport } from './transport/impls/ws/client';
5
+ import { WebSocketClientTransport } from '../transport/impls/ws/client';
6
6
  import { Static, TObject } from '@sinclair/typebox';
7
- import { Procedure, ServiceContext } from './router';
8
- import { OpaqueTransportMessage, TransportClientId, TransportMessage } from './transport';
7
+ import { Procedure, ServiceContext } from '../router';
8
+ import { OpaqueTransportMessage, TransportClientId, TransportMessage } from '../transport';
9
9
  import { Pushable } from 'it-pushable';
10
- import { Result, RiverError, RiverUncaughtSchema } from './router/result';
11
- import { Codec } from './codec';
12
- import { WebSocketServerTransport } from './transport/impls/ws/server';
10
+ import { Result, RiverError, RiverUncaughtSchema } from '../router/result';
11
+ import { Codec } from '../codec';
12
+ import { WebSocketServerTransport } from '../transport/impls/ws/server';
13
13
  /**
14
14
  * Creates a WebSocket server instance using the provided HTTP server.
15
15
  * Only used as helper for testing.
@@ -61,12 +61,25 @@ export declare function asClientRpc<State extends object | unknown, I extends TO
61
61
  * @param {State} state - The state object.
62
62
  * @param {Procedure<State, 'stream', I, O>} proc - The procedure to handle the stream.
63
63
  * @param {Omit<ServiceContext, 'state'>} [extendedContext] - The extended context object.
64
- * @returns {[Pushable<Static<I>>, Pushable<Static<O>>]} - Pair of input and output streams.
64
+ * @returns Pair of input and output streams.
65
65
  */
66
66
  export declare function asClientStream<State extends object | unknown, I extends TObject, O extends TObject, E extends RiverError>(state: State, proc: Procedure<State, 'stream', I, O, E>, extendedContext?: Omit<ServiceContext, 'state'>): [
67
67
  Pushable<Static<I>>,
68
68
  Pushable<Result<Static<O>, Static<E> | Static<typeof RiverUncaughtSchema>>>
69
69
  ];
70
+ /**
71
+ * Transforms a subscription procedure definition into a procedure that returns an output stream.
72
+ * Input messages can be pushed into the input stream.
73
+ * This should only be used for testing.
74
+ * @template State - The type of the state object.
75
+ * @template I - The type of the input object.
76
+ * @template O - The type of the output object.
77
+ * @param {State} state - The state object.
78
+ * @param {Procedure<State, 'stream', I, O>} proc - The procedure to handle the stream.
79
+ * @param {Omit<ServiceContext, 'state'>} [extendedContext] - The extended context object.
80
+ * @returns A function that when passed a message, returns the output stream.
81
+ */
82
+ export declare function asClientSubscription<State extends object | unknown, I extends TObject, O extends TObject, E extends RiverError>(state: State, proc: Procedure<State, 'subscription', I, O, E>, extendedContext?: Omit<ServiceContext, 'state'>): (msg: Static<I>) => Promise<Pushable<Result<Static<O>, Static<E> | Static<typeof RiverUncaughtSchema>>>>;
70
83
  /**
71
84
  * Converts a payload object to a transport message with reasonable defaults.
72
85
  * This should only be used for testing.
@@ -80,4 +93,10 @@ export declare function payloadToTransportMessage<Payload extends object>(payloa
80
93
  * @returns The created opaque transport message.
81
94
  */
82
95
  export declare function createDummyTransportMessage(): OpaqueTransportMessage;
83
- //# sourceMappingURL=testUtils.d.ts.map
96
+ /**
97
+ * Retrieves the next value from an async iterable iterator.
98
+ * @param iter The async iterable iterator.
99
+ * @returns A promise that resolves to the next value from the iterator.
100
+ */
101
+ export declare function iterNext<T>(iter: AsyncIterableIterator<T>): Promise<any>;
102
+ //# sourceMappingURL=testHelpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testHelpers.d.ts","sourceRoot":"","sources":["../../util/testHelpers.ts"],"names":[],"mappings":"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"}
@@ -1,10 +1,10 @@
1
1
  import WebSocket from 'isomorphic-ws';
2
2
  import { WebSocketServer } from 'ws';
3
- import { WebSocketClientTransport } from './transport/impls/ws/client';
4
- import { msg, reply, } from './transport';
3
+ import { WebSocketClientTransport } from '../transport/impls/ws/client';
4
+ import { msg, reply, } from '../transport';
5
5
  import { pushable } from 'it-pushable';
6
- import { Err, UNCAUGHT_ERROR, } from './router/result';
7
- import { WebSocketServerTransport } from './transport/impls/ws/server';
6
+ import { Err, UNCAUGHT_ERROR, } from '../router/result';
7
+ import { WebSocketServerTransport } from '../transport/impls/ws/server';
8
8
  /**
9
9
  * Creates a WebSocket server instance using the provided HTTP server.
10
10
  * Only used as helper for testing.
@@ -91,7 +91,7 @@ export function asClientRpc(state, proc, extendedContext) {
91
91
  * @param {State} state - The state object.
92
92
  * @param {Procedure<State, 'stream', I, O>} proc - The procedure to handle the stream.
93
93
  * @param {Omit<ServiceContext, 'state'>} [extendedContext] - The extended context object.
94
- * @returns {[Pushable<Static<I>>, Pushable<Static<O>>]} - Pair of input and output streams.
94
+ * @returns Pair of input and output streams.
95
95
  */
96
96
  export function asClientStream(state, proc, extendedContext) {
97
97
  const rawInput = pushable({ objectMode: true });
@@ -133,6 +133,44 @@ export function asClientStream(state, proc, extendedContext) {
133
133
  })();
134
134
  return [rawInput, rawOutput];
135
135
  }
136
+ /**
137
+ * Transforms a subscription procedure definition into a procedure that returns an output stream.
138
+ * Input messages can be pushed into the input stream.
139
+ * This should only be used for testing.
140
+ * @template State - The type of the state object.
141
+ * @template I - The type of the input object.
142
+ * @template O - The type of the output object.
143
+ * @param {State} state - The state object.
144
+ * @param {Procedure<State, 'stream', I, O>} proc - The procedure to handle the stream.
145
+ * @param {Omit<ServiceContext, 'state'>} [extendedContext] - The extended context object.
146
+ * @returns A function that when passed a message, returns the output stream.
147
+ */
148
+ export function asClientSubscription(state, proc, extendedContext) {
149
+ const rawOutput = pushable({
150
+ objectMode: true,
151
+ });
152
+ const transportOutput = pushable({
153
+ objectMode: true,
154
+ });
155
+ // unwrap from transport
156
+ (async () => {
157
+ for await (const transportRes of transportOutput) {
158
+ rawOutput.push(transportRes.payload);
159
+ }
160
+ })();
161
+ return async (msg) => {
162
+ proc
163
+ .handler({ ...extendedContext, state }, payloadToTransportMessage(msg), transportOutput)
164
+ .catch((err) => {
165
+ const errorMsg = err instanceof Error ? err.message : `[coerced to error] ${err}`;
166
+ return Err({
167
+ code: UNCAUGHT_ERROR,
168
+ message: errorMsg,
169
+ });
170
+ });
171
+ return rawOutput;
172
+ };
173
+ }
136
174
  /**
137
175
  * Converts a payload object to a transport message with reasonable defaults.
138
176
  * This should only be used for testing.
@@ -153,3 +191,11 @@ export function createDummyTransportMessage() {
153
191
  test: Math.random(),
154
192
  });
155
193
  }
194
+ /**
195
+ * Retrieves the next value from an async iterable iterator.
196
+ * @param iter The async iterable iterator.
197
+ * @returns A promise that resolves to the next value from the iterator.
198
+ */
199
+ export function iterNext(iter) {
200
+ return iter.next().then((res) => res.value);
201
+ }
package/package.json CHANGED
@@ -2,13 +2,13 @@
2
2
  "name": "@replit/river",
3
3
  "sideEffects": false,
4
4
  "description": "It's like tRPC but... with JSON Schema Support, duplex streaming and support for service multiplexing. Transport agnostic!",
5
- "version": "0.7.2",
5
+ "version": "0.8.0",
6
6
  "type": "module",
7
7
  "exports": {
8
8
  ".": "./dist/router/index.js",
9
9
  "./logging": "./dist/logging/index.js",
10
10
  "./codec": "./dist/codec/index.js",
11
- "./test-util": "./dist/testUtils.js",
11
+ "./test-util": "./dist/util/testHelpers.js",
12
12
  "./transport": "./dist/transport/index.js",
13
13
  "./transport/ws/client": "./dist/transport/impls/ws/client.js",
14
14
  "./transport/ws/server": "./dist/transport/impls/ws/server.js",
@@ -1 +0,0 @@
1
- {"version":3,"file":"fixtures.d.ts","sourceRoot":"","sources":["../../__tests__/fixtures.ts"],"names":[],"mappings":"AAKA,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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"testUtils.d.ts","sourceRoot":"","sources":["../testUtils.ts"],"names":[],"mappings":";AAAA,OAAO,SAAS,MAAM,eAAe,CAAC;AACtC,OAAO,EAAE,eAAe,EAAE,MAAM,IAAI,CAAC;AACrC,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,wBAAwB,EAAE,MAAM,6BAA6B,CAAC;AACvE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AACrD,OAAO,EACL,sBAAsB,EACtB,iBAAiB,EACjB,gBAAgB,EAGjB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,QAAQ,EAAY,MAAM,aAAa,CAAC;AACjD,OAAO,EAEL,MAAM,EACN,UAAU,EACV,mBAAmB,EAEpB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,OAAO,EAAE,wBAAwB,EAAE,MAAM,6BAA6B,CAAC;AAEvE;;;;;GAKG;AACH,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,4EAE9D;AAED;;;;;;GAMG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAWxE;AAED;;;;;GAKG;AACH,wBAAsB,0BAA0B,CAAC,IAAI,EAAE,MAAM,sBAI5D;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,eAAe,EACpB,KAAK,CAAC,EAAE,KAAK,GACZ,CAAC,wBAAwB,EAAE,wBAAwB,CAAC,CAWtD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CACzB,KAAK,SAAS,MAAM,GAAG,OAAO,EAC9B,CAAC,SAAS,OAAO,EACjB,CAAC,SAAS,OAAO,EACjB,CAAC,SAAS,UAAU,EAEpB,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,SAAS,CAAC,KAAK,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EACtC,eAAe,CAAC,EAAE,IAAI,CAAC,cAAc,EAAE,OAAO,CAAC,SAGxC,OAAO,CAAC,CAAC,KACb,QACD,OAAO,OAAO,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,GAAG,OAAO,0BAA0B,CAAC,CAAC,CAClE,CAYF;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAC5B,KAAK,SAAS,MAAM,GAAG,OAAO,EAC9B,CAAC,SAAS,OAAO,EACjB,CAAC,SAAS,OAAO,EACjB,CAAC,SAAS,UAAU,EAEpB,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,SAAS,CAAC,KAAK,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EACzC,eAAe,CAAC,EAAE,IAAI,CAAC,cAAc,EAAE,OAAO,CAAC,GAC9C;IACD,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IACnB,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,OAAO,mBAAmB,CAAC,CAAC,CAAC;CAC5E,CAuDA;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,SAAS,MAAM,EAC9D,OAAO,EAAE,OAAO,EAChB,QAAQ,CAAC,EAAE,MAAM,EACjB,IAAI,GAAE,iBAA4B,EAClC,EAAE,GAAE,iBAA4B,GAC/B,gBAAgB,CAAC,OAAO,CAAC,CAE3B;AAED;;;GAGG;AACH,wBAAgB,2BAA2B,IAAI,sBAAsB,CAKpE"}