@zmdb/transport-grpc 1.0.0-beta.1
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/LICENSE +674 -0
- package/README.md +83 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime.d.ts +16 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +711 -0
- package/dist/runtime.js.map +1 -0
- package/dist/types.d.ts +120 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -0
- package/package.json +52 -0
- package/src/index.ts +22 -0
- package/src/runtime.ts +947 -0
- package/src/types.ts +157 -0
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,947 @@
|
|
|
1
|
+
import { once } from 'node:events';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Client,
|
|
5
|
+
Metadata,
|
|
6
|
+
Server,
|
|
7
|
+
ServerCredentials,
|
|
8
|
+
credentials,
|
|
9
|
+
status,
|
|
10
|
+
type ClientDuplexStream,
|
|
11
|
+
type ClientReadableStream,
|
|
12
|
+
type ClientUnaryCall,
|
|
13
|
+
type ClientWritableStream,
|
|
14
|
+
type MethodDefinition,
|
|
15
|
+
type ServerDuplexStream,
|
|
16
|
+
type ServerErrorResponse,
|
|
17
|
+
type ServerReadableStream,
|
|
18
|
+
type ServerUnaryCall,
|
|
19
|
+
type ServerWritableStream,
|
|
20
|
+
type ServiceDefinition,
|
|
21
|
+
type StatusObject,
|
|
22
|
+
type UntypedHandleCall,
|
|
23
|
+
type UntypedServiceImplementation,
|
|
24
|
+
type sendUnaryData,
|
|
25
|
+
} from '@grpc/grpc-js';
|
|
26
|
+
import type { ApplicationExtension } from '@zmdb/app';
|
|
27
|
+
import type { GrpcLoadedMethod, GrpcMethodDef, GrpcServiceDef } from '@zmdb/protobuf';
|
|
28
|
+
|
|
29
|
+
import {
|
|
30
|
+
GrpcError,
|
|
31
|
+
type GrpcBinding,
|
|
32
|
+
type GrpcCall,
|
|
33
|
+
type GrpcClient,
|
|
34
|
+
type GrpcClientCallOptions,
|
|
35
|
+
type GrpcClientOptions,
|
|
36
|
+
type GrpcClientTlsOptions,
|
|
37
|
+
type GrpcHandlers,
|
|
38
|
+
type GrpcMetadata,
|
|
39
|
+
type GrpcMetadataValidator,
|
|
40
|
+
type GrpcServerOptions,
|
|
41
|
+
type GrpcServerTlsOptions,
|
|
42
|
+
type GrpcServiceSpec,
|
|
43
|
+
type GrpcStatus,
|
|
44
|
+
} from './types.js';
|
|
45
|
+
|
|
46
|
+
export interface OpenedGrpcServer {
|
|
47
|
+
readonly port: number;
|
|
48
|
+
close(graceMs: number): Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type RuntimeMethod = GrpcLoadedMethod<GrpcMethodDef>;
|
|
52
|
+
|
|
53
|
+
type DecodedRequest = { readonly ok: true; readonly value: unknown } | { readonly ok: false; readonly error: unknown };
|
|
54
|
+
|
|
55
|
+
interface ServerCallSurface {
|
|
56
|
+
readonly cancelled: boolean;
|
|
57
|
+
readonly metadata: Metadata;
|
|
58
|
+
getDeadline(): Date | number;
|
|
59
|
+
getPeer(): string;
|
|
60
|
+
on(event: string, listener: () => void): this;
|
|
61
|
+
removeListener(event: string, listener: () => void): this;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface WritableResponseCall extends ServerCallSurface {
|
|
65
|
+
write(value: unknown): boolean;
|
|
66
|
+
end(metadata?: Metadata): void;
|
|
67
|
+
destroy(error: Error): void;
|
|
68
|
+
once(event: 'drain', listener: () => void): this;
|
|
69
|
+
removeListener(event: 'drain', listener: () => void): this;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface ReadableRequestCall extends ServerCallSurface, AsyncIterable<DecodedRequest> {}
|
|
73
|
+
|
|
74
|
+
interface CallScope {
|
|
75
|
+
readonly signal: AbortSignal;
|
|
76
|
+
readonly trailers: Readonly<Record<string, string>>;
|
|
77
|
+
remainingMs(): number;
|
|
78
|
+
setTrailer(key: string, value: string): void;
|
|
79
|
+
reason(): GrpcError;
|
|
80
|
+
close(): void;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface ClientSurfaceCall {
|
|
84
|
+
cancel(): void;
|
|
85
|
+
on(event: 'metadata', listener: (metadata: Metadata) => void): this;
|
|
86
|
+
on(event: 'status', listener: (status: StatusObject) => void): this;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface RequestPump {
|
|
90
|
+
readonly done: Promise<void>;
|
|
91
|
+
failure(): { readonly failed: false } | { readonly failed: true; readonly error: unknown };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
class BoundGrpcService<S extends GrpcServiceDef> implements GrpcBinding {
|
|
95
|
+
readonly service: string;
|
|
96
|
+
readonly methods: readonly string[];
|
|
97
|
+
readonly #spec: GrpcServiceSpec<S>;
|
|
98
|
+
readonly #handlers: GrpcHandlers<S>;
|
|
99
|
+
|
|
100
|
+
constructor(spec: GrpcServiceSpec<S>, handlers: GrpcHandlers<S>) {
|
|
101
|
+
this.#spec = spec;
|
|
102
|
+
this.#handlers = handlers;
|
|
103
|
+
this.service = spec.definition.name;
|
|
104
|
+
this.methods = Object.freeze(Object.keys(spec.definition.methods));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
register(server: Server): void {
|
|
108
|
+
const definition: Record<string, MethodDefinition<DecodedRequest, unknown>> = Object.create(null);
|
|
109
|
+
const implementation: UntypedServiceImplementation = Object.create(null);
|
|
110
|
+
|
|
111
|
+
for (const [name, method] of methodEntries(this.#spec.definition.methods)) {
|
|
112
|
+
definition[name] = {
|
|
113
|
+
path: method.path,
|
|
114
|
+
requestStream: method.requestStream,
|
|
115
|
+
responseStream: method.responseStream,
|
|
116
|
+
requestSerialize: value => grpcBytes(serializeRequest(method, value)),
|
|
117
|
+
requestDeserialize: bytes => decodeRequest(method, bytes),
|
|
118
|
+
responseSerialize: value => grpcBytes(serializeResponse(method, value)),
|
|
119
|
+
responseDeserialize: bytes => deserializeResponse(method, bytes),
|
|
120
|
+
};
|
|
121
|
+
implementation[name] = serverHandler(name, method, this.#spec, this.#handlers);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const serviceDefinition: ServiceDefinition = definition;
|
|
125
|
+
server.addService(serviceDefinition, implementation);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Bind one generated service definition to its exhaustive typed handler map. */
|
|
130
|
+
export function bindGrpcService<S extends GrpcServiceDef>(
|
|
131
|
+
service: GrpcServiceSpec<S>,
|
|
132
|
+
handlers: GrpcHandlers<S>,
|
|
133
|
+
): GrpcBinding {
|
|
134
|
+
validateServiceSpec(service);
|
|
135
|
+
return new BoundGrpcService(service, handlers);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Attach one gRPC server to the protocol-neutral application lifecycle. */
|
|
139
|
+
export function grpcExtension(options: GrpcServerOptions): ApplicationExtension {
|
|
140
|
+
let opened: OpenedGrpcServer | undefined;
|
|
141
|
+
return {
|
|
142
|
+
name: '@zmdb/transport-grpc',
|
|
143
|
+
async start() {
|
|
144
|
+
opened = await openGrpcServer(options);
|
|
145
|
+
},
|
|
146
|
+
async stop({ graceMs }) {
|
|
147
|
+
try {
|
|
148
|
+
await opened?.close(graceMs);
|
|
149
|
+
} finally {
|
|
150
|
+
opened = undefined;
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Create a typed client from the same generated service artifact used by the server. */
|
|
157
|
+
export function createGrpcClient<S extends GrpcServiceDef>(options: GrpcClientOptions<S>): GrpcClient<S> {
|
|
158
|
+
validatePositiveDuration(options.deadlineMs, 'deadlineMs');
|
|
159
|
+
const channel = new Client(options.address, clientCredentials(options.credentials));
|
|
160
|
+
const client: GrpcClient<S> = Object.create(null);
|
|
161
|
+
|
|
162
|
+
for (const [name, method] of methodEntries(options.definition.methods)) {
|
|
163
|
+
if (name === 'close') {
|
|
164
|
+
channel.close();
|
|
165
|
+
throw new Error(
|
|
166
|
+
'@zmdb/transport-grpc: a gRPC method cannot be named "close" because the typed client owns that member',
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
Object.defineProperty(client, name, {
|
|
170
|
+
configurable: false,
|
|
171
|
+
enumerable: true,
|
|
172
|
+
value: clientCaller(channel, method, options),
|
|
173
|
+
writable: false,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const close = (): void => {
|
|
178
|
+
channel.close();
|
|
179
|
+
};
|
|
180
|
+
Object.defineProperties(client, {
|
|
181
|
+
close: { configurable: false, enumerable: false, value: close, writable: false },
|
|
182
|
+
[Symbol.dispose]: { configurable: false, enumerable: false, value: close, writable: false },
|
|
183
|
+
});
|
|
184
|
+
return client;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Start all bound services. Kept internal to the application lifecycle. */
|
|
188
|
+
export async function openGrpcServer(options: GrpcServerOptions): Promise<OpenedGrpcServer> {
|
|
189
|
+
if (options.address.length === 0) {
|
|
190
|
+
throw new RangeError('@zmdb/transport-grpc: a gRPC server address cannot be empty');
|
|
191
|
+
}
|
|
192
|
+
if (options.bindings.length === 0) {
|
|
193
|
+
throw new RangeError('@zmdb/transport-grpc: a gRPC server requires at least one binding');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const server = new Server();
|
|
197
|
+
try {
|
|
198
|
+
for (const binding of options.bindings) {
|
|
199
|
+
if (!(binding instanceof BoundGrpcService)) {
|
|
200
|
+
throw new TypeError('@zmdb/transport-grpc: gRPC bindings must be created by bindGrpcService');
|
|
201
|
+
}
|
|
202
|
+
binding.register(server);
|
|
203
|
+
}
|
|
204
|
+
const port = await bindServer(server, options.address, serverCredentials(options.credentials));
|
|
205
|
+
let closePromise: Promise<void> | undefined;
|
|
206
|
+
return {
|
|
207
|
+
port,
|
|
208
|
+
close: graceMs => {
|
|
209
|
+
closePromise ??= closeServer(server, graceMs);
|
|
210
|
+
return closePromise;
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
} catch (error) {
|
|
214
|
+
server.forceShutdown();
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function methodEntries(
|
|
220
|
+
methods: Readonly<Record<string, RuntimeMethod>>,
|
|
221
|
+
): readonly (readonly [string, RuntimeMethod])[] {
|
|
222
|
+
return Object.entries(methods);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function validateServiceSpec<S extends GrpcServiceDef>(service: GrpcServiceSpec<S>): void {
|
|
226
|
+
if (service.definition.name.length === 0) {
|
|
227
|
+
throw new RangeError('@zmdb/transport-grpc: a gRPC service name cannot be empty');
|
|
228
|
+
}
|
|
229
|
+
if (Object.keys(service.definition.methods).length === 0) {
|
|
230
|
+
throw new RangeError(`@zmdb/transport-grpc: gRPC service "${service.definition.name}" has no methods`);
|
|
231
|
+
}
|
|
232
|
+
if (service.maxDurationMs !== undefined) {
|
|
233
|
+
validatePositiveDuration(service.maxDurationMs, 'maxDurationMs');
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function validatePositiveDuration(value: number, name: string): void {
|
|
238
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
239
|
+
throw new RangeError(`@zmdb/transport-grpc: ${name} must be a positive finite number`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function grpcBytes(bytes: Uint8Array): ReturnType<typeof globalThis.Buffer.from> {
|
|
244
|
+
return globalThis.Buffer.from(bytes);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function decodeRequest(method: RuntimeMethod, bytes: Uint8Array): DecodedRequest {
|
|
248
|
+
try {
|
|
249
|
+
const value = method.deserializeRequest(bytes);
|
|
250
|
+
return { ok: true, value: method.validateRequest(value) };
|
|
251
|
+
} catch (error) {
|
|
252
|
+
return { ok: false, error };
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function serializeRequest(method: RuntimeMethod, value: unknown): Uint8Array {
|
|
257
|
+
return method.serializeRequest(method.validateRequest(value));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function serializeResponse(method: RuntimeMethod, value: unknown): Uint8Array {
|
|
261
|
+
return method.serializeResponse(method.validateResponse(value));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function deserializeResponse(method: RuntimeMethod, bytes: Uint8Array): unknown {
|
|
265
|
+
return method.validateResponse(method.deserializeResponse(bytes));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function serverHandler<S extends GrpcServiceDef>(
|
|
269
|
+
name: string,
|
|
270
|
+
method: RuntimeMethod,
|
|
271
|
+
spec: GrpcServiceSpec<S>,
|
|
272
|
+
handlers: GrpcHandlers<S>,
|
|
273
|
+
): UntypedHandleCall {
|
|
274
|
+
if (method.requestStream) {
|
|
275
|
+
return method.responseStream
|
|
276
|
+
? (call: ServerDuplexStream<DecodedRequest, unknown>) => {
|
|
277
|
+
void runBidi(call, name, method, spec, handlers);
|
|
278
|
+
}
|
|
279
|
+
: (call: ServerReadableStream<DecodedRequest, unknown>, callback: sendUnaryData<unknown>) => {
|
|
280
|
+
void runClientStream(call, callback, name, method, spec, handlers);
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
return method.responseStream
|
|
284
|
+
? (call: ServerWritableStream<DecodedRequest, unknown>) => {
|
|
285
|
+
void runServerStream(call, name, method, spec, handlers);
|
|
286
|
+
}
|
|
287
|
+
: (call: ServerUnaryCall<DecodedRequest, unknown>, callback: sendUnaryData<unknown>) => {
|
|
288
|
+
void runUnary(call, callback, name, method, spec, handlers);
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function runUnary<S extends GrpcServiceDef>(
|
|
293
|
+
call: ServerUnaryCall<DecodedRequest, unknown>,
|
|
294
|
+
callback: sendUnaryData<unknown>,
|
|
295
|
+
name: string,
|
|
296
|
+
method: RuntimeMethod,
|
|
297
|
+
spec: GrpcServiceSpec<S>,
|
|
298
|
+
handlers: GrpcHandlers<S>,
|
|
299
|
+
): Promise<void> {
|
|
300
|
+
const scope = callScope(call, spec.maxDurationMs);
|
|
301
|
+
try {
|
|
302
|
+
const request = requestValue(call.request);
|
|
303
|
+
const context = grpcCall(call, spec, name, request, scope);
|
|
304
|
+
const handler = handlerAt<(call: GrpcCall<unknown>) => Promise<unknown>>(handlers, name);
|
|
305
|
+
const response = method.validateResponse(await handler(context));
|
|
306
|
+
callback(null, response, trailers(scope.trailers));
|
|
307
|
+
} catch (error) {
|
|
308
|
+
callback(serverError(boundaryError(error, scope, spec, name)));
|
|
309
|
+
} finally {
|
|
310
|
+
scope.close();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function runClientStream<S extends GrpcServiceDef>(
|
|
315
|
+
call: ServerReadableStream<DecodedRequest, unknown>,
|
|
316
|
+
callback: sendUnaryData<unknown>,
|
|
317
|
+
name: string,
|
|
318
|
+
method: RuntimeMethod,
|
|
319
|
+
spec: GrpcServiceSpec<S>,
|
|
320
|
+
handlers: GrpcHandlers<S>,
|
|
321
|
+
): Promise<void> {
|
|
322
|
+
const scope = callScope(call, spec.maxDurationMs);
|
|
323
|
+
try {
|
|
324
|
+
const requests = requestStream(call, scope);
|
|
325
|
+
const context = grpcCall(call, spec, name, requests, scope);
|
|
326
|
+
const handler = handlerAt<(call: GrpcCall<AsyncIterable<unknown>>) => Promise<unknown>>(handlers, name);
|
|
327
|
+
const response = method.validateResponse(await handler(context));
|
|
328
|
+
callback(null, response, trailers(scope.trailers));
|
|
329
|
+
} catch (error) {
|
|
330
|
+
callback(serverError(boundaryError(error, scope, spec, name)));
|
|
331
|
+
} finally {
|
|
332
|
+
scope.close();
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function runServerStream<S extends GrpcServiceDef>(
|
|
337
|
+
call: ServerWritableStream<DecodedRequest, unknown>,
|
|
338
|
+
name: string,
|
|
339
|
+
method: RuntimeMethod,
|
|
340
|
+
spec: GrpcServiceSpec<S>,
|
|
341
|
+
handlers: GrpcHandlers<S>,
|
|
342
|
+
): Promise<void> {
|
|
343
|
+
const scope = callScope(call, spec.maxDurationMs);
|
|
344
|
+
try {
|
|
345
|
+
const request = requestValue(call.request);
|
|
346
|
+
const context = grpcCall(call, spec, name, request, scope);
|
|
347
|
+
const handler = handlerAt<(call: GrpcCall<unknown>) => AsyncIterable<unknown>>(handlers, name);
|
|
348
|
+
await writeResponses(call, handler(context), method, scope);
|
|
349
|
+
call.end(trailers(scope.trailers));
|
|
350
|
+
} catch (error) {
|
|
351
|
+
call.destroy(serverError(boundaryError(error, scope, spec, name)));
|
|
352
|
+
} finally {
|
|
353
|
+
scope.close();
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function runBidi<S extends GrpcServiceDef>(
|
|
358
|
+
call: ServerDuplexStream<DecodedRequest, unknown>,
|
|
359
|
+
name: string,
|
|
360
|
+
method: RuntimeMethod,
|
|
361
|
+
spec: GrpcServiceSpec<S>,
|
|
362
|
+
handlers: GrpcHandlers<S>,
|
|
363
|
+
): Promise<void> {
|
|
364
|
+
const scope = callScope(call, spec.maxDurationMs);
|
|
365
|
+
try {
|
|
366
|
+
const requests = requestStream(call, scope);
|
|
367
|
+
const context = grpcCall(call, spec, name, requests, scope);
|
|
368
|
+
const handler = handlerAt<(call: GrpcCall<AsyncIterable<unknown>>) => AsyncIterable<unknown>>(handlers, name);
|
|
369
|
+
await writeResponses(call, handler(context), method, scope);
|
|
370
|
+
call.end(trailers(scope.trailers));
|
|
371
|
+
} catch (error) {
|
|
372
|
+
call.destroy(serverError(boundaryError(error, scope, spec, name)));
|
|
373
|
+
} finally {
|
|
374
|
+
scope.close();
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function requestValue(decoded: DecodedRequest): unknown {
|
|
379
|
+
if (!decoded.ok) {
|
|
380
|
+
throw new GrpcError('INVALID_ARGUMENT', 'invalid request');
|
|
381
|
+
}
|
|
382
|
+
return decoded.value;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async function* requestStream(call: ReadableRequestCall, scope: CallScope): AsyncIterable<unknown> {
|
|
386
|
+
const iterator = call[Symbol.asyncIterator]();
|
|
387
|
+
try {
|
|
388
|
+
for (;;) {
|
|
389
|
+
const next = await nextRequest(iterator, scope);
|
|
390
|
+
if (next.done) return;
|
|
391
|
+
yield requestValue(next.value);
|
|
392
|
+
}
|
|
393
|
+
} finally {
|
|
394
|
+
await iterator.return?.();
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function nextRequest(
|
|
399
|
+
iterator: AsyncIterator<DecodedRequest>,
|
|
400
|
+
scope: CallScope,
|
|
401
|
+
): Promise<IteratorResult<DecodedRequest>> {
|
|
402
|
+
if (scope.signal.aborted) throw scope.reason();
|
|
403
|
+
let removeAbort = (): void => undefined;
|
|
404
|
+
const aborted = new Promise<never>((_resolve, reject) => {
|
|
405
|
+
const onAbort = (): void => {
|
|
406
|
+
reject(scope.reason());
|
|
407
|
+
};
|
|
408
|
+
scope.signal.addEventListener('abort', onAbort, { once: true });
|
|
409
|
+
removeAbort = () => {
|
|
410
|
+
scope.signal.removeEventListener('abort', onAbort);
|
|
411
|
+
};
|
|
412
|
+
});
|
|
413
|
+
try {
|
|
414
|
+
return await Promise.race([iterator.next(), aborted]);
|
|
415
|
+
} finally {
|
|
416
|
+
removeAbort();
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async function writeResponses(
|
|
421
|
+
call: WritableResponseCall,
|
|
422
|
+
responses: AsyncIterable<unknown>,
|
|
423
|
+
method: RuntimeMethod,
|
|
424
|
+
scope: CallScope,
|
|
425
|
+
): Promise<void> {
|
|
426
|
+
for await (const response of responses) {
|
|
427
|
+
if (scope.signal.aborted) throw scope.reason();
|
|
428
|
+
const valid = method.validateResponse(response);
|
|
429
|
+
if (!call.write(valid)) {
|
|
430
|
+
await waitForDrain(call, scope);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function handlerAt<T>(handlers: object, name: string): T {
|
|
436
|
+
return Reflect.get(handlers, name);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function waitForDrain(call: WritableResponseCall, scope: CallScope): Promise<void> {
|
|
440
|
+
if (scope.signal.aborted) return Promise.reject(scope.reason());
|
|
441
|
+
return new Promise<void>((resolve, reject) => {
|
|
442
|
+
const onDrain = (): void => {
|
|
443
|
+
scope.signal.removeEventListener('abort', onAbort);
|
|
444
|
+
resolve();
|
|
445
|
+
};
|
|
446
|
+
const onAbort = (): void => {
|
|
447
|
+
call.removeListener('drain', onDrain);
|
|
448
|
+
reject(scope.reason());
|
|
449
|
+
};
|
|
450
|
+
call.once('drain', onDrain);
|
|
451
|
+
scope.signal.addEventListener('abort', onAbort, { once: true });
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function grpcCall<T, S extends GrpcServiceDef>(
|
|
456
|
+
call: ServerCallSurface,
|
|
457
|
+
spec: GrpcServiceSpec<S>,
|
|
458
|
+
method: string,
|
|
459
|
+
payload: T,
|
|
460
|
+
scope: CallScope,
|
|
461
|
+
): GrpcCall<T> {
|
|
462
|
+
let metadata: GrpcMetadata;
|
|
463
|
+
try {
|
|
464
|
+
metadata = validateMetadata(call.metadata, spec.validateMetadata);
|
|
465
|
+
} catch {
|
|
466
|
+
throw new GrpcError('INVALID_ARGUMENT', 'invalid metadata');
|
|
467
|
+
}
|
|
468
|
+
return {
|
|
469
|
+
kind: 'grpc',
|
|
470
|
+
service: spec.definition.name,
|
|
471
|
+
method,
|
|
472
|
+
payload,
|
|
473
|
+
headers: metadata.headers,
|
|
474
|
+
binaryHeaders: metadata.binaryHeaders,
|
|
475
|
+
peer: call.getPeer(),
|
|
476
|
+
signal: scope.signal,
|
|
477
|
+
remainingMs: () => scope.remainingMs(),
|
|
478
|
+
setTrailer: (key, value) => {
|
|
479
|
+
scope.setTrailer(key, value);
|
|
480
|
+
},
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function callScope(call: ServerCallSurface, maxDurationMs?: number): CallScope {
|
|
485
|
+
const controller = new AbortController();
|
|
486
|
+
const trailerValues: Record<string, string> = Object.create(null);
|
|
487
|
+
const now = Date.now();
|
|
488
|
+
const callerDeadline = deadlineTime(call.getDeadline());
|
|
489
|
+
const serverDeadline = maxDurationMs === undefined ? Number.POSITIVE_INFINITY : now + maxDurationMs;
|
|
490
|
+
const deadline = Math.min(callerDeadline, serverDeadline);
|
|
491
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
492
|
+
|
|
493
|
+
const abort = (reason: GrpcError): void => {
|
|
494
|
+
if (!controller.signal.aborted) controller.abort(reason);
|
|
495
|
+
};
|
|
496
|
+
const onCancelled = (): void => {
|
|
497
|
+
const expired = Number.isFinite(deadline) && Date.now() >= deadline;
|
|
498
|
+
abort(
|
|
499
|
+
expired ? new GrpcError('DEADLINE_EXCEEDED', 'deadline exceeded') : new GrpcError('CANCELLED', 'call cancelled'),
|
|
500
|
+
);
|
|
501
|
+
};
|
|
502
|
+
const scheduleDeadline = (): void => {
|
|
503
|
+
if (!Number.isFinite(deadline) || controller.signal.aborted) return;
|
|
504
|
+
const remaining = deadline - Date.now();
|
|
505
|
+
if (remaining <= 0) {
|
|
506
|
+
abort(new GrpcError('DEADLINE_EXCEEDED', 'deadline exceeded'));
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
timer = setTimeout(scheduleDeadline, Math.min(remaining, 2_147_483_647));
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
call.on('cancelled', onCancelled);
|
|
513
|
+
if (call.cancelled) onCancelled();
|
|
514
|
+
scheduleDeadline();
|
|
515
|
+
|
|
516
|
+
return {
|
|
517
|
+
signal: controller.signal,
|
|
518
|
+
trailers: trailerValues,
|
|
519
|
+
remainingMs: () => {
|
|
520
|
+
if (controller.signal.aborted) return 0;
|
|
521
|
+
return Number.isFinite(deadline) ? Math.max(0, deadline - Date.now()) : Number.POSITIVE_INFINITY;
|
|
522
|
+
},
|
|
523
|
+
setTrailer: (key, value) => {
|
|
524
|
+
const probe = new Metadata();
|
|
525
|
+
probe.set(key, value);
|
|
526
|
+
trailerValues[key] = value;
|
|
527
|
+
},
|
|
528
|
+
reason: () =>
|
|
529
|
+
controller.signal.reason instanceof GrpcError
|
|
530
|
+
? controller.signal.reason
|
|
531
|
+
: new GrpcError('CANCELLED', 'call cancelled'),
|
|
532
|
+
close: () => {
|
|
533
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
534
|
+
call.removeListener('cancelled', onCancelled);
|
|
535
|
+
},
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function deadlineTime(deadline: Date | number): number {
|
|
540
|
+
return deadline instanceof Date ? deadline.getTime() : deadline;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function validateMetadata(metadata: Metadata, validate: GrpcMetadataValidator): GrpcMetadata {
|
|
544
|
+
return validate(metadataValue(metadata));
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function metadataValue(metadata: Metadata): GrpcMetadata {
|
|
548
|
+
const headers: Record<string, string> = Object.create(null);
|
|
549
|
+
const binaryHeaders: Record<string, Uint8Array> = Object.create(null);
|
|
550
|
+
for (const [key, values] of Object.entries(metadata.toJSON())) {
|
|
551
|
+
const value = values[0];
|
|
552
|
+
if (typeof value === 'string') {
|
|
553
|
+
headers[key] = value;
|
|
554
|
+
} else if (value !== undefined) {
|
|
555
|
+
binaryHeaders[key] = Uint8Array.from(value);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return { headers, binaryHeaders };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function outboundMetadata(metadata?: GrpcMetadata): Metadata {
|
|
562
|
+
const result = new Metadata();
|
|
563
|
+
if (metadata === undefined) return result;
|
|
564
|
+
for (const [key, value] of Object.entries(metadata.headers)) result.set(key, value);
|
|
565
|
+
for (const [key, value] of Object.entries(metadata.binaryHeaders)) result.set(key, grpcBytes(value));
|
|
566
|
+
return result;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function trailers(values: Readonly<Record<string, string>>): Metadata {
|
|
570
|
+
const result = new Metadata();
|
|
571
|
+
for (const [key, value] of Object.entries(values)) result.set(key, value);
|
|
572
|
+
return result;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function boundaryError<S extends GrpcServiceDef>(
|
|
576
|
+
error: unknown,
|
|
577
|
+
scope: CallScope,
|
|
578
|
+
spec: GrpcServiceSpec<S>,
|
|
579
|
+
method: string,
|
|
580
|
+
): GrpcError {
|
|
581
|
+
if (error instanceof GrpcError) return error;
|
|
582
|
+
if (scope.signal.aborted) return scope.reason();
|
|
583
|
+
try {
|
|
584
|
+
spec.onError({
|
|
585
|
+
service: spec.definition.name,
|
|
586
|
+
method,
|
|
587
|
+
status: 'INTERNAL',
|
|
588
|
+
error,
|
|
589
|
+
});
|
|
590
|
+
} catch {
|
|
591
|
+
// Observation must not replace the fixed boundary error.
|
|
592
|
+
}
|
|
593
|
+
return new GrpcError('INTERNAL', 'internal error');
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function serverError(error: GrpcError): ServerErrorResponse {
|
|
597
|
+
return Object.assign(new Error(error.details), {
|
|
598
|
+
code: statusCode(error.status),
|
|
599
|
+
details: error.details,
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function statusCode(value: GrpcStatus): number {
|
|
604
|
+
switch (value) {
|
|
605
|
+
case 'OK':
|
|
606
|
+
return status.OK;
|
|
607
|
+
case 'CANCELLED':
|
|
608
|
+
return status.CANCELLED;
|
|
609
|
+
case 'INVALID_ARGUMENT':
|
|
610
|
+
return status.INVALID_ARGUMENT;
|
|
611
|
+
case 'DEADLINE_EXCEEDED':
|
|
612
|
+
return status.DEADLINE_EXCEEDED;
|
|
613
|
+
case 'NOT_FOUND':
|
|
614
|
+
return status.NOT_FOUND;
|
|
615
|
+
case 'ALREADY_EXISTS':
|
|
616
|
+
return status.ALREADY_EXISTS;
|
|
617
|
+
case 'PERMISSION_DENIED':
|
|
618
|
+
return status.PERMISSION_DENIED;
|
|
619
|
+
case 'RESOURCE_EXHAUSTED':
|
|
620
|
+
return status.RESOURCE_EXHAUSTED;
|
|
621
|
+
case 'FAILED_PRECONDITION':
|
|
622
|
+
return status.FAILED_PRECONDITION;
|
|
623
|
+
case 'UNIMPLEMENTED':
|
|
624
|
+
return status.UNIMPLEMENTED;
|
|
625
|
+
case 'INTERNAL':
|
|
626
|
+
return status.INTERNAL;
|
|
627
|
+
case 'UNAVAILABLE':
|
|
628
|
+
return status.UNAVAILABLE;
|
|
629
|
+
case 'UNAUTHENTICATED':
|
|
630
|
+
return status.UNAUTHENTICATED;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function serverCredentials(options: 'insecure' | GrpcServerTlsOptions): ServerCredentials {
|
|
635
|
+
if (options === 'insecure') return ServerCredentials.createInsecure();
|
|
636
|
+
return ServerCredentials.createSsl(
|
|
637
|
+
options.rootCertificates === undefined ? null : grpcBytes(options.rootCertificates),
|
|
638
|
+
options.keyCertPairs.map(pair => ({
|
|
639
|
+
private_key: grpcBytes(pair.privateKey),
|
|
640
|
+
cert_chain: grpcBytes(pair.certificateChain),
|
|
641
|
+
})),
|
|
642
|
+
options.checkClientCertificate,
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function clientCredentials(options: 'insecure' | GrpcClientTlsOptions): ReturnType<typeof credentials.createSsl> {
|
|
647
|
+
if (options === 'insecure') return credentials.createInsecure();
|
|
648
|
+
const hasPrivateKey = options.privateKey !== undefined;
|
|
649
|
+
const hasCertificate = options.certificateChain !== undefined;
|
|
650
|
+
if (hasPrivateKey !== hasCertificate) {
|
|
651
|
+
throw new Error('@zmdb/transport-grpc: gRPC client privateKey and certificateChain must be supplied together');
|
|
652
|
+
}
|
|
653
|
+
return credentials.createSsl(
|
|
654
|
+
options.rootCertificates === undefined ? null : grpcBytes(options.rootCertificates),
|
|
655
|
+
options.privateKey === undefined ? null : grpcBytes(options.privateKey),
|
|
656
|
+
options.certificateChain === undefined ? null : grpcBytes(options.certificateChain),
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function bindServer(server: Server, address: string, creds: ServerCredentials): Promise<number> {
|
|
661
|
+
return new Promise<number>((resolve, reject) => {
|
|
662
|
+
server.bindAsync(address, creds, (error, port) => {
|
|
663
|
+
if (error === null) resolve(port);
|
|
664
|
+
else reject(error);
|
|
665
|
+
});
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function closeServer(server: Server, graceMs: number): Promise<void> {
|
|
670
|
+
if (!Number.isFinite(graceMs) || graceMs < 0) {
|
|
671
|
+
throw new RangeError('@zmdb/transport-grpc: graceMs must be a non-negative finite number');
|
|
672
|
+
}
|
|
673
|
+
if (graceMs === 0) {
|
|
674
|
+
server.forceShutdown();
|
|
675
|
+
return Promise.resolve();
|
|
676
|
+
}
|
|
677
|
+
return new Promise<void>((resolve, reject) => {
|
|
678
|
+
let settled = false;
|
|
679
|
+
const timer = setTimeout(() => {
|
|
680
|
+
if (settled) return;
|
|
681
|
+
settled = true;
|
|
682
|
+
server.forceShutdown();
|
|
683
|
+
resolve();
|
|
684
|
+
}, graceMs);
|
|
685
|
+
server.tryShutdown(error => {
|
|
686
|
+
if (settled) return;
|
|
687
|
+
settled = true;
|
|
688
|
+
clearTimeout(timer);
|
|
689
|
+
if (error === undefined) resolve();
|
|
690
|
+
else reject(error);
|
|
691
|
+
});
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function clientCaller<S extends GrpcServiceDef>(
|
|
696
|
+
client: Client,
|
|
697
|
+
method: RuntimeMethod,
|
|
698
|
+
options: GrpcClientOptions<S>,
|
|
699
|
+
): unknown {
|
|
700
|
+
if (method.requestStream) {
|
|
701
|
+
return method.responseStream
|
|
702
|
+
? (payload: AsyncIterable<unknown>, callOptions?: GrpcClientCallOptions) =>
|
|
703
|
+
bidiCall(client, method, payload, options, callOptions)
|
|
704
|
+
: (payload: AsyncIterable<unknown>, callOptions?: GrpcClientCallOptions) =>
|
|
705
|
+
clientStreamCall(client, method, payload, options, callOptions);
|
|
706
|
+
}
|
|
707
|
+
return method.responseStream
|
|
708
|
+
? (payload: unknown, callOptions?: GrpcClientCallOptions) =>
|
|
709
|
+
serverStreamCall(client, method, payload, options, callOptions)
|
|
710
|
+
: (payload: unknown, callOptions?: GrpcClientCallOptions) =>
|
|
711
|
+
unaryCall(client, method, payload, options, callOptions);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function unaryCall<S extends GrpcServiceDef>(
|
|
715
|
+
client: Client,
|
|
716
|
+
method: RuntimeMethod,
|
|
717
|
+
payload: unknown,
|
|
718
|
+
options: GrpcClientOptions<S>,
|
|
719
|
+
callOptions?: GrpcClientCallOptions,
|
|
720
|
+
): Promise<unknown> {
|
|
721
|
+
const request = method.validateRequest(payload);
|
|
722
|
+
const deadlineMs = callDeadline(options.deadlineMs, callOptions);
|
|
723
|
+
return new Promise<unknown>((resolve, reject) => {
|
|
724
|
+
let observation: ClientObservation | undefined;
|
|
725
|
+
let removeAbort = (): void => undefined;
|
|
726
|
+
const call = client.makeUnaryRequest(
|
|
727
|
+
method.path,
|
|
728
|
+
value => grpcBytes(method.serializeRequest(method.validateRequest(value))),
|
|
729
|
+
bytes => method.validateResponse(method.deserializeResponse(bytes)),
|
|
730
|
+
request,
|
|
731
|
+
outboundMetadata(callOptions?.metadata),
|
|
732
|
+
{ deadline: Date.now() + deadlineMs },
|
|
733
|
+
(error, response) => {
|
|
734
|
+
removeAbort();
|
|
735
|
+
if (error !== null) {
|
|
736
|
+
reject(observation?.error ?? error);
|
|
737
|
+
} else {
|
|
738
|
+
resolve(response);
|
|
739
|
+
}
|
|
740
|
+
},
|
|
741
|
+
);
|
|
742
|
+
observation = new ClientObservation(call, options.validateMetadata, callOptions);
|
|
743
|
+
removeAbort = attachAbort(call, callOptions?.signal);
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function clientStreamCall<S extends GrpcServiceDef>(
|
|
748
|
+
client: Client,
|
|
749
|
+
method: RuntimeMethod,
|
|
750
|
+
payload: AsyncIterable<unknown>,
|
|
751
|
+
options: GrpcClientOptions<S>,
|
|
752
|
+
callOptions?: GrpcClientCallOptions,
|
|
753
|
+
): Promise<unknown> {
|
|
754
|
+
const deadlineMs = callDeadline(options.deadlineMs, callOptions);
|
|
755
|
+
return new Promise<unknown>((resolve, reject) => {
|
|
756
|
+
let settled = false;
|
|
757
|
+
let observation: ClientObservation | undefined;
|
|
758
|
+
let removeAbort = (): void => undefined;
|
|
759
|
+
const call = client.makeClientStreamRequest(
|
|
760
|
+
method.path,
|
|
761
|
+
value => grpcBytes(method.serializeRequest(method.validateRequest(value))),
|
|
762
|
+
bytes => method.validateResponse(method.deserializeResponse(bytes)),
|
|
763
|
+
outboundMetadata(callOptions?.metadata),
|
|
764
|
+
{ deadline: Date.now() + deadlineMs },
|
|
765
|
+
(error, response) => {
|
|
766
|
+
if (settled) return;
|
|
767
|
+
settled = true;
|
|
768
|
+
removeAbort();
|
|
769
|
+
if (error !== null) reject(observation?.error ?? error);
|
|
770
|
+
else resolve(response);
|
|
771
|
+
},
|
|
772
|
+
);
|
|
773
|
+
observation = new ClientObservation(call, options.validateMetadata, callOptions);
|
|
774
|
+
removeAbort = attachAbort(call, callOptions?.signal);
|
|
775
|
+
void pumpRequests(call, payload, method).catch(error => {
|
|
776
|
+
if (settled) return;
|
|
777
|
+
settled = true;
|
|
778
|
+
removeAbort();
|
|
779
|
+
call.cancel();
|
|
780
|
+
reject(error);
|
|
781
|
+
});
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function serverStreamCall<S extends GrpcServiceDef>(
|
|
786
|
+
client: Client,
|
|
787
|
+
method: RuntimeMethod,
|
|
788
|
+
payload: unknown,
|
|
789
|
+
options: GrpcClientOptions<S>,
|
|
790
|
+
callOptions?: GrpcClientCallOptions,
|
|
791
|
+
): AsyncIterable<unknown> {
|
|
792
|
+
const request = method.validateRequest(payload);
|
|
793
|
+
const deadlineMs = callDeadline(options.deadlineMs, callOptions);
|
|
794
|
+
const call = client.makeServerStreamRequest(
|
|
795
|
+
method.path,
|
|
796
|
+
value => grpcBytes(method.serializeRequest(method.validateRequest(value))),
|
|
797
|
+
bytes => method.validateResponse(method.deserializeResponse(bytes)),
|
|
798
|
+
request,
|
|
799
|
+
outboundMetadata(callOptions?.metadata),
|
|
800
|
+
{ deadline: Date.now() + deadlineMs },
|
|
801
|
+
);
|
|
802
|
+
const observation = new ClientObservation(call, options.validateMetadata, callOptions);
|
|
803
|
+
const removeAbort = attachAbort(call, callOptions?.signal);
|
|
804
|
+
return clientResponses(call, observation, removeAbort);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function bidiCall<S extends GrpcServiceDef>(
|
|
808
|
+
client: Client,
|
|
809
|
+
method: RuntimeMethod,
|
|
810
|
+
payload: AsyncIterable<unknown>,
|
|
811
|
+
options: GrpcClientOptions<S>,
|
|
812
|
+
callOptions?: GrpcClientCallOptions,
|
|
813
|
+
): AsyncIterable<unknown> {
|
|
814
|
+
const deadlineMs = callDeadline(options.deadlineMs, callOptions);
|
|
815
|
+
const call = client.makeBidiStreamRequest(
|
|
816
|
+
method.path,
|
|
817
|
+
value => grpcBytes(method.serializeRequest(method.validateRequest(value))),
|
|
818
|
+
bytes => method.validateResponse(method.deserializeResponse(bytes)),
|
|
819
|
+
outboundMetadata(callOptions?.metadata),
|
|
820
|
+
{ deadline: Date.now() + deadlineMs },
|
|
821
|
+
);
|
|
822
|
+
const observation = new ClientObservation(call, options.validateMetadata, callOptions);
|
|
823
|
+
const removeAbort = attachAbort(call, callOptions?.signal);
|
|
824
|
+
const pumping = requestPump(call, payload, method);
|
|
825
|
+
return clientResponses(call, observation, removeAbort, pumping);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function callDeadline(defaultMs: number, options?: GrpcClientCallOptions): number {
|
|
829
|
+
const value = options?.deadlineMs ?? defaultMs;
|
|
830
|
+
validatePositiveDuration(value, 'deadlineMs');
|
|
831
|
+
return value;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
async function pumpRequests(
|
|
835
|
+
call: ClientWritableStream<unknown> | ClientDuplexStream<unknown, unknown>,
|
|
836
|
+
requests: AsyncIterable<unknown>,
|
|
837
|
+
method: RuntimeMethod,
|
|
838
|
+
): Promise<void> {
|
|
839
|
+
for await (const request of requests) {
|
|
840
|
+
const valid = method.validateRequest(request);
|
|
841
|
+
if (!call.write(valid)) await once(call, 'drain');
|
|
842
|
+
}
|
|
843
|
+
call.end();
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function requestPump(
|
|
847
|
+
call: ClientDuplexStream<unknown, unknown>,
|
|
848
|
+
requests: AsyncIterable<unknown>,
|
|
849
|
+
method: RuntimeMethod,
|
|
850
|
+
): RequestPump {
|
|
851
|
+
let failure: ReturnType<RequestPump['failure']> = { failed: false };
|
|
852
|
+
const done = pumpRequests(call, requests, method).catch(error => {
|
|
853
|
+
failure = { failed: true, error };
|
|
854
|
+
call.cancel();
|
|
855
|
+
});
|
|
856
|
+
return {
|
|
857
|
+
done,
|
|
858
|
+
failure: () => failure,
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
async function* clientResponses(
|
|
863
|
+
call: ClientReadableStream<unknown> | ClientDuplexStream<unknown, unknown>,
|
|
864
|
+
observation: ClientObservation,
|
|
865
|
+
removeAbort: () => void,
|
|
866
|
+
pumping?: RequestPump,
|
|
867
|
+
): AsyncIterable<unknown> {
|
|
868
|
+
let completed = false;
|
|
869
|
+
try {
|
|
870
|
+
for await (const response of call) {
|
|
871
|
+
observation.throwIfInvalid();
|
|
872
|
+
yield response;
|
|
873
|
+
}
|
|
874
|
+
if (pumping !== undefined) {
|
|
875
|
+
await pumping.done;
|
|
876
|
+
throwPumpFailure(pumping);
|
|
877
|
+
}
|
|
878
|
+
await observation.finished;
|
|
879
|
+
observation.throwIfInvalid();
|
|
880
|
+
completed = true;
|
|
881
|
+
} catch (error) {
|
|
882
|
+
observation.throwIfInvalid();
|
|
883
|
+
if (pumping !== undefined) throwPumpFailure(pumping);
|
|
884
|
+
throw error;
|
|
885
|
+
} finally {
|
|
886
|
+
removeAbort();
|
|
887
|
+
if (!completed) call.cancel();
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function throwPumpFailure(pumping: RequestPump): void {
|
|
892
|
+
const failure = pumping.failure();
|
|
893
|
+
if (failure.failed) throw failure.error;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
class ClientObservation {
|
|
897
|
+
error: unknown;
|
|
898
|
+
readonly finished: Promise<void>;
|
|
899
|
+
readonly #finish: () => void;
|
|
900
|
+
readonly #call: ClientSurfaceCall;
|
|
901
|
+
readonly #validate: GrpcMetadataValidator;
|
|
902
|
+
|
|
903
|
+
constructor(call: ClientSurfaceCall, validate: GrpcMetadataValidator, options?: GrpcClientCallOptions) {
|
|
904
|
+
this.#call = call;
|
|
905
|
+
this.#validate = validate;
|
|
906
|
+
let finish = (): void => undefined;
|
|
907
|
+
this.finished = new Promise<void>(resolve => {
|
|
908
|
+
finish = resolve;
|
|
909
|
+
});
|
|
910
|
+
this.#finish = finish;
|
|
911
|
+
call.on('metadata', metadata => {
|
|
912
|
+
this.#observe(metadata, options?.onMetadata);
|
|
913
|
+
});
|
|
914
|
+
call.on('status', result => {
|
|
915
|
+
this.#observe(result.metadata, options?.onTrailer);
|
|
916
|
+
this.#finish();
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
throwIfInvalid(): void {
|
|
921
|
+
if (this.error !== undefined) throw this.error;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
#observe(metadata: Metadata, callback?: (metadata: GrpcMetadata) => void): void {
|
|
925
|
+
if (this.error !== undefined) return;
|
|
926
|
+
try {
|
|
927
|
+
const value = validateMetadata(metadata, this.#validate);
|
|
928
|
+
callback?.(value);
|
|
929
|
+
} catch (error) {
|
|
930
|
+
this.error = error;
|
|
931
|
+
this.#call.cancel();
|
|
932
|
+
this.#finish();
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function attachAbort(call: ClientUnaryCall | ClientSurfaceCall, signal?: AbortSignal): () => void {
|
|
938
|
+
if (signal === undefined) return () => undefined;
|
|
939
|
+
const onAbort = (): void => {
|
|
940
|
+
call.cancel();
|
|
941
|
+
};
|
|
942
|
+
if (signal.aborted) onAbort();
|
|
943
|
+
else signal.addEventListener('abort', onAbort, { once: true });
|
|
944
|
+
return () => {
|
|
945
|
+
signal.removeEventListener('abort', onAbort);
|
|
946
|
+
};
|
|
947
|
+
}
|