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.
@@ -0,0 +1,269 @@
1
+ // @generated by grpc-dc codegen. DO NOT EDIT.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ // Stream multiplexor: one data channel, many concurrent calls.
4
+ // Incoming HEADERS are handed to `dispatcher` (installed by Server);
5
+ // everything else is routed to the owning stream's event queue.
6
+
7
+ import {
8
+ FrameType,
9
+ StatusCode,
10
+ StatusError,
11
+ asUint8Array,
12
+ decodeFrame,
13
+ encodeFrame,
14
+ headersFrame,
15
+ parseHeaders,
16
+ parseStatus,
17
+ statusFrame,
18
+ } from "./codec.js";
19
+
20
+ /** Async queue of stream events with terminal-error semantics. */
21
+ export class StreamInput {
22
+ #items = [];
23
+ #waiters = [];
24
+ #error = null;
25
+ #ended = false;
26
+
27
+ push(item) {
28
+ if (this.#ended) return;
29
+ const waiter = this.#waiters.shift();
30
+ if (waiter) waiter({ value: item, done: false });
31
+ else this.#items.push(item);
32
+ }
33
+
34
+ end() {
35
+ if (this.#ended) return;
36
+ this.#ended = true;
37
+ for (const w of this.#waiters.splice(0)) w({ value: undefined, done: true });
38
+ }
39
+
40
+ fail(error) {
41
+ if (this.#ended) return;
42
+ this.#ended = true;
43
+ this.#error = error;
44
+ for (const w of this.#waiters.splice(0)) w({ error, done: true });
45
+ }
46
+
47
+ [Symbol.asyncIterator]() {
48
+ return {
49
+ next: () => {
50
+ if (this.#items.length) {
51
+ return Promise.resolve({ value: this.#items.shift(), done: false });
52
+ }
53
+ if (this.#error) return Promise.reject(this.#error);
54
+ if (this.#ended) return Promise.resolve({ value: undefined, done: true });
55
+ return new Promise((resolve) => {
56
+ this.#waiters.push((r) => {
57
+ if (r.error) resolve(Promise.reject(r.error));
58
+ else resolve(r);
59
+ });
60
+ }).then((r) => r);
61
+ },
62
+ };
63
+ }
64
+ }
65
+
66
+ const DRAIN_THRESHOLD = 1 << 20; // 1 MiB of bufferedAmount before pausing
67
+
68
+ export class Endpoint {
69
+ #dc;
70
+ #streams = new Map();
71
+ #nextId;
72
+ #sendChain = Promise.resolve();
73
+ dispatcher = null; // async (path, headers, input, sink) => void, set by Server
74
+
75
+ /**
76
+ * @param dc an RTCDataChannel (browser, werift, node-datachannel) or any
77
+ * object with send() + message events. Memory pair in loopback.js works.
78
+ * @param parity 'odd' if this peer created the channel, else 'even'.
79
+ */
80
+ constructor(dc, { parity = "odd" } = {}) {
81
+ this.#dc = dc;
82
+ this.#nextId = parity === "odd" ? 1 : 2;
83
+ try {
84
+ dc.binaryType = "arraybuffer";
85
+ } catch {
86
+ /* structural channels may not expose binaryType */
87
+ }
88
+ const onMessage = (event) => {
89
+ const data = event?.data ?? event;
90
+ if (data instanceof Blob) {
91
+ data.arrayBuffer().then((b) => this.#onBytes(new Uint8Array(b)));
92
+ } else {
93
+ try {
94
+ this.#onBytes(asUint8Array(data));
95
+ } catch {
96
+ /* ignore non-binary messages */
97
+ }
98
+ }
99
+ };
100
+ if (typeof dc.addEventListener === "function") {
101
+ dc.addEventListener("message", onMessage);
102
+ } else {
103
+ dc.onmessage = onMessage;
104
+ }
105
+ }
106
+
107
+ get dataChannel() {
108
+ return this.#dc;
109
+ }
110
+
111
+ /** Allocate a locally-initiated stream. Returns [id, input]. */
112
+ openStream() {
113
+ const id = this.#nextId;
114
+ this.#nextId += 2;
115
+ const input = new StreamInput();
116
+ this.#streams.set(id, input);
117
+ return [id, input];
118
+ }
119
+
120
+ closeStream(id) {
121
+ const input = this.#streams.get(id);
122
+ if (input) {
123
+ input.end();
124
+ this.#streams.delete(id);
125
+ }
126
+ }
127
+
128
+ async sendFrame(frameBytes) {
129
+ // Serialize sends so HEADERS never overtakes a previous frame on the
130
+ // wire, and pause while the SCTP send buffer is above the threshold.
131
+ const run = async () => {
132
+ await this.#waitForDrain();
133
+ if (this.#dc.readyState && this.#dc.readyState !== "open") {
134
+ throw StatusError.unavailable("data channel is not open");
135
+ }
136
+ this.#dc.send(frameBytes);
137
+ };
138
+ this.#sendChain = this.#sendChain.then(run, run);
139
+ return this.#sendChain;
140
+ }
141
+
142
+ async #waitForDrain() {
143
+ const dc = this.#dc;
144
+ if (typeof dc.bufferedAmount !== "number" || dc.bufferedAmount <= DRAIN_THRESHOLD) return;
145
+ await new Promise((resolve) => {
146
+ if (typeof dc.addEventListener === "function") {
147
+ try {
148
+ dc.bufferedAmountLowThreshold = DRAIN_THRESHOLD / 2;
149
+ } catch {
150
+ /* optional */
151
+ }
152
+ const done = () => {
153
+ dc.removeEventListener?.("bufferedamountlow", done);
154
+ resolve();
155
+ };
156
+ dc.addEventListener("bufferedamountlow", done);
157
+ setTimeout(done, 50); // fallback poll for structural channels
158
+ } else {
159
+ setTimeout(resolve, 10);
160
+ }
161
+ });
162
+ if (typeof dc.bufferedAmount === "number" && dc.bufferedAmount > DRAIN_THRESHOLD) {
163
+ return this.#waitForDrain();
164
+ }
165
+ }
166
+
167
+ #onBytes(bytes) {
168
+ let frame;
169
+ try {
170
+ frame = decodeFrame(bytes);
171
+ } catch {
172
+ return; // malformed frame: drop, keep the channel up
173
+ }
174
+ const { type, streamId, payload } = frame;
175
+ switch (type) {
176
+ case FrameType.HEADERS:
177
+ this.#onHeaders(streamId, payload);
178
+ break;
179
+ case FrameType.DATA:
180
+ this.#push(streamId, { kind: "data", payload });
181
+ break;
182
+ case FrameType.TRAILERS:
183
+ this.#terminal(streamId, payload, "trailers");
184
+ break;
185
+ case FrameType.HALF_CLOSE:
186
+ this.#push(streamId, { kind: "half-close" });
187
+ break;
188
+ case FrameType.RST:
189
+ this.#terminal(streamId, payload, "rst");
190
+ break;
191
+ default:
192
+ break; // unknown frame type: ignore for forward compatibility
193
+ }
194
+ }
195
+
196
+ #push(streamId, event) {
197
+ this.#streams.get(streamId)?.push(event);
198
+ }
199
+
200
+ #terminal(streamId, payload, kind) {
201
+ const status = parseStatus(payload);
202
+ this.#streams.get(streamId)?.push({ kind, status });
203
+ this.#streams.delete(streamId);
204
+ }
205
+
206
+ async #onHeaders(streamId, payload) {
207
+ let headers;
208
+ try {
209
+ headers = parseHeaders(payload);
210
+ } catch (e) {
211
+ this.sendFrame(statusFrame(FrameType.RST, streamId, e)).catch(() => {});
212
+ return;
213
+ }
214
+ if (headers.path === null) {
215
+ // Server initial metadata for one of our outbound streams.
216
+ this.#streams.get(streamId)?.push({ kind: "headers", headers });
217
+ return;
218
+ }
219
+ if (this.#streams.has(streamId)) {
220
+ this.sendFrame(
221
+ statusFrame(
222
+ FrameType.RST,
223
+ streamId,
224
+ StatusError.internal("HEADERS for stream already in use"),
225
+ ),
226
+ ).catch(() => {});
227
+ return;
228
+ }
229
+ if (!this.dispatcher) {
230
+ this.sendFrame(
231
+ statusFrame(
232
+ FrameType.TRAILERS,
233
+ streamId,
234
+ StatusError.unimplemented(`unknown method ${headers.path}`),
235
+ ),
236
+ ).catch(() => {});
237
+ return;
238
+ }
239
+ const input = new StreamInput();
240
+ this.#streams.set(streamId, input);
241
+ const sink = {
242
+ sendData: (payload) => this.sendFrame(encodeFrame(FrameType.DATA, streamId, payload)),
243
+ sendHeaders: () => this.sendFrame(headersFrame(streamId, null)),
244
+ sendTrailers: (status) =>
245
+ this.sendFrame(statusFrame(FrameType.TRAILERS, streamId, status)),
246
+ };
247
+ try {
248
+ await sink.sendHeaders();
249
+ await this.dispatcher(headers.path, headers, input, sink);
250
+ await sink.sendTrailers(StatusError.ok());
251
+ } catch (e) {
252
+ const status =
253
+ e instanceof StatusError ? e : StatusError.internal(e?.message ?? String(e));
254
+ await sink.sendTrailers(status).catch(() => {});
255
+ } finally {
256
+ this.#streams.delete(streamId);
257
+ }
258
+ }
259
+
260
+ /** Fail every live stream (channel teardown). */
261
+ closeAll() {
262
+ for (const input of this.#streams.values()) {
263
+ input.fail(StatusError.unavailable("data channel closed"));
264
+ }
265
+ this.#streams.clear();
266
+ }
267
+ }
268
+
269
+ export { StatusCode };
@@ -0,0 +1,60 @@
1
+ // @generated by grpc-dc codegen. DO NOT EDIT.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ // In-memory RTCDataChannel pair for Node tests and demos: message
4
+ // boundaries preserved, ordered, reliable — the same contract a real
5
+ // RTCDataChannel offers. Delivery is a microtask, like a fast network.
6
+
7
+ class MemoryDataChannel {
8
+ #listeners = new Map();
9
+
10
+ constructor(label) {
11
+ this.label = label;
12
+ this.readyState = "open";
13
+ this.bufferedAmount = 0;
14
+ this.binaryType = "arraybuffer";
15
+ this.peer = null;
16
+ }
17
+
18
+ addEventListener(type, fn) {
19
+ if (!this.#listeners.has(type)) this.#listeners.set(type, []);
20
+ this.#listeners.get(type).push(fn);
21
+ }
22
+
23
+ removeEventListener(type, fn) {
24
+ const list = this.#listeners.get(type) ?? [];
25
+ this.#listeners.set(type, list.filter((f) => f !== fn));
26
+ }
27
+
28
+ #emit(type, event) {
29
+ for (const fn of [...(this.#listeners.get(type) ?? [])]) fn(event);
30
+ const handler = this[`on${type}`];
31
+ if (typeof handler === "function") handler(event);
32
+ }
33
+
34
+ send(data) {
35
+ if (this.readyState !== "open") throw new Error("data channel is not open");
36
+ const copy = data instanceof Uint8Array ? data.slice() : data;
37
+ const peer = this.peer;
38
+ queueMicrotask(() => {
39
+ if (peer.readyState === "open") peer.#emit("message", { data: copy });
40
+ });
41
+ }
42
+
43
+ close() {
44
+ if (this.readyState === "closed") return;
45
+ this.readyState = "closed";
46
+ this.#emit("close", {});
47
+ if (this.peer.readyState !== "closed") {
48
+ this.peer.readyState = "closed";
49
+ this.peer.#emit("close", {});
50
+ }
51
+ }
52
+ }
53
+
54
+ export function memoryDataChannelPair() {
55
+ const a = new MemoryDataChannel("a");
56
+ const b = new MemoryDataChannel("b");
57
+ a.peer = b;
58
+ b.peer = a;
59
+ return [a, b];
60
+ }
package/src/server.js ADDED
@@ -0,0 +1,126 @@
1
+ // @generated by grpc-dc codegen. DO NOT EDIT.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ // Server side: registers generated service definitions (createXService)
4
+ // on an endpoint and dispatches HEADERS by path.
5
+
6
+ import {
7
+ StatusError,
8
+ StatusName,
9
+ parseTimeout,
10
+ parseTraceparent,
11
+ traceEnd,
12
+ traceStart,
13
+ } from "./codec.js";
14
+ import { Endpoint } from "./endpoint.js";
15
+
16
+ async function* typedRequests(input, ReqType) {
17
+ for await (const event of input) {
18
+ if (event.kind === "data") {
19
+ yield ReqType.decode(event.payload);
20
+ } else if (event.kind === "half-close") {
21
+ return;
22
+ } else if (event.kind === "trailers" || event.kind === "rst") {
23
+ throw event.status;
24
+ }
25
+ }
26
+ }
27
+
28
+ async function firstRequest(requests) {
29
+ const iterator = requests[Symbol.asyncIterator]();
30
+ const first = await iterator.next();
31
+ if (first.done) {
32
+ throw StatusError.invalidArgument("missing request message");
33
+ }
34
+ return first.value;
35
+ }
36
+
37
+ export class Server {
38
+ #methods = new Map();
39
+
40
+ addService(definition) {
41
+ for (const [name, method] of Object.entries(definition.methods)) {
42
+ if (typeof method.handler !== "function") {
43
+ throw new Error(`grpc-dc: missing handler for ${definition.name}.${name}`);
44
+ }
45
+ this.#methods.set(method.path, { ...method, service: definition.name, method: name });
46
+ }
47
+ return this;
48
+ }
49
+
50
+ /** Attach to an endpoint (or raw channel, wrapped with even parity). */
51
+ serve(dcOrEndpoint, options) {
52
+ const endpoint =
53
+ dcOrEndpoint instanceof Endpoint
54
+ ? dcOrEndpoint
55
+ : new Endpoint(dcOrEndpoint, { parity: "even", ...options });
56
+ endpoint.dispatcher = (path, headers, input, sink) =>
57
+ this.#dispatch(path, headers, input, sink);
58
+ return endpoint;
59
+ }
60
+
61
+ async #dispatch(path, headers, input, sink) {
62
+ const method = this.#methods.get(path);
63
+ if (!method) {
64
+ throw StatusError.unimplemented(`unknown method ${path}`);
65
+ }
66
+ const metadata = headers.metadata ?? {};
67
+ const timeoutMs = parseTimeout(metadata["grpc-timeout"]);
68
+ const deadline = timeoutMs === null ? null : Date.now() + timeoutMs;
69
+ const ctx = {
70
+ path,
71
+ metadata,
72
+ service: method.service,
73
+ method: method.method,
74
+ deadline,
75
+ timeRemaining: () => (deadline === null ? null : deadline - Date.now()),
76
+ };
77
+ const traceCtx = traceStart("server", path, parseTraceparent(metadata.traceparent));
78
+ let traceStatus = "OK";
79
+ try {
80
+ await this.#dispatchTraced(path, method, ctx, input, sink);
81
+ } catch (e) {
82
+ traceStatus =
83
+ e instanceof StatusError
84
+ ? (StatusName[e.code] ?? `CODE_${e.code}`)
85
+ : "INTERNAL";
86
+ throw e;
87
+ } finally {
88
+ traceEnd("server", path, traceCtx, traceStatus);
89
+ }
90
+ }
91
+
92
+ async #dispatchTraced(path, method, ctx, input, sink) {
93
+ const requests = typedRequests(input, method.requestType);
94
+ const encode = (resp) => method.responseType.encode(resp);
95
+ switch (method.kind) {
96
+ case "unary": {
97
+ const req = await firstRequest(requests);
98
+ const resp = await method.handler(req, ctx);
99
+ await sink.sendData(encode(resp));
100
+ return;
101
+ }
102
+ case "server-streaming": {
103
+ const req = await firstRequest(requests);
104
+ const out = await method.handler(req, ctx);
105
+ for await (const resp of out) {
106
+ await sink.sendData(encode(resp));
107
+ }
108
+ return;
109
+ }
110
+ case "client-streaming": {
111
+ const resp = await method.handler(requests, ctx);
112
+ await sink.sendData(encode(resp));
113
+ return;
114
+ }
115
+ case "bidi": {
116
+ const out = await method.handler(requests, ctx);
117
+ for await (const resp of out) {
118
+ await sink.sendData(encode(resp));
119
+ }
120
+ return;
121
+ }
122
+ default:
123
+ throw StatusError.unimplemented(`unknown method kind ${method.kind}`);
124
+ }
125
+ }
126
+ }
@@ -0,0 +1,241 @@
1
+ // @generated by grpc-dc codegen. DO NOT EDIT.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ // WASM bridge: the generic byte-level surface that generated WASM
4
+ // client facades (Go/Rust/...) call into. The JS runtime owns the
5
+ // wire; the facade owns the types. Payloads cross the boundary as
6
+ // Uint8Array protobuf bytes, so no object mapping is needed.
7
+ //
8
+ // unary(path, payload, options?) -> Promise<Uint8Array>
9
+ // serverStream(path, payload, onMessage, options?) -> Promise<void>
10
+ // clientStream(path, payloads, options?) -> Promise<Uint8Array>
11
+ // bidi(path, payloads, onMessage, options?) -> Promise<void>
12
+ //
13
+ // options may carry {metadata, timeout} (forwarded to the call)
14
+ // and, for streaming calls, {cancelAfter: n} to abort the stream
15
+ // after n responses. onMessage receives one Uint8Array per
16
+ // response; the promise resolves on OK trailers and rejects
17
+ // with StatusError otherwise.
18
+
19
+ import { Channel } from "./channel.js";
20
+ import { Server } from "./server.js";
21
+ import { StatusError } from "./codec.js";
22
+
23
+ const identity = { encode: (bytes) => bytes, decode: (bytes) => bytes };
24
+
25
+ async function* fromList(items) {
26
+ yield* items;
27
+ }
28
+
29
+ export function createGrpcDcBridge(dcOrChannel) {
30
+ const channel = dcOrChannel instanceof Channel ? dcOrChannel : new Channel(dcOrChannel);
31
+ const callOptions = (options) => ({
32
+ metadata: options?.metadata ?? {},
33
+ timeout: options?.timeout,
34
+ });
35
+ return {
36
+ async unary(path, payload, options) {
37
+ return channel.unary(path, payload, identity, identity, callOptions(options));
38
+ },
39
+ async serverStream(path, payload, onMessage, options) {
40
+ const responses = await channel.serverStream(path, payload, identity, identity, callOptions(options));
41
+ let seen = 0;
42
+ for await (const resp of responses) {
43
+ onMessage(resp);
44
+ seen++;
45
+ // cancelAfter: the caller aborts the stream after this
46
+ // many responses (breaking the iteration tears the call
47
+ // down); the promise then resolves normally.
48
+ if (options?.cancelAfter && seen >= options.cancelAfter) break;
49
+ }
50
+ },
51
+ async clientStream(path, payloads, options) {
52
+ return channel.clientStream(path, fromList(payloads), identity, identity, callOptions(options));
53
+ },
54
+ async bidi(path, payloads, onMessage, options) {
55
+ const responses = await channel.bidi(path, fromList(payloads), identity, identity, callOptions(options));
56
+ let seen = 0;
57
+ for await (const resp of responses) {
58
+ onMessage(resp);
59
+ seen++;
60
+ if (options?.cancelAfter && seen >= options.cancelAfter) break;
61
+ }
62
+ },
63
+ };
64
+ }
65
+
66
+ // Server bridge: the mirror image for generated WASM server
67
+ // facades. The JS runtime still owns the wire (a real Server
68
+ // on the channel); the facade registers typed handlers per
69
+ // path. Handlers receive protobuf bytes plus metadata as a
70
+ // flattened [key, value, ...] string array and respond in
71
+ // bytes; streaming handlers push responses through onMessage
72
+ // and settle their promise at the end. Handler failures cross
73
+ // as {code, message} objects and become StatusError here.
74
+ // Facade client-streaming/bidi calls deliver all requests up
75
+ // front, so clientStream/bidi handlers receive the full list
76
+ // (same shape as the client bridge's payloads).
77
+ function toStatusError(err) {
78
+ if (err instanceof StatusError) return err;
79
+ const code = err && typeof err.code === "number" ? err.code : 13;
80
+ const message = err && err.message ? String(err.message) : String(err);
81
+ return new StatusError(code, message);
82
+ }
83
+
84
+ function metadataPairs(metadata) {
85
+ return Object.entries(metadata ?? {}).flat();
86
+ }
87
+
88
+ async function* collectResponses(invoke) {
89
+ const queue = [];
90
+ let notify = null;
91
+ let settled = false;
92
+ let failure = null;
93
+ const wake = () => {
94
+ if (notify) {
95
+ const resolve = notify;
96
+ notify = null;
97
+ resolve();
98
+ }
99
+ };
100
+ invoke((bytes) => {
101
+ queue.push(bytes);
102
+ wake();
103
+ }).then(
104
+ () => {
105
+ settled = true;
106
+ wake();
107
+ },
108
+ (err) => {
109
+ failure = err;
110
+ settled = true;
111
+ wake();
112
+ }
113
+ );
114
+ for (;;) {
115
+ while (queue.length > 0) yield queue.shift();
116
+ if (settled) break;
117
+ await new Promise((resolve) => {
118
+ notify = resolve;
119
+ });
120
+ }
121
+ if (failure) throw toStatusError(failure);
122
+ }
123
+
124
+ /** Pull-based view of a request stream for incremental handlers. */
125
+ function requestSource(requests) {
126
+ const iterator = requests[Symbol.asyncIterator]();
127
+ return {
128
+ next: async () => {
129
+ const result = await iterator.next();
130
+ return result.done ? null : result.value;
131
+ },
132
+ };
133
+ }
134
+
135
+ export function createGrpcDcServerBridge(dcOrChannel) {
136
+ const channel = dcOrChannel instanceof Channel ? dcOrChannel : new Channel(dcOrChannel);
137
+ const server = new Server();
138
+ const methods = {};
139
+ let started = false;
140
+ const register = (path, method) => {
141
+ methods[path] = method;
142
+ };
143
+ return {
144
+ registerUnary(path, handler) {
145
+ register(path, {
146
+ path,
147
+ kind: "unary",
148
+ requestType: identity,
149
+ responseType: identity,
150
+ handler: async (req, ctx) => {
151
+ try {
152
+ return await handler(req, metadataPairs(ctx.metadata));
153
+ } catch (err) {
154
+ throw toStatusError(err);
155
+ }
156
+ },
157
+ });
158
+ },
159
+ registerServerStream(path, handler) {
160
+ register(path, {
161
+ path,
162
+ kind: "server-streaming",
163
+ requestType: identity,
164
+ responseType: identity,
165
+ handler: (req, ctx) =>
166
+ collectResponses((onMessage) => handler(req, metadataPairs(ctx.metadata), onMessage)),
167
+ });
168
+ },
169
+ registerClientStream(path, handler) {
170
+ register(path, {
171
+ path,
172
+ kind: "client-streaming",
173
+ requestType: identity,
174
+ responseType: identity,
175
+ handler: async (requests, ctx) => {
176
+ const payloads = [];
177
+ for await (const req of requests) payloads.push(req);
178
+ try {
179
+ return await handler(payloads, metadataPairs(ctx.metadata));
180
+ } catch (err) {
181
+ throw toStatusError(err);
182
+ }
183
+ },
184
+ });
185
+ },
186
+ registerBidi(path, handler) {
187
+ register(path, {
188
+ path,
189
+ kind: "bidi",
190
+ requestType: identity,
191
+ responseType: identity,
192
+ handler: async (requests, ctx) => {
193
+ const payloads = [];
194
+ for await (const req of requests) payloads.push(req);
195
+ return collectResponses((onMessage) =>
196
+ handler(payloads, metadataPairs(ctx.metadata), onMessage)
197
+ );
198
+ },
199
+ });
200
+ },
201
+ // Incremental variants: the handler pulls requests one at a
202
+ // time from source.next() (null at end of stream) instead of
203
+ // receiving every payload up front, so a bidi handler can
204
+ // answer each request before the next one is sent.
205
+ registerClientStreamInc(path, handler) {
206
+ register(path, {
207
+ path,
208
+ kind: "client-streaming",
209
+ requestType: identity,
210
+ responseType: identity,
211
+ handler: async (requests, ctx) => {
212
+ try {
213
+ return await handler(requestSource(requests), metadataPairs(ctx.metadata));
214
+ } catch (err) {
215
+ throw toStatusError(err);
216
+ }
217
+ },
218
+ });
219
+ },
220
+ registerBidiInc(path, handler) {
221
+ register(path, {
222
+ path,
223
+ kind: "bidi",
224
+ requestType: identity,
225
+ responseType: identity,
226
+ handler: (requests, ctx) =>
227
+ collectResponses((onMessage) =>
228
+ handler(requestSource(requests), metadataPairs(ctx.metadata), onMessage)
229
+ ),
230
+ });
231
+ },
232
+ start(serviceName) {
233
+ if (started) return;
234
+ started = true;
235
+ server.addService({ name: serviceName ?? "grpc-dc-bridge", methods });
236
+ // Serve on the channel's endpoint (a raw DataChannel is
237
+ // wrapped by Server itself; a Channel must not be).
238
+ server.serve(channel instanceof Channel ? channel.endpoint : channel);
239
+ },
240
+ };
241
+ }