starpc 0.50.0 → 0.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/cmd/protoc-gen-es-starpc/typescript.ts +37 -0
  2. package/dist/cmd/protoc-gen-es-starpc/typescript.js +24 -0
  3. package/dist/echo/echo_srpc.pb.d.ts +44 -1
  4. package/dist/echo/server.d.ts +9 -8
  5. package/dist/echo/server.js +6 -6
  6. package/dist/integration/cross-language/ts-client.js +5 -79
  7. package/dist/integration/cross-language/ts-server.js +4 -100
  8. package/dist/mock/mock_srpc.pb.d.ts +14 -1
  9. package/dist/srpc/channel.js +6 -3
  10. package/dist/srpc/channel.test.js +20 -1
  11. package/dist/srpc/client.d.ts +1 -3
  12. package/dist/srpc/client.js +2 -22
  13. package/dist/srpc/common-rpc.d.ts +1 -9
  14. package/dist/srpc/common-rpc.js +4 -62
  15. package/dist/srpc/handler.d.ts +12 -4
  16. package/dist/srpc/index.d.ts +2 -4
  17. package/dist/srpc/index.js +1 -3
  18. package/dist/srpc/invoker.d.ts +2 -2
  19. package/dist/srpc/invoker.js +2 -2
  20. package/dist/srpc/rpcproto.pb.d.ts +0 -44
  21. package/dist/srpc/rpcproto.pb.js +0 -53
  22. package/dist/srpc/server-context.d.ts +11 -0
  23. package/dist/srpc/server-context.js +28 -0
  24. package/dist/srpc/server-rpc.js +3 -3
  25. package/dist/srpc/server.test.js +65 -2
  26. package/dist/srpc/watchdog.test.js +1 -0
  27. package/echo/echo.go +8 -8
  28. package/echo/echo.pb.go +12 -36
  29. package/echo/echo_srpc.pb.cpp +1 -1
  30. package/echo/echo_srpc.pb.go +1 -1
  31. package/echo/echo_srpc.pb.hpp +1 -1
  32. package/echo/echo_srpc.pb.rs +1 -1
  33. package/echo/echo_srpc.pb.ts +74 -0
  34. package/echo/server.go +1 -1
  35. package/echo/server.ts +24 -5
  36. package/go.mod +1 -1
  37. package/go.sum +2 -0
  38. package/integration/cross-language/go-client/main.go +4 -142
  39. package/integration/cross-language/go-server/main.go +2 -170
  40. package/integration/cross-language/run.bash +5 -105
  41. package/integration/cross-language/ts-client.ts +5 -90
  42. package/integration/cross-language/ts-server.ts +4 -112
  43. package/mock/mock.go +1 -1
  44. package/mock/mock.pb.go +12 -36
  45. package/mock/mock_srpc.pb.cpp +1 -1
  46. package/mock/mock_srpc.pb.go +1 -1
  47. package/mock/mock_srpc.pb.hpp +1 -1
  48. package/mock/mock_srpc.pb.rs +1 -1
  49. package/mock/mock_srpc.pb.ts +19 -1
  50. package/package.json +2 -2
  51. package/srpc/channel.test.ts +21 -1
  52. package/srpc/channel.ts +7 -3
  53. package/srpc/client-prefix.go +1 -1
  54. package/srpc/client-set.go +1 -1
  55. package/srpc/client.go +1 -1
  56. package/srpc/client.ts +4 -31
  57. package/srpc/common-rpc.go +15 -69
  58. package/srpc/common-rpc.ts +4 -77
  59. package/srpc/common-rpc_test.go +81 -0
  60. package/srpc/errors.go +26 -1
  61. package/srpc/handler.ts +54 -5
  62. package/srpc/index.ts +7 -4
  63. package/srpc/invoker-prefix.go +1 -1
  64. package/srpc/invoker.go +2 -2
  65. package/srpc/invoker.ts +20 -7
  66. package/srpc/message.go +1 -1
  67. package/srpc/msg-stream.go +1 -9
  68. package/srpc/mux-verbose.go +1 -1
  69. package/srpc/mux.go +1 -1
  70. package/srpc/rpcproto.pb.cc +4 -15
  71. package/srpc/rpcproto.pb.go +102 -419
  72. package/srpc/rpcproto.pb.h +0 -59
  73. package/srpc/rpcproto.pb.rs +0 -45
  74. package/srpc/rpcproto.pb.ts +0 -62
  75. package/srpc/rpcproto.proto +0 -16
  76. package/srpc/server-context.ts +55 -0
  77. package/srpc/server-rpc.go +1 -1
  78. package/srpc/server-rpc.ts +4 -6
  79. package/srpc/server.test.ts +94 -1
  80. package/srpc/stream-pipe.go +1 -1
  81. package/srpc/stream-rwc.go +1 -1
  82. package/srpc/stream.go +0 -20
  83. package/srpc/watchdog.test.ts +1 -0
  84. package/dist/rpcstream/receipt.test.d.ts +0 -1
  85. package/dist/rpcstream/receipt.test.js +0 -41
  86. package/dist/srpc/call-receipt.d.ts +0 -17
  87. package/dist/srpc/call-receipt.js +0 -106
  88. package/dist/srpc/call-receipt.test.d.ts +0 -1
  89. package/dist/srpc/call-receipt.test.js +0 -375
  90. package/dist/srpc/server-invocation.d.ts +0 -17
  91. package/dist/srpc/server-invocation.js +0 -37
  92. package/integration/cross-language/go-server/fixture-owner_test.go +0 -127
  93. package/srpc/call-receipt-e2e_test.go +0 -111
  94. package/srpc/call-receipt.go +0 -112
  95. package/srpc/call-receipt.test.ts +0 -441
  96. package/srpc/call-receipt.ts +0 -131
  97. package/srpc/call-receipt_test.go +0 -536
  98. package/srpc/schema-ownership_test.go +0 -115
  99. package/srpc/server-invocation.go +0 -40
  100. package/srpc/server-invocation.ts +0 -76
@@ -1,106 +0,0 @@
1
- /// <reference lib="es2024.promise" />
2
- import { ERR_RPC_ABORT } from './errors.js';
3
- import { TerminalKind } from './rpcproto.pb.js';
4
- // CallReceipt holds a unary call until it is committed or aborted.
5
- export class CallReceipt {
6
- #call;
7
- #iterator;
8
- #terminalPromise;
9
- #terminal;
10
- #done;
11
- #requestCommitted = false;
12
- #resolveDone;
13
- #rejectDone;
14
- constructor(call, iterator) {
15
- this.#call = call;
16
- this.#iterator = iterator;
17
- const { promise, resolve, reject } = Promise.withResolvers();
18
- this.#done = promise;
19
- this.#resolveDone = resolve;
20
- this.#rejectDone = reject;
21
- this.#done.catch(() => undefined);
22
- this.#terminalPromise = this.#observeTerminal();
23
- this.#terminalPromise.catch(() => undefined);
24
- }
25
- async #observeTerminal() {
26
- try {
27
- const result = await this.#iterator.next();
28
- if (!result.done) {
29
- throw new Error('unexpected trailing response data');
30
- }
31
- const terminal = this.#call.getTerminalKind();
32
- if (terminal !== TerminalKind.COMMITTED ||
33
- !this.#requestCommitted ||
34
- this.#terminal !== 'committed') {
35
- throw new Error('receipt closed before commit');
36
- }
37
- return result;
38
- }
39
- catch (err) {
40
- this.#rejectDone(err);
41
- const error = err instanceof Error ? err : new Error('receipt terminal failed');
42
- try {
43
- await this.#call.close(error);
44
- }
45
- catch {
46
- // The primary terminal error is already recorded on the receipt.
47
- }
48
- throw err;
49
- }
50
- }
51
- // done resolves after committed close and rejects on terminal failure.
52
- get done() {
53
- return this.#done;
54
- }
55
- // settled reports whether a terminal transition has been claimed.
56
- get settled() {
57
- return this.#terminal !== undefined;
58
- }
59
- // commit sends request completion and waits for server finalization.
60
- async commit() {
61
- if (this.#terminal === 'aborted') {
62
- throw new Error(ERR_RPC_ABORT);
63
- }
64
- if (this.#terminal === 'committed') {
65
- return this.#done;
66
- }
67
- this.#terminal = 'committed';
68
- this.#requestCommitted = true;
69
- try {
70
- await this.#call.writeCallData(undefined, true);
71
- await this.#terminalPromise;
72
- await this.#call.close();
73
- this.#resolveDone();
74
- }
75
- catch (err) {
76
- await this.#call.close(err instanceof Error ? err : new Error('receipt commit failed'));
77
- this.#rejectDone(err);
78
- throw err;
79
- }
80
- }
81
- // abort sends request cancellation and never rejects.
82
- async abort(reason) {
83
- if (this.#terminal !== undefined) {
84
- return;
85
- }
86
- this.#terminal = 'aborted';
87
- let terminalError = reason;
88
- try {
89
- await this.#call.writeCallCancel(true);
90
- }
91
- catch (err) {
92
- terminalError ??= err;
93
- }
94
- try {
95
- await this.#call.close();
96
- }
97
- catch (err) {
98
- terminalError ??= err;
99
- }
100
- this.#rejectDone(terminalError ?? new Error(ERR_RPC_ABORT));
101
- }
102
- // asyncDispose aborts a receipt that has not reached a terminal.
103
- async [Symbol.asyncDispose]() {
104
- await this.abort();
105
- }
106
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,375 +0,0 @@
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
- }
@@ -1,17 +0,0 @@
1
- import type { TerminalKind } from './rpcproto.pb.js';
2
- export declare class ServerInvocation implements AbortSignal {
3
- readonly signal: AbortSignal;
4
- private readonly waitFn;
5
- constructor(signal: AbortSignal, waitFn: (ownerSignal: AbortSignal) => Promise<TerminalKind>);
6
- get aborted(): boolean;
7
- get onabort(): typeof this.signal.onabort;
8
- set onabort(value: typeof this.signal.onabort);
9
- get reason(): any;
10
- throwIfAborted(): void;
11
- addEventListener<K extends keyof AbortSignalEventMap>(type: K, listener: (this: AbortSignal, ev: AbortSignalEventMap[K]) => unknown, options?: boolean | AddEventListenerOptions): void;
12
- addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
13
- removeEventListener<K extends keyof AbortSignalEventMap>(type: K, listener: (this: AbortSignal, ev: AbortSignalEventMap[K]) => unknown, options?: boolean | EventListenerOptions): void;
14
- removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
15
- dispatchEvent(event: Event): boolean;
16
- waitTerminal(ownerSignal: AbortSignal): Promise<TerminalKind>;
17
- }
@@ -1,37 +0,0 @@
1
- // ServerInvocation exposes the invocation signal and terminal wait.
2
- export class ServerInvocation {
3
- signal;
4
- waitFn;
5
- constructor(signal, waitFn) {
6
- this.signal = signal;
7
- this.waitFn = waitFn;
8
- }
9
- get aborted() {
10
- return this.signal.aborted;
11
- }
12
- get onabort() {
13
- return this.signal.onabort;
14
- }
15
- set onabort(value) {
16
- this.signal.onabort = value;
17
- }
18
- get reason() {
19
- return this.signal.reason;
20
- }
21
- throwIfAborted() {
22
- this.signal.throwIfAborted();
23
- }
24
- addEventListener(type, listener, options) {
25
- this.signal.addEventListener(type, listener, options);
26
- }
27
- removeEventListener(type, listener, options) {
28
- this.signal.removeEventListener(type, listener, options);
29
- }
30
- dispatchEvent(event) {
31
- return this.signal.dispatchEvent(event);
32
- }
33
- // waitTerminal waits for a remote terminal or owner-signal cancellation.
34
- waitTerminal(ownerSignal) {
35
- return this.waitFn(ownerSignal);
36
- }
37
- }
@@ -1,127 +0,0 @@
1
- package main
2
-
3
- import (
4
- "go/ast"
5
- "go/parser"
6
- "go/token"
7
- "os"
8
- "strconv"
9
- "testing"
10
- )
11
-
12
- // forbiddenFixtureCalls names the pipeline constructors that rebuild a second
13
- // srpc server inside a fixture. The receipt post-mortem removed a hand-built
14
- // ReadPump pipeline; the fixture must drive one srpc.Server through one
15
- // Server.HandleStream and observe framed packets by decorating net.Conn.
16
- var forbiddenFixtureCalls = []string{"NewServerRPC", "NewPacketReadWriter", "ReadPump"}
17
-
18
- // fixtureOwnerViolations returns the ways a cross-language Go fixture source
19
- // diverges from the single production server owner.
20
- func fixtureOwnerViolations(src string) []string {
21
- fset := token.NewFileSet()
22
- file, err := parser.ParseFile(fset, "fixture.go", src, 0)
23
- if err != nil {
24
- return []string{"parse error: " + err.Error()}
25
- }
26
-
27
- seen := map[string]bool{}
28
- handleStreamCalls := 0
29
- newServerCalls := 0
30
- ast.Inspect(file, func(n ast.Node) bool {
31
- call, ok := n.(*ast.CallExpr)
32
- if !ok {
33
- return true
34
- }
35
- sel, ok := call.Fun.(*ast.SelectorExpr)
36
- if !ok {
37
- return true
38
- }
39
- switch sel.Sel.Name {
40
- case "HandleStream":
41
- handleStreamCalls++
42
- case "NewServer":
43
- newServerCalls++
44
- case "NewServerRPC", "NewPacketReadWriter", "ReadPump":
45
- seen[sel.Sel.Name] = true
46
- }
47
- return true
48
- })
49
-
50
- var violations []string
51
- for _, name := range forbiddenFixtureCalls {
52
- if seen[name] {
53
- violations = append(violations, "reconstructs server pipeline via "+name)
54
- }
55
- }
56
- if newServerCalls != 1 {
57
- violations = append(violations, "expected exactly one srpc.NewServer, found "+strconv.Itoa(newServerCalls))
58
- }
59
- if handleStreamCalls != 1 {
60
- violations = append(violations, "expected exactly one Server.HandleStream entrypoint, found "+strconv.Itoa(handleStreamCalls))
61
- }
62
- if !decoratesNetConn(file) {
63
- violations = append(violations, "no fixture type decorates net.Conn")
64
- }
65
- return violations
66
- }
67
-
68
- // decoratesNetConn reports whether the source embeds net.Conn in a struct, the
69
- // only sanctioned way for fixture instrumentation to observe framed packets.
70
- func decoratesNetConn(file *ast.File) bool {
71
- found := false
72
- ast.Inspect(file, func(n ast.Node) bool {
73
- st, ok := n.(*ast.StructType)
74
- if !ok {
75
- return true
76
- }
77
- for _, field := range st.Fields.List {
78
- if len(field.Names) != 0 {
79
- continue
80
- }
81
- sel, ok := field.Type.(*ast.SelectorExpr)
82
- if !ok {
83
- continue
84
- }
85
- pkg, ok := sel.X.(*ast.Ident)
86
- if ok && pkg.Name == "net" && sel.Sel.Name == "Conn" {
87
- found = true
88
- }
89
- }
90
- return true
91
- })
92
- return found
93
- }
94
-
95
- // TestFixtureUsesSingleServerOwner proves the shipped fixture drives one
96
- // srpc.Server through one Server.HandleStream and decorates net.Conn.
97
- func TestFixtureUsesSingleServerOwner(t *testing.T) {
98
- src, err := os.ReadFile("main.go")
99
- if err != nil {
100
- t.Fatalf("read main.go: %v", err)
101
- }
102
- if violations := fixtureOwnerViolations(string(src)); len(violations) != 0 {
103
- t.Fatalf("fixture diverged from single server owner: %v", violations)
104
- }
105
- }
106
-
107
- // TestFixtureGuardDetectsSecondPipeline proves the guard fires when a fixture
108
- // rebuilds a second RPC server pipeline.
109
- func TestFixtureGuardDetectsSecondPipeline(t *testing.T) {
110
- bad := `package main
111
-
112
- import (
113
- "net"
114
-
115
- "github.com/aperturerobotics/starpc/srpc"
116
- )
117
-
118
- func run(conn net.Conn) {
119
- prw := srpc.NewPacketReadWriter(conn)
120
- rpc := srpc.NewServerRPC(nil, nil)
121
- _ = rpc.ReadPump(prw)
122
- }
123
- `
124
- if violations := fixtureOwnerViolations(bad); len(violations) == 0 {
125
- t.Fatal("guard failed to flag a second server pipeline")
126
- }
127
- }