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,5 +1,5 @@
1
1
  import { pushable } from 'it-pushable';
2
- import { Packet, TerminalKind, } from './rpcproto.pb.js';
2
+ import { Packet } 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,25 +24,13 @@ 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
27
  // remoteError records a remote error or transport failure.
30
28
  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.
29
+ // invocationController cancels the server invocation when the RPC closes.
36
30
  invocationController = new AbortController();
37
- // terminalPromise resolves when a remote terminal is recorded.
38
- terminalPromise;
39
- resolveTerminal;
40
31
  // writeDrainAbort wakes writers waiting for outbound stream drain on close.
41
32
  writeDrainAbort = new AbortController();
42
33
  constructor() {
43
- const { promise, resolve } = Promise.withResolvers();
44
- this.terminalPromise = promise;
45
- this.resolveTerminal = resolve;
46
34
  this.sink = this._createSink();
47
35
  this.source = this._source;
48
36
  this.rpcDataSource = this._rpcDataSource;
@@ -51,49 +39,10 @@ export class CommonRPC {
51
39
  get isClosed() {
52
40
  return this.closed ?? false;
53
41
  }
54
- // invocationSignal is canceled when the RPC reaches a terminal.
42
+ // invocationSignal is canceled when the RPC closes.
55
43
  get invocationSignal() {
56
44
  return this.invocationController.signal;
57
45
  }
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
- }
97
46
  // writeCallData writes the call data packet.
98
47
  async writeCallData(data, complete, error) {
99
48
  await this.writeCallDataPacket(data, complete, error);
@@ -221,20 +170,16 @@ export class CommonRPC {
221
170
  if (remoteError) {
222
171
  this.remoteError ??= remoteError;
223
172
  this.invocationController.abort();
224
- this.recordRemoteTerminal(TerminalKind.TRANSPORT_LOST);
225
173
  }
226
174
  if (packet.complete && !remoteError) {
227
- this.remoteCompleted = true;
228
- this.recordRemoteTerminal(TerminalKind.COMMITTED);
229
175
  this._rpcDataSource.end(remoteError);
230
176
  }
231
177
  else if (remoteError) {
232
178
  this._rpcDataSource.end(remoteError);
233
179
  }
234
180
  }
235
- // handleCallCancel handles a CallCancel packet.
181
+ // handleCallCancel aborts the invocation and closes the call.
236
182
  async handleCallCancel() {
237
- this.recordRemoteTerminal(TerminalKind.CANCELED);
238
183
  await this.close(new Error(ERR_RPC_ABORT));
239
184
  }
240
185
  // close closes the call, optionally with an error.
@@ -246,7 +191,6 @@ export class CommonRPC {
246
191
  if (!this.remoteError && err) {
247
192
  this.remoteError = err;
248
193
  }
249
- this.recordRemoteTerminal(err ? TerminalKind.TRANSPORT_LOST : TerminalKind.CLOSED);
250
194
  this.invocationController.abort();
251
195
  // note: this does nothing if _source is already ended.
252
196
  if (err && err.message) {
@@ -272,8 +216,6 @@ export class CommonRPC {
272
216
  await this.handlePacket(msg);
273
217
  }
274
218
  }
275
- this.remoteSourceClosed = true;
276
- this.recordRemoteTerminal(TerminalKind.CLOSED);
277
219
  }
278
220
  catch (err) {
279
221
  this.close(err);
@@ -1,7 +1,14 @@
1
1
  import type { Sink, Source } from 'it-stream-types';
2
- import { ServiceDefinition, ServiceMethodDefinitions } from './definition.js';
3
- import type { ServerInvocation } from './server-invocation.js';
4
- export type InvokeFn = (dataSource: Source<Uint8Array>, dataSink: Sink<Source<Uint8Array>>, invocation?: ServerInvocation) => Promise<void>;
2
+ import { MethodKind, type MessageType } from '@aptre/protobuf-es-lite';
3
+ import type { MessageStream } from './message.js';
4
+ import { type MethodDefinition, ServiceDefinition, ServiceMethodDefinitions } from './definition.js';
5
+ import type { ServerContext } from './server-context.js';
6
+ type MessageOf<T> = T extends MessageType<infer M> ? M : never;
7
+ type ServerMethod<T> = T extends MethodDefinition<infer Request, infer Response, infer Kind, infer _Idempotency> ? Kind extends MethodKind.Unary ? (request: MessageOf<Request>, abortSignal: AbortSignal, context: ServerContext) => Promise<MessageOf<Response>> : Kind extends MethodKind.ServerStreaming ? (request: MessageOf<Request>, abortSignal: AbortSignal, context: ServerContext) => MessageStream<MessageOf<Response>> : Kind extends MethodKind.ClientStreaming ? (request: MessageStream<MessageOf<Request>>, abortSignal: AbortSignal, context: ServerContext) => Promise<MessageOf<Response>> : (request: MessageStream<MessageOf<Request>>, abortSignal: AbortSignal, context: ServerContext) => MessageStream<MessageOf<Response>> : never;
8
+ export type HandlerImplementation<T extends ServiceMethodDefinitions> = Partial<{
9
+ [Method in keyof T]: ServerMethod<T[Method]>;
10
+ }>;
11
+ export type InvokeFn = (dataSource: Source<Uint8Array>, dataSink: Sink<Source<Uint8Array>>, context: ServerContext) => Promise<void>;
5
12
  export interface Handler {
6
13
  getServiceID(): string;
7
14
  getMethodIDs(): string[];
@@ -18,4 +25,5 @@ export declare class StaticHandler implements Handler {
18
25
  getMethodIDs(): string[];
19
26
  lookupMethod(serviceID: string, methodID: string): Promise<InvokeFn | null>;
20
27
  }
21
- export declare function createHandler<T extends ServiceMethodDefinitions = ServiceMethodDefinitions>(definition: ServiceDefinition<T>, impl: any, serviceID?: string): Handler;
28
+ export declare function createHandler<T extends ServiceMethodDefinitions = ServiceMethodDefinitions>(definition: ServiceDefinition<T>, impl: HandlerImplementation<T>, serviceID?: string): Handler;
29
+ export {};
@@ -1,9 +1,5 @@
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';
7
3
  export { Server } from './server.js';
8
4
  export { StreamConn } from './conn.js';
9
5
  export type { StreamConnParams, StreamHandler } from './conn.js';
@@ -31,3 +27,5 @@ export { HandleStreamCtr } from './handle-stream-ctr.js';
31
27
  export { writeToPushable, buildPushableSink, messagePushable, } from './pushable.js';
32
28
  export { Watchdog } from './watchdog.js';
33
29
  export type { ProtoRpc } from './proto-rpc.js';
30
+ export { createContextKey, serverContextValue, withServerContextValue, } from './server-context.js';
31
+ export type { ContextKey, ServerContext } from './server-context.js';
@@ -1,8 +1,5 @@
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';
6
3
  export { Server } from './server.js';
7
4
  export { StreamConn } from './conn.js';
8
5
  export { WebSocketConn } from './websocket.js';
@@ -21,3 +18,4 @@ export { OpenStreamCtr } from './open-stream-ctr.js';
21
18
  export { HandleStreamCtr } from './handle-stream-ctr.js';
22
19
  export { writeToPushable, buildPushableSink, messagePushable, } from './pushable.js';
23
20
  export { Watchdog } from './watchdog.js';
21
+ export { createContextKey, serverContextValue, withServerContextValue, } from './server-context.js';
@@ -1,7 +1,7 @@
1
1
  import type { MethodDefinition } from './definition.js';
2
+ import type { ServerContext } from './server-context.js';
2
3
  import { InvokeFn } from './handler.js';
3
4
  import type { MessageType, Message } from '@aptre/protobuf-es-lite';
4
5
  import { MethodIdempotency, MethodKind } from '@aptre/protobuf-es-lite';
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
+ export type MethodProto<R extends Message<R>, O extends Message<O>> = ((request: R, abortSignal: AbortSignal, context: ServerContext) => Promise<O>) | ((request: R, abortSignal: AbortSignal, context: ServerContext) => AsyncIterable<O>) | ((request: AsyncIterable<R>, abortSignal: AbortSignal, context: ServerContext) => Promise<O>) | ((request: AsyncIterable<R>, abortSignal: AbortSignal, context: ServerContext) => AsyncIterable<O>);
7
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;
@@ -6,7 +6,7 @@ import { MethodKind } from '@aptre/protobuf-es-lite';
6
6
  // createInvokeFn builds an InvokeFn from a method definition and a function prototype.
7
7
  export function createInvokeFn(methodInfo, methodProto) {
8
8
  const requestDecode = buildDecodeMessageTransform(methodInfo.I);
9
- return async (dataSource, dataSink, invocation) => {
9
+ return async (dataSource, dataSink, context) => {
10
10
  // responseSink is a Sink for response messages.
11
11
  const responseSink = pushable({
12
12
  objectMode: true,
@@ -34,7 +34,7 @@ export function createInvokeFn(methodInfo, methodProto) {
34
34
  }
35
35
  // Call the implementation.
36
36
  try {
37
- const responseObj = methodProto(requestArg, invocation);
37
+ const responseObj = methodProto(requestArg, context.signal, context);
38
38
  if (!responseObj) {
39
39
  throw new Error('return value was undefined');
40
40
  }
@@ -1,49 +1,5 @@
1
1
  import type { MessageType } from '@aptre/protobuf-es-lite/message';
2
2
  export declare const protobufPackage = "srpc";
3
- /**
4
- * TerminalKind is the terminal state of a held unary invocation.
5
- *
6
- * @generated from enum srpc.TerminalKind
7
- */
8
- export declare enum TerminalKind {
9
- /**
10
- * TERMINAL_KIND_UNSPECIFIED is not a valid terminal state.
11
- *
12
- * @generated from enum value: TERMINAL_KIND_UNSPECIFIED = 0;
13
- */
14
- UNSPECIFIED = 0,
15
- /**
16
- * TERMINAL_KIND_COMMITTED identifies an explicit remote CallData completion.
17
- *
18
- * @generated from enum value: TERMINAL_KIND_COMMITTED = 1;
19
- */
20
- COMMITTED = 1,
21
- /**
22
- * TERMINAL_KIND_CANCELED identifies a remote CallCancel packet.
23
- *
24
- * @generated from enum value: TERMINAL_KIND_CANCELED = 2;
25
- */
26
- CANCELED = 2,
27
- /**
28
- * TERMINAL_KIND_TRANSPORT_LOST identifies a remote error or transport failure.
29
- *
30
- * @generated from enum value: TERMINAL_KIND_TRANSPORT_LOST = 3;
31
- */
32
- TRANSPORT_LOST = 3,
33
- /**
34
- * TERMINAL_KIND_CLOSED identifies a bare remote close without completion.
35
- *
36
- * @generated from enum value: TERMINAL_KIND_CLOSED = 4;
37
- */
38
- CLOSED = 4,
39
- /**
40
- * TERMINAL_KIND_ABANDONED identifies owner-context expiry without a remote terminal.
41
- *
42
- * @generated from enum value: TERMINAL_KIND_ABANDONED = 5;
43
- */
44
- ABANDONED = 5
45
- }
46
- export declare const TerminalKind_Enum: import("@aptre/protobuf-es-lite/enum").EnumType;
47
3
  /**
48
4
  * CallStart requests starting a new RPC call.
49
5
  *
@@ -1,62 +1,9 @@
1
1
  // @generated by protoc-gen-es-lite unknown with parameter "target=ts,ts_nocheck=false"
2
2
  // @generated from file github.com/aperturerobotics/starpc/srpc/rpcproto.proto (package srpc, syntax proto3)
3
3
  /* eslint-disable */
4
- import { createEnumType } from '@aptre/protobuf-es-lite/enum';
5
4
  import { createMessageType } from '@aptre/protobuf-es-lite/message';
6
5
  import { ScalarType } from '@aptre/protobuf-es-lite/scalar';
7
6
  export const protobufPackage = 'srpc';
8
- /**
9
- * TerminalKind is the terminal state of a held unary invocation.
10
- *
11
- * @generated from enum srpc.TerminalKind
12
- */
13
- export var TerminalKind;
14
- (function (TerminalKind) {
15
- /**
16
- * TERMINAL_KIND_UNSPECIFIED is not a valid terminal state.
17
- *
18
- * @generated from enum value: TERMINAL_KIND_UNSPECIFIED = 0;
19
- */
20
- TerminalKind[TerminalKind["UNSPECIFIED"] = 0] = "UNSPECIFIED";
21
- /**
22
- * TERMINAL_KIND_COMMITTED identifies an explicit remote CallData completion.
23
- *
24
- * @generated from enum value: TERMINAL_KIND_COMMITTED = 1;
25
- */
26
- TerminalKind[TerminalKind["COMMITTED"] = 1] = "COMMITTED";
27
- /**
28
- * TERMINAL_KIND_CANCELED identifies a remote CallCancel packet.
29
- *
30
- * @generated from enum value: TERMINAL_KIND_CANCELED = 2;
31
- */
32
- TerminalKind[TerminalKind["CANCELED"] = 2] = "CANCELED";
33
- /**
34
- * TERMINAL_KIND_TRANSPORT_LOST identifies a remote error or transport failure.
35
- *
36
- * @generated from enum value: TERMINAL_KIND_TRANSPORT_LOST = 3;
37
- */
38
- TerminalKind[TerminalKind["TRANSPORT_LOST"] = 3] = "TRANSPORT_LOST";
39
- /**
40
- * TERMINAL_KIND_CLOSED identifies a bare remote close without completion.
41
- *
42
- * @generated from enum value: TERMINAL_KIND_CLOSED = 4;
43
- */
44
- TerminalKind[TerminalKind["CLOSED"] = 4] = "CLOSED";
45
- /**
46
- * TERMINAL_KIND_ABANDONED identifies owner-context expiry without a remote terminal.
47
- *
48
- * @generated from enum value: TERMINAL_KIND_ABANDONED = 5;
49
- */
50
- TerminalKind[TerminalKind["ABANDONED"] = 5] = "ABANDONED";
51
- })(TerminalKind || (TerminalKind = {}));
52
- export const TerminalKind_Enum = /* @__PURE__ */ createEnumType('srpc.TerminalKind', [
53
- [0, 'TERMINAL_KIND_UNSPECIFIED'],
54
- [1, 'TERMINAL_KIND_COMMITTED'],
55
- [2, 'TERMINAL_KIND_CANCELED'],
56
- [3, 'TERMINAL_KIND_TRANSPORT_LOST'],
57
- [4, 'TERMINAL_KIND_CLOSED'],
58
- [5, 'TERMINAL_KIND_ABANDONED'],
59
- ]);
60
7
  export const CallStart =
61
8
  /* @__PURE__ */ createMessageType({
62
9
  typeName: 'srpc.CallStart',
@@ -0,0 +1,11 @@
1
+ declare const contextKeyValue: unique symbol;
2
+ export interface ContextKey<T> {
3
+ readonly [contextKeyValue]?: T;
4
+ }
5
+ export interface ServerContext {
6
+ readonly signal: AbortSignal;
7
+ }
8
+ export declare function createContextKey<T>(): ContextKey<T>;
9
+ export declare function withServerContextValue<T>(context: ServerContext, key: ContextKey<T>, value: T): ServerContext;
10
+ export declare function serverContextValue<T>(context: ServerContext, key: ContextKey<T>): T | undefined;
11
+ export {};
@@ -0,0 +1,28 @@
1
+ const contextParent = Symbol('server context parent');
2
+ const contextKey = Symbol('server context key');
3
+ const contextStoredValue = Symbol('server context value');
4
+ // createContextKey constructs an identity key for one server-context value.
5
+ export function createContextKey() {
6
+ return {};
7
+ }
8
+ // withServerContextValue derives a context with one immutable typed value.
9
+ export function withServerContextValue(context, key, value) {
10
+ const derived = {
11
+ signal: context.signal,
12
+ [contextParent]: context,
13
+ [contextKey]: key,
14
+ [contextStoredValue]: value,
15
+ };
16
+ return derived;
17
+ }
18
+ // serverContextValue retrieves the nearest value for a typed identity key.
19
+ export function serverContextValue(context, key) {
20
+ let current = context;
21
+ while (current) {
22
+ if (current[contextKey] === key) {
23
+ return current[contextStoredValue];
24
+ }
25
+ current = current[contextParent];
26
+ }
27
+ return undefined;
28
+ }
@@ -1,5 +1,4 @@
1
1
  import { CommonRPC } from './common-rpc.js';
2
- import { ServerInvocation } from './server-invocation.js';
3
2
  // ServerRPC is an ongoing RPC from the server side.
4
3
  export class ServerRPC extends CommonRPC {
5
4
  // lookupMethod looks up the incoming RPC methods.
@@ -38,9 +37,10 @@ export class ServerRPC extends CommonRPC {
38
37
  // invokeRPC starts invoking the RPC handler.
39
38
  async invokeRPC(invokeFn) {
40
39
  const dataSink = this._createDataSink();
41
- const invocation = new ServerInvocation(this.invocationSignal, (ownerSignal) => this.waitTerminal(ownerSignal));
42
40
  try {
43
- await invokeFn(this.rpcDataSource, dataSink, invocation);
41
+ await invokeFn(this.rpcDataSource, dataSink, {
42
+ signal: this.invocationSignal,
43
+ });
44
44
  }
45
45
  catch (err) {
46
46
  this.close(err);
@@ -1,7 +1,7 @@
1
1
  import { describe, it, beforeEach, expect, vi } from 'vitest';
2
2
  import { pipe } from 'it-pipe';
3
- import { createHandler, createMux, Server, Client, StreamConn, ChannelStream, combineUint8ArrayListTransform, Packet, } from '../srpc/index.js';
4
- import { EchoerDefinition, EchoerServer, EchoerServiceName, runClientTest, } from '../echo/index.js';
3
+ import { createHandler, createMux, Server, Client, StreamConn, ChannelStream, combineUint8ArrayListTransform, Packet, createContextKey, serverContextValue, withServerContextValue, } from '../srpc/index.js';
4
+ import { EchoerDefinition, EchoerServer, EchoerServiceName, EchoMsg, runClientTest, } from '../echo/index.js';
5
5
  import { runAbortControllerTest, runRpcStreamTest, } from '../echo/client-test.js';
6
6
  describe('srpc server', () => {
7
7
  let client;
@@ -39,6 +39,62 @@ describe('srpc server', () => {
39
39
  it('should pass rpc stream tests', async () => {
40
40
  await runRpcStreamTest(client);
41
41
  });
42
+ it('passes the exact invocation context after async request decode', async () => {
43
+ const controller = new AbortController();
44
+ const callerKey = createContextKey();
45
+ let observedAbortSignal;
46
+ let observedContextSignal;
47
+ let observedCaller;
48
+ const handler = createHandler(EchoerDefinition, {
49
+ Echo: async (request, abortSignal, context) => {
50
+ observedAbortSignal = abortSignal;
51
+ observedContextSignal = context.signal;
52
+ observedCaller = serverContextValue(context, callerKey);
53
+ return request;
54
+ },
55
+ });
56
+ const invokeFn = await handler.lookupMethod(EchoerServiceName, 'Echo');
57
+ if (!invokeFn) {
58
+ throw new Error('Echo method was not found');
59
+ }
60
+ const request = EchoMsg.create({ body: 'signal identity' });
61
+ const drained = Promise.withResolvers();
62
+ await invokeFn((async function* () {
63
+ await Promise.resolve();
64
+ yield EchoMsg.toBinary(request);
65
+ })(), async (source) => {
66
+ for await (const _data of source) {
67
+ // Drain the encoded response so the invocation pipeline completes.
68
+ }
69
+ drained.resolve();
70
+ }, withServerContextValue({ signal: controller.signal }, callerKey, 'caller-1'));
71
+ await drained.promise;
72
+ expect(observedAbortSignal).toBe(controller.signal);
73
+ expect(observedContextSignal).toBe(controller.signal);
74
+ expect(observedCaller).toBe('caller-1');
75
+ });
76
+ it('keeps two-argument server handlers compatible', async () => {
77
+ const controller = new AbortController();
78
+ let observedSignal;
79
+ const handler = createHandler(EchoerDefinition, {
80
+ Echo: async (request, abortSignal) => {
81
+ observedSignal = abortSignal;
82
+ return request;
83
+ },
84
+ });
85
+ const invokeFn = await handler.lookupMethod(EchoerServiceName, 'Echo');
86
+ if (!invokeFn)
87
+ throw new Error('Echo method was not found');
88
+ const request = EchoMsg.create({ body: 'legacy handler' });
89
+ await invokeFn((async function* () {
90
+ yield EchoMsg.toBinary(request);
91
+ })(), async (source) => {
92
+ for await (const _data of source) {
93
+ // Drain the response.
94
+ }
95
+ }, { signal: controller.signal });
96
+ expect(observedSignal).toBe(controller.signal);
97
+ });
42
98
  it('keeps detached server-streaming responses open after request source completes', async () => {
43
99
  const mux = createMux();
44
100
  const response = new TextEncoder().encode('delayed init');
@@ -166,6 +222,13 @@ describe('srpc server', () => {
166
222
  await new Promise((resolve) => setTimeout(resolve, 50));
167
223
  expect(removeEventListener).toHaveBeenCalledWith('abort', expect.any(Function));
168
224
  });
225
+ it('settles a late abort after the call has already closed', async () => {
226
+ const controller = new AbortController();
227
+ vi.spyOn(controller.signal, 'removeEventListener').mockImplementation(() => undefined);
228
+ await client.request(EchoerServiceName, 'Echo', new TextEncoder().encode('late abort'), controller.signal);
229
+ controller.abort();
230
+ await Promise.resolve();
231
+ });
169
232
  it('tears down passive channel close state', async () => {
170
233
  const { port1, port2 } = new MessageChannel();
171
234
  const opts = { idleTimeoutMs: 1000, keepAliveMs: 1000 };
@@ -5,6 +5,7 @@ describe('Watchdog', () => {
5
5
  vi.useFakeTimers();
6
6
  });
7
7
  afterEach(() => {
8
+ vi.useRealTimers();
8
9
  vi.restoreAllMocks();
9
10
  });
10
11
  it('should call expired callback after timeout', () => {
package/echo/echo.go CHANGED
@@ -7,15 +7,15 @@ import (
7
7
 
8
8
  // _ is a type assertion
9
9
  var (
10
- _ srpc.StreamRecv[*EchoMsg] = (SRPCEchoer_EchoBidiStreamClient)(nil)
11
- _ srpc.StreamRecv[*EchoMsg] = (SRPCEchoer_EchoServerStreamClient)(nil)
10
+ _ srpc.StreamRecv[*EchoMsg] = SRPCEchoer_EchoBidiStreamClient(nil)
11
+ _ srpc.StreamRecv[*EchoMsg] = SRPCEchoer_EchoServerStreamClient(nil)
12
12
 
13
- _ srpc.StreamSend[*EchoMsg] = (SRPCEchoer_EchoBidiStreamClient)(nil)
14
- _ srpc.StreamSend[*EchoMsg] = (SRPCEchoer_EchoClientStreamClient)(nil)
13
+ _ srpc.StreamSend[*EchoMsg] = SRPCEchoer_EchoBidiStreamClient(nil)
14
+ _ srpc.StreamSend[*EchoMsg] = SRPCEchoer_EchoClientStreamClient(nil)
15
15
 
16
- _ srpc.StreamSendAndClose[*EchoMsg] = (SRPCEchoer_EchoBidiStreamStream)(nil)
17
- _ srpc.StreamSendAndClose[*EchoMsg] = (SRPCEchoer_EchoServerStreamStream)(nil)
16
+ _ srpc.StreamSendAndClose[*EchoMsg] = SRPCEchoer_EchoBidiStreamStream(nil)
17
+ _ srpc.StreamSendAndClose[*EchoMsg] = SRPCEchoer_EchoServerStreamStream(nil)
18
18
 
19
- _ srpc.StreamRecv[*rpcstream.RpcStreamPacket] = (SRPCEchoer_RpcStreamStream)(nil)
20
- _ srpc.StreamSendAndClose[*rpcstream.RpcStreamPacket] = (SRPCEchoer_RpcStreamStream)(nil)
19
+ _ srpc.StreamRecv[*rpcstream.RpcStreamPacket] = SRPCEchoer_RpcStreamStream(nil)
20
+ _ srpc.StreamSendAndClose[*rpcstream.RpcStreamPacket] = SRPCEchoer_RpcStreamStream(nil)
21
21
  )
package/echo/echo.pb.go CHANGED
@@ -1,5 +1,5 @@
1
1
  // Code generated by protoc-gen-go-lite. DO NOT EDIT.
2
- // protoc-gen-go-lite version: v0.13.0
2
+ // protoc-gen-go-lite version: v0.16.0
3
3
  // source: github.com/aperturerobotics/starpc/echo/echo.proto
4
4
 
5
5
  package echo
@@ -8,8 +8,6 @@ import (
8
8
  fmt "fmt"
9
9
  io "io"
10
10
  slices "slices"
11
- strconv "strconv"
12
- strings "strings"
13
11
 
14
12
  protobuf_go_lite "github.com/aperturerobotics/protobuf-go-lite"
15
13
  json "github.com/aperturerobotics/protobuf-go-lite/json"
@@ -141,13 +139,10 @@ func (m *EchoMsg) MarshalToSizedBufferVT(dAtA []byte) (int, error) {
141
139
  var l int
142
140
  _ = l
143
141
  if m.unknownFields != nil {
144
- i -= len(m.unknownFields)
145
- copy(dAtA[i:], m.unknownFields)
142
+ i = protobuf_go_lite.EncodeRawBytes(dAtA, i, m.unknownFields)
146
143
  }
147
144
  if len(m.Body) > 0 {
148
- i -= len(m.Body)
149
- copy(dAtA[i:], m.Body)
150
- i = protobuf_go_lite.EncodeVarint(dAtA, i, uint64(len(m.Body)))
145
+ i = protobuf_go_lite.EncodeString(dAtA, i, m.Body)
151
146
  i--
152
147
  dAtA[i] = 0xa
153
148
  }
@@ -160,26 +155,19 @@ func (m *EchoMsg) SizeVT() (n int) {
160
155
  }
161
156
  var l int
162
157
  _ = l
163
- l = len(m.Body)
164
- if l > 0 {
165
- n += 1 + l + protobuf_go_lite.SizeOfVarint(uint64(l))
166
- }
158
+ n += protobuf_go_lite.SizeStringNonEmpty(1, m.Body)
167
159
  n += len(m.unknownFields)
168
160
  return n
169
161
  }
170
162
 
171
163
  func (x *EchoMsg) MarshalProtoText() string {
172
- var sb strings.Builder
173
- sb.WriteString("EchoMsg {")
164
+ var sb protobuf_go_lite.TextBuilder
165
+ initialLen := protobuf_go_lite.TextStartMessage(&sb, "EchoMsg")
174
166
  if x.Body != "" {
175
- if sb.Len() > 9 {
176
- sb.WriteString(" ")
177
- }
178
- sb.WriteString("body: ")
179
- sb.WriteString(strconv.Quote(x.Body))
167
+ protobuf_go_lite.TextWriteFieldPrefix(&sb, initialLen, "body")
168
+ protobuf_go_lite.TextWriteString(&sb, x.Body)
180
169
  }
181
- sb.WriteString("}")
182
- return sb.String()
170
+ return protobuf_go_lite.TextFinishMessage(&sb)
183
171
  }
184
172
 
185
173
  func (x *EchoMsg) String() string {
@@ -210,24 +198,12 @@ func (m *EchoMsg) UnmarshalVT(dAtA []byte) error {
210
198
  if wireType != 2 {
211
199
  return fmt.Errorf("proto: wrong wireType = %d for field Body", wireType)
212
200
  }
213
- var stringLen uint64
214
- stringLen, iNdEx, err = protobuf_go_lite.DecodeVarint(dAtA, iNdEx)
201
+ var v string
202
+ v, iNdEx, err = protobuf_go_lite.DecodeString(dAtA, iNdEx)
215
203
  if err != nil {
216
204
  return err
217
205
  }
218
- intStringLen := int(stringLen)
219
- if intStringLen < 0 {
220
- return protobuf_go_lite.ErrInvalidLength
221
- }
222
- postIndex := iNdEx + intStringLen
223
- if postIndex < 0 {
224
- return protobuf_go_lite.ErrInvalidLength
225
- }
226
- if postIndex > l {
227
- return io.ErrUnexpectedEOF
228
- }
229
- m.Body = string(dAtA[iNdEx:postIndex])
230
- iNdEx = postIndex
206
+ m.Body = v
231
207
  default:
232
208
  iNdEx = preIndex
233
209
  skippy, err := protobuf_go_lite.Skip(dAtA[iNdEx:])
@@ -1,7 +1,7 @@
1
1
  //go:build deps_only
2
2
 
3
3
  // Code generated by protoc-gen-starpc-cpp. DO NOT EDIT.
4
- // protoc-gen-starpc-cpp version: v0.49.7
4
+ // protoc-gen-starpc-cpp version: v0.49.21
5
5
  // source: github.com/aperturerobotics/starpc/echo/echo.proto
6
6
 
7
7
  #include "echo_srpc.pb.hpp"
@@ -1,5 +1,5 @@
1
1
  // Code generated by protoc-gen-srpc. DO NOT EDIT.
2
- // protoc-gen-srpc version: v0.49.7
2
+ // protoc-gen-srpc version: v0.49.21
3
3
  // source: github.com/aperturerobotics/starpc/echo/echo.proto
4
4
 
5
5
  package echo
@@ -1,7 +1,7 @@
1
1
  //go:build deps_only && cgo
2
2
 
3
3
  // Code generated by protoc-gen-starpc-cpp. DO NOT EDIT.
4
- // protoc-gen-starpc-cpp version: v0.49.7
4
+ // protoc-gen-starpc-cpp version: v0.49.21
5
5
  // source: github.com/aperturerobotics/starpc/echo/echo.proto
6
6
 
7
7
  #pragma once
@@ -1,5 +1,5 @@
1
1
  // Code generated by protoc-gen-starpc-rust. DO NOT EDIT.
2
- // protoc-gen-starpc-rust version: v0.49.7
2
+ // protoc-gen-starpc-rust version: v0.49.21
3
3
  // source: github.com/aperturerobotics/starpc/echo/echo.proto
4
4
 
5
5
  #[allow(unused_imports)]