@mark1russell7/service 0.3.0 → 0.4.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.
@@ -20,20 +20,6 @@ const operationProblems = (owner, name, operation) => {
20
20
  return problems;
21
21
  };
22
22
  //#endregion
23
- //#region src/clock.ts
24
- /**
25
- * This clock reads `performance.now()` and uses `setTimeout`. Its timers do not keep a Node
26
- * process alive, thus a timer that is not stopped cannot stop the exit of the process.
27
- */
28
- const realClock = {
29
- now: () => performance.now(),
30
- after(ms, run) {
31
- const timer = setTimeout(run, Math.max(0, ms));
32
- timer.unref?.();
33
- return () => clearTimeout(timer);
34
- }
35
- };
36
- //#endregion
37
- export { mayReissue as n, operationProblems as r, realClock as t };
23
+ export { operationProblems as n, mayReissue as t };
38
24
 
39
- //# sourceMappingURL=clock-DlJFFZlX.js.map
25
+ //# sourceMappingURL=operation-DL5RxFds.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"operation-DL5RxFds.js","names":[],"sources":["../src/operation.ts"],"sourcesContent":["import type { Extent, Quantity } from \"@mark1russell7/quantity\";\nimport { compare, convertible } from \"@mark1russell7/quantity\";\n\n/**\n * This type tells if an operation can go again after a failure. It is a property of the\n * operation, not of the dependency. One Redis client sends a GET, which is safe to send again. It\n * also sends an INCR, which counts two times when it goes again after a timeout. Each operation\n * declares its idempotency:\n * - `true`: a second issue has the same effect. Examples are an HTTP GET or PUT, a SELECT, a\n * Redis GET or SET, and a write that the server removes when it is a copy.\n * - `false`: the client does not send it again after it can have reached the server.\n * - keyed: the server keeps a key for a window, as with an `Idempotency-Key` header, and it\n * ignores a copy in that window. A copy after the window is a second operation. Thus the\n * window is a timer, and it must be longer than all retries of the operation.\n */\nexport type Idempotency = boolean | { readonly key: string; readonly window: Extent };\n\n/** An operation is an endpoint that the service calls or serves, a command, or a query. */\nexport interface Operation {\n readonly idempotent: Idempotency;\n}\n\n/**\n * This function tells if an operation can go again `since` its first issue. An idempotent\n * operation can always go again, and other operations cannot. A keyed operation can go again\n * while the server keeps its key.\n */\nexport const mayReissue = (operation: Operation, since: Quantity): boolean => {\n const { idempotent } = operation;\n if (typeof idempotent === \"boolean\") return idempotent;\n return compare(since, idempotent.window) < 0;\n};\n\n/** This function gives the problems in the declaration of an operation that `describe()` refuses. */\nexport const operationProblems = (owner: string, name: string, operation: Operation): string[] => {\n const { idempotent } = operation;\n if (typeof idempotent === \"boolean\") return [];\n const problems: string[] = [];\n if (idempotent.key.trim().length === 0) problems.push(`${owner}'s ${name} names no key`);\n if (!convertible(idempotent.window.unit, \"s\"))\n problems.push(`${owner}'s ${name} holds its key for a span that is not a time`);\n return problems;\n};\n"],"mappings":";;;;;;;AA2BA,MAAa,cAAc,WAAsB,UAA6B;CAC5E,MAAM,EAAE,eAAe;CACvB,IAAI,OAAO,eAAe,WAAW,OAAO;CAC5C,OAAO,QAAQ,OAAO,WAAW,MAAM,IAAI;AAC7C;;AAGA,MAAa,qBAAqB,OAAe,MAAc,cAAmC;CAChG,MAAM,EAAE,eAAe;CACvB,IAAI,OAAO,eAAe,WAAW,OAAO,CAAC;CAC7C,MAAM,WAAqB,CAAC;CAC5B,IAAI,WAAW,IAAI,KAAK,CAAC,CAAC,WAAW,GAAG,SAAS,KAAK,GAAG,MAAM,KAAK,KAAK,cAAc;CACvF,IAAI,CAAC,YAAY,WAAW,OAAO,MAAM,GAAG,GAC1C,SAAS,KAAK,GAAG,MAAM,KAAK,KAAK,6CAA6C;CAChF,OAAO;AACT"}
package/build/redis.d.ts CHANGED
@@ -1,4 +1,6 @@
1
- import { B as Check, G as Gate, I as Recorder, J as Step, R as Clock, U as Dependency, Z as Operation, et as Poll, j as CheckFn, n as ServiceBuilder, q as ServiceDescription, s as DependencyHandle } from "./builder-BFxOuULH.js";
1
+ import { t as Backoff } from "./backoff-B-jGWVDm.js";
2
+ import { n as Clock } from "./clock-D-38uH_z.js";
3
+ import { B as Dependency, G as Step, H as Gate, I as Recorder, J as Operation, L as Check, W as ServiceDescription, Z as Poll, j as CheckFn, n as ServiceBuilder, rt as heartbeatDetection, s as DependencyHandle } from "./builder-DnfUDW9u.js";
2
4
  import { Extent, Quantity } from "@mark1russell7/quantity";
3
5
  import { Machine } from "@mark1russell7/algebra";
4
6
  //#region src/redis/connection.d.ts
@@ -250,11 +252,10 @@ interface RedisClient {
250
252
  problems(description: ServiceDescription): string[];
251
253
  }
252
254
  /**
253
- * This function gives the time that the watchdog takes to find a silent server, at the most. A PING
254
- * starts each interval, from the start of the one before. Each one fails at its timeout, and the
255
- * last of `misses` failures in sequence closes the connection: `misses` intervals plus a timeout.
255
+ * This function gives the backoff of the tries to connect: from `retry_base`, doubling to
256
+ * `retry_cap`, each with a jitter up to the base. The client rounds each delay to a whole millisecond.
256
257
  */
257
- export declare const heartbeatDetection: (interval: Extent, timeout: Quantity, misses: number) => Extent;
258
+ export declare const retryBackoff: (knobs: RedisKnobs) => Backoff;
258
259
  /** This function gives the problems of a spec: the knobs that the client cannot use. */
259
260
  export declare const specProblems: (name: string, spec: RedisClientSpec) => string[];
260
261
  /** This dial function opens a TCP connection, sends an inline PING, and waits for +PONG. */
@@ -290,5 +291,5 @@ export declare const REDIS_OPERATIONS: Readonly<Record<string, Operation>>;
290
291
  /** This function gives the name of a command as the table writes it: upper case, and CLIENT ID with its subcommand. */
291
292
  export declare const commandName: (command: string, args: readonly unknown[]) => string;
292
293
  //#endregion
293
- export type { CheckOptions, ClientStep, ConnectionFactory, Detector, DialFn, Join, Meeting, RedisClient, RedisClientOptions, RedisClientSpec, RedisConnection, RedisConnectionOptions, RedisKnobs, RedisSocket, Sent, Situation };
294
+ export { type CheckOptions, type ClientStep, type ConnectionFactory, type Detector, type DialFn, type Join, type Meeting, type RedisClient, type RedisClientOptions, type RedisClientSpec, type RedisConnection, type RedisConnectionOptions, type RedisKnobs, type RedisSocket, type Sent, type Situation, heartbeatDetection };
294
295
  //# sourceMappingURL=redis.d.ts.map
package/build/redis.js CHANGED
@@ -1,4 +1,8 @@
1
- import { n as mayReissue, t as realClock } from "./clock-DlJFFZlX.js";
1
+ import { realClock } from "./clock.js";
2
+ import { heartbeat } from "./heartbeat.js";
3
+ import { i as heartbeatDetection } from "./timing-DGRi3XCI.js";
4
+ import { t as mayReissue } from "./operation-DL5RxFds.js";
5
+ import { backoffDelay } from "./backoff.js";
2
6
  import { quantity, valueIn } from "@mark1russell7/quantity";
3
7
  import { connect } from "node:net";
4
8
  //#region src/redis/connection.ts
@@ -245,14 +249,18 @@ const finite = (amount) => {
245
249
  };
246
250
  const message = (error) => error instanceof Error ? error.message : String(error);
247
251
  /**
248
- * This function gives the time that the watchdog takes to find a silent server, at the most. A PING
249
- * starts each interval, from the start of the one before. Each one fails at its timeout, and the
250
- * last of `misses` failures in sequence closes the connection: `misses` intervals plus a timeout.
252
+ * This function gives the backoff of the tries to connect: from `retry_base`, doubling to
253
+ * `retry_cap`, each with a jitter up to the base. The client rounds each delay to a whole millisecond.
251
254
  */
252
- const heartbeatDetection = (interval, timeout, misses) => interval.value === "infinite" ? {
253
- value: "infinite",
254
- unit: "s"
255
- } : quantity((misses * ms(interval) + ms(timeout)) / 1e3, "s");
255
+ const retryBackoff = (knobs) => ({
256
+ base: ms(knobs.retry_base),
257
+ factor: 2,
258
+ cap: ms(knobs.retry_cap),
259
+ jitter: {
260
+ kind: "add",
261
+ amount: ms(knobs.retry_base)
262
+ }
263
+ });
256
264
  /** This function gives the problems of a spec: the knobs that the client cannot use. */
257
265
  const specProblems = (name, spec) => {
258
266
  const problems = [];
@@ -317,6 +325,7 @@ const redisClient = (name, spec, options = {}) => {
317
325
  };
318
326
  const requestTimeout = ms(knobs.request_timeout);
319
327
  const detection = heartbeatDetection(knobs.heartbeat_interval, knobs.heartbeat_timeout, spec.heartbeatMisses);
328
+ const backoff = retryBackoff(knobs);
320
329
  let connection = null;
321
330
  let status = "none";
322
331
  let closing = false;
@@ -341,8 +350,7 @@ const redisClient = (name, spec, options = {}) => {
341
350
  record(status, status, "retry.window-ended");
342
351
  return null;
343
352
  }
344
- const delay = Math.min(ms(knobs.retry_base) * 2 ** (times - 1), ms(knobs.retry_cap));
345
- return Math.round(delay + random() * ms(knobs.retry_base));
353
+ return Math.round(backoffDelay(backoff, times, random));
346
354
  };
347
355
  const connectionOptions = {
348
356
  host: spec.host,
@@ -525,40 +533,33 @@ const redisClient = (name, spec, options = {}) => {
525
533
  attempts: 2
526
534
  };
527
535
  };
528
- let misses = 0;
529
- let nextPing = null;
530
- const heartbeatInterval = ms(knobs.heartbeat_interval);
531
- const ping = () => {
532
- nextPing = clock.after(heartbeatInterval, ping);
533
- const c = connection;
534
- if (c === null || closing) return;
535
- if (c.status === "end") {
536
- record(status, status, "watchdog.revive");
537
- retrySince = null;
538
- c.connect().catch(() => {});
539
- return;
540
- }
541
- if (c.status !== "ready") return;
542
- const controller = new AbortController();
543
- const timer = clock.after(ms(knobs.heartbeat_timeout), () => controller.abort());
544
- attempt("PING", [], controller.signal).then(async (result) => {
545
- timer();
546
- const answered = result.outcome === "ok" || result.outcome === "error" && result.reply;
547
- await turn();
548
- if (answered) {
549
- misses = 0;
550
- return;
551
- }
552
- if (connection?.status !== "ready" || closing) return;
553
- misses += 1;
554
- record(status, status, "watchdog.miss", `${misses} of ${spec.heartbeatMisses}`);
555
- if (misses >= spec.heartbeatMisses) {
556
- misses = 0;
557
- record(status, status, "watchdog.reconnect");
558
- connection.disconnect(true);
536
+ let watchdog = null;
537
+ const startWatchdog = () => heartbeat({
538
+ interval: ms(knobs.heartbeat_interval),
539
+ timeout: ms(knobs.heartbeat_timeout),
540
+ misses: spec.heartbeatMisses,
541
+ clock,
542
+ turn,
543
+ overlap: true,
544
+ due: () => {
545
+ const c = connection;
546
+ if (c === null || closing) return false;
547
+ if (c.status === "end") {
548
+ record(status, status, "watchdog.revive");
549
+ retrySince = null;
550
+ c.connect().catch(() => {});
551
+ return false;
559
552
  }
560
- });
561
- };
553
+ return c.status === "ready";
554
+ },
555
+ ping: (signal) => attempt("PING", [], signal).then((result) => result.outcome === "ok" || result.outcome === "error" && result.reply),
556
+ live: () => connection?.status === "ready" && !closing,
557
+ onMiss: (misses) => record(status, status, "watchdog.miss", `${misses} of ${spec.heartbeatMisses}`),
558
+ onDead: () => {
559
+ record(status, status, "watchdog.reconnect");
560
+ connection?.disconnect(true);
561
+ }
562
+ });
562
563
  const handle = {
563
564
  async connect(signal) {
564
565
  closing = false;
@@ -567,12 +568,13 @@ const redisClient = (name, spec, options = {}) => {
567
568
  watch(c);
568
569
  if (signal.aborted) throw new Error(`${name}: the service stopped before the connect`);
569
570
  await c.connect().catch((error) => record(status, status, "connect.failed", message(error)));
570
- if (Number.isFinite(heartbeatInterval)) nextPing = clock.after(heartbeatInterval, ping);
571
+ watchdog?.stop();
572
+ watchdog = startWatchdog();
571
573
  },
572
574
  async close() {
573
575
  closing = true;
574
- nextPing?.();
575
- nextPing = null;
576
+ watchdog?.stop();
577
+ watchdog = null;
576
578
  const c = connection;
577
579
  if (c === null) return;
578
580
  const quit = c.quit().then(() => "quit", () => "quit");
@@ -848,6 +850,6 @@ const REDIS_CLIENT_MACHINE = {
848
850
  ]
849
851
  };
850
852
  //#endregion
851
- export { REDIS_CLIENT_MACHINE, REDIS_OPERATIONS, commandName, firstDetector, heartbeatDetection, ioredis, meetings, redisClient, specProblems, tcpDial, unwatched };
853
+ export { REDIS_CLIENT_MACHINE, REDIS_OPERATIONS, commandName, firstDetector, heartbeatDetection, ioredis, meetings, redisClient, retryBackoff, specProblems, tcpDial, unwatched };
852
854
 
853
855
  //# sourceMappingURL=redis.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"redis.js","names":["dialTcp"],"sources":["../src/redis/connection.ts","../src/redis/meets.ts","../src/redis/operations.ts","../src/redis/client.ts","../src/redis/machine.ts"],"sourcesContent":["/**\n * A Redis connection is the part of an ioredis client that the wrapper uses. An ioredis 6 client has\n * each part. A fake for tests has them too. Thus the package depends on no type of ioredis.\n */\nexport interface RedisConnection {\n /** This text is the state of the client: wait, connecting, connect, ready, close, reconnecting or end. */\n readonly status: string;\n /** This socket is the socket of the current connection, when there is one. */\n readonly stream?: RedisSocket | undefined;\n connect(): Promise<void>;\n /** This method sends one command, and gives the reply. */\n call(command: string, ...args: Array<string | number>): Promise<unknown>;\n quit(): Promise<unknown>;\n /** This method closes the connection at once. With `true`, the client then connects again. */\n disconnect(reconnect?: boolean): void;\n on(event: string, listener: (...args: unknown[]) => void): unknown;\n off(event: string, listener: (...args: unknown[]) => void): unknown;\n}\n\n/** A Redis socket is the part of the socket of a connection that the join of the prober reads. */\nexport interface RedisSocket {\n readonly localAddress?: string | undefined;\n readonly localPort?: number | undefined;\n readonly remoteAddress?: string | undefined;\n readonly remotePort?: number | undefined;\n /** This method closes the socket at once. A disconnect only half-closes it, and a silent peer does not answer that. */\n destroy?(): void;\n}\n\n/**\n * These are the options that the wrapper gives ioredis. Each one comes from the declaration. The\n * wrapper sets each option that has an effect on a timer or on a send again, thus no default of\n * ioredis decides one.\n */\nexport interface RedisConnectionOptions {\n readonly host: string;\n readonly port: number;\n readonly db?: number | undefined;\n readonly username?: string | undefined;\n readonly password?: string | undefined;\n /** The wrapper connects in its own step of the startup. */\n readonly lazyConnect: true;\n readonly connectTimeout: number;\n readonly commandTimeout: number;\n /** This time destroys a socket that gives no data while a command waits. Undefined is no bound. */\n readonly socketTimeout: number | undefined;\n /** This time is the idle time of TCP keepalive. False is no keepalive. */\n readonly keepAlive: number | false;\n /** A command while the client is not ready fails at once: it did not reach the server. */\n readonly enableOfflineQueue: false;\n /** The client does not send again a command that can have reached the server. */\n readonly autoResendUnfulfilledCommands: false;\n readonly maxRetriesPerRequest: null;\n readonly enableReadyCheck: true;\n readonly connectionName: string;\n /** This function gives the delay before the next try to connect, or null to stop. */\n readonly retryStrategy: (times: number) => number | null;\n}\n\n/** A connection factory makes the connection of the client from its options. */\nexport type ConnectionFactory = (options: RedisConnectionOptions) => RedisConnection | Promise<RedisConnection>;\n\n/**\n * This factory makes an ioredis client. It loads ioredis only when a client connects, thus a\n * service that does not use Redis does not need ioredis.\n */\nexport const ioredis: ConnectionFactory = async (options) => {\n const loaded = (await import(\"ioredis\")) as unknown as {\n readonly default: new (options: RedisConnectionOptions) => RedisConnection;\n };\n return new loaded.default(options);\n};\n","import type { Extent, Quantity } from \"@mark1russell7/quantity\";\nimport { quantity, valueIn } from \"@mark1russell7/quantity\";\nimport type { RedisKnobs } from \"./client.ts\";\n\n/** A situation is what the connection did when the failure started: a command in flight, or idle. */\nexport type Situation = \"in flight\" | \"idle\";\n\n/** A detector is a mechanism that finds a failure, and the most time that it takes. */\nexport interface Detector {\n /** This name identifies the mechanism: a knob of the declaration, or the kernel. */\n readonly by: string;\n /** This time is the most that the detector takes, after the failure starts. \"infinite\" tells that it does not find the failure. */\n readonly within: Extent;\n readonly situation: Situation;\n /** This text tells what the detector does, for example that it fails a command without a change of state. */\n readonly effect: string;\n}\n\n/**\n * A meeting is a manifestation that the client can meet, and its detectors (E9). The names of the\n * manifestations are the names of the vocabulary of the harness and the corpus. `app-stall` is a\n * server whose process is alive and does not answer, a proposal of the vocabulary.\n */\nexport interface Meeting {\n readonly manifestation: string;\n /** This text tells the failure in words. */\n readonly what: string;\n readonly detectors: readonly Detector[];\n}\n\nconst never: Extent = { value: \"infinite\", unit: \"s\" };\nconst s = (value: number): Quantity => quantity(Math.round(value * 1e6) / 1e6, \"s\");\nconst seconds = (amount: Extent): number =>\n amount.value === \"infinite\" ? Number.POSITIVE_INFINITY : valueIn(amount as Quantity, \"s\");\nconst plus = (amount: Extent, more: number): Extent =>\n amount.value === \"infinite\" ? never : s(seconds(amount) + more);\n\n/** Node gives keepalive 10 probes, 1 s apart, after its idle time: a silent peer is found 10 s after the idle time. */\nconst NODE_PROBES = 10;\nconst NODE_INTERVAL = 1;\n\n/**\n * This function gives the manifestations that a Redis client can meet, and the detectors of each, from\n * its knobs. A reset or a FIN arrives on the next segment. A black hole and a host that is gone give\n * no answer at all. A frozen process and a stalled server keep a kernel that acknowledges each\n * segment. Thus TCP keepalive does not fire on them, and only the client's own timers find them.\n */\nexport const meetings = (name: string, knobs: RedisKnobs, heartbeat: Extent): Meeting[] => {\n const keepalive = plus(knobs.keepalive_idle, NODE_PROBES * NODE_INTERVAL);\n const watchdog = (situation: Situation): Detector => ({\n by: \"heartbeat_detection_time\",\n within: heartbeat,\n situation,\n effect: `the watchdog of ${name} closes the connection, and the client connects again`,\n });\n const writeoff: Detector = {\n by: \"driver_writeoff_time\",\n within: knobs.driver_writeoff_time,\n situation: \"in flight\",\n effect: \"ioredis destroys the socket of a command without data, and the client connects again\",\n };\n const command: Detector = {\n by: \"request_timeout\",\n within: knobs.request_timeout,\n situation: \"in flight\",\n effect: \"the command fails as indeterminate, and the state of the connection does not change\",\n };\n const giveUp: Detector = {\n by: \"retrans_giveup_time\",\n within: knobs.retrans_giveup_time,\n situation: \"in flight\",\n effect: \"the kernel gives up the retransmissions and closes the socket\",\n };\n const silent = (manifestation: string, what: string): Meeting => ({\n manifestation,\n what,\n detectors: [\n command,\n writeoff,\n watchdog(\"in flight\"),\n giveUp,\n watchdog(\"idle\"),\n { by: \"keepalive_idle\", within: keepalive, situation: \"idle\", effect: \"TCP keepalive gives up, and the socket closes\" },\n ],\n });\n const acknowledged = (manifestation: string, what: string): Meeting => ({\n manifestation,\n what,\n detectors: [\n command,\n writeoff,\n watchdog(\"in flight\"),\n watchdog(\"idle\"),\n { by: \"keepalive_idle\", within: never, situation: \"idle\", effect: \"the kernel of the server answers each probe\" },\n ],\n });\n const immediate = (situation: Situation, effect: string): Detector => ({ by: \"the next segment\", within: s(0), situation, effect });\n return [\n {\n manifestation: \"rst\",\n what: \"the server answers each segment with a reset, and refuses each new connection\",\n detectors: [\n immediate(\"in flight\", \"the command fails, and the client connects again, and each attempt is refused\"),\n watchdog(\"idle\"),\n { by: \"keepalive_idle\", within: keepalive.value === \"infinite\" ? never : knobs.keepalive_idle, situation: \"idle\", effect: \"the first probe gets a reset\" },\n ],\n },\n {\n manifestation: \"fin\",\n what: \"the process of the server ends, and its kernel closes the connection\",\n detectors: [immediate(\"in flight\", \"the close arrives, and the command fails\"), immediate(\"idle\", \"the close arrives\")],\n },\n silent(\"blackhole\", \"the network drops each segment in the two directions\"),\n silent(\"freeze-host\", \"the host of the server stops, without a close: the death without a close\"),\n acknowledged(\"freeze-process\", \"the process of the server stops, and its kernel stays up\"),\n acknowledged(\"app-stall\", \"the server stops its replies for a time, for example a long command, and its kernel stays up\"),\n ];\n};\n\n/** This function gives the first detector of a manifestation in a situation, or null when no detector has an end (C3). */\nexport const firstDetector = (meeting: Meeting, situation: Situation): Detector | null => {\n const ranked = meeting.detectors\n .filter((detector) => detector.situation === situation && detector.within.value !== \"infinite\")\n .sort((a, b) => seconds(a.within) - seconds(b.within));\n return ranked[0] ?? null;\n};\n\n/** This function gives each manifestation and situation that no detector of the client finds: the standing rule, as a query (C3, E9). */\nexport const unwatched = (meets: readonly Meeting[]): Array<{ readonly manifestation: string; readonly situation: Situation }> =>\n meets.flatMap((meeting) =>\n ([\"in flight\", \"idle\"] as const).flatMap((situation) =>\n firstDetector(meeting, situation) === null ? [{ manifestation: meeting.manifestation, situation }] : [],\n ),\n );\n","import type { Operation } from \"../operation.ts\";\n\n/**\n * These are the Redis commands whose idempotency the client declares when the declaration does not\n * say. A second issue of an idempotent command has the same effect. Thus the client can send it\n * again after a failure that left its result unknown. The client does not send again a command\n * that is not in this table: a declaration that uses it says its idempotency itself.\n */\nconst SAME_EFFECT = [\n // reads\n \"PING\",\n \"ECHO\",\n \"GET\",\n \"MGET\",\n \"STRLEN\",\n \"GETRANGE\",\n \"EXISTS\",\n \"TYPE\",\n \"TTL\",\n \"PTTL\",\n \"EXPIRETIME\",\n \"HGET\",\n \"HMGET\",\n \"HGETALL\",\n \"HKEYS\",\n \"HVALS\",\n \"HLEN\",\n \"HEXISTS\",\n \"SMEMBERS\",\n \"SISMEMBER\",\n \"SMISMEMBER\",\n \"SCARD\",\n \"ZRANGE\",\n \"ZRANGEBYSCORE\",\n \"ZSCORE\",\n \"ZMSCORE\",\n \"ZRANK\",\n \"ZCARD\",\n \"ZCOUNT\",\n \"LRANGE\",\n \"LINDEX\",\n \"LLEN\",\n \"XRANGE\",\n \"XREVRANGE\",\n \"XLEN\",\n \"SCAN\",\n \"SSCAN\",\n \"HSCAN\",\n \"ZSCAN\",\n \"DBSIZE\",\n \"INFO\",\n \"CLIENT ID\",\n // writes that put a state, not a change of a state: a second issue puts the same state\n \"SET\",\n \"MSET\",\n \"SETEX\",\n \"PSETEX\",\n \"DEL\",\n \"UNLINK\",\n \"EXPIRE\",\n \"PEXPIRE\",\n \"EXPIREAT\",\n \"PEXPIREAT\",\n \"PERSIST\",\n \"HSET\",\n \"HDEL\",\n \"SADD\",\n \"SREM\",\n \"ZADD\",\n \"ZREM\",\n];\n\n/**\n * These commands change a state relative to the state before them. A second issue changes it again:\n * an INCR that the client sends again after a timeout counts two times.\n */\nconst CHANGE = [\n \"INCR\",\n \"INCRBY\",\n \"INCRBYFLOAT\",\n \"DECR\",\n \"DECRBY\",\n \"HINCRBY\",\n \"HINCRBYFLOAT\",\n \"ZINCRBY\",\n \"APPEND\",\n \"LPUSH\",\n \"RPUSH\",\n \"LPOP\",\n \"RPOP\",\n \"BLPOP\",\n \"BRPOP\",\n \"LMOVE\",\n \"BLMOVE\",\n \"SPOP\",\n \"XADD\",\n \"XACK\",\n \"PUBLISH\",\n \"EVAL\",\n \"EVALSHA\",\n \"FCALL\",\n \"MULTI\",\n \"EXEC\",\n \"GETDEL\",\n \"GETSET\",\n \"SETNX\",\n];\n\n/** These are the operations of the client: the idempotency of each command that the table names. */\nexport const REDIS_OPERATIONS: Readonly<Record<string, Operation>> = Object.fromEntries([\n ...SAME_EFFECT.map((command) => [command, { idempotent: true }] as const),\n ...CHANGE.map((command) => [command, { idempotent: false }] as const),\n]);\n\n/** This function gives the name of a command as the table writes it: upper case, and CLIENT ID with its subcommand. */\nexport const commandName = (command: string, args: readonly unknown[]): string => {\n const name = command.toUpperCase();\n if ((name === \"CLIENT\" || name === \"CONFIG\" || name === \"DEBUG\") && typeof args[0] === \"string\")\n return `${name} ${args[0].toUpperCase()}`;\n return name;\n};\n","import { connect as dialTcp } from \"node:net\";\nimport type { Extent, Quantity } from \"@mark1russell7/quantity\";\nimport { quantity, valueIn } from \"@mark1russell7/quantity\";\nimport type { ServiceBuilder } from \"../builder.ts\";\nimport type { Cancel, Clock } from \"../clock.ts\";\nimport { realClock } from \"../clock.ts\";\nimport type { Check, Dependency, Gate, ServiceDescription, Step } from \"../description.ts\";\nimport type { Recorder } from \"../events.ts\";\nimport type { Outcome } from \"../latch.ts\";\nimport type { DependencyHandle } from \"../lifecycle.ts\";\nimport type { Operation } from \"../operation.ts\";\nimport { mayReissue } from \"../operation.ts\";\nimport type { CheckFn } from \"../poller.ts\";\nimport type { Poll } from \"../timing.ts\";\nimport type { ConnectionFactory, RedisConnection, RedisConnectionOptions } from \"./connection.ts\";\nimport { ioredis } from \"./connection.ts\";\nimport type { Meeting } from \"./meets.ts\";\nimport { meetings } from \"./meets.ts\";\nimport { REDIS_OPERATIONS, commandName } from \"./operations.ts\";\n\n/**\n * These are the knobs of the client, each a time, by the names of the ordering law where the law\n * has them. The runtime has no defaults, thus each knob is necessary, and a bound that nobody gives\n * is \"infinite\", not zero.\n */\nexport interface RedisKnobs {\n /** This time bounds a try to connect, until the TCP connection exists (`connectTimeout`). */\n readonly connect_bound: Quantity;\n /** This time bounds each command, the handshake commands too (`commandTimeout`). */\n readonly request_timeout: Quantity;\n /** This time destroys a socket that gives no data while a command waits (`socketTimeout`). */\n readonly driver_writeoff_time: Extent;\n /** This time is the idle time of TCP keepalive. Node then sends 10 probes 1 s apart. */\n readonly keepalive_idle: Extent;\n /** This time is the interval of the in-band watchdog: a PING on the connection of the application. */\n readonly heartbeat_interval: Extent;\n /** This time bounds each PING of the watchdog. */\n readonly heartbeat_timeout: Quantity;\n /** This time is how long the client tries to connect again before it stops. The watchdog then starts it again. */\n readonly client_retry_window: Extent;\n /** This time is the first delay before a new try to connect. The delay doubles after each try. */\n readonly retry_base: Quantity;\n /** This time is the most that one delay can be, before its jitter. */\n readonly retry_cap: Quantity;\n /** This time bounds the close of the client at the shutdown: a QUIT, then a disconnect. */\n readonly close_bound: Quantity;\n /** This time is the age at which the client replaces its connection. The client supports only \"infinite\". */\n readonly max_connection_age: Extent;\n /**\n * This time is when the kernel gives up a segment that it retransmits. Node cannot set it, thus\n * the declaration states it for the ordering law (O17), from the platform.\n */\n readonly retrans_giveup_time: Extent;\n}\n\n/** A Redis client spec is the declaration of one Redis client: where it connects, and its knobs. */\nexport interface RedisClientSpec {\n readonly host: string;\n readonly port: number;\n readonly db?: number | undefined;\n readonly username?: string | undefined;\n readonly password?: string | undefined;\n /** This identifier is the component in the corpus (`systems.component.redis`), or null. */\n readonly system: string | null;\n readonly knobs: RedisKnobs;\n /** This count is the number of PINGs in sequence without an answer that make the watchdog connect again. */\n readonly heartbeatMisses: number;\n /** These operations add to the table of the client, or change it. */\n readonly operations?: Readonly<Record<string, Operation>> | undefined;\n}\n\n/** A dial function opens a new TCP connection, sends one PING, and reads the reply. The `dial` check uses it. */\nexport type DialFn = (host: string, port: number, signal: AbortSignal) => Promise<void>;\n\n/** These options make a client. Without them, it uses ioredis, the real clock and `Math.random`. */\nexport interface RedisClientOptions {\n readonly create?: ConnectionFactory | undefined;\n readonly clock?: Clock | undefined;\n readonly random?: (() => number) | undefined;\n /** This function receives each transition of the client, for the prober (D3). */\n readonly record?: Recorder | undefined;\n readonly dial?: DialFn | undefined;\n /**\n * This function waits one turn of the event loop. The watchdog waits it before it counts a miss,\n * thus a reply that is in the socket is read first. Without it, the client uses `setImmediate`.\n */\n readonly turn?: (() => Promise<void>) | undefined;\n}\n\n/**\n * A join is one generation of the connection, at its `ready` (D1, D2). The prober joins its kernel\n * records to the server's view of the connection through it. A join holds the server's id of the\n * connection, and the local address and port of the socket.\n */\nexport interface Join {\n readonly generation: number;\n /** This time is when the connection became ready, in milliseconds on the monotonic clock. */\n readonly at: number;\n /** This number is the reply of CLIENT ID, or null when the server gave none. */\n readonly clientId: number | null;\n readonly localAddress: string | null;\n readonly localPort: number | null;\n readonly remoteAddress: string | null;\n readonly remotePort: number | null;\n}\n\n/**\n * A sent result is the outcome of a command, in three values (C8):\n * - `ok`: the server answered.\n * - `error`: the command did not apply. The server answered with an error (`reply`), or the command\n * did not leave the client.\n * - `indeterminate`: the command can have reached the server, and nothing tells if it applied.\n */\nexport type Sent<T> =\n | { readonly outcome: \"ok\"; readonly value: T; readonly attempts: number }\n | { readonly outcome: \"error\"; readonly error: string; readonly reply: boolean; readonly attempts: number }\n | { readonly outcome: \"indeterminate\"; readonly error: string; readonly attempts: number };\n\n/** Send options tell how the client sends one command. */\nexport interface SendOptions {\n /** This signal abandons the command. A command that left the client is then indeterminate. */\n readonly signal?: AbortSignal | undefined;\n /** False stops the client: it does not send the command again. A check that must fail at once uses it. */\n readonly reissue?: boolean | undefined;\n}\n\ntype Attempt =\n | { readonly outcome: \"ok\"; readonly value: unknown }\n | { readonly outcome: \"error\"; readonly error: string; readonly reply: boolean }\n | { readonly outcome: \"indeterminate\"; readonly error: string };\n\n/** The steps of a connection that the client gives a check for (B14). */\nexport type ClientStep = Extract<Step, \"dial\" | \"round-trip\" | \"local-state\">;\n\n/** Check options tell which checks the client gives the health runtime, and which policy each one controls. */\nexport interface CheckOptions {\n readonly poll: Poll;\n /** For each step, the policy that its check controls, or null for a check that only reports. A step that is absent has no check. */\n readonly steps: Partial<Record<ClientStep, Gate | null>>;\n}\n\n/** A Redis client is a dependency of a service: its declaration, its handle, its checks and its commands. */\nexport interface RedisClient {\n readonly name: string;\n /** This declaration goes to `svc.dependency(name, declaration, handle)`. */\n readonly declaration: Omit<Dependency, \"name\">;\n readonly handle: DependencyHandle;\n /** These are the manifestations that the client can meet, and the detector of each (E9). */\n readonly meets: readonly Meeting[];\n /** This text is the state of the connection, as ioredis names it, or \"none\" before the connect. */\n readonly status: string;\n /** These are the generations of the connection, the latest last. */\n readonly joins: readonly Join[];\n /**\n * This method sends a command, and gives its outcome in three values. The client sends again\n * only an idempotent operation, one time at most, after an outcome that is not certain. The\n * second try stays in the bound of the request. The client does not send again an operation that\n * the table does not name.\n */\n send<T = unknown>(command: string, args?: ReadonlyArray<string | number>, options?: SendOptions): Promise<Sent<T>>;\n /** This method gives the checks of the client and their functions, for `svc.check(check, run)`. */\n checks(options: CheckOptions): ReadonlyArray<readonly [Check, CheckFn]>;\n /** This method declares the client in a service: its dependency, its handle and its checks. */\n attach(svc: ServiceBuilder, options: CheckOptions): ServiceBuilder;\n /** This method gives the problems of the client in a description: a close that does not fit in its phase, for example. */\n problems(description: ServiceDescription): string[];\n}\n\nconst ms = (amount: Extent): number =>\n amount.value === \"infinite\" ? Number.POSITIVE_INFINITY : valueIn(amount as Quantity, \"ms\");\nconst finite = (amount: Extent): number | undefined => {\n const value = ms(amount);\n return Number.isFinite(value) ? value : undefined;\n};\nconst message = (error: unknown): string => (error instanceof Error ? error.message : String(error));\n\n/**\n * This function gives the time that the watchdog takes to find a silent server, at the most. A PING\n * starts each interval, from the start of the one before. Each one fails at its timeout, and the\n * last of `misses` failures in sequence closes the connection: `misses` intervals plus a timeout.\n */\nexport const heartbeatDetection = (interval: Extent, timeout: Quantity, misses: number): Extent =>\n interval.value === \"infinite\"\n ? { value: \"infinite\", unit: \"s\" }\n : quantity((misses * ms(interval) + ms(timeout)) / 1000, \"s\");\n\n/** This function gives the problems of a spec: the knobs that the client cannot use. */\nexport const specProblems = (name: string, spec: RedisClientSpec): string[] => {\n const problems: string[] = [];\n const { knobs } = spec;\n if (!Number.isInteger(spec.heartbeatMisses) || spec.heartbeatMisses < 1)\n problems.push(`${name}'s watchdog closes the connection after ${spec.heartbeatMisses} misses, not a whole number above zero`);\n if (knobs.max_connection_age.value !== \"infinite\")\n problems.push(`${name} cannot replace its connection at an age: max_connection_age must be \"infinite\"`);\n for (const [knob, amount] of Object.entries(knobs) as Array<[string, Extent]>) {\n if (amount.value !== \"infinite\" && !(ms(amount) >= 0))\n problems.push(`${name}'s ${knob} is not a time of zero or more`);\n }\n if (ms(knobs.request_timeout) <= 0) problems.push(`${name}'s request_timeout is not above zero: each command must have a bound`);\n if (ms(knobs.heartbeat_timeout) <= 0) problems.push(`${name}'s heartbeat_timeout is not above zero`);\n if (ms(knobs.retry_cap) < ms(knobs.retry_base)) problems.push(`${name}'s retry_cap is below its retry_base`);\n return problems;\n};\n\n/** This dial function opens a TCP connection, sends an inline PING, and waits for +PONG. */\nexport const tcpDial: DialFn = (host, port, signal) =>\n new Promise((resolve, reject) => {\n if (signal.aborted) {\n reject(new Error(\"the dial was aborted\"));\n return;\n }\n const socket = dialTcp({ host, port });\n let reply = \"\";\n const end = (error?: Error): void => {\n signal.removeEventListener(\"abort\", onAbort);\n socket.destroy();\n if (error === undefined) resolve();\n else reject(error);\n };\n const onAbort = (): void => end(new Error(\"the dial was aborted\"));\n signal.addEventListener(\"abort\", onAbort, { once: true });\n socket.once(\"connect\", () => socket.write(\"PING\\r\\n\"));\n socket.on(\"data\", (chunk) => {\n reply += chunk.toString();\n if (reply.includes(\"\\r\\n\")) end(reply.startsWith(\"+PONG\") ? undefined : new Error(`the reply was ${reply.trim()}`));\n });\n socket.once(\"error\", (error) => end(error));\n socket.once(\"close\", () => end(new Error(\"the server closed the dial\")));\n });\n\n/**\n * This function makes a Redis client. It is a dependency of a service, with ioredis under it:\n * - It sets each timer of ioredis from the declaration, and it does not let ioredis send a command\n * again (R5 §8).\n * - It tells the outcome of a command in three values, and it settles each command in flight as\n * indeterminate when its connection closes (C8, C10).\n * - It records the server's id of each connection at `ready` (D1).\n * - A watchdog sends a PING in the band of the connection, and it connects again after the misses\n * of the declaration. It also starts a client that stopped (principle 19).\n */\nexport const redisClient = (name: string, spec: RedisClientSpec, options: RedisClientOptions = {}): RedisClient => {\n const problems = specProblems(name, spec);\n if (problems.length > 0) throw new Error(problems.join(\"; \"));\n const { knobs } = spec;\n const clock = options.clock ?? realClock;\n const random = options.random ?? Math.random;\n const create = options.create ?? ioredis;\n const dial = options.dial ?? tcpDial;\n const turn = options.turn ?? (() => new Promise<void>((resolve) => setImmediate(resolve)));\n const operations: Readonly<Record<string, Operation>> = { ...REDIS_OPERATIONS, ...spec.operations };\n const requestTimeout = ms(knobs.request_timeout);\n const detection = heartbeatDetection(knobs.heartbeat_interval, knobs.heartbeat_timeout, spec.heartbeatMisses);\n\n let connection: RedisConnection | null = null;\n let status = \"none\";\n let closing = false;\n const joins: Join[] = [];\n // the commands that the client wrote while the connection was ready: a close settles each one\n const inFlight = new Set<(attempt: Attempt) => void>();\n const readyWaiters = new Set<() => void>();\n const listeners: Array<[string, (...args: unknown[]) => void]> = [];\n\n const record = (from: string, to: string, on: string, detail?: string): void =>\n options.record?.({ at: clock.now(), machine: \"client\", instance: name, from, to, on, ...(detail === undefined ? {} : { detail }) });\n\n // the delays of the connection attempts: doubling from the base to the cap, each with a jitter up\n // to the base, and none after the retry window. The watchdog starts the client again after that\n let retrySince: number | null = null;\n const retryStrategy = (times: number): number | null => {\n const now = clock.now();\n retrySince ??= now;\n if (now - retrySince > ms(knobs.client_retry_window)) {\n record(status, status, \"retry.window-ended\");\n return null;\n }\n const delay = Math.min(ms(knobs.retry_base) * 2 ** (times - 1), ms(knobs.retry_cap));\n return Math.round(delay + random() * ms(knobs.retry_base));\n };\n\n const connectionOptions: RedisConnectionOptions = {\n host: spec.host,\n port: spec.port,\n ...(spec.db === undefined ? {} : { db: spec.db }),\n ...(spec.username === undefined ? {} : { username: spec.username }),\n ...(spec.password === undefined ? {} : { password: spec.password }),\n lazyConnect: true,\n connectTimeout: ms(knobs.connect_bound),\n commandTimeout: requestTimeout,\n socketTimeout: finite(knobs.driver_writeoff_time),\n keepAlive: finite(knobs.keepalive_idle) ?? false,\n enableOfflineQueue: false,\n autoResendUnfulfilledCommands: false,\n maxRetriesPerRequest: null,\n enableReadyCheck: true,\n connectionName: name,\n retryStrategy,\n };\n\n /** This function gives the outcome of a failed command: a reply of the server, a command that did not leave the client, or neither. */\n const failed = (error: unknown): Attempt => {\n const text = message(error);\n if (error instanceof Error && error.name === \"ReplyError\") return { outcome: \"error\", error: text, reply: true };\n if (/isn't writeable|enableOfflineQueue/i.test(text)) return { outcome: \"error\", error: text, reply: false };\n return { outcome: \"indeterminate\", error: text };\n };\n\n const onReady = async (): Promise<void> => {\n retrySince = null;\n const c = connection;\n if (c === null) return;\n const generation = joins.length + 1;\n const at = clock.now();\n const socket = c.stream;\n let clientId: number | null = null;\n try {\n const reply = await c.call(\"CLIENT\", \"ID\");\n clientId = typeof reply === \"number\" ? reply : Number(reply);\n if (!Number.isFinite(clientId)) clientId = null;\n } catch {\n clientId = null;\n }\n // a connection that closed before its reply is a generation that never served\n if (connection !== c || c.status !== \"ready\") return;\n joins.push({\n generation,\n at,\n clientId,\n localAddress: socket?.localAddress ?? null,\n localPort: socket?.localPort ?? null,\n remoteAddress: socket?.remoteAddress ?? null,\n remotePort: socket?.remotePort ?? null,\n });\n record(\"ready\", \"ready\", \"join\", `generation ${generation}, client id ${clientId ?? \"unknown\"}`);\n for (const wake of [...readyWaiters]) wake();\n };\n\n const onStatus = (to: string) => (): void => {\n const from = status;\n status = to;\n record(from, to, `status.${to}`);\n if (to === \"close\" || to === \"end\") {\n // the lower layer gave up the connection: each command in flight learns it now (C10)\n for (const settle of [...inFlight]) settle({ outcome: \"indeterminate\", error: `the connection closed with the command in flight (${from})` });\n }\n if (to === \"ready\") void onReady();\n };\n\n const watch = (c: RedisConnection): void => {\n for (const state of [\"connecting\", \"connect\", \"ready\", \"close\", \"reconnecting\", \"end\"]) {\n const listener = onStatus(state);\n c.on(state, listener);\n listeners.push([state, listener]);\n }\n // an error of the connection comes before its close, which settles the commands: here it is only recorded\n const onError = (error: unknown): void => record(status, status, \"error\", message(error));\n c.on(\"error\", onError);\n listeners.push([\"error\", onError]);\n };\n\n /** This function waits until the connection is ready, for `within` milliseconds at most. It gives true when it is ready. */\n const waitReady = (within: number, signal?: AbortSignal): Promise<boolean> =>\n new Promise((resolve) => {\n if (connection?.status === \"ready\") {\n resolve(true);\n return;\n }\n let cancel: Cancel = () => {};\n const done = (value: boolean): void => {\n readyWaiters.delete(wake);\n cancel();\n signal?.removeEventListener(\"abort\", aborted);\n resolve(value);\n };\n const wake = (): void => done(true);\n const aborted = (): void => done(false);\n readyWaiters.add(wake);\n cancel = clock.after(within, () => done(connection?.status === \"ready\"));\n signal?.addEventListener(\"abort\", aborted, { once: true });\n });\n\n const attempt = (command: string, args: ReadonlyArray<string | number>, signal?: AbortSignal): Promise<Attempt> => {\n const c = connection;\n if (c === null) return Promise.resolve({ outcome: \"error\", error: `${name} has not connected`, reply: false });\n if (c.status !== \"ready\") return Promise.resolve({ outcome: \"error\", error: `${name} is ${c.status}, not ready: the command did not leave the client`, reply: false });\n if (signal?.aborted) return Promise.resolve({ outcome: \"error\", error: \"the caller aborted the command before it left the client\", reply: false });\n return new Promise<Attempt>((resolve) => {\n let settled = false;\n const settle = (value: Attempt): void => {\n if (settled) return;\n settled = true;\n inFlight.delete(settle);\n signal?.removeEventListener(\"abort\", aborted);\n resolve(value);\n };\n // a sent command cannot be cancelled: the caller abandons it, and its outcome is not known (principle 9)\n const aborted = (): void => settle({ outcome: \"indeterminate\", error: \"the caller abandoned the command after it was sent\" });\n inFlight.add(settle);\n signal?.addEventListener(\"abort\", aborted, { once: true });\n c.call(command, ...args).then(\n (value) => settle({ outcome: \"ok\", value }),\n (error: unknown) => settle(failed(error)),\n );\n });\n };\n\n const send = async <T>(command: string, args: ReadonlyArray<string | number> = [], sendOptions: SendOptions = {}): Promise<Sent<T>> => {\n const { signal } = sendOptions;\n const operation = sendOptions.reissue === false ? undefined : operations[commandName(command, args)];\n const started = clock.now();\n const first = await attempt(command, args, signal);\n if (first.outcome === \"ok\") return { outcome: \"ok\", value: first.value as T, attempts: 1 };\n // a reply of the server is final, and an operation that the table does not name is never sent again\n const again =\n !(first.outcome === \"error\" && first.reply) &&\n operation !== undefined &&\n signal?.aborted !== true &&\n mayReissue(operation, quantity((clock.now() - started) / 1000, \"s\"));\n const left = requestTimeout - (clock.now() - started);\n if (!again || left <= 0 || !(await waitReady(left, signal))) return { ...first, attempts: 1 };\n record(status, status, \"command.reissue\", commandName(command, args));\n // the second attempt ends at the bound of the request, not one command timeout after it starts\n const bound = new AbortController();\n const end = clock.after(Math.max(0, requestTimeout - (clock.now() - started)), () => bound.abort());\n const forward = (): void => bound.abort();\n signal?.addEventListener(\"abort\", forward, { once: true });\n const second = await attempt(command, args, bound.signal);\n end();\n signal?.removeEventListener(\"abort\", forward);\n return second.outcome === \"ok\" ? { outcome: \"ok\", value: second.value as T, attempts: 2 } : { ...second, attempts: 2 };\n };\n\n // the watchdog: a PING each interval from the start of the one before, on the connection of the\n // application. A reply, an error reply too, is an answer. It waits one turn of the event loop\n // before it counts a miss, thus a slow loop is not a silent server. It acts only on a ready\n // connection, thus it never closes a connection in the middle of a new attempt\n let misses = 0;\n let nextPing: Cancel | null = null;\n const heartbeatInterval = ms(knobs.heartbeat_interval);\n const ping = (): void => {\n nextPing = clock.after(heartbeatInterval, ping);\n const c = connection;\n if (c === null || closing) return;\n if (c.status === \"end\") {\n // the retry window ended: the watchdog starts the client again\n record(status, status, \"watchdog.revive\");\n retrySince = null;\n c.connect().catch(() => {});\n return;\n }\n if (c.status !== \"ready\") return;\n const controller = new AbortController();\n const timer = clock.after(ms(knobs.heartbeat_timeout), () => controller.abort());\n void attempt(\"PING\", [], controller.signal).then(async (result) => {\n timer();\n const answered = result.outcome === \"ok\" || (result.outcome === \"error\" && result.reply);\n await turn();\n if (answered) {\n misses = 0;\n return;\n }\n if (connection?.status !== \"ready\" || closing) return;\n misses += 1;\n record(status, status, \"watchdog.miss\", `${misses} of ${spec.heartbeatMisses}`);\n if (misses >= spec.heartbeatMisses) {\n misses = 0;\n record(status, status, \"watchdog.reconnect\");\n connection.disconnect(true);\n }\n });\n };\n\n const handle: DependencyHandle = {\n async connect(signal) {\n closing = false;\n const c = await create(connectionOptions);\n connection = c;\n watch(c);\n if (signal.aborted) throw new Error(`${name}: the service stopped before the connect`);\n // a server that is down at the start does not stop the startup: the client connects again in\n // the background, and its checks keep readiness tripped until it is ready (readiness is earned)\n await c.connect().catch((error: unknown) => record(status, status, \"connect.failed\", message(error)));\n if (Number.isFinite(heartbeatInterval)) nextPing = clock.after(heartbeatInterval, ping);\n },\n async close() {\n closing = true;\n nextPing?.();\n nextPing = null;\n const c = connection;\n if (c === null) return;\n // a QUIT lets the replies in flight arrive. A silent server never answers it, thus the close has its bound\n const quit = c.quit().then(\n () => \"quit\" as const,\n () => \"quit\" as const,\n );\n let cancel: Cancel = () => {};\n const bound = new Promise<\"bound\">((resolve) => {\n cancel = clock.after(ms(knobs.close_bound), () => resolve(\"bound\"));\n });\n const startedClose = clock.now();\n const first = await Promise.race([quit, bound]);\n cancel();\n if (first === \"bound\") record(status, status, \"close.bound\");\n // a client that is not ready refuses the QUIT at once, and it would connect again: the close stops it\n if (c.status !== \"end\") c.disconnect(false);\n // ioredis is at `end` when its socket closes, a moment after the reply: the close waits for it, in its bound\n const ended = (within: number): Promise<void> =>\n c.status === \"end\"\n ? Promise.resolve()\n : new Promise<void>((resolve) => {\n let stop: Cancel = () => {};\n const end = (): void => {\n c.off(\"end\", end);\n stop();\n resolve();\n };\n c.on(\"end\", end);\n stop = clock.after(Math.max(0, within), end);\n });\n await ended(ms(knobs.close_bound) - (clock.now() - startedClose));\n // a disconnect only half-closes the socket, and a silent server never closes its half: the bound destroys it\n if (c.status !== \"end\") {\n record(status, status, \"close.destroy\");\n c.stream?.destroy?.();\n await ended(0);\n }\n for (const [event, listener] of listeners) c.off(event, listener);\n listeners.length = 0;\n for (const settle of [...inFlight]) settle({ outcome: \"indeterminate\", error: \"the client closed with the command in flight\" });\n status = \"end\";\n },\n };\n\n const declaration: Omit<Dependency, \"name\"> = {\n system: spec.system,\n knobs: {\n connect_bound: knobs.connect_bound,\n request_timeout: knobs.request_timeout,\n driver_writeoff_time: knobs.driver_writeoff_time,\n keepalive_idle: knobs.keepalive_idle,\n heartbeat_interval: knobs.heartbeat_interval,\n heartbeat_timeout: knobs.heartbeat_timeout,\n heartbeat_detection_time: detection,\n client_retry_window: knobs.client_retry_window,\n retry_base: knobs.retry_base,\n retry_cap: knobs.retry_cap,\n close_bound: knobs.close_bound,\n max_connection_age: knobs.max_connection_age,\n retrans_giveup_time: knobs.retrans_giveup_time,\n },\n operations,\n };\n\n const checks = (checkOptions: CheckOptions): ReadonlyArray<readonly [Check, CheckFn]> => {\n const made: Array<readonly [Check, CheckFn]> = [];\n const declare = (step: ClientStep, ownConnection: boolean, run: CheckFn): void => {\n if (!(step in checkOptions.steps)) return;\n made.push([\n { name: `${name}.${step}`, dependency: name, step, ownConnection, gates: checkOptions.steps[step] ?? null, poll: checkOptions.poll },\n run,\n ]);\n };\n // the state of the client, without a round trip\n declare(\"local-state\", true, () => (connection?.status === \"ready\" ? \"ok\" : \"error\"));\n // a PING on the connection of the application: blind to nothing that the application meets\n declare(\"round-trip\", true, async (signal): Promise<Outcome> => {\n // a check measures the step now: it does not wait for a new connection, and it sends nothing again\n const result = await send(\"PING\", [], { signal, reissue: false });\n return result.outcome === \"ok\" ? \"ok\" : \"error\";\n });\n // a new connection each time: it finds a server that refuses connections, but not a stalled connection of the application\n declare(\"dial\", false, async (signal): Promise<Outcome> => {\n await dial(spec.host, spec.port, signal);\n return \"ok\";\n });\n return made;\n };\n\n return {\n name,\n declaration,\n handle,\n meets: meetings(name, knobs, detection),\n get status() {\n return connection === null ? \"none\" : connection.status;\n },\n joins,\n send,\n checks,\n attach(svc, checkOptions) {\n svc.dependency(name, declaration, handle);\n for (const [check, run] of checks(checkOptions)) svc.check(check, run);\n return svc;\n },\n problems(description) {\n const found: string[] = [];\n const teardown = description.shutdown.phases.find((phase) => phase.name === \"tear down dependencies\");\n if (teardown === undefined) found.push(`${name} needs the phase \"tear down dependencies\" to close`);\n else if (ms(knobs.close_bound) >= valueIn(teardown.cap, \"ms\"))\n found.push(`${name}'s close_bound of ${ms(knobs.close_bound)} ms does not fit in the cap of \"tear down dependencies\"`);\n for (const check of description.checks) {\n if (check.dependency !== name) continue;\n const slowest = valueIn(check.poll.timeout, \"ms\");\n if (check.step === \"round-trip\" && requestTimeout > slowest + valueIn(check.poll.interval, \"ms\") * (1 + check.poll.jitter))\n found.push(`${name}'s request_timeout outlives one jittered poll of ${check.name} (B7, O13)`);\n }\n return found;\n },\n };\n};\n","import type { Machine, TransitionDef } from \"@mark1russell7/algebra\";\n\nconst t = (id: string, from: string | readonly string[], on: string | readonly string[], to: string, extra: Partial<TransitionDef> = {}): TransitionDef => ({\n id,\n from,\n on,\n to,\n ...extra,\n});\nconst knob = (key: string) => ({ key, scope: \"client\", unit: \"s\", default: null }) as const;\n\n/** The states where the connection exists or tries to exist, thus where it can close. */\nconst LIVE = [\"connecting\", \"connect\", \"ready\", \"reconnecting\"] as const;\nconst ANY = [\"none\", \"connecting\", \"connect\", \"ready\", \"close\", \"reconnecting\", \"end\"] as const;\n\n/**\n * The Redis client as a `mark1russell7.machine/1` document. Its states are the states of ioredis.\n * The wrapper adds its own events: the join at each `ready`, and the misses of its watchdog. It also\n * adds a command that it sends again, and the bound of its close. A test records the transitions of the\n * client and replays them against this document.\n *\n * The state `ready` hides states that the client cannot see (D4). In a black hole, ioredis stays\n * `ready` while the kernel retransmits, until `driver_writeoff_time` or the watchdog ends that\n * hidden interval. A timer of this document bounds each hidden interval.\n */\nexport const REDIS_CLIENT_MACHINE: Machine = {\n format: \"mark1russell7.machine/1\",\n id: \"service.machine.redis-client\",\n title: \"The Redis client of the service, over ioredis 6\",\n layer: \"driver\",\n version: \"0.2.0\",\n sources: [{ ref: \"packages/service/src/redis (the client implements this document)\", rung: \"implemented\" }],\n states: {\n none: { initial: true, note: \"the client exists, and its connection does not\" },\n connecting: { note: \"a TCP connection attempt\" },\n connect: { note: \"the handshake: HELLO, then the ready check (INFO)\" },\n ready: { note: \"it can hide a black hole or a frozen server, until a timer of the client ends it\" },\n close: { note: \"the socket closed. Each command in flight settles as indeterminate\" },\n reconnecting: { note: \"the delay of the retry strategy, which doubles to its cap\" },\n end: { note: \"the retry window ended, or the service closed the client. The watchdog starts it again\" },\n },\n events: {\n \"status.connecting\": { kind: \"local\" },\n \"status.connect\": { kind: \"rx\" },\n \"status.ready\": { kind: \"rx\" },\n \"status.close\": { kind: \"rx\" },\n \"status.reconnecting\": { kind: \"timer\" },\n \"status.end\": { kind: \"local\" },\n join: { kind: \"rx\", note: \"the reply of CLIENT ID at ready (D1)\" },\n error: { kind: \"rx\" },\n \"connect.failed\": { kind: \"local\" },\n \"command.written\": { kind: \"tx\" },\n \"command.reissue\": { kind: \"local\" },\n data: { kind: \"rx\" },\n heartbeat: { kind: \"timer\" },\n \"watchdog.miss\": { kind: \"local\" },\n \"watchdog.reconnect\": { kind: \"local\" },\n \"watchdog.revive\": { kind: \"local\" },\n \"retry.window-ended\": { kind: \"local\" },\n \"close.request\": { kind: \"local\" },\n \"close.bound\": { kind: \"timer\" },\n \"close.destroy\": { kind: \"local\", note: \"the bound destroys a socket that a silent server does not close\" },\n },\n knobs: {\n connect_bound: knob(\"connectTimeout\"),\n driver_writeoff_time: knob(\"socketTimeout\"),\n heartbeat_interval: knob(\"watchdog.interval\"),\n close_bound: knob(\"close.bound\"),\n },\n timers: {\n connect_timeout: {\n duration: { knob: \"connect_bound\", unit: \"s\" },\n arms: [\"enter:connecting\"],\n cancels: [\"status.connect\", \"status.close\"],\n fires: \"status.close\",\n note: \"ioredis destroys the attempt\",\n },\n writeoff: {\n duration: { knob: \"driver_writeoff_time\", unit: \"s\" },\n arms: [\"command.written\"],\n cancels: [\"data\", \"status.close\"],\n fires: \"status.close\",\n note: \"ioredis destroys a socket that gives no data while a command waits: it ends the hidden interval of a black hole\",\n },\n heartbeat: {\n duration: { knob: \"heartbeat_interval\", unit: \"s\" },\n arms: [\"enter:ready\", \"heartbeat\"],\n cancels: [\"status.close\", \"status.end\"],\n fires: \"heartbeat\",\n note: \"a PING on the connection of the application\",\n },\n close: {\n duration: { knob: \"close_bound\", unit: \"s\" },\n arms: [\"close.request\"],\n cancels: [\"status.end\"],\n fires: \"close.bound\",\n },\n },\n transitions: [\n t(\"connect\", [\"none\", \"reconnecting\", \"end\", \"close\"], \"status.connecting\", \"connecting\"),\n t(\"tcp\", \"connecting\", \"status.connect\", \"connect\"),\n t(\"ready\", \"connect\", \"status.ready\", \"ready\"),\n t(\"close\", [...LIVE], \"status.close\", \"close\"),\n t(\"retry\", \"close\", \"status.reconnecting\", \"reconnecting\"),\n t(\"end\", [...LIVE, \"close\"], \"status.end\", \"end\"),\n t(\"join\", \"ready\", \"join\", \"ready\"),\n t(\"miss\", \"ready\", \"watchdog.miss\", \"ready\"),\n t(\"watchdog-reconnect\", \"ready\", \"watchdog.reconnect\", \"ready\"),\n t(\"revive\", \"end\", \"watchdog.revive\", \"end\"),\n // the retry strategy answers in `close`, before ioredis moves to `reconnecting` or `end`\n t(\"window-ended\", \"close\", \"retry.window-ended\", \"close\"),\n ...ANY.map((state) => t(`error-${state}`, state, \"error\", state)),\n ...ANY.map((state) => t(`connect-failed-${state}`, state, \"connect.failed\", state)),\n ...ANY.map((state) => t(`reissue-${state}`, state, \"command.reissue\", state)),\n ...ANY.map((state) => t(`close-bound-${state}`, state, \"close.bound\", state, { timer: \"close\" })),\n ...ANY.map((state) => t(`close-destroy-${state}`, state, \"close.destroy\", state)),\n ],\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"redis.js","names":["dialTcp"],"sources":["../src/redis/connection.ts","../src/redis/meets.ts","../src/redis/operations.ts","../src/redis/client.ts","../src/redis/machine.ts"],"sourcesContent":["/**\n * A Redis connection is the part of an ioredis client that the wrapper uses. An ioredis 6 client has\n * each part. A fake for tests has them too. Thus the package depends on no type of ioredis.\n */\nexport interface RedisConnection {\n /** This text is the state of the client: wait, connecting, connect, ready, close, reconnecting or end. */\n readonly status: string;\n /** This socket is the socket of the current connection, when there is one. */\n readonly stream?: RedisSocket | undefined;\n connect(): Promise<void>;\n /** This method sends one command, and gives the reply. */\n call(command: string, ...args: Array<string | number>): Promise<unknown>;\n quit(): Promise<unknown>;\n /** This method closes the connection at once. With `true`, the client then connects again. */\n disconnect(reconnect?: boolean): void;\n on(event: string, listener: (...args: unknown[]) => void): unknown;\n off(event: string, listener: (...args: unknown[]) => void): unknown;\n}\n\n/** A Redis socket is the part of the socket of a connection that the join of the prober reads. */\nexport interface RedisSocket {\n readonly localAddress?: string | undefined;\n readonly localPort?: number | undefined;\n readonly remoteAddress?: string | undefined;\n readonly remotePort?: number | undefined;\n /** This method closes the socket at once. A disconnect only half-closes it, and a silent peer does not answer that. */\n destroy?(): void;\n}\n\n/**\n * These are the options that the wrapper gives ioredis. Each one comes from the declaration. The\n * wrapper sets each option that has an effect on a timer or on a send again, thus no default of\n * ioredis decides one.\n */\nexport interface RedisConnectionOptions {\n readonly host: string;\n readonly port: number;\n readonly db?: number | undefined;\n readonly username?: string | undefined;\n readonly password?: string | undefined;\n /** The wrapper connects in its own step of the startup. */\n readonly lazyConnect: true;\n readonly connectTimeout: number;\n readonly commandTimeout: number;\n /** This time destroys a socket that gives no data while a command waits. Undefined is no bound. */\n readonly socketTimeout: number | undefined;\n /** This time is the idle time of TCP keepalive. False is no keepalive. */\n readonly keepAlive: number | false;\n /** A command while the client is not ready fails at once: it did not reach the server. */\n readonly enableOfflineQueue: false;\n /** The client does not send again a command that can have reached the server. */\n readonly autoResendUnfulfilledCommands: false;\n readonly maxRetriesPerRequest: null;\n readonly enableReadyCheck: true;\n readonly connectionName: string;\n /** This function gives the delay before the next try to connect, or null to stop. */\n readonly retryStrategy: (times: number) => number | null;\n}\n\n/** A connection factory makes the connection of the client from its options. */\nexport type ConnectionFactory = (options: RedisConnectionOptions) => RedisConnection | Promise<RedisConnection>;\n\n/**\n * This factory makes an ioredis client. It loads ioredis only when a client connects, thus a\n * service that does not use Redis does not need ioredis.\n */\nexport const ioredis: ConnectionFactory = async (options) => {\n const loaded = (await import(\"ioredis\")) as unknown as {\n readonly default: new (options: RedisConnectionOptions) => RedisConnection;\n };\n return new loaded.default(options);\n};\n","import type { Extent, Quantity } from \"@mark1russell7/quantity\";\nimport { quantity, valueIn } from \"@mark1russell7/quantity\";\nimport type { RedisKnobs } from \"./client.ts\";\n\n/** A situation is what the connection did when the failure started: a command in flight, or idle. */\nexport type Situation = \"in flight\" | \"idle\";\n\n/** A detector is a mechanism that finds a failure, and the most time that it takes. */\nexport interface Detector {\n /** This name identifies the mechanism: a knob of the declaration, or the kernel. */\n readonly by: string;\n /** This time is the most that the detector takes, after the failure starts. \"infinite\" tells that it does not find the failure. */\n readonly within: Extent;\n readonly situation: Situation;\n /** This text tells what the detector does, for example that it fails a command without a change of state. */\n readonly effect: string;\n}\n\n/**\n * A meeting is a manifestation that the client can meet, and its detectors (E9). The names of the\n * manifestations are the names of the vocabulary of the harness and the corpus. `app-stall` is a\n * server whose process is alive and does not answer, a proposal of the vocabulary.\n */\nexport interface Meeting {\n readonly manifestation: string;\n /** This text tells the failure in words. */\n readonly what: string;\n readonly detectors: readonly Detector[];\n}\n\nconst never: Extent = { value: \"infinite\", unit: \"s\" };\nconst s = (value: number): Quantity => quantity(Math.round(value * 1e6) / 1e6, \"s\");\nconst seconds = (amount: Extent): number =>\n amount.value === \"infinite\" ? Number.POSITIVE_INFINITY : valueIn(amount as Quantity, \"s\");\nconst plus = (amount: Extent, more: number): Extent =>\n amount.value === \"infinite\" ? never : s(seconds(amount) + more);\n\n/** Node gives keepalive 10 probes, 1 s apart, after its idle time: a silent peer is found 10 s after the idle time. */\nconst NODE_PROBES = 10;\nconst NODE_INTERVAL = 1;\n\n/**\n * This function gives the manifestations that a Redis client can meet, and the detectors of each, from\n * its knobs. A reset or a FIN arrives on the next segment. A black hole and a host that is gone give\n * no answer at all. A frozen process and a stalled server keep a kernel that acknowledges each\n * segment. Thus TCP keepalive does not fire on them, and only the client's own timers find them.\n */\nexport const meetings = (name: string, knobs: RedisKnobs, heartbeat: Extent): Meeting[] => {\n const keepalive = plus(knobs.keepalive_idle, NODE_PROBES * NODE_INTERVAL);\n const watchdog = (situation: Situation): Detector => ({\n by: \"heartbeat_detection_time\",\n within: heartbeat,\n situation,\n effect: `the watchdog of ${name} closes the connection, and the client connects again`,\n });\n const writeoff: Detector = {\n by: \"driver_writeoff_time\",\n within: knobs.driver_writeoff_time,\n situation: \"in flight\",\n effect: \"ioredis destroys the socket of a command without data, and the client connects again\",\n };\n const command: Detector = {\n by: \"request_timeout\",\n within: knobs.request_timeout,\n situation: \"in flight\",\n effect: \"the command fails as indeterminate, and the state of the connection does not change\",\n };\n const giveUp: Detector = {\n by: \"retrans_giveup_time\",\n within: knobs.retrans_giveup_time,\n situation: \"in flight\",\n effect: \"the kernel gives up the retransmissions and closes the socket\",\n };\n const silent = (manifestation: string, what: string): Meeting => ({\n manifestation,\n what,\n detectors: [\n command,\n writeoff,\n watchdog(\"in flight\"),\n giveUp,\n watchdog(\"idle\"),\n { by: \"keepalive_idle\", within: keepalive, situation: \"idle\", effect: \"TCP keepalive gives up, and the socket closes\" },\n ],\n });\n const acknowledged = (manifestation: string, what: string): Meeting => ({\n manifestation,\n what,\n detectors: [\n command,\n writeoff,\n watchdog(\"in flight\"),\n watchdog(\"idle\"),\n { by: \"keepalive_idle\", within: never, situation: \"idle\", effect: \"the kernel of the server answers each probe\" },\n ],\n });\n const immediate = (situation: Situation, effect: string): Detector => ({ by: \"the next segment\", within: s(0), situation, effect });\n return [\n {\n manifestation: \"rst\",\n what: \"the server answers each segment with a reset, and refuses each new connection\",\n detectors: [\n immediate(\"in flight\", \"the command fails, and the client connects again, and each attempt is refused\"),\n watchdog(\"idle\"),\n { by: \"keepalive_idle\", within: keepalive.value === \"infinite\" ? never : knobs.keepalive_idle, situation: \"idle\", effect: \"the first probe gets a reset\" },\n ],\n },\n {\n manifestation: \"fin\",\n what: \"the process of the server ends, and its kernel closes the connection\",\n detectors: [immediate(\"in flight\", \"the close arrives, and the command fails\"), immediate(\"idle\", \"the close arrives\")],\n },\n silent(\"blackhole\", \"the network drops each segment in the two directions\"),\n silent(\"freeze-host\", \"the host of the server stops, without a close: the death without a close\"),\n acknowledged(\"freeze-process\", \"the process of the server stops, and its kernel stays up\"),\n acknowledged(\"app-stall\", \"the server stops its replies for a time, for example a long command, and its kernel stays up\"),\n ];\n};\n\n/** This function gives the first detector of a manifestation in a situation, or null when no detector has an end (C3). */\nexport const firstDetector = (meeting: Meeting, situation: Situation): Detector | null => {\n const ranked = meeting.detectors\n .filter((detector) => detector.situation === situation && detector.within.value !== \"infinite\")\n .sort((a, b) => seconds(a.within) - seconds(b.within));\n return ranked[0] ?? null;\n};\n\n/** This function gives each manifestation and situation that no detector of the client finds: the standing rule, as a query (C3, E9). */\nexport const unwatched = (meets: readonly Meeting[]): Array<{ readonly manifestation: string; readonly situation: Situation }> =>\n meets.flatMap((meeting) =>\n ([\"in flight\", \"idle\"] as const).flatMap((situation) =>\n firstDetector(meeting, situation) === null ? [{ manifestation: meeting.manifestation, situation }] : [],\n ),\n );\n","import type { Operation } from \"../operation.ts\";\n\n/**\n * These are the Redis commands whose idempotency the client declares when the declaration does not\n * say. A second issue of an idempotent command has the same effect. Thus the client can send it\n * again after a failure that left its result unknown. The client does not send again a command\n * that is not in this table: a declaration that uses it says its idempotency itself.\n */\nconst SAME_EFFECT = [\n // reads\n \"PING\",\n \"ECHO\",\n \"GET\",\n \"MGET\",\n \"STRLEN\",\n \"GETRANGE\",\n \"EXISTS\",\n \"TYPE\",\n \"TTL\",\n \"PTTL\",\n \"EXPIRETIME\",\n \"HGET\",\n \"HMGET\",\n \"HGETALL\",\n \"HKEYS\",\n \"HVALS\",\n \"HLEN\",\n \"HEXISTS\",\n \"SMEMBERS\",\n \"SISMEMBER\",\n \"SMISMEMBER\",\n \"SCARD\",\n \"ZRANGE\",\n \"ZRANGEBYSCORE\",\n \"ZSCORE\",\n \"ZMSCORE\",\n \"ZRANK\",\n \"ZCARD\",\n \"ZCOUNT\",\n \"LRANGE\",\n \"LINDEX\",\n \"LLEN\",\n \"XRANGE\",\n \"XREVRANGE\",\n \"XLEN\",\n \"SCAN\",\n \"SSCAN\",\n \"HSCAN\",\n \"ZSCAN\",\n \"DBSIZE\",\n \"INFO\",\n \"CLIENT ID\",\n // writes that put a state, not a change of a state: a second issue puts the same state\n \"SET\",\n \"MSET\",\n \"SETEX\",\n \"PSETEX\",\n \"DEL\",\n \"UNLINK\",\n \"EXPIRE\",\n \"PEXPIRE\",\n \"EXPIREAT\",\n \"PEXPIREAT\",\n \"PERSIST\",\n \"HSET\",\n \"HDEL\",\n \"SADD\",\n \"SREM\",\n \"ZADD\",\n \"ZREM\",\n];\n\n/**\n * These commands change a state relative to the state before them. A second issue changes it again:\n * an INCR that the client sends again after a timeout counts two times.\n */\nconst CHANGE = [\n \"INCR\",\n \"INCRBY\",\n \"INCRBYFLOAT\",\n \"DECR\",\n \"DECRBY\",\n \"HINCRBY\",\n \"HINCRBYFLOAT\",\n \"ZINCRBY\",\n \"APPEND\",\n \"LPUSH\",\n \"RPUSH\",\n \"LPOP\",\n \"RPOP\",\n \"BLPOP\",\n \"BRPOP\",\n \"LMOVE\",\n \"BLMOVE\",\n \"SPOP\",\n \"XADD\",\n \"XACK\",\n \"PUBLISH\",\n \"EVAL\",\n \"EVALSHA\",\n \"FCALL\",\n \"MULTI\",\n \"EXEC\",\n \"GETDEL\",\n \"GETSET\",\n \"SETNX\",\n];\n\n/** These are the operations of the client: the idempotency of each command that the table names. */\nexport const REDIS_OPERATIONS: Readonly<Record<string, Operation>> = Object.fromEntries([\n ...SAME_EFFECT.map((command) => [command, { idempotent: true }] as const),\n ...CHANGE.map((command) => [command, { idempotent: false }] as const),\n]);\n\n/** This function gives the name of a command as the table writes it: upper case, and CLIENT ID with its subcommand. */\nexport const commandName = (command: string, args: readonly unknown[]): string => {\n const name = command.toUpperCase();\n if ((name === \"CLIENT\" || name === \"CONFIG\" || name === \"DEBUG\") && typeof args[0] === \"string\")\n return `${name} ${args[0].toUpperCase()}`;\n return name;\n};\n","import { connect as dialTcp } from \"node:net\";\nimport type { Extent, Quantity } from \"@mark1russell7/quantity\";\nimport { quantity, valueIn } from \"@mark1russell7/quantity\";\nimport type { Backoff } from \"../backoff.ts\";\nimport { backoffDelay } from \"../backoff.ts\";\nimport type { ServiceBuilder } from \"../builder.ts\";\nimport type { Cancel, Clock } from \"../clock.ts\";\nimport { realClock } from \"../clock.ts\";\nimport type { Check, Dependency, Gate, ServiceDescription, Step } from \"../description.ts\";\nimport type { Recorder } from \"../events.ts\";\nimport type { Heartbeat } from \"../heartbeat.ts\";\nimport { heartbeat } from \"../heartbeat.ts\";\nimport type { Outcome } from \"../latch.ts\";\nimport type { DependencyHandle } from \"../lifecycle.ts\";\nimport type { Operation } from \"../operation.ts\";\nimport { mayReissue } from \"../operation.ts\";\nimport type { CheckFn } from \"../poller.ts\";\nimport type { Poll } from \"../timing.ts\";\nimport { heartbeatDetection } from \"../timing.ts\";\nimport type { ConnectionFactory, RedisConnection, RedisConnectionOptions } from \"./connection.ts\";\nimport { ioredis } from \"./connection.ts\";\nimport type { Meeting } from \"./meets.ts\";\nimport { meetings } from \"./meets.ts\";\nimport { REDIS_OPERATIONS, commandName } from \"./operations.ts\";\n\n/**\n * These are the knobs of the client, each a time, by the names of the ordering law where the law\n * has them. The runtime has no defaults, thus each knob is necessary, and a bound that nobody gives\n * is \"infinite\", not zero.\n */\nexport interface RedisKnobs {\n /** This time bounds a try to connect, until the TCP connection exists (`connectTimeout`). */\n readonly connect_bound: Quantity;\n /** This time bounds each command, the handshake commands too (`commandTimeout`). */\n readonly request_timeout: Quantity;\n /** This time destroys a socket that gives no data while a command waits (`socketTimeout`). */\n readonly driver_writeoff_time: Extent;\n /** This time is the idle time of TCP keepalive. Node then sends 10 probes 1 s apart. */\n readonly keepalive_idle: Extent;\n /** This time is the interval of the in-band watchdog: a PING on the connection of the application. */\n readonly heartbeat_interval: Extent;\n /** This time bounds each PING of the watchdog. */\n readonly heartbeat_timeout: Quantity;\n /** This time is how long the client tries to connect again before it stops. The watchdog then starts it again. */\n readonly client_retry_window: Extent;\n /** This time is the first delay before a new try to connect. The delay doubles after each try. */\n readonly retry_base: Quantity;\n /** This time is the most that one delay can be, before its jitter. */\n readonly retry_cap: Quantity;\n /** This time bounds the close of the client at the shutdown: a QUIT, then a disconnect. */\n readonly close_bound: Quantity;\n /** This time is the age at which the client replaces its connection. The client supports only \"infinite\". */\n readonly max_connection_age: Extent;\n /**\n * This time is when the kernel gives up a segment that it retransmits. Node cannot set it, thus\n * the declaration states it for the ordering law (O17), from the platform.\n */\n readonly retrans_giveup_time: Extent;\n}\n\n/** A Redis client spec is the declaration of one Redis client: where it connects, and its knobs. */\nexport interface RedisClientSpec {\n readonly host: string;\n readonly port: number;\n readonly db?: number | undefined;\n readonly username?: string | undefined;\n readonly password?: string | undefined;\n /** This identifier is the component in the corpus (`systems.component.redis`), or null. */\n readonly system: string | null;\n readonly knobs: RedisKnobs;\n /** This count is the number of PINGs in sequence without an answer that make the watchdog connect again. */\n readonly heartbeatMisses: number;\n /** These operations add to the table of the client, or change it. */\n readonly operations?: Readonly<Record<string, Operation>> | undefined;\n}\n\n/** A dial function opens a new TCP connection, sends one PING, and reads the reply. The `dial` check uses it. */\nexport type DialFn = (host: string, port: number, signal: AbortSignal) => Promise<void>;\n\n/** These options make a client. Without them, it uses ioredis, the real clock and `Math.random`. */\nexport interface RedisClientOptions {\n readonly create?: ConnectionFactory | undefined;\n readonly clock?: Clock | undefined;\n readonly random?: (() => number) | undefined;\n /** This function receives each transition of the client, for the prober (D3). */\n readonly record?: Recorder | undefined;\n readonly dial?: DialFn | undefined;\n /**\n * This function waits one turn of the event loop. The watchdog waits it before it counts a miss,\n * thus a reply that is in the socket is read first. Without it, the client uses `setImmediate`.\n */\n readonly turn?: (() => Promise<void>) | undefined;\n}\n\n/**\n * A join is one generation of the connection, at its `ready` (D1, D2). The prober joins its kernel\n * records to the server's view of the connection through it. A join holds the server's id of the\n * connection, and the local address and port of the socket.\n */\nexport interface Join {\n readonly generation: number;\n /** This time is when the connection became ready, in milliseconds on the monotonic clock. */\n readonly at: number;\n /** This number is the reply of CLIENT ID, or null when the server gave none. */\n readonly clientId: number | null;\n readonly localAddress: string | null;\n readonly localPort: number | null;\n readonly remoteAddress: string | null;\n readonly remotePort: number | null;\n}\n\n/**\n * A sent result is the outcome of a command, in three values (C8):\n * - `ok`: the server answered.\n * - `error`: the command did not apply. The server answered with an error (`reply`), or the command\n * did not leave the client.\n * - `indeterminate`: the command can have reached the server, and nothing tells if it applied.\n */\nexport type Sent<T> =\n | { readonly outcome: \"ok\"; readonly value: T; readonly attempts: number }\n | { readonly outcome: \"error\"; readonly error: string; readonly reply: boolean; readonly attempts: number }\n | { readonly outcome: \"indeterminate\"; readonly error: string; readonly attempts: number };\n\n/** Send options tell how the client sends one command. */\nexport interface SendOptions {\n /** This signal abandons the command. A command that left the client is then indeterminate. */\n readonly signal?: AbortSignal | undefined;\n /** False stops the client: it does not send the command again. A check that must fail at once uses it. */\n readonly reissue?: boolean | undefined;\n}\n\ntype Attempt =\n | { readonly outcome: \"ok\"; readonly value: unknown }\n | { readonly outcome: \"error\"; readonly error: string; readonly reply: boolean }\n | { readonly outcome: \"indeterminate\"; readonly error: string };\n\n/** The steps of a connection that the client gives a check for (B14). */\nexport type ClientStep = Extract<Step, \"dial\" | \"round-trip\" | \"local-state\">;\n\n/** Check options tell which checks the client gives the health runtime, and which policy each one controls. */\nexport interface CheckOptions {\n readonly poll: Poll;\n /** For each step, the policy that its check controls, or null for a check that only reports. A step that is absent has no check. */\n readonly steps: Partial<Record<ClientStep, Gate | null>>;\n}\n\n/** A Redis client is a dependency of a service: its declaration, its handle, its checks and its commands. */\nexport interface RedisClient {\n readonly name: string;\n /** This declaration goes to `svc.dependency(name, declaration, handle)`. */\n readonly declaration: Omit<Dependency, \"name\">;\n readonly handle: DependencyHandle;\n /** These are the manifestations that the client can meet, and the detector of each (E9). */\n readonly meets: readonly Meeting[];\n /** This text is the state of the connection, as ioredis names it, or \"none\" before the connect. */\n readonly status: string;\n /** These are the generations of the connection, the latest last. */\n readonly joins: readonly Join[];\n /**\n * This method sends a command, and gives its outcome in three values. The client sends again\n * only an idempotent operation, one time at most, after an outcome that is not certain. The\n * second try stays in the bound of the request. The client does not send again an operation that\n * the table does not name.\n */\n send<T = unknown>(command: string, args?: ReadonlyArray<string | number>, options?: SendOptions): Promise<Sent<T>>;\n /** This method gives the checks of the client and their functions, for `svc.check(check, run)`. */\n checks(options: CheckOptions): ReadonlyArray<readonly [Check, CheckFn]>;\n /** This method declares the client in a service: its dependency, its handle and its checks. */\n attach(svc: ServiceBuilder, options: CheckOptions): ServiceBuilder;\n /** This method gives the problems of the client in a description: a close that does not fit in its phase, for example. */\n problems(description: ServiceDescription): string[];\n}\n\nconst ms = (amount: Extent): number =>\n amount.value === \"infinite\" ? Number.POSITIVE_INFINITY : valueIn(amount as Quantity, \"ms\");\nconst finite = (amount: Extent): number | undefined => {\n const value = ms(amount);\n return Number.isFinite(value) ? value : undefined;\n};\nconst message = (error: unknown): string => (error instanceof Error ? error.message : String(error));\n\n/**\n * This function gives the backoff of the tries to connect: from `retry_base`, doubling to\n * `retry_cap`, each with a jitter up to the base. The client rounds each delay to a whole millisecond.\n */\nexport const retryBackoff = (knobs: RedisKnobs): Backoff => ({\n base: ms(knobs.retry_base),\n factor: 2,\n cap: ms(knobs.retry_cap),\n jitter: { kind: \"add\", amount: ms(knobs.retry_base) },\n});\n\n/** This function gives the problems of a spec: the knobs that the client cannot use. */\nexport const specProblems = (name: string, spec: RedisClientSpec): string[] => {\n const problems: string[] = [];\n const { knobs } = spec;\n if (!Number.isInteger(spec.heartbeatMisses) || spec.heartbeatMisses < 1)\n problems.push(`${name}'s watchdog closes the connection after ${spec.heartbeatMisses} misses, not a whole number above zero`);\n if (knobs.max_connection_age.value !== \"infinite\")\n problems.push(`${name} cannot replace its connection at an age: max_connection_age must be \"infinite\"`);\n for (const [knob, amount] of Object.entries(knobs) as Array<[string, Extent]>) {\n if (amount.value !== \"infinite\" && !(ms(amount) >= 0))\n problems.push(`${name}'s ${knob} is not a time of zero or more`);\n }\n if (ms(knobs.request_timeout) <= 0) problems.push(`${name}'s request_timeout is not above zero: each command must have a bound`);\n if (ms(knobs.heartbeat_timeout) <= 0) problems.push(`${name}'s heartbeat_timeout is not above zero`);\n if (ms(knobs.retry_cap) < ms(knobs.retry_base)) problems.push(`${name}'s retry_cap is below its retry_base`);\n return problems;\n};\n\n/** This dial function opens a TCP connection, sends an inline PING, and waits for +PONG. */\nexport const tcpDial: DialFn = (host, port, signal) =>\n new Promise((resolve, reject) => {\n if (signal.aborted) {\n reject(new Error(\"the dial was aborted\"));\n return;\n }\n const socket = dialTcp({ host, port });\n let reply = \"\";\n const end = (error?: Error): void => {\n signal.removeEventListener(\"abort\", onAbort);\n socket.destroy();\n if (error === undefined) resolve();\n else reject(error);\n };\n const onAbort = (): void => end(new Error(\"the dial was aborted\"));\n signal.addEventListener(\"abort\", onAbort, { once: true });\n socket.once(\"connect\", () => socket.write(\"PING\\r\\n\"));\n socket.on(\"data\", (chunk) => {\n reply += chunk.toString();\n if (reply.includes(\"\\r\\n\")) end(reply.startsWith(\"+PONG\") ? undefined : new Error(`the reply was ${reply.trim()}`));\n });\n socket.once(\"error\", (error) => end(error));\n socket.once(\"close\", () => end(new Error(\"the server closed the dial\")));\n });\n\n/**\n * This function makes a Redis client. It is a dependency of a service, with ioredis under it:\n * - It sets each timer of ioredis from the declaration, and it does not let ioredis send a command\n * again (R5 §8).\n * - It tells the outcome of a command in three values, and it settles each command in flight as\n * indeterminate when its connection closes (C8, C10).\n * - It records the server's id of each connection at `ready` (D1).\n * - A watchdog sends a PING in the band of the connection, and it connects again after the misses\n * of the declaration. It also starts a client that stopped (principle 19).\n */\nexport const redisClient = (name: string, spec: RedisClientSpec, options: RedisClientOptions = {}): RedisClient => {\n const problems = specProblems(name, spec);\n if (problems.length > 0) throw new Error(problems.join(\"; \"));\n const { knobs } = spec;\n const clock = options.clock ?? realClock;\n const random = options.random ?? Math.random;\n const create = options.create ?? ioredis;\n const dial = options.dial ?? tcpDial;\n const turn = options.turn ?? (() => new Promise<void>((resolve) => setImmediate(resolve)));\n const operations: Readonly<Record<string, Operation>> = { ...REDIS_OPERATIONS, ...spec.operations };\n const requestTimeout = ms(knobs.request_timeout);\n const detection = heartbeatDetection(knobs.heartbeat_interval, knobs.heartbeat_timeout, spec.heartbeatMisses);\n const backoff = retryBackoff(knobs);\n\n let connection: RedisConnection | null = null;\n let status = \"none\";\n let closing = false;\n const joins: Join[] = [];\n // the commands that the client wrote while the connection was ready: a close settles each one\n const inFlight = new Set<(attempt: Attempt) => void>();\n const readyWaiters = new Set<() => void>();\n const listeners: Array<[string, (...args: unknown[]) => void]> = [];\n\n const record = (from: string, to: string, on: string, detail?: string): void =>\n options.record?.({ at: clock.now(), machine: \"client\", instance: name, from, to, on, ...(detail === undefined ? {} : { detail }) });\n\n // the delays of the tries to connect (`retryBackoff`), and none after the retry window. The\n // watchdog starts the client again after that\n let retrySince: number | null = null;\n const retryStrategy = (times: number): number | null => {\n const now = clock.now();\n retrySince ??= now;\n if (now - retrySince > ms(knobs.client_retry_window)) {\n record(status, status, \"retry.window-ended\");\n return null;\n }\n return Math.round(backoffDelay(backoff, times, random));\n };\n\n const connectionOptions: RedisConnectionOptions = {\n host: spec.host,\n port: spec.port,\n ...(spec.db === undefined ? {} : { db: spec.db }),\n ...(spec.username === undefined ? {} : { username: spec.username }),\n ...(spec.password === undefined ? {} : { password: spec.password }),\n lazyConnect: true,\n connectTimeout: ms(knobs.connect_bound),\n commandTimeout: requestTimeout,\n socketTimeout: finite(knobs.driver_writeoff_time),\n keepAlive: finite(knobs.keepalive_idle) ?? false,\n enableOfflineQueue: false,\n autoResendUnfulfilledCommands: false,\n maxRetriesPerRequest: null,\n enableReadyCheck: true,\n connectionName: name,\n retryStrategy,\n };\n\n /** This function gives the outcome of a failed command: a reply of the server, a command that did not leave the client, or neither. */\n const failed = (error: unknown): Attempt => {\n const text = message(error);\n if (error instanceof Error && error.name === \"ReplyError\") return { outcome: \"error\", error: text, reply: true };\n if (/isn't writeable|enableOfflineQueue/i.test(text)) return { outcome: \"error\", error: text, reply: false };\n return { outcome: \"indeterminate\", error: text };\n };\n\n const onReady = async (): Promise<void> => {\n retrySince = null;\n const c = connection;\n if (c === null) return;\n const generation = joins.length + 1;\n const at = clock.now();\n const socket = c.stream;\n let clientId: number | null = null;\n try {\n const reply = await c.call(\"CLIENT\", \"ID\");\n clientId = typeof reply === \"number\" ? reply : Number(reply);\n if (!Number.isFinite(clientId)) clientId = null;\n } catch {\n clientId = null;\n }\n // a connection that closed before its reply is a generation that never served\n if (connection !== c || c.status !== \"ready\") return;\n joins.push({\n generation,\n at,\n clientId,\n localAddress: socket?.localAddress ?? null,\n localPort: socket?.localPort ?? null,\n remoteAddress: socket?.remoteAddress ?? null,\n remotePort: socket?.remotePort ?? null,\n });\n record(\"ready\", \"ready\", \"join\", `generation ${generation}, client id ${clientId ?? \"unknown\"}`);\n for (const wake of [...readyWaiters]) wake();\n };\n\n const onStatus = (to: string) => (): void => {\n const from = status;\n status = to;\n record(from, to, `status.${to}`);\n if (to === \"close\" || to === \"end\") {\n // the lower layer gave up the connection: each command in flight learns it now (C10)\n for (const settle of [...inFlight]) settle({ outcome: \"indeterminate\", error: `the connection closed with the command in flight (${from})` });\n }\n if (to === \"ready\") void onReady();\n };\n\n const watch = (c: RedisConnection): void => {\n for (const state of [\"connecting\", \"connect\", \"ready\", \"close\", \"reconnecting\", \"end\"]) {\n const listener = onStatus(state);\n c.on(state, listener);\n listeners.push([state, listener]);\n }\n // an error of the connection comes before its close, which settles the commands: here it is only recorded\n const onError = (error: unknown): void => record(status, status, \"error\", message(error));\n c.on(\"error\", onError);\n listeners.push([\"error\", onError]);\n };\n\n /** This function waits until the connection is ready, for `within` milliseconds at most. It gives true when it is ready. */\n const waitReady = (within: number, signal?: AbortSignal): Promise<boolean> =>\n new Promise((resolve) => {\n if (connection?.status === \"ready\") {\n resolve(true);\n return;\n }\n let cancel: Cancel = () => {};\n const done = (value: boolean): void => {\n readyWaiters.delete(wake);\n cancel();\n signal?.removeEventListener(\"abort\", aborted);\n resolve(value);\n };\n const wake = (): void => done(true);\n const aborted = (): void => done(false);\n readyWaiters.add(wake);\n cancel = clock.after(within, () => done(connection?.status === \"ready\"));\n signal?.addEventListener(\"abort\", aborted, { once: true });\n });\n\n const attempt = (command: string, args: ReadonlyArray<string | number>, signal?: AbortSignal): Promise<Attempt> => {\n const c = connection;\n if (c === null) return Promise.resolve({ outcome: \"error\", error: `${name} has not connected`, reply: false });\n if (c.status !== \"ready\") return Promise.resolve({ outcome: \"error\", error: `${name} is ${c.status}, not ready: the command did not leave the client`, reply: false });\n if (signal?.aborted) return Promise.resolve({ outcome: \"error\", error: \"the caller aborted the command before it left the client\", reply: false });\n return new Promise<Attempt>((resolve) => {\n let settled = false;\n const settle = (value: Attempt): void => {\n if (settled) return;\n settled = true;\n inFlight.delete(settle);\n signal?.removeEventListener(\"abort\", aborted);\n resolve(value);\n };\n // a sent command cannot be cancelled: the caller abandons it, and its outcome is not known (principle 9)\n const aborted = (): void => settle({ outcome: \"indeterminate\", error: \"the caller abandoned the command after it was sent\" });\n inFlight.add(settle);\n signal?.addEventListener(\"abort\", aborted, { once: true });\n c.call(command, ...args).then(\n (value) => settle({ outcome: \"ok\", value }),\n (error: unknown) => settle(failed(error)),\n );\n });\n };\n\n const send = async <T>(command: string, args: ReadonlyArray<string | number> = [], sendOptions: SendOptions = {}): Promise<Sent<T>> => {\n const { signal } = sendOptions;\n const operation = sendOptions.reissue === false ? undefined : operations[commandName(command, args)];\n const started = clock.now();\n const first = await attempt(command, args, signal);\n if (first.outcome === \"ok\") return { outcome: \"ok\", value: first.value as T, attempts: 1 };\n // a reply of the server is final, and an operation that the table does not name is never sent again\n const again =\n !(first.outcome === \"error\" && first.reply) &&\n operation !== undefined &&\n signal?.aborted !== true &&\n mayReissue(operation, quantity((clock.now() - started) / 1000, \"s\"));\n const left = requestTimeout - (clock.now() - started);\n if (!again || left <= 0 || !(await waitReady(left, signal))) return { ...first, attempts: 1 };\n record(status, status, \"command.reissue\", commandName(command, args));\n // the second attempt ends at the bound of the request, not one command timeout after it starts\n const bound = new AbortController();\n const end = clock.after(Math.max(0, requestTimeout - (clock.now() - started)), () => bound.abort());\n const forward = (): void => bound.abort();\n signal?.addEventListener(\"abort\", forward, { once: true });\n const second = await attempt(command, args, bound.signal);\n end();\n signal?.removeEventListener(\"abort\", forward);\n return second.outcome === \"ok\" ? { outcome: \"ok\", value: second.value as T, attempts: 2 } : { ...second, attempts: 2 };\n };\n\n // the watchdog: a heartbeat of PINGs on the connection of the application, each interval from the\n // start of the one before. A reply, an error reply too, is an answer. It waits one turn of the\n // event loop before it counts a miss, thus a slow loop is not a silent server. It acts only on a\n // ready connection, thus it never closes a connection in the middle of a new attempt\n let watchdog: Heartbeat | null = null;\n const startWatchdog = (): Heartbeat =>\n heartbeat({\n interval: ms(knobs.heartbeat_interval),\n timeout: ms(knobs.heartbeat_timeout),\n misses: spec.heartbeatMisses,\n clock,\n turn,\n // a PING starts at each tick, also while an earlier PING waits\n overlap: true,\n due: () => {\n const c = connection;\n if (c === null || closing) return false;\n if (c.status === \"end\") {\n // the retry window ended: the watchdog starts the client again\n record(status, status, \"watchdog.revive\");\n retrySince = null;\n c.connect().catch(() => {});\n return false;\n }\n return c.status === \"ready\";\n },\n ping: (signal) =>\n attempt(\"PING\", [], signal).then((result) => result.outcome === \"ok\" || (result.outcome === \"error\" && result.reply)),\n live: () => connection?.status === \"ready\" && !closing,\n onMiss: (misses) => record(status, status, \"watchdog.miss\", `${misses} of ${spec.heartbeatMisses}`),\n onDead: () => {\n record(status, status, \"watchdog.reconnect\");\n connection?.disconnect(true);\n },\n });\n\n const handle: DependencyHandle = {\n async connect(signal) {\n closing = false;\n const c = await create(connectionOptions);\n connection = c;\n watch(c);\n if (signal.aborted) throw new Error(`${name}: the service stopped before the connect`);\n // a server that is down at the start does not stop the startup: the client connects again in\n // the background, and its checks keep readiness tripped until it is ready (readiness is earned)\n await c.connect().catch((error: unknown) => record(status, status, \"connect.failed\", message(error)));\n watchdog?.stop();\n watchdog = startWatchdog();\n },\n async close() {\n closing = true;\n watchdog?.stop();\n watchdog = null;\n const c = connection;\n if (c === null) return;\n // a QUIT lets the replies in flight arrive. A silent server never answers it, thus the close has its bound\n const quit = c.quit().then(\n () => \"quit\" as const,\n () => \"quit\" as const,\n );\n let cancel: Cancel = () => {};\n const bound = new Promise<\"bound\">((resolve) => {\n cancel = clock.after(ms(knobs.close_bound), () => resolve(\"bound\"));\n });\n const startedClose = clock.now();\n const first = await Promise.race([quit, bound]);\n cancel();\n if (first === \"bound\") record(status, status, \"close.bound\");\n // a client that is not ready refuses the QUIT at once, and it would connect again: the close stops it\n if (c.status !== \"end\") c.disconnect(false);\n // ioredis is at `end` when its socket closes, a moment after the reply: the close waits for it, in its bound\n const ended = (within: number): Promise<void> =>\n c.status === \"end\"\n ? Promise.resolve()\n : new Promise<void>((resolve) => {\n let stop: Cancel = () => {};\n const end = (): void => {\n c.off(\"end\", end);\n stop();\n resolve();\n };\n c.on(\"end\", end);\n stop = clock.after(Math.max(0, within), end);\n });\n await ended(ms(knobs.close_bound) - (clock.now() - startedClose));\n // a disconnect only half-closes the socket, and a silent server never closes its half: the bound destroys it\n if (c.status !== \"end\") {\n record(status, status, \"close.destroy\");\n c.stream?.destroy?.();\n await ended(0);\n }\n for (const [event, listener] of listeners) c.off(event, listener);\n listeners.length = 0;\n for (const settle of [...inFlight]) settle({ outcome: \"indeterminate\", error: \"the client closed with the command in flight\" });\n status = \"end\";\n },\n };\n\n const declaration: Omit<Dependency, \"name\"> = {\n system: spec.system,\n knobs: {\n connect_bound: knobs.connect_bound,\n request_timeout: knobs.request_timeout,\n driver_writeoff_time: knobs.driver_writeoff_time,\n keepalive_idle: knobs.keepalive_idle,\n heartbeat_interval: knobs.heartbeat_interval,\n heartbeat_timeout: knobs.heartbeat_timeout,\n heartbeat_detection_time: detection,\n client_retry_window: knobs.client_retry_window,\n retry_base: knobs.retry_base,\n retry_cap: knobs.retry_cap,\n close_bound: knobs.close_bound,\n max_connection_age: knobs.max_connection_age,\n retrans_giveup_time: knobs.retrans_giveup_time,\n },\n operations,\n };\n\n const checks = (checkOptions: CheckOptions): ReadonlyArray<readonly [Check, CheckFn]> => {\n const made: Array<readonly [Check, CheckFn]> = [];\n const declare = (step: ClientStep, ownConnection: boolean, run: CheckFn): void => {\n if (!(step in checkOptions.steps)) return;\n made.push([\n { name: `${name}.${step}`, dependency: name, step, ownConnection, gates: checkOptions.steps[step] ?? null, poll: checkOptions.poll },\n run,\n ]);\n };\n // the state of the client, without a round trip\n declare(\"local-state\", true, () => (connection?.status === \"ready\" ? \"ok\" : \"error\"));\n // a PING on the connection of the application: blind to nothing that the application meets\n declare(\"round-trip\", true, async (signal): Promise<Outcome> => {\n // a check measures the step now: it does not wait for a new connection, and it sends nothing again\n const result = await send(\"PING\", [], { signal, reissue: false });\n return result.outcome === \"ok\" ? \"ok\" : \"error\";\n });\n // a new connection each time: it finds a server that refuses connections, but not a stalled connection of the application\n declare(\"dial\", false, async (signal): Promise<Outcome> => {\n await dial(spec.host, spec.port, signal);\n return \"ok\";\n });\n return made;\n };\n\n return {\n name,\n declaration,\n handle,\n meets: meetings(name, knobs, detection),\n get status() {\n return connection === null ? \"none\" : connection.status;\n },\n joins,\n send,\n checks,\n attach(svc, checkOptions) {\n svc.dependency(name, declaration, handle);\n for (const [check, run] of checks(checkOptions)) svc.check(check, run);\n return svc;\n },\n problems(description) {\n const found: string[] = [];\n const teardown = description.shutdown.phases.find((phase) => phase.name === \"tear down dependencies\");\n if (teardown === undefined) found.push(`${name} needs the phase \"tear down dependencies\" to close`);\n else if (ms(knobs.close_bound) >= valueIn(teardown.cap, \"ms\"))\n found.push(`${name}'s close_bound of ${ms(knobs.close_bound)} ms does not fit in the cap of \"tear down dependencies\"`);\n for (const check of description.checks) {\n if (check.dependency !== name) continue;\n const slowest = valueIn(check.poll.timeout, \"ms\");\n if (check.step === \"round-trip\" && requestTimeout > slowest + valueIn(check.poll.interval, \"ms\") * (1 + check.poll.jitter))\n found.push(`${name}'s request_timeout outlives one jittered poll of ${check.name} (B7, O13)`);\n }\n return found;\n },\n };\n};\n","import type { Machine, TransitionDef } from \"@mark1russell7/algebra\";\n\nconst t = (id: string, from: string | readonly string[], on: string | readonly string[], to: string, extra: Partial<TransitionDef> = {}): TransitionDef => ({\n id,\n from,\n on,\n to,\n ...extra,\n});\nconst knob = (key: string) => ({ key, scope: \"client\", unit: \"s\", default: null }) as const;\n\n/** The states where the connection exists or tries to exist, thus where it can close. */\nconst LIVE = [\"connecting\", \"connect\", \"ready\", \"reconnecting\"] as const;\nconst ANY = [\"none\", \"connecting\", \"connect\", \"ready\", \"close\", \"reconnecting\", \"end\"] as const;\n\n/**\n * The Redis client as a `mark1russell7.machine/1` document. Its states are the states of ioredis.\n * The wrapper adds its own events: the join at each `ready`, and the misses of its watchdog. It also\n * adds a command that it sends again, and the bound of its close. A test records the transitions of the\n * client and replays them against this document.\n *\n * The state `ready` hides states that the client cannot see (D4). In a black hole, ioredis stays\n * `ready` while the kernel retransmits, until `driver_writeoff_time` or the watchdog ends that\n * hidden interval. A timer of this document bounds each hidden interval.\n */\nexport const REDIS_CLIENT_MACHINE: Machine = {\n format: \"mark1russell7.machine/1\",\n id: \"service.machine.redis-client\",\n title: \"The Redis client of the service, over ioredis 6\",\n layer: \"driver\",\n version: \"0.2.0\",\n sources: [{ ref: \"packages/service/src/redis (the client implements this document)\", rung: \"implemented\" }],\n states: {\n none: { initial: true, note: \"the client exists, and its connection does not\" },\n connecting: { note: \"a TCP connection attempt\" },\n connect: { note: \"the handshake: HELLO, then the ready check (INFO)\" },\n ready: { note: \"it can hide a black hole or a frozen server, until a timer of the client ends it\" },\n close: { note: \"the socket closed. Each command in flight settles as indeterminate\" },\n reconnecting: { note: \"the delay of the retry strategy, which doubles to its cap\" },\n end: { note: \"the retry window ended, or the service closed the client. The watchdog starts it again\" },\n },\n events: {\n \"status.connecting\": { kind: \"local\" },\n \"status.connect\": { kind: \"rx\" },\n \"status.ready\": { kind: \"rx\" },\n \"status.close\": { kind: \"rx\" },\n \"status.reconnecting\": { kind: \"timer\" },\n \"status.end\": { kind: \"local\" },\n join: { kind: \"rx\", note: \"the reply of CLIENT ID at ready (D1)\" },\n error: { kind: \"rx\" },\n \"connect.failed\": { kind: \"local\" },\n \"command.written\": { kind: \"tx\" },\n \"command.reissue\": { kind: \"local\" },\n data: { kind: \"rx\" },\n heartbeat: { kind: \"timer\" },\n \"watchdog.miss\": { kind: \"local\" },\n \"watchdog.reconnect\": { kind: \"local\" },\n \"watchdog.revive\": { kind: \"local\" },\n \"retry.window-ended\": { kind: \"local\" },\n \"close.request\": { kind: \"local\" },\n \"close.bound\": { kind: \"timer\" },\n \"close.destroy\": { kind: \"local\", note: \"the bound destroys a socket that a silent server does not close\" },\n },\n knobs: {\n connect_bound: knob(\"connectTimeout\"),\n driver_writeoff_time: knob(\"socketTimeout\"),\n heartbeat_interval: knob(\"watchdog.interval\"),\n close_bound: knob(\"close.bound\"),\n },\n timers: {\n connect_timeout: {\n duration: { knob: \"connect_bound\", unit: \"s\" },\n arms: [\"enter:connecting\"],\n cancels: [\"status.connect\", \"status.close\"],\n fires: \"status.close\",\n note: \"ioredis destroys the attempt\",\n },\n writeoff: {\n duration: { knob: \"driver_writeoff_time\", unit: \"s\" },\n arms: [\"command.written\"],\n cancels: [\"data\", \"status.close\"],\n fires: \"status.close\",\n note: \"ioredis destroys a socket that gives no data while a command waits: it ends the hidden interval of a black hole\",\n },\n heartbeat: {\n duration: { knob: \"heartbeat_interval\", unit: \"s\" },\n arms: [\"enter:ready\", \"heartbeat\"],\n cancels: [\"status.close\", \"status.end\"],\n fires: \"heartbeat\",\n note: \"a PING on the connection of the application\",\n },\n close: {\n duration: { knob: \"close_bound\", unit: \"s\" },\n arms: [\"close.request\"],\n cancels: [\"status.end\"],\n fires: \"close.bound\",\n },\n },\n transitions: [\n t(\"connect\", [\"none\", \"reconnecting\", \"end\", \"close\"], \"status.connecting\", \"connecting\"),\n t(\"tcp\", \"connecting\", \"status.connect\", \"connect\"),\n t(\"ready\", \"connect\", \"status.ready\", \"ready\"),\n t(\"close\", [...LIVE], \"status.close\", \"close\"),\n t(\"retry\", \"close\", \"status.reconnecting\", \"reconnecting\"),\n t(\"end\", [...LIVE, \"close\"], \"status.end\", \"end\"),\n t(\"join\", \"ready\", \"join\", \"ready\"),\n t(\"miss\", \"ready\", \"watchdog.miss\", \"ready\"),\n t(\"watchdog-reconnect\", \"ready\", \"watchdog.reconnect\", \"ready\"),\n t(\"revive\", \"end\", \"watchdog.revive\", \"end\"),\n // the retry strategy answers in `close`, before ioredis moves to `reconnecting` or `end`\n t(\"window-ended\", \"close\", \"retry.window-ended\", \"close\"),\n ...ANY.map((state) => t(`error-${state}`, state, \"error\", state)),\n ...ANY.map((state) => t(`connect-failed-${state}`, state, \"connect.failed\", state)),\n ...ANY.map((state) => t(`reissue-${state}`, state, \"command.reissue\", state)),\n ...ANY.map((state) => t(`close-bound-${state}`, state, \"close.bound\", state, { timer: \"close\" })),\n ...ANY.map((state) => t(`close-destroy-${state}`, state, \"close.destroy\", state)),\n ],\n};\n"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
- import { C as ProcessPort, M as Outcome, R as Clock, S as ExitSignal, j as CheckFn, n as ServiceBuilder } from "./builder-BFxOuULH.js";
1
+ import { n as Clock } from "./clock-D-38uH_z.js";
2
+ import { C as ProcessPort, M as Outcome, S as ExitSignal, j as CheckFn, n as ServiceBuilder } from "./builder-DnfUDW9u.js";
2
3
  //#region src/fakes.d.ts
3
4
  /** A fake clock moves only when a test tells it to. */
4
5
  interface FakeClock extends Clock {
package/build/testing.js CHANGED
@@ -1,4 +1,4 @@
1
- import { i as recoverSpan, r as failSpan } from "./timing-DxZ4nkQg.js";
1
+ import { a as recoverSpan, r as failSpan } from "./timing-DGRi3XCI.js";
2
2
  import { valueIn } from "@mark1russell7/quantity";
3
3
  import { createServer } from "node:net";
4
4
  //#region src/fakes.ts
@@ -1,4 +1,5 @@
1
- import { convertExact, quantity, rationals } from "@mark1russell7/quantity";
1
+ import { detectionTime } from "./heartbeat.js";
2
+ import { convertExact, quantity, rationals, valueIn } from "@mark1russell7/quantity";
2
3
  //#region src/timing.ts
3
4
  const seconds = (amount) => convertExact(rationals.fromNumber(amount.value), amount.unit, "s");
4
5
  const out = (value) => quantity(rationals.toNumber(value), "s");
@@ -73,7 +74,15 @@ const asDebounce = (timing, poll) => {
73
74
  recoverAfter: polls(timing.recoverAfter === void 0 ? void 0 : seconds(timing.recoverAfter))
74
75
  };
75
76
  };
77
+ /**
78
+ * This function gives the most time that a heartbeat takes to find a silent peer, as an amount
79
+ * for the ordering law (`detectionTime`). An interval of "infinite" is a heartbeat that is off.
80
+ */
81
+ const heartbeatDetection = (interval, timeout, misses) => interval.value === "infinite" ? {
82
+ value: "infinite",
83
+ unit: "s"
84
+ } : quantity(detectionTime(valueIn(interval, "ms"), valueIn(timeout, "ms"), misses) / 1e3, "s");
76
85
  //#endregion
77
- export { recoverSpan as i, asWindow as n, failSpan as r, asDebounce as t };
86
+ export { recoverSpan as a, heartbeatDetection as i, asWindow as n, failSpan as r, asDebounce as t };
78
87
 
79
- //# sourceMappingURL=timing-DxZ4nkQg.js.map
88
+ //# sourceMappingURL=timing-DGRi3XCI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timing-DGRi3XCI.js","names":["r"],"sources":["../src/timing.ts"],"sourcesContent":["import type { Extent, Quantity, Rational } from \"@mark1russell7/quantity\";\nimport { convertExact, quantity, rationals as r, valueIn } from \"@mark1russell7/quantity\";\nimport { detectionTime } from \"./heartbeat.ts\";\n\n/**\n * All policies have the same timing shape. A timing is a count of polls (a debounce) or a\n * duration (a window), and each converts to the other. A long window is easier to read as a\n * duration, and that is the only difference between the two.\n */\nexport type Timing =\n | {\n readonly type: \"debounce\";\n /** This interval is the time between two polls of the signal. */\n readonly interval: Quantity;\n /** This count is the number of failed polls in sequence before the policy fails. */\n readonly failAfter: number;\n /** This count is the number of passed polls in sequence before the policy recovers. */\n readonly recoverAfter: number;\n }\n | {\n readonly type: \"window\";\n /** This duration is how long the signal fails before the policy fails, on the monotonic clock. */\n readonly failAfter: Quantity;\n /** This duration is how long the signal passes before the policy recovers. Without it, the first pass recovers the policy. */\n readonly recoverAfter?: Quantity | undefined;\n };\n\n/** A poll has an interval, a jitter as a fraction (±20 % is 0.2) and a timeout. */\nexport interface Poll {\n readonly interval: Quantity;\n readonly jitter: number;\n readonly timeout: Quantity;\n}\n\n/** A span is a range of times, from the earliest to the latest. */\nexport interface Span {\n readonly min: Quantity;\n readonly max: Quantity;\n}\n\n/**\n * This type tells how a failed step answers a poll.\n * - An `\"error\"` step refuses at once.\n * - A `\"timeout\"` step (a black hole) answers only when the timeout of the poll ends. Each poll\n * makes a new call, and each call ends at its timeout. Thus the step is slower by that timeout,\n * at the two ends of the span.\n * - A `\"hang\"` step makes a use that does not end. The first poll fails at its timeout. While the\n * use stays pending, the runtime does not start the check again, and each later poll fails at\n * once. Thus a debounce of two polls or more fails as fast as on an error.\n */\nexport type FailsBy = \"error\" | \"timeout\" | \"hang\";\n\nconst seconds = (amount: Quantity): Rational =>\n convertExact(r.fromNumber(amount.value), amount.unit, \"s\");\nconst out = (value: Rational): Quantity => quantity(r.toNumber(value), \"s\");\nconst fraction = (jitter: number): Rational => {\n if (!(jitter >= 0 && jitter < 1))\n throw new RangeError(`jitter is a fraction below 1, not ${jitter}`);\n return r.fromNumber(jitter);\n};\nconst count = (polls: number, what: string): Rational => {\n if (!Number.isInteger(polls) || polls < 1)\n throw new RangeError(`${what} is a whole number of polls, not ${polls}`);\n return r.rational(BigInt(polls));\n};\n\n/** N polls with jitter take from N − 1 short intervals to N long intervals. */\nconst debounced = (polls: Rational, interval: Rational, jitter: Rational): [Rational, Rational] => [\n r.mul(r.sub(polls, r.ONE), r.mul(interval, r.sub(r.ONE, jitter))),\n r.mul(polls, r.mul(interval, r.add(r.ONE, jitter))),\n];\n\n/**\n * This function gives the time that a policy takes to fail after its signal becomes bad. A\n * debounce of N polls takes approximately N intervals, with the jitter. A window takes its\n * duration, plus one interval at most for the poll that starts its sequence. A step that fails by\n * a timeout adds the timeout of the poll. At 3 polls of 5 s ± 20 %, the latch fails in 8–18 s on\n * an error, and in 12–22 s on a 4 s timeout. A step that hangs adds the timeout only where its\n * first failure ends the sequence: a window, or a debounce of one poll.\n */\nexport const failSpan = (timing: Timing, poll: Poll, by: FailsBy = \"error\"): Span => {\n const jitter = fraction(poll.jitter);\n const first = timing.type === \"window\" || timing.failAfter === 1;\n const late =\n by === \"timeout\" || (by === \"hang\" && first) ? seconds(poll.timeout) : r.ZERO;\n if (timing.type === \"debounce\") {\n const [min, max] = debounced(\n count(timing.failAfter, \"failAfter\"),\n seconds(timing.interval),\n jitter,\n );\n return { min: out(r.add(min, late)), max: out(r.add(max, late)) };\n }\n const duration = seconds(timing.failAfter);\n const interval = r.mul(seconds(poll.interval), r.add(r.ONE, jitter));\n return { min: out(r.add(duration, late)), max: out(r.add(r.add(duration, interval), late)) };\n};\n\n/** This function gives the time that a policy takes to recover after its signal becomes good. */\nexport const recoverSpan = (timing: Timing, poll: Poll): Span => {\n const jitter = fraction(poll.jitter);\n if (timing.type === \"debounce\") {\n const [min, max] = debounced(\n count(timing.recoverAfter, \"recoverAfter\"),\n seconds(timing.interval),\n jitter,\n );\n return { min: out(min), max: out(max) };\n }\n const duration = timing.recoverAfter === undefined ? r.ZERO : seconds(timing.recoverAfter);\n const interval = r.mul(seconds(poll.interval), r.add(r.ONE, jitter));\n return { min: out(duration), max: out(r.add(duration, interval)) };\n};\n\n/** This function gives the window that a debounce approximates: N polls of an interval fail after N intervals. */\nexport const asWindow = (timing: Timing): Timing =>\n timing.type === \"window\"\n ? timing\n : {\n type: \"window\",\n failAfter: out(r.mul(count(timing.failAfter, \"failAfter\"), seconds(timing.interval))),\n recoverAfter: out(\n r.mul(count(timing.recoverAfter, \"recoverAfter\"), seconds(timing.interval)),\n ),\n };\n\n/** This function gives the debounce that approximates a window on a poll: the polls that fit, rounded up. */\nexport const asDebounce = (timing: Timing, poll: Poll): Timing => {\n if (timing.type === \"debounce\") return timing;\n const interval = seconds(poll.interval);\n const polls = (duration: Rational | undefined): number =>\n duration === undefined ? 1 : Math.max(1, Number(r.ceil(r.div(duration, interval)).n));\n return {\n type: \"debounce\",\n interval: poll.interval,\n failAfter: polls(seconds(timing.failAfter)),\n recoverAfter: polls(\n timing.recoverAfter === undefined ? undefined : seconds(timing.recoverAfter),\n ),\n };\n};\n\n/**\n * This function gives the most time that a heartbeat takes to find a silent peer, as an amount\n * for the ordering law (`detectionTime`). An interval of \"infinite\" is a heartbeat that is off.\n */\nexport const heartbeatDetection = (interval: Extent, timeout: Quantity, misses: number): Extent =>\n interval.value === \"infinite\"\n ? { value: \"infinite\", unit: \"s\" }\n : quantity(detectionTime(valueIn(interval as Quantity, \"ms\"), valueIn(timeout, \"ms\"), misses) / 1000, \"s\");\n"],"mappings":"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"}