@mark1russell7/service 0.5.11 → 0.5.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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/limiter.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 { Clock } from \"../clock.ts\";\nimport type { Decision } from \"../limiter/policy.ts\";\nimport type { LimiterStore } from \"../limiter/store.ts\";\nimport type { RedisClient } from \"./client.ts\";\n\n/**\n * This script decides one take in one atomic step of Redis. It does the arithmetic of `decide`, and\n * it reads `TIME` of the server, the one clock of all replicas. A test can give the time in its place.\n * - KEYS[1] is the state of the key. KEYS[2] is the index of the limiter, a sorted set of its keys\n * by the time at which each key is free.\n * - ARGV is the type of the rule, its two numbers, the cap of keys, the key, and the time or \"\".\n * - The state of a bucket is its full time. The state of a window is the time of each take in it.\n * - The state expires at its free time (PX), and the index lets the cap count only busy keys.\n * - The reply is `{1, remaining}`, or `{0, retryAfter, cause}` with the time as text. Redis gives\n * each Lua number as a whole number, thus the fraction of a millisecond goes as text.\n */\nexport const LIMITER_SCRIPT = `\nlocal kind = ARGV[1]\nlocal a = tonumber(ARGV[2])\nlocal b = tonumber(ARGV[3])\nlocal cap = tonumber(ARGV[4])\nlocal member = ARGV[5]\nlocal now\nif ARGV[6] == '' then\n local t = redis.call('TIME')\n now = tonumber(t[1]) * 1000 + tonumber(t[2]) / 1000\nelse\n now = tonumber(ARGV[6])\nend\nlocal TOL = 1e-6\nlocal function num(x) return string.format('%.6f', x) end\nlocal raw = redis.call('GET', KEYS[1])\nlocal full = nil\nlocal stamps = nil\nif raw then\n if kind == 'token-bucket' then\n full = tonumber(raw)\n else\n stamps = {}\n for x in string.gmatch(raw, '[^,]+') do stamps[#stamps + 1] = tonumber(x) end\n end\nend\nlocal function freeAt()\n if kind == 'token-bucket' then return full end\n if #stamps == 0 then return -math.huge end\n return stamps[#stamps] + b\nend\nif raw and freeAt() <= now then\n redis.call('DEL', KEYS[1])\n redis.call('ZREM', KEYS[2], member)\n raw = nil\n full = nil\n stamps = nil\nend\nif not raw then\n if redis.call('ZCARD', KEYS[2]) >= cap then\n redis.call('ZREMRANGEBYSCORE', KEYS[2], '-inf', num(now))\n if redis.call('ZCARD', KEYS[2]) >= cap then\n local first = redis.call('ZRANGE', KEYS[2], 0, 0, 'WITHSCORES')\n return {0, num(tonumber(first[2]) - now), 'cap'}\n end\n end\nend\nlocal remaining\nlocal state\nif kind == 'token-bucket' then\n local span = a * b\n local base = now\n if full and full > now then base = full end\n local nxt = base + a\n if nxt - now > span + TOL then\n return {0, num(nxt - span - now), 'limit'}\n end\n remaining = math.floor((span - (nxt - now)) / a + TOL)\n if remaining < 0 then remaining = 0 end\n full = nxt\n state = num(full)\nelse\n local live = {}\n for _, stamp in ipairs(stamps or {}) do\n if stamp + b > now + TOL then live[#live + 1] = stamp end\n end\n if #live >= a then\n return {0, num(live[1] + b - now), 'limit'}\n end\n live[#live + 1] = now\n remaining = a - #live\n stamps = live\n local parts = {}\n for i, stamp in ipairs(live) do parts[i] = num(stamp) end\n state = table.concat(parts, ',')\nend\nlocal free = freeAt()\nlocal ttl = math.ceil(free - now)\nif ttl < 1 then ttl = 1 end\nredis.call('SET', KEYS[1], state, 'PX', ttl)\nredis.call('ZADD', KEYS[2], num(free), member)\nif redis.call('PTTL', KEYS[2]) < ttl then redis.call('PEXPIRE', KEYS[2], ttl) end\nreturn {1, remaining}\n`;\n\n/** These options make a store of a limiter in Redis. */\nexport interface RedisLimiterOptions {\n /** This text starts the name of each key of the store in Redis, for example `limit:`. It has no braces. */\n readonly prefix: string;\n /**\n * This clock replaces `TIME` of the server, for a test that compares the store with `decide`. Without\n * it, the script reads `TIME`, thus each replica gets the one clock of the store.\n */\n readonly clock?: Clock | undefined;\n}\n\n/** A Redis client part is the part of a Redis client that the store uses: `send`. */\nexport type RedisSender = Pick<RedisClient, \"send\">;\n\n/**\n * This function makes a store that the replicas share, in Redis 7. Each take is one `EVAL` of\n * `LIMITER_SCRIPT`. The client does not send it again, because a second take counts two times.\n * - The names of the keys have the limiter in braces, a hash tag. Thus the state and the index of a\n * limiter are in one slot of a Redis Cluster.\n * - Each state expires at the time that the key is free, thus the eviction is exact, as in the memory\n * store. The index of the limiter bounds the count of busy keys to `maxKeys`.\n * - A reply of an error, an outcome that is not certain, or a reply of another shape rejects the\n * take of the store. `keyedLimiter` then applies the `onStoreFailure` of the spec.\n * - `TIME` is the wall clock of the Redis host, not a monotonic clock. Thus a step of that clock\n * moves the counts of each key. The replicas still share one clock.\n */\nexport const redisLimiterStore = (client: RedisSender, options: RedisLimiterOptions): LimiterStore => {\n if (/[{}]/.test(options.prefix)) throw new Error(`the prefix ${options.prefix} of a limiter store has a brace: the store puts the hash tag in it`);\n return {\n kind: \"shared\",\n async take({ limiter, key, rule, maxKeys }): Promise<Decision> {\n const base = `${options.prefix}{${limiter}}`;\n const numbers = rule.type === \"token-bucket\" ? [rule.interval, rule.burst] : [rule.limit, rule.window];\n const now = options.clock === undefined ? \"\" : String(options.clock.now());\n const sent = await client.send<unknown[]>(\"EVAL\", [\n LIMITER_SCRIPT,\n 2,\n `${base}:key:${key}`,\n `${base}:keys`,\n rule.type,\n String(numbers[0]),\n String(numbers[1]),\n String(maxKeys),\n key,\n now,\n ]);\n if (sent.outcome !== \"ok\") throw new Error(`the limiter store ${limiter} failed (${sent.outcome}): ${sent.error}`);\n return replyOf(limiter, sent.value);\n },\n };\n};\n\n/** This function gives the decision of a reply of `LIMITER_SCRIPT`. A reply of another shape throws. */\nconst replyOf = (limiter: string, reply: unknown): Decision => {\n if (Array.isArray(reply) && Number(reply[0]) === 1 && reply.length === 2) return { allowed: true, remaining: Number(reply[1]) };\n if (Array.isArray(reply) && Number(reply[0]) === 0 && reply.length === 3 && (reply[2] === \"limit\" || reply[2] === \"cap\")) {\n const retryAfter = Number(reply[1]);\n if (Number.isFinite(retryAfter) && retryAfter > 0) return { allowed: false, retryAfter: { value: retryAfter, unit: \"ms\" }, cause: reply[2] };\n }\n throw new Error(`the limiter store ${limiter} gave a reply that is not a decision: ${JSON.stringify(reply)}`);\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":["knob","t"],"sources":["../src/redis/connection.ts","../src/redis/meets.ts","../src/redis/operations.ts","../src/redis/client.ts","../src/redis/limiter.ts","../src/redis/machine.ts"],"sourcesContent":["import type { ClientSocket, Factory } from \"../client/core.ts\";\n\n/**\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 socket of a client in the core: the join of the prober and the bounded close read it. */\nexport type RedisSocket = ClientSocket;\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 = Factory<RedisConnectionOptions, 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 } from \"@mark1russell7/quantity\";\nimport type { Detector, Meeting, Situation } from \"../client/meets.ts\";\nimport { WHAT, immediate, keepaliveAnswered, keepaliveReset, keepaliveSilent, kernelGiveUp } from \"../client/meets.ts\";\nimport type { RedisKnobs } from \"./client.ts\";\n\n// the shapes and the queries moved to the core of the clients: this entry keeps them for the users of 0.5\nexport type { Detector, Meeting, Situation } from \"../client/meets.ts\";\nexport { firstDetector, unwatched } from \"../client/meets.ts\";\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 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 = kernelGiveUp(knobs.retrans_giveup_time);\n const silent = (manifestation: \"blackhole\" | \"freeze-host\"): Meeting => ({\n manifestation,\n what: WHAT[manifestation],\n detectors: [command, writeoff, watchdog(\"in flight\"), giveUp, watchdog(\"idle\"), keepaliveSilent(knobs.keepalive_idle)],\n });\n const acknowledged = (manifestation: string, what: string): Meeting => ({\n manifestation,\n what,\n detectors: [command, writeoff, watchdog(\"in flight\"), watchdog(\"idle\"), keepaliveAnswered()],\n });\n return [\n {\n manifestation: \"rst\",\n what: WHAT.rst,\n detectors: [\n immediate(\"in flight\", \"the command fails, and the client connects again, and each attempt is refused\"),\n watchdog(\"idle\"),\n keepaliveReset(knobs.keepalive_idle),\n ],\n },\n {\n manifestation: \"fin\",\n what: WHAT.fin,\n detectors: [immediate(\"in flight\", \"the close arrives, and the command fails\"), immediate(\"idle\", \"the close arrives\")],\n },\n silent(\"blackhole\"),\n silent(\"freeze-host\"),\n acknowledged(\"freeze-process\", WHAT[\"freeze-process\"]),\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","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 type { Extent, Quantity } from \"@mark1russell7/quantity\";\nimport { backoffDelay } from \"../backoff.ts\";\nimport type { Cancel } from \"../clock.ts\";\nimport { realClock } from \"../clock.ts\";\nimport type { Attempt, CheckOptions, Client, ClientJoin, ClientKnobs, ClientOptions, ClientSpec, ClientStep, DialFn, RetryKnobs, SendOptions, Sent } from \"../client/core.ts\";\nimport {\n attachClient,\n clientBounds,\n clientProblems,\n clientRecorder,\n dialCheck,\n finiteMs,\n inFlight,\n localStateCheck,\n message,\n missesProblems,\n ms,\n notSent,\n operationTable,\n readyWait,\n retryBackoff,\n retryProblems,\n roundTripCheck,\n sendWithReissue,\n socketEnds,\n stepChecks,\n timeProblems,\n withinBound,\n} from \"../client/core.ts\";\nimport { helloDial } from \"../client/dial.ts\";\nimport type { Dependency } from \"../description.ts\";\nimport type { Heartbeat } from \"../heartbeat.ts\";\nimport { heartbeat } from \"../heartbeat.ts\";\nimport type { DependencyHandle } from \"../lifecycle.ts\";\nimport { heartbeatDetection } from \"../timing.ts\";\nimport type { ConnectionFactory, RedisConnection, RedisConnectionOptions } from \"./connection.ts\";\nimport { ioredis } from \"./connection.ts\";\nimport { meetings } from \"./meets.ts\";\nimport { REDIS_OPERATIONS, commandName } from \"./operations.ts\";\n\n// the three outcomes, the send options and the checks moved to the core of the clients: this entry keeps them for the users of 0.5\nexport type { CheckOptions, ClientStep, DialFn, SendOptions, Sent } from \"../client/core.ts\";\n// the backoff of the tries to connect moved to the core, for RabbitMQ too: this entry keeps it for the users of 0.5\nexport { retryBackoff } from \"../client/core.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. The knobs that each client has come from the core.\n */\nexport interface RedisKnobs extends ClientKnobs, RetryKnobs {\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 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\n/** A Redis client spec is the declaration of one Redis client: where it connects, and its knobs. */\nexport interface RedisClientSpec extends ClientSpec<RedisKnobs> {\n readonly db?: number | undefined;\n readonly username?: string | undefined;\n /** This count is the number of PINGs in sequence without an answer that make the watchdog connect again. */\n readonly heartbeatMisses: number;\n}\n\n/** These options make a client. Without them, it uses ioredis, the real clock and `Math.random`. */\nexport interface RedisClientOptions extends ClientOptions<ConnectionFactory> {\n readonly random?: (() => number) | 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 extends ClientJoin {\n /** This number is the reply of CLIENT ID, or null when the server gave none. */\n readonly clientId: number | 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 extends Client<ClientStep, Join> {\n /** This text is the state of the connection, as ioredis names it, or \"none\" before the connect. */\n readonly status: string;\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}\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 problems.push(...missesProblems(name, spec.heartbeatMisses, \"watchdog\"));\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 problems.push(...timeProblems(name, knobs));\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 problems.push(...retryProblems(name, knobs));\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 = helloDial(\"PING\\r\\n\", (received) => {\n const reply = received.toString();\n if (!reply.includes(\"\\r\\n\")) return \"more\";\n return reply.startsWith(\"+PONG\") ? null : new Error(`the reply was ${reply.trim()}`);\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 *\n * The outcomes, the second try, the commands in flight, the bounded close and the checks come\n * from the core of the clients. This entry gives what ioredis does in its own way.\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 = operationTable(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 flight = inFlight();\n // the commands that wait for a ready connection before their second try\n const readied = readyWait(clock, () => connection?.status === \"ready\");\n const listeners: Array<[string, (...args: unknown[]) => void]> = [];\n\n const record = clientRecorder(options.record, clock, name);\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 // ioredis reads undefined as no socket timeout, and false as no keepalive\n socketTimeout: finiteMs(knobs.driver_writeoff_time),\n keepAlive: finiteMs(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 notSent(text);\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({ generation, at, clientId, ...socketEnds(socket) });\n record(\"ready\", \"ready\", \"join\", `generation ${generation}, client id ${clientId ?? \"unknown\"}`);\n readied.wake();\n };\n\n const onStatus = (to: string) => (): void => {\n const from = status;\n status = to;\n record(from, to, `status.${to}`);\n // the lower layer gave up the connection: each command in flight learns it now (C10)\n if (to === \"close\" || to === \"end\") flight.settleAll(`the connection closed with the command in flight (${from})`);\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 const attempt = (command: string, args: ReadonlyArray<string | number>, signal?: AbortSignal): Promise<Attempt> => {\n const c = connection;\n if (c === null) return Promise.resolve(notSent(`${name} has not connected`));\n // ioredis has no offline queue here: a command while the client is not ready fails at once, before it leaves the client\n if (c.status !== \"ready\") return Promise.resolve(notSent(`${name} is ${c.status}, not ready: the command did not leave the client`));\n if (signal?.aborted) return Promise.resolve(notSent(\"the caller aborted the command before it left the client\"));\n return flight.track(signal, \"the caller abandoned the command after it was sent\", (settle) => {\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 = <T>(command: string, args: ReadonlyArray<string | number> = [], sendOptions: SendOptions = {}): Promise<Sent<T>> =>\n sendWithReissue({\n clock,\n bound: requestTimeout,\n operation: operations[commandName(command, args)],\n options: sendOptions,\n attempt: (signal) => attempt(command, args, signal) as Promise<Attempt<T>>,\n // ioredis connects again by itself: the second try waits for a ready connection, in the time that is left\n beforeAgain: (left, signal) => readied.wait(left, signal),\n onReissue: () => record(status, status, \"command.reissue\", commandName(command, args)),\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 startedClose = clock.now();\n if (!(await withinBound(clock, ms(knobs.close_bound), c.quit()))) 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 flight.settleAll(\"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) =>\n stepChecks<ClientStep>(name, checkOptions, {\n // the state of the client, without a round trip\n \"local-state\": localStateCheck(() => connection?.status === \"ready\"),\n // a PING on the connection of the application: blind to nothing that the application meets\n \"round-trip\": roundTripCheck((sendOptions) => send(\"PING\", [], sendOptions)),\n // a new connection each time: it finds a server that refuses connections, but not a stalled connection of the application\n dial: dialCheck(dial, spec.host, spec.port),\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) => attachClient(svc, name, declaration, handle, checks(checkOptions)),\n problems: (description) => clientProblems(name, description, clientBounds(knobs)),\n };\n};\n","import type { Clock } from \"../clock.ts\";\nimport type { Decision } from \"../limiter/policy.ts\";\nimport type { LimiterStore } from \"../limiter/store.ts\";\nimport type { RedisClient } from \"./client.ts\";\n\n/**\n * This script decides one take in one atomic step of Redis. It does the arithmetic of `decide`, and\n * it reads `TIME` of the server, the one clock of all replicas. A test can give the time in its place.\n * - KEYS[1] is the state of the key. KEYS[2] is the index of the limiter, a sorted set of its keys\n * by the time at which each key is free.\n * - ARGV is the type of the rule, its two numbers, the cap of keys, the key, and the time or \"\".\n * - The state of a bucket is its full time. The state of a window is the time of each take in it.\n * - The state expires at its free time (PX), and the index lets the cap count only busy keys.\n * - The reply is `{1, remaining}`, or `{0, retryAfter, cause}` with the time as text. Redis gives\n * each Lua number as a whole number, thus the fraction of a millisecond goes as text.\n */\nexport const LIMITER_SCRIPT = `\nlocal kind = ARGV[1]\nlocal a = tonumber(ARGV[2])\nlocal b = tonumber(ARGV[3])\nlocal cap = tonumber(ARGV[4])\nlocal member = ARGV[5]\nlocal now\nif ARGV[6] == '' then\n local t = redis.call('TIME')\n now = tonumber(t[1]) * 1000 + tonumber(t[2]) / 1000\nelse\n now = tonumber(ARGV[6])\nend\nlocal TOL = 1e-6\nlocal function num(x) return string.format('%.6f', x) end\nlocal raw = redis.call('GET', KEYS[1])\nlocal full = nil\nlocal stamps = nil\nif raw then\n if kind == 'token-bucket' then\n full = tonumber(raw)\n else\n stamps = {}\n for x in string.gmatch(raw, '[^,]+') do stamps[#stamps + 1] = tonumber(x) end\n end\nend\nlocal function freeAt()\n if kind == 'token-bucket' then return full end\n if #stamps == 0 then return -math.huge end\n return stamps[#stamps] + b\nend\nif raw and freeAt() <= now then\n redis.call('DEL', KEYS[1])\n redis.call('ZREM', KEYS[2], member)\n raw = nil\n full = nil\n stamps = nil\nend\nif not raw then\n if redis.call('ZCARD', KEYS[2]) >= cap then\n redis.call('ZREMRANGEBYSCORE', KEYS[2], '-inf', num(now))\n if redis.call('ZCARD', KEYS[2]) >= cap then\n local first = redis.call('ZRANGE', KEYS[2], 0, 0, 'WITHSCORES')\n return {0, num(tonumber(first[2]) - now), 'cap'}\n end\n end\nend\nlocal remaining\nlocal state\nif kind == 'token-bucket' then\n local span = a * b\n local base = now\n if full and full > now then base = full end\n local nxt = base + a\n if nxt - now > span + TOL then\n return {0, num(nxt - span - now), 'limit'}\n end\n remaining = math.floor((span - (nxt - now)) / a + TOL)\n if remaining < 0 then remaining = 0 end\n full = nxt\n state = num(full)\nelse\n local live = {}\n for _, stamp in ipairs(stamps or {}) do\n if stamp + b > now + TOL then live[#live + 1] = stamp end\n end\n if #live >= a then\n return {0, num(live[1] + b - now), 'limit'}\n end\n live[#live + 1] = now\n remaining = a - #live\n stamps = live\n local parts = {}\n for i, stamp in ipairs(live) do parts[i] = num(stamp) end\n state = table.concat(parts, ',')\nend\nlocal free = freeAt()\nlocal ttl = math.ceil(free - now)\nif ttl < 1 then ttl = 1 end\nredis.call('SET', KEYS[1], state, 'PX', ttl)\nredis.call('ZADD', KEYS[2], num(free), member)\nif redis.call('PTTL', KEYS[2]) < ttl then redis.call('PEXPIRE', KEYS[2], ttl) end\nreturn {1, remaining}\n`;\n\n/** These options make a store of a limiter in Redis. */\nexport interface RedisLimiterOptions {\n /** This text starts the name of each key of the store in Redis, for example `limit:`. It has no braces. */\n readonly prefix: string;\n /**\n * This clock replaces `TIME` of the server, for a test that compares the store with `decide`. Without\n * it, the script reads `TIME`, thus each replica gets the one clock of the store.\n */\n readonly clock?: Clock | undefined;\n}\n\n/** A Redis client part is the part of a Redis client that the store uses: `send`. */\nexport type RedisSender = Pick<RedisClient, \"send\">;\n\n/**\n * This function makes a store that the replicas share, in Redis 7. Each take is one `EVAL` of\n * `LIMITER_SCRIPT`. The client does not send it again, because a second take counts two times.\n * - The names of the keys have the limiter in braces, a hash tag. Thus the state and the index of a\n * limiter are in one slot of a Redis Cluster.\n * - Each state expires at the time that the key is free, thus the eviction is exact, as in the memory\n * store. The index of the limiter bounds the count of busy keys to `maxKeys`.\n * - A reply of an error, an outcome that is not certain, or a reply of another shape rejects the\n * take of the store. `keyedLimiter` then applies the `onStoreFailure` of the spec.\n * - `TIME` is the wall clock of the Redis host, not a monotonic clock. Thus a step of that clock\n * moves the counts of each key. The replicas still share one clock.\n */\nexport const redisLimiterStore = (client: RedisSender, options: RedisLimiterOptions): LimiterStore => {\n if (/[{}]/.test(options.prefix)) throw new Error(`the prefix ${options.prefix} of a limiter store has a brace: the store puts the hash tag in it`);\n return {\n kind: \"shared\",\n async take({ limiter, key, rule, maxKeys }): Promise<Decision> {\n const base = `${options.prefix}{${limiter}}`;\n const numbers = rule.type === \"token-bucket\" ? [rule.interval, rule.burst] : [rule.limit, rule.window];\n const now = options.clock === undefined ? \"\" : String(options.clock.now());\n const sent = await client.send<unknown[]>(\"EVAL\", [\n LIMITER_SCRIPT,\n 2,\n `${base}:key:${key}`,\n `${base}:keys`,\n rule.type,\n String(numbers[0]),\n String(numbers[1]),\n String(maxKeys),\n key,\n now,\n ]);\n if (sent.outcome !== \"ok\") throw new Error(`the limiter store ${limiter} failed (${sent.outcome}): ${sent.error}`);\n return replyOf(limiter, sent.value);\n },\n };\n};\n\n/** This function gives the decision of a reply of `LIMITER_SCRIPT`. A reply of another shape throws. */\nconst replyOf = (limiter: string, reply: unknown): Decision => {\n if (Array.isArray(reply) && Number(reply[0]) === 1 && reply.length === 2) return { allowed: true, remaining: Number(reply[1]) };\n if (Array.isArray(reply) && Number(reply[0]) === 0 && reply.length === 3 && (reply[2] === \"limit\" || reply[2] === \"cap\")) {\n const retryAfter = Number(reply[1]);\n if (Number.isFinite(retryAfter) && retryAfter > 0) return { allowed: false, retryAfter: { value: retryAfter, unit: \"ms\" }, cause: reply[2] };\n }\n throw new Error(`the limiter store ${limiter} gave a reply that is not a decision: ${JSON.stringify(reply)}`);\n};\n","import type { Machine } from \"@mark1russell7/algebra\";\nimport { clientKnob as knob, closeTimer, transition as t } from \"../client/machine.ts\";\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: closeTimer(),\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,6 +1,6 @@
1
1
  import { n as Clock } from "./clock-mVqhXD0b.js";
2
- import { D as Outcome, E as CheckFn, ct as ExitTrigger, d as TransportImpl, pt as ProcessPort, r as Lifecycle, st as ExitSignal } from "./lifecycle-CGEegMI0.js";
3
- import { n as ServiceBuilder } from "./builder-Dn0cby6Q.js";
2
+ import { D as Outcome, E as CheckFn, ct as ExitSignal, d as TransportImpl, lt as ExitTrigger, mt as ProcessPort, r as Lifecycle } from "./lifecycle-fgYfxrHK.js";
3
+ import { n as ServiceBuilder } from "./builder-DawJhq4p.js";
4
4
  //#region src/fakes.d.ts
5
5
  /** A fake clock moves only when a test tells it to. */
6
6
  interface FakeClock extends Clock {
@@ -0,0 +1,275 @@
1
+ import { Y as Poll, wt as Operation } from "./lifecycle-fgYfxrHK.js";
2
+ import { C as MANIFESTATIONS, D as firstDetector, E as Situation, O as unwatched, S as Detector, T as Meeting, _ as Sent, a as ClientKnobs, b as retryProblems, c as ClientSpec, d as Factory, f as ReadyWait, g as SendOptions, h as STARTING_POLL, i as ClientJoin, l as ClientStep, m as RetrySchedule, n as CheckOptions, o as ClientOptions, p as RetryKnobs, r as Client, s as ClientSocket, t as Attempt, u as DialFn, v as readyWait, w as Manifestation, x as retrySchedule, y as retryBackoff } from "./core-BPLRDcUJ.js";
3
+ import { n as statusLineJudge, t as Judge } from "./dial-DA8vJfh2.js";
4
+ import { Extent, Quantity } from "@mark1russell7/quantity";
5
+ import { Machine } from "@mark1russell7/algebra";
6
+ //#region src/websocket/connection.d.ts
7
+ /**
8
+ * These are the listeners of one connection. The connection holds each event until the client
9
+ * listens. Thus an event that comes between the creation and the listen does not go away.
10
+ */
11
+ interface WsListeners {
12
+ /** This event tells that the upgrade ended with 101, and the connection can send. */
13
+ open(): void;
14
+ /** This event gives one message of the server. A text message is UTF-8 in the buffer. */
15
+ message(data: Buffer, binary: boolean): void;
16
+ /** This event gives the payload of one pong frame. */
17
+ pong(data: Buffer): void;
18
+ /** This event tells that the connection closed, with the code and the reason of the close frame. Code 1006 tells that no close frame came. */
19
+ close(code: number, reason: string): void;
20
+ /** This event gives an error of the connection. A close follows it. */
21
+ error(error: Error): void;
22
+ }
23
+ /**
24
+ * A connection is the part of a `WebSocket` of ws 8 that the wrapper uses. A fake for tests has
25
+ * each part too. Thus the package depends on no type of ws.
26
+ */
27
+ interface WsConnection {
28
+ /** This method gives the listeners of the connection. The connection then gives each event that it holds. */
29
+ listen(listeners: WsListeners): void;
30
+ /** This method writes one message. The callback gets an error when the write fails. It throws while the connection is not open. */
31
+ send(data: string | Buffer, callback: (error?: Error) => void): void;
32
+ /** This method writes one ping frame with this payload. It throws while the connection is not open. */
33
+ ping(data: Buffer): void;
34
+ /** This method writes a close frame, and waits for the close frame of the server. It destroys nothing. */
35
+ close(code: number, reason: string): void;
36
+ /** This method destroys the socket at once. The cells measured 0.3 ms to 1 ms from the call to the close of the socket. */
37
+ terminate(): void;
38
+ /** This field holds the socket after the upgrade, or undefined before it. The join reads its addresses. */
39
+ readonly socket: ClientSocket | undefined;
40
+ /** This text is the subprotocol that the server chose, or "" when it chose none. */
41
+ readonly protocol: string;
42
+ }
43
+ /**
44
+ * These are the options that the wrapper gives `new WebSocket(url, protocols, options)` of ws. Each
45
+ * time comes from the declaration. ws gives the options that it does not read to `net.connect`.
46
+ */
47
+ interface WsDriverOptions {
48
+ /** This time bounds the TCP connect and the upgrade, in milliseconds. */
49
+ readonly handshakeTimeout: number;
50
+ /** ws answers each ping of the server with a pong. The heartbeat of the server then finds the client alive. */
51
+ readonly autoPong: true;
52
+ /** This time bounds the close handshake of ws, in milliseconds. The wrapper gives its own bound, `close_bound`. */
53
+ readonly closeTimeout: number;
54
+ /** ws sets no keepalive on its socket. The wrapper turns it on only when the declaration gives an idle time. */
55
+ readonly keepAlive: boolean;
56
+ /** This time is the idle time of TCP keepalive, in milliseconds. Node truncates it to whole seconds. */
57
+ readonly keepAliveInitialDelay?: number | undefined;
58
+ /** These headers go with the upgrade request, for example `authorization`. */
59
+ readonly headers?: Readonly<Record<string, string>> | undefined;
60
+ }
61
+ /** These are the arguments of `new WebSocket(url, protocols, options)`. */
62
+ interface WsConnectOptions {
63
+ readonly url: string;
64
+ readonly protocols: readonly string[];
65
+ readonly options: WsDriverOptions;
66
+ }
67
+ /** A connection factory makes one connection from its options. The connection opens later, and tells it with the event `open`. */
68
+ type WsFactory = Factory<WsConnectOptions, WsConnection>;
69
+ /** This type is the part of a `WebSocket` of ws 8 that the adapter uses. */
70
+ interface WsDriver {
71
+ on(event: string, listener: (...args: never[]) => void): unknown;
72
+ send(data: string | Buffer, callback: (error?: Error | null) => void): void;
73
+ ping(data: Buffer): void;
74
+ close(code: number, reason: string): void;
75
+ terminate(): void;
76
+ readonly protocol: string;
77
+ }
78
+ /**
79
+ * This function adapts a `WebSocket` of ws to a connection. It listens to `error` at once, because
80
+ * in Node an error with no listener throws. It takes the socket from the event `upgrade`.
81
+ */
82
+ export declare const adaptWs: (ws: WsDriver) => WsConnection;
83
+ /**
84
+ * This factory makes a `WebSocket` of ws. It loads ws only when a client connects, thus a service
85
+ * that does not use WebSocket does not need it. The specifier is a value, thus the package needs no types of ws.
86
+ */
87
+ export declare const nodeWs: WsFactory;
88
+ //#endregion
89
+ //#region src/websocket/client.d.ts
90
+ /**
91
+ * These are the knobs of the client, each a time, by the names of the ordering law. The runtime has
92
+ * no defaults, thus each knob is necessary. A bound that nobody gives is "infinite", not zero. The
93
+ * constant `WS_CLIENT_STARTING_KNOBS` gives a declaration from the WS cells. The knobs that each client has come from the core.
94
+ */
95
+ interface WsKnobs extends ClientKnobs, RetryKnobs {
96
+ /** This time bounds the TCP connect and the upgrade (`handshakeTimeout` of ws). The wrapper also bounds the open with it. */
97
+ readonly connect_bound: Quantity;
98
+ /** This time bounds each call: a request until its reply, and a ping until its pong. The wrapper has this timer, because ws has none. */
99
+ readonly request_timeout: Quantity;
100
+ /** This time is the interval of the ping frames of the heartbeat, from the start of one to the start of the next. "infinite" sends no ping. */
101
+ readonly heartbeat_interval: Extent;
102
+ /** This time bounds each ping of the heartbeat. A ping with no pong in this time is a miss. */
103
+ readonly heartbeat_timeout: Quantity;
104
+ /** This time is the first delay before a new try to connect. The delay doubles after each try. */
105
+ readonly retry_base: Quantity;
106
+ /** This time is the most that one delay can be, before its jitter. */
107
+ readonly retry_cap: Quantity;
108
+ /** This time is the idle time of TCP keepalive (`keepAliveInitialDelay`), in whole seconds. "infinite" keeps keepalive off, as ws does. */
109
+ readonly keepalive_idle: Extent;
110
+ /** The client does not replace its connection at an age, thus this knob must be "infinite". */
111
+ readonly max_connection_age: Extent;
112
+ /** This time bounds the close at the shutdown: the calls in flight, then the close handshake. At the bound, `terminate()` destroys the socket. */
113
+ readonly close_bound: Quantity;
114
+ }
115
+ /** A reply is the id of the request that a message answers, and the error of the server when the reply tells one. */
116
+ interface WsReply {
117
+ readonly id: string;
118
+ readonly error?: string | undefined;
119
+ }
120
+ /**
121
+ * The replies of a protocol tell which message of the server answers which request. WebSocket has no
122
+ * reply of its own: each application protocol puts an id in its messages, for example JSON-RPC 2.0.
123
+ */
124
+ interface WsReplies {
125
+ /** This function reads one message of the server. It gives its reply, or null for a message that answers no request. */
126
+ reply(data: Buffer, binary: boolean): WsReply | null;
127
+ }
128
+ /** A WebSocket client spec is the declaration of one WebSocket client: where it connects, and its knobs. */
129
+ interface WsClientSpec extends ClientSpec<WsKnobs> {
130
+ /** This path is the path of the upgrade request. The default is "/". */
131
+ readonly path?: string | undefined;
132
+ /** These names are the subprotocols that the client offers (`Sec-WebSocket-Protocol`). */
133
+ readonly protocols?: readonly string[] | undefined;
134
+ /** These headers go with the upgrade request and with the dial, for example `authorization`. */
135
+ readonly headers?: Readonly<Record<string, string>> | undefined;
136
+ /** This count is the number of heartbeat misses in sequence that end the connection. The cells used 1. */
137
+ readonly heartbeatMisses: number;
138
+ /** These replies match a message of the server to its request. Without them, the client sends no request. */
139
+ readonly replies?: WsReplies | undefined;
140
+ /**
141
+ * These operations are the calls of the service, by name, with their idempotency. A message does
142
+ * not tell if the server removes its duplicates. Thus the client sends again only a call that this table names, and a ping.
143
+ */
144
+ readonly operations?: ClientSpec<WsKnobs>["operations"];
145
+ }
146
+ /** These options make a client. Without them, it uses ws, the real clock, `Math.random`, and a dial with the upgrade request. */
147
+ interface WsClientOptions extends ClientOptions<WsFactory> {
148
+ readonly random?: (() => number) | undefined;
149
+ /**
150
+ * This function waits one turn of the event loop. The heartbeat waits it before it counts a miss,
151
+ * thus a pong that is in the socket counts first. The default is `setImmediate`.
152
+ */
153
+ readonly turn?: (() => Promise<void>) | undefined;
154
+ }
155
+ /** A join is one generation of the client: one connection, when its upgrade ended with 101 (D1, D2). WebSocket gives no id of a connection. */
156
+ interface WsJoin extends ClientJoin {
157
+ /** This URL is the URL of the connection. */
158
+ readonly url: string;
159
+ /** This text is the subprotocol that the server chose, or "" when it chose none. */
160
+ readonly protocol: string;
161
+ }
162
+ /** A request is one message to the server, with the id that its reply gives back. */
163
+ interface WsRequest {
164
+ readonly id: string;
165
+ /** This data goes as one message. A string goes as a text message, and a buffer as a binary message. */
166
+ readonly data: string | Buffer;
167
+ }
168
+ /** A response is the message of the server that answered a request. */
169
+ interface WsResponse {
170
+ readonly data: Buffer;
171
+ readonly binary: boolean;
172
+ }
173
+ /** A message listener receives each message of the server that answers no request. */
174
+ type WsMessageListener = (data: Buffer, binary: boolean) => void;
175
+ /** A WebSocket client is a dependency of a service: its declaration, its handle, its checks and its calls. */
176
+ interface WsClient extends Client<ClientStep, WsJoin> {
177
+ /** This text is the state of the client, as `WS_CLIENT_MACHINE` names it. */
178
+ readonly status: string;
179
+ /**
180
+ * This method sends a request, and waits for its reply. A reply is `ok`, or a certain error when it
181
+ * tells one. The client sends again only an idempotent operation, one time at most, after an outcome that is not certain.
182
+ */
183
+ request(operation: string, call: WsRequest, options?: SendOptions): Promise<Sent<WsResponse>>;
184
+ /** This method sends a ping frame, and waits for a pong. A pong is `ok`. A ping is idempotent, thus the client can send it again. */
185
+ ping(options?: SendOptions): Promise<Sent<void>>;
186
+ /** This method sends one message with no reply. It gives true when ws took the message, and false when the client is not ready. */
187
+ send(data: string | Buffer): boolean;
188
+ /** This method adds a listener of the messages that answer no request. It gives the function that removes it. */
189
+ onMessage(listener: WsMessageListener): () => void;
190
+ }
191
+ /** These operations are in each table: a ping frame is idempotent (RFC 6455 §5.5.2). */
192
+ export declare const WS_OPERATIONS: Readonly<Record<string, Operation>>;
193
+ /** This function gives the problems of a spec: the knobs that the client cannot use. */
194
+ export declare const specProblems: (name: string, spec: WsClientSpec) => string[];
195
+ /** This function gives the URL of a spec. The client speaks ws:// only. */
196
+ export declare const wsUrl: (spec: WsClientSpec) => string;
197
+ /** This function gives the arguments of `new WebSocket` from a spec. Each timer of ws comes from the declaration. */
198
+ export declare const wsOptions: (spec: WsClientSpec) => WsConnectOptions;
199
+ /** This function gives the bytes of the dial: the upgrade request of RFC 6455 §4.1, with the subprotocols and the headers of the spec. */
200
+ export declare const upgradeRequest: (spec: Pick<WsClientSpec, "host" | "port" | "path" | "protocols" | "headers">) => string;
201
+ /** This judge reads the status line of the answer to the upgrade. Only 101 passes: a WebSocket server takes the upgrade. */
202
+ export declare const upgradeJudge: Judge;
203
+ /** This function makes the dial of a spec: a new TCP connection, the upgrade request, and a wait for 101. The dial then destroys its socket. */
204
+ export declare const wsDial: (spec: Pick<WsClientSpec, "path" | "protocols" | "headers">) => DialFn;
205
+ /**
206
+ * This function makes a WebSocket client, a dependency of a service over ws 8.
207
+ * - It has one connection as one generation. The end of the connection ends the generation. The
208
+ * client then connects again by itself, after a backoff.
209
+ * - Its calls are a request until its reply, and a ping until its pong. Each call has a timer of
210
+ * the wrapper, because ws has none.
211
+ * - Its heartbeat sends a ping frame each interval, one at a time. A ping with no pong in its timeout
212
+ * is a miss. At the last miss, `terminate()` destroys the socket at once, and the client connects again.
213
+ * - It tells the outcome of a call in three values, and it settles each call in flight at the end of its generation (C8, C10).
214
+ * - It records the URL and the ends of the socket of each connection at its open (D1).
215
+ *
216
+ * These parts come from the core of the clients: the outcomes, the second try and the wait for a ready
217
+ * client. The backoff and its schedule, the calls in flight, the bounded close and the checks come from
218
+ * the core too. The heartbeat is the heartbeat of the package. This entry gives what ws does in its own way.
219
+ */
220
+ export declare const wsClient: (name: string, spec: WsClientSpec, options?: WsClientOptions) => WsClient;
221
+ //#endregion
222
+ //#region src/websocket/machine.d.ts
223
+ /**
224
+ * The WebSocket client of the service as a `mark1russell7.machine/1` document. It is one generation
225
+ * at a time: one `WebSocket` of ws 8. The states:
226
+ * - `ready`: the upgrade ended with 101, and the client sends each call. Its heartbeat sends a ping frame each interval.
227
+ * - `failing`: no generation is open. The client connects again after its backoff.
228
+ * - `closing`: the calls in flight, then the close handshake, in `close_bound`. At the bound, `terminate()`.
229
+ *
230
+ * The end of the connection and the last miss of the heartbeat end the generation. A test records
231
+ * the transitions of the client and replays them against this document.
232
+ */
233
+ export declare const WS_CLIENT_MACHINE: Machine;
234
+ //#endregion
235
+ //#region src/websocket/meets.d.ts
236
+ /**
237
+ * This function gives the manifestations that a WebSocket client can meet, and the detectors of
238
+ * each, from its knobs. The WS cells measured them on ws 8:
239
+ * - At a heartbeat of 2 s, a timeout of 1 s and 1 miss, the drop came 0.999 s to 1.0035 s after the
240
+ * ping that got no pong. The slowest of 495 runs took 1.003761 s.
241
+ * - `terminate()` closes the socket 0.3 ms to 1 ms after the drop. The close of the global WebSocket
242
+ * only writes a close frame, and its socket stayed ESTABLISHED.
243
+ * - The kernel of a frozen server acknowledges each ping, thus only the heartbeat finds it.
244
+ * - At `tcp_retries2 = 3`, the kernel gave up before a pong timeout of 10 s.
245
+ * - Without a heartbeat, the client waited for the keepalive of undici: 70 s. ws sets no keepalive.
246
+ */
247
+ export declare const wsMeetings: (name: string, knobs: WsKnobs, misses: number) => Meeting[];
248
+ //#endregion
249
+ //#region src/websocket/starting.d.ts
250
+ /**
251
+ * This poll is the poll of the checks that `WS_CLIENT_STARTING_KNOBS` fits. It is `STARTING_POLL` of
252
+ * the core: each 5 s with a jitter of 20 %, and a timeout of 4 s (B6, O12). The poll does not depend on the driver.
253
+ */
254
+ export declare const WS_CLIENT_STARTING_POLL: Poll;
255
+ /** This count is the heartbeat misses to start from: 1, as in the WS cells. The drop came 0.999 s to 1.0035 s after the ping with no pong. */
256
+ export declare const WS_CLIENT_STARTING_MISSES = 1;
257
+ /**
258
+ * These knobs are a declaration to start from, from what the WS cells measured on ws 8. The runtime
259
+ * has no defaults: a service gives these knobs, or its own. Each value holds the borders that the cells measured:
260
+ * - `connect_bound` 5 s: the connect timeout of DP44. ws sets no handshake timeout by default.
261
+ * - `request_timeout` 9.5 s: in the slowest poll of 10 s (O13).
262
+ * - `heartbeat_interval` 4 s: in the WST cells, a ping each 4 s kept a flow through a router that
263
+ * forgets it at 5 s. A ping each 6 s did not (O03).
264
+ * - `heartbeat_timeout` 3 s: a stall of 1 s with a pong timeout of 3 s dropped nothing. The detection is 4 s + 3 s = 7 s.
265
+ * - `retry_base` 0.5 s and `retry_cap` 4 s: the reconnect of the cells, from 500 ms, doubling to 4 s.
266
+ * The client adds a jitter up to the base. Thus the most delay is 4.5 s, in the slowest poll of 10 s.
267
+ * - `keepalive_idle` "infinite": ws sets no keepalive, and the heartbeat refreshes an idle flow.
268
+ * - `max_connection_age` "infinite": the client does not replace its connection at an age.
269
+ * - `close_bound` 2 s: a close frame to a silent server gets no answer. The `closeTimeout` of ws is 30 s.
270
+ * - `retrans_giveup_time` 924.6 s: the give-up of Linux at `tcp_retries2 = 15`.
271
+ */
272
+ export declare const WS_CLIENT_STARTING_KNOBS: WsKnobs;
273
+ //#endregion
274
+ export { type Attempt, type CheckOptions, type Client, type ClientJoin, type ClientKnobs, type ClientOptions, type ClientSocket, type ClientSpec, type ClientStep, type Detector, type DialFn, type Factory, type Judge, MANIFESTATIONS, type Manifestation, type Meeting, type ReadyWait, type RetryKnobs, type RetrySchedule, STARTING_POLL, type SendOptions, type Sent, type Situation, type WsClient, type WsClientOptions, type WsClientSpec, type WsConnectOptions, type WsConnection, type WsDriverOptions, type WsFactory, type WsJoin, type WsKnobs, type WsListeners, type WsMessageListener, type WsReplies, type WsReply, type WsRequest, type WsResponse, firstDetector, readyWait, retryBackoff, retryProblems, retrySchedule, statusLineJudge, unwatched };
275
+ //# sourceMappingURL=websocket.d.ts.map