grpc-dc 0.0.0-stage → 0.30.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/src/codec.js ADDED
@@ -0,0 +1,220 @@
1
+ // @generated by grpc-dc codegen. DO NOT EDIT.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ // Frame codec: one DataChannel message == one frame (SPEC.md).
4
+ // type:u8 | stream_id:u32be | payload_len:u32be | payload
5
+
6
+ import { FrameType, StatusCode, StatusName } from "../wire_gen.js";
7
+
8
+ export { FrameType, StatusCode, StatusName };
9
+
10
+ export const HEADER_LEN = 9;
11
+ export const MAX_DATA_PAYLOAD = 16 * 1024 * 1024;
12
+
13
+ const textEncoder = new TextEncoder();
14
+ const textDecoder = new TextDecoder();
15
+
16
+ export class StatusError extends Error {
17
+ constructor(code, message = "") {
18
+ super(`${StatusName[code] ?? `CODE_${code}`}: ${message}`);
19
+ this.name = "StatusError";
20
+ this.code = code;
21
+ }
22
+ static ok() {
23
+ return new StatusError(StatusCode.OK, "");
24
+ }
25
+ static unimplemented(message) {
26
+ return new StatusError(StatusCode.UNIMPLEMENTED, message);
27
+ }
28
+ static internal(message) {
29
+ return new StatusError(StatusCode.INTERNAL, message);
30
+ }
31
+ static unavailable(message) {
32
+ return new StatusError(StatusCode.UNAVAILABLE, message);
33
+ }
34
+ static invalidArgument(message) {
35
+ return new StatusError(StatusCode.INVALID_ARGUMENT, message);
36
+ }
37
+ }
38
+
39
+ export function encodeFrame(type, streamId, payload = new Uint8Array(0)) {
40
+ if (!(type in FrameTypeByValue)) {
41
+ throw StatusError.invalidArgument(`unknown frame type ${type}`);
42
+ }
43
+ if (type === FrameType.DATA && payload.length > MAX_DATA_PAYLOAD) {
44
+ throw new StatusError(StatusCode.RESOURCE_EXHAUSTED, "DATA payload exceeds 16 MiB");
45
+ }
46
+ const out = new Uint8Array(HEADER_LEN + payload.length);
47
+ const view = new DataView(out.buffer);
48
+ view.setUint8(0, type);
49
+ view.setUint32(1, streamId, false);
50
+ view.setUint32(5, payload.length, false);
51
+ out.set(payload, HEADER_LEN);
52
+ return out;
53
+ }
54
+
55
+ const FrameTypeByValue = Object.fromEntries(
56
+ Object.entries(FrameType).map(([k, v]) => [v, k]),
57
+ );
58
+
59
+ export function decodeFrame(data) {
60
+ const bytes = asUint8Array(data);
61
+ if (bytes.length < HEADER_LEN) {
62
+ throw StatusError.invalidArgument(`frame shorter than 9-byte header: ${bytes.length} bytes`);
63
+ }
64
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
65
+ const type = view.getUint8(0);
66
+ const streamId = view.getUint32(1, false);
67
+ const len = view.getUint32(5, false);
68
+ if (len !== bytes.length - HEADER_LEN) {
69
+ throw StatusError.invalidArgument(
70
+ `payload length ${len} does not match message size ${bytes.length}`,
71
+ );
72
+ }
73
+ return { type, streamId, payload: bytes.slice(HEADER_LEN) };
74
+ }
75
+
76
+ export function asUint8Array(data) {
77
+ if (data instanceof Uint8Array) return data;
78
+ if (data instanceof ArrayBuffer) return new Uint8Array(data);
79
+ if (ArrayBuffer.isView(data)) return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
80
+ throw StatusError.internal("data channel message is not binary");
81
+ }
82
+
83
+ export function headersFrame(streamId, path, metadata = {}) {
84
+ const body = path === null ? {} : { path, metadata };
85
+ return encodeFrame(FrameType.HEADERS, streamId, textEncoder.encode(JSON.stringify(body)));
86
+ }
87
+
88
+ // gRPC timeout grammar (grpc-timeout metadata): <digits><unit>,
89
+ // units H/M/S/m/u/n (at most 8 digits). Encoding uses
90
+ // milliseconds (seconds past the digit limit), rounded up so a
91
+ // deadline never shrinks in propagation.
92
+ const TIMEOUT_UNITS_MS = { H: 3600000, M: 60000, S: 1000, m: 1, u: 0.001, n: 0.000001 };
93
+
94
+ export function encodeTimeout(timeoutMs) {
95
+ const ms = Math.ceil(timeoutMs);
96
+ if (ms <= 99999999) return `${ms}m`;
97
+ return `${Math.ceil(timeoutMs / 1000)}S`;
98
+ }
99
+
100
+ export function parseTimeout(text) {
101
+ if (typeof text !== "string" || text.length < 2) return null;
102
+ const unit = TIMEOUT_UNITS_MS[text[text.length - 1]];
103
+ const digits = text.slice(0, -1);
104
+ if (unit === undefined || digits.length > 8 || !/^\d+$/.test(digits)) return null;
105
+ return Number(digits) * unit;
106
+ }
107
+
108
+ export function statusFrame(frameType, streamId, status) {
109
+ const body = JSON.stringify({ code: status.code, message: status.message ?? "" });
110
+ return encodeFrame(frameType, streamId, textEncoder.encode(body));
111
+ }
112
+
113
+ export function parseHeaders(payload) {
114
+ try {
115
+ const parsed = JSON.parse(textDecoder.decode(payload));
116
+ return { path: parsed.path ?? null, metadata: parsed.metadata ?? {} };
117
+ } catch {
118
+ throw StatusError.invalidArgument("malformed HEADERS payload");
119
+ }
120
+ }
121
+
122
+ export function parseStatus(payload) {
123
+ try {
124
+ const parsed = JSON.parse(textDecoder.decode(payload));
125
+ return new StatusError(parsed.code ?? 2, parsed.message ?? "");
126
+ } catch {
127
+ return StatusError.internal("malformed status payload");
128
+ }
129
+ }
130
+
131
+ export function toHex(bytes) {
132
+ return [...asUint8Array(bytes)].map((b) => b.toString(16).padStart(2, "0")).join("");
133
+ }
134
+
135
+ export function fromHex(hex) {
136
+ const clean = hex.replace(/\s+/g, "");
137
+ const out = new Uint8Array(clean.length / 2);
138
+ for (let i = 0; i < out.length; i++) {
139
+ out[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
140
+ }
141
+ return out;
142
+ }
143
+
144
+ // --- Homegrown tracing (W3C traceparent propagation) -------------
145
+ // Dormant until setTracer() registers an observer with
146
+ // onStart(kind, method, traceId, spanId, parentSpanId) and
147
+ // onEnd(kind, method, traceId, spanId, statusName). Generated code
148
+ // stays dependency-free: sampling and export are the app's problem.
149
+
150
+ let tracer = null;
151
+
152
+ export function setTracer(t) {
153
+ tracer = t;
154
+ }
155
+
156
+ export function getTracer() {
157
+ return tracer;
158
+ }
159
+
160
+ let traceFilter = [];
161
+
162
+ // Restrict traced methods: ';'-separated paths, a trailing '*'
163
+ // means prefix match ('/echo.Echo/*'). Empty = trace all.
164
+ export function setTraceFilter(patterns) {
165
+ traceFilter = patterns.split(";").filter(Boolean);
166
+ }
167
+
168
+ function traceAllowed(method) {
169
+ if (traceFilter.length === 0) return true;
170
+ return traceFilter.some((p) =>
171
+ p.endsWith("*") ? method.startsWith(p.slice(0, -1)) : method === p
172
+ );
173
+ }
174
+
175
+ function randomHex(bytes) {
176
+ const buf = new Uint8Array(bytes);
177
+ if (globalThis.crypto?.getRandomValues) {
178
+ globalThis.crypto.getRandomValues(buf);
179
+ } else {
180
+ for (let i = 0; i < bytes; i++) buf[i] = Math.floor(Math.random() * 256);
181
+ }
182
+ return toHex(buf);
183
+ }
184
+
185
+ export function formatTraceparent(traceId, spanId) {
186
+ return `00-${traceId}-${spanId}-01`;
187
+ }
188
+
189
+ export function parseTraceparent(value) {
190
+ if (typeof value !== "string") return null;
191
+ const parts = value.split("-");
192
+ if (parts.length !== 4) return null;
193
+ const [, traceId, spanId] = parts;
194
+ if (!/^[0-9a-f]{32}$/.test(traceId) || !/^[0-9a-f]{16}$/.test(spanId)) return null;
195
+ return { traceId, spanId };
196
+ }
197
+
198
+ export function traceStart(kind, method, parent) {
199
+ if (tracer === null || !traceAllowed(method)) return null;
200
+ const ctx = {
201
+ traceId: parent ? parent.traceId : randomHex(16),
202
+ spanId: randomHex(8),
203
+ parentSpanId: parent ? parent.spanId : "",
204
+ };
205
+ try {
206
+ tracer.onStart?.(kind, method, ctx.traceId, ctx.spanId, ctx.parentSpanId);
207
+ } catch {
208
+ // observers must never break the call
209
+ }
210
+ return ctx;
211
+ }
212
+
213
+ export function traceEnd(kind, method, ctx, status) {
214
+ if (ctx === null || tracer === null) return;
215
+ try {
216
+ tracer.onEnd?.(kind, method, ctx.traceId, ctx.spanId, status);
217
+ } catch {
218
+ // observers must never break the call
219
+ }
220
+ }
package/src/dynamic.js ADDED
@@ -0,0 +1,426 @@
1
+ // @generated by grpc-dc codegen. DO NOT EDIT.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ // Dynamic, descriptor-driven client and server.
4
+ //
5
+ // Parses a FileDescriptorSet (as produced by
6
+ // `protoc --descriptor_set_out`) at runtime and builds clients and
7
+ // servers whose messages are generic field maps — no generated stubs
8
+ // required. Schemas arrive as data (descriptor.proto), not as code.
9
+ //
10
+ // Generic values by field type: varint family -> number, string ->
11
+ // string, bytes -> Uint8Array, message -> Map, fixed/float -> number,
12
+ // bool -> boolean. GenericMessage wraps Map<number, values[]>.
13
+
14
+ import { StatusError } from "./codec.js";
15
+ import { StatusCode } from "../wire_gen.js";
16
+
17
+ export const FieldType = {
18
+ DOUBLE: 1, FLOAT: 2, INT64: 3, UINT64: 4, INT32: 5, FIXED64: 6,
19
+ FIXED32: 7, BOOL: 8, STRING: 9, GROUP: 10, MESSAGE: 11, BYTES: 12,
20
+ UINT32: 13, ENUM: 14, SFIXED32: 15, SFIXED64: 16, SINT32: 17,
21
+ SINT64: 18,
22
+ };
23
+
24
+ const VARINT_TYPES = new Set([3, 4, 5, 8, 13, 14, 17, 18]);
25
+ const WIRE_TYPE = new Map();
26
+ for (const t of VARINT_TYPES) WIRE_TYPE.set(t, 0);
27
+ for (const t of [6, 16, 1]) WIRE_TYPE.set(t, 1);
28
+ for (const t of [9, 11, 12, 10]) WIRE_TYPE.set(t, 2);
29
+ for (const t of [7, 15, 2]) WIRE_TYPE.set(t, 5);
30
+
31
+ const LABEL_REPEATED = 3;
32
+
33
+ class PbReader {
34
+ constructor(buf) {
35
+ this.buf = buf;
36
+ this.pos = 0;
37
+ }
38
+
39
+ varint() {
40
+ let result = 0n;
41
+ let shift = 0n;
42
+ for (;;) {
43
+ if (this.pos >= this.buf.length) throw new Error("truncated varint");
44
+ const b = this.buf[this.pos++];
45
+ result |= BigInt(b & 0x7f) << shift;
46
+ if ((b & 0x80) === 0) return result;
47
+ shift += 7n;
48
+ if (shift >= 70n) throw new Error("varint too long");
49
+ }
50
+ }
51
+
52
+ bytesField() {
53
+ const n = Number(this.varint());
54
+ if (this.pos + n > this.buf.length) throw new Error("truncated bytes field");
55
+ const out = this.buf.subarray(this.pos, this.pos + n);
56
+ this.pos += n;
57
+ return out;
58
+ }
59
+
60
+ *fields() {
61
+ while (this.pos < this.buf.length) {
62
+ const start = this.pos;
63
+ const tag = this.varint();
64
+ const number = Number(tag >> 3n);
65
+ const wire = Number(tag & 7n);
66
+ if (wire === 0) {
67
+ const v = this.varint();
68
+ yield [number, wire, v, this.buf.slice(start, this.pos)];
69
+ } else if (wire === 2) {
70
+ const v = this.bytesField();
71
+ yield [number, wire, v, this.buf.slice(start, this.pos)];
72
+ } else if (wire === 1) {
73
+ if (this.pos + 8 > this.buf.length) throw new Error("truncated fixed64");
74
+ const value = this.buf.subarray(this.pos, this.pos + 8);
75
+ this.pos += 8;
76
+ yield [number, wire, value, this.buf.slice(start, this.pos)];
77
+ } else if (wire === 5) {
78
+ if (this.pos + 4 > this.buf.length) throw new Error("truncated fixed32");
79
+ const value = this.buf.subarray(this.pos, this.pos + 4);
80
+ this.pos += 4;
81
+ yield [number, wire, value, this.buf.slice(start, this.pos)];
82
+ } else {
83
+ throw new Error(`unsupported wire type ${wire}`);
84
+ }
85
+ }
86
+ }
87
+ }
88
+
89
+ function pbString(fields, number) {
90
+ for (const [num, , value] of fields) {
91
+ if (num === number) return new TextDecoder().decode(value);
92
+ }
93
+ return "";
94
+ }
95
+
96
+ function pbUint(fields, number) {
97
+ for (const [num, , value] of fields) {
98
+ if (num === number) return value;
99
+ }
100
+ return 0n;
101
+ }
102
+
103
+ export class GenericMessage {
104
+ constructor(model = null) {
105
+ this.model = model;
106
+ this.values = new Map();
107
+ // Raw encoded chunks of fields not in the model (protobuf
108
+ // unknown-field retention); re-emitted after known fields.
109
+ this.unknown = [];
110
+ }
111
+
112
+ set(number, value) {
113
+ this.values.set(number, [value]);
114
+ return this;
115
+ }
116
+
117
+ add(number, value) {
118
+ if (!this.values.has(number)) this.values.set(number, []);
119
+ this.values.get(number).push(value);
120
+ return this;
121
+ }
122
+
123
+ getAll(number) {
124
+ return [...(this.values.get(number) ?? [])];
125
+ }
126
+
127
+ get(number, fallback = undefined) {
128
+ const items = this.values.get(number);
129
+ return items && items.length ? items[0] : fallback;
130
+ }
131
+
132
+ getString(number, fallback = "") {
133
+ const value = this.get(number);
134
+ return typeof value === "string" ? value : fallback;
135
+ }
136
+
137
+ getInt(number, fallback = 0) {
138
+ const value = this.get(number);
139
+ return typeof value === "number" ? value : fallback;
140
+ }
141
+ }
142
+
143
+ function encodeVarint(value) {
144
+ let v = BigInt(value) & 0xffffffffffffffffn;
145
+ const out = [];
146
+ while (v > 0x7fn) {
147
+ out.push(Number((v & 0x7fn) | 0x80n));
148
+ v >>= 7n;
149
+ }
150
+ out.push(Number(v));
151
+ return Uint8Array.from(out);
152
+ }
153
+
154
+ function concatBytes(parts) {
155
+ const total = parts.reduce((n, p) => n + p.length, 0);
156
+ const out = new Uint8Array(total);
157
+ let offset = 0;
158
+ for (const p of parts) {
159
+ out.set(p, offset);
160
+ offset += p.length;
161
+ }
162
+ return out;
163
+ }
164
+
165
+ function encodeValue(field, value, fileModel) {
166
+ const wire = WIRE_TYPE.get(field.type);
167
+ const parts = [encodeVarint(BigInt((field.number << 3) | wire))];
168
+ if (wire === 0) {
169
+ if (field.type === FieldType.SINT32 || field.type === FieldType.SINT64) {
170
+ const v = BigInt(value);
171
+ parts.push(encodeVarint((v << 1n) ^ (v >> 63n)));
172
+ } else if (field.type === FieldType.BOOL) {
173
+ parts.push(encodeVarint(value ? 1 : 0));
174
+ } else {
175
+ parts.push(encodeVarint(BigInt(value)));
176
+ }
177
+ } else if (wire === 1) {
178
+ const buf = new Uint8Array(8);
179
+ const view = new DataView(buf.buffer);
180
+ if (field.type === FieldType.DOUBLE) view.setFloat64(0, value, true);
181
+ else view.setBigUint64(0, BigInt(value), true);
182
+ parts.push(buf);
183
+ } else if (wire === 5) {
184
+ const buf = new Uint8Array(4);
185
+ const view = new DataView(buf.buffer);
186
+ if (field.type === FieldType.FLOAT) view.setFloat32(0, value, true);
187
+ else view.setUint32(0, value, true);
188
+ parts.push(buf);
189
+ } else {
190
+ let data;
191
+ if (field.type === FieldType.STRING) data = new TextEncoder().encode(value);
192
+ else if (field.type === FieldType.MESSAGE) {
193
+ data = encodeGeneric(fileModel.messages.get(field.typeName), value, fileModel);
194
+ } else data = value;
195
+ parts.push(encodeVarint(data.length), data);
196
+ }
197
+ return concatBytes(parts);
198
+ }
199
+
200
+ export function encodeGeneric(model, message, fileModel) {
201
+ const parts = [];
202
+ const numbers = [...message.values.keys()].sort((a, b) => a - b);
203
+ for (const number of numbers) {
204
+ const field = model.fields.get(number);
205
+ if (!field) {
206
+ throw new StatusError(StatusCode.INVALID_ARGUMENT, `unknown field ${number} for ${model.fullName}`);
207
+ }
208
+ const values = message.values.get(number);
209
+ if (field.repeated) {
210
+ for (const value of values) parts.push(encodeValue(field, value, fileModel));
211
+ } else if (values.length) {
212
+ parts.push(encodeValue(field, values[0], fileModel));
213
+ }
214
+ }
215
+ for (const chunk of message.unknown ?? []) parts.push(chunk);
216
+ return concatBytes(parts);
217
+ }
218
+
219
+ function decodeValue(field, wire, raw, fileModel) {
220
+ if (wire === 0) {
221
+ if (field.type === FieldType.BOOL) return raw !== 0n;
222
+ if (field.type === FieldType.SINT32 || field.type === FieldType.SINT64) {
223
+ return Number((raw >> 1n) ^ -(raw & 1n));
224
+ }
225
+ if (field.type === FieldType.INT32 || field.type === FieldType.ENUM) {
226
+ return Number(BigInt.asIntN(32, raw));
227
+ }
228
+ if (field.type === FieldType.INT64) return Number(BigInt.asIntN(64, raw));
229
+ return Number(raw);
230
+ }
231
+ if (wire === 1) {
232
+ const view = new DataView(raw.buffer, raw.byteOffset, 8);
233
+ if (field.type === FieldType.DOUBLE) return view.getFloat64(0, true);
234
+ return Number(view.getBigUint64(0, true));
235
+ }
236
+ if (wire === 5) {
237
+ const view = new DataView(raw.buffer, raw.byteOffset, 4);
238
+ if (field.type === FieldType.FLOAT) return view.getFloat32(0, true);
239
+ return view.getUint32(0, true);
240
+ }
241
+ if (field.type === FieldType.STRING) return new TextDecoder().decode(raw);
242
+ if (field.type === FieldType.MESSAGE) {
243
+ return decodeGeneric(fileModel.messages.get(field.typeName), raw, fileModel);
244
+ }
245
+ return new Uint8Array(raw);
246
+ }
247
+
248
+ export function decodeGeneric(model, data, fileModel) {
249
+ const message = new GenericMessage(model);
250
+ for (const [number, wire, raw, chunk] of new PbReader(data).fields()) {
251
+ const field = model.fields.get(number);
252
+ if (!field) {
253
+ // Unknown field: retain the raw encoded chunk verbatim.
254
+ message.unknown.push(chunk);
255
+ continue;
256
+ }
257
+ if (!message.values.has(number)) message.values.set(number, []);
258
+ message.values.get(number).push(decodeValue(field, wire, raw, fileModel));
259
+ }
260
+ return message;
261
+ }
262
+
263
+ export function parseDescriptorSet(data) {
264
+ const fileModel = { messages: new Map(), services: new Map() };
265
+ const pending = [];
266
+ for (const [number, , fileBytes] of new PbReader(data).fields()) {
267
+ if (number !== 1) continue;
268
+ const fields = [...new PbReader(fileBytes).fields()];
269
+ const pkg = pbString(fields, 2);
270
+ for (const [fnum, , fval] of fields) {
271
+ if (fnum === 4) parseMessage(pkg, "", fval, fileModel);
272
+ else if (fnum === 6) pending.push([pkg, fval]);
273
+ }
274
+ }
275
+ for (const [pkg, serviceBytes] of pending) {
276
+ parseService(pkg, serviceBytes, fileModel);
277
+ }
278
+ return fileModel;
279
+ }
280
+
281
+ function parseMessage(pkg, outer, data, fileModel) {
282
+ const fields = [...new PbReader(data).fields()];
283
+ const name = pbString(fields, 1);
284
+ const full = (pkg ? pkg + "." : "") + (outer ? outer + "." : "") + name;
285
+ const model = { fullName: full, fields: new Map() };
286
+ fileModel.messages.set("." + full, model);
287
+ for (const [fnum, , fval] of fields) {
288
+ if (fnum === 2) {
289
+ const field = parseField(fval);
290
+ model.fields.set(field.number, field);
291
+ } else if (fnum === 3) {
292
+ parseMessage(pkg, full.slice(pkg ? pkg.length + 1 : 0), fval, fileModel);
293
+ }
294
+ }
295
+ return model;
296
+ }
297
+
298
+ function parseField(data) {
299
+ const fields = [...new PbReader(data).fields()];
300
+ return {
301
+ number: Number(pbUint(fields, 3)),
302
+ name: pbString(fields, 1),
303
+ type: Number(pbUint(fields, 5)),
304
+ repeated: Number(pbUint(fields, 4)) === LABEL_REPEATED,
305
+ typeName: pbString(fields, 6),
306
+ };
307
+ }
308
+
309
+ function methodKind(clientStreaming, serverStreaming) {
310
+ if (clientStreaming && serverStreaming) return "bidi";
311
+ if (serverStreaming) return "server-streaming";
312
+ if (clientStreaming) return "client-streaming";
313
+ return "unary";
314
+ }
315
+
316
+ function parseService(pkg, data, fileModel) {
317
+ const fields = [...new PbReader(data).fields()];
318
+ const name = pbString(fields, 1);
319
+ const full = pkg ? `${pkg}.${name}` : name;
320
+ const service = { fullName: full, methods: new Map() };
321
+ fileModel.services.set(full, service);
322
+ for (const [fnum, , fval] of fields) {
323
+ if (fnum !== 2) continue;
324
+ const mf = [...new PbReader(fval).fields()];
325
+ const mname = pbString(mf, 1);
326
+ service.methods.set(mname, {
327
+ name: mname,
328
+ path: `/${full}/${mname}`,
329
+ kind: methodKind(pbUint(mf, 5) !== 0n, pbUint(mf, 6) !== 0n),
330
+ input: fileModel.messages.get(pbString(mf, 2)),
331
+ output: fileModel.messages.get(pbString(mf, 3)),
332
+ });
333
+ }
334
+ return service;
335
+ }
336
+
337
+ function boundType(model, fileModel) {
338
+ return {
339
+ encode: (generic) => encodeGeneric(model, generic ?? new GenericMessage(model), fileModel),
340
+ decode: (bytes) => decodeGeneric(model, bytes, fileModel),
341
+ };
342
+ }
343
+
344
+ /** Runtime-built client: methods resolve from the file model. */
345
+ export class DynamicClient {
346
+ #channel;
347
+ #model;
348
+
349
+ constructor(channel, fileModel) {
350
+ this.#channel = channel;
351
+ this.#model = fileModel;
352
+ }
353
+
354
+ #method(serviceName, methodName) {
355
+ const service = this.#model.services.get(serviceName);
356
+ if (!service) throw new StatusError(StatusCode.INVALID_ARGUMENT, `unknown service ${serviceName}`);
357
+ const method = service.methods.get(methodName);
358
+ if (!method) throw new StatusError(StatusCode.INVALID_ARGUMENT, `unknown method ${methodName}`);
359
+ return method;
360
+ }
361
+
362
+ async unary(serviceName, methodName, request, { metadata } = {}) {
363
+ const method = this.#method(serviceName, methodName);
364
+ return this.#channel.unary(method.path, request,
365
+ boundType(method.input, this.#model), boundType(method.output, this.#model), { metadata });
366
+ }
367
+
368
+ async serverStream(serviceName, methodName, request, { metadata } = {}) {
369
+ const method = this.#method(serviceName, methodName);
370
+ return this.#channel.serverStream(method.path, request,
371
+ boundType(method.input, this.#model), boundType(method.output, this.#model), { metadata });
372
+ }
373
+
374
+ async clientStream(serviceName, methodName, requests, { metadata } = {}) {
375
+ const method = this.#method(serviceName, methodName);
376
+ return this.#channel.clientStream(method.path, requests,
377
+ boundType(method.input, this.#model), boundType(method.output, this.#model), { metadata });
378
+ }
379
+
380
+ async bidi(serviceName, methodName, requests, { metadata } = {}) {
381
+ const method = this.#method(serviceName, methodName);
382
+ return this.#channel.bidi(method.path, requests,
383
+ boundType(method.input, this.#model), boundType(method.output, this.#model), { metadata });
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Build a Server service definition from descriptors + a handler
389
+ * object with methods by kind:
390
+ * unary(ctx, method, request) -> GenericMessage
391
+ * serverStream(ctx, method, request) -> iterable/async iterable of GenericMessage
392
+ * clientStream(ctx, method, requests) -> GenericMessage
393
+ * bidi(ctx, method, requests) -> iterable/async iterable of GenericMessage
394
+ * (requests are async iterables of GenericMessage.) Pass the result
395
+ * to server.addService(definition).
396
+ */
397
+ export function createDynamicService(fileModel, serviceName, handler) {
398
+ const service = fileModel.services.get(serviceName);
399
+ if (!service) throw new StatusError(StatusCode.INVALID_ARGUMENT, `unknown service ${serviceName}`);
400
+ const methods = {};
401
+ for (const method of service.methods.values()) {
402
+ methods[method.name] = {
403
+ path: method.path,
404
+ kind: method.kind,
405
+ requestType: boundType(method.input, fileModel),
406
+ responseType: boundType(method.output, fileModel),
407
+ handler: async (...args) => {
408
+ if (method.kind === "unary") {
409
+ const [req, ctx] = args;
410
+ return handler.unary(ctx, method, req);
411
+ }
412
+ if (method.kind === "server-streaming") {
413
+ const [req, ctx] = args;
414
+ return handler.serverStream(ctx, method, req);
415
+ }
416
+ if (method.kind === "client-streaming") {
417
+ const [requests, ctx] = args;
418
+ return handler.clientStream(ctx, method, requests);
419
+ }
420
+ const [requests, ctx] = args;
421
+ return handler.bidi(ctx, method, requests);
422
+ },
423
+ };
424
+ }
425
+ return { name: service.fullName, methods };
426
+ }