@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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-client.js","names":["knob","t"],"sources":["../src/http-client/connection.ts","../src/http-client/meets.ts","../src/http-client/client.ts","../src/http-client/machine.ts","../src/http-client/starting.ts"],"sourcesContent":["import type { ClientSocket, Factory } from \"../client/core.ts\";\n\n/** An HTTP socket is the socket of a client in the core: the join of the prober and the bounded close read it. */\nexport type HttpSocket = ClientSocket;\n\n/** An HTTP response is a whole response: its status, its headers, and its whole body. */\nexport interface HttpResponse {\n readonly status: number;\n /** These are the headers of the response, by their names in lower case, as Node gives them. */\n readonly headers: Readonly<Record<string, string | readonly string[] | undefined>>;\n readonly body: Buffer;\n}\n\n/** An agent request is one request that the wrapper gives the agent. */\nexport interface AgentRequest {\n readonly host: string;\n readonly port: number;\n readonly method: string;\n readonly path: string;\n readonly headers: Readonly<Record<string, string>>;\n readonly body?: Uint8Array | undefined;\n}\n\n/**\n * These are the events of one request that the wrapper reads. The agent gives each one at most one\n * time. `connect` comes only for a new connection. A request on a free socket left the client when it got that socket.\n */\nexport interface AgentEvents {\n /** The agent gave the request a socket. `reused` is true for a socket from the free list (`req.reusedSocket`). */\n socket(socket: HttpSocket, reused: boolean): void;\n /** The new socket of the request connected. The request then leaves the client. */\n connect(): void;\n}\n\n/** An agent exchange is one request in the agent: the promise of its whole response, and the abort that destroys its socket. */\nexport interface AgentExchange {\n /** This promise resolves with the whole response, or rejects with the error of the request, the socket or the body. */\n readonly response: Promise<HttpResponse>;\n /** This method destroys the request and its socket. A request that waits for a socket leaves the queue of the agent. */\n abort(error: Error): void;\n}\n\n/**\n * An agent is the part of an `http.Agent` of Node that the wrapper uses, with `http.request` on it.\n * A fake for tests has each part too. Thus the wrapper does not load node:http until it connects.\n */\nexport interface HttpAgent {\n /** This method sends one request on the agent. */\n request(request: AgentRequest, events: AgentEvents): AgentExchange;\n /** This method gives each socket of the agent that is open: the busy ones and the free ones. */\n sockets(): readonly HttpSocket[];\n /** This method destroys each socket of the agent. A request on a socket then fails. */\n destroy(): void;\n}\n\n/**\n * These are the options that the wrapper gives `new http.Agent`. Each one comes from the declaration.\n * The wrapper sets each option that has an effect on a timer, thus no default of Node decides one.\n */\nexport interface HttpAgentOptions {\n readonly keepAlive: true;\n /**\n * This time is the free socket timer (`timeout`). The agent closes a free socket at the smaller of\n * this time and the hint of the server less `agentKeepAliveTimeoutBuffer`. 0 is no timer, and then the hint does not apply.\n */\n readonly timeout: number;\n /** This time is subtracted from the hint of the server (`Keep-Alive: timeout=…`). A hint at or below it gives no reuse. */\n readonly agentKeepAliveTimeoutBuffer: number;\n /** This time is the idle time of TCP keepalive. The agent always turns keepalive on, and Node truncates the time to whole seconds. */\n readonly keepAliveMsecs: number;\n /** This count is the most sockets for one origin. A request waits in the agent for a socket above it. */\n readonly maxSockets: number;\n /** This count is the most free sockets for one origin. */\n readonly maxFreeSockets: number;\n /** The agent gives the socket that was free last, as the global agent does. */\n readonly scheduling: \"lifo\";\n}\n\n/** An agent factory makes the agent of the client from its options. */\nexport type AgentFactory = Factory<HttpAgentOptions, HttpAgent>;\n\n/** These are the parts of `node:http` that the factory uses. The specifier is a value, thus the entry loads node:http only at the connect. */\ninterface NodeHttp {\n readonly Agent: new (options: HttpAgentOptions) => NodeAgent;\n request(options: object): NodeRequest;\n}\n\ninterface NodeAgent {\n readonly sockets: Readonly<Record<string, readonly HttpSocket[] | undefined>>;\n readonly freeSockets: Readonly<Record<string, readonly HttpSocket[] | undefined>>;\n destroy(): void;\n}\n\ninterface NodeSocket extends HttpSocket {\n readonly connecting: boolean;\n once(event: \"connect\", listener: () => void): unknown;\n}\n\ninterface NodeIncoming {\n readonly statusCode?: number | undefined;\n readonly headers: Readonly<Record<string, string | string[] | undefined>>;\n readonly complete: boolean;\n on(event: \"data\", listener: (chunk: Buffer) => void): unknown;\n on(event: \"end\" | \"close\", listener: () => void): unknown;\n on(event: \"error\", listener: (error: Error) => void): unknown;\n}\n\ninterface NodeRequest {\n readonly reusedSocket: boolean;\n on(event: \"socket\", listener: (socket: NodeSocket) => void): unknown;\n on(event: \"response\", listener: (response: NodeIncoming) => void): unknown;\n on(event: \"error\", listener: (error: Error) => void): unknown;\n end(body?: Uint8Array): void;\n destroy(error?: Error): void;\n}\n\n/**\n * This factory makes an `http.Agent` of Node. It loads node:http only when a client connects.\n * Each request goes through `http.request` with the agent. The response settles at the end of its body.\n * A body that ends before it is complete rejects the response.\n */\nexport const nodeAgent: AgentFactory = async (options) => {\n const specifier = \"node:http\";\n const http = (await import(specifier)) as NodeHttp;\n const agent = new http.Agent(options);\n const all = (lists: Readonly<Record<string, readonly HttpSocket[] | undefined>>): HttpSocket[] => Object.values(lists).flatMap((list) => list ?? []);\n return {\n request(request, events) {\n const req = http.request({ host: request.host, port: request.port, method: request.method, path: request.path, headers: request.headers, agent });\n const response = new Promise<HttpResponse>((resolve, reject) => {\n req.on(\"socket\", (socket) => {\n events.socket(socket, req.reusedSocket);\n if (socket.connecting) socket.once(\"connect\", () => events.connect());\n else if (!req.reusedSocket) events.connect();\n });\n req.on(\"response\", (incoming) => {\n const chunks: Buffer[] = [];\n incoming.on(\"data\", (chunk) => chunks.push(chunk));\n incoming.on(\"error\", reject);\n incoming.on(\"end\", () => resolve({ status: incoming.statusCode ?? 0, headers: incoming.headers, body: Buffer.concat(chunks) }));\n incoming.on(\"close\", () => {\n if (!incoming.complete) reject(new Error(\"the connection closed before the end of the body\"));\n });\n });\n req.on(\"error\", reject);\n });\n req.end(request.body);\n return { response, abort: (error) => req.destroy(error) };\n },\n sockets: () => [...all(agent.sockets), ...all(agent.freeSockets)],\n destroy: () => agent.destroy(),\n };\n};\n","import type { Detector, Meeting, Situation } from \"../client/meets.ts\";\nimport { WHAT, immediate, keepaliveAnswered, keepaliveReset, keepaliveSilent, kernelGiveUp } from \"../client/meets.ts\";\nimport type { HttpClientKnobs } from \"./client.ts\";\n\n/**\n * This function gives the manifestations that an HTTP client can meet, and the detectors of each,\n * from its knobs. The HTTP cells measured them on the agent of Node 24.21.0:\n * - The agent has no timer of a call, thus the timer of the wrapper finds each silent call in flight.\n * - The free socket timer closes an idle socket at `pool_idle_expiry` at most. Thus no request goes\n * later on a socket that went silent while it was free.\n * - A FIN on a busy socket fails the call with \"socket hang up\". A FIN on a free socket destroys it.\n */\nexport const httpClientMeetings = (name: string, knobs: HttpClientKnobs): Meeting[] => {\n const call: Detector = {\n by: \"request_timeout\",\n within: knobs.request_timeout,\n situation: \"in flight\",\n effect: `the timer of the wrapper ends the call as indeterminate, and ${name} destroys its socket`,\n };\n const idleExpiry = (situation: Situation): Detector => ({\n by: \"pool_idle_expiry\",\n within: knobs.pool_idle_expiry,\n situation,\n effect: \"the free socket timer of the agent closes the socket, and the next request opens a new socket\",\n });\n const giveUp = kernelGiveUp(knobs.retrans_giveup_time, \"the call fails as indeterminate\");\n const silent = (manifestation: \"blackhole\" | \"freeze-host\"): Meeting => ({\n manifestation,\n what: WHAT[manifestation],\n detectors: [call, giveUp, idleExpiry(\"idle\"), keepaliveSilent(knobs.keepalive_idle)],\n });\n return [\n {\n manifestation: \"rst\",\n what: WHAT.rst,\n detectors: [\n immediate(\"in flight\", \"the call fails with ECONNRESET as indeterminate, and the next request opens a new socket\"),\n idleExpiry(\"idle\"),\n keepaliveReset(knobs.keepalive_idle),\n ],\n },\n {\n manifestation: \"fin\",\n what: WHAT.fin,\n detectors: [\n immediate(\"in flight\", 'the call fails with \"socket hang up\" as indeterminate'),\n immediate(\"idle\", \"the agent destroys the free socket, and the next request opens a new socket\"),\n ],\n },\n silent(\"blackhole\"),\n silent(\"freeze-host\"),\n {\n manifestation: \"freeze-process\",\n what: WHAT[\"freeze-process\"],\n detectors: [call, idleExpiry(\"idle\"), keepaliveAnswered()],\n },\n {\n manifestation: \"app-stall\",\n what: \"the server holds its reply, for example in a slow handler, and its kernel stays up\",\n detectors: [call, idleExpiry(\"idle\"), keepaliveAnswered()],\n },\n ];\n};\n","import type { Extent, Quantity } from \"@mark1russell7/quantity\";\nimport type { Cancel } from \"../clock.ts\";\nimport { realClock } from \"../clock.ts\";\nimport type { Attempt, CheckOptions, Client, ClientJoin, ClientKnobs, ClientOptions, ClientSpec, ClientStep, DialFn, SendOptions, Sent } from \"../client/core.ts\";\nimport {\n ageProblems,\n attachClient,\n clientBounds,\n clientProblems,\n clientRecorder,\n dialCheck,\n idleCloseProblem,\n inFlight,\n keepaliveOnProblems,\n localStateCheck,\n message,\n ms,\n notSent,\n operationTable,\n poolProblems,\n roundTripCheck,\n sendWithReissue,\n socketEnds,\n stepChecks,\n timeProblems,\n trackWithTimer,\n wholeProblems,\n withinBound,\n wrapperTimerProblems,\n zeroProblems,\n} from \"../client/core.ts\";\nimport type { Judge } from \"../client/dial.ts\";\nimport { authority, helloDial, statusLineJudge } from \"../client/dial.ts\";\nimport type { Dependency } from \"../description.ts\";\nimport type { DependencyHandle } from \"../lifecycle.ts\";\nimport type { Operation } from \"../operation.ts\";\nimport type { AgentExchange, AgentFactory, HttpAgent, HttpAgentOptions, HttpResponse, HttpSocket } from \"./connection.ts\";\nimport { nodeAgent } from \"./connection.ts\";\nimport { httpClientMeetings } from \"./meets.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. The constant\n * `HTTP_CLIENT_STARTING_KNOBS` gives a declaration from the HTTP cells. The knobs that each client has come from the core.\n */\nexport interface HttpClientKnobs extends ClientKnobs {\n /** This time bounds the TCP connect of a new socket. The wrapper has this timer, because the agent has none. */\n readonly connect_bound: Quantity;\n /**\n * This time bounds each call: from its start until the end of the body of its response. It also\n * bounds the wait for a socket. The wrapper has this timer, because the agent has none.\n */\n readonly request_timeout: Quantity;\n /**\n * This time is the free socket timer of the agent (`timeout`). The agent closes a free socket at the\n * smaller of this time and the hint of the server less `hint_buffer`. It must be below `server_idle_close` (O01).\n */\n readonly pool_idle_expiry: Extent;\n /** This time is what the agent subtracts from the hint of the server (`agentKeepAliveTimeoutBuffer`). A hint at or below it gives no reuse. */\n readonly hint_buffer: Quantity;\n /**\n * This time is when the server closes an idle connection, after its response. A Node server closes\n * at `keepAliveTimeout` plus `keepAliveTimeoutBuffer`. \"infinite\" is a server that keeps an idle\n * connection with no bound. The client does not set it: `problems` compares it with `pool_idle_expiry` (O01).\n */\n readonly server_idle_close: Extent;\n /** This time is the idle time of TCP keepalive (`keepAliveMsecs`), in whole seconds. The agent always turns keepalive on. */\n readonly keepalive_idle: Extent;\n /** The agent does not replace a connection at an age, thus this knob must be \"infinite\". */\n readonly max_connection_age: Extent;\n /** This time bounds the close of the client: the wait for the calls in flight, then a destroy of each socket. */\n readonly close_bound: Quantity;\n}\n\n/** A check request is the request of the round-trip check, and of the first connection at the connect. */\nexport interface CheckRequest {\n readonly method: string;\n readonly path: string;\n}\n\n/**\n * This request asks about the server, not about a resource (RFC 9110 §9.3.7). Thus it changes no\n * state. A server that does not route it still answers with a status, and the status tells that it is alive.\n */\nexport const OPTIONS_STAR: CheckRequest = { method: \"OPTIONS\", path: \"*\" };\n\n/** An HTTP client spec is the declaration of one HTTP client: the origin that it connects to, its pool, and its knobs. */\nexport interface HttpClientSpec extends ClientSpec<HttpClientKnobs> {\n /** This count is the most sockets of the pool (`maxSockets`). It is also the most free sockets. */\n readonly pool: { readonly max: number };\n /** This request is the request of the round-trip check and of the first connection. Without it, the client uses `OPTIONS *`. */\n readonly check?: CheckRequest | undefined;\n /**\n * These operations add to the table of the client, or change it. The table names each method by\n * its idempotency in RFC 9110. A caller can name an operation of its own, for example a POST with an `Idempotency-Key`.\n */\n readonly operations?: Readonly<Record<string, Operation>> | undefined;\n}\n\n/** These options make a client. Without them, it uses the agent of Node, the real clock, and a dial with `OPTIONS *`. */\nexport type HttpClientOptions = ClientOptions<AgentFactory>;\n\n/** An HTTP call is one request. A string body goes as UTF-8, with its `content-length`. */\nexport interface HttpCall {\n readonly method: string;\n readonly path: string;\n readonly headers?: Readonly<Record<string, string>> | undefined;\n readonly body?: string | Uint8Array | undefined;\n /** This name is the operation of the call in the table. Without it, the operation is the method. */\n readonly operation?: string | undefined;\n}\n\n/** An HTTP client is a dependency of a service: its declaration, its handle, its checks and its requests. */\nexport interface HttpClient extends Client<ClientStep, ClientJoin> {\n /** This text is the state of the client, as `HTTP_CLIENT_MACHINE` names it. */\n readonly status: string;\n /**\n * This method sends a request, and gives its outcome in three values. A whole response of each\n * status is `ok`. The client sends again only an idempotent operation, one time at most, after an\n * outcome that is not certain. The second try stays in the bound of the request.\n */\n request(call: HttpCall, options?: SendOptions): Promise<Sent<HttpResponse>>;\n}\n\n/**\n * These operations are the methods of HTTP, by their idempotency in RFC 9110 §9.2.2. A second\n * request of GET, HEAD, OPTIONS, TRACE, PUT or DELETE has the same effect as one. POST and PATCH do not.\n */\nexport const HTTP_OPERATIONS: Readonly<Record<string, Operation>> = {\n GET: { idempotent: true },\n HEAD: { idempotent: true },\n OPTIONS: { idempotent: true },\n TRACE: { idempotent: true },\n PUT: { idempotent: true },\n DELETE: { idempotent: true },\n POST: { idempotent: false },\n PATCH: { idempotent: false },\n};\n\n/** This function gives the problems of a spec: the knobs that the agent cannot use. */\nexport const specProblems = (name: string, spec: HttpClientSpec): string[] => {\n const { knobs } = spec;\n const problems = [\n ...timeProblems(name, knobs),\n // the agent has no timer of a connect or of a call\n ...wrapperTimerProblems(name, knobs, { connect_bound: \"a connect\", request_timeout: \"a call\" }),\n // the agent reads a timeout of 0 as no free socket timer, and then the hint of the server does not apply\n ...zeroProblems(name, \"the agent\", knobs, { bounded: [\"pool_idle_expiry\"] }),\n ...keepaliveOnProblems(name, knobs, \"the agent\"),\n // Node truncates the keepalive delay to whole seconds\n ...wholeProblems(name, knobs, { millis: [\"connect_bound\", \"request_timeout\", \"pool_idle_expiry\", \"hint_buffer\"], seconds: [\"keepalive_idle\"] }),\n ...poolProblems(name, { max: spec.pool.max, min: 0 }),\n ...ageProblems(name, knobs, \"the agent does not replace a connection at an age\"),\n ];\n if (!Number.isFinite(ms(knobs.hint_buffer))) problems.push(`${name}'s hint_buffer is not a finite time: the agent subtracts it from the hint of the server`);\n return problems;\n};\n\n/** This function gives the options of `new http.Agent` from a spec. Each timer of the agent comes from the declaration. */\nexport const agentOptions = (spec: HttpClientSpec): HttpAgentOptions => {\n const { knobs } = spec;\n return {\n keepAlive: true,\n timeout: ms(knobs.pool_idle_expiry),\n agentKeepAliveTimeoutBuffer: ms(knobs.hint_buffer),\n keepAliveMsecs: ms(knobs.keepalive_idle),\n maxSockets: spec.pool.max,\n maxFreeSockets: spec.pool.max,\n scheduling: \"lifo\",\n };\n};\n\n/** This function gives the bytes of the dial: `OPTIONS *` with the Host header, and a close after the answer. */\nexport const optionsRequest = (host: string, port: number): string => `OPTIONS * HTTP/1.1\\r\\nHost: ${authority(host, port)}\\r\\nConnection: close\\r\\n\\r\\n`;\n\n/** This judge reads the status line of the answer. Any status passes, because it tells that an HTTP server answered. */\nexport const statusJudge: Judge = statusLineJudge(() => null);\n\n/** This dial function opens a TCP connection, sends `OPTIONS *`, and waits for the status line. It changes no state on the server. */\nexport const httpDial: DialFn = (host, port, signal) => helloDial(optionsRequest(host, port), statusJudge)(host, port, signal);\n\n/**\n * This function makes an HTTP client, a dependency of a service over an `http.Agent` of Node.\n * - It sets each timer of the agent from the declaration. The agent sends no request again by itself,\n * and the client sends again only an idempotent operation of its table.\n * - Its call is a request until the end of the body of its response. The wrapper has a timer for the\n * connect and one for the call, because the agent has none.\n * - It tells the outcome of a call in three values. A request that did not get a connected socket\n * did not leave the client. A failure after that is not certain.\n * - It destroys the socket of each call that is not `ok`, thus no later request gets that socket.\n * - It records each new socket of the pool (D1). HTTP gives no id of a connection.\n *\n * The core of the clients gives the outcomes, the second try, the calls in flight and the timer of a\n * call. It also gives the bounded close and the checks. This entry gives what the agent does in its own way.\n */\nexport const httpClient = (name: string, spec: HttpClientSpec, options: HttpClientOptions = {}): HttpClient => {\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 create = options.create ?? nodeAgent;\n const dial = options.dial ?? httpDial;\n const operations = operationTable(HTTP_OPERATIONS, spec.operations);\n const requestTimeout = ms(knobs.request_timeout);\n const connectBound = ms(knobs.connect_bound);\n const check = spec.check ?? OPTIONS_STAR;\n\n let agent: HttpAgent | null = null;\n let status = \"none\";\n const joins: ClientJoin[] = [];\n // the calls that left the client: the close waits for them, in its bound\n const flight = inFlight();\n // the calls that did not leave the client: the close ends each one as not sent\n const waiting = new Set<(error: string) => void>();\n\n const emit = clientRecorder(options.record, clock, name);\n // the state of the client is the outcome of the last connect of a socket of its pool\n const record = (on: string, to: string = status, detail?: string): void => {\n const from = status;\n status = to;\n emit(from, to, on, detail);\n };\n const closing = (): boolean => status === \"closing\" || status === \"end\";\n\n /**\n * This function makes one try of a call. The try waits for a socket of the agent: a free one, or a\n * new one in `connect_bound`. A failure before the socket is ready is certain: the request did not\n * leave the client. After that, the try waits for the whole response in the rest of `request_timeout`.\n */\n const attempt = async (call: HttpCall, signal: AbortSignal | undefined): Promise<Attempt<HttpResponse>> => {\n const a = agent;\n if (a === null) return notSent(`${name} has not connected`);\n if (closing()) return notSent(`${name} is ${status}: the request did not leave the client`);\n if (signal?.aborted) return notSent(\"the caller aborted the request before it left the client\");\n const started = clock.now();\n const body = typeof call.body === \"string\" ? Buffer.from(call.body, \"utf8\") : call.body;\n const headers = { ...call.headers, ...(body === undefined ? {} : { \"content-length\": String(body.byteLength) }) };\n\n let socket: HttpSocket | undefined;\n let reused = false;\n let left = false;\n // true when the wrapper ended the try itself: the error of the agent that follows is not a failed connect\n let ending = false;\n let connectTimer: Cancel = () => {};\n let decide: (value: true | Attempt<never>) => void = () => {};\n const leaving = new Promise<true | Attempt<never>>((resolve) => {\n let decided = false;\n decide = (value) => {\n if (decided) return;\n decided = true;\n resolve(value);\n };\n });\n // a result of the agent that arrives before the try records it\n let deliver: ((value: Attempt<HttpResponse>, cause?: Error) => void) | null = null;\n let early: { readonly value: Attempt<HttpResponse>; readonly cause: Error | undefined } | null = null;\n let settled = false;\n const settleWith = (value: Attempt<HttpResponse>, cause?: Error): void => {\n if (deliver === null) early ??= { value, cause };\n else deliver(value, cause);\n };\n\n let exchange: AgentExchange | null = null;\n const stop = (text: string): void => {\n if (left) return;\n ending = true;\n connectTimer();\n exchange?.abort(new Error(text));\n decide(notSent(text));\n };\n const ready = (): void => {\n left = true;\n connectTimer();\n decide(true);\n };\n exchange = a.request(\n { host: spec.host, port: spec.port, method: call.method, path: call.path, headers, body },\n {\n socket(given, isReused) {\n socket = given;\n reused = isReused;\n if (isReused) {\n ready();\n return;\n }\n connectTimer = clock.after(connectBound, () => {\n if (left) return;\n record(\"connect.failed\", closing() ? status : \"failing\", \"no connection in connect_bound\");\n stop(\"no connection in connect_bound: the request did not leave the client\");\n });\n },\n connect() {\n const generation = joins.length + 1;\n joins.push({ generation, at: clock.now(), ...socketEnds(socket) });\n record(\"join\", closing() ? status : \"ready\", `socket ${generation}`);\n ready();\n },\n },\n );\n // the agent can fail a request in its queue or at its connect: it did not leave the client\n exchange.response.then(\n (response) => settleWith({ outcome: \"ok\", value: response }),\n (error: unknown) => {\n const cause = error instanceof Error ? error : new Error(message(error));\n if (!left) {\n connectTimer();\n if (!ending) record(\"connect.failed\", closing() ? status : \"failing\", message(error));\n decide(notSent(message(error)));\n return;\n }\n // the abort of the wrapper after the call settled is not a failure of the socket\n if (settled) return;\n // a reused socket that fails before the response: the server closed it, as in the keep-alive race\n if (reused) record(\"reuse.failed\", status, message(error));\n settleWith({ outcome: \"indeterminate\", error: message(error) }, cause);\n },\n );\n\n const abandoned = (): void => stop(\"the caller abandoned the request before it left the client\");\n signal?.addEventListener(\"abort\", abandoned, { once: true });\n const queued = clock.after(requestTimeout, () => stop(\"no socket in request_timeout: the request did not leave the client\"));\n waiting.add(stop);\n const got = await leaving;\n signal?.removeEventListener(\"abort\", abandoned);\n queued();\n waiting.delete(stop);\n if (got !== true) return got;\n\n const sent = exchange;\n return trackWithTimer<HttpResponse>(\n flight,\n clock,\n Math.max(0, requestTimeout - (clock.now() - started)),\n signal,\n \"the caller abandoned the request after it left the client\",\n \"no whole response in request_timeout: the server can have the request\",\n (settle) => {\n deliver = settle;\n if (early !== null) settle(early.value, early.cause);\n },\n (value, cause) => {\n settled = true;\n // the socket of a call that is not ok can hold a part of a response: it goes, and no later request gets it\n if (value.outcome !== \"ok\") sent.abort(cause ?? new Error(value.error));\n },\n );\n };\n\n const request = (call: HttpCall, sendOptions: SendOptions = {}): Promise<Sent<HttpResponse>> => {\n const method = call.method.toUpperCase();\n const named = call.operation ?? method;\n return sendWithReissue({\n clock,\n bound: requestTimeout,\n operation: operations[named],\n options: sendOptions,\n attempt: (signal) => attempt({ ...call, method }, signal),\n onReissue: () => record(\"command.reissue\", status, named),\n });\n };\n\n const handle: DependencyHandle = {\n async connect(signal) {\n record(\"status.connecting\", \"connecting\");\n agent = await create(agentOptions(spec));\n if (signal.aborted) throw new Error(`${name}: the service stopped before the connect`);\n // the check request opens the first socket of the pool. A server that is down does not stop the\n // startup: the checks keep readiness tripped until a connect succeeds (readiness is earned)\n await request(check, { signal, reissue: false });\n },\n async close() {\n const a = agent;\n if (a === null || closing()) return;\n record(\"close.request\", \"closing\");\n for (const stop of [...waiting]) stop(\"the client closed before the request left\");\n // the agent has no close that waits: the client waits for its calls in flight, in its bound\n if (!(await withinBound(clock, ms(knobs.close_bound), flight.idle()))) {\n record(\"close.bound\");\n const open = a.sockets().length;\n flight.settleAll(\"the client closed with the request in flight\");\n a.destroy();\n record(\"close.destroy\", status, `open sockets: ${open}`);\n } else {\n // each free socket closes with a FIN\n a.destroy();\n }\n record(\"status.end\", \"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 pool_idle_expiry: knobs.pool_idle_expiry,\n hint_buffer: knobs.hint_buffer,\n keepalive_idle: knobs.keepalive_idle,\n // the agent sends nothing on a free socket, thus its keepalive is the only refresh of an idle flow (O03)\n idle_refresh_interval: knobs.keepalive_idle,\n max_connection_age: knobs.max_connection_age,\n close_bound: knobs.close_bound,\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 outcome of the last connect of a socket, without a round trip\n \"local-state\": localStateCheck(() => status === \"ready\"),\n // the check request through the pool of the application. A status of 500 or more fails it\n \"round-trip\": roundTripCheck(async (sendOptions) => {\n const sent = await request(check, sendOptions);\n return sent.outcome === \"ok\" && sent.value.status >= 500\n ? { outcome: \"error\", error: `the server answered ${sent.value.status}`, reply: true, attempts: sent.attempts }\n : sent;\n }),\n // a new connection each time, with OPTIONS *\n dial: dialCheck(dial, spec.host, spec.port),\n });\n\n return {\n name,\n declaration,\n handle,\n meets: httpClientMeetings(name, knobs),\n get status() {\n return status;\n },\n joins,\n request,\n checks,\n attach: (svc, checkOptions) => attachClient(svc, name, declaration, handle, checks(checkOptions)),\n problems(description) {\n // O01: the pool must close an idle socket before the server does\n const idle = idleCloseProblem(name, knobs.pool_idle_expiry, knobs.server_idle_close);\n return [...clientProblems(name, description, clientBounds(knobs)), ...(idle === null ? [] : [idle])];\n },\n };\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 agent exists and serves. */\nconst SERVING = [\"connecting\", \"ready\", \"failing\"] as const;\nconst OPEN = [...SERVING, \"closing\"] as const;\n\n/**\n * The HTTP client of the service as a `mark1russell7.machine/1` document. It is the agent as the\n * wrapper sees it, not one socket: the document `node-http-agent` of `@mark1russell7/machines`\n * gives each socket of the agent. The state is the outcome of the last connect of a socket:\n * - `ready`: the last connect succeeded.\n * - `failing`: the last connect failed, or did not end in connect_bound. The agent does not connect\n * again by itself. The next request tries again.\n *\n * A free socket that the agent closes does not change the state. A request on a reused socket that\n * fails records `reuse.failed`, and the state stays. A test records the transitions of the client\n * and replays them against this document.\n */\nexport const HTTP_CLIENT_MACHINE: Machine = {\n format: \"mark1russell7.machine/1\",\n id: \"service.machine.http-client\",\n title: \"The HTTP client of the service, over an http.Agent of Node\",\n layer: \"driver\",\n version: \"0.1.0\",\n sources: [{ ref: \"packages/service/src/http-client (the client implements this document)\", rung: \"implemented\" }],\n states: {\n none: { initial: true, note: \"the client exists, and its agent does not\" },\n connecting: { note: \"the check request of the startup opens the first socket\" },\n ready: { note: \"the last connect succeeded. A dead free socket stays hidden until the free socket timer or a request meets it\" },\n failing: { note: \"the last connect failed. A request that waits for a socket of a full pool does not change the state\" },\n closing: { note: \"the client waits for its calls in flight in close_bound, then destroys each socket of the agent\" },\n end: { final: true, note: \"the service closed the client\" },\n },\n events: {\n \"status.connecting\": { kind: \"local\" },\n join: { kind: \"rx\", note: \"a new socket of the agent connected: the request on it leaves the client (D1)\" },\n \"connect.failed\": { kind: \"local\", note: \"a connect failed: its error, or connect_bound\" },\n \"reuse.failed\": { kind: \"rx\", note: \"a request on a free socket that the agent reused failed: the server closed that socket\" },\n \"command.reissue\": { kind: \"local\" },\n \"close.request\": { kind: \"local\" },\n \"close.bound\": { kind: \"timer\", note: \"a call in flight did not end in close_bound\" },\n \"close.destroy\": { kind: \"local\", note: \"the bound destroys each socket of the agent, and each call in flight is indeterminate\" },\n \"status.end\": { kind: \"local\" },\n },\n knobs: {\n connect_bound: knob(\"connect.bound\"),\n request_timeout: knob(\"request.bound\"),\n pool_idle_expiry: knob(\"timeout\"),\n hint_buffer: knob(\"agentKeepAliveTimeoutBuffer\"),\n keepalive_idle: knob(\"keepAliveMsecs\"),\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: [\"join\", \"connect.failed\"],\n fires: \"connect.failed\",\n note: \"the timer of the wrapper on the connect of a new socket. The agent has no connect timer\",\n },\n close: closeTimer(),\n },\n transitions: [\n t(\"connect\", \"none\", \"status.connecting\", \"connecting\"),\n t(\"join\", [...SERVING], \"join\", \"ready\"),\n t(\"connect-failed\", [...SERVING], \"connect.failed\", \"failing\", { timer: \"connect_timeout\" }),\n // a request that is in flight at the close can still connect, or fail\n t(\"join-closing\", \"closing\", \"join\", \"closing\"),\n t(\"connect-failed-closing\", \"closing\", \"connect.failed\", \"closing\"),\n ...OPEN.map((state) => t(`reuse-failed-${state}`, state, \"reuse.failed\", state)),\n ...OPEN.map((state) => t(`reissue-${state}`, state, \"command.reissue\", state)),\n t(\"close\", [...SERVING], \"close.request\", \"closing\"),\n t(\"close-bound\", \"closing\", \"close.bound\", \"closing\", { timer: \"close\" }),\n t(\"close-destroy\", \"closing\", \"close.destroy\", \"closing\"),\n t(\"end\", \"closing\", \"status.end\", \"end\"),\n ],\n};\n","import { STARTING_POLL } from \"../client/core.ts\";\nimport type { Poll } from \"../timing.ts\";\nimport type { HttpClientKnobs } from \"./client.ts\";\n\n/**\n * This poll is the poll of the checks that `HTTP_CLIENT_STARTING_KNOBS` fits. It is `STARTING_POLL` of\n * 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.\n */\nexport const HTTP_CLIENT_STARTING_POLL: Poll = STARTING_POLL;\n\n/**\n * These knobs are a declaration to start from. They come from what the HTTP cells measured on the\n * agent of Node 24.21.0 against a Node server at its defaults. The runtime has no defaults: a service gives\n * these knobs, or its own. Each value holds the borders that the cells measured:\n * - `connect_bound` 5 s: the connect timeout of DP44. The agent has no connect timer.\n * - `request_timeout` 9.5 s: in the slowest poll of 10 s (O13). The agent has no timer of a call.\n * A frozen server held the call of undici until its headersTimeout, at 10.187 s.\n * - `pool_idle_expiry` 4 s: the free socket timer. The agent then closes a free socket at 4 s, the\n * hint of 5 s less `hint_buffer`. That is below the idle close of the server.\n * - `hint_buffer` 1 s: the default of the agent.\n * - `server_idle_close` 6 s: a Node server closes an idle connection at its `keepAliveTimeout` of 5 s\n * plus its buffer of 1 s. The cells measured 6.0003 s. O01 then has a margin of 2 s.\n * - `keepalive_idle` 1 s: the default of the agent. Node truncates it to whole seconds.\n * - `max_connection_age` \"infinite\": the agent does not replace a connection at an age.\n * - `close_bound` 2 s: a call in flight behind a black hole holds its socket with no bound.\n * - `retrans_giveup_time` 924.6 s: the give-up of Linux at `tcp_retries2 = 15`.\n */\nexport const HTTP_CLIENT_STARTING_KNOBS: HttpClientKnobs = {\n connect_bound: { value: 5, unit: \"s\" },\n request_timeout: { value: 9.5, unit: \"s\" },\n pool_idle_expiry: { value: 4, unit: \"s\" },\n hint_buffer: { value: 1, unit: \"s\" },\n server_idle_close: { value: 6, unit: \"s\" },\n keepalive_idle: { value: 1, unit: \"s\" },\n max_connection_age: { value: \"infinite\", unit: \"s\" },\n close_bound: { value: 2, unit: \"s\" },\n retrans_giveup_time: { value: 924.6, unit: \"s\" },\n};\n"],"mappings":";;;;;;;;AAyHA,MAAa,YAA0B,OAAO,YAAY;CAExD,MAAM,OAAQ,MAAM,OAAO;CAC3B,MAAM,QAAQ,IAAI,KAAK,MAAM,OAAO;CACpC,MAAM,OAAO,UAAqF,OAAO,OAAO,KAAK,CAAC,CAAC,SAAS,SAAS,QAAQ,CAAC,CAAC;CACnJ,OAAO;EACL,QAAQ,SAAS,QAAQ;GACvB,MAAM,MAAM,KAAK,QAAQ;IAAE,MAAM,QAAQ;IAAM,MAAM,QAAQ;IAAM,QAAQ,QAAQ;IAAQ,MAAM,QAAQ;IAAM,SAAS,QAAQ;IAAS;GAAM,CAAC;GAChJ,MAAM,WAAW,IAAI,SAAuB,SAAS,WAAW;IAC9D,IAAI,GAAG,WAAW,WAAW;KAC3B,OAAO,OAAO,QAAQ,IAAI,YAAY;KACtC,IAAI,OAAO,YAAY,OAAO,KAAK,iBAAiB,OAAO,QAAQ,CAAC;UAC/D,IAAI,CAAC,IAAI,cAAc,OAAO,QAAQ;IAC7C,CAAC;IACD,IAAI,GAAG,aAAa,aAAa;KAC/B,MAAM,SAAmB,CAAC;KAC1B,SAAS,GAAG,SAAS,UAAU,OAAO,KAAK,KAAK,CAAC;KACjD,SAAS,GAAG,SAAS,MAAM;KAC3B,SAAS,GAAG,aAAa,QAAQ;MAAE,QAAQ,SAAS,cAAc;MAAG,SAAS,SAAS;MAAS,MAAM,OAAO,OAAO,MAAM;KAAE,CAAC,CAAC;KAC9H,SAAS,GAAG,eAAe;MACzB,IAAI,CAAC,SAAS,UAAU,uBAAO,IAAI,MAAM,kDAAkD,CAAC;KAC9F,CAAC;IACH,CAAC;IACD,IAAI,GAAG,SAAS,MAAM;GACxB,CAAC;GACD,IAAI,IAAI,QAAQ,IAAI;GACpB,OAAO;IAAE;IAAU,QAAQ,UAAU,IAAI,QAAQ,KAAK;GAAE;EAC1D;EACA,eAAe,CAAC,GAAG,IAAI,MAAM,OAAO,GAAG,GAAG,IAAI,MAAM,WAAW,CAAC;EAChE,eAAe,MAAM,QAAQ;CAC/B;AACF;;;;;;;;;;;AC5IA,MAAa,sBAAsB,MAAc,UAAsC;CACrF,MAAM,OAAiB;EACrB,IAAI;EACJ,QAAQ,MAAM;EACd,WAAW;EACX,QAAQ,gEAAgE,KAAK;CAC/E;CACA,MAAM,cAAc,eAAoC;EACtD,IAAI;EACJ,QAAQ,MAAM;EACd;EACA,QAAQ;CACV;CACA,MAAM,SAAS,aAAa,MAAM,qBAAqB,iCAAiC;CACxF,MAAM,UAAU,mBAAyD;EACvE;EACA,MAAM,KAAK;EACX,WAAW;GAAC;GAAM;GAAQ,WAAW,MAAM;GAAG,gBAAgB,MAAM,cAAc;EAAC;CACrF;CACA,OAAO;EACL;GACE,eAAe;GACf,MAAM,KAAK;GACX,WAAW;IACT,UAAU,aAAa,0FAA0F;IACjH,WAAW,MAAM;IACjB,eAAe,MAAM,cAAc;GACrC;EACF;EACA;GACE,eAAe;GACf,MAAM,KAAK;GACX,WAAW,CACT,UAAU,aAAa,yDAAuD,GAC9E,UAAU,QAAQ,6EAA6E,CACjG;EACF;EACA,OAAO,WAAW;EAClB,OAAO,aAAa;EACpB;GACE,eAAe;GACf,MAAM,KAAK;GACX,WAAW;IAAC;IAAM,WAAW,MAAM;IAAG,kBAAkB;GAAC;EAC3D;EACA;GACE,eAAe;GACf,MAAM;GACN,WAAW;IAAC;IAAM,WAAW,MAAM;IAAG,kBAAkB;GAAC;EAC3D;CACF;AACF;;;;;;;ACsBA,MAAa,eAA6B;CAAE,QAAQ;CAAW,MAAM;AAAI;;;;;AA4CzE,MAAa,kBAAuD;CAClE,KAAK,EAAE,YAAY,KAAK;CACxB,MAAM,EAAE,YAAY,KAAK;CACzB,SAAS,EAAE,YAAY,KAAK;CAC5B,OAAO,EAAE,YAAY,KAAK;CAC1B,KAAK,EAAE,YAAY,KAAK;CACxB,QAAQ,EAAE,YAAY,KAAK;CAC3B,MAAM,EAAE,YAAY,MAAM;CAC1B,OAAO,EAAE,YAAY,MAAM;AAC7B;;AAGA,MAAa,gBAAgB,MAAc,SAAmC;CAC5E,MAAM,EAAE,UAAU;CAClB,MAAM,WAAW;EACf,GAAG,aAAa,MAAM,KAAK;EAE3B,GAAG,qBAAqB,MAAM,OAAO;GAAE,eAAe;GAAa,iBAAiB;EAAS,CAAC;EAE9F,GAAG,aAAa,MAAM,aAAa,OAAO,EAAE,SAAS,CAAC,kBAAkB,EAAE,CAAC;EAC3E,GAAG,oBAAoB,MAAM,OAAO,WAAW;EAE/C,GAAG,cAAc,MAAM,OAAO;GAAE,QAAQ;IAAC;IAAiB;IAAmB;IAAoB;GAAa;GAAG,SAAS,CAAC,gBAAgB;EAAE,CAAC;EAC9I,GAAG,aAAa,MAAM;GAAE,KAAK,KAAK,KAAK;GAAK,KAAK;EAAE,CAAC;EACpD,GAAG,YAAY,MAAM,OAAO,mDAAmD;CACjF;CACA,IAAI,CAAC,OAAO,SAAS,GAAG,MAAM,WAAW,CAAC,GAAG,SAAS,KAAK,GAAG,KAAK,wFAAwF;CAC3J,OAAO;AACT;;AAGA,MAAa,gBAAgB,SAA2C;CACtE,MAAM,EAAE,UAAU;CAClB,OAAO;EACL,WAAW;EACX,SAAS,GAAG,MAAM,gBAAgB;EAClC,6BAA6B,GAAG,MAAM,WAAW;EACjD,gBAAgB,GAAG,MAAM,cAAc;EACvC,YAAY,KAAK,KAAK;EACtB,gBAAgB,KAAK,KAAK;EAC1B,YAAY;CACd;AACF;;AAGA,MAAa,kBAAkB,MAAc,SAAyB,+BAA+B,UAAU,MAAM,IAAI,EAAE;;AAG3H,MAAa,cAAqB,sBAAsB,IAAI;;AAG5D,MAAa,YAAoB,MAAM,MAAM,WAAW,UAAU,eAAe,MAAM,IAAI,GAAG,WAAW,CAAC,CAAC,MAAM,MAAM,MAAM;;;;;;;;;;;;;;;AAgB7H,MAAa,cAAc,MAAc,MAAsB,UAA6B,CAAC,MAAkB;CAC7G,MAAM,WAAW,aAAa,MAAM,IAAI;CACxC,IAAI,SAAS,SAAS,GAAG,MAAM,IAAI,MAAM,SAAS,KAAK,IAAI,CAAC;CAC5D,MAAM,EAAE,UAAU;CAClB,MAAM,QAAQ,QAAQ,SAAS;CAC/B,MAAM,SAAS,QAAQ,UAAU;CACjC,MAAM,OAAO,QAAQ,QAAQ;CAC7B,MAAM,aAAa,eAAe,iBAAiB,KAAK,UAAU;CAClE,MAAM,iBAAiB,GAAG,MAAM,eAAe;CAC/C,MAAM,eAAe,GAAG,MAAM,aAAa;CAC3C,MAAM,QAAQ,KAAK,SAAS;CAE5B,IAAI,QAA0B;CAC9B,IAAI,SAAS;CACb,MAAM,QAAsB,CAAC;CAE7B,MAAM,SAAS,SAAS;CAExB,MAAM,0BAAU,IAAI,IAA6B;CAEjD,MAAM,OAAO,eAAe,QAAQ,QAAQ,OAAO,IAAI;CAEvD,MAAM,UAAU,IAAY,KAAa,QAAQ,WAA0B;EACzE,MAAM,OAAO;EACb,SAAS;EACT,KAAK,MAAM,IAAI,IAAI,MAAM;CAC3B;CACA,MAAM,gBAAyB,WAAW,aAAa,WAAW;;;;;;CAOlE,MAAM,UAAU,OAAO,MAAgB,WAAoE;EACzG,MAAM,IAAI;EACV,IAAI,MAAM,MAAM,OAAO,QAAQ,GAAG,KAAK,mBAAmB;EAC1D,IAAI,QAAQ,GAAG,OAAO,QAAQ,GAAG,KAAK,MAAM,OAAO,uCAAuC;EAC1F,IAAI,QAAQ,SAAS,OAAO,QAAQ,0DAA0D;EAC9F,MAAM,UAAU,MAAM,IAAI;EAC1B,MAAM,OAAO,OAAO,KAAK,SAAS,WAAW,OAAO,KAAK,KAAK,MAAM,MAAM,IAAI,KAAK;EACnF,MAAM,UAAU;GAAE,GAAG,KAAK;GAAS,GAAI,SAAS,KAAA,IAAY,CAAC,IAAI,EAAE,kBAAkB,OAAO,KAAK,UAAU,EAAE;EAAG;EAEhH,IAAI;EACJ,IAAI,SAAS;EACb,IAAI,OAAO;EAEX,IAAI,SAAS;EACb,IAAI,qBAA6B,CAAC;EAClC,IAAI,eAAuD,CAAC;EAC5D,MAAM,UAAU,IAAI,SAAgC,YAAY;GAC9D,IAAI,UAAU;GACd,UAAU,UAAU;IAClB,IAAI,SAAS;IACb,UAAU;IACV,QAAQ,KAAK;GACf;EACF,CAAC;EAED,IAAI,UAA0E;EAC9E,IAAI,QAA6F;EACjG,IAAI,UAAU;EACd,MAAM,cAAc,OAA8B,UAAwB;GACxE,IAAI,YAAY,MAAM,UAAU;IAAE;IAAO;GAAM;QAC1C,QAAQ,OAAO,KAAK;EAC3B;EAEA,IAAI,WAAiC;EACrC,MAAM,QAAQ,SAAuB;GACnC,IAAI,MAAM;GACV,SAAS;GACT,aAAa;GACb,UAAU,MAAM,IAAI,MAAM,IAAI,CAAC;GAC/B,OAAO,QAAQ,IAAI,CAAC;EACtB;EACA,MAAM,cAAoB;GACxB,OAAO;GACP,aAAa;GACb,OAAO,IAAI;EACb;EACA,WAAW,EAAE,QACX;GAAE,MAAM,KAAK;GAAM,MAAM,KAAK;GAAM,QAAQ,KAAK;GAAQ,MAAM,KAAK;GAAM;GAAS;EAAK,GACxF;GACE,OAAO,OAAO,UAAU;IACtB,SAAS;IACT,SAAS;IACT,IAAI,UAAU;KACZ,MAAM;KACN;IACF;IACA,eAAe,MAAM,MAAM,oBAAoB;KAC7C,IAAI,MAAM;KACV,OAAO,kBAAkB,QAAQ,IAAI,SAAS,WAAW,gCAAgC;KACzF,KAAK,sEAAsE;IAC7E,CAAC;GACH;GACA,UAAU;IACR,MAAM,aAAa,MAAM,SAAS;IAClC,MAAM,KAAK;KAAE;KAAY,IAAI,MAAM,IAAI;KAAG,GAAG,WAAW,MAAM;IAAE,CAAC;IACjE,OAAO,QAAQ,QAAQ,IAAI,SAAS,SAAS,UAAU,YAAY;IACnE,MAAM;GACR;EACF,CACF;EAEA,SAAS,SAAS,MACf,aAAa,WAAW;GAAE,SAAS;GAAM,OAAO;EAAS,CAAC,IAC1D,UAAmB;GAClB,MAAM,QAAQ,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,QAAQ,KAAK,CAAC;GACvE,IAAI,CAAC,MAAM;IACT,aAAa;IACb,IAAI,CAAC,QAAQ,OAAO,kBAAkB,QAAQ,IAAI,SAAS,WAAW,QAAQ,KAAK,CAAC;IACpF,OAAO,QAAQ,QAAQ,KAAK,CAAC,CAAC;IAC9B;GACF;GAEA,IAAI,SAAS;GAEb,IAAI,QAAQ,OAAO,gBAAgB,QAAQ,QAAQ,KAAK,CAAC;GACzD,WAAW;IAAE,SAAS;IAAiB,OAAO,QAAQ,KAAK;GAAE,GAAG,KAAK;EACvE,CACF;EAEA,MAAM,kBAAwB,KAAK,4DAA4D;EAC/F,QAAQ,iBAAiB,SAAS,WAAW,EAAE,MAAM,KAAK,CAAC;EAC3D,MAAM,SAAS,MAAM,MAAM,sBAAsB,KAAK,oEAAoE,CAAC;EAC3H,QAAQ,IAAI,IAAI;EAChB,MAAM,MAAM,MAAM;EAClB,QAAQ,oBAAoB,SAAS,SAAS;EAC9C,OAAO;EACP,QAAQ,OAAO,IAAI;EACnB,IAAI,QAAQ,MAAM,OAAO;EAEzB,MAAM,OAAO;EACb,OAAO,eACL,QACA,OACA,KAAK,IAAI,GAAG,kBAAkB,MAAM,IAAI,IAAI,QAAQ,GACpD,QACA,6DACA,0EACC,WAAW;GACV,UAAU;GACV,IAAI,UAAU,MAAM,OAAO,MAAM,OAAO,MAAM,KAAK;EACrD,IACC,OAAO,UAAU;GAChB,UAAU;GAEV,IAAI,MAAM,YAAY,MAAM,KAAK,MAAM,SAAS,IAAI,MAAM,MAAM,KAAK,CAAC;EACxE,CACF;CACF;CAEA,MAAM,WAAW,MAAgB,cAA2B,CAAC,MAAmC;EAC9F,MAAM,SAAS,KAAK,OAAO,YAAY;EACvC,MAAM,QAAQ,KAAK,aAAa;EAChC,OAAO,gBAAgB;GACrB;GACA,OAAO;GACP,WAAW,WAAW;GACtB,SAAS;GACT,UAAU,WAAW,QAAQ;IAAE,GAAG;IAAM;GAAO,GAAG,MAAM;GACxD,iBAAiB,OAAO,mBAAmB,QAAQ,KAAK;EAC1D,CAAC;CACH;CAEA,MAAM,SAA2B;EAC/B,MAAM,QAAQ,QAAQ;GACpB,OAAO,qBAAqB,YAAY;GACxC,QAAQ,MAAM,OAAO,aAAa,IAAI,CAAC;GACvC,IAAI,OAAO,SAAS,MAAM,IAAI,MAAM,GAAG,KAAK,yCAAyC;GAGrF,MAAM,QAAQ,OAAO;IAAE;IAAQ,SAAS;GAAM,CAAC;EACjD;EACA,MAAM,QAAQ;GACZ,MAAM,IAAI;GACV,IAAI,MAAM,QAAQ,QAAQ,GAAG;GAC7B,OAAO,iBAAiB,SAAS;GACjC,KAAK,MAAM,QAAQ,CAAC,GAAG,OAAO,GAAG,KAAK,2CAA2C;GAEjF,IAAI,CAAE,MAAM,YAAY,OAAO,GAAG,MAAM,WAAW,GAAG,OAAO,KAAK,CAAC,GAAI;IACrE,OAAO,aAAa;IACpB,MAAM,OAAO,EAAE,QAAQ,CAAC,CAAC;IACzB,OAAO,UAAU,8CAA8C;IAC/D,EAAE,QAAQ;IACV,OAAO,iBAAiB,QAAQ,iBAAiB,MAAM;GACzD,OAEE,EAAE,QAAQ;GAEZ,OAAO,cAAc,KAAK;EAC5B;CACF;CAEA,MAAM,cAAwC;EAC5C,QAAQ,KAAK;EACb,OAAO;GACL,eAAe,MAAM;GACrB,iBAAiB,MAAM;GACvB,kBAAkB,MAAM;GACxB,aAAa,MAAM;GACnB,gBAAgB,MAAM;GAEtB,uBAAuB,MAAM;GAC7B,oBAAoB,MAAM;GAC1B,aAAa,MAAM;GACnB,qBAAqB,MAAM;EAC7B;EACA;CACF;CAEA,MAAM,UAAU,iBACd,WAAuB,MAAM,cAAc;EAEzC,eAAe,sBAAsB,WAAW,OAAO;EAEvD,cAAc,eAAe,OAAO,gBAAgB;GAClD,MAAM,OAAO,MAAM,QAAQ,OAAO,WAAW;GAC7C,OAAO,KAAK,YAAY,QAAQ,KAAK,MAAM,UAAU,MACjD;IAAE,SAAS;IAAS,OAAO,uBAAuB,KAAK,MAAM;IAAU,OAAO;IAAM,UAAU,KAAK;GAAS,IAC5G;EACN,CAAC;EAED,MAAM,UAAU,MAAM,KAAK,MAAM,KAAK,IAAI;CAC5C,CAAC;CAEH,OAAO;EACL;EACA;EACA;EACA,OAAO,mBAAmB,MAAM,KAAK;EACrC,IAAI,SAAS;GACX,OAAO;EACT;EACA;EACA;EACA;EACA,SAAS,KAAK,iBAAiB,aAAa,KAAK,MAAM,aAAa,QAAQ,OAAO,YAAY,CAAC;EAChG,SAAS,aAAa;GAEpB,MAAM,OAAO,iBAAiB,MAAM,MAAM,kBAAkB,MAAM,iBAAiB;GACnF,OAAO,CAAC,GAAG,eAAe,MAAM,aAAa,aAAa,KAAK,CAAC,GAAG,GAAI,SAAS,OAAO,CAAC,IAAI,CAAC,IAAI,CAAE;EACrG;CACF;AACF;;;;ACpbA,MAAM,UAAU;CAAC;CAAc;CAAS;AAAS;AACjD,MAAM,OAAO,CAAC,GAAG,SAAS,SAAS;;;;;;;;;;;;;AAcnC,MAAa,sBAA+B;CAC1C,QAAQ;CACR,IAAI;CACJ,OAAO;CACP,OAAO;CACP,SAAS;CACT,SAAS,CAAC;EAAE,KAAK;EAA0E,MAAM;CAAc,CAAC;CAChH,QAAQ;EACN,MAAM;GAAE,SAAS;GAAM,MAAM;EAA4C;EACzE,YAAY,EAAE,MAAM,0DAA0D;EAC9E,OAAO,EAAE,MAAM,gHAAgH;EAC/H,SAAS,EAAE,MAAM,sGAAsG;EACvH,SAAS,EAAE,MAAM,kGAAkG;EACnH,KAAK;GAAE,OAAO;GAAM,MAAM;EAAgC;CAC5D;CACA,QAAQ;EACN,qBAAqB,EAAE,MAAM,QAAQ;EACrC,MAAM;GAAE,MAAM;GAAM,MAAM;EAAgF;EAC1G,kBAAkB;GAAE,MAAM;GAAS,MAAM;EAAgD;EACzF,gBAAgB;GAAE,MAAM;GAAM,MAAM;EAAyF;EAC7H,mBAAmB,EAAE,MAAM,QAAQ;EACnC,iBAAiB,EAAE,MAAM,QAAQ;EACjC,eAAe;GAAE,MAAM;GAAS,MAAM;EAA8C;EACpF,iBAAiB;GAAE,MAAM;GAAS,MAAM;EAAwF;EAChI,cAAc,EAAE,MAAM,QAAQ;CAChC;CACA,OAAO;EACL,eAAeA,WAAK,eAAe;EACnC,iBAAiBA,WAAK,eAAe;EACrC,kBAAkBA,WAAK,SAAS;EAChC,aAAaA,WAAK,6BAA6B;EAC/C,gBAAgBA,WAAK,gBAAgB;EACrC,aAAaA,WAAK,aAAa;CACjC;CACA,QAAQ;EACN,iBAAiB;GACf,UAAU;IAAE,MAAM;IAAiB,MAAM;GAAI;GAC7C,MAAM,CAAC,kBAAkB;GACzB,SAAS,CAAC,QAAQ,gBAAgB;GAClC,OAAO;GACP,MAAM;EACR;EACA,OAAO,WAAW;CACpB;CACA,aAAa;EACXC,WAAE,WAAW,QAAQ,qBAAqB,YAAY;EACtDA,WAAE,QAAQ,CAAC,GAAG,OAAO,GAAG,QAAQ,OAAO;EACvCA,WAAE,kBAAkB,CAAC,GAAG,OAAO,GAAG,kBAAkB,WAAW,EAAE,OAAO,kBAAkB,CAAC;EAE3FA,WAAE,gBAAgB,WAAW,QAAQ,SAAS;EAC9CA,WAAE,0BAA0B,WAAW,kBAAkB,SAAS;EAClE,GAAG,KAAK,KAAK,UAAUA,WAAE,gBAAgB,SAAS,OAAO,gBAAgB,KAAK,CAAC;EAC/E,GAAG,KAAK,KAAK,UAAUA,WAAE,WAAW,SAAS,OAAO,mBAAmB,KAAK,CAAC;EAC7EA,WAAE,SAAS,CAAC,GAAG,OAAO,GAAG,iBAAiB,SAAS;EACnDA,WAAE,eAAe,WAAW,eAAe,WAAW,EAAE,OAAO,QAAQ,CAAC;EACxEA,WAAE,iBAAiB,WAAW,iBAAiB,SAAS;EACxDA,WAAE,OAAO,WAAW,cAAc,KAAK;CACzC;AACF;;;;;;;ACrEA,MAAa,4BAAkC;;;;;;;;;;;;;;;;;;AAmB/C,MAAa,6BAA8C;CACzD,eAAe;EAAE,OAAO;EAAG,MAAM;CAAI;CACrC,iBAAiB;EAAE,OAAO;EAAK,MAAM;CAAI;CACzC,kBAAkB;EAAE,OAAO;EAAG,MAAM;CAAI;CACxC,aAAa;EAAE,OAAO;EAAG,MAAM;CAAI;CACnC,mBAAmB;EAAE,OAAO;EAAG,MAAM;CAAI;CACzC,gBAAgB;EAAE,OAAO;EAAG,MAAM;CAAI;CACtC,oBAAoB;EAAE,OAAO;EAAY,MAAM;CAAI;CACnD,aAAa;EAAE,OAAO;EAAG,MAAM;CAAI;CACnC,qBAAqB;EAAE,OAAO;EAAO,MAAM;CAAI;AACjD"}
package/build/http.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { n as Clock } from "./clock-mVqhXD0b.js";
2
2
  import { d as Refused } from "./policy-G4QJ1ZZq.js";
3
- import { d as TransportImpl, m as RequestEnd, n as HealthServer, r as Lifecycle, v as RequestStream, w as ProbeKind, x as Health, y as RequestWork } from "./lifecycle-CGEegMI0.js";
3
+ import { d as TransportImpl, m as RequestEnd, n as HealthServer, r as Lifecycle, v as RequestStream, w as ProbeKind, x as Health, y as RequestWork } from "./lifecycle-fgYfxrHK.js";
4
4
  import { Extent, Quantity } from "@mark1russell7/quantity";
5
5
  import { ListenOptions } from "node:net";
6
6
  import { IncomingMessage, Server, ServerEventMap, ServerResponse } from "node:http";
package/build/index.d.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  import { a as backoffDelay, i as backoffBand, n as DelayBand, o as backoffProblems, r as Jitter, t as Backoff } from "./backoff-B-jGWVDm.js";
2
2
  import { a as realClock, i as platformClock, n as Clock, r as PlatformClockOptions, t as Cancel } from "./clock-mVqhXD0b.js";
3
- import { Heartbeat, HeartbeatOptions, detectionTime, heartbeat } from "./heartbeat.js";
4
3
  import { S as retryAfterSeconds, _ as freeAt, a as LimitRule, b as limiterProblems, c as LimiterSpec, d as Refused, f as SlidingWindow, g as decide, h as TokenBucket, i as LimitPolicy, l as LimiterStoreSpec, m as TakeStep, n as Decision, o as Limiter, p as StoreFailure, r as KeyState, s as LimiterEviction, t as Allowed, u as RefusalCause, v as idleSpan, x as longestRetryAfter, y as limitRule } from "./policy-G4QJ1ZZq.js";
5
- import { $ as failSpan, A as LifecycleEvent, B as Gate, C as ProbeAnswer, Ct as Operation, D as Outcome, E as CheckFn, F as CooperativeFloor, G as Step, H as Phase, I as Coupling, J as Poll, K as Transport, L as Dependency, M as Recorder, N as Check, O as Result, P as Controller, Q as asWindow, R as ExitCodes, S as HealthVerdict, St as Idempotency, T as StartOptions, U as RepeatSignal, V as MessageTrigger, W as ServiceDescription, X as Timing, Y as Span, Z as asDebounce, _ as RequestOptions, _t as isExitSignal, a as PhaseContext, at as EXIT_SIGNALS, b as CheckVerdict, bt as nodeProcess, c as STAGES, ct as ExitTrigger, d as TransportImpl, dt as PROCESS_TRIGGERS, et as heartbeatDetection, f as cooperativeAbortAt, ft as ProcessHost, g as RequestFn, gt as isExitCode, h as RequestEvent, ht as embeddedProcess, i as LifecycleState, it as RouteRequest, j as MachineName, l as ShutdownOptions, lt as HostStream, m as RequestEnd, mt as ProcessTrigger, n as HealthServer, nt as OperationOf, o as PhaseWork, ot as EmbeddedProcess, p as stageRank, pt as ProcessPort, q as FailsBy, r as Lifecycle, rt as RoutePattern, s as RunOptions, st as ExitSignal, t as DependencyHandle, tt as recoverSpan, u as Stage, ut as MessageShape, v as RequestStream, vt as isMessageShape, w as ProbeKind, wt as mayReissue, x as Health, xt as processPortOf, y as RequestWork, yt as matchesMessage, z as FORMAT } from "./lifecycle-CGEegMI0.js";
6
- import { a as WatchdogSpec, c as Profile, d as kubernetes, f as localDev, i as Watchdog, l as daemon, n as ServiceBuilder, o as watchdog, r as service, s as PodTiming, t as ControllerBuilder, u as ephemeral } from "./builder-Dn0cby6Q.js";
4
+ import { $ as asWindow, A as LifecycleEvent, B as Gate, C as ProbeAnswer, Ct as Idempotency, D as Outcome, E as CheckFn, F as CooperativeFloor, G as ServiceDescription, H as Phase, I as Coupling, J as FailsBy, K as Step, L as Dependency, M as Recorder, N as Check, O as Result, P as Controller, Q as asDebounce, R as ExitCodes, S as HealthVerdict, St as processPortOf, T as StartOptions, Tt as mayReissue, U as ProgressMarks, V as MessageTrigger, W as RepeatSignal, X as Span, Y as Poll, Z as Timing, _ as RequestOptions, _t as isExitCode, a as PhaseContext, at as RouteRequest, b as CheckVerdict, bt as matchesMessage, c as STAGES, ct as ExitSignal, d as TransportImpl, dt as MessageShape, et as failSpan, f as cooperativeAbortAt, ft as PROCESS_TRIGGERS, g as RequestFn, gt as embeddedProcess, h as RequestEvent, ht as ProcessTrigger, i as LifecycleState, it as RoutePattern, j as MachineName, l as ShutdownOptions, lt as ExitTrigger, m as RequestEnd, mt as ProcessPort, n as HealthServer, nt as recoverSpan, o as PhaseWork, ot as EXIT_SIGNALS, p as stageRank, pt as ProcessHost, q as Transport, r as Lifecycle, rt as OperationOf, s as RunOptions, st as EmbeddedProcess, t as DependencyHandle, tt as heartbeatDetection, u as Stage, ut as HostStream, v as RequestStream, vt as isExitSignal, w as ProbeKind, wt as Operation, x as Health, xt as nodeProcess, y as RequestWork, yt as isMessageShape, z as FORMAT } from "./lifecycle-fgYfxrHK.js";
5
+ import { a as WatchdogSpec, c as Profile, d as kubernetes, f as localDev, i as Watchdog, l as daemon, n as ServiceBuilder, o as watchdog, r as service, s as PodTiming, t as ControllerBuilder, u as ephemeral } from "./builder-DawJhq4p.js";
6
+ import { Heartbeat, HeartbeatOptions, detectionTime, heartbeat } from "./heartbeat.js";
7
7
  import { a as keyedLimiter, i as TakeRequest, n as LimiterStore, o as memoryStore, r as MemoryStore, t as KeyedLimiter } from "./store-D5HLgBnW.js";
8
8
  import { a as limiterStateOf, i as limiterMachine, n as SLIDING_WINDOW_MACHINE, r as TOKEN_BUCKET_MACHINE, t as LimiterState } from "./limiter-B0849Vhp.js";
9
+ import { a as JobSource, c as QueueKnobs, d as queueDrain, f as redeliverOnce, i as JobSettle, l as RedeliveryPolicy, n as JobContext, o as QueueDrain, p as UnitRequest, r as JobFailure, s as QueueDrainOptions, t as JobAction, u as RequeueCause } from "./queue-snRHK3Jm.js";
9
10
  import { Extent, Quantity } from "@mark1russell7/quantity";
10
11
  import { replay } from "@mark1russell7/algebra/replay";
11
12
  import { Machine } from "@mark1russell7/algebra";
@@ -152,28 +153,16 @@ export declare const serviceBindings: (svc: ServiceDescription, scenario: Scenar
152
153
  };
153
154
  /** This identifier is the rule of the findings about a field that the declaration does not give. */
154
155
  export declare const DECLARATION_RULE = "declaration";
156
+ /** This identifier is the rule of the findings about the stall time of a progress check (`Check.marks`). */
157
+ export declare const PROGRESS_RULE = "progress";
155
158
  /**
156
159
  * This function applies each service rule to a declaration and a scenario. A rule applies one time
157
160
  * for the service, for each client, for each consumer and worker, or for each limiter. The function
158
161
  * also gives a finding for each necessary field that the description does not give (`DECLARATION_RULE`).
162
+ * Each progress check that names its loops also gets its findings (`PROGRESS_RULE`).
159
163
  */
160
164
  export declare const lint: (svc: ServiceDescription, scenario: Scenario, rules: readonly Rule[]) => Finding[];
161
165
  //#endregion
162
- //#region src/units.d.ts
163
- /**
164
- * A unit request names the route of each unit of work in the request tracking of the service. Then
165
- * the service admits, counts, drains and cuts each unit as it does each request (`life.request`).
166
- * The controller declares the route, thus its idempotency tells if a cut unit can go again.
167
- */
168
- interface UnitRequest<U> {
169
- /** This name is the controller that declares the route. */
170
- readonly controller: string;
171
- /** This name is the route of each unit, or a function that gives it from the unit. A route can match a route pattern. */
172
- readonly route: string | ((unit: U) => string);
173
- /** This function gives the detail of a unit, for the function of a route pattern. */
174
- readonly detail?: ((unit: U) => unknown) | undefined;
175
- }
176
- //#endregion
177
166
  //#region src/worker.d.ts
178
167
  /**
179
168
  * These are the knobs of a worker loop, by the names of the ordering law where the law has them.
@@ -263,136 +252,6 @@ interface WorkerLoop extends TransportImpl {
263
252
  */
264
253
  export declare const workerLoop: <S = undefined>(knobs: WorkerKnobs, options: WorkerLoopOptions<S>) => WorkerLoop;
265
254
  //#endregion
266
- //#region src/queue.d.ts
267
- /**
268
- * These are the knobs of a queue drain, by the names of the ordering law where the law has them.
269
- * The drain has no defaults, thus each knob is necessary. A bound that nobody gives is "infinite".
270
- */
271
- interface QueueKnobs {
272
- /** This time is the wait after a take that gives no job, before the next take. A time without a bound waits for `wake()`. */
273
- readonly queue_poll_interval: Extent;
274
- /**
275
- * This time is the most that one job can take. At it, the drain aborts the signal of the job, and
276
- * a throw after it is a failure. The law reads it as `unit_processing_time@consumer` (O20).
277
- */
278
- readonly unit_processing_time: Extent;
279
- /** This time is the delay after the first failed take in sequence, or after a refusal of the service. The delay is two times longer after each one. */
280
- readonly retry_base: Quantity;
281
- /** This time is the most that one delay can be, before its jitter. */
282
- readonly retry_cap: Quantity;
283
- /** This time bounds the close: the end of the jobs after their abort, and then the close of the source. */
284
- readonly close_bound: Quantity;
285
- /** This count (the unit `count`) is the most jobs in flight at one time. A count without a bound takes each job that comes. */
286
- readonly concurrency: Extent;
287
- /** The other knobs go to the declaration, for example `lease_duration@broker`. The drain does not read them. */
288
- readonly [knob: string]: Extent;
289
- }
290
- /**
291
- * A job source gives the jobs of a queue drain:
292
- * - A function takes the next job, or gives null when no job is ready. Its signal aborts when the intake stops.
293
- * - An async iterable gives the jobs in sequence. The drain asks for the next job only when it has a free slot.
294
- * At the stop of the intake, the drain ends the iterator through `return()`.
295
- */
296
- type JobSource<J> = ((signal: AbortSignal) => J | null | undefined | PromiseLike<J | null | undefined>) | AsyncIterable<J>;
297
- /**
298
- * A job action is what the drain does with a job that failed:
299
- * - `requeue`: the job goes back on the queue.
300
- * - `reject`: the job leaves the queue without a success, for example to the dead-letter route of the queue.
301
- * - `ack`: the job leaves the queue, as a poison message does.
302
- */
303
- type JobAction = "requeue" | "reject" | "ack";
304
- /** A job failure is a job whose handler failed, with the count of its deliveries, 1 at the first. */
305
- interface JobFailure<J> {
306
- readonly job: J;
307
- readonly error: unknown;
308
- readonly delivery: number;
309
- }
310
- /** A redelivery policy gives the action for a job that failed. The queue declares it, and it has no default (S6 A11). */
311
- type RedeliveryPolicy<J> = (failure: JobFailure<J>) => JobAction;
312
- /**
313
- * A requeue cause tells why a job goes back on the queue:
314
- * - `failed`: the redelivery policy sent it back after a failure.
315
- * - `unstarted`: the drain took the job and did not start it, because its intake stopped or the service refused it.
316
- * - `cut`: the shutdown cut the job, and its route declares that it can go again.
317
- */
318
- type RequeueCause = "failed" | "unstarted" | "cut";
319
- /** A job settle is how the drain tells the queue the end of each job. Each function can give a promise. */
320
- interface JobSettle<J> {
321
- /** This function removes the job from the queue: a success, or the action `ack`. */
322
- ack(job: J): void | PromiseLike<void>;
323
- /** This function puts the job back on the queue, or hands it back to the queue at the stop. */
324
- requeue(job: J, cause: RequeueCause): void | PromiseLike<void>;
325
- /** This function removes the job from the queue without a success: the action `reject`, or a cut job that cannot go again. */
326
- reject(job: J, error: unknown): void | PromiseLike<void>;
327
- /**
328
- * This function gives the count of deliveries of a job from the queue, 1 at the first. The queue
329
- * knows it, for example `x-delivery-count` of RabbitMQ or `ApproximateReceiveCount` of SQS. A
330
- * queue in memory counts for itself. The drain has no default, because a broker gives a new object at each delivery.
331
- */
332
- delivery(job: J): number;
333
- }
334
- /** A job context is what the drain gives the handler of each job. */
335
- interface JobContext {
336
- /** This signal aborts at the cooperative abort of the drain, at the bound of the job, and at the close. */
337
- readonly signal: AbortSignal;
338
- /** This signal aborts when the intake stops. */
339
- readonly intake: AbortSignal;
340
- /** This count is the delivery of the job, 1 at the first. */
341
- readonly delivery: number;
342
- /** This lifecycle is the lifecycle that the drain got at its start. */
343
- readonly life: Lifecycle;
344
- }
345
- /** These options make a queue drain. Each function can give a promise. */
346
- interface QueueDrainOptions<J> {
347
- readonly source: JobSource<J>;
348
- /** This function does one job. A throw is a failure, and the redelivery policy then gives its action. */
349
- readonly handle: (job: J, context: JobContext) => unknown;
350
- readonly settle: JobSettle<J>;
351
- /** This policy gives the action for each failed job. `redeliverOnce` gives the policy of S6 A11. */
352
- readonly redelivery: RedeliveryPolicy<J>;
353
- /** This function closes the source at the close, after the jobs end: for example, it closes a channel. */
354
- readonly close?: (() => void | PromiseLike<void>) | undefined;
355
- /** This clock is the clock of the lifecycle. The polls, the backoffs and the bounds use it. The default is `realClock`. */
356
- readonly clock?: Clock | undefined;
357
- /** This function gives the random numbers of the jitter, from 0 up to 1. The default is `Math.random`. */
358
- readonly random?: (() => number) | undefined;
359
- /**
360
- * This route puts each job through `life.request`: the service then admits it, counts it, drains
361
- * it and can cut it. The idempotency of the route tells if a cut job goes back on the queue.
362
- */
363
- readonly request?: UnitRequest<J> | undefined;
364
- /**
365
- * This function receives each failure: of a take, of a job, of a settle and of a policy. It also
366
- * receives the end of a source while the intake is open. The job is undefined for a take and for that end.
367
- */
368
- readonly onError?: ((error: unknown, job: J | undefined) => void) | undefined;
369
- }
370
- /** A queue drain is a transport of the lifecycle whose work is the jobs of a queue (S6 A11). */
371
- interface QueueDrain extends TransportImpl {
372
- /** These knobs go to the declaration of the consumer: `svc.consumer(name, drain.knobs, drain)`. */
373
- readonly knobs: QueueKnobs;
374
- /** This method ends the wait of a poll at once, for example when a job comes. It gives false after the intake stops. */
375
- wake(): boolean;
376
- /** This method gives the problems of the knobs in a declaration: the close bound and the bound of a job against the caps. */
377
- problems(description: ServiceDescription): string[];
378
- }
379
- /**
380
- * This function gives the redelivery policy of S6 A11. A poison message gets `ack`. Each other job
381
- * goes back on the queue after its first failure, one time. After a later failure, `then` gives the
382
- * action: the declared policy of the queue.
383
- */
384
- export declare const redeliverOnce: <J>(then: JobAction | RedeliveryPolicy<J>, poison?: (error: unknown, job: J) => boolean) => RedeliveryPolicy<J>;
385
- /**
386
- * This function makes a queue drain as a transport (S6 A11). One taker takes the jobs from the
387
- * source while a slot is free, and each job operates in its slot.
388
- * - A success gets `ack`. A failure gets the action of the redelivery policy.
389
- * - `stopIntake` takes no new job, and a job that the drain took and did not start goes back (`requeue`, `unstarted`).
390
- * - `drain` waits for each job in flight until the deadline, and gives it the cooperative abort.
391
- * - `close` aborts each job, waits for its end, and closes the source, all in `close_bound`. A job
392
- * that does not end in the bound gets `reject`.
393
- */
394
- export declare const queueDrain: <J>(knobs: QueueKnobs, options: QueueDrainOptions<J>) => QueueDrain;
395
- //#endregion
396
255
  //#region src/machines.d.ts
397
256
  /**
398
257
  * The startup gate opens one time. The lifecycle moves it from `closed` to `starting` while its
@@ -433,5 +292,5 @@ export declare const drainMachine: (description: ServiceDescription) => Machine;
433
292
  /** These are the static machines of the runtime, by the name that their events carry. */
434
293
  export declare const MACHINES: Readonly<Record<Exclude<MachineName, "drain" | "client">, Machine>>;
435
294
  //#endregion
436
- export { type Allowed, type Backoff, type Binding, type Bound, type Cancel, type Check, type CheckFn, type CheckVerdict, type Clock, type Controller, type ControllerBuilder, type CooperativeFloor, type Coupling, type Decision, type DelayBand, type Dependency, type DependencyHandle, EXIT_SIGNALS, type EmbeddedProcess, type Evaluated, type ExitCodes, type ExitSignal, type ExitTrigger, FORMAT, type FailsBy, type Finding, type Gate, type Health, type HealthServer, type HealthVerdict, type Heartbeat, type HeartbeatOptions, type HostStream, type Idempotency, type Inequality, type IterationContext, type Jitter, type JobAction, type JobContext, type JobFailure, type JobSettle, type JobSource, type KeyState, type KeyedLimiter, type Lifecycle, type LifecycleEvent, type LifecycleState, type LimitPolicy, type LimitRule, type Limiter, type LimiterEviction, type LimiterSpec, type LimiterState, type LimiterStore, type LimiterStoreSpec, type MachineName, type MemoryStore, type MessageShape, type MessageTrigger, type Operation, type OperationOf, type Outcome, PROCESS_TRIGGERS, type Phase, type PhaseContext, type PhaseWork, type PlatformClockOptions, type PodTiming, type Poll, type ProbeAnswer, type ProbeKind, type ProcessHost, type ProcessPort, type ProcessTrigger, type Profile, type QueueDrain, type QueueDrainOptions, type QueueKnobs, type Recorder, type RedeliveryPolicy, type RefusalCause, type Refused, type RepeatSignal, type RequestEnd, type RequestEvent, type RequestFn, type RequestOptions, type RequestStream, type RequestWork, type RequeueCause, type Result, type RoutePattern, type RouteRequest, type Rule, type RunOptions, SLIDING_WINDOW_MACHINE, STAGES, type Scenario, type ServiceBuilder, type ServiceDescription, type ShutdownOptions, type Side, type SlidingWindow, type Span, type Stage, type StartOptions, type Step, type StoreFailure, TOKEN_BUCKET_MACHINE, type TakeRequest, type TakeStep, type Term, type Timing, type TokenBucket, type Transport, type TransportImpl, type UnitRequest, type Verdict, type Watchdog, type WatchdogSpec, type WorkerKnobs, type WorkerLoop, type WorkerLoopOptions, asDebounce, asWindow, backoffBand, backoffDelay, backoffProblems, cooperativeAbortAt, daemon, decide, detectionTime, embeddedProcess, ephemeral, failSpan, freeAt, heartbeat, heartbeatDetection, idleSpan, isExitCode, isExitSignal, isMessageShape, keyedLimiter, kubernetes, limitRule, limiterMachine, limiterProblems, limiterStateOf, localDev, longestRetryAfter, matchesMessage, mayReissue, memoryStore, nodeProcess, platformClock, processPortOf, realClock, recoverSpan, replay, retryAfterSeconds, service, stageRank, watchdog };
295
+ export { type Allowed, type Backoff, type Binding, type Bound, type Cancel, type Check, type CheckFn, type CheckVerdict, type Clock, type Controller, type ControllerBuilder, type CooperativeFloor, type Coupling, type Decision, type DelayBand, type Dependency, type DependencyHandle, EXIT_SIGNALS, type EmbeddedProcess, type Evaluated, type ExitCodes, type ExitSignal, type ExitTrigger, FORMAT, type FailsBy, type Finding, type Gate, type Health, type HealthServer, type HealthVerdict, type Heartbeat, type HeartbeatOptions, type HostStream, type Idempotency, type Inequality, type IterationContext, type Jitter, type JobAction, type JobContext, type JobFailure, type JobSettle, type JobSource, type KeyState, type KeyedLimiter, type Lifecycle, type LifecycleEvent, type LifecycleState, type LimitPolicy, type LimitRule, type Limiter, type LimiterEviction, type LimiterSpec, type LimiterState, type LimiterStore, type LimiterStoreSpec, type MachineName, type MemoryStore, type MessageShape, type MessageTrigger, type Operation, type OperationOf, type Outcome, PROCESS_TRIGGERS, type Phase, type PhaseContext, type PhaseWork, type PlatformClockOptions, type PodTiming, type Poll, type ProbeAnswer, type ProbeKind, type ProcessHost, type ProcessPort, type ProcessTrigger, type Profile, type ProgressMarks, type QueueDrain, type QueueDrainOptions, type QueueKnobs, type Recorder, type RedeliveryPolicy, type RefusalCause, type Refused, type RepeatSignal, type RequestEnd, type RequestEvent, type RequestFn, type RequestOptions, type RequestStream, type RequestWork, type RequeueCause, type Result, type RoutePattern, type RouteRequest, type Rule, type RunOptions, SLIDING_WINDOW_MACHINE, STAGES, type Scenario, type ServiceBuilder, type ServiceDescription, type ShutdownOptions, type Side, type SlidingWindow, type Span, type Stage, type StartOptions, type Step, type StoreFailure, TOKEN_BUCKET_MACHINE, type TakeRequest, type TakeStep, type Term, type Timing, type TokenBucket, type Transport, type TransportImpl, type UnitRequest, type Verdict, type Watchdog, type WatchdogSpec, type WorkerKnobs, type WorkerLoop, type WorkerLoopOptions, asDebounce, asWindow, backoffBand, backoffDelay, backoffProblems, cooperativeAbortAt, daemon, decide, detectionTime, embeddedProcess, ephemeral, failSpan, freeAt, heartbeat, heartbeatDetection, idleSpan, isExitCode, isExitSignal, isMessageShape, keyedLimiter, kubernetes, limitRule, limiterMachine, limiterProblems, limiterStateOf, localDev, longestRetryAfter, matchesMessage, mayReissue, memoryStore, nodeProcess, platformClock, processPortOf, queueDrain, realClock, recoverSpan, redeliverOnce, replay, retryAfterSeconds, service, stageRank, watchdog };
437
296
  //# sourceMappingURL=index.d.ts.map
package/build/index.js CHANGED
@@ -1656,6 +1656,25 @@ const watchdog = (clock, spec, trace) => {
1656
1656
  const isTime$1 = (amount) => convertible(amount.unit, "s");
1657
1657
  const above = (amount) => amount.value === "infinite" || isTime$1(amount) && valueIn(amount, "s") > 0;
1658
1658
  const whole = (count) => Number.isInteger(count) && count >= 1;
1659
+ /** This function copies a check, with its poll and the loops that mark it. */
1660
+ const copyCheck = (check) => ({
1661
+ ...check,
1662
+ poll: { ...check.poll },
1663
+ ...check.marks === void 0 ? {} : { marks: {
1664
+ loops: [...check.marks.loops],
1665
+ stall: { ...check.marks.stall }
1666
+ } }
1667
+ });
1668
+ /** This function gives the problems of the loops that mark a check. `lint()` compares the stall time with the loops. */
1669
+ const marksProblems = (check) => {
1670
+ const marks = check.marks;
1671
+ if (marks === void 0) return [];
1672
+ const problems = [];
1673
+ if (check.step !== "progress") problems.push(`check ${check.name} names the loops that mark it, but its step is ${check.step}, not progress`);
1674
+ if (marks.loops.length === 0) problems.push(`check ${check.name} names no loop that marks it`);
1675
+ if (typeof marks.stall.value !== "number" || !above(marks.stall)) problems.push(`check ${check.name} has a stall time that is not a time above zero`);
1676
+ return problems;
1677
+ };
1659
1678
  /** This function gives the problems of a timing: a zero interval or count fails decoding. */
1660
1679
  const timingProblems = (policy, timing) => {
1661
1680
  if (timing.type === "debounce") {
@@ -1844,14 +1863,11 @@ const service = (name) => {
1844
1863
  check(check, run) {
1845
1864
  once(checks, check.name, "check");
1846
1865
  if (started && run === void 0) throw new Error(`check ${check.name} joins after the start, thus it needs its function`);
1847
- checks.push({ ...check });
1866
+ checks.push(copyCheck(check));
1848
1867
  if (run !== void 0) runs.set(check.name, run);
1849
1868
  if (started) {
1850
1869
  partial = true;
1851
- live?.addCheck({
1852
- ...check,
1853
- poll: { ...check.poll }
1854
- }, run);
1870
+ live?.addCheck(copyCheck(check), run);
1855
1871
  }
1856
1872
  return builder;
1857
1873
  },
@@ -1989,6 +2005,7 @@ const service = (name) => {
1989
2005
  if (!isTime$1(check.poll.interval) || !isTime$1(check.poll.timeout)) problems.push(`check ${check.name} polls in a unit that is not a time`);
1990
2006
  else if (!above(check.poll.interval) || !above(check.poll.timeout)) problems.push(`check ${check.name} polls with an interval or a timeout that is not above zero`);
1991
2007
  if (!(check.poll.jitter >= 0 && check.poll.jitter < 1)) problems.push(`check ${check.name} has a jitter of ${check.poll.jitter}, not a fraction below 1`);
2008
+ problems.push(...marksProblems(check));
1992
2009
  const timing = check.gates === null ? null : policies[check.gates];
1993
2010
  if (timing?.type === "debounce" && isTime$1(check.poll.interval) && above(timing.interval) && valueIn(check.poll.interval, "s") !== valueIn(timing.interval, "s")) problems.push(`check ${check.name} polls every ${valueIn(check.poll.interval, "s")} s, but the ${check.gates} debounce counts polls every ${valueIn(timing.interval, "s")} s`);
1994
2011
  }
@@ -2031,10 +2048,7 @@ const service = (name) => {
2031
2048
  operations: { ...entry.operations }
2032
2049
  })),
2033
2050
  controllers: controllersOut,
2034
- checks: checks.map((entry) => ({
2035
- ...entry,
2036
- poll: { ...entry.poll }
2037
- })),
2051
+ checks: checks.map(copyCheck),
2038
2052
  ...limiters.length === 0 ? {} : { limiters: limiters.map((entry) => ({
2039
2053
  ...entry,
2040
2054
  policy: { ...entry.policy },
@@ -2385,14 +2399,144 @@ const declarationFindings = (svc) => svc.shutdown.onRepeatSignal === void 0 ? [{
2385
2399
  evaluated: [],
2386
2400
  unbound: ["shutdown.onRepeatSignal"]
2387
2401
  }] : [];
2402
+ /** This identifier is the rule of the findings about the stall time of a progress check (`Check.marks`). */
2403
+ const PROGRESS_RULE = "progress";
2404
+ /**
2405
+ * This rule compares the stall time of a progress check with the longest gap between two marks of
2406
+ * the slowest loop that it names. A shorter stall time trips the check while each loop turns on time.
2407
+ */
2408
+ const PROGRESS_LAW = {
2409
+ id: PROGRESS_RULE,
2410
+ rule: "the stall time of a progress check is at least the longest gap between two marks of the slowest loop that it names",
2411
+ window: "a loop that turns on time can trip the check",
2412
+ expr: {
2413
+ lhs: { terms: [{
2414
+ knob: "stall",
2415
+ coef: 1,
2416
+ at: "check"
2417
+ }] },
2418
+ op: ">=",
2419
+ rhs: { terms: [{
2420
+ knob: "mark_gap",
2421
+ coef: 1,
2422
+ at: "loop"
2423
+ }] },
2424
+ margin: 0
2425
+ }
2426
+ };
2427
+ /** These are the knobs of a worker that bound the gap between two of its marks. */
2428
+ const WORKER_GAP_KNOBS = [
2429
+ "worker_sleep",
2430
+ "unit_processing_time",
2431
+ "retry_base",
2432
+ "retry_cap"
2433
+ ];
2434
+ /**
2435
+ * This function gives the longest gap between two marks of each loop with a name. The rule counts
2436
+ * a mark at the end of each turn of the loop, also a turn that fails.
2437
+ * - A check polls on a fixed delay from the start of each poll, and each poll ends at its timeout
2438
+ * at the latest. Thus the gap is `interval × (1 + jitter) + timeout`.
2439
+ * - After an iteration, a lane of a worker waits its sleep, or its backoff after a failure. The
2440
+ * longest backoff is `retry_cap` plus its jitter of up to `retry_base`. The next iteration then
2441
+ * takes up to `unit_processing_time`. Thus the gap is the longer wait plus that bound.
2442
+ *
2443
+ * A sleep or a bound without a limit gives a gap without a limit. An iteration that does not end
2444
+ * at its abort can take more time, and the rule does not bound it.
2445
+ */
2446
+ const gapsOf = (svc, loop) => {
2447
+ const gaps = svc.checks.filter((check) => check.name === loop).map((check) => ({
2448
+ loop,
2449
+ seconds: secondsOf(check.poll.interval) * (1 + check.poll.jitter) + secondsOf(check.poll.timeout),
2450
+ unbound: []
2451
+ }));
2452
+ for (const worker of svc.transports) {
2453
+ if (worker.kind !== "worker" || worker.name !== loop) continue;
2454
+ const knob = (name) => {
2455
+ const amount = worker.knobs[name];
2456
+ if (amount === void 0 || !isTime(amount.unit)) return void 0;
2457
+ return amount.value === "infinite" ? Infinity : secondsOf(amount);
2458
+ };
2459
+ const unbound = WORKER_GAP_KNOBS.filter((name) => knob(name) === void 0).map((name) => `${loop}.${name}`);
2460
+ if (unbound.length > 0) {
2461
+ gaps.push({
2462
+ loop,
2463
+ seconds: void 0,
2464
+ unbound
2465
+ });
2466
+ continue;
2467
+ }
2468
+ const [sleep, unit, base, cap] = WORKER_GAP_KNOBS.map((name) => knob(name));
2469
+ const backoff = Number.isFinite(base) && Number.isFinite(cap) ? backoffBand({
2470
+ base,
2471
+ factor: 2,
2472
+ cap,
2473
+ jitter: {
2474
+ kind: "add",
2475
+ amount: base
2476
+ }
2477
+ }).high : Infinity;
2478
+ gaps.push({
2479
+ loop,
2480
+ seconds: Math.max(sleep, backoff) + unit,
2481
+ unbound: []
2482
+ });
2483
+ }
2484
+ return gaps;
2485
+ };
2486
+ /**
2487
+ * This function gives the findings of each check that names the loops that mark it. A name that is
2488
+ * not a check or a worker of the service gets a finding of its own, with the verdict `depends`.
2489
+ * Without `marks`, a check gets no finding.
2490
+ */
2491
+ const progressFindings = (svc) => {
2492
+ const findings = [];
2493
+ const finding = (check, fields) => ({
2494
+ rule: PROGRESS_RULE,
2495
+ scope: check.name,
2496
+ statement: PROGRESS_LAW.rule,
2497
+ window: PROGRESS_LAW.window,
2498
+ ...fields
2499
+ });
2500
+ for (const check of svc.checks) {
2501
+ const marks = check.marks;
2502
+ if (marks === void 0) continue;
2503
+ const gaps = [];
2504
+ for (const loop of marks.loops) {
2505
+ const found = gapsOf(svc, loop);
2506
+ if (found.length > 0) gaps.push(...found);
2507
+ else findings.push(finding(check, {
2508
+ verdict: "depends",
2509
+ statement: "each loop that a progress check names is a check or a worker of the service",
2510
+ window: `the check waits for the marks of ${loop}, and the service has no loop with that name`,
2511
+ evaluated: [],
2512
+ unbound: [loop]
2513
+ }));
2514
+ }
2515
+ if (gaps.length === 0) continue;
2516
+ const unbound = [...isTime(marks.stall.unit) ? [] : [`${check.name}.marks.stall`], ...gaps.flatMap((gap) => gap.unbound)];
2517
+ const slowest = gaps.reduce((worst, gap) => (gap.seconds ?? -Infinity) > (worst.seconds ?? -Infinity) ? gap : worst);
2518
+ const bind = {};
2519
+ if (isTime(marks.stall.unit)) bind["stall@check"] = boundOf(marks.stall);
2520
+ if (unbound.length === 0 && slowest.seconds !== void 0) bind["mark_gap@loop"] = Number.isFinite(slowest.seconds) ? slowest.seconds : "inf";
2521
+ const evaluated = evaluate(PROGRESS_LAW, bind);
2522
+ findings.push(finding(check, {
2523
+ verdict: verdictOf(PROGRESS_LAW, bind, evaluated),
2524
+ ...unbound.length === 0 ? { statement: `${PROGRESS_LAW.rule} (${slowest.loop})` } : {},
2525
+ evaluated,
2526
+ unbound
2527
+ }));
2528
+ }
2529
+ return findings;
2530
+ };
2388
2531
  /**
2389
2532
  * This function applies each service rule to a declaration and a scenario. A rule applies one time
2390
2533
  * for the service, for each client, for each consumer and worker, or for each limiter. The function
2391
2534
  * also gives a finding for each necessary field that the description does not give (`DECLARATION_RULE`).
2535
+ * Each progress check that names its loops also gets its findings (`PROGRESS_RULE`).
2392
2536
  */
2393
2537
  const lint = (svc, scenario, rules) => {
2394
2538
  const service = serviceBindings(svc, scenario).bind;
2395
- const findings = [...declarationFindings(svc)];
2539
+ const findings = [...declarationFindings(svc), ...progressFindings(svc)];
2396
2540
  for (const rule of rules) {
2397
2541
  if (!SERVICE_RULES.includes(rule.id)) continue;
2398
2542
  const symbols = symbolsOf(rule);
@@ -3411,6 +3555,6 @@ const MACHINES = {
3411
3555
  watchdog: WATCHDOG_MACHINE
3412
3556
  };
3413
3557
  //#endregion
3414
- export { DECLARATION_RULE, EXIT_MACHINE, EXIT_SIGNALS, FORMAT, GATE_MACHINE, LATCH_MACHINE, LIMITER_RULES, MACHINES, POLLER_MACHINE, PROCESS_TRIGGERS, SERVICE_RULES, SLIDING_WINDOW_MACHINE, STAGES, TOKEN_BUCKET_MACHINE, WATCHDOG_MACHINE, WINDOW_MACHINE, asDebounce, asWindow, backoffBand, backoffDelay, backoffProblems, capKnob, cooperativeAbortAt, daemon, decide, detectionTime, drainMachine, embeddedProcess, ephemeral, evaluate, failSpan, freeAt, heartbeat, heartbeatDetection, idleSpan, inequalities, isExitCode, isExitSignal, isMessageShape, keyedLimiter, kubernetes, limitRule, limiterMachine, limiterProblems, limiterStateOf, lint, localDev, longestRetryAfter, matchesMessage, mayReissue, memoryStore, nodeProcess, platformClock, processPortOf, queueDrain, realClock, recoverSpan, redeliverOnce, replay, retryAfterSeconds, service, serviceBindings, stageRank, symbolsOf, verdictOf, watchdog, workerLoop };
3558
+ export { DECLARATION_RULE, EXIT_MACHINE, EXIT_SIGNALS, FORMAT, GATE_MACHINE, LATCH_MACHINE, LIMITER_RULES, MACHINES, POLLER_MACHINE, PROCESS_TRIGGERS, PROGRESS_RULE, SERVICE_RULES, SLIDING_WINDOW_MACHINE, STAGES, TOKEN_BUCKET_MACHINE, WATCHDOG_MACHINE, WINDOW_MACHINE, asDebounce, asWindow, backoffBand, backoffDelay, backoffProblems, capKnob, cooperativeAbortAt, daemon, decide, detectionTime, drainMachine, embeddedProcess, ephemeral, evaluate, failSpan, freeAt, heartbeat, heartbeatDetection, idleSpan, inequalities, isExitCode, isExitSignal, isMessageShape, keyedLimiter, kubernetes, limitRule, limiterMachine, limiterProblems, limiterStateOf, lint, localDev, longestRetryAfter, matchesMessage, mayReissue, memoryStore, nodeProcess, platformClock, processPortOf, queueDrain, realClock, recoverSpan, redeliverOnce, replay, retryAfterSeconds, service, serviceBindings, stageRank, symbolsOf, verdictOf, watchdog, workerLoop };
3415
3559
 
3416
3560
  //# sourceMappingURL=index.js.map