starpc 0.49.18 → 0.50.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.
- package/README.md +1 -3
- package/cmd/protoc-gen-es-starpc/typescript.ts +11 -6
- package/dist/cmd/protoc-gen-es-starpc/typescript.js +6 -5
- package/dist/echo/echo.pb.d.ts +1 -1
- package/dist/echo/echo.pb.js +3 -3
- package/dist/integration/cross-language/ts-client.js +84 -11
- package/dist/integration/cross-language/ts-server.js +100 -4
- package/dist/mock/index.d.ts +2 -2
- package/dist/mock/index.js +2 -2
- package/dist/mock/mock.pb.d.ts +1 -1
- package/dist/mock/mock.pb.js +3 -3
- package/dist/rpcstream/receipt.test.d.ts +1 -0
- package/dist/rpcstream/receipt.test.js +41 -0
- package/dist/rpcstream/rpcstream.pb.d.ts +1 -1
- package/dist/rpcstream/rpcstream.pb.js +14 -8
- package/dist/srpc/call-receipt.d.ts +17 -0
- package/dist/srpc/call-receipt.js +106 -0
- package/dist/srpc/call-receipt.test.d.ts +1 -0
- package/dist/srpc/call-receipt.test.js +375 -0
- package/dist/srpc/client.d.ts +3 -1
- package/dist/srpc/client.js +20 -0
- package/dist/srpc/common-rpc.d.ts +13 -3
- package/dist/srpc/common-rpc.js +87 -10
- package/dist/srpc/handler.d.ts +2 -1
- package/dist/srpc/index.d.ts +4 -0
- package/dist/srpc/index.js +3 -0
- package/dist/srpc/invoker.d.ts +2 -1
- package/dist/srpc/invoker.js +2 -2
- package/dist/srpc/rpcproto.pb.d.ts +45 -1
- package/dist/srpc/rpcproto.pb.js +60 -7
- package/dist/srpc/server-invocation.d.ts +17 -0
- package/dist/srpc/server-invocation.js +37 -0
- package/dist/srpc/server-rpc.js +3 -1
- package/echo/echo.pb.go +1 -1
- package/echo/echo.pb.ts +6 -5
- package/echo/echo_srpc.pb.cpp +1 -1
- package/echo/echo_srpc.pb.go +1 -1
- package/echo/echo_srpc.pb.hpp +1 -1
- package/echo/echo_srpc.pb.rs +1 -1
- package/go.mod +13 -12
- package/go.sum +24 -24
- package/integration/cross-language/go-client/main.go +137 -5
- package/integration/cross-language/go-server/fixture-owner_test.go +127 -0
- package/integration/cross-language/go-server/main.go +169 -4
- package/integration/cross-language/run.bash +117 -13
- package/integration/cross-language/ts-client.ts +94 -13
- package/integration/cross-language/ts-server.ts +115 -7
- package/mock/index.ts +2 -2
- package/mock/mock.pb.go +1 -1
- package/mock/mock.pb.ts +6 -5
- package/mock/mock_srpc.pb.cpp +1 -1
- package/mock/mock_srpc.pb.go +1 -1
- package/mock/mock_srpc.pb.hpp +1 -1
- package/mock/mock_srpc.pb.rs +1 -1
- package/package.json +19 -26
- package/srpc/accept.go +1 -1
- package/srpc/call-receipt-e2e_test.go +111 -0
- package/srpc/call-receipt.go +112 -0
- package/srpc/call-receipt.test.ts +441 -0
- package/srpc/call-receipt.ts +131 -0
- package/srpc/call-receipt_test.go +536 -0
- package/srpc/client-rpc.go +1 -8
- package/srpc/client.ts +29 -2
- package/srpc/common-rpc.go +114 -29
- package/srpc/common-rpc.ts +104 -13
- package/srpc/handler.ts +2 -0
- package/srpc/index.ts +4 -0
- package/srpc/invoker.ts +10 -5
- package/srpc/msg-stream.go +8 -0
- package/srpc/muxed-conn.go +9 -4
- package/srpc/muxed-conn_test.go +79 -0
- package/srpc/muxed-yamux.go +1 -1
- package/srpc/rpcproto.pb.cc +15 -4
- package/srpc/rpcproto.pb.go +97 -1
- package/srpc/rpcproto.pb.h +59 -0
- package/srpc/rpcproto.pb.rs +45 -0
- package/srpc/rpcproto.pb.ts +90 -27
- package/srpc/rpcproto.proto +16 -0
- package/srpc/schema-ownership_test.go +115 -0
- package/srpc/server-invocation.go +40 -0
- package/srpc/server-invocation.ts +76 -0
- package/srpc/server-rpc.go +1 -1
- package/srpc/server-rpc.ts +6 -2
- package/srpc/stream-yamux.go +1 -1
- package/srpc/stream.go +20 -0
- package/srpc/websocket.go +1 -1
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/// <reference lib="es2024.promise" />
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { pushable } from 'it-pushable';
|
|
4
|
+
import { EchoMsg } from '../echo/echo.pb.js';
|
|
5
|
+
import { Client } from './client.js';
|
|
6
|
+
import { ClientRPC } from './client-rpc.js';
|
|
7
|
+
import { Server } from './server.js';
|
|
8
|
+
import { CallReceipt } from './call-receipt.js';
|
|
9
|
+
import { Packet, TerminalKind } from './rpcproto.pb.js';
|
|
10
|
+
import { ServerRPC } from './server-rpc.js';
|
|
11
|
+
const response = new Uint8Array([1, 2, 3]);
|
|
12
|
+
describe('generated service compatibility', () => {
|
|
13
|
+
it('accepts plain abort signals and terminal-capable handlers', async () => {
|
|
14
|
+
const server = {
|
|
15
|
+
Echo: async (request, invocation) => {
|
|
16
|
+
void invocation;
|
|
17
|
+
return request;
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
const request = EchoMsg.create({ body: 'compatibility' });
|
|
21
|
+
const signal = new AbortController().signal;
|
|
22
|
+
await expect(server.Echo(request, signal)).resolves.toEqual(request);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
describe('held unary receipt', () => {
|
|
26
|
+
it('waits for a server completion acknowledgment', async () => {
|
|
27
|
+
const sent = [];
|
|
28
|
+
const terminal = Promise.withResolvers();
|
|
29
|
+
const client = new Client(async () => ({
|
|
30
|
+
source: responseSource(terminal.promise),
|
|
31
|
+
sink: async (source) => {
|
|
32
|
+
for await (const data of source) {
|
|
33
|
+
const packet = Packet.fromBinary(data);
|
|
34
|
+
sent.push(packet);
|
|
35
|
+
if (packet.body?.case === 'callData' && packet.body.value.complete) {
|
|
36
|
+
terminal.resolve();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
}));
|
|
41
|
+
const held = await client.requestWithReceipt('test.Service', 'Unary', response);
|
|
42
|
+
const commit = held.receipt.commit();
|
|
43
|
+
expect(held.receipt.settled).toBe(true);
|
|
44
|
+
await expect(commit).resolves.toBeUndefined();
|
|
45
|
+
const terminalPackets = sent.filter((packet) => {
|
|
46
|
+
const body = packet.body;
|
|
47
|
+
return (body?.case === 'callCancel' ||
|
|
48
|
+
(body?.case === 'callData' && body.value.complete));
|
|
49
|
+
});
|
|
50
|
+
expect(terminalPackets).toHaveLength(1);
|
|
51
|
+
expect(terminalPackets[0]?.body?.case).toBe('callData');
|
|
52
|
+
await expect(held.receipt.done).resolves.toBeUndefined();
|
|
53
|
+
});
|
|
54
|
+
it('commits through a real TypeScript server', async () => {
|
|
55
|
+
const terminal = Promise.withResolvers();
|
|
56
|
+
const server = new Server(async () => {
|
|
57
|
+
return async (_source, dataSink, invocation) => {
|
|
58
|
+
await dataSink((async function* () {
|
|
59
|
+
yield response;
|
|
60
|
+
if (!invocation) {
|
|
61
|
+
throw new Error('missing invocation');
|
|
62
|
+
}
|
|
63
|
+
terminal.resolve(await invocation.waitTerminal(new AbortController().signal));
|
|
64
|
+
})());
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
const client = new Client(async () => {
|
|
68
|
+
const clientToServer = pushable({ objectMode: true });
|
|
69
|
+
const serverToClient = pushable({ objectMode: true });
|
|
70
|
+
server.handlePacketStream({
|
|
71
|
+
source: clientToServer,
|
|
72
|
+
sink: async (source) => {
|
|
73
|
+
for await (const packet of source) {
|
|
74
|
+
serverToClient.push(packet);
|
|
75
|
+
}
|
|
76
|
+
serverToClient.end();
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
return {
|
|
80
|
+
source: serverToClient,
|
|
81
|
+
sink: async (source) => {
|
|
82
|
+
for await (const packet of source) {
|
|
83
|
+
clientToServer.push(packet);
|
|
84
|
+
}
|
|
85
|
+
clientToServer.end();
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
const held = await client.requestWithReceipt('test.Service', 'Unary', response);
|
|
90
|
+
await expect(held.receipt.commit()).resolves.toBeUndefined();
|
|
91
|
+
await expect(terminal.promise).resolves.toBe(TerminalKind.COMMITTED);
|
|
92
|
+
});
|
|
93
|
+
it('rejects commit after a bare remote close', async () => {
|
|
94
|
+
const call = new ClientRPC('test.Service', 'Unary');
|
|
95
|
+
const iterator = call.rpcDataSource[Symbol.asyncIterator]();
|
|
96
|
+
await call.handleCallData({
|
|
97
|
+
data: response,
|
|
98
|
+
dataIsZero: false,
|
|
99
|
+
complete: false,
|
|
100
|
+
error: '',
|
|
101
|
+
});
|
|
102
|
+
const first = await iterator.next();
|
|
103
|
+
expect(first.done).toBe(false);
|
|
104
|
+
const receipt = new CallReceipt(call, iterator);
|
|
105
|
+
await call.close();
|
|
106
|
+
await expect(receipt.commit()).rejects.toThrow();
|
|
107
|
+
await expect(receipt.done).rejects.toThrow('receipt closed before commit');
|
|
108
|
+
});
|
|
109
|
+
it('aborts a held receipt with one cancel packet', async () => {
|
|
110
|
+
const sent = [];
|
|
111
|
+
const terminal = Promise.withResolvers();
|
|
112
|
+
const cancelSeen = Promise.withResolvers();
|
|
113
|
+
const client = new Client(async () => ({
|
|
114
|
+
source: responseSource(terminal.promise),
|
|
115
|
+
sink: async (source) => {
|
|
116
|
+
for await (const data of source) {
|
|
117
|
+
const packet = Packet.fromBinary(data);
|
|
118
|
+
sent.push(packet);
|
|
119
|
+
if (packet.body?.case === 'callCancel') {
|
|
120
|
+
cancelSeen.resolve();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
}));
|
|
125
|
+
const held = await client.requestWithReceipt('test.Service', 'Unary', response);
|
|
126
|
+
await held.receipt.abort();
|
|
127
|
+
await cancelSeen.promise;
|
|
128
|
+
const terminalPackets = sent.filter((packet) => packet.body?.case === 'callCancel');
|
|
129
|
+
expect(terminalPackets).toHaveLength(1);
|
|
130
|
+
await expect(held.receipt.done).rejects.toThrow();
|
|
131
|
+
});
|
|
132
|
+
it('allows exactly one concurrent commit or abort terminal', async () => {
|
|
133
|
+
const sent = [];
|
|
134
|
+
const terminal = Promise.withResolvers();
|
|
135
|
+
const client = new Client(async () => ({
|
|
136
|
+
source: responseSource(terminal.promise),
|
|
137
|
+
sink: async (source) => {
|
|
138
|
+
for await (const data of source) {
|
|
139
|
+
const packet = Packet.fromBinary(data);
|
|
140
|
+
sent.push(packet);
|
|
141
|
+
if (packet.body?.case === 'callData' && packet.body.value.complete) {
|
|
142
|
+
terminal.resolve();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
}));
|
|
147
|
+
const held = await client.requestWithReceipt('test.Service', 'Unary', response);
|
|
148
|
+
const results = await Promise.allSettled([
|
|
149
|
+
held.receipt.commit(),
|
|
150
|
+
held.receipt.abort(),
|
|
151
|
+
]);
|
|
152
|
+
const terminalPackets = sent.filter((packet) => {
|
|
153
|
+
const body = packet.body;
|
|
154
|
+
return (body?.case === 'callCancel' ||
|
|
155
|
+
(body?.case === 'callData' && body.value.complete));
|
|
156
|
+
});
|
|
157
|
+
expect(terminalPackets).toHaveLength(1);
|
|
158
|
+
expect(results).toHaveLength(2);
|
|
159
|
+
});
|
|
160
|
+
it('rejects a trailing response datum instead of committing', async () => {
|
|
161
|
+
const terminal = Promise.withResolvers();
|
|
162
|
+
const client = new Client(async () => ({
|
|
163
|
+
source: (async function* () {
|
|
164
|
+
yield Packet.toBinary({
|
|
165
|
+
body: {
|
|
166
|
+
case: 'callData',
|
|
167
|
+
value: {
|
|
168
|
+
data: response,
|
|
169
|
+
dataIsZero: false,
|
|
170
|
+
complete: false,
|
|
171
|
+
error: '',
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
await terminal.promise;
|
|
176
|
+
yield Packet.toBinary({
|
|
177
|
+
body: {
|
|
178
|
+
case: 'callData',
|
|
179
|
+
value: {
|
|
180
|
+
data: new Uint8Array([9]),
|
|
181
|
+
dataIsZero: false,
|
|
182
|
+
complete: false,
|
|
183
|
+
error: '',
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
})(),
|
|
188
|
+
sink: async (source) => {
|
|
189
|
+
for await (const data of source) {
|
|
190
|
+
const packet = Packet.fromBinary(data);
|
|
191
|
+
if (packet.body?.case === 'callData' && packet.body.value.complete) {
|
|
192
|
+
terminal.resolve();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
}));
|
|
197
|
+
const held = await client.requestWithReceipt('test.Service', 'Unary', response);
|
|
198
|
+
await expect(held.receipt.commit()).rejects.toThrow('unexpected trailing response data');
|
|
199
|
+
});
|
|
200
|
+
it('rejects done on a remote terminal error', async () => {
|
|
201
|
+
const client = new Client(async () => ({
|
|
202
|
+
source: (async function* () {
|
|
203
|
+
yield Packet.toBinary({
|
|
204
|
+
body: {
|
|
205
|
+
case: 'callData',
|
|
206
|
+
value: {
|
|
207
|
+
data: response,
|
|
208
|
+
dataIsZero: false,
|
|
209
|
+
complete: false,
|
|
210
|
+
error: '',
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
yield Packet.toBinary({
|
|
215
|
+
body: {
|
|
216
|
+
case: 'callData',
|
|
217
|
+
value: {
|
|
218
|
+
data: new Uint8Array(),
|
|
219
|
+
dataIsZero: false,
|
|
220
|
+
complete: true,
|
|
221
|
+
error: 'remote failure',
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
})(),
|
|
226
|
+
sink: async (source) => {
|
|
227
|
+
for await (const _data of source) {
|
|
228
|
+
void _data;
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
}));
|
|
232
|
+
const held = await client.requestWithReceipt('test.Service', 'Unary', response);
|
|
233
|
+
await expect(held.receipt.done).rejects.toThrow('remote failure');
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
describe('server invocation terminal', () => {
|
|
237
|
+
it.each([
|
|
238
|
+
['explicit completion', TerminalKind.COMMITTED],
|
|
239
|
+
['cancel', TerminalKind.CANCELED],
|
|
240
|
+
['transport loss', TerminalKind.TRANSPORT_LOST],
|
|
241
|
+
['remote error packet', TerminalKind.TRANSPORT_LOST],
|
|
242
|
+
['bare close', TerminalKind.CLOSED],
|
|
243
|
+
['remote error packet with completion', TerminalKind.TRANSPORT_LOST],
|
|
244
|
+
])('%s is classified distinctly', async (_name, expected) => {
|
|
245
|
+
const captured = Promise.withResolvers();
|
|
246
|
+
const owner = new AbortController();
|
|
247
|
+
const observed = Promise.withResolvers();
|
|
248
|
+
const rpc = new ServerRPC(async () => {
|
|
249
|
+
return async (_source, _sink, invocation) => {
|
|
250
|
+
if (!invocation) {
|
|
251
|
+
throw new Error('missing invocation');
|
|
252
|
+
}
|
|
253
|
+
captured.resolve(invocation);
|
|
254
|
+
observed.resolve(await invocation.waitTerminal(owner.signal));
|
|
255
|
+
};
|
|
256
|
+
});
|
|
257
|
+
await rpc.handleCallStart({
|
|
258
|
+
rpcService: 'test.Service',
|
|
259
|
+
rpcMethod: 'Unary',
|
|
260
|
+
data: new Uint8Array(),
|
|
261
|
+
dataIsZero: true,
|
|
262
|
+
});
|
|
263
|
+
const invocation = await captured.promise;
|
|
264
|
+
if (expected === TerminalKind.COMMITTED) {
|
|
265
|
+
await rpc.handleCallData({ complete: true });
|
|
266
|
+
}
|
|
267
|
+
else if (expected === TerminalKind.CANCELED) {
|
|
268
|
+
await rpc.handleCallCancel();
|
|
269
|
+
}
|
|
270
|
+
else if (expected === TerminalKind.TRANSPORT_LOST) {
|
|
271
|
+
if (_name === 'remote error packet' ||
|
|
272
|
+
_name === 'remote error packet with completion') {
|
|
273
|
+
await rpc.handleCallData({
|
|
274
|
+
complete: _name === 'remote error packet with completion',
|
|
275
|
+
error: 'remote failure',
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
await rpc.close(new Error('transport loss'));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
await rpc.close();
|
|
284
|
+
}
|
|
285
|
+
await expect(observed.promise).resolves.toBe(expected);
|
|
286
|
+
owner.abort();
|
|
287
|
+
if (_name === 'remote error packet with completion') {
|
|
288
|
+
const rpcState = rpc;
|
|
289
|
+
if (!('remoteCompleted' in rpcState)) {
|
|
290
|
+
throw new Error('remote completion discriminator is missing');
|
|
291
|
+
}
|
|
292
|
+
expect(rpcState.remoteCompleted).toBe(false);
|
|
293
|
+
}
|
|
294
|
+
if (expected === TerminalKind.CLOSED ||
|
|
295
|
+
expected === TerminalKind.TRANSPORT_LOST) {
|
|
296
|
+
expect(invocation.signal.aborted).toBe(true);
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
it.each(['transport loss', 'cancel'])('keeps completion after %s', async (followUp) => {
|
|
300
|
+
const captured = Promise.withResolvers();
|
|
301
|
+
const owner = new AbortController();
|
|
302
|
+
const rpc = new ServerRPC(async () => {
|
|
303
|
+
return async (_source, _sink, invocation) => {
|
|
304
|
+
if (!invocation) {
|
|
305
|
+
throw new Error('missing invocation');
|
|
306
|
+
}
|
|
307
|
+
captured.resolve(invocation);
|
|
308
|
+
await invocation.waitTerminal(owner.signal);
|
|
309
|
+
};
|
|
310
|
+
});
|
|
311
|
+
await rpc.handleCallStart({
|
|
312
|
+
rpcService: 'test.Service',
|
|
313
|
+
rpcMethod: 'Unary',
|
|
314
|
+
data: new Uint8Array(),
|
|
315
|
+
dataIsZero: true,
|
|
316
|
+
});
|
|
317
|
+
const invocation = await captured.promise;
|
|
318
|
+
await rpc.handleCallData({ complete: true });
|
|
319
|
+
if (followUp === 'transport loss') {
|
|
320
|
+
await rpc.close(new Error('transport loss'));
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
await rpc.handleCallCancel();
|
|
324
|
+
}
|
|
325
|
+
await expect(invocation.waitTerminal(owner.signal)).resolves.toBe(TerminalKind.COMMITTED);
|
|
326
|
+
owner.abort();
|
|
327
|
+
});
|
|
328
|
+
it('returns abandoned only for owner cancellation without a remote terminal', async () => {
|
|
329
|
+
const captured = Promise.withResolvers();
|
|
330
|
+
const owner = new AbortController();
|
|
331
|
+
const rpc = new ServerRPC(async () => {
|
|
332
|
+
return async (_source, _sink, invocation) => {
|
|
333
|
+
if (!invocation) {
|
|
334
|
+
throw new Error('missing invocation');
|
|
335
|
+
}
|
|
336
|
+
captured.resolve(invocation);
|
|
337
|
+
await invocation.waitTerminal(owner.signal);
|
|
338
|
+
};
|
|
339
|
+
});
|
|
340
|
+
await rpc.handleCallStart({
|
|
341
|
+
rpcService: 'test.Service',
|
|
342
|
+
rpcMethod: 'Unary',
|
|
343
|
+
data: new Uint8Array(),
|
|
344
|
+
dataIsZero: true,
|
|
345
|
+
});
|
|
346
|
+
const invocation = await captured.promise;
|
|
347
|
+
owner.abort();
|
|
348
|
+
await expect(invocation.waitTerminal(owner.signal)).resolves.toBe(TerminalKind.ABANDONED);
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
async function* responseSource(terminal) {
|
|
352
|
+
yield Packet.toBinary({
|
|
353
|
+
body: {
|
|
354
|
+
case: 'callData',
|
|
355
|
+
value: {
|
|
356
|
+
data: response,
|
|
357
|
+
dataIsZero: false,
|
|
358
|
+
complete: false,
|
|
359
|
+
error: '',
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
await terminal;
|
|
364
|
+
yield Packet.toBinary({
|
|
365
|
+
body: {
|
|
366
|
+
case: 'callData',
|
|
367
|
+
value: {
|
|
368
|
+
data: new Uint8Array(),
|
|
369
|
+
dataIsZero: false,
|
|
370
|
+
complete: true,
|
|
371
|
+
error: '',
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
}
|
package/dist/srpc/client.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { ProtoRpc } from './proto-rpc.js';
|
|
2
2
|
import type { OpenStreamFunc } from './stream.js';
|
|
3
|
-
|
|
3
|
+
import type { HeldCall, ReceiptRpc } from './call-receipt.js';
|
|
4
|
+
export declare class Client implements ProtoRpc, ReceiptRpc {
|
|
4
5
|
private openStreamCtr;
|
|
5
6
|
constructor(openStreamFn?: OpenStreamFunc);
|
|
6
7
|
setOpenStreamFn(openStreamFn?: OpenStreamFunc): void;
|
|
7
8
|
request(service: string, method: string, data: Uint8Array, abortSignal?: AbortSignal): Promise<Uint8Array>;
|
|
9
|
+
requestWithReceipt(service: string, method: string, data: Uint8Array, abortSignal?: AbortSignal): Promise<HeldCall>;
|
|
8
10
|
clientStreamingRequest(service: string, method: string, data: AsyncIterable<Uint8Array>, abortSignal?: AbortSignal): Promise<Uint8Array>;
|
|
9
11
|
serverStreamingRequest(service: string, method: string, data: Uint8Array, abortSignal?: AbortSignal): AsyncIterable<Uint8Array>;
|
|
10
12
|
bidirectionalStreamingRequest(service: string, method: string, data: AsyncIterable<Uint8Array>, abortSignal?: AbortSignal): AsyncIterable<Uint8Array>;
|
package/dist/srpc/client.js
CHANGED
|
@@ -5,6 +5,7 @@ import { ClientRPC } from './client-rpc.js';
|
|
|
5
5
|
import { writeToPushable } from './pushable.js';
|
|
6
6
|
import { decodePacketSource, encodePacketSource } from './packet.js';
|
|
7
7
|
import { OpenStreamCtr } from './open-stream-ctr.js';
|
|
8
|
+
import { CallReceipt } from './call-receipt.js';
|
|
8
9
|
// Client implements the ts-proto Rpc interface with the drpcproto protocol.
|
|
9
10
|
export class Client {
|
|
10
11
|
// openStreamCtr contains the OpenStreamFunc.
|
|
@@ -27,6 +28,25 @@ export class Client {
|
|
|
27
28
|
call.close(err);
|
|
28
29
|
throw err;
|
|
29
30
|
}
|
|
31
|
+
// requestWithReceipt reads one response and retains the call terminal.
|
|
32
|
+
async requestWithReceipt(service, method, data, abortSignal) {
|
|
33
|
+
const call = await this.startRpc(service, method, data, abortSignal);
|
|
34
|
+
const iterator = call.rpcDataSource[Symbol.asyncIterator]();
|
|
35
|
+
try {
|
|
36
|
+
const result = await iterator.next();
|
|
37
|
+
if (result.done) {
|
|
38
|
+
throw new Error('empty response');
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
response: result.value,
|
|
42
|
+
receipt: new CallReceipt(call, iterator),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
await call.close(err instanceof Error ? err : new Error('receipt read failed'));
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
30
50
|
// clientStreamingRequest starts a client side streaming request.
|
|
31
51
|
async clientStreamingRequest(service, method, data, abortSignal) {
|
|
32
52
|
const call = await this.startRpc(service, method, null, abortSignal);
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { Sink, Source } from 'it-stream-types';
|
|
2
|
-
import type
|
|
3
|
-
import { Packet } from './rpcproto.pb.js';
|
|
2
|
+
import { Packet, TerminalKind, type CallData, type CallStart } from './rpcproto.pb.js';
|
|
4
3
|
export declare class CommonRPC {
|
|
5
4
|
readonly sink: Sink<Source<Packet>>;
|
|
6
5
|
readonly source: AsyncIterable<Packet>;
|
|
@@ -10,12 +9,23 @@ export declare class CommonRPC {
|
|
|
10
9
|
protected service?: string;
|
|
11
10
|
protected method?: string;
|
|
12
11
|
private closed?;
|
|
12
|
+
private remoteCompleted;
|
|
13
|
+
private remoteError?;
|
|
14
|
+
private remoteSourceClosed;
|
|
15
|
+
private remoteTerminal?;
|
|
16
|
+
private readonly invocationController;
|
|
17
|
+
private readonly terminalPromise;
|
|
18
|
+
private resolveTerminal;
|
|
13
19
|
private readonly writeDrainAbort;
|
|
14
20
|
constructor();
|
|
15
21
|
get isClosed(): boolean | Error;
|
|
22
|
+
protected get invocationSignal(): AbortSignal;
|
|
23
|
+
protected waitTerminal(ownerSignal: AbortSignal): Promise<TerminalKind>;
|
|
24
|
+
getTerminalKind(): TerminalKind | undefined;
|
|
25
|
+
private recordRemoteTerminal;
|
|
16
26
|
writeCallData(data?: Uint8Array, complete?: boolean, error?: string): Promise<void>;
|
|
17
27
|
private writeCallDataPacket;
|
|
18
|
-
writeCallCancel(): Promise<void>;
|
|
28
|
+
writeCallCancel(waitForDrain?: boolean): Promise<void>;
|
|
19
29
|
writeCallDataFromSource(dataSource: AsyncIterable<Uint8Array>): Promise<void>;
|
|
20
30
|
protected writePacket(packet: Packet, options?: WritePacketOptions): Promise<void>;
|
|
21
31
|
handleMessage(message: Uint8Array): Promise<void>;
|
package/dist/srpc/common-rpc.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { pushable } from 'it-pushable';
|
|
2
|
-
import { Packet } from './rpcproto.pb.js';
|
|
2
|
+
import { Packet, TerminalKind, } from './rpcproto.pb.js';
|
|
3
3
|
import { ERR_RPC_ABORT, RemoteRPCError } from './errors.js';
|
|
4
4
|
const maxBufferedOutgoingPackets = 1;
|
|
5
5
|
// CommonRPC is common logic between server and client RPCs.
|
|
@@ -24,9 +24,25 @@ export class CommonRPC {
|
|
|
24
24
|
method;
|
|
25
25
|
// closed indicates this rpc has been closed already.
|
|
26
26
|
closed;
|
|
27
|
+
// remoteCompleted is set only by an explicit remote CallData completion.
|
|
28
|
+
remoteCompleted = false;
|
|
29
|
+
// remoteError records a remote error or transport failure.
|
|
30
|
+
remoteError;
|
|
31
|
+
// remoteSourceClosed records an incoming source ending without a packet error.
|
|
32
|
+
remoteSourceClosed = false;
|
|
33
|
+
// remoteTerminal is the first valid remote terminal.
|
|
34
|
+
remoteTerminal;
|
|
35
|
+
// invocationController cancels the server invocation on a remote terminal.
|
|
36
|
+
invocationController = new AbortController();
|
|
37
|
+
// terminalPromise resolves when a remote terminal is recorded.
|
|
38
|
+
terminalPromise;
|
|
39
|
+
resolveTerminal;
|
|
27
40
|
// writeDrainAbort wakes writers waiting for outbound stream drain on close.
|
|
28
41
|
writeDrainAbort = new AbortController();
|
|
29
42
|
constructor() {
|
|
43
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
44
|
+
this.terminalPromise = promise;
|
|
45
|
+
this.resolveTerminal = resolve;
|
|
30
46
|
this.sink = this._createSink();
|
|
31
47
|
this.source = this._source;
|
|
32
48
|
this.rpcDataSource = this._rpcDataSource;
|
|
@@ -35,6 +51,49 @@ export class CommonRPC {
|
|
|
35
51
|
get isClosed() {
|
|
36
52
|
return this.closed ?? false;
|
|
37
53
|
}
|
|
54
|
+
// invocationSignal is canceled when the RPC reaches a terminal.
|
|
55
|
+
get invocationSignal() {
|
|
56
|
+
return this.invocationController.signal;
|
|
57
|
+
}
|
|
58
|
+
// waitTerminal waits for the remote terminal or external owner cancellation.
|
|
59
|
+
async waitTerminal(ownerSignal) {
|
|
60
|
+
const { promise: ownerDone, resolve: resolveOwnerDone } = Promise.withResolvers();
|
|
61
|
+
const onAbort = () => resolveOwnerDone();
|
|
62
|
+
ownerSignal.addEventListener('abort', onAbort, { once: true });
|
|
63
|
+
let ownerAborted = ownerSignal.aborted;
|
|
64
|
+
try {
|
|
65
|
+
for (;;) {
|
|
66
|
+
const terminal = this.getTerminalKind();
|
|
67
|
+
if (terminal !== undefined) {
|
|
68
|
+
if (terminal === TerminalKind.CLOSED &&
|
|
69
|
+
this.remoteSourceClosed &&
|
|
70
|
+
!this.closed) {
|
|
71
|
+
await this.close();
|
|
72
|
+
}
|
|
73
|
+
return terminal;
|
|
74
|
+
}
|
|
75
|
+
if (ownerAborted) {
|
|
76
|
+
return TerminalKind.ABANDONED;
|
|
77
|
+
}
|
|
78
|
+
await Promise.race([this.terminalPromise, ownerDone]);
|
|
79
|
+
ownerAborted = ownerSignal.aborted;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
ownerSignal.removeEventListener('abort', onAbort);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// getTerminalKind returns the observed remote terminal, if any.
|
|
87
|
+
getTerminalKind() {
|
|
88
|
+
return this.remoteTerminal;
|
|
89
|
+
}
|
|
90
|
+
recordRemoteTerminal(kind) {
|
|
91
|
+
if (this.remoteTerminal !== undefined) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
this.remoteTerminal = kind;
|
|
95
|
+
this.resolveTerminal();
|
|
96
|
+
}
|
|
38
97
|
// writeCallData writes the call data packet.
|
|
39
98
|
async writeCallData(data, complete, error) {
|
|
40
99
|
await this.writeCallDataPacket(data, complete, error);
|
|
@@ -55,13 +114,16 @@ export class CommonRPC {
|
|
|
55
114
|
}, writeOptions);
|
|
56
115
|
}
|
|
57
116
|
// writeCallCancel writes the call cancel packet.
|
|
58
|
-
async writeCallCancel() {
|
|
117
|
+
async writeCallCancel(waitForDrain = false) {
|
|
59
118
|
await this.writePacket({
|
|
60
119
|
body: {
|
|
61
120
|
case: 'callCancel',
|
|
62
121
|
value: true,
|
|
63
122
|
},
|
|
64
123
|
}, { waitForDrain: false });
|
|
124
|
+
if (waitForDrain) {
|
|
125
|
+
await this._source.onEmpty({ signal: this.writeDrainAbort.signal });
|
|
126
|
+
}
|
|
65
127
|
}
|
|
66
128
|
// writeCallDataFromSource writes all call data from the iterable.
|
|
67
129
|
async writeCallDataFromSource(dataSource) {
|
|
@@ -153,16 +215,27 @@ export class CommonRPC {
|
|
|
153
215
|
throw new Error('call start must be sent before call data');
|
|
154
216
|
}
|
|
155
217
|
this.pushRpcData(packet.data, packet.dataIsZero);
|
|
156
|
-
|
|
157
|
-
|
|
218
|
+
const remoteError = packet.error
|
|
219
|
+
? new RemoteRPCError(this.service, this.method, packet.error)
|
|
220
|
+
: undefined;
|
|
221
|
+
if (remoteError) {
|
|
222
|
+
this.remoteError ??= remoteError;
|
|
223
|
+
this.invocationController.abort();
|
|
224
|
+
this.recordRemoteTerminal(TerminalKind.TRANSPORT_LOST);
|
|
158
225
|
}
|
|
159
|
-
|
|
160
|
-
this.
|
|
226
|
+
if (packet.complete && !remoteError) {
|
|
227
|
+
this.remoteCompleted = true;
|
|
228
|
+
this.recordRemoteTerminal(TerminalKind.COMMITTED);
|
|
229
|
+
this._rpcDataSource.end(remoteError);
|
|
230
|
+
}
|
|
231
|
+
else if (remoteError) {
|
|
232
|
+
this._rpcDataSource.end(remoteError);
|
|
161
233
|
}
|
|
162
234
|
}
|
|
163
235
|
// handleCallCancel handles a CallCancel packet.
|
|
164
236
|
async handleCallCancel() {
|
|
165
|
-
this.
|
|
237
|
+
this.recordRemoteTerminal(TerminalKind.CANCELED);
|
|
238
|
+
await this.close(new Error(ERR_RPC_ABORT));
|
|
166
239
|
}
|
|
167
240
|
// close closes the call, optionally with an error.
|
|
168
241
|
async close(err) {
|
|
@@ -170,6 +243,11 @@ export class CommonRPC {
|
|
|
170
243
|
return;
|
|
171
244
|
}
|
|
172
245
|
this.closed = err ?? true;
|
|
246
|
+
if (!this.remoteError && err) {
|
|
247
|
+
this.remoteError = err;
|
|
248
|
+
}
|
|
249
|
+
this.recordRemoteTerminal(err ? TerminalKind.TRANSPORT_LOST : TerminalKind.CLOSED);
|
|
250
|
+
this.invocationController.abort();
|
|
173
251
|
// note: this does nothing if _source is already ended.
|
|
174
252
|
if (err && err.message) {
|
|
175
253
|
await this.writeCallDataPacket(undefined, true, err.message, {
|
|
@@ -181,22 +259,21 @@ export class CommonRPC {
|
|
|
181
259
|
this._source.end();
|
|
182
260
|
this._rpcDataSource.end(err);
|
|
183
261
|
}
|
|
184
|
-
// _createSink returns a value for the sink field.
|
|
185
262
|
_createSink() {
|
|
186
263
|
return async (source) => {
|
|
187
264
|
try {
|
|
188
265
|
if (Symbol.asyncIterator in source) {
|
|
189
|
-
// Handle async source
|
|
190
266
|
for await (const msg of source) {
|
|
191
267
|
await this.handlePacket(msg);
|
|
192
268
|
}
|
|
193
269
|
}
|
|
194
270
|
else {
|
|
195
|
-
// Handle sync source
|
|
196
271
|
for (const msg of source) {
|
|
197
272
|
await this.handlePacket(msg);
|
|
198
273
|
}
|
|
199
274
|
}
|
|
275
|
+
this.remoteSourceClosed = true;
|
|
276
|
+
this.recordRemoteTerminal(TerminalKind.CLOSED);
|
|
200
277
|
}
|
|
201
278
|
catch (err) {
|
|
202
279
|
this.close(err);
|
package/dist/srpc/handler.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Sink, Source } from 'it-stream-types';
|
|
2
2
|
import { ServiceDefinition, ServiceMethodDefinitions } from './definition.js';
|
|
3
|
-
|
|
3
|
+
import type { ServerInvocation } from './server-invocation.js';
|
|
4
|
+
export type InvokeFn = (dataSource: Source<Uint8Array>, dataSink: Sink<Source<Uint8Array>>, invocation?: ServerInvocation) => Promise<void>;
|
|
4
5
|
export interface Handler {
|
|
5
6
|
getServiceID(): string;
|
|
6
7
|
getMethodIDs(): string[];
|
package/dist/srpc/index.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
export { ERR_RPC_ABORT, isAbortError, ERR_STREAM_IDLE, isStreamIdleError, castToError, } from './errors.js';
|
|
2
2
|
export { Client } from './client.js';
|
|
3
|
+
export { CallReceipt } from './call-receipt.js';
|
|
4
|
+
export type { HeldCall, ReceiptRpc } from './call-receipt.js';
|
|
5
|
+
export { ServerInvocation } from './server-invocation.js';
|
|
6
|
+
export { TerminalKind } from './rpcproto.pb.js';
|
|
3
7
|
export { Server } from './server.js';
|
|
4
8
|
export { StreamConn } from './conn.js';
|
|
5
9
|
export type { StreamConnParams, StreamHandler } from './conn.js';
|
package/dist/srpc/index.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export { ERR_RPC_ABORT, isAbortError, ERR_STREAM_IDLE, isStreamIdleError, castToError, } from './errors.js';
|
|
2
2
|
export { Client } from './client.js';
|
|
3
|
+
export { CallReceipt } from './call-receipt.js';
|
|
4
|
+
export { ServerInvocation } from './server-invocation.js';
|
|
5
|
+
export { TerminalKind } from './rpcproto.pb.js';
|
|
3
6
|
export { Server } from './server.js';
|
|
4
7
|
export { StreamConn } from './conn.js';
|
|
5
8
|
export { WebSocketConn } from './websocket.js';
|
package/dist/srpc/invoker.d.ts
CHANGED
|
@@ -2,5 +2,6 @@ import type { MethodDefinition } from './definition.js';
|
|
|
2
2
|
import { InvokeFn } from './handler.js';
|
|
3
3
|
import type { MessageType, Message } from '@aptre/protobuf-es-lite';
|
|
4
4
|
import { MethodIdempotency, MethodKind } from '@aptre/protobuf-es-lite';
|
|
5
|
-
|
|
5
|
+
import type { ServerInvocation } from './server-invocation.js';
|
|
6
|
+
export type MethodProto<R extends Message<R>, O extends Message<O>> = ((request: R, invocation?: ServerInvocation) => Promise<O>) | ((request: R, invocation?: ServerInvocation) => AsyncIterable<O>) | ((request: AsyncIterable<R>, invocation?: ServerInvocation) => Promise<O>) | ((request: AsyncIterable<R>, invocation?: ServerInvocation) => AsyncIterable<O>);
|
|
6
7
|
export declare function createInvokeFn<R extends Message<R>, O extends Message<O>>(methodInfo: MethodDefinition<MessageType<R>, MessageType<O>, MethodKind, MethodIdempotency | undefined>, methodProto: MethodProto<R, O>): InvokeFn;
|