@mark1russell7/service 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,6 +15,7 @@ This release has the declaration, its export, the law, the health runtime, the l
15
15
  - **The law.** `evaluate` and `verdictOf` decide one rule of the timer-ordering law on one binding. A rule is data: linear inequalities over `symbol@hop`, with a margin. A verdict is `holds`, `violated`, `unbounded` (it holds only because a timer is absent) or `depends` (nothing is bound).
16
16
  - **The linter.** `lint(description, scenario, rules)` applies the service rules (`SERVICE_RULES`) to a declaration. The declaration binds the symbols of the service: the polls of its checks, the windows of its policies, its drain. The scenario binds the symbols of the platform and of each dependency. A `depends` finding names the symbols that have no bound.
17
17
  - **The scopes of a rule.** A rule about a client applies one time for each dependency. A rule about a consumer, for example O20, applies one time for each consumer and each worker.
18
+ - **Keyed limiters.** `svc.limiter(name, spec)` declares a rate limit for each key, as data. `keyedLimiter` makes its runtime on a store ([The keyed limiter](#the-keyed-limiter)).
18
19
  - **The hold.** The cap of the `fail readiness` phase binds `readiness_hold@process`, and a service without that phase holds for zero. The hold keeps the listener open after the signal, as a preStop sleep does. Thus O04 can compare the sum of the two with the propagation of the endpoints.
19
20
 
20
21
  The format of a description is `mark1russell7.service/1`.
@@ -80,6 +81,7 @@ A process has one service, and one entry that uses `run()`. A component gives pa
80
81
  - **The parts.** A component gives a `TransportImpl` for each source of work, a `DependencyHandle` for each dependency that it owns, and each check with its `CheckFn`. It can also give work for a declared phase (`onPhase`).
81
82
  - **The entry.** The entry declares one service: the parts of each component, the policies, the startup budget, the phases and the cooperative window. Only the entry uses `run()`. Thus the process has one set of signal handlers, one staged shutdown and one exit.
82
83
  - **What a component does not do.** A component does not use `start()` or `run()`. It installs no signal handler, and it does not end the process. A service has one health server, thus the entry chooses it.
84
+ - **An entry in a process that another part owns.** A script, a test of an application or a library entry can start a service in a process that it does not own. It gives `run()` the port `embeddedProcess()`. That port adds no signal handler, it does not hold the process, and its `exit` only records the code. The caller stops the service with `life.shutdown()` and reads the code from `life.done`.
83
85
  - **Names.** A service refuses a second part of one kind with the same name. Thus the entry gives each component a prefix, for example `app.http` and `ingest.http`.
84
86
  - **The sequence.** The lifecycle connects the dependencies of all components in their declared sequence, and then it starts each transport. Each stage of the shutdown acts on the transports of all components together. The dependencies close in the reverse sequence.
85
87
  - **Requests.** The transports start before the startup ends, but `run()` gives the `Lifecycle` only at that end. Thus each transport gets the lifecycle at its start (`start(signal, life)`), and its request tracking operates at once.
@@ -299,6 +301,65 @@ socket.addEventListener("message", (event) => {
299
301
  const delay = backoffDelay({ base: 1000, factor: 1.5, cap: 30_000, jitter: { kind: "subtract", fraction: 1 } }, attempt);
300
302
  ```
301
303
 
304
+ ## The keyed limiter
305
+
306
+ A keyed limiter gives each key its own limit, for example each tenant or each principal. `@mark1russell7/service/limiter` has it. The entry loads only the quantity package: no `node:` module and no zod. Thus a client in a browser can use it. The root of the package also gives each function.
307
+
308
+ | Export | Function |
309
+ | --- | --- |
310
+ | `svc.limiter(name, spec)` | The declaration of a limiter, as data. `describe()` shows it in `limiters`. |
311
+ | `keyedLimiter(name, spec, store)` | The runtime of a limiter: `take(key)` gives a `Decision` |
312
+ | `memoryStore(clock)` | The store of one replica, on the injected monotonic clock |
313
+ | `decide(rule, state, now)`, `limitRule(policy)`, `freeAt(rule, state)` | The arithmetic of each store, for a store of your own |
314
+ | `idleSpan(policy)`, `longestRetryAfter(policy)`, `retryAfterSeconds(retryAfter)` | The times of a policy, and `Retry-After` in whole seconds |
315
+ | `limiterProblems(name, spec)` | The problems of a spec that `describe()` and `keyedLimiter` refuse |
316
+ | `LIMITER_RULES` | The rules O29 and O30 of the law, as data. Their identifiers are provisional. |
317
+
318
+ - **No defaults.** A spec has a key, a policy, a store and a bound on its keys. `describe()` refuses a spec without its rate, its burst, its limit, its window, its store, the failure policy of a shared store or its bound. The error names the part.
319
+ - **The key.** `key` tells what the key of each take is, for example `tenant`. The caller gives each key to `take(key)`.
320
+ - **A token bucket.** `{ type: "token-bucket", rate, burst }` gives a key `burst` takes at once, and then `rate` takes in each unit of time. The rate is a count for each unit of time, for example `count/s` or `count/min`.
321
+ - **A sliding window.** `{ type: "sliding-window", limit, window }` gives a key at most `limit` takes in each window. Each take counts for one window from its time. Graph limits its writes with this policy.
322
+ - **The decision.** `take(key)` gives `{ allowed: true, remaining }` or `{ allowed: false, retryAfter, cause }`. `retryAfter` is a `Quantity` above zero: the time until the limiter lets the same take in. The cause is `limit`, `cap` or `store`.
323
+ - **The clock.** `memoryStore(clock)` reads time only through its clock. Thus a step of the wall clock moves no count, and a fake clock gives exact decisions in a test.
324
+ - **The store.** A `LimiterStore` decides each take in one atomic step, on its own clock. A store in another process does the arithmetic of `decide` in one step of its own. An example is one script of Redis that reads `TIME`.
325
+ - **No shared store.** With `{ kind: "local" }`, each replica keeps its own counts. For a limit L over N replicas, declare L / N on each replica.
326
+ - **Only on average.** The sum of L / N holds only when the balancer spreads the takes of each key evenly. A balancer that sends each take of a key to one replica gives that key only L / N.
327
+ - **A shared store.** With `{ kind: "shared", dependency, onStoreFailure }`, the counts are in a dependency of the service. `describe()` refuses a dependency that the service does not declare, and `keyedLimiter` refuses a store of the wrong kind.
328
+ - **A failure of the store.** A shared store declares what a take does when the store fails (`onStoreFailure`). The field has no default, and `describe()` refuses a shared store without it.
329
+ - **Open or closed.** With `"open"`, the limiter lets the take in, with `remaining` 0. With `"closed"`, it refuses the take with the cause `store` and the longest `retryAfter` of the policy. Each decision gives the failure as text (`failure`).
330
+ - **A local store.** A local store does not fail, thus it needs no `onStoreFailure`, and the limiter ignores one. A failure of a local store of your own rejects the take.
331
+ - **Exact eviction.** A store removes a key only when its state is equal to the state of a new key. A bucket is new again after `burst / rate` idle, and a window after one window idle (`idleSpan`). Thus an eviction changes no decision.
332
+ - **A bound on the keys.** `evict.maxKeys` is the most keys that the store holds for the limiter. `describe()` refuses a count without a bound.
333
+ - **The cost of a new key at the cap.** At the cap, the store refuses a new key with the cause `cap`. Its `retryAfter` is at most the time until the earliest held key is free. That time is at most `idleSpan`, and a held key keeps its count.
334
+ - **The cost of the bound.** A sweep at the cap reads each key one time. It occurs only when a held key can be free. A token bucket keeps one number for each key, thus at most `maxKeys` numbers. A sliding window keeps the time of each take in its window: at most `maxKeys × limit` times.
335
+ - **The HTTP edge.** `retryAfterSeconds(retryAfter)` gives whole seconds, rounded up, for `Retry-After` (RFC 9110). `limitedResponse(refused)` of `@mark1russell7/service/http` gives 429 with that header (RFC 6585).
336
+ - **The law.** `lint()` applies O29 and O30 (`LIMITER_RULES`) one time for each limiter. The scenario gives the values of the callers of each limiter (`scenario.limiters`).
337
+ - **Provisional identifiers.** The corpus of the law has the rules O01 to O28. It does not have these two rules yet, thus O29 and O30 are provisional identifiers.
338
+ - **O29.** The longest `Retry-After` of a limiter (`retry_after@limiter`) is at most the retry window of its caller (`client_retry_window@caller`). Its second inequality applies the same bound to a new key at the cap (`retry_after_at_cap@limiter`). Otherwise the caller stops before the limit lets its call in.
339
+ - **O30.** The least delay of a caller before a retry (`retry_delay@caller`) is at least the longest `Retry-After` of the policy. Otherwise a caller that does not read the header sends again too early.
340
+ - **A closed failure.** The `retryAfter` of the cause `store` is the longest `retryAfter` of the policy. Thus O29 and O30 apply to it too.
341
+ - **The symbols.** `retry_after@limiter` is the longest `Retry-After` of the policy in whole seconds: one interval of the bucket, or one window. `retry_after_at_cap@limiter` is the longest at the cap, `idleSpan` in whole seconds.
342
+
343
+ ```ts
344
+ import { keyedLimiter, memoryStore, realClock } from "@mark1russell7/service";
345
+ import type { LimiterSpec } from "@mark1russell7/service";
346
+ import { limitedResponse } from "@mark1russell7/service/http";
347
+
348
+ const count = (value: number) => ({ value, unit: "count" });
349
+ // the writes of each tenant: 120 in each sliding minute, for at most 10 000 tenants on this replica
350
+ const writes: LimiterSpec = {
351
+ key: "tenant",
352
+ policy: { type: "sliding-window", limit: count(120), window: { value: 60, unit: "s" } },
353
+ store: { kind: "local" },
354
+ evict: { maxKeys: count(10_000) },
355
+ };
356
+ svc.limiter("writes", writes); // describe() shows it, and lint() applies O29 and O30 to it
357
+ const limiter = keyedLimiter("writes", writes, memoryStore(realClock));
358
+
359
+ const decision = await limiter.take(tenantOf(req));
360
+ if (!decision.allowed) return limitedResponse(decision); // 429, with Retry-After from 1 to 60
361
+ ```
362
+
302
363
  ## The HTTP edge
303
364
 
304
365
  `@mark1russell7/service/http` is the edge of a service in Node. Of the modules of Node, it imports only `node:http` and `node:net`. The root of the package does not import it. It is not a framework: it has no routing language, no body parser and no middleware.
@@ -312,6 +373,7 @@ const delay = backoffDelay({ base: 1000, factor: 1.5, cap: 30_000, jitter: { kin
312
373
  | `writeOutcome(res, stream, options)` | The stream of a request as a response of `node:http`, unary or a stream |
313
374
  | `ndjson`, `sse` | The formats of a stream: NDJSON, and server-sent events |
314
375
  | `outcomeResponse`, `endRecord`, `probeResponse`, `pathOf` | The same answers as data, for any HTTP framework or none |
376
+ | `limitedResponse(refused)` | The answer to a take that a keyed limiter refused: 429, with `Retry-After` in whole seconds |
315
377
 
316
378
  - **The transport.** `httpTransport(server, knobs, options)` makes a server of `node:http` a transport of the lifecycle. Give `transport.knobs` to `svc.server(name, knobs, impl)`. Thus the declaration and the law read the same knobs.
317
379
  - **The knobs.** The transport has no defaults. `server_idle_sweep_interval` is the interval of the sweep of idle connections. `server_retry_after` goes in the `Retry-After` of each 503 of the transport.
@@ -1,5 +1,6 @@
1
1
  import { n as Clock } from "./clock-D-38uH_z.js";
2
- import { B as Gate, D as Outcome, E as CheckFn, F as CooperativeFloor, H as Phase, I as Coupling, J as Poll, L as Dependency, N as Check, R as ExitCodes, T as StartOptions, U as RepeatSignal, V as MessageTrigger, W as ServiceDescription, X as Timing, d as TransportImpl, k as Trace, n as HealthServer, nt as OperationOf, o as PhaseWork, pt as ProcessTrigger, r as Lifecycle, s as RunOptions, st as ExitTrigger, t as DependencyHandle, x as Health, xt as Operation } from "./lifecycle-DtJZW09J.js";
2
+ import { c as LimiterSpec } from "./policy-G4QJ1ZZq.js";
3
+ import { B as Gate, Ct as Operation, D as Outcome, E as CheckFn, F as CooperativeFloor, H as Phase, I as Coupling, J as Poll, L as Dependency, N as Check, R as ExitCodes, T as StartOptions, U as RepeatSignal, V as MessageTrigger, W as ServiceDescription, X as Timing, ct as ExitTrigger, d as TransportImpl, k as Trace, mt as ProcessTrigger, n as HealthServer, nt as OperationOf, o as PhaseWork, r as Lifecycle, s as RunOptions, t as DependencyHandle, x as Health } from "./lifecycle-Chu9Pri2.js";
3
4
  import { Extent, Quantity } from "@mark1russell7/quantity";
4
5
  //#region src/profiles.d.ts
5
6
  /**
@@ -151,6 +152,12 @@ interface ServiceBuilder {
151
152
  controller(name: string): ControllerBuilder;
152
153
  /** This method declares a check, and the function that measures its step. `start()` needs the function. */
153
154
  check(check: Check, run?: CheckFn): ServiceBuilder;
155
+ /**
156
+ * This method declares a keyed limiter as data: its key, its policy, its store and the bound on its
157
+ * keys. `describe()` shows it and refuses a part that is absent, because a limiter has no defaults.
158
+ * `keyedLimiter(name, spec, store)` makes its runtime from the same spec.
159
+ */
160
+ limiter(name: string, spec: LimiterSpec): ServiceBuilder;
154
161
  readiness(timing: Timing): ServiceBuilder;
155
162
  liveness(timing: Timing): ServiceBuilder;
156
163
  startup(budget: Quantity): ServiceBuilder;
@@ -201,4 +208,4 @@ interface ServiceBuilder {
201
208
  declare const service: (name: string) => ServiceBuilder;
202
209
  //#endregion
203
210
  export { WatchdogSpec as a, Profile as c, kubernetes as d, localDev as f, Watchdog as i, daemon as l, ServiceBuilder as n, watchdog as o, service as r, PodTiming as s, ControllerBuilder as t, ephemeral as u };
204
- //# sourceMappingURL=builder-QVxu1WcL.d.ts.map
211
+ //# sourceMappingURL=builder-DyDB7zM9.d.ts.map
package/build/http.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { n as Clock } from "./clock-D-38uH_z.js";
2
- import { d as TransportImpl, m as RequestEnd, n as HealthServer, r as Lifecycle, v as RequestStream, x as Health, y as RequestWork } from "./lifecycle-DtJZW09J.js";
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, x as Health, y as RequestWork } from "./lifecycle-Chu9Pri2.js";
3
4
  import { Extent, Quantity } from "@mark1russell7/quantity";
4
5
  import { ListenOptions } from "node:net";
5
6
  import { IncomingMessage, Server, ServerEventMap, ServerResponse } from "node:http";
@@ -245,6 +246,13 @@ export declare const sse: StreamFormat;
245
246
  * while the service cannot keep the connection.
246
247
  */
247
248
  export declare const outcomeResponse: <T>(items: readonly T[], end: RequestEnd, options: OutcomeOptions<T>) => OutcomeResponse;
249
+ /**
250
+ * This function gives the response of a take that a keyed limiter refused: 429 (RFC 6585), with
251
+ * `Retry-After` in whole seconds, rounded up (RFC 9110). The body tells the cause (`limit`, `cap` or
252
+ * `store`) and the same whole seconds, but not the failure of a store. The work did not start, thus
253
+ * the caller can send it again.
254
+ */
255
+ export declare const limitedResponse: (refused: Refused) => OutcomeResponse;
248
256
  /**
249
257
  * This function writes the stream of a request to a response of `node:http`, and gives its end. It
250
258
  * gives null when the client closed the connection first. Then the work stops through `return()`.
package/build/http.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { realClock } from "./clock.js";
2
+ import { s as retryAfterSeconds } from "./policy-CgS4y94U.js";
2
3
  import { r as millisecondsOf$1 } from "./knobs-BxG-c7Zb.js";
3
4
  import { valueIn } from "@mark1russell7/quantity";
4
5
  import { createServer } from "node:http";
@@ -627,6 +628,29 @@ const outcomeResponse = (items, end, options) => {
627
628
  }, options);
628
629
  return ruled(response, options.retryAfter, options.keepAlive);
629
630
  };
631
+ /**
632
+ * This function gives the response of a take that a keyed limiter refused: 429 (RFC 6585), with
633
+ * `Retry-After` in whole seconds, rounded up (RFC 9110). The body tells the cause (`limit`, `cap` or
634
+ * `store`) and the same whole seconds, but not the failure of a store. The work did not start, thus
635
+ * the caller can send it again.
636
+ */
637
+ const limitedResponse = (refused) => {
638
+ const seconds = retryAfterSeconds(refused.retryAfter);
639
+ return {
640
+ status: 429,
641
+ headers: {
642
+ "content-type": JSON_TYPE,
643
+ "retry-after": String(seconds)
644
+ },
645
+ body: JSON.stringify({
646
+ end: "refused",
647
+ delivered: 0,
648
+ retryable: true,
649
+ cause: `the limiter refused the take: ${refused.cause}`,
650
+ retryAfter: seconds
651
+ })
652
+ };
653
+ };
630
654
  /** This function writes a whole response of `node:http`. A response whose headers went gets no second status: it is destroyed. */
631
655
  const writeWhole = (res, response) => {
632
656
  if (res.headersSent) {
@@ -777,6 +801,6 @@ const handle = (controller, route, work, options = {}) => {
777
801
  };
778
802
  };
779
803
  //#endregion
780
- export { endRecord, handle, httpTransport, ndjson, outcomeResponse, pathOf, probeHandler, probeResponse, probeServer, sse, writeOutcome };
804
+ export { endRecord, handle, httpTransport, limitedResponse, ndjson, outcomeResponse, pathOf, probeHandler, probeResponse, probeServer, sse, writeOutcome };
781
805
 
782
806
  //# sourceMappingURL=http.js.map
package/build/http.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"http.js","names":["knobMilliseconds"],"sources":["../src/http/transport.ts","../src/http/probes.ts","../src/http/outcome.ts"],"sourcesContent":["import type { Extent, Quantity } from \"@mark1russell7/quantity\";\nimport type { IncomingMessage, Server, ServerEventMap, ServerResponse } from \"node:http\";\nimport type { ListenOptions, Socket } from \"node:net\";\nimport type { Cancel, Clock } from \"../clock.ts\";\nimport { realClock } from \"../clock.ts\";\nimport { millisecondsOf as knobMilliseconds } from \"../knobs.ts\";\nimport type { Lifecycle, TransportImpl } from \"../lifecycle.ts\";\n\n/**\n * These are the knobs of an HTTP transport, by the names of the ordering law. The transport has no\n * defaults, thus each knob is necessary. The declaration of the server gets the same record.\n */\nexport interface HttpKnobs {\n /**\n * This time is the real idle close of the server, which O01 compares with the idle time of a\n * client. Node closes an idle connection at `keepAliveTimeout` plus `keepAliveTimeoutBuffer`.\n * Thus the transport sets `keepAliveTimeout` to this time less the buffer of the server.\n * A time without a bound stops the timer.\n */\n readonly server_idle_close: Extent;\n /**\n * This time is the interval of the sweep of idle connections, from the stop of the intake until the\n * close. A time without a bound gives one sweep, at the stop of the intake.\n */\n readonly server_idle_sweep_interval: Extent;\n /** This time goes in the `Retry-After` of each 503 that the transport gives after its intake stops. */\n readonly server_retry_after: Quantity;\n /** The other knobs of the server go to the declaration. The transport does not read them. */\n readonly [knob: string]: Extent;\n}\n\n/**\n * These are the knobs that the transport gives the declaration: the knobs of the caller, and the two\n * times that the transport found. Thus the declaration shows the three times of the idle close.\n */\nexport interface HttpTransportKnobs extends HttpKnobs {\n /** This time is the hint that each response sends (`Keep-Alive: timeout=…`), in whole seconds. A time without a bound tells that no hint goes. */\n readonly server_idle_close_hint: Extent;\n /** This time is the `keepAliveTimeoutBuffer` of the server, which Node adds to the timer and not to the hint. */\n readonly server_idle_close_buffer: Quantity;\n}\n\n/** A mount stage is the stage of the shutdown at which a mount stops: the stop of the intake, or the close. */\nexport type MountStage = \"stop intake\" | \"close transports\";\n\n/** A mount context gives each handler the signals of the shutdown, the lifecycle and the time of a retry. */\nexport interface MountContext {\n /** This signal aborts when the intake stops. A long stream ends in its own protocol at it. */\n readonly intake: AbortSignal;\n /** This signal aborts at the cooperative abort of the drain, or at the close. The work in flight stops at it. */\n readonly signal: AbortSignal;\n /** This lifecycle is the lifecycle that the transport got at its start, or null before the start. */\n readonly life: Lifecycle | null;\n /** This time is the knob `server_retry_after`: the `Retry-After` of each 503 of the handlers. */\n readonly retryAfter: Quantity;\n}\n\n/**\n * A request handler answers a request and gives true, or it gives false and does not touch the\n * response. Then the next handler gets the request. `until` tells when the handler stops.\n */\nexport interface RequestHandler {\n (req: IncomingMessage, res: ServerResponse, context: MountContext): boolean | Promise<boolean>;\n readonly until?: MountStage | undefined;\n}\n\ntype UpgradeArgs = ServerEventMap[\"upgrade\"];\n\n/** An upgrade socket is the stream of an upgraded connection, as the `upgrade` event of Node gives it. */\nexport type UpgradeSocket = UpgradeArgs[1];\n\n/** An upgrade handler takes an upgraded connection, for example a WebSocket server in its `noServer` mode. */\nexport interface UpgradeHandler {\n (req: IncomingMessage, socket: UpgradeSocket, head: UpgradeArgs[2], context: MountContext): void;\n readonly until?: MountStage | undefined;\n}\n\n/** An upgrade match is a path, without its query, or a function that tells if a handler takes a request. */\nexport type UpgradeMatch = string | ((req: IncomingMessage) => boolean);\n\n/** These options change one mount. */\nexport interface MountOptions {\n /**\n * This stage is when the mount stops. Without it, the stage of the handler applies, and then\n * `stop intake`. A mount until `close transports` keeps the listener open through the drain.\n */\n readonly until?: MountStage | undefined;\n}\n\n/** These options make an HTTP transport. */\nexport interface HttpTransportOptions {\n /**\n * These are the listen options of `start()`, or a function that makes the server listen. Without\n * them, `start()` takes a server that listens already.\n */\n readonly listen?: ListenOptions | ((server: Server) => Promise<void> | void) | undefined;\n /** This clock is the clock of the lifecycle. The deadline of the drain and the sweep use it. */\n readonly clock?: Clock | undefined;\n /**\n * This function tells if a response can keep its connection open. Without it, the transport reads\n * `life.keepAlive()` of the lifecycle of its start. The transport reads it when the headers of\n * each response go, and false gives `Connection: close`.\n */\n readonly keepAlive?: (() => boolean) | undefined;\n /** This function receives each error of a handler. The transport then answers 500, or it destroys the response. */\n readonly onError?: ((error: unknown, req: IncomingMessage) => void) | undefined;\n}\n\n/** An HTTP transport is a server of `node:http` as a transport of the lifecycle, with a list of mounts on its listener. */\nexport interface HttpTransport extends TransportImpl {\n readonly server: Server;\n /** These knobs go to the declaration of the server: the knobs of the caller, the hint and the buffer. */\n readonly knobs: HttpTransportKnobs;\n /** This method adds a request handler at the end of the list, and gives the function that removes it. */\n mount(handler: RequestHandler, options?: MountOptions): () => void;\n /** This method adds an upgrade handler for a path, and gives the function that removes it. */\n upgrade(match: UpgradeMatch, handler: UpgradeHandler, options?: MountOptions): () => void;\n /** This count is the number of open connections, idle connections and upgraded connections too. */\n readonly connections: number;\n /** This flag tells if the transport stopped its intake. */\n readonly intakeStopped: boolean;\n}\n\ninterface RequestMount {\n readonly handler: RequestHandler;\n readonly until: MountStage;\n}\n\ninterface UpgradeMount {\n readonly matches: (req: IncomingMessage) => boolean;\n readonly handler: UpgradeHandler;\n readonly until: MountStage;\n}\n\n/** This function gives the path of a request target, without its query and fragment. */\nexport const pathOf = (url: string | undefined): string => {\n const raw = url ?? \"/\";\n const end = raw.search(/[?#]/);\n const path = end === -1 ? raw : raw.slice(0, end);\n if (path.startsWith(\"/\")) return path;\n // an absolute target, as a proxy sends it\n try {\n return new URL(raw).pathname;\n } catch {\n return path;\n }\n};\n\n/** This function gives a knob in milliseconds, or infinity for a time without a bound. */\nconst millisecondsOf = (knobs: Readonly<Record<string, Extent | undefined>>, name: string, owner = \"httpTransport\"): number =>\n knobMilliseconds(knobs, name, owner);\n\n/**\n * This function gives the `Retry-After` of the knob `server_retry_after`, in whole seconds, rounded\n * up. The knob is necessary, and it has a bound.\n */\nexport const retryAfterOf = (knobs: Readonly<Record<string, Extent | undefined>>, owner: string): string => {\n const retryAfter = millisecondsOf(knobs, \"server_retry_after\", owner);\n if (!Number.isFinite(retryAfter)) throw new Error(\"the knob server_retry_after has no bound: a Retry-After needs a time\");\n return String(Math.ceil(retryAfter / 1000));\n};\n\n/** This function tells if the headers of a call of `writeHead` name a header, an object or a flat list of names and values. */\nconst names = (headers: unknown, name: string): boolean => {\n if (Array.isArray(headers)) return headers.some((each, at) => at % 2 === 0 && String(each).toLowerCase() === name);\n return typeof headers === \"object\" && headers !== null && Object.keys(headers).some((key) => key.toLowerCase() === name);\n};\n\n/** This function gives the headers of a call of `writeHead` without one header. */\nconst without = (headers: unknown, name: string): unknown => {\n if (Array.isArray(headers)) {\n const kept: unknown[] = [];\n for (let at = 0; at + 1 < headers.length; at += 2)\n if (String(headers[at]).toLowerCase() !== name) kept.push(headers[at], headers[at + 1]);\n return kept;\n }\n if (typeof headers !== \"object\" || headers === null) return headers;\n return Object.fromEntries(Object.entries(headers).filter(([key]) => key.toLowerCase() !== name));\n};\n\n/**\n * This function applies the rule of `Retry-After` to a call of `writeHead`, and gives its arguments.\n * A 503 without `Retry-After` gets the time of `server_retry_after` (RFC 9110). A 500 loses each\n * `Retry-After`, from the call and from the response.\n */\nconst retryRule = (res: ServerResponse, args: unknown[], retryAfter: string): unknown[] => {\n const status = typeof args[0] === \"number\" ? args[0] : res.statusCode;\n const at = typeof args[1] === \"string\" ? 2 : 1;\n if (status === 503 && !res.hasHeader(\"retry-after\") && !names(args[at], \"retry-after\")) res.setHeader(\"retry-after\", retryAfter);\n if (status !== 500) return args;\n res.removeHeader(\"retry-after\");\n if (!names(args[at], \"retry-after\")) return args;\n const given = [...args];\n given[at] = without(args[at], \"retry-after\");\n return given;\n};\n\nconst STATUS_TEXT: Readonly<Record<number, string>> = { 404: \"Not Found\", 500: \"Internal Server Error\", 503: \"Service Unavailable\" };\n\n/**\n * This function makes a server of `node:http` a transport of the lifecycle (S6 A8 and A13). It is not\n * a framework: it has no routes, no body parser and no middleware.\n * - The count of work in flight comes from events: each response until its `close`, and each upgraded\n * connection until its `close`.\n * - `stopIntake` closes the listener, and new connections get a refusal. It closes the idle connections\n * at once and at each `server_idle_sweep_interval`. Each later response gets `Connection: close`.\n * - `drain` waits until no work is in flight, or until the deadline. It gives the cooperative abort to\n * the handlers through their context.\n * - `close` destroys each connection. A second close gives the same promise.\n * - The mounts share one port. The first request handler that gives true answers. After the stop of\n * the intake, only the mounts until `close transports` answer, and each other request gets a 503.\n */\nexport const httpTransport = (server: Server, knobs: HttpKnobs, options: HttpTransportOptions = {}): HttpTransport => {\n const clock = options.clock ?? realClock;\n const idleClose = millisecondsOf(knobs, \"server_idle_close\");\n const sweepEvery = millisecondsOf(knobs, \"server_idle_sweep_interval\");\n if (sweepEvery === 0)\n throw new Error(\"the knob server_idle_sweep_interval is zero: a sweep needs an interval above zero\");\n const retryAfterHeader = retryAfterOf(knobs, \"httpTransport\");\n for (const derived of [\"server_idle_close_hint\", \"server_idle_close_buffer\"])\n if ((knobs as Readonly<Record<string, Extent | undefined>>)[derived] !== undefined)\n throw new Error(`the knob ${derived} comes from the transport: give only server_idle_close`);\n if (server.listenerCount(\"request\") > 0 || server.listenerCount(\"upgrade\") > 0)\n throw new Error(\"the server has a request or upgrade listener of its own: mount it as a handler\");\n // Node closes an idle connection at keepAliveTimeout + keepAliveTimeoutBuffer, and its hint is\n // keepAliveTimeout. The buffer stays, thus the hint is less than the real close. An older Node has no buffer\n const buffer = (server as { keepAliveTimeoutBuffer?: number }).keepAliveTimeoutBuffer ?? 0;\n if (Number.isFinite(idleClose) && idleClose <= buffer)\n throw new Error(\n `the knob server_idle_close is ${idleClose} ms, at or below the keepAliveTimeoutBuffer of the server, ${buffer} ms: ` +\n \"the server closes an idle connection at keepAliveTimeout + keepAliveTimeoutBuffer\",\n );\n // 0 stops the keep-alive timer and the hint in Node\n server.keepAliveTimeout = Number.isFinite(idleClose) ? idleClose - buffer : 0;\n const declared: HttpTransportKnobs = {\n ...knobs,\n server_idle_close_hint: Number.isFinite(idleClose)\n ? { value: Math.floor(server.keepAliveTimeout / 1000), unit: \"s\" }\n : { value: \"infinite\", unit: \"s\" },\n server_idle_close_buffer: { value: buffer, unit: \"ms\" },\n };\n\n const requestMounts: RequestMount[] = [];\n const upgradeMounts: UpgradeMount[] = [];\n const responses = new Set<ServerResponse>();\n const upgraded = new Set<UpgradeSocket>();\n const sockets = new Set<Socket>();\n const idleWaiters = new Set<() => void>();\n const closedWaiters = new Set<() => void>();\n const intake = new AbortController();\n const cooperative = new AbortController();\n // the lifecycle comes at the start, thus the context reads it at each request\n let life: Lifecycle | null = null;\n const context: MountContext = {\n intake: intake.signal,\n signal: cooperative.signal,\n get life() {\n return life;\n },\n retryAfter: knobs.server_retry_after,\n };\n let intakeOpen = true;\n let listenerOpen = server.listening;\n let serverClosed = !server.listening;\n let sweep: Cancel | null = null;\n let closing: Promise<void> | null = null;\n\n const inFlight = (): number => responses.size + upgraded.size;\n const report = (error: unknown, req: IncomingMessage): void => {\n try {\n options.onError?.(error, req);\n } catch {\n // an error of the error hook does not stop the transport\n }\n };\n const settled = (): void => {\n if (inFlight() === 0) for (const done of [...idleWaiters]) done();\n if (closing !== null && inFlight() === 0 && sockets.size === 0 && serverClosed)\n for (const done of [...closedWaiters]) done();\n };\n // before its start, a transport has no lifecycle: a server that listens already keeps its connections\n const keepsAlive = (): boolean => intakeOpen && (options.keepAlive?.() ?? life?.keepAlive() ?? true);\n const live = <T extends { readonly until: MountStage }>(mounts: readonly T[]): T[] =>\n intakeOpen ? [...mounts] : mounts.filter((mount) => mount.until === \"close transports\");\n const throughDrain = (): boolean =>\n requestMounts.some((mount) => mount.until === \"close transports\") ||\n upgradeMounts.some((mount) => mount.until === \"close transports\");\n const closeListener = (): void => {\n if (!listenerOpen) return;\n listenerOpen = false;\n server.close();\n };\n const startSweep = (): void => {\n if (!Number.isFinite(sweepEvery) || closing !== null) return;\n sweep = clock.after(sweepEvery, () => {\n sweep = null;\n if (closing !== null) return;\n server.closeIdleConnections();\n startSweep();\n });\n };\n\n /**\n * This function watches the headers of each response when they go, whatever wrote them. Each 503\n * gets `Retry-After`, and a 500 gets none. The response closes its connection while the transport\n * cannot keep it.\n */\n const watchHeaders = (res: ServerResponse): void => {\n const writeHead = res.writeHead as (...args: unknown[]) => ServerResponse;\n res.writeHead = ((...args: unknown[]): ServerResponse => {\n if (res.headersSent) return writeHead.apply(res, args);\n if (!keepsAlive()) {\n res.shouldKeepAlive = false;\n res.setHeader(\"connection\", \"close\");\n }\n return writeHead.apply(res, retryRule(res, args, retryAfterHeader));\n }) as ServerResponse[\"writeHead\"];\n };\n const answer = (res: ServerResponse, status: number, headers: Readonly<Record<string, string>> = {}): void => {\n if (res.headersSent) {\n res.destroy();\n return;\n }\n const body = STATUS_TEXT[status] ?? \"\";\n res.writeHead(status, { \"content-type\": \"text/plain; charset=utf-8\", \"content-length\": String(Buffer.byteLength(body)), ...headers });\n res.end(body);\n };\n const refuse = (res: ServerResponse): void => answer(res, 503, { \"retry-after\": retryAfterHeader, connection: \"close\" });\n\n const route = async (req: IncomingMessage, res: ServerResponse): Promise<void> => {\n const mounts = live(requestMounts);\n try {\n for (const mount of mounts) {\n const given = mount.handler(req, res, context);\n const taken = typeof given === \"boolean\" ? given : await given;\n // a handler that wrote its response took the request, whatever it gave\n if (taken || res.headersSent || res.writableEnded) return;\n }\n } catch (error) {\n report(error, req);\n if (!res.writableEnded) answer(res, 500);\n return;\n }\n if (intakeOpen) answer(res, 404);\n else refuse(res);\n };\n\n const onRequest = (req: IncomingMessage, res: ServerResponse): void => {\n responses.add(res);\n res.once(\"close\", () => {\n responses.delete(res);\n settled();\n });\n watchHeaders(res);\n void route(req, res);\n };\n\n const onUpgrade = (req: IncomingMessage, socket: UpgradeSocket, head: UpgradeArgs[2]): void => {\n socket.on(\"error\", () => {});\n const mount = live(upgradeMounts).find((each) => each.matches(req));\n if (mount === undefined) {\n const status = intakeOpen ? 404 : 503;\n const extra = intakeOpen ? \"\" : `Retry-After: ${retryAfterHeader}\\r\\n`;\n socket.end(`HTTP/1.1 ${status} ${STATUS_TEXT[status]!}\\r\\nConnection: close\\r\\nContent-Length: 0\\r\\n${extra}\\r\\n`);\n return;\n }\n if (socket.destroyed) return;\n upgraded.add(socket);\n socket.once(\"close\", () => {\n upgraded.delete(socket);\n settled();\n });\n try {\n mount.handler(req, socket, head, context);\n } catch (error) {\n report(error, req);\n socket.destroy();\n }\n };\n\n const onConnection = (socket: Socket): void => {\n sockets.add(socket);\n socket.once(\"close\", () => {\n sockets.delete(socket);\n settled();\n });\n };\n\n server.on(\"request\", onRequest);\n server.on(\"upgrade\", onUpgrade);\n server.on(\"connection\", onConnection);\n server.on(\"listening\", () => {\n listenerOpen = true;\n serverClosed = false;\n });\n server.on(\"close\", () => {\n listenerOpen = false;\n serverClosed = true;\n settled();\n });\n\n const remover = <T>(list: T[], entry: T) => (): void => {\n const at = list.indexOf(entry);\n if (at >= 0) list.splice(at, 1);\n // the last mount through the drain went: the listener closes as the stop of the intake asks\n if (!intakeOpen && closing === null && !throughDrain()) closeListener();\n };\n\n return {\n server,\n knobs: declared,\n get connections() {\n return sockets.size;\n },\n get intakeStopped() {\n return !intakeOpen;\n },\n mount(handler, mountOptions = {}) {\n const entry: RequestMount = { handler, until: mountOptions.until ?? handler.until ?? \"stop intake\" };\n requestMounts.push(entry);\n return remover(requestMounts, entry);\n },\n upgrade(match, handler, mountOptions = {}) {\n const matches = typeof match === \"string\" ? (req: IncomingMessage) => pathOf(req.url) === match : match;\n const entry: UpgradeMount = { matches, handler, until: mountOptions.until ?? handler.until ?? \"stop intake\" };\n upgradeMounts.push(entry);\n return remover(upgradeMounts, entry);\n },\n async start(_signal, given) {\n life = given;\n if (closing !== null) throw new Error(\"the transport closed: a closed transport does not start again\");\n // a listener that another owner adds after the transport answers too, thus the start refuses it\n if (server.listenerCount(\"request\") > 1 || server.listenerCount(\"upgrade\") > 1)\n throw new Error(\"the server has a request or upgrade listener of its own: mount it as a handler\");\n if (server.listening) return;\n const listen = options.listen;\n if (listen === undefined) throw new Error(\"the server does not listen, and the transport has no listen options\");\n if (typeof listen === \"function\") {\n await listen(server);\n return;\n }\n await new Promise<void>((resolve, reject) => {\n const failed = (error: Error): void => {\n server.off(\"listening\", listened);\n reject(error);\n };\n const listened = (): void => {\n server.off(\"error\", failed);\n resolve();\n };\n server.once(\"error\", failed);\n server.once(\"listening\", listened);\n server.listen(listen);\n });\n },\n stopIntake() {\n if (!intakeOpen) return;\n intakeOpen = false;\n intake.abort(new Error(\"the transport stopped its intake\"));\n if (!throughDrain()) closeListener();\n server.closeIdleConnections();\n startSweep();\n },\n drain(deadline, signal) {\n const abort = (): void => {\n if (!cooperative.signal.aborted) cooperative.abort(signal.reason);\n };\n if (signal.aborted) abort();\n else signal.addEventListener(\"abort\", abort, { once: true });\n // nothing in flight, or a deadline that passed: the drain ends at once, with no timer\n if (inFlight() === 0 || deadline <= clock.now()) {\n signal.removeEventListener(\"abort\", abort);\n return Promise.resolve();\n }\n return new Promise<void>((resolve) => {\n let cancel: Cancel = () => {};\n const done = (): void => {\n idleWaiters.delete(done);\n cancel();\n signal.removeEventListener(\"abort\", abort);\n resolve();\n };\n idleWaiters.add(done);\n cancel = clock.after(Math.max(0, deadline - clock.now()), done);\n });\n },\n inFlight,\n close() {\n if (closing !== null) return closing;\n intakeOpen = false;\n if (!intake.signal.aborted) intake.abort(new Error(\"the transport closed\"));\n if (!cooperative.signal.aborted) cooperative.abort(new Error(\"the transport closed\"));\n sweep?.();\n sweep = null;\n closing = new Promise<void>((resolve) => {\n closedWaiters.add(resolve);\n });\n closeListener();\n server.closeAllConnections();\n for (const socket of [...upgraded]) socket.destroy();\n for (const socket of [...sockets]) socket.destroy();\n settled();\n return closing;\n },\n };\n};\n","import type { IncomingMessage, Server, ServerResponse } from \"node:http\";\nimport { createServer } from \"node:http\";\nimport type { ListenOptions, Socket } from \"node:net\";\nimport type { Health, ProbeKind } from \"../health.ts\";\nimport type { HealthServer } from \"../lifecycle.ts\";\nimport type { Quantity } from \"@mark1russell7/quantity\";\nimport type { MountContext, RequestHandler } from \"./transport.ts\";\nimport { pathOf, retryAfterOf } from \"./transport.ts\";\n\n/** A probe response is the status, the headers and the body of one probe, for any HTTP framework or none. */\nexport interface ProbeResponse {\n readonly status: number;\n readonly headers: Readonly<Record<string, string>>;\n readonly body: string;\n}\n\n/** A health source is the health runtime, or a function that gives it when it exists. */\nexport type HealthSource = Health | (() => Health | null | undefined);\n\nconst KINDS: readonly ProbeKind[] = [\"startup\", \"readiness\", \"liveness\", \"report\"];\n\nconst prefixProblem = (prefix: string): string | null =>\n prefix === \"\" || (prefix.startsWith(\"/\") && !prefix.endsWith(\"/\"))\n ? null\n : `the probe prefix \"${prefix}\" does not start with \"/\", or it ends with \"/\"`;\n\nconst json = (status: number, body: unknown): ProbeResponse => ({\n status,\n headers: { \"content-type\": \"application/health+json\", \"cache-control\": \"no-store\" },\n body: JSON.stringify(body),\n});\n\n/**\n * This function gives the response of a probe path from the cache of the health runtime. For a path\n * that is not a probe, it gives null. The paths are `/startup`, `/readiness`, `/liveness` and\n * `/report` after the prefix. `/readiness/<controller>` gives the readiness of one controller.\n */\nexport const probeResponse = (\n health: Health | null | undefined,\n method: string | undefined,\n path: string,\n prefix = \"\",\n): ProbeResponse | null => {\n if (!path.startsWith(`${prefix}/`)) return null;\n const parts = path.slice(prefix.length + 1).split(\"/\");\n const kind = KINDS.find((each) => each === parts[0]);\n if (kind === undefined) return null;\n const controller = parts[1];\n if (parts.length > 2 || (controller !== undefined && (kind !== \"readiness\" || controller === \"\"))) return null;\n if (method !== \"GET\" && method !== \"HEAD\")\n return { status: 405, headers: { allow: \"GET, HEAD\", \"content-length\": \"0\" }, body: \"\" };\n // before the runtime exists, each probe fails: the service has not started\n if (health === null || health === undefined) return json(503, { status: \"fail\", probe: kind });\n try {\n const answer = health.probe(kind, controller === undefined ? undefined : decodeURIComponent(controller));\n return { ...json(answer.status, answer.body), headers: { \"content-type\": answer.contentType, \"cache-control\": \"no-store\" } };\n } catch {\n // a controller that the service does not have\n return json(404, { status: \"fail\", probe: kind, controller });\n }\n};\n\n/** This function writes a probe response. A 503 gets `Retry-After`, when the writer has the time of a retry. */\nconst write = (req: IncomingMessage, res: ServerResponse, response: ProbeResponse, retryAfter: string | undefined): void => {\n const retry = response.status === 503 && retryAfter !== undefined ? { \"retry-after\": retryAfter } : {};\n res.writeHead(response.status, { ...response.headers, ...retry, \"content-length\": String(Buffer.byteLength(response.body)) });\n res.end(req.method === \"HEAD\" ? undefined : response.body);\n};\n\n/** These options mount the probes on the port of the application. */\nexport interface ProbeHandlerOptions {\n /** This path goes before each probe path, for example `/health`. It starts with `/`, or it is empty. */\n readonly prefix: string;\n}\n\n/**\n * This function gives a request handler that answers the probes on the port of the application. It\n * answers until `close transports`, thus its mount keeps the listener open through the drain. A path\n * that is not a probe goes to the next handler. Each 503 gets the `Retry-After` of the context.\n */\nexport const probeHandler = (health: HealthSource, options: ProbeHandlerOptions): RequestHandler => {\n const problem = prefixProblem(options.prefix);\n if (problem !== null) throw new Error(problem);\n const handler = (req: IncomingMessage, res: ServerResponse, context?: MountContext): boolean => {\n const runtime = typeof health === \"function\" ? health() : health;\n const response = probeResponse(runtime, req.method, pathOf(req.url), options.prefix);\n if (response === null) return false;\n // the transport gives server_retry_after through the context, and it adds the header to each 503 too\n const retryAfter = context?.retryAfter === undefined ? undefined : retryAfterOf({ server_retry_after: context.retryAfter }, \"probeHandler\");\n write(req, res, response, retryAfter);\n return true;\n };\n return Object.assign(handler, { until: \"close transports\" as const });\n};\n\n/** These options make a probe server: the listen options of its port, the prefix of its paths, and the time of a retry. */\nexport interface ProbeServerOptions extends ListenOptions {\n /**\n * This time goes in the `Retry-After` of each 503 of a probe, as on the port of the application.\n * The probe server has no defaults, thus the knob is necessary.\n */\n readonly server_retry_after: Quantity;\n /** This path goes before each probe path. The default is no prefix, because the port has only the probes. */\n readonly prefix?: string | undefined;\n /** This server answers the probes. Without it, the probe server makes a server of `node:http`. */\n readonly server?: Server | undefined;\n}\n\n/** A probe server is a health server on its own port. Its server is visible for the address of the port. */\nexport interface ProbeServer extends HealthServer {\n readonly server: Server;\n}\n\n/**\n * This function makes a health server on a port of its own (S6 decision D5). It starts first and\n * closes last, thus the probes answer through the whole drain. Each probe answers from the cache, and\n * each 503 gets the `Retry-After` of `server_retry_after`.\n */\nexport const probeServer = (options: ProbeServerOptions): ProbeServer => {\n const { prefix = \"\", server = createServer(), server_retry_after, ...listen } = options;\n const retryAfter = retryAfterOf({ server_retry_after }, \"probeServer\");\n const problem = prefixProblem(prefix);\n if (problem !== null) throw new Error(problem);\n if (server.listenerCount(\"request\") > 0) throw new Error(\"the probe server has a request listener of its own\");\n let health: Health | null = null;\n let closing: Promise<void> | null = null;\n const sockets = new Set<Socket>();\n server.on(\"connection\", (socket: Socket) => {\n sockets.add(socket);\n socket.once(\"close\", () => sockets.delete(socket));\n });\n server.on(\"request\", (req: IncomingMessage, res: ServerResponse) => {\n const response = probeResponse(health, req.method, pathOf(req.url), prefix);\n write(req, res, response ?? { status: 404, headers: { \"content-type\": \"text/plain; charset=utf-8\" }, body: \"Not Found\" }, retryAfter);\n });\n return {\n server,\n listen(runtime) {\n health = runtime;\n if (server.listening) return Promise.resolve();\n return new Promise<void>((resolve, reject) => {\n const failed = (error: Error): void => {\n server.off(\"listening\", listened);\n reject(error);\n };\n const listened = (): void => {\n server.off(\"error\", failed);\n resolve();\n };\n server.once(\"error\", failed);\n server.once(\"listening\", listened);\n server.listen(listen);\n });\n },\n close() {\n if (closing !== null) return closing;\n closing = new Promise<void>((resolve) => {\n if (!server.listening) {\n resolve();\n return;\n }\n server.close(() => resolve());\n server.closeAllConnections();\n for (const socket of [...sockets]) socket.destroy();\n });\n return closing;\n },\n };\n};\n","import type { Quantity } from \"@mark1russell7/quantity\";\nimport { valueIn } from \"@mark1russell7/quantity\";\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport type { RequestEnd, RequestEvent, RequestOptions, RequestStream, RequestWork } from \"../requests.ts\";\nimport type { MountContext, RequestHandler } from \"./transport.ts\";\nimport { pathOf } from \"./transport.ts\";\n\n/** An outcome response is the status, the headers and the body of a request, for any HTTP framework or none. */\nexport interface OutcomeResponse {\n readonly status: number;\n readonly headers: Readonly<Record<string, string>>;\n readonly body: string | Uint8Array;\n}\n\n/** A response part is what a hook gives for one value or one failure. The parts that it does not give keep the values of the mapping. */\nexport interface ResponsePart {\n readonly status?: number | undefined;\n readonly headers?: Readonly<Record<string, string>> | undefined;\n readonly body?: string | Uint8Array | undefined;\n}\n\n/**\n * A stream format writes the items of a stream and its end in the body, as they come. An end after\n * the first item goes in the body, because the status went with the first item.\n */\nexport interface StreamFormat<T = unknown> {\n readonly contentType: string;\n /** This function gives the bytes of one item. */\n item(value: T): string | Uint8Array;\n /** This function gives the bytes of the end, the last part of the body. */\n end(end: RequestEnd): string | Uint8Array;\n}\n\n/** These options map a request to a response. */\nexport interface OutcomeOptions<T> {\n /**\n * This value tells if the response can keep its connection open, as `life.keepAlive()` does. False\n * gives `Connection: close`. A function is read when the headers go.\n */\n readonly keepAlive: boolean | (() => boolean);\n /** This time goes in the `Retry-After` of each 503, in whole seconds, rounded up. It is the knob `server_retry_after`. */\n readonly retryAfter: Quantity;\n /**\n * This function gives the response of the value of a unary request. Without it, the mapping gives\n * 200 and the value as JSON, or 204 for no value.\n */\n readonly done?: ((value: T) => ResponsePart) | undefined;\n /**\n * This function gives the response of a failure of the route before the first byte. Without it,\n * the mapping gives 500 with no detail, because an error can hold private data.\n */\n readonly failed?: ((error: unknown) => ResponsePart) | undefined;\n /**\n * This format makes the response a stream: the status and each item go as they come, and the end\n * goes in the body. Without it, the response is unary: one status and the one value.\n */\n readonly stream?: StreamFormat<T> | undefined;\n}\n\nconst JSON_TYPE = \"application/json; charset=utf-8\";\n\n/**\n * This function gives an end as a record for a body: its kind and the count of its items. A cut and\n * a refusal also give their cause, and if the caller can send the request again. A failure gives\n * no detail.\n */\nexport const endRecord = (end: RequestEnd): Readonly<Record<string, unknown>> => {\n if (end.kind === \"refused\" || end.kind === \"cut\")\n return { end: end.kind, delivered: end.delivered, retryable: end.retryable, cause: end.cause };\n return { end: end.kind, delivered: end.delivered };\n};\n\n/** This format writes each item and the end as one line of JSON (NDJSON). */\nexport const ndjson: StreamFormat = {\n contentType: \"application/x-ndjson; charset=utf-8\",\n item: (value) => `${JSON.stringify(value)}\\n`,\n end: (end) => `${JSON.stringify(endRecord(end))}\\n`,\n};\n\n/** This format writes each item as an event of server-sent events, and the end as the event `end`. */\nexport const sse: StreamFormat = {\n contentType: \"text/event-stream; charset=utf-8\",\n item: (value) => `data: ${JSON.stringify(value)}\\n\\n`,\n end: (end) => `event: end\\ndata: ${JSON.stringify(endRecord(end))}\\n\\n`,\n};\n\nconst valueResponse = (value: unknown): OutcomeResponse => {\n if (value === undefined) return { status: 204, headers: {}, body: \"\" };\n if (typeof value === \"string\") return { status: 200, headers: { \"content-type\": \"text/plain; charset=utf-8\" }, body: value };\n if (value instanceof Uint8Array) return { status: 200, headers: { \"content-type\": \"application/octet-stream\" }, body: value };\n return { status: 200, headers: { \"content-type\": JSON_TYPE }, body: JSON.stringify(value) };\n};\n\nconst merge = (base: OutcomeResponse, part: ResponsePart | undefined): OutcomeResponse =>\n part === undefined\n ? base\n : { status: part.status ?? base.status, headers: { ...base.headers, ...part.headers }, body: part.body ?? base.body };\n\nconst keeps = (keepAlive: boolean | (() => boolean)): boolean => (typeof keepAlive === \"function\" ? keepAlive() : keepAlive);\n\n/**\n * This function applies the rules of the headers to a response. Each 503 gets `Retry-After`, and a\n * 500 gets none. The response closes its connection while the service cannot keep it.\n */\nconst ruled = (response: OutcomeResponse, retryAfter: Quantity, keepAlive: boolean | (() => boolean)): OutcomeResponse => {\n const headers: Record<string, string> = { ...response.headers };\n if (response.status === 503 && headers[\"retry-after\"] === undefined)\n headers[\"retry-after\"] = String(Math.max(0, Math.ceil(valueIn(retryAfter, \"s\"))));\n if (response.status === 500) delete headers[\"retry-after\"];\n if (!keeps(keepAlive)) headers[\"connection\"] = \"close\";\n return { ...response, headers };\n};\n\n/** This function gives the status response of an end before the first byte of the body. */\nconst endStatus = <T>(end: RequestEnd, options: OutcomeOptions<T>): OutcomeResponse => {\n const body = JSON.stringify(endRecord(end));\n switch (end.kind) {\n case \"refused\":\n return { status: 503, headers: { \"content-type\": JSON_TYPE }, body };\n case \"cut\":\n // a cut after the first item has no status of its own, but no byte went: its route decides\n return { status: end.status ?? (end.retryable ? 503 : 500), headers: { \"content-type\": JSON_TYPE }, body };\n case \"failed\":\n return merge({ status: 500, headers: { \"content-type\": JSON_TYPE }, body }, options.failed?.(end.error));\n case \"done\":\n return { status: 204, headers: {}, body: \"\" };\n }\n};\n\n/**\n * This function maps a unary request to its response, from its items and its end (S6 A12 and A13).\n * - One value and the end `done`: 200 with the value, or the response of the `done` hook.\n * - No value and the end `done`: 204. More than one value: 500, because the route is not a stream.\n * - `refused`: 503. The work did not start, thus the caller can send it again. The service did not\n * start, it stopped its intake, or a controller is not ready.\n * - `cut`: 503 for a route whose callers can send it again (S6 A12), and 500 for each other route.\n * - `failed`: 500 with no detail, or the response of the `failed` hook. A failure of the route is not a 503.\n *\n * Each 503 gets `Retry-After` (RFC 9110), and a 500 gets none. Each response gets `Connection: close`\n * while the service cannot keep the connection.\n */\nexport const outcomeResponse = <T>(items: readonly T[], end: RequestEnd, options: OutcomeOptions<T>): OutcomeResponse => {\n let response: OutcomeResponse;\n if (end.kind !== \"done\") response = endStatus(end, options);\n else if (items.length === 1) response = merge(valueResponse(items[0]), options.done?.(items[0]!));\n else if (items.length === 0) response = endStatus(end, options);\n else\n response = endStatus(\n { kind: \"failed\", error: new Error(`the work gave ${items.length} values, and a unary response has one`), delivered: items.length },\n options,\n );\n return ruled(response, options.retryAfter, options.keepAlive);\n};\n\n/** This function writes a whole response of `node:http`. A response whose headers went gets no second status: it is destroyed. */\nconst writeWhole = (res: ServerResponse, response: OutcomeResponse): void => {\n if (res.headersSent) {\n res.destroy();\n return;\n }\n if (response.headers[\"connection\"] === \"close\") res.shouldKeepAlive = false;\n // a 204 has no body, thus no length\n if (response.status === 204) {\n res.writeHead(204, response.headers);\n res.end();\n return;\n }\n const length = typeof response.body === \"string\" ? Buffer.byteLength(response.body) : response.body.byteLength;\n res.writeHead(response.status, { ...response.headers, \"content-length\": String(length) });\n res.end(response.body);\n};\n\n/**\n * This function writes the stream of a request to a response of `node:http`, and gives its end. It\n * gives null when the client closed the connection first. Then the work stops through `return()`.\n * - Unary (no format): it waits for the end, and it writes the status and the body of `outcomeResponse`.\n * - Stream: the status 200 goes with the first item, then each item goes as it comes, then the end\n * goes in the body (in-band). The next item waits until the client reads, thus a slow client\n * slows the work. An end before the first item gets a status, as a unary end does.\n */\nexport const writeOutcome = async <T>(res: ServerResponse, stream: RequestStream<T>, options: OutcomeOptions<T>): Promise<RequestEnd | null> => {\n const iterator = stream[Symbol.asyncIterator]();\n let gone: () => void = () => {};\n const closed = new Promise<\"gone\">((resolve) => {\n gone = () => resolve(\"gone\");\n });\n // a close before the end of the response is a client that went away\n const onClose = (): void => {\n if (!res.writableFinished) gone();\n };\n res.once(\"close\", onClose);\n if (res.destroyed) gone();\n const leave = async (): Promise<null> => {\n await iterator.return?.();\n return null;\n };\n const next = async (): Promise<RequestEvent<T> | \"gone\"> => {\n const step = await Promise.race([iterator.next(), closed]);\n if (step === \"gone\") return step;\n if (step.done === true) throw new Error(\"the stream of the request ended without its end\");\n return step.value;\n };\n /** This function writes one part of the body, and waits while the client reads slower than the work. */\n const send = async (chunk: string | Uint8Array): Promise<boolean> => {\n if (res.write(chunk)) return true;\n const drained = new Promise<\"drained\">((resolve) => res.once(\"drain\", () => resolve(\"drained\")));\n return (await Promise.race([drained, closed])) === \"drained\";\n };\n try {\n const format = options.stream;\n if (format === undefined) {\n const items: T[] = [];\n for (;;) {\n const event = await next();\n if (event === \"gone\") return await leave();\n if (event.kind === \"item\") {\n items.push(event.value);\n continue;\n }\n writeWhole(res, outcomeResponse(items, event, options));\n return event;\n }\n }\n const first = await next();\n if (first === \"gone\") return await leave();\n if (first.kind !== \"item\") {\n // an empty stream that ended well gives 200 and its end. Each other end before the first item gets a status\n if (first.kind === \"done\")\n writeWhole(res, ruled({ status: 200, headers: { \"content-type\": format.contentType, \"cache-control\": \"no-store\" }, body: format.end(first) }, options.retryAfter, options.keepAlive));\n else writeWhole(res, outcomeResponse([], first, options));\n return first;\n }\n if (res.headersSent) {\n res.destroy();\n return await leave();\n }\n const head = ruled({ status: 200, headers: { \"content-type\": format.contentType, \"cache-control\": \"no-store\" }, body: \"\" }, options.retryAfter, options.keepAlive);\n if (head.headers[\"connection\"] === \"close\") res.shouldKeepAlive = false;\n res.writeHead(200, head.headers);\n if (!(await send(format.item(first.value)))) return await leave();\n for (;;) {\n const event = await next();\n if (event === \"gone\") return await leave();\n if (event.kind === \"item\") {\n if (!(await send(format.item(event.value)))) return await leave();\n continue;\n }\n // the end after the first item is in-band: the status went with the first item\n res.end(format.end(event));\n return event;\n }\n } finally {\n res.off(\"close\", onClose);\n }\n};\n\n/** A route of a handler is the name of the route that a controller declares, or a function that gives it from the request. */\nexport type HandledRoute = string | ((req: IncomingMessage) => string | null);\n\n/** A handled work does the work of one request of `node:http`. Its signal aborts when the shutdown or the client stops the request. */\nexport type HandledWork<T> = (req: IncomingMessage, signal: AbortSignal) => RequestWork<T>;\n\n/** These options bind a request of `node:http` to the request tracking of the service. */\nexport interface HandleOptions<T> extends Omit<OutcomeOptions<T>, \"keepAlive\" | \"retryAfter\"> {\n /** This function gives the detail of a request, for the function of a route pattern. */\n readonly detail?: ((req: IncomingMessage) => unknown) | undefined;\n}\n\n/**\n * This function gives a request handler that does each request of a route through `life.request`\n * (S6 A12 and A13). The transport gives the lifecycle and `server_retry_after` through the context.\n * - A route name takes a request whose method and path are that name, for example `POST /orders`.\n * A function gives the name of a route or of a pattern, or null for a request of another handler.\n * - Before the start of the transport, each request gets a retryable 503.\n * - The response is unary, or a stream with the option `stream`, as `writeOutcome` writes it.\n */\nexport const handle = <T>(controller: string, route: HandledRoute, work: HandledWork<T>, options: HandleOptions<T> = {}): RequestHandler => {\n const nameOf = (req: IncomingMessage): string | null => {\n if (typeof route === \"function\") return route(req);\n return `${req.method ?? \"GET\"} ${pathOf(req.url)}` === route ? route : null;\n };\n return async (req: IncomingMessage, res: ServerResponse, context: MountContext): Promise<boolean> => {\n const name = nameOf(req);\n if (name === null) return false;\n const life = context.life;\n const written: OutcomeOptions<T> = {\n ...options,\n keepAlive: () => life?.keepAlive() ?? false,\n retryAfter: context.retryAfter,\n };\n if (life === null) {\n const refused: RequestEnd = { kind: \"refused\", status: 503, retryable: true, cause: \"the transport has not started\", delivered: 0 };\n writeWhole(res, outcomeResponse<T>([], refused, written));\n return true;\n }\n const given: RequestOptions = options.detail === undefined ? {} : { detail: options.detail(req) };\n await writeOutcome(res, life.request(controller, name, (signal) => work(req, signal), given), written);\n return true;\n };\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"http.js","names":["knobMilliseconds"],"sources":["../src/http/transport.ts","../src/http/probes.ts","../src/http/outcome.ts"],"sourcesContent":["import type { Extent, Quantity } from \"@mark1russell7/quantity\";\nimport type { IncomingMessage, Server, ServerEventMap, ServerResponse } from \"node:http\";\nimport type { ListenOptions, Socket } from \"node:net\";\nimport type { Cancel, Clock } from \"../clock.ts\";\nimport { realClock } from \"../clock.ts\";\nimport { millisecondsOf as knobMilliseconds } from \"../knobs.ts\";\nimport type { Lifecycle, TransportImpl } from \"../lifecycle.ts\";\n\n/**\n * These are the knobs of an HTTP transport, by the names of the ordering law. The transport has no\n * defaults, thus each knob is necessary. The declaration of the server gets the same record.\n */\nexport interface HttpKnobs {\n /**\n * This time is the real idle close of the server, which O01 compares with the idle time of a\n * client. Node closes an idle connection at `keepAliveTimeout` plus `keepAliveTimeoutBuffer`.\n * Thus the transport sets `keepAliveTimeout` to this time less the buffer of the server.\n * A time without a bound stops the timer.\n */\n readonly server_idle_close: Extent;\n /**\n * This time is the interval of the sweep of idle connections, from the stop of the intake until the\n * close. A time without a bound gives one sweep, at the stop of the intake.\n */\n readonly server_idle_sweep_interval: Extent;\n /** This time goes in the `Retry-After` of each 503 that the transport gives after its intake stops. */\n readonly server_retry_after: Quantity;\n /** The other knobs of the server go to the declaration. The transport does not read them. */\n readonly [knob: string]: Extent;\n}\n\n/**\n * These are the knobs that the transport gives the declaration: the knobs of the caller, and the two\n * times that the transport found. Thus the declaration shows the three times of the idle close.\n */\nexport interface HttpTransportKnobs extends HttpKnobs {\n /** This time is the hint that each response sends (`Keep-Alive: timeout=…`), in whole seconds. A time without a bound tells that no hint goes. */\n readonly server_idle_close_hint: Extent;\n /** This time is the `keepAliveTimeoutBuffer` of the server, which Node adds to the timer and not to the hint. */\n readonly server_idle_close_buffer: Quantity;\n}\n\n/** A mount stage is the stage of the shutdown at which a mount stops: the stop of the intake, or the close. */\nexport type MountStage = \"stop intake\" | \"close transports\";\n\n/** A mount context gives each handler the signals of the shutdown, the lifecycle and the time of a retry. */\nexport interface MountContext {\n /** This signal aborts when the intake stops. A long stream ends in its own protocol at it. */\n readonly intake: AbortSignal;\n /** This signal aborts at the cooperative abort of the drain, or at the close. The work in flight stops at it. */\n readonly signal: AbortSignal;\n /** This lifecycle is the lifecycle that the transport got at its start, or null before the start. */\n readonly life: Lifecycle | null;\n /** This time is the knob `server_retry_after`: the `Retry-After` of each 503 of the handlers. */\n readonly retryAfter: Quantity;\n}\n\n/**\n * A request handler answers a request and gives true, or it gives false and does not touch the\n * response. Then the next handler gets the request. `until` tells when the handler stops.\n */\nexport interface RequestHandler {\n (req: IncomingMessage, res: ServerResponse, context: MountContext): boolean | Promise<boolean>;\n readonly until?: MountStage | undefined;\n}\n\ntype UpgradeArgs = ServerEventMap[\"upgrade\"];\n\n/** An upgrade socket is the stream of an upgraded connection, as the `upgrade` event of Node gives it. */\nexport type UpgradeSocket = UpgradeArgs[1];\n\n/** An upgrade handler takes an upgraded connection, for example a WebSocket server in its `noServer` mode. */\nexport interface UpgradeHandler {\n (req: IncomingMessage, socket: UpgradeSocket, head: UpgradeArgs[2], context: MountContext): void;\n readonly until?: MountStage | undefined;\n}\n\n/** An upgrade match is a path, without its query, or a function that tells if a handler takes a request. */\nexport type UpgradeMatch = string | ((req: IncomingMessage) => boolean);\n\n/** These options change one mount. */\nexport interface MountOptions {\n /**\n * This stage is when the mount stops. Without it, the stage of the handler applies, and then\n * `stop intake`. A mount until `close transports` keeps the listener open through the drain.\n */\n readonly until?: MountStage | undefined;\n}\n\n/** These options make an HTTP transport. */\nexport interface HttpTransportOptions {\n /**\n * These are the listen options of `start()`, or a function that makes the server listen. Without\n * them, `start()` takes a server that listens already.\n */\n readonly listen?: ListenOptions | ((server: Server) => Promise<void> | void) | undefined;\n /** This clock is the clock of the lifecycle. The deadline of the drain and the sweep use it. */\n readonly clock?: Clock | undefined;\n /**\n * This function tells if a response can keep its connection open. Without it, the transport reads\n * `life.keepAlive()` of the lifecycle of its start. The transport reads it when the headers of\n * each response go, and false gives `Connection: close`.\n */\n readonly keepAlive?: (() => boolean) | undefined;\n /** This function receives each error of a handler. The transport then answers 500, or it destroys the response. */\n readonly onError?: ((error: unknown, req: IncomingMessage) => void) | undefined;\n}\n\n/** An HTTP transport is a server of `node:http` as a transport of the lifecycle, with a list of mounts on its listener. */\nexport interface HttpTransport extends TransportImpl {\n readonly server: Server;\n /** These knobs go to the declaration of the server: the knobs of the caller, the hint and the buffer. */\n readonly knobs: HttpTransportKnobs;\n /** This method adds a request handler at the end of the list, and gives the function that removes it. */\n mount(handler: RequestHandler, options?: MountOptions): () => void;\n /** This method adds an upgrade handler for a path, and gives the function that removes it. */\n upgrade(match: UpgradeMatch, handler: UpgradeHandler, options?: MountOptions): () => void;\n /** This count is the number of open connections, idle connections and upgraded connections too. */\n readonly connections: number;\n /** This flag tells if the transport stopped its intake. */\n readonly intakeStopped: boolean;\n}\n\ninterface RequestMount {\n readonly handler: RequestHandler;\n readonly until: MountStage;\n}\n\ninterface UpgradeMount {\n readonly matches: (req: IncomingMessage) => boolean;\n readonly handler: UpgradeHandler;\n readonly until: MountStage;\n}\n\n/** This function gives the path of a request target, without its query and fragment. */\nexport const pathOf = (url: string | undefined): string => {\n const raw = url ?? \"/\";\n const end = raw.search(/[?#]/);\n const path = end === -1 ? raw : raw.slice(0, end);\n if (path.startsWith(\"/\")) return path;\n // an absolute target, as a proxy sends it\n try {\n return new URL(raw).pathname;\n } catch {\n return path;\n }\n};\n\n/** This function gives a knob in milliseconds, or infinity for a time without a bound. */\nconst millisecondsOf = (knobs: Readonly<Record<string, Extent | undefined>>, name: string, owner = \"httpTransport\"): number =>\n knobMilliseconds(knobs, name, owner);\n\n/**\n * This function gives the `Retry-After` of the knob `server_retry_after`, in whole seconds, rounded\n * up. The knob is necessary, and it has a bound.\n */\nexport const retryAfterOf = (knobs: Readonly<Record<string, Extent | undefined>>, owner: string): string => {\n const retryAfter = millisecondsOf(knobs, \"server_retry_after\", owner);\n if (!Number.isFinite(retryAfter)) throw new Error(\"the knob server_retry_after has no bound: a Retry-After needs a time\");\n return String(Math.ceil(retryAfter / 1000));\n};\n\n/** This function tells if the headers of a call of `writeHead` name a header, an object or a flat list of names and values. */\nconst names = (headers: unknown, name: string): boolean => {\n if (Array.isArray(headers)) return headers.some((each, at) => at % 2 === 0 && String(each).toLowerCase() === name);\n return typeof headers === \"object\" && headers !== null && Object.keys(headers).some((key) => key.toLowerCase() === name);\n};\n\n/** This function gives the headers of a call of `writeHead` without one header. */\nconst without = (headers: unknown, name: string): unknown => {\n if (Array.isArray(headers)) {\n const kept: unknown[] = [];\n for (let at = 0; at + 1 < headers.length; at += 2)\n if (String(headers[at]).toLowerCase() !== name) kept.push(headers[at], headers[at + 1]);\n return kept;\n }\n if (typeof headers !== \"object\" || headers === null) return headers;\n return Object.fromEntries(Object.entries(headers).filter(([key]) => key.toLowerCase() !== name));\n};\n\n/**\n * This function applies the rule of `Retry-After` to a call of `writeHead`, and gives its arguments.\n * A 503 without `Retry-After` gets the time of `server_retry_after` (RFC 9110). A 500 loses each\n * `Retry-After`, from the call and from the response.\n */\nconst retryRule = (res: ServerResponse, args: unknown[], retryAfter: string): unknown[] => {\n const status = typeof args[0] === \"number\" ? args[0] : res.statusCode;\n const at = typeof args[1] === \"string\" ? 2 : 1;\n if (status === 503 && !res.hasHeader(\"retry-after\") && !names(args[at], \"retry-after\")) res.setHeader(\"retry-after\", retryAfter);\n if (status !== 500) return args;\n res.removeHeader(\"retry-after\");\n if (!names(args[at], \"retry-after\")) return args;\n const given = [...args];\n given[at] = without(args[at], \"retry-after\");\n return given;\n};\n\nconst STATUS_TEXT: Readonly<Record<number, string>> = { 404: \"Not Found\", 500: \"Internal Server Error\", 503: \"Service Unavailable\" };\n\n/**\n * This function makes a server of `node:http` a transport of the lifecycle (S6 A8 and A13). It is not\n * a framework: it has no routes, no body parser and no middleware.\n * - The count of work in flight comes from events: each response until its `close`, and each upgraded\n * connection until its `close`.\n * - `stopIntake` closes the listener, and new connections get a refusal. It closes the idle connections\n * at once and at each `server_idle_sweep_interval`. Each later response gets `Connection: close`.\n * - `drain` waits until no work is in flight, or until the deadline. It gives the cooperative abort to\n * the handlers through their context.\n * - `close` destroys each connection. A second close gives the same promise.\n * - The mounts share one port. The first request handler that gives true answers. After the stop of\n * the intake, only the mounts until `close transports` answer, and each other request gets a 503.\n */\nexport const httpTransport = (server: Server, knobs: HttpKnobs, options: HttpTransportOptions = {}): HttpTransport => {\n const clock = options.clock ?? realClock;\n const idleClose = millisecondsOf(knobs, \"server_idle_close\");\n const sweepEvery = millisecondsOf(knobs, \"server_idle_sweep_interval\");\n if (sweepEvery === 0)\n throw new Error(\"the knob server_idle_sweep_interval is zero: a sweep needs an interval above zero\");\n const retryAfterHeader = retryAfterOf(knobs, \"httpTransport\");\n for (const derived of [\"server_idle_close_hint\", \"server_idle_close_buffer\"])\n if ((knobs as Readonly<Record<string, Extent | undefined>>)[derived] !== undefined)\n throw new Error(`the knob ${derived} comes from the transport: give only server_idle_close`);\n if (server.listenerCount(\"request\") > 0 || server.listenerCount(\"upgrade\") > 0)\n throw new Error(\"the server has a request or upgrade listener of its own: mount it as a handler\");\n // Node closes an idle connection at keepAliveTimeout + keepAliveTimeoutBuffer, and its hint is\n // keepAliveTimeout. The buffer stays, thus the hint is less than the real close. An older Node has no buffer\n const buffer = (server as { keepAliveTimeoutBuffer?: number }).keepAliveTimeoutBuffer ?? 0;\n if (Number.isFinite(idleClose) && idleClose <= buffer)\n throw new Error(\n `the knob server_idle_close is ${idleClose} ms, at or below the keepAliveTimeoutBuffer of the server, ${buffer} ms: ` +\n \"the server closes an idle connection at keepAliveTimeout + keepAliveTimeoutBuffer\",\n );\n // 0 stops the keep-alive timer and the hint in Node\n server.keepAliveTimeout = Number.isFinite(idleClose) ? idleClose - buffer : 0;\n const declared: HttpTransportKnobs = {\n ...knobs,\n server_idle_close_hint: Number.isFinite(idleClose)\n ? { value: Math.floor(server.keepAliveTimeout / 1000), unit: \"s\" }\n : { value: \"infinite\", unit: \"s\" },\n server_idle_close_buffer: { value: buffer, unit: \"ms\" },\n };\n\n const requestMounts: RequestMount[] = [];\n const upgradeMounts: UpgradeMount[] = [];\n const responses = new Set<ServerResponse>();\n const upgraded = new Set<UpgradeSocket>();\n const sockets = new Set<Socket>();\n const idleWaiters = new Set<() => void>();\n const closedWaiters = new Set<() => void>();\n const intake = new AbortController();\n const cooperative = new AbortController();\n // the lifecycle comes at the start, thus the context reads it at each request\n let life: Lifecycle | null = null;\n const context: MountContext = {\n intake: intake.signal,\n signal: cooperative.signal,\n get life() {\n return life;\n },\n retryAfter: knobs.server_retry_after,\n };\n let intakeOpen = true;\n let listenerOpen = server.listening;\n let serverClosed = !server.listening;\n let sweep: Cancel | null = null;\n let closing: Promise<void> | null = null;\n\n const inFlight = (): number => responses.size + upgraded.size;\n const report = (error: unknown, req: IncomingMessage): void => {\n try {\n options.onError?.(error, req);\n } catch {\n // an error of the error hook does not stop the transport\n }\n };\n const settled = (): void => {\n if (inFlight() === 0) for (const done of [...idleWaiters]) done();\n if (closing !== null && inFlight() === 0 && sockets.size === 0 && serverClosed)\n for (const done of [...closedWaiters]) done();\n };\n // before its start, a transport has no lifecycle: a server that listens already keeps its connections\n const keepsAlive = (): boolean => intakeOpen && (options.keepAlive?.() ?? life?.keepAlive() ?? true);\n const live = <T extends { readonly until: MountStage }>(mounts: readonly T[]): T[] =>\n intakeOpen ? [...mounts] : mounts.filter((mount) => mount.until === \"close transports\");\n const throughDrain = (): boolean =>\n requestMounts.some((mount) => mount.until === \"close transports\") ||\n upgradeMounts.some((mount) => mount.until === \"close transports\");\n const closeListener = (): void => {\n if (!listenerOpen) return;\n listenerOpen = false;\n server.close();\n };\n const startSweep = (): void => {\n if (!Number.isFinite(sweepEvery) || closing !== null) return;\n sweep = clock.after(sweepEvery, () => {\n sweep = null;\n if (closing !== null) return;\n server.closeIdleConnections();\n startSweep();\n });\n };\n\n /**\n * This function watches the headers of each response when they go, whatever wrote them. Each 503\n * gets `Retry-After`, and a 500 gets none. The response closes its connection while the transport\n * cannot keep it.\n */\n const watchHeaders = (res: ServerResponse): void => {\n const writeHead = res.writeHead as (...args: unknown[]) => ServerResponse;\n res.writeHead = ((...args: unknown[]): ServerResponse => {\n if (res.headersSent) return writeHead.apply(res, args);\n if (!keepsAlive()) {\n res.shouldKeepAlive = false;\n res.setHeader(\"connection\", \"close\");\n }\n return writeHead.apply(res, retryRule(res, args, retryAfterHeader));\n }) as ServerResponse[\"writeHead\"];\n };\n const answer = (res: ServerResponse, status: number, headers: Readonly<Record<string, string>> = {}): void => {\n if (res.headersSent) {\n res.destroy();\n return;\n }\n const body = STATUS_TEXT[status] ?? \"\";\n res.writeHead(status, { \"content-type\": \"text/plain; charset=utf-8\", \"content-length\": String(Buffer.byteLength(body)), ...headers });\n res.end(body);\n };\n const refuse = (res: ServerResponse): void => answer(res, 503, { \"retry-after\": retryAfterHeader, connection: \"close\" });\n\n const route = async (req: IncomingMessage, res: ServerResponse): Promise<void> => {\n const mounts = live(requestMounts);\n try {\n for (const mount of mounts) {\n const given = mount.handler(req, res, context);\n const taken = typeof given === \"boolean\" ? given : await given;\n // a handler that wrote its response took the request, whatever it gave\n if (taken || res.headersSent || res.writableEnded) return;\n }\n } catch (error) {\n report(error, req);\n if (!res.writableEnded) answer(res, 500);\n return;\n }\n if (intakeOpen) answer(res, 404);\n else refuse(res);\n };\n\n const onRequest = (req: IncomingMessage, res: ServerResponse): void => {\n responses.add(res);\n res.once(\"close\", () => {\n responses.delete(res);\n settled();\n });\n watchHeaders(res);\n void route(req, res);\n };\n\n const onUpgrade = (req: IncomingMessage, socket: UpgradeSocket, head: UpgradeArgs[2]): void => {\n socket.on(\"error\", () => {});\n const mount = live(upgradeMounts).find((each) => each.matches(req));\n if (mount === undefined) {\n const status = intakeOpen ? 404 : 503;\n const extra = intakeOpen ? \"\" : `Retry-After: ${retryAfterHeader}\\r\\n`;\n socket.end(`HTTP/1.1 ${status} ${STATUS_TEXT[status]!}\\r\\nConnection: close\\r\\nContent-Length: 0\\r\\n${extra}\\r\\n`);\n return;\n }\n if (socket.destroyed) return;\n upgraded.add(socket);\n socket.once(\"close\", () => {\n upgraded.delete(socket);\n settled();\n });\n try {\n mount.handler(req, socket, head, context);\n } catch (error) {\n report(error, req);\n socket.destroy();\n }\n };\n\n const onConnection = (socket: Socket): void => {\n sockets.add(socket);\n socket.once(\"close\", () => {\n sockets.delete(socket);\n settled();\n });\n };\n\n server.on(\"request\", onRequest);\n server.on(\"upgrade\", onUpgrade);\n server.on(\"connection\", onConnection);\n server.on(\"listening\", () => {\n listenerOpen = true;\n serverClosed = false;\n });\n server.on(\"close\", () => {\n listenerOpen = false;\n serverClosed = true;\n settled();\n });\n\n const remover = <T>(list: T[], entry: T) => (): void => {\n const at = list.indexOf(entry);\n if (at >= 0) list.splice(at, 1);\n // the last mount through the drain went: the listener closes as the stop of the intake asks\n if (!intakeOpen && closing === null && !throughDrain()) closeListener();\n };\n\n return {\n server,\n knobs: declared,\n get connections() {\n return sockets.size;\n },\n get intakeStopped() {\n return !intakeOpen;\n },\n mount(handler, mountOptions = {}) {\n const entry: RequestMount = { handler, until: mountOptions.until ?? handler.until ?? \"stop intake\" };\n requestMounts.push(entry);\n return remover(requestMounts, entry);\n },\n upgrade(match, handler, mountOptions = {}) {\n const matches = typeof match === \"string\" ? (req: IncomingMessage) => pathOf(req.url) === match : match;\n const entry: UpgradeMount = { matches, handler, until: mountOptions.until ?? handler.until ?? \"stop intake\" };\n upgradeMounts.push(entry);\n return remover(upgradeMounts, entry);\n },\n async start(_signal, given) {\n life = given;\n if (closing !== null) throw new Error(\"the transport closed: a closed transport does not start again\");\n // a listener that another owner adds after the transport answers too, thus the start refuses it\n if (server.listenerCount(\"request\") > 1 || server.listenerCount(\"upgrade\") > 1)\n throw new Error(\"the server has a request or upgrade listener of its own: mount it as a handler\");\n if (server.listening) return;\n const listen = options.listen;\n if (listen === undefined) throw new Error(\"the server does not listen, and the transport has no listen options\");\n if (typeof listen === \"function\") {\n await listen(server);\n return;\n }\n await new Promise<void>((resolve, reject) => {\n const failed = (error: Error): void => {\n server.off(\"listening\", listened);\n reject(error);\n };\n const listened = (): void => {\n server.off(\"error\", failed);\n resolve();\n };\n server.once(\"error\", failed);\n server.once(\"listening\", listened);\n server.listen(listen);\n });\n },\n stopIntake() {\n if (!intakeOpen) return;\n intakeOpen = false;\n intake.abort(new Error(\"the transport stopped its intake\"));\n if (!throughDrain()) closeListener();\n server.closeIdleConnections();\n startSweep();\n },\n drain(deadline, signal) {\n const abort = (): void => {\n if (!cooperative.signal.aborted) cooperative.abort(signal.reason);\n };\n if (signal.aborted) abort();\n else signal.addEventListener(\"abort\", abort, { once: true });\n // nothing in flight, or a deadline that passed: the drain ends at once, with no timer\n if (inFlight() === 0 || deadline <= clock.now()) {\n signal.removeEventListener(\"abort\", abort);\n return Promise.resolve();\n }\n return new Promise<void>((resolve) => {\n let cancel: Cancel = () => {};\n const done = (): void => {\n idleWaiters.delete(done);\n cancel();\n signal.removeEventListener(\"abort\", abort);\n resolve();\n };\n idleWaiters.add(done);\n cancel = clock.after(Math.max(0, deadline - clock.now()), done);\n });\n },\n inFlight,\n close() {\n if (closing !== null) return closing;\n intakeOpen = false;\n if (!intake.signal.aborted) intake.abort(new Error(\"the transport closed\"));\n if (!cooperative.signal.aborted) cooperative.abort(new Error(\"the transport closed\"));\n sweep?.();\n sweep = null;\n closing = new Promise<void>((resolve) => {\n closedWaiters.add(resolve);\n });\n closeListener();\n server.closeAllConnections();\n for (const socket of [...upgraded]) socket.destroy();\n for (const socket of [...sockets]) socket.destroy();\n settled();\n return closing;\n },\n };\n};\n","import type { IncomingMessage, Server, ServerResponse } from \"node:http\";\nimport { createServer } from \"node:http\";\nimport type { ListenOptions, Socket } from \"node:net\";\nimport type { Health, ProbeKind } from \"../health.ts\";\nimport type { HealthServer } from \"../lifecycle.ts\";\nimport type { Quantity } from \"@mark1russell7/quantity\";\nimport type { MountContext, RequestHandler } from \"./transport.ts\";\nimport { pathOf, retryAfterOf } from \"./transport.ts\";\n\n/** A probe response is the status, the headers and the body of one probe, for any HTTP framework or none. */\nexport interface ProbeResponse {\n readonly status: number;\n readonly headers: Readonly<Record<string, string>>;\n readonly body: string;\n}\n\n/** A health source is the health runtime, or a function that gives it when it exists. */\nexport type HealthSource = Health | (() => Health | null | undefined);\n\nconst KINDS: readonly ProbeKind[] = [\"startup\", \"readiness\", \"liveness\", \"report\"];\n\nconst prefixProblem = (prefix: string): string | null =>\n prefix === \"\" || (prefix.startsWith(\"/\") && !prefix.endsWith(\"/\"))\n ? null\n : `the probe prefix \"${prefix}\" does not start with \"/\", or it ends with \"/\"`;\n\nconst json = (status: number, body: unknown): ProbeResponse => ({\n status,\n headers: { \"content-type\": \"application/health+json\", \"cache-control\": \"no-store\" },\n body: JSON.stringify(body),\n});\n\n/**\n * This function gives the response of a probe path from the cache of the health runtime. For a path\n * that is not a probe, it gives null. The paths are `/startup`, `/readiness`, `/liveness` and\n * `/report` after the prefix. `/readiness/<controller>` gives the readiness of one controller.\n */\nexport const probeResponse = (\n health: Health | null | undefined,\n method: string | undefined,\n path: string,\n prefix = \"\",\n): ProbeResponse | null => {\n if (!path.startsWith(`${prefix}/`)) return null;\n const parts = path.slice(prefix.length + 1).split(\"/\");\n const kind = KINDS.find((each) => each === parts[0]);\n if (kind === undefined) return null;\n const controller = parts[1];\n if (parts.length > 2 || (controller !== undefined && (kind !== \"readiness\" || controller === \"\"))) return null;\n if (method !== \"GET\" && method !== \"HEAD\")\n return { status: 405, headers: { allow: \"GET, HEAD\", \"content-length\": \"0\" }, body: \"\" };\n // before the runtime exists, each probe fails: the service has not started\n if (health === null || health === undefined) return json(503, { status: \"fail\", probe: kind });\n try {\n const answer = health.probe(kind, controller === undefined ? undefined : decodeURIComponent(controller));\n return { ...json(answer.status, answer.body), headers: { \"content-type\": answer.contentType, \"cache-control\": \"no-store\" } };\n } catch {\n // a controller that the service does not have\n return json(404, { status: \"fail\", probe: kind, controller });\n }\n};\n\n/** This function writes a probe response. A 503 gets `Retry-After`, when the writer has the time of a retry. */\nconst write = (req: IncomingMessage, res: ServerResponse, response: ProbeResponse, retryAfter: string | undefined): void => {\n const retry = response.status === 503 && retryAfter !== undefined ? { \"retry-after\": retryAfter } : {};\n res.writeHead(response.status, { ...response.headers, ...retry, \"content-length\": String(Buffer.byteLength(response.body)) });\n res.end(req.method === \"HEAD\" ? undefined : response.body);\n};\n\n/** These options mount the probes on the port of the application. */\nexport interface ProbeHandlerOptions {\n /** This path goes before each probe path, for example `/health`. It starts with `/`, or it is empty. */\n readonly prefix: string;\n}\n\n/**\n * This function gives a request handler that answers the probes on the port of the application. It\n * answers until `close transports`, thus its mount keeps the listener open through the drain. A path\n * that is not a probe goes to the next handler. Each 503 gets the `Retry-After` of the context.\n */\nexport const probeHandler = (health: HealthSource, options: ProbeHandlerOptions): RequestHandler => {\n const problem = prefixProblem(options.prefix);\n if (problem !== null) throw new Error(problem);\n const handler = (req: IncomingMessage, res: ServerResponse, context?: MountContext): boolean => {\n const runtime = typeof health === \"function\" ? health() : health;\n const response = probeResponse(runtime, req.method, pathOf(req.url), options.prefix);\n if (response === null) return false;\n // the transport gives server_retry_after through the context, and it adds the header to each 503 too\n const retryAfter = context?.retryAfter === undefined ? undefined : retryAfterOf({ server_retry_after: context.retryAfter }, \"probeHandler\");\n write(req, res, response, retryAfter);\n return true;\n };\n return Object.assign(handler, { until: \"close transports\" as const });\n};\n\n/** These options make a probe server: the listen options of its port, the prefix of its paths, and the time of a retry. */\nexport interface ProbeServerOptions extends ListenOptions {\n /**\n * This time goes in the `Retry-After` of each 503 of a probe, as on the port of the application.\n * The probe server has no defaults, thus the knob is necessary.\n */\n readonly server_retry_after: Quantity;\n /** This path goes before each probe path. The default is no prefix, because the port has only the probes. */\n readonly prefix?: string | undefined;\n /** This server answers the probes. Without it, the probe server makes a server of `node:http`. */\n readonly server?: Server | undefined;\n}\n\n/** A probe server is a health server on its own port. Its server is visible for the address of the port. */\nexport interface ProbeServer extends HealthServer {\n readonly server: Server;\n}\n\n/**\n * This function makes a health server on a port of its own (S6 decision D5). It starts first and\n * closes last, thus the probes answer through the whole drain. Each probe answers from the cache, and\n * each 503 gets the `Retry-After` of `server_retry_after`.\n */\nexport const probeServer = (options: ProbeServerOptions): ProbeServer => {\n const { prefix = \"\", server = createServer(), server_retry_after, ...listen } = options;\n const retryAfter = retryAfterOf({ server_retry_after }, \"probeServer\");\n const problem = prefixProblem(prefix);\n if (problem !== null) throw new Error(problem);\n if (server.listenerCount(\"request\") > 0) throw new Error(\"the probe server has a request listener of its own\");\n let health: Health | null = null;\n let closing: Promise<void> | null = null;\n const sockets = new Set<Socket>();\n server.on(\"connection\", (socket: Socket) => {\n sockets.add(socket);\n socket.once(\"close\", () => sockets.delete(socket));\n });\n server.on(\"request\", (req: IncomingMessage, res: ServerResponse) => {\n const response = probeResponse(health, req.method, pathOf(req.url), prefix);\n write(req, res, response ?? { status: 404, headers: { \"content-type\": \"text/plain; charset=utf-8\" }, body: \"Not Found\" }, retryAfter);\n });\n return {\n server,\n listen(runtime) {\n health = runtime;\n if (server.listening) return Promise.resolve();\n return new Promise<void>((resolve, reject) => {\n const failed = (error: Error): void => {\n server.off(\"listening\", listened);\n reject(error);\n };\n const listened = (): void => {\n server.off(\"error\", failed);\n resolve();\n };\n server.once(\"error\", failed);\n server.once(\"listening\", listened);\n server.listen(listen);\n });\n },\n close() {\n if (closing !== null) return closing;\n closing = new Promise<void>((resolve) => {\n if (!server.listening) {\n resolve();\n return;\n }\n server.close(() => resolve());\n server.closeAllConnections();\n for (const socket of [...sockets]) socket.destroy();\n });\n return closing;\n },\n };\n};\n","import type { Quantity } from \"@mark1russell7/quantity\";\nimport { valueIn } from \"@mark1russell7/quantity\";\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport type { Refused } from \"../limiter/policy.ts\";\nimport { retryAfterSeconds } from \"../limiter/policy.ts\";\nimport type { RequestEnd, RequestEvent, RequestOptions, RequestStream, RequestWork } from \"../requests.ts\";\nimport type { MountContext, RequestHandler } from \"./transport.ts\";\nimport { pathOf } from \"./transport.ts\";\n\n/** An outcome response is the status, the headers and the body of a request, for any HTTP framework or none. */\nexport interface OutcomeResponse {\n readonly status: number;\n readonly headers: Readonly<Record<string, string>>;\n readonly body: string | Uint8Array;\n}\n\n/** A response part is what a hook gives for one value or one failure. The parts that it does not give keep the values of the mapping. */\nexport interface ResponsePart {\n readonly status?: number | undefined;\n readonly headers?: Readonly<Record<string, string>> | undefined;\n readonly body?: string | Uint8Array | undefined;\n}\n\n/**\n * A stream format writes the items of a stream and its end in the body, as they come. An end after\n * the first item goes in the body, because the status went with the first item.\n */\nexport interface StreamFormat<T = unknown> {\n readonly contentType: string;\n /** This function gives the bytes of one item. */\n item(value: T): string | Uint8Array;\n /** This function gives the bytes of the end, the last part of the body. */\n end(end: RequestEnd): string | Uint8Array;\n}\n\n/** These options map a request to a response. */\nexport interface OutcomeOptions<T> {\n /**\n * This value tells if the response can keep its connection open, as `life.keepAlive()` does. False\n * gives `Connection: close`. A function is read when the headers go.\n */\n readonly keepAlive: boolean | (() => boolean);\n /** This time goes in the `Retry-After` of each 503, in whole seconds, rounded up. It is the knob `server_retry_after`. */\n readonly retryAfter: Quantity;\n /**\n * This function gives the response of the value of a unary request. Without it, the mapping gives\n * 200 and the value as JSON, or 204 for no value.\n */\n readonly done?: ((value: T) => ResponsePart) | undefined;\n /**\n * This function gives the response of a failure of the route before the first byte. Without it,\n * the mapping gives 500 with no detail, because an error can hold private data.\n */\n readonly failed?: ((error: unknown) => ResponsePart) | undefined;\n /**\n * This format makes the response a stream: the status and each item go as they come, and the end\n * goes in the body. Without it, the response is unary: one status and the one value.\n */\n readonly stream?: StreamFormat<T> | undefined;\n}\n\nconst JSON_TYPE = \"application/json; charset=utf-8\";\n\n/**\n * This function gives an end as a record for a body: its kind and the count of its items. A cut and\n * a refusal also give their cause, and if the caller can send the request again. A failure gives\n * no detail.\n */\nexport const endRecord = (end: RequestEnd): Readonly<Record<string, unknown>> => {\n if (end.kind === \"refused\" || end.kind === \"cut\")\n return { end: end.kind, delivered: end.delivered, retryable: end.retryable, cause: end.cause };\n return { end: end.kind, delivered: end.delivered };\n};\n\n/** This format writes each item and the end as one line of JSON (NDJSON). */\nexport const ndjson: StreamFormat = {\n contentType: \"application/x-ndjson; charset=utf-8\",\n item: (value) => `${JSON.stringify(value)}\\n`,\n end: (end) => `${JSON.stringify(endRecord(end))}\\n`,\n};\n\n/** This format writes each item as an event of server-sent events, and the end as the event `end`. */\nexport const sse: StreamFormat = {\n contentType: \"text/event-stream; charset=utf-8\",\n item: (value) => `data: ${JSON.stringify(value)}\\n\\n`,\n end: (end) => `event: end\\ndata: ${JSON.stringify(endRecord(end))}\\n\\n`,\n};\n\nconst valueResponse = (value: unknown): OutcomeResponse => {\n if (value === undefined) return { status: 204, headers: {}, body: \"\" };\n if (typeof value === \"string\") return { status: 200, headers: { \"content-type\": \"text/plain; charset=utf-8\" }, body: value };\n if (value instanceof Uint8Array) return { status: 200, headers: { \"content-type\": \"application/octet-stream\" }, body: value };\n return { status: 200, headers: { \"content-type\": JSON_TYPE }, body: JSON.stringify(value) };\n};\n\nconst merge = (base: OutcomeResponse, part: ResponsePart | undefined): OutcomeResponse =>\n part === undefined\n ? base\n : { status: part.status ?? base.status, headers: { ...base.headers, ...part.headers }, body: part.body ?? base.body };\n\nconst keeps = (keepAlive: boolean | (() => boolean)): boolean => (typeof keepAlive === \"function\" ? keepAlive() : keepAlive);\n\n/**\n * This function applies the rules of the headers to a response. Each 503 gets `Retry-After`, and a\n * 500 gets none. The response closes its connection while the service cannot keep it.\n */\nconst ruled = (response: OutcomeResponse, retryAfter: Quantity, keepAlive: boolean | (() => boolean)): OutcomeResponse => {\n const headers: Record<string, string> = { ...response.headers };\n if (response.status === 503 && headers[\"retry-after\"] === undefined)\n headers[\"retry-after\"] = String(Math.max(0, Math.ceil(valueIn(retryAfter, \"s\"))));\n if (response.status === 500) delete headers[\"retry-after\"];\n if (!keeps(keepAlive)) headers[\"connection\"] = \"close\";\n return { ...response, headers };\n};\n\n/** This function gives the status response of an end before the first byte of the body. */\nconst endStatus = <T>(end: RequestEnd, options: OutcomeOptions<T>): OutcomeResponse => {\n const body = JSON.stringify(endRecord(end));\n switch (end.kind) {\n case \"refused\":\n return { status: 503, headers: { \"content-type\": JSON_TYPE }, body };\n case \"cut\":\n // a cut after the first item has no status of its own, but no byte went: its route decides\n return { status: end.status ?? (end.retryable ? 503 : 500), headers: { \"content-type\": JSON_TYPE }, body };\n case \"failed\":\n return merge({ status: 500, headers: { \"content-type\": JSON_TYPE }, body }, options.failed?.(end.error));\n case \"done\":\n return { status: 204, headers: {}, body: \"\" };\n }\n};\n\n/**\n * This function maps a unary request to its response, from its items and its end (S6 A12 and A13).\n * - One value and the end `done`: 200 with the value, or the response of the `done` hook.\n * - No value and the end `done`: 204. More than one value: 500, because the route is not a stream.\n * - `refused`: 503. The work did not start, thus the caller can send it again. The service did not\n * start, it stopped its intake, or a controller is not ready.\n * - `cut`: 503 for a route whose callers can send it again (S6 A12), and 500 for each other route.\n * - `failed`: 500 with no detail, or the response of the `failed` hook. A failure of the route is not a 503.\n *\n * Each 503 gets `Retry-After` (RFC 9110), and a 500 gets none. Each response gets `Connection: close`\n * while the service cannot keep the connection.\n */\nexport const outcomeResponse = <T>(items: readonly T[], end: RequestEnd, options: OutcomeOptions<T>): OutcomeResponse => {\n let response: OutcomeResponse;\n if (end.kind !== \"done\") response = endStatus(end, options);\n else if (items.length === 1) response = merge(valueResponse(items[0]), options.done?.(items[0]!));\n else if (items.length === 0) response = endStatus(end, options);\n else\n response = endStatus(\n { kind: \"failed\", error: new Error(`the work gave ${items.length} values, and a unary response has one`), delivered: items.length },\n options,\n );\n return ruled(response, options.retryAfter, options.keepAlive);\n};\n\n/**\n * This function gives the response of a take that a keyed limiter refused: 429 (RFC 6585), with\n * `Retry-After` in whole seconds, rounded up (RFC 9110). The body tells the cause (`limit`, `cap` or\n * `store`) and the same whole seconds, but not the failure of a store. The work did not start, thus\n * the caller can send it again.\n */\nexport const limitedResponse = (refused: Refused): OutcomeResponse => {\n const seconds = retryAfterSeconds(refused.retryAfter);\n return {\n status: 429,\n headers: { \"content-type\": JSON_TYPE, \"retry-after\": String(seconds) },\n body: JSON.stringify({ end: \"refused\", delivered: 0, retryable: true, cause: `the limiter refused the take: ${refused.cause}`, retryAfter: seconds }),\n };\n};\n\n/** This function writes a whole response of `node:http`. A response whose headers went gets no second status: it is destroyed. */\nconst writeWhole = (res: ServerResponse, response: OutcomeResponse): void => {\n if (res.headersSent) {\n res.destroy();\n return;\n }\n if (response.headers[\"connection\"] === \"close\") res.shouldKeepAlive = false;\n // a 204 has no body, thus no length\n if (response.status === 204) {\n res.writeHead(204, response.headers);\n res.end();\n return;\n }\n const length = typeof response.body === \"string\" ? Buffer.byteLength(response.body) : response.body.byteLength;\n res.writeHead(response.status, { ...response.headers, \"content-length\": String(length) });\n res.end(response.body);\n};\n\n/**\n * This function writes the stream of a request to a response of `node:http`, and gives its end. It\n * gives null when the client closed the connection first. Then the work stops through `return()`.\n * - Unary (no format): it waits for the end, and it writes the status and the body of `outcomeResponse`.\n * - Stream: the status 200 goes with the first item, then each item goes as it comes, then the end\n * goes in the body (in-band). The next item waits until the client reads, thus a slow client\n * slows the work. An end before the first item gets a status, as a unary end does.\n */\nexport const writeOutcome = async <T>(res: ServerResponse, stream: RequestStream<T>, options: OutcomeOptions<T>): Promise<RequestEnd | null> => {\n const iterator = stream[Symbol.asyncIterator]();\n let gone: () => void = () => {};\n const closed = new Promise<\"gone\">((resolve) => {\n gone = () => resolve(\"gone\");\n });\n // a close before the end of the response is a client that went away\n const onClose = (): void => {\n if (!res.writableFinished) gone();\n };\n res.once(\"close\", onClose);\n if (res.destroyed) gone();\n const leave = async (): Promise<null> => {\n await iterator.return?.();\n return null;\n };\n const next = async (): Promise<RequestEvent<T> | \"gone\"> => {\n const step = await Promise.race([iterator.next(), closed]);\n if (step === \"gone\") return step;\n if (step.done === true) throw new Error(\"the stream of the request ended without its end\");\n return step.value;\n };\n /** This function writes one part of the body, and waits while the client reads slower than the work. */\n const send = async (chunk: string | Uint8Array): Promise<boolean> => {\n if (res.write(chunk)) return true;\n const drained = new Promise<\"drained\">((resolve) => res.once(\"drain\", () => resolve(\"drained\")));\n return (await Promise.race([drained, closed])) === \"drained\";\n };\n try {\n const format = options.stream;\n if (format === undefined) {\n const items: T[] = [];\n for (;;) {\n const event = await next();\n if (event === \"gone\") return await leave();\n if (event.kind === \"item\") {\n items.push(event.value);\n continue;\n }\n writeWhole(res, outcomeResponse(items, event, options));\n return event;\n }\n }\n const first = await next();\n if (first === \"gone\") return await leave();\n if (first.kind !== \"item\") {\n // an empty stream that ended well gives 200 and its end. Each other end before the first item gets a status\n if (first.kind === \"done\")\n writeWhole(res, ruled({ status: 200, headers: { \"content-type\": format.contentType, \"cache-control\": \"no-store\" }, body: format.end(first) }, options.retryAfter, options.keepAlive));\n else writeWhole(res, outcomeResponse([], first, options));\n return first;\n }\n if (res.headersSent) {\n res.destroy();\n return await leave();\n }\n const head = ruled({ status: 200, headers: { \"content-type\": format.contentType, \"cache-control\": \"no-store\" }, body: \"\" }, options.retryAfter, options.keepAlive);\n if (head.headers[\"connection\"] === \"close\") res.shouldKeepAlive = false;\n res.writeHead(200, head.headers);\n if (!(await send(format.item(first.value)))) return await leave();\n for (;;) {\n const event = await next();\n if (event === \"gone\") return await leave();\n if (event.kind === \"item\") {\n if (!(await send(format.item(event.value)))) return await leave();\n continue;\n }\n // the end after the first item is in-band: the status went with the first item\n res.end(format.end(event));\n return event;\n }\n } finally {\n res.off(\"close\", onClose);\n }\n};\n\n/** A route of a handler is the name of the route that a controller declares, or a function that gives it from the request. */\nexport type HandledRoute = string | ((req: IncomingMessage) => string | null);\n\n/** A handled work does the work of one request of `node:http`. Its signal aborts when the shutdown or the client stops the request. */\nexport type HandledWork<T> = (req: IncomingMessage, signal: AbortSignal) => RequestWork<T>;\n\n/** These options bind a request of `node:http` to the request tracking of the service. */\nexport interface HandleOptions<T> extends Omit<OutcomeOptions<T>, \"keepAlive\" | \"retryAfter\"> {\n /** This function gives the detail of a request, for the function of a route pattern. */\n readonly detail?: ((req: IncomingMessage) => unknown) | undefined;\n}\n\n/**\n * This function gives a request handler that does each request of a route through `life.request`\n * (S6 A12 and A13). The transport gives the lifecycle and `server_retry_after` through the context.\n * - A route name takes a request whose method and path are that name, for example `POST /orders`.\n * A function gives the name of a route or of a pattern, or null for a request of another handler.\n * - Before the start of the transport, each request gets a retryable 503.\n * - The response is unary, or a stream with the option `stream`, as `writeOutcome` writes it.\n */\nexport const handle = <T>(controller: string, route: HandledRoute, work: HandledWork<T>, options: HandleOptions<T> = {}): RequestHandler => {\n const nameOf = (req: IncomingMessage): string | null => {\n if (typeof route === \"function\") return route(req);\n return `${req.method ?? \"GET\"} ${pathOf(req.url)}` === route ? route : null;\n };\n return async (req: IncomingMessage, res: ServerResponse, context: MountContext): Promise<boolean> => {\n const name = nameOf(req);\n if (name === null) return false;\n const life = context.life;\n const written: OutcomeOptions<T> = {\n ...options,\n keepAlive: () => life?.keepAlive() ?? false,\n retryAfter: context.retryAfter,\n };\n if (life === null) {\n const refused: RequestEnd = { kind: \"refused\", status: 503, retryable: true, cause: \"the transport has not started\", delivered: 0 };\n writeWhole(res, outcomeResponse<T>([], refused, written));\n return true;\n }\n const given: RequestOptions = options.detail === undefined ? {} : { detail: options.detail(req) };\n await writeOutcome(res, life.request(controller, name, (signal) => work(req, signal), given), written);\n return true;\n };\n};\n"],"mappings":"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"}
package/build/index.d.ts CHANGED
@@ -1,8 +1,10 @@
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-D-38uH_z.js";
3
3
  import { Heartbeat, HeartbeatOptions, detectionTime, heartbeat } from "./heartbeat.js";
4
- import { $ as failSpan, A as LifecycleEvent, B as Gate, C as ProbeAnswer, 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 mayReissue, 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 matchesMessage, a as PhaseContext, at as EXIT_SIGNALS, b as CheckVerdict, bt as Idempotency, c as STAGES, ct as HostStream, d as TransportImpl, dt as ProcessHost, et as heartbeatDetection, f as cooperativeAbortAt, ft as ProcessPort, g as RequestFn, gt as isMessageShape, h as RequestEvent, ht as isExitSignal, i as LifecycleState, it as RouteRequest, j as MachineName, l as ShutdownOptions, lt as MessageShape, m as RequestEnd, mt as isExitCode, n as HealthServer, nt as OperationOf, o as PhaseWork, ot as ExitSignal, p as stageRank, pt as ProcessTrigger, q as FailsBy, r as Lifecycle, rt as RoutePattern, s as RunOptions, st as ExitTrigger, t as DependencyHandle, tt as recoverSpan, u as Stage, ut as PROCESS_TRIGGERS, v as RequestStream, vt as nodeProcess, w as ProbeKind, x as Health, xt as Operation, y as RequestWork, yt as processPortOf, z as FORMAT } from "./lifecycle-DtJZW09J.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-QVxu1WcL.js";
4
+ 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-Chu9Pri2.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-DyDB7zM9.js";
7
+ import { a as keyedLimiter, i as TakeRequest, n as LimiterStore, o as memoryStore, r as MemoryStore, t as KeyedLimiter } from "./limiter-DzTyosou.js";
6
8
  import { Extent, Quantity } from "@mark1russell7/quantity";
7
9
  import { replay } from "@mark1russell7/algebra/replay";
8
10
  import { Machine } from "@mark1russell7/algebra";
@@ -85,9 +87,10 @@ export declare const verdictOf: (rule: Rule, bind: Binding, evaluated: readonly
85
87
  * service, from its primitives: the polls of its checks, the windows of its policies, its drain.
86
88
  * The scenario binds the symbols of the platform (preStop, grace, the probes of the kubelet,
87
89
  * propagation) and of each dependency (its failover time, the idle close of its server). Thus the
88
- * service gives the inequalities, and the scenario gives the values. A rule about a client
89
- * applies one time for each dependency. A rule about a consumer applies one time for each consumer
90
- * and each worker.
90
+ * service gives the inequalities, and the scenario gives the values.
91
+ *
92
+ * A rule about a client applies one time for each dependency. A rule about a consumer applies one
93
+ * time for each consumer and each worker. A rule about a limiter applies one time for each limiter.
91
94
  */
92
95
  /** A scenario gives the values of the platform and of each dependency, by the `symbol@hop` of the law. */
93
96
  interface Scenario {
@@ -106,6 +109,11 @@ interface Scenario {
106
109
  readonly dependencies?: Readonly<Record<string, Readonly<Record<string, Extent>>>> | undefined;
107
110
  /** These are the values of the source of each consumer and worker, by its name and by the `symbol@hop` of the law: lease_duration@broker. */
108
111
  readonly transports?: Readonly<Record<string, Readonly<Record<string, Extent>>>> | undefined;
112
+ /**
113
+ * These are the values of the callers of each limiter, by its name and by the `symbol@hop` of the
114
+ * law: client_retry_window@caller, retry_delay@caller. A symbol without a hop binds at `caller`.
115
+ */
116
+ readonly limiters?: Readonly<Record<string, Readonly<Record<string, Extent>>>> | undefined;
109
117
  }
110
118
  /** A finding is the verdict of one rule for one scope: the service, or the client of a dependency. */
111
119
  interface Finding {
@@ -120,6 +128,20 @@ interface Finding {
120
128
  }
121
129
  /** These are the identifiers of the rules that each declaration gets. */
122
130
  export declare const SERVICE_RULES: readonly string[];
131
+ /**
132
+ * These are the two rules of the law for a keyed limiter, as data. The caller of `lint()` gives
133
+ * them, as it gives each other rule. Each one applies one time for each limiter.
134
+ * - O29: the longest `Retry-After` of a limiter is at most the retry window of its caller. Its
135
+ * second inequality applies the same bound to a new key at the cap of keys.
136
+ * Otherwise the caller stops before the limit lets its call in, and a refusal becomes a failure.
137
+ * - O30: the least delay of a caller before it sends a refused call again is at least the longest
138
+ * `Retry-After` of the policy. Otherwise a caller that does not read the header sends again too
139
+ * early, and the limiter refuses each early call again.
140
+ *
141
+ * The identifiers O29 and O30 are provisional. The corpus of the law in jarvis (R3, track B) has 28
142
+ * rules, O01 to O28. It does not have these two rules yet, thus they take the next two identifiers.
143
+ */
144
+ export declare const LIMITER_RULES: readonly Rule[];
123
145
  /** This function gives what the declaration tells about the service, as symbols of the law, and the source of each. */
124
146
  export declare const serviceBindings: (svc: ServiceDescription, scenario: Scenario) => {
125
147
  readonly bind: Record<string, Bound>;
@@ -129,8 +151,8 @@ export declare const serviceBindings: (svc: ServiceDescription, scenario: Scenar
129
151
  export declare const DECLARATION_RULE = "declaration";
130
152
  /**
131
153
  * This function applies each service rule to a declaration and a scenario. A rule applies one time
132
- * for the service, for each client, or for each consumer and worker. The function also gives a
133
- * finding for each necessary field that the description does not give (`DECLARATION_RULE`).
154
+ * for the service, for each client, for each consumer and worker, or for each limiter. The function
155
+ * also gives a finding for each necessary field that the description does not give (`DECLARATION_RULE`).
134
156
  */
135
157
  export declare const lint: (svc: ServiceDescription, scenario: Scenario, rules: readonly Rule[]) => Finding[];
136
158
  //#endregion
@@ -408,5 +430,5 @@ export declare const drainMachine: (description: ServiceDescription) => Machine;
408
430
  /** These are the static machines of the runtime, by the name that their events carry. */
409
431
  export declare const MACHINES: Readonly<Record<Exclude<MachineName, "drain" | "client">, Machine>>;
410
432
  //#endregion
411
- export { 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 DelayBand, type Dependency, type DependencyHandle, EXIT_SIGNALS, 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 Lifecycle, type LifecycleEvent, type LifecycleState, type MachineName, 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 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, STAGES, type Scenario, type ServiceBuilder, type ServiceDescription, type ShutdownOptions, type Side, type Span, type Stage, type StartOptions, type Step, type Term, type Timing, 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, detectionTime, ephemeral, failSpan, heartbeat, heartbeatDetection, isExitCode, isExitSignal, isMessageShape, kubernetes, localDev, matchesMessage, mayReissue, nodeProcess, platformClock, processPortOf, realClock, recoverSpan, replay, service, stageRank, watchdog };
433
+ 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 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, STAGES, type Scenario, type ServiceBuilder, type ServiceDescription, type ShutdownOptions, type Side, type SlidingWindow, type Span, type Stage, type StartOptions, type Step, type StoreFailure, 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, limiterProblems, localDev, longestRetryAfter, matchesMessage, mayReissue, memoryStore, nodeProcess, platformClock, processPortOf, realClock, recoverSpan, replay, retryAfterSeconds, service, stageRank, watchdog };
412
434
  //# sourceMappingURL=index.d.ts.map