@mark1russell7/service 0.5.12 → 0.5.13

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/build/grpc.js ADDED
@@ -0,0 +1,859 @@
1
+ import { realClock } from "./clock.js";
2
+ import { i as heartbeatDetection } from "./timing-DGRi3XCI.js";
3
+ import { B as zeroProblems, C as notSent, D as readyWait, F as timeProblems, G as immediate, H as NEVER, L as wholeProblems, M as sendWithReissue, N as socketEnds, P as stepChecks, R as withinBound, S as ms, U as WHAT, V as MANIFESTATIONS, W as firstDetector, X as unwatched, Y as kernelGiveUp, _ as inFlight, a as helloDial, b as message, c as ageProblems, d as clientProblems, f as clientRecorder, g as idleCloseProblem, h as finiteMs, i as authority, j as roundTripCheck, k as retryProblems, l as attachClient, m as dialCheck, n as closeTimer, r as transition, s as STARTING_POLL, t as clientKnob, u as clientBounds, w as operationTable, y as localStateCheck, z as wrapperTimerProblems } from "./machine-BhTRu3u9.js";
4
+ //#region src/grpc/connection.ts
5
+ /** These states are in the sequence of the enum of grpc-js: the index of a state is its value. */
6
+ const GRPC_STATES = [
7
+ "IDLE",
8
+ "CONNECTING",
9
+ "READY",
10
+ "TRANSIENT_FAILURE",
11
+ "SHUTDOWN"
12
+ ];
13
+ /** These functions give the bytes of a message as they are. The caller encodes and decodes its messages. */
14
+ const identity = (bytes) => bytes;
15
+ /** This function gives the state of a value of the enum of grpc-js. */
16
+ const stateOf = (value) => GRPC_STATES[value] ?? "SHUTDOWN";
17
+ /**
18
+ * This function adapts a `Client` of grpc-js to a channel. The deadline of grpc-js is a time of the
19
+ * wall clock, thus the adapter adds the timeout to `Date.now()`.
20
+ */
21
+ const adaptGrpc = (grpc, client) => {
22
+ const channel = client.getChannel();
23
+ return {
24
+ state: (connect) => stateOf(channel.getConnectivityState(connect)),
25
+ watch(current, changed) {
26
+ try {
27
+ channel.watchConnectivityState(GRPC_STATES.indexOf(current), Number.POSITIVE_INFINITY, (error) => {
28
+ if (error === void 0) changed();
29
+ });
30
+ } catch {}
31
+ },
32
+ unary(method, request, metadata, timeout, done) {
33
+ const sent = new grpc.Metadata();
34
+ for (const [key, value] of Object.entries(metadata)) sent.add(key, value);
35
+ const deadline = Number.isFinite(timeout) ? Date.now() + timeout : Number.POSITIVE_INFINITY;
36
+ return client.makeUnaryRequest(method, identity, identity, request, sent, { deadline }, (error, value) => {
37
+ if (error === null) done({
38
+ code: 0,
39
+ details: ""
40
+ }, value ?? Buffer.alloc(0));
41
+ else done({
42
+ code: error.code ?? 2,
43
+ details: error.details ?? error.message ?? ""
44
+ }, void 0);
45
+ });
46
+ },
47
+ close: () => client.close()
48
+ };
49
+ };
50
+ /**
51
+ * This factory makes a `Client` of grpc-js. It loads grpc-js only when a client connects, thus a
52
+ * service that does not use gRPC does not need it. The specifier is a value, thus the package needs no types of grpc-js.
53
+ */
54
+ const nodeGrpc = async ({ target, options }) => {
55
+ const loaded = await import("@grpc/grpc-js");
56
+ const grpc = loaded.Client === void 0 ? loaded.default : loaded;
57
+ if (grpc?.Client === void 0) throw new Error("@grpc/grpc-js gives no Client class");
58
+ return adaptGrpc(grpc, new grpc.Client(target, grpc.credentials.createInsecure(), options));
59
+ };
60
+ //#endregion
61
+ //#region src/grpc/meets.ts
62
+ /**
63
+ * This function gives the manifestations that a gRPC client can meet, and the detectors of each,
64
+ * from its knobs. The gRPC cells measured them on grpc-js 1.14.6:
65
+ * - At its defaults, grpc-js has no detector: no keepalive, no deadline, and no TCP keepalive.
66
+ * - A reset gave each call ECONNRESET in 1.1 ms to 1.4 ms. The FIN of a stopped server ended an idle connection in 43 µs.
67
+ * - The keepalive ended a silent transport at keepalive_time + keepalive_timeout: 80.012 s at 60 s
68
+ * against etcd, and 320.005 s at 300 s against grpc-go (DP24).
69
+ * - Without `keepaliveWithoutCalls`, grpc-js sends a PING only with a call in flight.
70
+ * - At client_idle_timeout, the channel goes IDLE, and the client connects again. A server that does
71
+ * not answer then keeps the channel out of READY.
72
+ */
73
+ const grpcMeetings = (name, knobs, withoutCalls) => {
74
+ const interval = finiteMs(knobs.keepalive_time);
75
+ const detection = heartbeatDetection(knobs.keepalive_time, knobs.keepalive_timeout, 1);
76
+ const keepalive = (situation) => ({
77
+ by: "heartbeat_detection_time",
78
+ within: situation === "idle" && !withoutCalls ? NEVER : detection,
79
+ situation,
80
+ effect: situation === "idle" && !withoutCalls ? "grpc-js sends no PING with no call in flight" : `the PING gets no ACK in keepalive_timeout, the transport ends, each call in flight fails with UNAVAILABLE, and ${name} connects again`
81
+ });
82
+ const deadline = {
83
+ by: "request_timeout",
84
+ within: knobs.request_timeout,
85
+ situation: "in flight",
86
+ effect: "the deadline ends the call with DEADLINE_EXCEEDED as indeterminate, and the transport stays"
87
+ };
88
+ const reconnect = {
89
+ by: "client_idle_timeout",
90
+ within: knobs.client_idle_timeout,
91
+ situation: "idle",
92
+ effect: `the channel goes IDLE, ${name} connects again, and the new connection does not become ready`
93
+ };
94
+ const giveUp = kernelGiveUp(knobs.retrans_giveup_time, "each call in flight fails with UNAVAILABLE");
95
+ const silent = (manifestation) => ({
96
+ manifestation,
97
+ what: WHAT[manifestation],
98
+ detectors: [
99
+ deadline,
100
+ keepalive("in flight"),
101
+ giveUp,
102
+ keepalive("idle"),
103
+ reconnect
104
+ ]
105
+ });
106
+ return [
107
+ {
108
+ manifestation: "rst",
109
+ what: WHAT.rst,
110
+ detectors: [
111
+ immediate("in flight", "the reset arrives, each call fails with UNAVAILABLE as indeterminate, and the channel goes IDLE"),
112
+ {
113
+ by: "keepalive_time",
114
+ within: interval === void 0 || !withoutCalls ? NEVER : knobs.keepalive_time,
115
+ situation: "idle",
116
+ effect: "the next PING gets a reset, and the client connects again"
117
+ },
118
+ {
119
+ ...reconnect,
120
+ effect: `the channel goes IDLE, ${name} connects again, and the server refuses the connect`
121
+ }
122
+ ]
123
+ },
124
+ {
125
+ manifestation: "fin",
126
+ what: WHAT.fin,
127
+ detectors: [immediate("in flight", "the FIN arrives, and each call fails with UNAVAILABLE as indeterminate"), immediate("idle", "the FIN arrives, the channel goes IDLE, and the client connects again")]
128
+ },
129
+ silent("blackhole"),
130
+ silent("freeze-host"),
131
+ {
132
+ manifestation: "freeze-process",
133
+ what: WHAT["freeze-process"],
134
+ detectors: [
135
+ deadline,
136
+ keepalive("in flight"),
137
+ keepalive("idle"),
138
+ reconnect
139
+ ]
140
+ },
141
+ {
142
+ manifestation: "app-stall",
143
+ what: "the server holds its reply, for example in a slow handler, and its library still answers each PING",
144
+ detectors: [
145
+ deadline,
146
+ {
147
+ ...keepalive("idle"),
148
+ within: NEVER,
149
+ effect: "the library of the server still answers each PING with an ACK"
150
+ },
151
+ {
152
+ ...reconnect,
153
+ within: NEVER,
154
+ effect: "the library of the server still takes a new connection"
155
+ }
156
+ ]
157
+ }
158
+ ];
159
+ };
160
+ //#endregion
161
+ //#region src/grpc/client.ts
162
+ /** The names of the status codes of gRPC, by their value. */
163
+ const GRPC_CODES = [
164
+ "OK",
165
+ "CANCELLED",
166
+ "UNKNOWN",
167
+ "INVALID_ARGUMENT",
168
+ "DEADLINE_EXCEEDED",
169
+ "NOT_FOUND",
170
+ "ALREADY_EXISTS",
171
+ "PERMISSION_DENIED",
172
+ "RESOURCE_EXHAUSTED",
173
+ "FAILED_PRECONDITION",
174
+ "ABORTED",
175
+ "OUT_OF_RANGE",
176
+ "UNIMPLEMENTED",
177
+ "INTERNAL",
178
+ "UNAVAILABLE",
179
+ "DATA_LOSS",
180
+ "UNAUTHENTICATED"
181
+ ];
182
+ /**
183
+ * These codes are certain errors: each one tells that the server or a proxy refused the call, and did
184
+ * not apply it. A proxy that answers 401, 403 or 404 gives UNAUTHENTICATED, PERMISSION_DENIED or UNIMPLEMENTED.
185
+ */
186
+ const CERTAIN = /* @__PURE__ */ new Set([
187
+ 3,
188
+ 5,
189
+ 6,
190
+ 7,
191
+ 9,
192
+ 10,
193
+ 11,
194
+ 12,
195
+ 16
196
+ ]);
197
+ /**
198
+ * These statuses of grpc-js tell that the call did not leave the client. A status with another
199
+ * details text of the same code can come after the send, thus it is indeterminate:
200
+ * - UNAVAILABLE "No connection established": the pick of a channel in TRANSIENT_FAILURE (picker.ts:105).
201
+ * - UNAVAILABLE "Name resolution failed": the resolver found no address (resolver-dns.ts:119).
202
+ * - UNAVAILABLE "Stream refused by server": RST_STREAM REFUSED_STREAM. The server did not process the stream (RFC 9113 §8.7).
203
+ * - INTERNAL "Failed to start HTTP/2 stream": the stream did not start (load-balancing-call.ts:281).
204
+ * - DEADLINE_EXCEEDED with "Waiting for LB pick" or "waiting for name resolution": the deadline came before a stream.
205
+ */
206
+ const NOT_SENT = [
207
+ [14, /^No connection established/],
208
+ [14, /^Name resolution failed/],
209
+ [14, /^Stream refused by server$/],
210
+ [13, /^Failed to start HTTP\/2 stream/],
211
+ [4, /(^|,)(Waiting for LB pick|waiting for name resolution)(,|$)/]
212
+ ];
213
+ /** This function gives the name of a status code, for example "UNAVAILABLE". */
214
+ const codeName = (code) => GRPC_CODES[code] ?? `code ${code}`;
215
+ /**
216
+ * This function gives the outcome of one call from its status:
217
+ * - OK is `ok`.
218
+ * - A code that tells that the server refused the call is a certain error: `reply` is true.
219
+ * - A status that tells that the call did not leave the client is a certain error with `reply` false.
220
+ * - Each other status is indeterminate: CANCELLED, UNKNOWN, DEADLINE_EXCEEDED, RESOURCE_EXHAUSTED,
221
+ * INTERNAL, UNAVAILABLE and DATA_LOSS can come after the server applied the call.
222
+ */
223
+ const grpcOutcome = (status, response) => {
224
+ if (status.code === 0) return {
225
+ outcome: "ok",
226
+ value: response ?? Buffer.alloc(0)
227
+ };
228
+ const error = `${codeName(status.code)}: ${status.details}`;
229
+ if (CERTAIN.has(status.code)) return {
230
+ outcome: "error",
231
+ error,
232
+ reply: true
233
+ };
234
+ if (NOT_SENT.some(([code, details]) => code === status.code && details.test(status.details))) return notSent(`${error} (the call did not leave the client)`);
235
+ return {
236
+ outcome: "indeterminate",
237
+ error
238
+ };
239
+ };
240
+ /** This method is the Check of the health service. Its request names a service, and its response gives a serving status. */
241
+ const HEALTH_CHECK = "/grpc.health.v1.Health/Check";
242
+ /** These operations are in each table: the Check of the health service has no side effects. */
243
+ const GRPC_OPERATIONS = { [HEALTH_CHECK]: { idempotent: true } };
244
+ /**
245
+ * This function makes a table of operations from the idempotency level of each method, by its full
246
+ * path. NO_SIDE_EFFECTS and IDEMPOTENT are idempotent. IDEMPOTENCY_UNKNOWN, the default of a proto, is not.
247
+ */
248
+ const grpcOperations = (levels) => Object.fromEntries(Object.entries(levels).map(([method, level]) => [method, { idempotent: level !== "IDEMPOTENCY_UNKNOWN" }]));
249
+ /** This function gives the bytes of a varint of protobuf. */
250
+ const varint = (value) => {
251
+ const bytes = [];
252
+ let rest = value;
253
+ while (rest > 127) {
254
+ bytes.push(rest & 127 | 128);
255
+ rest = Math.floor(rest / 128);
256
+ }
257
+ bytes.push(rest);
258
+ return Buffer.from(bytes);
259
+ };
260
+ /** This function gives the bytes of a HealthCheckRequest. The service "" is the whole server, and its message is empty. */
261
+ const healthRequest = (service) => {
262
+ if (service === "") return Buffer.alloc(0);
263
+ const name = Buffer.from(service, "utf8");
264
+ return Buffer.concat([
265
+ Buffer.from([10]),
266
+ varint(name.length),
267
+ name
268
+ ]);
269
+ };
270
+ const SERVING$1 = [
271
+ "UNKNOWN",
272
+ "SERVING",
273
+ "NOT_SERVING",
274
+ "SERVICE_UNKNOWN"
275
+ ];
276
+ /** This function gives the status of a HealthCheckResponse, or null when the bytes are not one. An empty message is UNKNOWN. */
277
+ const healthStatus = (bytes) => {
278
+ let at = 0;
279
+ let status = 0;
280
+ const read = () => {
281
+ let value = 0;
282
+ for (let shift = 0; shift < 35; shift += 7) {
283
+ const byte = bytes[at];
284
+ if (byte === void 0) return null;
285
+ at += 1;
286
+ value += (byte & 127) * 2 ** shift;
287
+ if ((byte & 128) === 0) return value;
288
+ }
289
+ return null;
290
+ };
291
+ while (at < bytes.length) {
292
+ const tag = read();
293
+ if (tag === null) return null;
294
+ const wire = tag & 7;
295
+ if (wire === 0) {
296
+ const value = read();
297
+ if (value === null) return null;
298
+ if (tag >> 3 === 1) status = value;
299
+ } else if (wire === 2) {
300
+ const length = read();
301
+ if (length === null || at + length > bytes.length) return null;
302
+ at += length;
303
+ } else if (wire === 1 || wire === 5) {
304
+ at += wire === 1 ? 8 : 4;
305
+ if (at > bytes.length) return null;
306
+ } else return null;
307
+ }
308
+ return SERVING$1[status] ?? null;
309
+ };
310
+ /** The longest timer of Node, in milliseconds. A longer delay starts at 1 ms, thus grpc-js cannot take it. */
311
+ const LONGEST_TIMER = 2147483647;
312
+ /** This function gives the problems of the times that grpc-js gives to `setTimeout` with no check: each one is at most the longest timer. */
313
+ const longestTimerProblems = (name, knobs, names) => names.flatMap((knob) => {
314
+ const amount = knobs[knob];
315
+ return amount.value !== "infinite" && ms(amount) > LONGEST_TIMER ? [`${name}'s ${knob} is above ${LONGEST_TIMER} ms, the longest timer of Node`] : [];
316
+ });
317
+ /** This function gives the problems of a spec: the knobs that the client cannot use. */
318
+ const specProblems = (name, spec) => {
319
+ const { knobs } = spec;
320
+ const timers = [
321
+ "keepalive_time",
322
+ "keepalive_timeout",
323
+ "client_idle_timeout",
324
+ "retry_base",
325
+ "retry_cap"
326
+ ];
327
+ const problems = [
328
+ ...timeProblems(name, knobs),
329
+ ...wrapperTimerProblems(name, knobs, {
330
+ connect_bound: "the wait of the startup for a ready channel",
331
+ request_timeout: "a call"
332
+ }),
333
+ ...zeroProblems(name, "grpc-js", knobs, { open: ["keepalive_time"] }),
334
+ ...wholeProblems(name, knobs, { millis: timers }),
335
+ ...longestTimerProblems(name, knobs, timers),
336
+ ...retryProblems(name, knobs),
337
+ ...ageProblems(name, knobs, "grpc-js does not replace its connection at an age")
338
+ ];
339
+ for (const knob of [
340
+ "keepalive_timeout",
341
+ "client_idle_timeout",
342
+ "retry_base",
343
+ "retry_cap"
344
+ ]) if (finiteMs(knobs[knob]) === void 0 || !(ms(knobs[knob]) > 0)) problems.push(`${name}'s ${knob} is not a finite time above zero`);
345
+ if (ms(knobs.client_idle_timeout) < 1e3) problems.push(`${name}'s client_idle_timeout is below 1 s: grpc-js raises it to 1 s`);
346
+ if (knobs.keepalive_idle.value !== "infinite") problems.push(`${name}'s keepalive_idle is not "infinite": grpc-js sets no TCP keepalive, and the PING of keepalive_time is its keepalive`);
347
+ if (spec.password !== void 0) problems.push(`${name} takes no password: give the credentials in metadata, for example authorization`);
348
+ return problems;
349
+ };
350
+ /** This function gives the target of a channel from a spec: the host and the port, for the DNS resolver of grpc-js. */
351
+ const grpcTarget = (spec) => authority(spec.host, spec.port);
352
+ /**
353
+ * This function gives the arguments of `new Client` from a spec. Each timer of grpc-js comes from the
354
+ * declaration. The retries of grpc-js are off, and the resolver gives no service config.
355
+ */
356
+ const grpcOptions = (spec) => {
357
+ const { knobs } = spec;
358
+ const keepalive = finiteMs(knobs.keepalive_time);
359
+ return {
360
+ target: grpcTarget(spec),
361
+ options: {
362
+ "grpc.enable_retries": 0,
363
+ "grpc.service_config_disable_resolution": 1,
364
+ ...keepalive === void 0 ? {} : { "grpc.keepalive_time_ms": keepalive },
365
+ "grpc.keepalive_timeout_ms": ms(knobs.keepalive_timeout),
366
+ "grpc.keepalive_permit_without_calls": spec.keepaliveWithoutCalls ? 1 : 0,
367
+ "grpc.client_idle_timeout_ms": ms(knobs.client_idle_timeout),
368
+ "grpc.initial_reconnect_backoff_ms": ms(knobs.retry_base),
369
+ "grpc.max_reconnect_backoff_ms": ms(knobs.retry_cap)
370
+ }
371
+ };
372
+ };
373
+ /** These are the bytes of the dial: the connection preface of the client (RFC 9113 §3.4), and a SETTINGS frame with no setting. */
374
+ const HTTP2_PREFACE = Buffer.concat([Buffer.from("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", "latin1"), Buffer.from([
375
+ 0,
376
+ 0,
377
+ 0,
378
+ 4,
379
+ 0,
380
+ 0,
381
+ 0,
382
+ 0,
383
+ 0
384
+ ])]);
385
+ /**
386
+ * This judge reads the first frame of the server. Only a SETTINGS frame on stream 0, with no ACK,
387
+ * passes: RFC 9113 §3.4 makes it the first frame of an HTTP/2 server. An HTTP/1.1 server answers with a status line.
388
+ */
389
+ const settingsJudge = (received) => {
390
+ if (received.length < 9) return "more";
391
+ const type = received[3];
392
+ const flags = received[4];
393
+ const stream = received.readUInt32BE(5) & 2147483647;
394
+ return type === 4 && (flags & 1) === 0 && stream === 0 ? null : /* @__PURE__ */ new Error(`the server did not answer the HTTP/2 preface with a SETTINGS frame: its first bytes are ${received.subarray(0, 9).toString("hex")}`);
395
+ };
396
+ /** This dial opens a new TCP connection, sends the HTTP/2 preface, and waits for the SETTINGS frame of the server. The dial then destroys its socket. */
397
+ const grpcDial = helloDial(HTTP2_PREFACE, settingsJudge);
398
+ /**
399
+ * This function makes a gRPC client, a dependency of a service over grpc-js 1.14.
400
+ * - It has one channel for its life. Each time that the channel becomes READY is one generation.
401
+ * - The grpc-js driver connects again by itself, after its own backoff. The client connects an IDLE channel again at once.
402
+ * - A call is one unary call, until its status. Its deadline is the time that is left in `request_timeout`.
403
+ * - It tells the outcome of a call in three values from its status code (C8).
404
+ * - The retries of grpc-js are off, thus only the core sends a call again.
405
+ * - Its keepalive is the HTTP/2 PING of grpc-js. It records the target of each generation (D1).
406
+ *
407
+ * These parts come from the core of the clients: the outcomes, the second try and the wait for a ready client.
408
+ * The calls in flight, the bounded close and the checks come from the core too. This entry gives what grpc-js does in its own way.
409
+ */
410
+ const grpcClient = (name, spec, options = {}) => {
411
+ const problems = specProblems(name, spec);
412
+ if (problems.length > 0) throw new Error(problems.join("; "));
413
+ const { knobs } = spec;
414
+ const clock = options.clock ?? realClock;
415
+ const create = options.create ?? nodeGrpc;
416
+ const dial = options.dial ?? grpcDial;
417
+ const operations = operationTable(GRPC_OPERATIONS, spec.operations);
418
+ const requestTimeout = ms(knobs.request_timeout);
419
+ const target = grpcTarget(spec);
420
+ let status = "none";
421
+ let channel = null;
422
+ let settled = null;
423
+ const joins = [];
424
+ const flight = inFlight();
425
+ const readied = readyWait(clock, () => status === "ready");
426
+ const emit = clientRecorder(options.record, clock, name);
427
+ const record = (on, to = status, detail) => {
428
+ const from = status;
429
+ status = to;
430
+ emit(from, to, on, detail);
431
+ };
432
+ const closing = () => status === "closing" || status === "end";
433
+ /** This function follows one state of the channel. The grpc-js driver ends each call in flight by itself when its transport ends. */
434
+ const follow = (c, state) => {
435
+ if (c !== channel || closing()) return;
436
+ if (state === "READY") {
437
+ if (status === "ready") return;
438
+ const generation = joins.length + 1;
439
+ joins.push({
440
+ generation,
441
+ at: clock.now(),
442
+ target,
443
+ ...socketEnds(void 0)
444
+ });
445
+ record("join", "ready", `generation ${generation}, ${target}`);
446
+ readied.wake();
447
+ settled?.();
448
+ } else if (state === "TRANSIENT_FAILURE") {
449
+ if (status === "failing") return;
450
+ record("connect.failed", "failing", "the channel is in TRANSIENT_FAILURE: grpc-js connects again after its backoff");
451
+ settled?.();
452
+ } else if (state === "CONNECTING") {
453
+ if (status !== "connecting") record("channel.connecting", "connecting", target);
454
+ } else if (state === "IDLE") {
455
+ if (status === "ready") record("connection.end", "idle", "the transport ended: a GOAWAY, a reset, a FIN, the keepalive, or client_idle_timeout");
456
+ else if (status !== "idle") record("channel.idle", "idle", "the channel is IDLE");
457
+ c.state(true);
458
+ }
459
+ };
460
+ /** This function watches the state of the channel. It watches again before it follows, thus no change goes away. */
461
+ const watch = (c, current) => c.watch(current, () => {
462
+ if (c !== channel || closing()) return;
463
+ const next = c.state(false);
464
+ if (next !== "SHUTDOWN") watch(c, next);
465
+ follow(c, next);
466
+ });
467
+ /** This function gives the channel for a call, or the reason that the call does not leave the client. */
468
+ const usable = (signal) => {
469
+ if (status === "none") return notSent(`${name} has not connected`);
470
+ if (status !== "ready" || channel === null) return notSent(`${name} is ${status}, not ready: the call did not leave the client`);
471
+ if (signal?.aborted) return notSent("the caller aborted the call before it left the client");
472
+ return channel;
473
+ };
474
+ /** This function makes one try of a call: the unary call until its status, with the deadline that is left. */
475
+ const attempt = (method, request, metadata, timeout, signal) => {
476
+ const c = usable(signal);
477
+ if ("outcome" in c) return Promise.resolve(c);
478
+ if (!(timeout > 0)) return Promise.resolve(notSent("no time is left in request_timeout"));
479
+ let handle = null;
480
+ let ended = false;
481
+ return flight.track(signal, "the caller abandoned the call after it left the client", (settle) => {
482
+ try {
483
+ handle = c.unary(method, request, metadata, timeout, (given, response) => {
484
+ ended = true;
485
+ settle(grpcOutcome(given, response));
486
+ });
487
+ } catch (error) {
488
+ ended = true;
489
+ settle(notSent(message(error)));
490
+ }
491
+ }, () => {
492
+ if (!ended) handle?.cancel();
493
+ });
494
+ };
495
+ const call = (method, request, callOptions = {}) => {
496
+ const started = clock.now();
497
+ const metadata = {
498
+ ...spec.metadata,
499
+ ...callOptions.metadata
500
+ };
501
+ return sendWithReissue({
502
+ clock,
503
+ bound: requestTimeout,
504
+ operation: operations[method],
505
+ options: callOptions,
506
+ attempt: (signal) => attempt(method, request, metadata, requestTimeout - (clock.now() - started), signal),
507
+ beforeAgain: (left, signal) => readied.wait(left, signal),
508
+ onReissue: () => record("command.reissue", status, method)
509
+ });
510
+ };
511
+ const health = async (service = "", sendOptions = {}) => {
512
+ const sent = await call(HEALTH_CHECK, healthRequest(service), sendOptions);
513
+ if (sent.outcome !== "ok") return sent;
514
+ const serving = healthStatus(sent.value);
515
+ return serving === null ? {
516
+ outcome: "error",
517
+ error: "the response is not a HealthCheckResponse",
518
+ reply: true,
519
+ attempts: sent.attempts
520
+ } : {
521
+ outcome: "ok",
522
+ value: serving,
523
+ attempts: sent.attempts
524
+ };
525
+ };
526
+ const handle = {
527
+ async connect(signal) {
528
+ if (signal.aborted) throw new Error(`${name}: the service stopped before the connect`);
529
+ record("status.connecting", "connecting", target);
530
+ let c;
531
+ try {
532
+ c = await create(grpcOptions(spec));
533
+ } catch (error) {
534
+ record("connect.failed", "failing", message(error));
535
+ throw error;
536
+ }
537
+ if (closing()) {
538
+ c.close();
539
+ return;
540
+ }
541
+ channel = c;
542
+ const first = new Promise((resolve) => settled = resolve);
543
+ const now = c.state(true);
544
+ watch(c, now);
545
+ if (now !== "IDLE") follow(c, now);
546
+ await withinBound(clock, ms(knobs.connect_bound), first);
547
+ settled = null;
548
+ },
549
+ async close() {
550
+ if (status === "none" || closing()) return;
551
+ record("close.request", "closing");
552
+ settled?.();
553
+ const c = channel;
554
+ if (c !== null) {
555
+ if (!await withinBound(clock, ms(knobs.close_bound), flight.idle())) {
556
+ record("close.bound", "closing");
557
+ record("close.destroy", "closing", `${flight.size} calls in flight`);
558
+ flight.settleAll("the client closed with the call in flight");
559
+ }
560
+ c.close();
561
+ }
562
+ channel = null;
563
+ flight.settleAll("the client closed with the call in flight");
564
+ record("status.end", "end");
565
+ }
566
+ };
567
+ const keepalive = finiteMs(knobs.keepalive_time);
568
+ const declaration = {
569
+ system: spec.system,
570
+ knobs: {
571
+ connect_bound: knobs.connect_bound,
572
+ request_timeout: knobs.request_timeout,
573
+ keepalive_time: knobs.keepalive_time,
574
+ keepalive_timeout: knobs.keepalive_timeout,
575
+ heartbeat_detection_time: heartbeatDetection(knobs.keepalive_time, knobs.keepalive_timeout, 1),
576
+ client_idle_timeout: knobs.client_idle_timeout,
577
+ retry_base: knobs.retry_base,
578
+ retry_cap: knobs.retry_cap,
579
+ keepalive_idle: knobs.keepalive_idle,
580
+ idle_refresh_interval: spec.keepaliveWithoutCalls && keepalive !== void 0 ? knobs.keepalive_time : NEVER,
581
+ max_connection_age: knobs.max_connection_age,
582
+ close_bound: knobs.close_bound,
583
+ retrans_giveup_time: knobs.retrans_giveup_time
584
+ },
585
+ operations
586
+ };
587
+ /** The round-trip check passes only for SERVING: a health service that answers NOT_SERVING fails it, as a 5xx fails the HTTP check. */
588
+ const roundTrip = async (sendOptions) => {
589
+ const sent = await health(spec.healthService ?? "", sendOptions);
590
+ return sent.outcome === "ok" && sent.value !== "SERVING" ? {
591
+ outcome: "error",
592
+ error: `the health service answered ${sent.value}`,
593
+ reply: true,
594
+ attempts: sent.attempts
595
+ } : sent;
596
+ };
597
+ const checks = (checkOptions) => stepChecks(name, checkOptions, {
598
+ "local-state": localStateCheck(() => status === "ready"),
599
+ "round-trip": roundTripCheck(roundTrip),
600
+ dial: dialCheck(dial, spec.host, spec.port)
601
+ });
602
+ return {
603
+ name,
604
+ declaration,
605
+ handle,
606
+ meets: grpcMeetings(name, knobs, spec.keepaliveWithoutCalls),
607
+ get status() {
608
+ return status;
609
+ },
610
+ joins,
611
+ call,
612
+ health,
613
+ checks,
614
+ attach: (svc, checkOptions) => attachClient(svc, name, declaration, handle, checks(checkOptions)),
615
+ problems(description) {
616
+ const found = clientProblems(name, description, clientBounds(knobs));
617
+ const idle = idleCloseProblem(name, knobs.client_idle_timeout, knobs.server_idle_close, "client_idle_timeout");
618
+ if (idle !== null) found.push(idle);
619
+ if (keepalive !== void 0 && keepalive < ms(knobs.server_keepalive_min_time)) found.push(`${name}'s keepalive_time is below server_keepalive_min_time: the server answers with GOAWAY too_many_pings (O26)`);
620
+ return found;
621
+ }
622
+ };
623
+ };
624
+ //#endregion
625
+ //#region src/grpc/machine.ts
626
+ /** The states where the client can close, and the states where a call can start its second try. */
627
+ const SERVING = [
628
+ "connecting",
629
+ "ready",
630
+ "idle",
631
+ "failing"
632
+ ];
633
+ const LIVE = [...SERVING, "closing"];
634
+ /**
635
+ * The gRPC client of the service as a `mark1russell7.machine/1` document. It is one channel of
636
+ * grpc-js 1.14 for the life of the client. Each READY of the channel is one generation. The states:
637
+ * - `ready`: the channel is READY, and the client sends each call. The keepalive sends a PING each keepalive_time.
638
+ * - `idle`: the transport ended, or the channel had no call in client_idle_timeout. The client connects again at once.
639
+ * - `failing`: the channel is in TRANSIENT_FAILURE. The grpc-js driver connects again after its backoff.
640
+ * - `closing`: the calls in flight, in `close_bound`. At the bound, each call gets RST_STREAM CANCEL.
641
+ *
642
+ * A test records the transitions of the client and replays them against this document.
643
+ */
644
+ const GRPC_CLIENT_MACHINE = {
645
+ format: "mark1russell7.machine/1",
646
+ id: "service.machine.grpc-client",
647
+ title: "The gRPC client of the service, over grpc-js 1.14",
648
+ layer: "driver",
649
+ version: "0.1.0",
650
+ sources: [{
651
+ ref: "packages/service/src/grpc (the client implements this document)",
652
+ rung: "implemented"
653
+ }],
654
+ states: {
655
+ none: {
656
+ initial: true,
657
+ note: "the client exists, and its channel does not"
658
+ },
659
+ connecting: { note: "the channel is CONNECTING: the TCP connect and the HTTP/2 preface. The grpc-js driver has no connect timeout" },
660
+ ready: { note: "the channel is READY. The deadline ends a call that waits, and the keepalive finds a silent transport" },
661
+ idle: { note: "the channel is IDLE. The client asks it to connect at once" },
662
+ failing: { note: "the channel is in TRANSIENT_FAILURE. The grpc-js driver connects again after its backoff, and each call fails as not sent" },
663
+ closing: { note: "the calls in flight, in close_bound. At the bound, each call gets RST_STREAM CANCEL, then the channel closes" },
664
+ end: {
665
+ final: true,
666
+ note: "the service closed the client"
667
+ }
668
+ },
669
+ events: {
670
+ "status.connecting": { kind: "local" },
671
+ join: {
672
+ kind: "rx",
673
+ note: "the channel became READY: a new generation (D1)"
674
+ },
675
+ "connect.failed": {
676
+ kind: "rx",
677
+ note: "the channel went to TRANSIENT_FAILURE: a connect failed"
678
+ },
679
+ "channel.connecting": {
680
+ kind: "rx",
681
+ note: "the channel started a new connect"
682
+ },
683
+ "channel.idle": {
684
+ kind: "rx",
685
+ note: "the channel went to IDLE before it was READY"
686
+ },
687
+ "connection.end": {
688
+ kind: "rx",
689
+ note: "the transport ended: a GOAWAY, a reset, a FIN, the keepalive, or client_idle_timeout. The grpc-js driver ends each call in flight with UNAVAILABLE"
690
+ },
691
+ "command.reissue": { kind: "local" },
692
+ "close.request": { kind: "local" },
693
+ "close.bound": {
694
+ kind: "timer",
695
+ note: "the calls in flight did not end in close_bound"
696
+ },
697
+ "close.destroy": {
698
+ kind: "local",
699
+ note: "the client cancels each call in flight, then closes the channel"
700
+ },
701
+ "status.end": { kind: "local" }
702
+ },
703
+ knobs: {
704
+ keepalive_time: clientKnob("grpc.keepalive_time_ms"),
705
+ keepalive_timeout: clientKnob("grpc.keepalive_timeout_ms"),
706
+ client_idle_timeout: clientKnob("grpc.client_idle_timeout_ms"),
707
+ retry_base: clientKnob("grpc.initial_reconnect_backoff_ms"),
708
+ retry_cap: clientKnob("grpc.max_reconnect_backoff_ms"),
709
+ close_bound: clientKnob("close.bound")
710
+ },
711
+ timers: {
712
+ keepalive: {
713
+ duration: {
714
+ expr: "keepalive_time + keepalive_timeout",
715
+ unit: "s"
716
+ },
717
+ arms: ["join"],
718
+ cancels: ["connection.end", "close.request"],
719
+ fires: "connection.end",
720
+ note: "a timer of grpc-js: one PING each keepalive_time, and the end of the transport at keepalive_timeout with no ACK"
721
+ },
722
+ idle: {
723
+ duration: {
724
+ knob: "client_idle_timeout",
725
+ unit: "s"
726
+ },
727
+ arms: ["join"],
728
+ cancels: ["connection.end", "close.request"],
729
+ fires: "connection.end",
730
+ note: "a timer of grpc-js: client_idle_timeout with no call puts the channel in IDLE"
731
+ },
732
+ retry: {
733
+ duration: {
734
+ expr: "min(retry_cap, retry_base * pow(1.6, fired)) * 1.2",
735
+ unit: "s"
736
+ },
737
+ arms: ["connect.failed"],
738
+ cancels: ["close.request"],
739
+ fires: "join",
740
+ backoff: { resets: ["join"] },
741
+ note: "a timer of grpc-js, the most delay: the backoff grows by 1.6 from retry_base to retry_cap, with a jitter of 20 %. The first delay has no jitter"
742
+ },
743
+ close: closeTimer()
744
+ },
745
+ transitions: [
746
+ transition("connect", "none", "status.connecting", "connecting"),
747
+ transition("join", [
748
+ "connecting",
749
+ "idle",
750
+ "failing"
751
+ ], "join", "ready"),
752
+ transition("connect-failed", [
753
+ "connecting",
754
+ "idle",
755
+ "ready"
756
+ ], "connect.failed", "failing"),
757
+ transition("retry", "failing", "join", "ready", { timer: "retry" }),
758
+ transition("channel-connecting", ["idle", "failing"], "channel.connecting", "connecting"),
759
+ transition("channel-idle", ["connecting", "failing"], "channel.idle", "idle"),
760
+ transition("connection-end", "ready", "connection.end", "idle"),
761
+ transition("keepalive", "ready", "connection.end", "idle", { timer: "keepalive" }),
762
+ transition("idle-timeout", "ready", "connection.end", "idle", { timer: "idle" }),
763
+ ...LIVE.map((state) => transition(`reissue-${state}`, state, "command.reissue", state)),
764
+ transition("close", [...SERVING], "close.request", "closing"),
765
+ transition("close-bound", "closing", "close.bound", "closing", { timer: "close" }),
766
+ transition("close-destroy", "closing", "close.destroy", "closing"),
767
+ transition("end", "closing", "status.end", "end")
768
+ ]
769
+ };
770
+ //#endregion
771
+ //#region src/grpc/starting.ts
772
+ /**
773
+ * This poll is the poll of the checks that `GRPC_CLIENT_STARTING_KNOBS` fits. It is `STARTING_POLL` of
774
+ * the core: each 5 s with a jitter of 20 %, and a timeout of 4 s (B6, O12). The poll does not depend on the driver.
775
+ */
776
+ const GRPC_CLIENT_STARTING_POLL = STARTING_POLL;
777
+ /**
778
+ * This value tells if the keepalive sends a PING with no call in flight, to start from: false. The
779
+ * grpc-go server refuses such a PING at its defaults (`PermitWithoutStream` false). It answers with GOAWAY too_many_pings (O26).
780
+ */
781
+ const GRPC_CLIENT_STARTING_WITHOUT_CALLS = false;
782
+ /**
783
+ * These knobs are a declaration to start from, from what the gRPC cells measured on grpc-js 1.14.6.
784
+ * The runtime has no defaults: a service gives these knobs, or its own. Each value holds the borders that the cells measured:
785
+ * - `connect_bound` 5 s: the connect timeout of DP44. The grpc-js driver has no connect timeout, thus this bounds the wait of the startup.
786
+ * - `request_timeout` 9.5 s: in the slowest poll of 10 s (O13). In DP44 a deadline of 4 s ended each
787
+ * call at 4.003 s to 4.004 s. The race of the two deadlines gave CANCELLED to 2 to 4 calls of 11.
788
+ * - `keepalive_time` 300 s and `keepalive_timeout` 20 s: DP24. The PING must not come before the
789
+ * `MinTime` of the server, 5 min at the defaults of grpc-go (O26). The transport ended at 320.005 s.
790
+ * Against etcd, with a `MinTime` of 5 s, 60 s ended it at 80.012 s. A PING each 4 s against 5 s got GOAWAY at the 4th PING.
791
+ * - `server_keepalive_min_time` 300 s: the `MinTime` of grpc-go v1.84.0 at its defaults.
792
+ * - `client_idle_timeout` 60 s and `server_idle_close` 75 s: the proxies of the cells sent GOAWAY to an idle connection.
793
+ * They did that at 75 s (nginx), 180 s (Traefik), 300 s (Caddy) and 1 h (Envoy). The client closes first (O01).
794
+ * The default of grpc-js is 30 min.
795
+ * - `retry_base` 1 s: the first backoff of grpc-js in T07, with no jitter. `retry_cap` 4 s: the most
796
+ * delay is 4.8 s with the jitter of 20 %, in the slowest poll of 10 s. The default of grpc-js is 120 s.
797
+ * - `keepalive_idle` "infinite": grpc-js sets no TCP keepalive, and the PING is its keepalive.
798
+ * - `max_connection_age` "infinite": the client does not replace its connection at an age.
799
+ * - `close_bound` 2 s: the calls in flight, then RST_STREAM CANCEL for each call that stays.
800
+ * - `retrans_giveup_time` 924.6 s: the give-up of Linux at `tcp_retries2 = 15`.
801
+ */
802
+ const GRPC_CLIENT_STARTING_KNOBS = {
803
+ connect_bound: {
804
+ value: 5,
805
+ unit: "s"
806
+ },
807
+ request_timeout: {
808
+ value: 9.5,
809
+ unit: "s"
810
+ },
811
+ keepalive_time: {
812
+ value: 300,
813
+ unit: "s"
814
+ },
815
+ keepalive_timeout: {
816
+ value: 20,
817
+ unit: "s"
818
+ },
819
+ server_keepalive_min_time: {
820
+ value: 300,
821
+ unit: "s"
822
+ },
823
+ client_idle_timeout: {
824
+ value: 60,
825
+ unit: "s"
826
+ },
827
+ server_idle_close: {
828
+ value: 75,
829
+ unit: "s"
830
+ },
831
+ retry_base: {
832
+ value: 1,
833
+ unit: "s"
834
+ },
835
+ retry_cap: {
836
+ value: 4,
837
+ unit: "s"
838
+ },
839
+ keepalive_idle: {
840
+ value: "infinite",
841
+ unit: "s"
842
+ },
843
+ max_connection_age: {
844
+ value: "infinite",
845
+ unit: "s"
846
+ },
847
+ close_bound: {
848
+ value: 2,
849
+ unit: "s"
850
+ },
851
+ retrans_giveup_time: {
852
+ value: 924.6,
853
+ unit: "s"
854
+ }
855
+ };
856
+ //#endregion
857
+ export { GRPC_CLIENT_MACHINE, GRPC_CLIENT_STARTING_KNOBS, GRPC_CLIENT_STARTING_POLL, GRPC_CLIENT_STARTING_WITHOUT_CALLS, GRPC_CODES, GRPC_OPERATIONS, GRPC_STATES, HEALTH_CHECK, HTTP2_PREFACE, MANIFESTATIONS, STARTING_POLL, adaptGrpc, codeName, firstDetector, grpcClient, grpcDial, grpcMeetings, grpcOperations, grpcOptions, grpcOutcome, grpcTarget, healthRequest, healthStatus, nodeGrpc, readyWait, retryProblems, settingsJudge, specProblems, unwatched };
858
+
859
+ //# sourceMappingURL=grpc.js.map