grpc-dc 0.0.0-stage → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -2
- package/index.d.ts +193 -0
- package/index.js +51 -0
- package/package.json +41 -4
- package/src/channel.js +228 -0
- package/src/codec.js +220 -0
- package/src/dynamic.js +426 -0
- package/src/endpoint.js +269 -0
- package/src/loopback.js +60 -0
- package/src/server.js +126 -0
- package/src/wasm_bridge.js +241 -0
- package/src/webrtc_direct.js +119 -0
- package/wire_gen.js +50 -0
package/README.md
CHANGED
|
@@ -1,3 +1,34 @@
|
|
|
1
|
-
#
|
|
1
|
+
# JavaScript (browser + Node) — grpc-dc runtime
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Start here if you are new to the project. The runtime in
|
|
4
|
+
this directory is generated, dependency-free, and speaks
|
|
5
|
+
the gRPC wire protocol over WebRTC DataChannels.
|
|
6
|
+
|
|
7
|
+
## First run
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
node js/examples/echo_demo.js # all four method shapes, in-memory pair
|
|
11
|
+
node --test js/test/*.test.js # full suite
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Use it in your project
|
|
15
|
+
|
|
16
|
+
npm install grpc-dc — or copy this directory; there are no dependencies. TypeScript types ship in `index.d.ts`.
|
|
17
|
+
|
|
18
|
+
## Regenerate stubs for your schema
|
|
19
|
+
|
|
20
|
+
From the repository root (one wrapper per language in `bin/`):
|
|
21
|
+
|
|
22
|
+
protoc --plugin=protoc-gen-grpc-dc-js=$PWD/bin/protoc-gen-grpc-dc-js \
|
|
23
|
+
--grpc-dc-js_out=out -I . your.proto
|
|
24
|
+
|
|
25
|
+
Or generate in the browser and download the zip:
|
|
26
|
+
https://browser-grpc.github.io/grpc-over-datachannels/app/
|
|
27
|
+
|
|
28
|
+
## Where things live
|
|
29
|
+
|
|
30
|
+
- Runtime: this directory (see its generated sources).
|
|
31
|
+
- Worked example: `examples/` (Echo in every method shape).
|
|
32
|
+
- Tests: `tests/` — the same BDD scenarios every language runs.
|
|
33
|
+
|
|
34
|
+
Full project quickstart: [`../QUICKSTART.md`](../QUICKSTART.md).
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// TypeScript declarations for the grpc-dc JavaScript runtime.
|
|
2
|
+
// Hand-written against js/index.js + js/src/*; generated stubs
|
|
3
|
+
// (echo_*_grpc_dc.js etc.) are plain JS and can be wrapped with
|
|
4
|
+
// typed facades using the Channel generics below.
|
|
5
|
+
|
|
6
|
+
export type Metadata = Record<string, string>;
|
|
7
|
+
|
|
8
|
+
export interface CallOptions {
|
|
9
|
+
metadata?: Metadata;
|
|
10
|
+
timeout?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface MessageType<T> {
|
|
14
|
+
encode(message: T): Uint8Array;
|
|
15
|
+
decode(bytes: Uint8Array): T;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export declare const FrameType: Record<string, number>;
|
|
19
|
+
export declare const StatusCode: Record<string, number>;
|
|
20
|
+
export declare const StatusName: Record<number, string>;
|
|
21
|
+
|
|
22
|
+
export declare class StatusError extends Error {
|
|
23
|
+
code: number;
|
|
24
|
+
constructor(code: number, message?: string);
|
|
25
|
+
static ok(): StatusError;
|
|
26
|
+
static unimplemented(message: string): StatusError;
|
|
27
|
+
static internal(message: string): StatusError;
|
|
28
|
+
static unavailable(message: string): StatusError;
|
|
29
|
+
static invalidArgument(message: string): StatusError;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export declare function encodeFrame(
|
|
33
|
+
type: number, streamId: number, payload?: Uint8Array): Uint8Array;
|
|
34
|
+
export declare function decodeFrame(
|
|
35
|
+
data: Uint8Array | ArrayBuffer): { type: number; streamId: number; payload: Uint8Array };
|
|
36
|
+
export declare function encodeTimeout(timeoutMs: number): string;
|
|
37
|
+
export declare function parseTimeout(text: string): number | null;
|
|
38
|
+
export declare function setTracer(tracer: unknown): void;
|
|
39
|
+
export declare function setTraceFilter(patterns: unknown): void;
|
|
40
|
+
export declare function toHex(bytes: Uint8Array): string;
|
|
41
|
+
export declare function fromHex(hex: string): Uint8Array;
|
|
42
|
+
|
|
43
|
+
export type DataChannelLike = {
|
|
44
|
+
send(data: unknown): void;
|
|
45
|
+
addEventListener?: (type: string, listener: (event: unknown) => void) => void;
|
|
46
|
+
binaryType?: string;
|
|
47
|
+
readyState?: string;
|
|
48
|
+
bufferedAmount?: number;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export declare class StreamInput<T = unknown> {
|
|
52
|
+
push(item: T): void;
|
|
53
|
+
end(): void;
|
|
54
|
+
fail(error: unknown): void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export declare class Endpoint {
|
|
58
|
+
dispatcher: unknown;
|
|
59
|
+
constructor(dc: DataChannelLike, options?: { parity?: "odd" | "even" });
|
|
60
|
+
openStream(): StreamInput;
|
|
61
|
+
closeStream(id: number): void;
|
|
62
|
+
sendFrame(frameBytes: Uint8Array): Promise<void>;
|
|
63
|
+
closeAll(): void;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export declare class Channel {
|
|
67
|
+
readonly endpoint: Endpoint;
|
|
68
|
+
constructor(dcOrEndpoint: DataChannelLike | Endpoint,
|
|
69
|
+
options?: { parity?: "odd" | "even" });
|
|
70
|
+
unary<Req, Res>(path: string, request: Req,
|
|
71
|
+
reqType: MessageType<Req>, resType: MessageType<Res>,
|
|
72
|
+
options?: CallOptions): Promise<Res>;
|
|
73
|
+
serverStream<Req, Res>(path: string, request: Req,
|
|
74
|
+
reqType: MessageType<Req>, resType: MessageType<Res>,
|
|
75
|
+
options?: CallOptions): Promise<AsyncIterable<Res>>;
|
|
76
|
+
clientStream<Req, Res>(path: string, requests: AsyncIterable<Req>,
|
|
77
|
+
reqType: MessageType<Req>, resType: MessageType<Res>,
|
|
78
|
+
options?: CallOptions): Promise<Res>;
|
|
79
|
+
bidi<Req, Res>(path: string, requests: AsyncIterable<Req>,
|
|
80
|
+
reqType: MessageType<Req>, resType: MessageType<Res>,
|
|
81
|
+
options?: CallOptions): AsyncIterable<Res>;
|
|
82
|
+
cancel(streamId: number): Promise<void>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface MethodDef {
|
|
86
|
+
path: string;
|
|
87
|
+
kind: "unary" | "server-streaming" | "client-streaming" | "bidi";
|
|
88
|
+
requestType: MessageType<unknown>;
|
|
89
|
+
responseType: MessageType<unknown>;
|
|
90
|
+
handler: (...args: never[]) => unknown;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface ServiceDef {
|
|
94
|
+
name: string;
|
|
95
|
+
methods: Record<string, MethodDef>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface CallContext {
|
|
99
|
+
path: string;
|
|
100
|
+
metadata: Metadata;
|
|
101
|
+
streamId: number;
|
|
102
|
+
deadline: number | null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export declare class Server {
|
|
106
|
+
addService(definition: ServiceDef): void;
|
|
107
|
+
serve(dcOrEndpoint: DataChannelLike | Endpoint,
|
|
108
|
+
options?: { parity?: "odd" | "even" }): void;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export declare function memoryDataChannelPair(): [DataChannelLike, DataChannelLike];
|
|
112
|
+
|
|
113
|
+
export declare function createPeer(
|
|
114
|
+
dc: DataChannelLike,
|
|
115
|
+
options?: { parity?: "odd" | "even" },
|
|
116
|
+
): { endpoint: Endpoint; server: Server; channel: Channel };
|
|
117
|
+
|
|
118
|
+
export declare class GenericMessage {
|
|
119
|
+
constructor(model?: unknown);
|
|
120
|
+
set(number: number, value: unknown): void;
|
|
121
|
+
add(number: number, value: unknown): void;
|
|
122
|
+
getAll(number: number): unknown[];
|
|
123
|
+
get(number: number, fallback?: unknown): unknown;
|
|
124
|
+
getString(number: number, fallback?: string): string;
|
|
125
|
+
getInt(number: number, fallback?: number): number;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface FileModel {
|
|
129
|
+
services: Map<string, unknown>;
|
|
130
|
+
messages: Map<string, unknown>;
|
|
131
|
+
enums: Map<string, unknown>;
|
|
132
|
+
fileName: string;
|
|
133
|
+
packageName: string;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export declare function parseDescriptorSet(data: Uint8Array): FileModel;
|
|
137
|
+
export declare function createDynamicService(
|
|
138
|
+
fileModel: FileModel, serviceName: string,
|
|
139
|
+
handler: Record<string, (...args: never[]) => unknown>): ServiceDef;
|
|
140
|
+
|
|
141
|
+
export declare class DynamicClient {
|
|
142
|
+
constructor(channel: Channel, fileModel: FileModel);
|
|
143
|
+
unary(serviceName: string, methodName: string, request: GenericMessage,
|
|
144
|
+
options?: CallOptions): Promise<GenericMessage>;
|
|
145
|
+
serverStream(serviceName: string, methodName: string, request: GenericMessage,
|
|
146
|
+
options?: CallOptions): Promise<AsyncIterable<GenericMessage>>;
|
|
147
|
+
clientStream(serviceName: string, methodName: string,
|
|
148
|
+
requests: AsyncIterable<GenericMessage>,
|
|
149
|
+
options?: CallOptions): Promise<GenericMessage>;
|
|
150
|
+
bidi(serviceName: string, methodName: string,
|
|
151
|
+
requests: AsyncIterable<GenericMessage>,
|
|
152
|
+
options?: CallOptions): AsyncIterable<GenericMessage>;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface GrpcDcBridge {
|
|
156
|
+
unary(path: string, payload: Uint8Array,
|
|
157
|
+
options?: CallOptions): Promise<Uint8Array>;
|
|
158
|
+
serverStream(path: string, payload: Uint8Array,
|
|
159
|
+
onMessage: (message: Uint8Array) => void,
|
|
160
|
+
options?: CallOptions): Promise<void>;
|
|
161
|
+
clientStream(path: string, payloads: Uint8Array[] | AsyncIterable<Uint8Array>,
|
|
162
|
+
options?: CallOptions): Promise<Uint8Array>;
|
|
163
|
+
bidi(path: string, payloads: Uint8Array[] | AsyncIterable<Uint8Array>,
|
|
164
|
+
onMessage: (message: Uint8Array) => void,
|
|
165
|
+
options?: CallOptions): Promise<void>;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export declare function createGrpcDcBridge(
|
|
169
|
+
dcOrChannel: DataChannelLike | Channel): GrpcDcBridge;
|
|
170
|
+
|
|
171
|
+
export interface GrpcDcServerBridge {
|
|
172
|
+
registerUnary(path: string,
|
|
173
|
+
handler: (payload: Uint8Array, metadata: [string, string][]) => Promise<Uint8Array> | Uint8Array): void;
|
|
174
|
+
registerServerStream(path: string,
|
|
175
|
+
handler: (payload: Uint8Array, metadata: [string, string][],
|
|
176
|
+
onMessage: (message: Uint8Array) => void) => unknown): void;
|
|
177
|
+
registerClientStream(path: string,
|
|
178
|
+
handler: (payloads: Uint8Array[], metadata: [string, string][]) => Promise<Uint8Array> | Uint8Array): void;
|
|
179
|
+
registerBidi(path: string,
|
|
180
|
+
handler: (payloads: Uint8Array[], metadata: [string, string][],
|
|
181
|
+
onMessage: (message: Uint8Array) => void) => unknown): void;
|
|
182
|
+
registerClientStreamInc(path: string,
|
|
183
|
+
handler: (source: { next(): Promise<Uint8Array | null> },
|
|
184
|
+
metadata: [string, string][]) => Promise<Uint8Array> | Uint8Array): void;
|
|
185
|
+
registerBidiInc(path: string,
|
|
186
|
+
handler: (source: { next(): Promise<Uint8Array | null> },
|
|
187
|
+
metadata: [string, string][],
|
|
188
|
+
onMessage: (message: Uint8Array) => void) => unknown): void;
|
|
189
|
+
start(serviceName?: string): void;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export declare function createGrpcDcServerBridge(
|
|
193
|
+
dcOrChannel: DataChannelLike | Channel): GrpcDcServerBridge;
|
package/index.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// grpc-dc: gRPC over WebRTC DataChannels (JavaScript runtime).
|
|
2
|
+
// Wire tables in wire_gen.js and the protoc plugin that generates service
|
|
3
|
+
// stubs are produced by the grpc-dc code generator.
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
FrameType,
|
|
7
|
+
StatusCode,
|
|
8
|
+
StatusName,
|
|
9
|
+
StatusError,
|
|
10
|
+
encodeFrame,
|
|
11
|
+
decodeFrame,
|
|
12
|
+
encodeTimeout,
|
|
13
|
+
parseTimeout,
|
|
14
|
+
setTracer,
|
|
15
|
+
setTraceFilter,
|
|
16
|
+
toHex,
|
|
17
|
+
fromHex,
|
|
18
|
+
} from "./src/codec.js";
|
|
19
|
+
export { Endpoint, StreamInput } from "./src/endpoint.js";
|
|
20
|
+
export { Channel } from "./src/channel.js";
|
|
21
|
+
export { Server } from "./src/server.js";
|
|
22
|
+
export { memoryDataChannelPair } from "./src/loopback.js";
|
|
23
|
+
export { createGrpcDcBridge, createGrpcDcServerBridge } from "./src/wasm_bridge.js";
|
|
24
|
+
// Note: the Node WebRTC-Direct adapter (src/webrtc_direct.js)
|
|
25
|
+
// is deliberately NOT re-exported here -- it imports Node-only
|
|
26
|
+
// modules (node:dgram via webrtc/js/direct.js) and this index
|
|
27
|
+
// is also loaded in browsers. Import it directly:
|
|
28
|
+
// import { directClientEndpoint } from "grpc-dc/webrtc_direct";
|
|
29
|
+
|
|
30
|
+
import { Channel } from "./src/channel.js";
|
|
31
|
+
import { Endpoint } from "./src/endpoint.js";
|
|
32
|
+
import { Server } from "./src/server.js";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A bidirectional peer: a Channel and a Server sharing one endpoint, so
|
|
36
|
+
* both sides can call each other over the same data channel. The peer
|
|
37
|
+
* that created the channel allocates odd stream ids; the answering peer
|
|
38
|
+
* must pass { parity: "even" }.
|
|
39
|
+
*/
|
|
40
|
+
export function createPeer(dc, { parity = "odd" } = {}) {
|
|
41
|
+
const endpoint = new Endpoint(dc, { parity });
|
|
42
|
+
const server = new Server();
|
|
43
|
+
server.serve(endpoint);
|
|
44
|
+
return { endpoint, server, channel: new Channel(endpoint) };
|
|
45
|
+
}
|
|
46
|
+
export {
|
|
47
|
+
DynamicClient,
|
|
48
|
+
GenericMessage,
|
|
49
|
+
createDynamicService,
|
|
50
|
+
parseDescriptorSet,
|
|
51
|
+
} from "./src/dynamic.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "grpc-dc",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.31.0",
|
|
4
|
+
"description": "gRPC in the browser over WebRTC DataChannels: dependency-free generated gRPC clients & servers for JavaScript — no grpc-web proxy, no external runtime",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"grpc",
|
|
7
|
+
"grpc-web",
|
|
8
|
+
"webrtc",
|
|
9
|
+
"datachannel",
|
|
10
|
+
"rtcdatachannel",
|
|
11
|
+
"protobuf",
|
|
12
|
+
"protoc",
|
|
13
|
+
"wasm",
|
|
14
|
+
"browser",
|
|
15
|
+
"peer-to-peer"
|
|
16
|
+
],
|
|
17
|
+
"homepage": "https://browser-grpc.github.io/grpc-over-datachannels/",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/browser-grpc/grpc-over-datachannels.git"
|
|
21
|
+
},
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "index.js",
|
|
24
|
+
"types": "index.d.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./index.d.ts",
|
|
28
|
+
"default": "./index.js"
|
|
29
|
+
},
|
|
30
|
+
"./wire_gen.js": "./wire_gen.js",
|
|
31
|
+
"./webrtc_direct": "./src/webrtc_direct.js"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"index.js",
|
|
35
|
+
"index.d.ts",
|
|
36
|
+
"wire_gen.js",
|
|
37
|
+
"src"
|
|
38
|
+
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"test": "node --test test/*.test.js"
|
|
41
|
+
},
|
|
42
|
+
"license": "Apache-2.0"
|
|
43
|
+
}
|
package/src/channel.js
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
// @generated by grpc-dc codegen. DO NOT EDIT.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
// Client side: opens streams on one endpoint, all four gRPC shapes.
|
|
4
|
+
// Message types are classes with static encode(msg)/decode(bytes).
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
FrameType,
|
|
8
|
+
StatusError,
|
|
9
|
+
StatusName,
|
|
10
|
+
encodeFrame,
|
|
11
|
+
encodeTimeout,
|
|
12
|
+
formatTraceparent,
|
|
13
|
+
getTracer,
|
|
14
|
+
headersFrame,
|
|
15
|
+
parseTraceparent,
|
|
16
|
+
statusFrame,
|
|
17
|
+
traceEnd,
|
|
18
|
+
traceStart,
|
|
19
|
+
} from "./codec.js";
|
|
20
|
+
import { Endpoint } from "./endpoint.js";
|
|
21
|
+
|
|
22
|
+
export class Channel {
|
|
23
|
+
#endpoint;
|
|
24
|
+
#traces = new Map();
|
|
25
|
+
|
|
26
|
+
/** Accepts an Endpoint or a raw RTCDataChannel-like object. */
|
|
27
|
+
constructor(dcOrEndpoint, options) {
|
|
28
|
+
this.#endpoint =
|
|
29
|
+
dcOrEndpoint instanceof Endpoint ? dcOrEndpoint : new Endpoint(dcOrEndpoint, options);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get endpoint() {
|
|
33
|
+
return this.#endpoint;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async #open(path, metadata, timeout) {
|
|
37
|
+
let deadline = null;
|
|
38
|
+
if (timeout !== undefined && timeout !== null) {
|
|
39
|
+
deadline = Date.now() + timeout;
|
|
40
|
+
metadata = { ...metadata, "grpc-timeout": encodeTimeout(timeout) };
|
|
41
|
+
}
|
|
42
|
+
let traceCtx = null;
|
|
43
|
+
if (getTracer() !== null) {
|
|
44
|
+
traceCtx = traceStart("client", path, parseTraceparent(metadata?.traceparent));
|
|
45
|
+
if (traceCtx !== null) {
|
|
46
|
+
metadata = {
|
|
47
|
+
...metadata,
|
|
48
|
+
traceparent: formatTraceparent(traceCtx.traceId, traceCtx.spanId),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const [id, input] = this.#endpoint.openStream();
|
|
53
|
+
if (traceCtx !== null) this.#traces.set(id, { ctx: traceCtx, path });
|
|
54
|
+
try {
|
|
55
|
+
await this.#endpoint.sendFrame(headersFrame(id, path, metadata));
|
|
56
|
+
} catch (e) {
|
|
57
|
+
this.#endTrace(id, "UNAVAILABLE");
|
|
58
|
+
this.#endpoint.closeStream(id);
|
|
59
|
+
throw e;
|
|
60
|
+
}
|
|
61
|
+
return [id, input, deadline];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
#endTrace(id, status) {
|
|
65
|
+
const entry = this.#traces.get(id);
|
|
66
|
+
if (entry) {
|
|
67
|
+
this.#traces.delete(id);
|
|
68
|
+
traceEnd("client", entry.path, entry.ctx, status);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Next event from input's iterator, enforcing the call deadline:
|
|
73
|
+
// on expiry the stream is aborted (RST) and DEADLINE_EXCEEDED
|
|
74
|
+
// (code 4) is thrown.
|
|
75
|
+
async #next(id, iterator, deadline) {
|
|
76
|
+
if (deadline === null) return iterator.next();
|
|
77
|
+
const remaining = deadline - Date.now();
|
|
78
|
+
if (remaining <= 0) {
|
|
79
|
+
await this.cancel(id).catch(() => {});
|
|
80
|
+
throw new StatusError(4, "deadline exceeded");
|
|
81
|
+
}
|
|
82
|
+
let timer;
|
|
83
|
+
try {
|
|
84
|
+
return await Promise.race([
|
|
85
|
+
iterator.next(),
|
|
86
|
+
new Promise((_, reject) => {
|
|
87
|
+
timer = setTimeout(
|
|
88
|
+
() => reject(new StatusError(4, "deadline exceeded")),
|
|
89
|
+
remaining,
|
|
90
|
+
);
|
|
91
|
+
}),
|
|
92
|
+
]);
|
|
93
|
+
} catch (e) {
|
|
94
|
+
if (e instanceof StatusError && e.code === 4) {
|
|
95
|
+
await this.cancel(id).catch(() => {});
|
|
96
|
+
}
|
|
97
|
+
throw e;
|
|
98
|
+
} finally {
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async #sendMessage(id, ReqType, request) {
|
|
104
|
+
await this.#endpoint.sendFrame(encodeFrame(FrameType.DATA, id, ReqType.encode(request)));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async #collectOne(id, input, RespType, deadline = null) {
|
|
108
|
+
const iterator = input[Symbol.asyncIterator]();
|
|
109
|
+
try {
|
|
110
|
+
let response = null;
|
|
111
|
+
for (;;) {
|
|
112
|
+
const step = await this.#next(id, iterator, deadline);
|
|
113
|
+
if (step.done) break;
|
|
114
|
+
const event = step.value;
|
|
115
|
+
if (event.kind === "data") {
|
|
116
|
+
if (response !== null) {
|
|
117
|
+
throw StatusError.internal("unary call received more than one response");
|
|
118
|
+
}
|
|
119
|
+
response = RespType.decode(event.payload);
|
|
120
|
+
} else if (event.kind === "trailers") {
|
|
121
|
+
this.#endTrace(id, StatusName[event.status.code] ?? `CODE_${event.status.code}`);
|
|
122
|
+
if (event.status.code !== 0) throw event.status;
|
|
123
|
+
if (response === null) throw StatusError.internal("call closed with no response");
|
|
124
|
+
return response;
|
|
125
|
+
} else if (event.kind === "rst") {
|
|
126
|
+
this.#endTrace(id, StatusName[event.status.code] ?? `CODE_${event.status.code}`);
|
|
127
|
+
throw event.status;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
throw StatusError.unavailable("data channel closed");
|
|
131
|
+
} finally {
|
|
132
|
+
this.#endTrace(id, "UNAVAILABLE");
|
|
133
|
+
this.#endpoint.closeStream(id);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async *#responses(id, input, RespType, deadline = null) {
|
|
138
|
+
const iterator = input[Symbol.asyncIterator]();
|
|
139
|
+
try {
|
|
140
|
+
for (;;) {
|
|
141
|
+
const step = await this.#next(id, iterator, deadline);
|
|
142
|
+
if (step.done) break;
|
|
143
|
+
const event = step.value;
|
|
144
|
+
if (event.kind === "data") {
|
|
145
|
+
yield RespType.decode(event.payload);
|
|
146
|
+
} else if (event.kind === "trailers") {
|
|
147
|
+
this.#endTrace(id, StatusName[event.status.code] ?? `CODE_${event.status.code}`);
|
|
148
|
+
if (event.status.code !== 0) throw event.status;
|
|
149
|
+
return;
|
|
150
|
+
} else if (event.kind === "rst") {
|
|
151
|
+
this.#endTrace(id, StatusName[event.status.code] ?? `CODE_${event.status.code}`);
|
|
152
|
+
throw event.status;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
throw StatusError.unavailable("data channel closed");
|
|
156
|
+
} finally {
|
|
157
|
+
this.#endTrace(id, "UNAVAILABLE");
|
|
158
|
+
this.#endpoint.closeStream(id);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async #pump(id, requests, ReqType) {
|
|
163
|
+
try {
|
|
164
|
+
for await (const request of requests) {
|
|
165
|
+
await this.#sendMessage(id, ReqType, request);
|
|
166
|
+
}
|
|
167
|
+
await this.#endpoint.sendFrame(encodeFrame(FrameType.HALF_CLOSE, id));
|
|
168
|
+
} catch (e) {
|
|
169
|
+
const status = e instanceof StatusError ? e : StatusError.internal(e?.message ?? String(e));
|
|
170
|
+
await this.#endpoint
|
|
171
|
+
.sendFrame(statusFrame(FrameType.RST, id, status))
|
|
172
|
+
.catch(() => {});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Unary: one request, one response. */
|
|
177
|
+
async unary(path, request, ReqType, RespType, { metadata = {}, timeout } = {}) {
|
|
178
|
+
const [id, input, deadline] = await this.#open(path, metadata, timeout);
|
|
179
|
+
await this.#sendMessage(id, ReqType, request);
|
|
180
|
+
await this.#endpoint.sendFrame(encodeFrame(FrameType.HALF_CLOSE, id));
|
|
181
|
+
return this.#collectOne(id, input, RespType, deadline);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Server-streaming: one request, async iterable of responses. */
|
|
185
|
+
serverStream(path, request, ReqType, RespType, { metadata = {}, timeout } = {}) {
|
|
186
|
+
return (async () => {
|
|
187
|
+
const [id, input, deadline] = await this.#open(path, metadata, timeout);
|
|
188
|
+
await this.#sendMessage(id, ReqType, request);
|
|
189
|
+
await this.#endpoint.sendFrame(encodeFrame(FrameType.HALF_CLOSE, id));
|
|
190
|
+
return this.#responses(id, input, RespType, deadline);
|
|
191
|
+
})();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Client-streaming: async iterable of requests, one response. */
|
|
195
|
+
async clientStream(path, requests, ReqType, RespType, { metadata = {}, timeout } = {}) {
|
|
196
|
+
const [id, input, deadline] = await this.#open(path, metadata, timeout);
|
|
197
|
+
const pump = this.#pump(id, requests, ReqType);
|
|
198
|
+
try {
|
|
199
|
+
return await this.#collectOne(id, input, RespType, deadline);
|
|
200
|
+
} finally {
|
|
201
|
+
pump.catch(() => {});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Bidirectional: async iterable of requests and of responses. */
|
|
206
|
+
bidi(path, requests, ReqType, RespType, { metadata = {}, timeout } = {}) {
|
|
207
|
+
return (async () => {
|
|
208
|
+
const [id, input, deadline] = await this.#open(path, metadata, timeout);
|
|
209
|
+
const pump = this.#pump(id, requests, ReqType);
|
|
210
|
+
pump.catch(() => {});
|
|
211
|
+
return this.#responses(id, input, RespType, deadline);
|
|
212
|
+
})();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Abort a call: sends RST CANCELLED. */
|
|
216
|
+
async cancel(streamId) {
|
|
217
|
+
await this.#endpoint
|
|
218
|
+
.sendFrame(
|
|
219
|
+
statusFrame(
|
|
220
|
+
FrameType.RST,
|
|
221
|
+
streamId,
|
|
222
|
+
new StatusError(1, "cancelled by caller"),
|
|
223
|
+
),
|
|
224
|
+
)
|
|
225
|
+
.catch(() => {});
|
|
226
|
+
this.#endpoint.closeStream(streamId);
|
|
227
|
+
}
|
|
228
|
+
}
|