starpc 0.51.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.
@@ -26,6 +26,7 @@ import {
26
26
  } from '@aptre/protobuf-es-lite/protoplugin/ecmascript'
27
27
 
28
28
  const MessageStream = createImportSymbol('MessageStream', 'starpc')
29
+ const ServerContext = createImportSymbol('ServerContext', 'starpc')
29
30
  const RuntimeMethodKind = createImportSymbol(
30
31
  'MethodKind',
31
32
  '@aptre/protobuf-es-lite',
@@ -128,6 +129,42 @@ function generateService(
128
129
  f.print("}");
129
130
  f.print();
130
131
 
132
+ // Generate the server implementation interface.
133
+ f.print(f.jsDoc(service));
134
+ f.print("export interface ", localName(service), "Handler {");
135
+ for (let i = 0; i < service.methods.length; i++) {
136
+ const method = service.methods[i];
137
+ f.print(f.jsDoc(method, " "));
138
+ if (method.methodKind === MethodKind.Unary) {
139
+ f.print(
140
+ " ", method.name,
141
+ "(request: ", method.input, ", abortSignal: AbortSignal, context: ", ServerContext, "): ",
142
+ "Promise<", method.output, ">;"
143
+ );
144
+ } else if (method.methodKind === MethodKind.ServerStreaming) {
145
+ f.print(
146
+ " ", method.name,
147
+ "(request: ", method.input, ", abortSignal: AbortSignal, context: ", ServerContext, "): ",
148
+ MessageStream, "<", method.output, ">;"
149
+ );
150
+ } else if (method.methodKind === MethodKind.ClientStreaming) {
151
+ f.print(
152
+ " ", method.name,
153
+ "(request: ", MessageStream, "<", method.input, ">, abortSignal: AbortSignal, context: ", ServerContext, "): ",
154
+ "Promise<", method.output, ">;"
155
+ );
156
+ } else if (method.methodKind === MethodKind.BiDiStreaming) {
157
+ f.print(
158
+ " ", method.name,
159
+ "(request: ", MessageStream, "<", method.input, ">, abortSignal: AbortSignal, context: ", ServerContext, "): ",
160
+ MessageStream, "<", method.output, ">;"
161
+ );
162
+ }
163
+ if (i < service.methods.length - 1) f.print();
164
+ }
165
+ f.print("}");
166
+ f.print();
167
+
131
168
 
132
169
  // Generate the service name constant
133
170
  f.print("export const ", localName(service), "ServiceName = ", localName(service), "Definition.typeName");
@@ -15,6 +15,7 @@
15
15
  import { MethodIdempotency, MethodKind, localName, } from '@aptre/protobuf-es-lite';
16
16
  import { createImportSymbol, } from '@aptre/protobuf-es-lite/protoplugin/ecmascript';
17
17
  const MessageStream = createImportSymbol('MessageStream', 'starpc');
18
+ const ServerContext = createImportSymbol('ServerContext', 'starpc');
18
19
  const RuntimeMethodKind = createImportSymbol('MethodKind', '@aptre/protobuf-es-lite');
19
20
  const RuntimeMethodIdempotency = createImportSymbol('MethodIdempotency', '@aptre/protobuf-es-lite');
20
21
  // const Message = createImportSymbol('Message', '@aptre/protobuf-es-lite')
@@ -74,6 +75,29 @@ function generateService(f, service) {
74
75
  }
75
76
  f.print("}");
76
77
  f.print();
78
+ // Generate the server implementation interface.
79
+ f.print(f.jsDoc(service));
80
+ f.print("export interface ", localName(service), "Handler {");
81
+ for (let i = 0; i < service.methods.length; i++) {
82
+ const method = service.methods[i];
83
+ f.print(f.jsDoc(method, " "));
84
+ if (method.methodKind === MethodKind.Unary) {
85
+ f.print(" ", method.name, "(request: ", method.input, ", abortSignal: AbortSignal, context: ", ServerContext, "): ", "Promise<", method.output, ">;");
86
+ }
87
+ else if (method.methodKind === MethodKind.ServerStreaming) {
88
+ f.print(" ", method.name, "(request: ", method.input, ", abortSignal: AbortSignal, context: ", ServerContext, "): ", MessageStream, "<", method.output, ">;");
89
+ }
90
+ else if (method.methodKind === MethodKind.ClientStreaming) {
91
+ f.print(" ", method.name, "(request: ", MessageStream, "<", method.input, ">, abortSignal: AbortSignal, context: ", ServerContext, "): ", "Promise<", method.output, ">;");
92
+ }
93
+ else if (method.methodKind === MethodKind.BiDiStreaming) {
94
+ f.print(" ", method.name, "(request: ", MessageStream, "<", method.input, ">, abortSignal: AbortSignal, context: ", ServerContext, "): ", MessageStream, "<", method.output, ">;");
95
+ }
96
+ if (i < service.methods.length - 1)
97
+ f.print();
98
+ }
99
+ f.print("}");
100
+ f.print();
77
101
  // Generate the service name constant
78
102
  f.print("export const ", localName(service), "ServiceName = ", localName(service), "Definition.typeName");
79
103
  f.print();
@@ -2,7 +2,7 @@ import { EchoMsg } from './echo.pb.js';
2
2
  import { MethodKind } from '@aptre/protobuf-es-lite';
3
3
  import { RpcStreamPacket } from '../rpcstream/rpcstream.pb.js';
4
4
  import { Empty } from '@aptre/protobuf-es-lite/google/protobuf/empty';
5
- import { MessageStream, ProtoRpc } from 'starpc';
5
+ import { MessageStream, ProtoRpc, ServerContext } from 'starpc';
6
6
  /**
7
7
  * Echoer service returns the given message.
8
8
  *
@@ -122,6 +122,49 @@ export interface Echoer {
122
122
  */
123
123
  DoNothing(request: Empty, abortSignal?: AbortSignal): Promise<Empty>;
124
124
  }
125
+ /**
126
+ * Echoer service returns the given message.
127
+ *
128
+ * @generated from service echo.Echoer
129
+ */
130
+ export interface EchoerHandler {
131
+ /**
132
+ * Echo returns the given message.
133
+ *
134
+ * @generated from rpc echo.Echoer.Echo
135
+ */
136
+ Echo(request: EchoMsg, abortSignal: AbortSignal, context: ServerContext): Promise<EchoMsg>;
137
+ /**
138
+ * EchoServerStream is an example of a server -> client one-way stream.
139
+ *
140
+ * @generated from rpc echo.Echoer.EchoServerStream
141
+ */
142
+ EchoServerStream(request: EchoMsg, abortSignal: AbortSignal, context: ServerContext): MessageStream<EchoMsg>;
143
+ /**
144
+ * EchoClientStream is an example of client->server one-way stream.
145
+ *
146
+ * @generated from rpc echo.Echoer.EchoClientStream
147
+ */
148
+ EchoClientStream(request: MessageStream<EchoMsg>, abortSignal: AbortSignal, context: ServerContext): Promise<EchoMsg>;
149
+ /**
150
+ * EchoBidiStream is an example of a two-way stream.
151
+ *
152
+ * @generated from rpc echo.Echoer.EchoBidiStream
153
+ */
154
+ EchoBidiStream(request: MessageStream<EchoMsg>, abortSignal: AbortSignal, context: ServerContext): MessageStream<EchoMsg>;
155
+ /**
156
+ * RpcStream opens a nested rpc call stream.
157
+ *
158
+ * @generated from rpc echo.Echoer.RpcStream
159
+ */
160
+ RpcStream(request: MessageStream<RpcStreamPacket>, abortSignal: AbortSignal, context: ServerContext): MessageStream<RpcStreamPacket>;
161
+ /**
162
+ * DoNothing does nothing.
163
+ *
164
+ * @generated from rpc echo.Echoer.DoNothing
165
+ */
166
+ DoNothing(request: Empty, abortSignal: AbortSignal, context: ServerContext): Promise<Empty>;
167
+ }
125
168
  export declare const EchoerServiceName: "echo.Echoer";
126
169
  export declare class EchoerClient implements Echoer {
127
170
  private readonly rpc;
@@ -2,16 +2,17 @@ import { Message } from '@aptre/protobuf-es-lite';
2
2
  import { Empty } from '@aptre/protobuf-es-lite/google/protobuf/empty';
3
3
  import { EchoMsg } from './echo.pb.js';
4
4
  import { Server } from '../srpc/server.js';
5
+ import type { ServerContext } from '../srpc/server-context.js';
5
6
  import { RpcStreamPacket } from '../rpcstream/rpcstream.pb.js';
6
7
  import { MessageStream } from '../srpc/message.js';
7
- import { Echoer } from './echo_srpc.pb.js';
8
- export declare class EchoerServer implements Echoer {
8
+ import type { EchoerHandler } from './echo_srpc.pb.js';
9
+ export declare class EchoerServer implements EchoerHandler {
9
10
  private proxyServer?;
10
11
  constructor(proxyServer?: Server);
11
- Echo(request: EchoMsg): Promise<Message<EchoMsg>>;
12
- EchoServerStream(request: EchoMsg): MessageStream<EchoMsg>;
13
- EchoClientStream(request: MessageStream<EchoMsg>): Promise<Message<EchoMsg>>;
14
- EchoBidiStream(request: MessageStream<EchoMsg>): MessageStream<EchoMsg>;
15
- RpcStream(request: MessageStream<RpcStreamPacket>): MessageStream<RpcStreamPacket>;
16
- DoNothing(): Promise<Empty>;
12
+ Echo(request: EchoMsg, _abortSignal: AbortSignal, _context: ServerContext): Promise<Message<EchoMsg>>;
13
+ EchoServerStream(request: EchoMsg, _abortSignal: AbortSignal, _context: ServerContext): MessageStream<EchoMsg>;
14
+ EchoClientStream(request: MessageStream<EchoMsg>, _abortSignal: AbortSignal, _context: ServerContext): Promise<Message<EchoMsg>>;
15
+ EchoBidiStream(request: MessageStream<EchoMsg>, _abortSignal: AbortSignal, _context: ServerContext): MessageStream<EchoMsg>;
16
+ RpcStream(request: MessageStream<RpcStreamPacket>, _abortSignal: AbortSignal, _context: ServerContext): MessageStream<RpcStreamPacket>;
17
+ DoNothing(_request: Empty, _abortSignal: AbortSignal, _context: ServerContext): Promise<Empty>;
17
18
  }
@@ -8,16 +8,16 @@ export class EchoerServer {
8
8
  constructor(proxyServer) {
9
9
  this.proxyServer = proxyServer;
10
10
  }
11
- async Echo(request) {
11
+ async Echo(request, _abortSignal, _context) {
12
12
  return request;
13
13
  }
14
- async *EchoServerStream(request) {
14
+ async *EchoServerStream(request, _abortSignal, _context) {
15
15
  for (let i = 0; i < 5; i++) {
16
16
  yield request;
17
17
  await new Promise((resolve) => setTimeout(resolve, 200));
18
18
  }
19
19
  }
20
- async EchoClientStream(request) {
20
+ async EchoClientStream(request, _abortSignal, _context) {
21
21
  // return the first message sent by the client.
22
22
  const message = await first(request);
23
23
  if (!message) {
@@ -25,14 +25,14 @@ export class EchoerServer {
25
25
  }
26
26
  return message;
27
27
  }
28
- EchoBidiStream(request) {
28
+ EchoBidiStream(request, _abortSignal, _context) {
29
29
  // build result observable
30
30
  const result = messagePushable();
31
31
  result.push({ body: 'hello from server' });
32
32
  writeToPushable(request, result);
33
33
  return result;
34
34
  }
35
- RpcStream(request) {
35
+ RpcStream(request, _abortSignal, _context) {
36
36
  return handleRpcStream(request[Symbol.asyncIterator](), async () => {
37
37
  if (!this.proxyServer) {
38
38
  throw new Error('rpc stream proxy server not set');
@@ -40,7 +40,7 @@ export class EchoerServer {
40
40
  return this.proxyServer.rpcStreamHandler;
41
41
  });
42
42
  }
43
- async DoNothing() {
43
+ async DoNothing(_request, _abortSignal, _context) {
44
44
  return {};
45
45
  }
46
46
  }
@@ -1,6 +1,6 @@
1
1
  import { MockMsg } from './mock.pb.js';
2
2
  import { MethodKind } from '@aptre/protobuf-es-lite';
3
- import { ProtoRpc } from 'starpc';
3
+ import { ProtoRpc, ServerContext } from 'starpc';
4
4
  /**
5
5
  * Mock service mocks some RPCs for the e2e tests.
6
6
  *
@@ -35,6 +35,19 @@ export interface Mock {
35
35
  */
36
36
  MockRequest(request: MockMsg, abortSignal?: AbortSignal): Promise<MockMsg>;
37
37
  }
38
+ /**
39
+ * Mock service mocks some RPCs for the e2e tests.
40
+ *
41
+ * @generated from service e2e.mock.Mock
42
+ */
43
+ export interface MockHandler {
44
+ /**
45
+ * MockRequest runs a mock unary request.
46
+ *
47
+ * @generated from rpc e2e.mock.Mock.MockRequest
48
+ */
49
+ MockRequest(request: MockMsg, abortSignal: AbortSignal, context: ServerContext): Promise<MockMsg>;
50
+ }
38
51
  export declare const MockServiceName: "e2e.mock.Mock";
39
52
  export declare class MockClient implements Mock {
40
53
  private readonly rpc;
@@ -1,6 +1,9 @@
1
1
  import { pushable } from 'it-pushable';
2
2
  import { Watchdog } from './watchdog.js';
3
3
  import { ERR_STREAM_IDLE } from './errors.js';
4
+ function isMessagePort(channel) {
5
+ return 'postMessage' in channel && 'start' in channel;
6
+ }
4
7
  // ChannelStream implements a Stream over a BroadcastChannel duplex or MessagePort.
5
8
  //
6
9
  // NOTE: there is no way to tell if a BroadcastChannel or MessagePort is closed.
@@ -99,7 +102,7 @@ export class ChannelStream {
99
102
  this._source = source;
100
103
  // wire up the message handlers
101
104
  const onMessage = this.onMessage.bind(this);
102
- if (channel instanceof MessagePort) {
105
+ if (isMessagePort(channel)) {
103
106
  // MessagePort
104
107
  channel.onmessage = onMessage;
105
108
  channel.start();
@@ -124,7 +127,7 @@ export class ChannelStream {
124
127
  return;
125
128
  }
126
129
  msg.from = this.localId;
127
- if (this.channel instanceof MessagePort) {
130
+ if (isMessagePort(this.channel)) {
128
131
  this.channel.postMessage(msg);
129
132
  }
130
133
  else {
@@ -165,7 +168,7 @@ export class ChannelStream {
165
168
  this.localWriteClosed = true;
166
169
  this.remoteWriteClosed = true;
167
170
  // close channels
168
- if (this.channel instanceof MessagePort) {
171
+ if (isMessagePort(this.channel)) {
169
172
  this.channel.onmessage = null;
170
173
  this.channel.close();
171
174
  }
@@ -1,7 +1,26 @@
1
- import { describe, expect, it } from 'vitest';
1
+ import { describe, expect, it, vi } from 'vitest';
2
2
  import { pushable } from 'it-pushable';
3
3
  import { ChannelStream } from './channel.js';
4
4
  describe('ChannelStream', () => {
5
+ it('recognizes MessagePort implementations from another realm', () => {
6
+ const port = {
7
+ close: vi.fn(),
8
+ onmessage: null,
9
+ postMessage: vi.fn(),
10
+ start: vi.fn(),
11
+ };
12
+ const stream = new ChannelStream('client', port);
13
+ try {
14
+ expect(port.start).toHaveBeenCalledOnce();
15
+ expect(port.postMessage).toHaveBeenCalledWith({
16
+ ack: true,
17
+ from: 'client',
18
+ });
19
+ }
20
+ finally {
21
+ stream.close();
22
+ }
23
+ });
5
24
  it('keeps MessagePort peer writes open after local source completes normally', async () => {
6
25
  const { port1, port2 } = new MessageChannel();
7
26
  const client = new ChannelStream('client', port1);
@@ -1,6 +1,14 @@
1
1
  import type { Sink, Source } from 'it-stream-types';
2
- import { ServiceDefinition, ServiceMethodDefinitions } from './definition.js';
3
- export type InvokeFn = (dataSource: Source<Uint8Array>, dataSink: Sink<Source<Uint8Array>>, invocation?: AbortSignal) => 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>;
4
12
  export interface Handler {
5
13
  getServiceID(): string;
6
14
  getMethodIDs(): string[];
@@ -17,4 +25,5 @@ export declare class StaticHandler implements Handler {
17
25
  getMethodIDs(): string[];
18
26
  lookupMethod(serviceID: string, methodID: string): Promise<InvokeFn | null>;
19
27
  }
20
- 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 {};
@@ -27,3 +27,5 @@ export { HandleStreamCtr } from './handle-stream-ctr.js';
27
27
  export { writeToPushable, buildPushableSink, messagePushable, } from './pushable.js';
28
28
  export { Watchdog } from './watchdog.js';
29
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';
@@ -18,3 +18,4 @@ export { OpenStreamCtr } from './open-stream-ctr.js';
18
18
  export { HandleStreamCtr } from './handle-stream-ctr.js';
19
19
  export { writeToPushable, buildPushableSink, messagePushable, } from './pushable.js';
20
20
  export { Watchdog } from './watchdog.js';
21
+ export { createContextKey, serverContextValue, withServerContextValue, } from './server-context.js';
@@ -1,6 +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
- export type MethodProto<R extends Message<R>, O extends Message<O>> = ((request: R, invocation?: AbortSignal) => Promise<O>) | ((request: R, invocation?: AbortSignal) => AsyncIterable<O>) | ((request: AsyncIterable<R>, invocation?: AbortSignal) => Promise<O>) | ((request: AsyncIterable<R>, invocation?: AbortSignal) => 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>);
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;
@@ -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
  }
@@ -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
+ }
@@ -38,7 +38,9 @@ export class ServerRPC extends CommonRPC {
38
38
  async invokeRPC(invokeFn) {
39
39
  const dataSink = this._createDataSink();
40
40
  try {
41
- await invokeFn(this.rpcDataSource, dataSink, this.invocationSignal);
41
+ await invokeFn(this.rpcDataSource, dataSink, {
42
+ signal: this.invocationSignal,
43
+ });
42
44
  }
43
45
  catch (err) {
44
46
  this.close(err);
@@ -1,6 +1,6 @@
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';
3
+ import { createHandler, createMux, Server, Client, StreamConn, ChannelStream, combineUint8ArrayListTransform, Packet, createContextKey, serverContextValue, withServerContextValue, } from '../srpc/index.js';
4
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', () => {
@@ -39,12 +39,17 @@ 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 signal after async request decode', async () => {
42
+ it('passes the exact invocation context after async request decode', async () => {
43
43
  const controller = new AbortController();
44
- let observedSignal;
44
+ const callerKey = createContextKey();
45
+ let observedAbortSignal;
46
+ let observedContextSignal;
47
+ let observedCaller;
45
48
  const handler = createHandler(EchoerDefinition, {
46
- Echo: async (request, signal) => {
47
- observedSignal = signal;
49
+ Echo: async (request, abortSignal, context) => {
50
+ observedAbortSignal = abortSignal;
51
+ observedContextSignal = context.signal;
52
+ observedCaller = serverContextValue(context, callerKey);
48
53
  return request;
49
54
  },
50
55
  });
@@ -62,8 +67,32 @@ describe('srpc server', () => {
62
67
  // Drain the encoded response so the invocation pipeline completes.
63
68
  }
64
69
  drained.resolve();
65
- }, controller.signal);
70
+ }, withServerContextValue({ signal: controller.signal }, callerKey, 'caller-1'));
66
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 });
67
96
  expect(observedSignal).toBe(controller.signal);
68
97
  });
69
98
  it('keeps detached server-streaming responses open after request source completes', async () => {
@@ -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', () => {
@@ -11,6 +11,7 @@ import {
11
11
  buildEncodeMessageTransform,
12
12
  MessageStream,
13
13
  ProtoRpc,
14
+ ServerContext,
14
15
  } from 'starpc'
15
16
 
16
17
  /**
@@ -151,6 +152,79 @@ export interface Echoer {
151
152
  DoNothing(request: Empty, abortSignal?: AbortSignal): Promise<Empty>
152
153
  }
153
154
 
155
+ /**
156
+ * Echoer service returns the given message.
157
+ *
158
+ * @generated from service echo.Echoer
159
+ */
160
+ export interface EchoerHandler {
161
+ /**
162
+ * Echo returns the given message.
163
+ *
164
+ * @generated from rpc echo.Echoer.Echo
165
+ */
166
+ Echo(
167
+ request: EchoMsg,
168
+ abortSignal: AbortSignal,
169
+ context: ServerContext,
170
+ ): Promise<EchoMsg>
171
+
172
+ /**
173
+ * EchoServerStream is an example of a server -> client one-way stream.
174
+ *
175
+ * @generated from rpc echo.Echoer.EchoServerStream
176
+ */
177
+ EchoServerStream(
178
+ request: EchoMsg,
179
+ abortSignal: AbortSignal,
180
+ context: ServerContext,
181
+ ): MessageStream<EchoMsg>
182
+
183
+ /**
184
+ * EchoClientStream is an example of client->server one-way stream.
185
+ *
186
+ * @generated from rpc echo.Echoer.EchoClientStream
187
+ */
188
+ EchoClientStream(
189
+ request: MessageStream<EchoMsg>,
190
+ abortSignal: AbortSignal,
191
+ context: ServerContext,
192
+ ): Promise<EchoMsg>
193
+
194
+ /**
195
+ * EchoBidiStream is an example of a two-way stream.
196
+ *
197
+ * @generated from rpc echo.Echoer.EchoBidiStream
198
+ */
199
+ EchoBidiStream(
200
+ request: MessageStream<EchoMsg>,
201
+ abortSignal: AbortSignal,
202
+ context: ServerContext,
203
+ ): MessageStream<EchoMsg>
204
+
205
+ /**
206
+ * RpcStream opens a nested rpc call stream.
207
+ *
208
+ * @generated from rpc echo.Echoer.RpcStream
209
+ */
210
+ RpcStream(
211
+ request: MessageStream<RpcStreamPacket>,
212
+ abortSignal: AbortSignal,
213
+ context: ServerContext,
214
+ ): MessageStream<RpcStreamPacket>
215
+
216
+ /**
217
+ * DoNothing does nothing.
218
+ *
219
+ * @generated from rpc echo.Echoer.DoNothing
220
+ */
221
+ DoNothing(
222
+ request: Empty,
223
+ abortSignal: AbortSignal,
224
+ context: ServerContext,
225
+ ): Promise<Empty>
226
+ }
227
+
154
228
  export const EchoerServiceName = EchoerDefinition.typeName
155
229
 
156
230
  export class EchoerClient implements Echoer {
package/echo/server.ts CHANGED
@@ -3,14 +3,15 @@ import { Message } from '@aptre/protobuf-es-lite'
3
3
  import { Empty } from '@aptre/protobuf-es-lite/google/protobuf/empty'
4
4
  import { EchoMsg } from './echo.pb.js'
5
5
  import { Server } from '../srpc/server.js'
6
+ import type { ServerContext } from '../srpc/server-context.js'
6
7
  import { messagePushable, writeToPushable } from '../srpc/pushable.js'
7
8
  import { RpcStreamPacket } from '../rpcstream/rpcstream.pb.js'
8
9
  import { MessageStream } from '../srpc/message.js'
9
10
  import { handleRpcStream, RpcStreamHandler } from '../rpcstream/rpcstream.js'
10
- import { Echoer } from './echo_srpc.pb.js'
11
+ import type { EchoerHandler } from './echo_srpc.pb.js'
11
12
 
12
13
  // EchoServer implements the Echoer server.
13
- export class EchoerServer implements Echoer {
14
+ export class EchoerServer implements EchoerHandler {
14
15
  // proxyServer is the server used for RpcStream requests.
15
16
  private proxyServer?: Server
16
17
 
@@ -18,11 +19,19 @@ export class EchoerServer implements Echoer {
18
19
  this.proxyServer = proxyServer
19
20
  }
20
21
 
21
- public async Echo(request: EchoMsg): Promise<Message<EchoMsg>> {
22
+ public async Echo(
23
+ request: EchoMsg,
24
+ _abortSignal: AbortSignal,
25
+ _context: ServerContext,
26
+ ): Promise<Message<EchoMsg>> {
22
27
  return request
23
28
  }
24
29
 
25
- public async *EchoServerStream(request: EchoMsg): MessageStream<EchoMsg> {
30
+ public async *EchoServerStream(
31
+ request: EchoMsg,
32
+ _abortSignal: AbortSignal,
33
+ _context: ServerContext,
34
+ ): MessageStream<EchoMsg> {
26
35
  for (let i = 0; i < 5; i++) {
27
36
  yield request
28
37
  await new Promise((resolve) => setTimeout(resolve, 200))
@@ -31,6 +40,8 @@ export class EchoerServer implements Echoer {
31
40
 
32
41
  public async EchoClientStream(
33
42
  request: MessageStream<EchoMsg>,
43
+ _abortSignal: AbortSignal,
44
+ _context: ServerContext,
34
45
  ): Promise<Message<EchoMsg>> {
35
46
  // return the first message sent by the client.
36
47
  const message = await first(request)
@@ -42,6 +53,8 @@ export class EchoerServer implements Echoer {
42
53
 
43
54
  public EchoBidiStream(
44
55
  request: MessageStream<EchoMsg>,
56
+ _abortSignal: AbortSignal,
57
+ _context: ServerContext,
45
58
  ): MessageStream<EchoMsg> {
46
59
  // build result observable
47
60
  const result = messagePushable<EchoMsg>()
@@ -52,6 +65,8 @@ export class EchoerServer implements Echoer {
52
65
 
53
66
  public RpcStream(
54
67
  request: MessageStream<RpcStreamPacket>,
68
+ _abortSignal: AbortSignal,
69
+ _context: ServerContext,
55
70
  ): MessageStream<RpcStreamPacket> {
56
71
  return handleRpcStream(
57
72
  request[Symbol.asyncIterator](),
@@ -64,7 +79,11 @@ export class EchoerServer implements Echoer {
64
79
  )
65
80
  }
66
81
 
67
- public async DoNothing(): Promise<Empty> {
82
+ public async DoNothing(
83
+ _request: Empty,
84
+ _abortSignal: AbortSignal,
85
+ _context: ServerContext,
86
+ ): Promise<Empty> {
68
87
  return {}
69
88
  }
70
89
  }
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { MockMsg } from './mock.pb.js'
6
6
  import { MethodKind } from '@aptre/protobuf-es-lite'
7
- import { ProtoRpc } from 'starpc'
7
+ import { ProtoRpc, ServerContext } from 'starpc'
8
8
 
9
9
  /**
10
10
  * Mock service mocks some RPCs for the e2e tests.
@@ -42,6 +42,24 @@ export interface Mock {
42
42
  MockRequest(request: MockMsg, abortSignal?: AbortSignal): Promise<MockMsg>
43
43
  }
44
44
 
45
+ /**
46
+ * Mock service mocks some RPCs for the e2e tests.
47
+ *
48
+ * @generated from service e2e.mock.Mock
49
+ */
50
+ export interface MockHandler {
51
+ /**
52
+ * MockRequest runs a mock unary request.
53
+ *
54
+ * @generated from rpc e2e.mock.Mock.MockRequest
55
+ */
56
+ MockRequest(
57
+ request: MockMsg,
58
+ abortSignal: AbortSignal,
59
+ context: ServerContext,
60
+ ): Promise<MockMsg>
61
+ }
62
+
45
63
  export const MockServiceName = MockDefinition.typeName
46
64
 
47
65
  export class MockClient implements Mock {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "starpc",
3
- "version": "0.51.0",
3
+ "version": "0.52.0",
4
4
  "description": "Streaming protobuf RPC service protocol over any two-way channel.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -114,7 +114,7 @@
114
114
  "happy-dom": "^20.9.0",
115
115
  "husky": "^9.1.7",
116
116
  "lint-staged": "^17.0.0",
117
- "oxfmt": "0.61.0",
117
+ "oxfmt": "0.62.0",
118
118
  "oxlint": "^1.76.0",
119
119
  "rimraf": "^6.1.3",
120
120
  "tsx": "^4.20.4",
@@ -1,9 +1,29 @@
1
- import { describe, expect, it } from 'vitest'
1
+ import { describe, expect, it, vi } from 'vitest'
2
2
  import { pushable } from 'it-pushable'
3
3
 
4
4
  import { ChannelStream } from './channel.js'
5
5
 
6
6
  describe('ChannelStream', () => {
7
+ it('recognizes MessagePort implementations from another realm', () => {
8
+ const port = {
9
+ close: vi.fn(),
10
+ onmessage: null,
11
+ postMessage: vi.fn(),
12
+ start: vi.fn(),
13
+ } as unknown as MessagePort
14
+
15
+ const stream = new ChannelStream<Uint8Array>('client', port)
16
+ try {
17
+ expect(port.start).toHaveBeenCalledOnce()
18
+ expect(port.postMessage).toHaveBeenCalledWith({
19
+ ack: true,
20
+ from: 'client',
21
+ })
22
+ } finally {
23
+ stream.close()
24
+ }
25
+ })
26
+
7
27
  it('keeps MessagePort peer writes open after local source completes normally', async () => {
8
28
  const { port1, port2 } = new MessageChannel()
9
29
  const client = new ChannelStream<Uint8Array>('client', port1)
package/srpc/channel.ts CHANGED
@@ -26,6 +26,10 @@ export type ChannelPort =
26
26
  | MessagePort
27
27
  | { tx: BroadcastChannel; rx: BroadcastChannel }
28
28
 
29
+ function isMessagePort(channel: ChannelPort): channel is MessagePort {
30
+ return 'postMessage' in channel && 'start' in channel
31
+ }
32
+
29
33
  // ChannelStreamOpts are options for ChannelStream.
30
34
  export interface ChannelStreamOpts {
31
35
  // remoteOpen indicates that the remote already knows the channel is open.
@@ -149,7 +153,7 @@ export class ChannelStream<T = Uint8Array> implements Duplex<
149
153
 
150
154
  // wire up the message handlers
151
155
  const onMessage = this.onMessage.bind(this)
152
- if (channel instanceof MessagePort) {
156
+ if (isMessagePort(channel)) {
153
157
  // MessagePort
154
158
  channel.onmessage = onMessage
155
159
  channel.start()
@@ -180,7 +184,7 @@ export class ChannelStream<T = Uint8Array> implements Duplex<
180
184
  return
181
185
  }
182
186
  msg.from = this.localId
183
- if (this.channel instanceof MessagePort) {
187
+ if (isMessagePort(this.channel)) {
184
188
  this.channel.postMessage(msg)
185
189
  } else {
186
190
  this.channel.tx.postMessage(msg)
@@ -222,7 +226,7 @@ export class ChannelStream<T = Uint8Array> implements Duplex<
222
226
  this.localWriteClosed = true
223
227
  this.remoteWriteClosed = true
224
228
  // close channels
225
- if (this.channel instanceof MessagePort) {
229
+ if (isMessagePort(this.channel)) {
226
230
  this.channel.onmessage = null
227
231
  this.channel.close()
228
232
  } else {
package/srpc/handler.ts CHANGED
@@ -1,12 +1,58 @@
1
1
  import type { Sink, Source } from 'it-stream-types'
2
- import { ServiceDefinition, ServiceMethodDefinitions } from './definition.js'
2
+ import { MethodKind, type MessageType } from '@aptre/protobuf-es-lite'
3
+ import type { MessageStream } from './message.js'
4
+ import {
5
+ type MethodDefinition,
6
+ ServiceDefinition,
7
+ ServiceMethodDefinitions,
8
+ } from './definition.js'
3
9
  import { createInvokeFn } from './invoker.js'
10
+ import type { ServerContext } from './server-context.js'
11
+
12
+ type MessageOf<T> = T extends MessageType<infer M> ? M : never
13
+
14
+ type ServerMethod<T> =
15
+ T extends MethodDefinition<
16
+ infer Request,
17
+ infer Response,
18
+ infer Kind,
19
+ infer _Idempotency
20
+ >
21
+ ? Kind extends MethodKind.Unary
22
+ ? (
23
+ request: MessageOf<Request>,
24
+ abortSignal: AbortSignal,
25
+ context: ServerContext,
26
+ ) => Promise<MessageOf<Response>>
27
+ : Kind extends MethodKind.ServerStreaming
28
+ ? (
29
+ request: MessageOf<Request>,
30
+ abortSignal: AbortSignal,
31
+ context: ServerContext,
32
+ ) => MessageStream<MessageOf<Response>>
33
+ : Kind extends MethodKind.ClientStreaming
34
+ ? (
35
+ request: MessageStream<MessageOf<Request>>,
36
+ abortSignal: AbortSignal,
37
+ context: ServerContext,
38
+ ) => Promise<MessageOf<Response>>
39
+ : (
40
+ request: MessageStream<MessageOf<Request>>,
41
+ abortSignal: AbortSignal,
42
+ context: ServerContext,
43
+ ) => MessageStream<MessageOf<Response>>
44
+ : never
45
+
46
+ export type HandlerImplementation<T extends ServiceMethodDefinitions> =
47
+ Partial<{
48
+ [Method in keyof T]: ServerMethod<T[Method]>
49
+ }>
4
50
 
5
51
  // InvokeFn describes an SRPC call method invoke function.
6
52
  export type InvokeFn = (
7
53
  dataSource: Source<Uint8Array>,
8
54
  dataSink: Sink<Source<Uint8Array>>,
9
- invocation?: AbortSignal,
55
+ context: ServerContext,
10
56
  ) => Promise<void>
11
57
 
12
58
  // Handler describes a SRPC call handler implementation.
@@ -62,7 +108,11 @@ export class StaticHandler implements Handler {
62
108
  // if serviceID is not set, uses the fullName of the service as the identifier.
63
109
  export function createHandler<
64
110
  T extends ServiceMethodDefinitions = ServiceMethodDefinitions,
65
- >(definition: ServiceDefinition<T>, impl: any, serviceID?: string): Handler {
111
+ >(
112
+ definition: ServiceDefinition<T>,
113
+ impl: HandlerImplementation<T>,
114
+ serviceID?: string,
115
+ ): Handler {
66
116
  // serviceID defaults to the full name of the service from Protobuf.
67
117
  serviceID = serviceID || definition.typeName
68
118
 
@@ -70,7 +120,7 @@ export function createHandler<
70
120
  const methodMap: MethodMap = {}
71
121
  for (const methodInfo of Object.values(definition.methods)) {
72
122
  const methodName = methodInfo.name
73
- let methodProto = impl[methodName]
123
+ let methodProto = impl[methodName as keyof T] as any
74
124
  if (!methodProto) {
75
125
  continue
76
126
  }
package/srpc/index.ts CHANGED
@@ -83,3 +83,10 @@ export {
83
83
  } from './pushable.js'
84
84
  export { Watchdog } from './watchdog.js'
85
85
  export type { ProtoRpc } from './proto-rpc.js'
86
+
87
+ export {
88
+ createContextKey,
89
+ serverContextValue,
90
+ withServerContextValue,
91
+ } from './server-context.js'
92
+ export type { ContextKey, ServerContext } from './server-context.js'
package/srpc/invoker.ts CHANGED
@@ -2,6 +2,7 @@ import { Sink, Source } from 'it-stream-types'
2
2
  import { pushable } from 'it-pushable'
3
3
  import { pipe } from 'it-pipe'
4
4
  import type { MethodDefinition } from './definition.js'
5
+ import type { ServerContext } from './server-context.js'
5
6
  import { InvokeFn } from './handler.js'
6
7
  import {
7
8
  buildDecodeMessageTransform,
@@ -13,10 +14,26 @@ import { MethodIdempotency, MethodKind } from '@aptre/protobuf-es-lite'
13
14
 
14
15
  // MethodProto is a function which matches one of the RPC signatures.
15
16
  export type MethodProto<R extends Message<R>, O extends Message<O>> =
16
- | ((request: R, invocation?: AbortSignal) => Promise<O>)
17
- | ((request: R, invocation?: AbortSignal) => AsyncIterable<O>)
18
- | ((request: AsyncIterable<R>, invocation?: AbortSignal) => Promise<O>)
19
- | ((request: AsyncIterable<R>, invocation?: AbortSignal) => AsyncIterable<O>)
17
+ | ((
18
+ request: R,
19
+ abortSignal: AbortSignal,
20
+ context: ServerContext,
21
+ ) => Promise<O>)
22
+ | ((
23
+ request: R,
24
+ abortSignal: AbortSignal,
25
+ context: ServerContext,
26
+ ) => AsyncIterable<O>)
27
+ | ((
28
+ request: AsyncIterable<R>,
29
+ abortSignal: AbortSignal,
30
+ context: ServerContext,
31
+ ) => Promise<O>)
32
+ | ((
33
+ request: AsyncIterable<R>,
34
+ abortSignal: AbortSignal,
35
+ context: ServerContext,
36
+ ) => AsyncIterable<O>)
20
37
 
21
38
  // createInvokeFn builds an InvokeFn from a method definition and a function prototype.
22
39
  export function createInvokeFn<R extends Message<R>, O extends Message<O>>(
@@ -32,7 +49,7 @@ export function createInvokeFn<R extends Message<R>, O extends Message<O>>(
32
49
  return async (
33
50
  dataSource: Source<Uint8Array>,
34
51
  dataSink: Sink<Source<Uint8Array>>,
35
- invocation?: AbortSignal,
52
+ context: ServerContext,
36
53
  ) => {
37
54
  // responseSink is a Sink for response messages.
38
55
  const responseSink = pushable<O>({
@@ -67,7 +84,7 @@ export function createInvokeFn<R extends Message<R>, O extends Message<O>>(
67
84
 
68
85
  // Call the implementation.
69
86
  try {
70
- const responseObj = methodProto(requestArg, invocation)
87
+ const responseObj = methodProto(requestArg, context.signal, context)
71
88
  if (!responseObj) {
72
89
  throw new Error('return value was undefined')
73
90
  }
@@ -0,0 +1,55 @@
1
+ declare const contextKeyValue: unique symbol
2
+ const contextParent = Symbol('server context parent')
3
+ const contextKey = Symbol('server context key')
4
+ const contextStoredValue = Symbol('server context value')
5
+
6
+ // ContextKey identifies one typed server-context value.
7
+ export interface ContextKey<T> {
8
+ readonly [contextKeyValue]?: T
9
+ }
10
+
11
+ // ServerContext carries cancellation for one server invocation.
12
+ export interface ServerContext {
13
+ readonly signal: AbortSignal
14
+ }
15
+
16
+ type StoredServerContext = ServerContext & {
17
+ readonly [contextParent]?: ServerContext
18
+ readonly [contextKey]?: ContextKey<unknown>
19
+ readonly [contextStoredValue]?: unknown
20
+ }
21
+
22
+ // createContextKey constructs an identity key for one server-context value.
23
+ export function createContextKey<T>(): ContextKey<T> {
24
+ return {}
25
+ }
26
+
27
+ // withServerContextValue derives a context with one immutable typed value.
28
+ export function withServerContextValue<T>(
29
+ context: ServerContext,
30
+ key: ContextKey<T>,
31
+ value: T,
32
+ ): ServerContext {
33
+ const derived: StoredServerContext = {
34
+ signal: context.signal,
35
+ [contextParent]: context,
36
+ [contextKey]: key as ContextKey<unknown>,
37
+ [contextStoredValue]: value,
38
+ }
39
+ return derived
40
+ }
41
+
42
+ // serverContextValue retrieves the nearest value for a typed identity key.
43
+ export function serverContextValue<T>(
44
+ context: ServerContext,
45
+ key: ContextKey<T>,
46
+ ): T | undefined {
47
+ let current: StoredServerContext | undefined = context as StoredServerContext
48
+ while (current) {
49
+ if (current[contextKey] === key) {
50
+ return current[contextStoredValue] as T
51
+ }
52
+ current = current[contextParent] as StoredServerContext | undefined
53
+ }
54
+ return undefined
55
+ }
@@ -4,6 +4,7 @@ import type { CallData, CallStart } from './rpcproto.pb.js'
4
4
  import { CommonRPC } from './common-rpc.js'
5
5
  import { InvokeFn } from './handler.js'
6
6
  import { LookupMethod } from './mux.js'
7
+ import type { ServerContext } from './server-context.js'
7
8
 
8
9
  // ServerRPC is an ongoing RPC from the server side.
9
10
  export class ServerRPC extends CommonRPC {
@@ -47,7 +48,9 @@ export class ServerRPC extends CommonRPC {
47
48
  private async invokeRPC(invokeFn: InvokeFn) {
48
49
  const dataSink = this._createDataSink()
49
50
  try {
50
- await invokeFn(this.rpcDataSource, dataSink, this.invocationSignal)
51
+ await invokeFn(this.rpcDataSource, dataSink, {
52
+ signal: this.invocationSignal,
53
+ } satisfies ServerContext)
51
54
  } catch (err) {
52
55
  this.close(err as Error)
53
56
  }
@@ -10,6 +10,10 @@ import {
10
10
  combineUint8ArrayListTransform,
11
11
  ChannelStreamOpts,
12
12
  Packet,
13
+ createContextKey,
14
+ serverContextValue,
15
+ withServerContextValue,
16
+ type ServerContext,
13
17
  } from '../srpc/index.js'
14
18
  import {
15
19
  EchoerDefinition,
@@ -78,12 +82,21 @@ describe('srpc server', () => {
78
82
  it('should pass rpc stream tests', async () => {
79
83
  await runRpcStreamTest(client)
80
84
  })
81
- it('passes the exact invocation signal after async request decode', async () => {
85
+ it('passes the exact invocation context after async request decode', async () => {
82
86
  const controller = new AbortController()
83
- let observedSignal: AbortSignal | undefined
87
+ const callerKey = createContextKey<string>()
88
+ let observedAbortSignal: AbortSignal | undefined
89
+ let observedContextSignal: AbortSignal | undefined
90
+ let observedCaller: string | undefined
84
91
  const handler = createHandler(EchoerDefinition, {
85
- Echo: async (request: EchoMsgType, signal?: AbortSignal) => {
86
- observedSignal = signal
92
+ Echo: async (
93
+ request: EchoMsgType,
94
+ abortSignal: AbortSignal,
95
+ context: ServerContext,
96
+ ) => {
97
+ observedAbortSignal = abortSignal
98
+ observedContextSignal = context.signal
99
+ observedCaller = serverContextValue(context, callerKey)
87
100
  return request
88
101
  },
89
102
  })
@@ -104,9 +117,41 @@ describe('srpc server', () => {
104
117
  }
105
118
  drained.resolve()
106
119
  },
107
- controller.signal,
120
+ withServerContextValue(
121
+ { signal: controller.signal },
122
+ callerKey,
123
+ 'caller-1',
124
+ ),
108
125
  )
109
126
  await drained.promise
127
+ expect(observedAbortSignal).toBe(controller.signal)
128
+ expect(observedContextSignal).toBe(controller.signal)
129
+ expect(observedCaller).toBe('caller-1')
130
+ })
131
+
132
+ it('keeps two-argument server handlers compatible', async () => {
133
+ const controller = new AbortController()
134
+ let observedSignal: AbortSignal | undefined
135
+ const handler = createHandler(EchoerDefinition, {
136
+ Echo: async (request: EchoMsgType, abortSignal?: AbortSignal) => {
137
+ observedSignal = abortSignal
138
+ return request
139
+ },
140
+ })
141
+ const invokeFn = await handler.lookupMethod(EchoerServiceName, 'Echo')
142
+ if (!invokeFn) throw new Error('Echo method was not found')
143
+ const request = EchoMsg.create({ body: 'legacy handler' })
144
+ await invokeFn(
145
+ (async function* () {
146
+ yield EchoMsg.toBinary(request)
147
+ })(),
148
+ async (source) => {
149
+ for await (const _data of source) {
150
+ // Drain the response.
151
+ }
152
+ },
153
+ { signal: controller.signal },
154
+ )
110
155
  expect(observedSignal).toBe(controller.signal)
111
156
  })
112
157
 
@@ -7,6 +7,7 @@ describe('Watchdog', () => {
7
7
  })
8
8
 
9
9
  afterEach(() => {
10
+ vi.useRealTimers()
10
11
  vi.restoreAllMocks()
11
12
  })
12
13