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.
- package/cmd/protoc-gen-es-starpc/typescript.ts +37 -0
- package/dist/cmd/protoc-gen-es-starpc/typescript.js +24 -0
- package/dist/echo/echo_srpc.pb.d.ts +44 -1
- package/dist/echo/server.d.ts +9 -8
- package/dist/echo/server.js +6 -6
- package/dist/mock/mock_srpc.pb.d.ts +14 -1
- package/dist/srpc/channel.js +6 -3
- package/dist/srpc/channel.test.js +20 -1
- package/dist/srpc/handler.d.ts +12 -3
- package/dist/srpc/index.d.ts +2 -0
- package/dist/srpc/index.js +1 -0
- package/dist/srpc/invoker.d.ts +2 -1
- package/dist/srpc/invoker.js +2 -2
- package/dist/srpc/server-context.d.ts +11 -0
- package/dist/srpc/server-context.js +28 -0
- package/dist/srpc/server-rpc.js +3 -1
- package/dist/srpc/server.test.js +35 -6
- package/dist/srpc/watchdog.test.js +1 -0
- package/echo/echo_srpc.pb.ts +74 -0
- package/echo/server.ts +24 -5
- package/mock/mock_srpc.pb.ts +19 -1
- package/package.json +2 -2
- package/srpc/channel.test.ts +21 -1
- package/srpc/channel.ts +7 -3
- package/srpc/handler.ts +54 -4
- package/srpc/index.ts +7 -0
- package/srpc/invoker.ts +23 -6
- package/srpc/server-context.ts +55 -0
- package/srpc/server-rpc.ts +4 -1
- package/srpc/server.test.ts +50 -5
- package/srpc/watchdog.test.ts +1 -0
|
@@ -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;
|
package/dist/echo/server.d.ts
CHANGED
|
@@ -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 {
|
|
8
|
-
export declare class EchoerServer implements
|
|
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
|
|
14
|
-
EchoBidiStream(request: MessageStream<EchoMsg
|
|
15
|
-
RpcStream(request: 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
|
}
|
package/dist/echo/server.js
CHANGED
|
@@ -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;
|
package/dist/srpc/channel.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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);
|
package/dist/srpc/handler.d.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { Sink, Source } from 'it-stream-types';
|
|
2
|
-
import {
|
|
3
|
-
|
|
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:
|
|
28
|
+
export declare function createHandler<T extends ServiceMethodDefinitions = ServiceMethodDefinitions>(definition: ServiceDefinition<T>, impl: HandlerImplementation<T>, serviceID?: string): Handler;
|
|
29
|
+
export {};
|
package/dist/srpc/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/srpc/index.js
CHANGED
|
@@ -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';
|
package/dist/srpc/invoker.d.ts
CHANGED
|
@@ -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,
|
|
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;
|
package/dist/srpc/invoker.js
CHANGED
|
@@ -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,
|
|
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,
|
|
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
|
+
}
|
package/dist/srpc/server-rpc.js
CHANGED
|
@@ -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,
|
|
41
|
+
await invokeFn(this.rpcDataSource, dataSink, {
|
|
42
|
+
signal: this.invocationSignal,
|
|
43
|
+
});
|
|
42
44
|
}
|
|
43
45
|
catch (err) {
|
|
44
46
|
this.close(err);
|
package/dist/srpc/server.test.js
CHANGED
|
@@ -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
|
|
42
|
+
it('passes the exact invocation context after async request decode', async () => {
|
|
43
43
|
const controller = new AbortController();
|
|
44
|
-
|
|
44
|
+
const callerKey = createContextKey();
|
|
45
|
+
let observedAbortSignal;
|
|
46
|
+
let observedContextSignal;
|
|
47
|
+
let observedCaller;
|
|
45
48
|
const handler = createHandler(EchoerDefinition, {
|
|
46
|
-
Echo: async (request,
|
|
47
|
-
|
|
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 () => {
|
package/echo/echo_srpc.pb.ts
CHANGED
|
@@ -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 {
|
|
11
|
+
import type { EchoerHandler } from './echo_srpc.pb.js'
|
|
11
12
|
|
|
12
13
|
// EchoServer implements the Echoer server.
|
|
13
|
-
export class EchoerServer implements
|
|
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(
|
|
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(
|
|
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(
|
|
82
|
+
public async DoNothing(
|
|
83
|
+
_request: Empty,
|
|
84
|
+
_abortSignal: AbortSignal,
|
|
85
|
+
_context: ServerContext,
|
|
86
|
+
): Promise<Empty> {
|
|
68
87
|
return {}
|
|
69
88
|
}
|
|
70
89
|
}
|
package/mock/mock_srpc.pb.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
117
|
+
"oxfmt": "0.62.0",
|
|
118
118
|
"oxlint": "^1.76.0",
|
|
119
119
|
"rimraf": "^6.1.3",
|
|
120
120
|
"tsx": "^4.20.4",
|
package/srpc/channel.test.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
-
>(
|
|
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
|
-
| ((
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
+
}
|
package/srpc/server-rpc.ts
CHANGED
|
@@ -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,
|
|
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
|
}
|
package/srpc/server.test.ts
CHANGED
|
@@ -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
|
|
85
|
+
it('passes the exact invocation context after async request decode', async () => {
|
|
82
86
|
const controller = new AbortController()
|
|
83
|
-
|
|
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 (
|
|
86
|
-
|
|
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
|
-
|
|
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
|
|