@mark1russell7/service 0.5.0 → 0.5.2
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 +95 -4
- package/build/{builder-B_q-JeW8.d.ts → builder-BzjhWk2E.d.ts} +9 -2
- package/build/http.d.ts +1 -1
- package/build/http.js +3 -10
- package/build/http.js.map +1 -1
- package/build/index.d.ts +244 -7
- package/build/index.js +806 -25
- package/build/index.js.map +1 -1
- package/build/knobs-BxG-c7Zb.js +39 -0
- package/build/knobs-BxG-c7Zb.js.map +1 -0
- package/build/{lifecycle-DIj4YGmO.d.ts → lifecycle-DjEpgpRk.d.ts} +57 -8
- package/build/redis.d.ts +2 -2
- package/build/testing.d.ts +8 -2
- package/build/testing.js +23 -4
- package/build/testing.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -14,6 +14,7 @@ This release has the declaration, its export, the law, the health runtime, the l
|
|
|
14
14
|
- **Keyed operations.** A keyed operation has the window that the server keeps its key for. `mayReissue` answers from the declaration of an operation. An operation without a declaration does not go again.
|
|
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
|
+
- **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.
|
|
17
18
|
- **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.
|
|
18
19
|
|
|
19
20
|
The format of a description is `mark1russell7.service/1`.
|
|
@@ -36,13 +37,16 @@ The format of a description is `mark1russell7.service/1`.
|
|
|
36
37
|
|
|
37
38
|
`await svc.run()` starts the whole life of the service, and gives a `Lifecycle` when the startup ends. It refuses a declaration that it cannot start: a transport without its implementation, or a stage with work and no declared phase.
|
|
38
39
|
|
|
39
|
-
- **The parts.** `svc.server(name, knobs, impl)`, `svc.consumer(…)` and `svc.worker(…)` give a transport its implementation. Each transport has one contract: `start(signal, life)`, `stopIntake`, `drain(deadline, signal)`, `inFlight` and `close`. `svc.dependency(name, spec, handle)` gives a dependency a handle that connects and closes. `svc.healthServer(server)` gives the server of the probes.
|
|
40
|
+
- **The parts.** `svc.server(name, knobs, impl)`, `svc.consumer(…)` and `svc.worker(…)` give a transport its implementation. Each transport has one contract: `start(signal, life)`, `stopIntake`, `drain(deadline, signal)`, `inFlight` and `close`. `workerLoop` and `queueDrain` give the implementation of a worker and of a consumer ([Workers and queues](#workers-and-queues)). `svc.dependency(name, spec, handle)` gives a dependency a handle that connects and closes. `svc.healthServer(server)` gives the server of the probes.
|
|
40
41
|
- **The startup.** The health server starts first, thus the probes answer while the dependencies connect. Then each dependency connects, in its declared sequence. Then each transport starts, and the startup gate opens last, after one warm-up sweep of each check. A part that does not start fails the startup. Then only the parts that started stop, and the exit code is 1.
|
|
41
42
|
- **The startup budget.** When the gate is not open at the end of its budget, the gate says so. The runtime does not stop itself, because the startup probe of the platform acts on it.
|
|
42
43
|
- **The signals.** The runtime owns the handlers of SIGINT, SIGTERM and SIGQUIT. The first signal starts the shutdown. A signal during the startup gets its shutdown after the startup ends.
|
|
43
44
|
- **A repeat signal.** Each declaration tells what a signal during the shutdown does (`svc.onRepeatSignal`). With `"exit"`, the signal ends the process at once, with the code 1. With `"ignore"`, the signal has no effect, as in 0.4. A second `shutdown()` and a second process trigger do not end the process.
|
|
44
45
|
- **A declaration without the policy.** `describe()` refuses a declaration that does not declare `onRepeatSignal`. A description from another source can lack it, for example the JSON of 0.4. Then `lint()` gives a `depends` finding of the rule `declaration`, and `unbound` names the field.
|
|
45
46
|
- **Process triggers.** `svc.trigger("stdin-close")` starts the shutdown when the standard input ends, as when the parent of a stdio child stops. `"ipc-close"` starts it when the IPC channel to the parent closes. Each trigger is opt-in. The process must read its input, as a stdio transport does, or Node does not see the end.
|
|
47
|
+
- **A message of the IPC channel.** Windows cannot give a child a SIGTERM that the child can catch, thus a parent sends a message. `svc.trigger("ipc-message", { match: "shutdown", as: "SIGTERM" })` starts the shutdown at that message.
|
|
48
|
+
- **The shape of a message.** `match` is a value of JSON. A string, a number, a boolean or null matches an equal message. An object matches a message that has each of its fields, and an array matches item by item.
|
|
49
|
+
- **A message as a signal.** With `as`, the message is that exit signal: it gets its exit code and the policy of a repeat signal. Without `as`, the message is the trigger `ipc-message`, with its own exit code (`svc.exitCode("ipc-message", code)`).
|
|
46
50
|
- **The shutdown.** Readiness fails first. Then the declared phases occur in their sequence, and each phase races its cap. `svc.onPhase(name, work)` adds the work of an owner to a phase.
|
|
47
51
|
- **The stages.** A declared phase with the name of a stage gets the work of that stage. The stages are `fail readiness`, `stop intake`, `drain`, `close transports`, `stop the poller`, `tear down dependencies`, `close the health server` and `flush the output`. The phase `fail readiness` holds for its cap while the endpoints update. The signal of the service aborts after the poller stops. `describe()` refuses the stages out of their sequence.
|
|
48
52
|
- **The cooperative abort.** During the drain, the requests and the transports get a signal that aborts at the cap less the cooperative window. With the floor `half`, that time is half the cap at the least, and a window without a bound gives half the cap. With `svc.cooperativeFloor("none")`, a window of the whole cap aborts the work at the start of the drain.
|
|
@@ -59,6 +63,8 @@ The format of a description is `mark1russell7.service/1`.
|
|
|
59
63
|
- **The exit.** Only the lifecycle ends the process, through its `ProcessPort`. The exit code is 0 after a clean shutdown. It is 1 after an error: a startup that failed, a phase whose work did not end in its cap, or a work that threw. A phase over its cap does not stop the sequence.
|
|
60
64
|
- **Exit codes.** `life.shutdown(cause, { code })` gives the exit code of the work, a whole number from 0 to 255. The first request with a code sets it. `svc.exitCode(trigger, code)` gives the code of a shutdown that a trigger starts, for example 130 for SIGINT. A code that is not 0 stays after an error. A code of 0, or no code, becomes 1 after an error.
|
|
61
65
|
- **The output.** The stage `flush the output` comes last. The owners of the phase write their last output first, for example a log or a trace exporter. Then the `ProcessPort` writes the buffered output of stdout and stderr, in the cap of the stage. Without this phase, the process exits without a wait, as in 0.4.
|
|
66
|
+
- **The hold.** The lifecycle holds its process from its start to its exit, through `ProcessPort.hold`, as a listening socket does. The hold is one timer of the event loop, and the exit releases it. The other timers of the runtime still do not keep the process alive (A14).
|
|
67
|
+
- **Why the hold lasts to the exit.** A drain can wait on a timer that does not keep the process alive. Without the hold, Node then ends the process with the code 0 in the drain. The teardown and the declared exit code are then lost. Thus the hold does not end at the stop of the intake, and it is not a knob.
|
|
62
68
|
- **The watchdog.** `svc.watchdog(spec)` declares a check on the event loop. A blocked loop delays each timer, thus a tick that comes late by more than the threshold fails the check. A liveness window then reads the wedge of the process.
|
|
63
69
|
|
|
64
70
|
## The machines
|
|
@@ -74,6 +80,7 @@ A process has one service, and one entry that uses `run()`. A component gives pa
|
|
|
74
80
|
- **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`).
|
|
75
81
|
- **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.
|
|
76
82
|
- **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.
|
|
83
|
+
- **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`.
|
|
77
84
|
- **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`.
|
|
78
85
|
- **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.
|
|
79
86
|
- **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.
|
|
@@ -122,6 +129,84 @@ const life = await svc.run(); // only the entry calls run(), and the process exi
|
|
|
122
129
|
|
|
123
130
|
The requirements that the lifecycle follows are in [the requirements of S6](https://github.com/mark1russell7/systems/blob/main/docs/S6.md).
|
|
124
131
|
|
|
132
|
+
## Workers and queues
|
|
133
|
+
|
|
134
|
+
`workerLoop(knobs, options)` makes a worker from a loop, and `queueDrain(knobs, options)` makes a consumer from a queue. Each one is a `TransportImpl`, with no defaults. Give `loop.knobs` to `svc.worker(name, knobs, impl)`, and `drain.knobs` to `svc.consumer(…)`. Thus the declaration and the law read the same knobs.
|
|
135
|
+
|
|
136
|
+
- **The worker loop (S6 A10).** A worker has a setup, an iteration and a teardown. The start does the setup, and each iteration gets its state. Each lane does its iterations one at a time, thus two iterations of a lane do not overlap. `concurrency` is the number of lanes.
|
|
137
|
+
- **The sleep.** `worker_sleep` is the time from the end of one iteration to the start of the next. An iteration that gives `"more"` starts the next one after a zero sleep. A sleep without a bound waits for `wake()`.
|
|
138
|
+
- **The first iteration.** The first iteration of each lane comes after the startup ends, because the service refuses a request before that end.
|
|
139
|
+
- **The failures.** A throw is a failure, and the lane then waits its backoff. The first delay is `retry_base`, and each delay is two times longer, up to `retry_cap`, with a jitter up to the base. A success ends the sequence. `onError` gets each failure, and the loop does not end the process (A5).
|
|
140
|
+
- **`wake()`.** It ends each sleep at once, also a sleep that comes after the iteration in flight. It does not end a backoff. After the stop of the intake, it gives false.
|
|
141
|
+
- **The queue drain (S6 A11).** One taker takes a job from the source while a slot is free. `concurrency` is the number of slots, and a count without a bound takes each job that comes. The source is a function that takes the next job, or an async iterable.
|
|
142
|
+
- **An empty source.** A take with no job waits `queue_poll_interval`, or until `wake()`. A failed take waits the backoff.
|
|
143
|
+
- **The end of a source.** The end of an async iterable stops the takes. While the intake is open, `onError` gets that end, because the drain then takes no more jobs. An async generator that throws gives its throw and then its end.
|
|
144
|
+
- **The end of each job.** A success gets `settle.ack`. A failure gets the action of the redelivery policy: `requeue`, `reject` or `ack`. The queue declares its policy, because the drain has no default.
|
|
145
|
+
- **The policy of A11.** `redeliverOnce(then, poison)` gives `ack` to a poison message. Each other job goes back one time after its first failure, and `then` decides each later failure.
|
|
146
|
+
- **The deliveries.** The policy gets the count of deliveries of the job, 1 at the first. `settle.delivery` gives that count from the queue, for example `x-delivery-count` of RabbitMQ or `ApproximateReceiveCount` of SQS. It is necessary, because a broker gives a new object at each delivery. A queue in memory counts for itself.
|
|
147
|
+
- **The bound of a unit.** `unit_processing_time` is the most time of one iteration or one job. At it, the signal of the unit aborts, and a throw after it is a failure. The law reads it as `unit_processing_time@consumer`.
|
|
148
|
+
- **The stop of the intake.** No new iteration starts, and the drain takes no new job. Each sleep and each backoff ends. The drain ends an async iterable through `return()`. A take that gives a job after the stop gives it back (`settle.requeue(job, "unstarted")`).
|
|
149
|
+
- **The drain.** `drain(deadline, signal)` waits for each unit in flight until the deadline, and gives each one the cooperative abort. A throw after that abort is a cut, not a failure.
|
|
150
|
+
- **A cut job.** A cut job goes back (`"cut"`) only when its route can go again (A12). Each other cut job gets `settle.reject`.
|
|
151
|
+
- **The close.** `close` aborts each unit, waits for its end, and then does the teardown or `options.close`, all in `close_bound`. The teardown occurs each time that the setup started, also after a failed setup. The close rejects a job that does not end in the bound.
|
|
152
|
+
- **The request tracking.** With `request: { controller, route }`, each unit goes through `life.request`. The service then admits, counts, drains and cuts each unit as it does each request. A route function and `detail` can give each job a route of a pattern.
|
|
153
|
+
- **The refusals.** While the service refuses its units, the loop and the taker wait their backoff, and each refused job goes back (`"unstarted"`). Thus a worker stops while a dependency of its controller is not ready.
|
|
154
|
+
- **The clock.** The sleeps, the backoffs and the bounds use the clock of the options. Give the clock of the lifecycle. With the real clock, the timers do not keep a Node process alive (A14). The lifecycle holds the process until its exit, thus a process whose only parts are workers does not end while it waits.
|
|
155
|
+
- **The law.** `lint()` applies O20 one time for each consumer and each worker. A knob of the transport binds at the hop `consumer`, and a knob with a hop in its name binds at that hop. The scenario gives the lease of each source, for example the visibility timeout of SQS.
|
|
156
|
+
- **The problems.** `problems(description)` compares the bounds with the caps. `close_bound` fits in the cap of `close transports`. `unit_processing_time` fits in the cap of `drain`, or the drain can cut a unit.
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
import { kubernetes, lint, queueDrain, redeliverOnce, service, workerLoop } from "@mark1russell7/service";
|
|
160
|
+
|
|
161
|
+
const s = (value: number) => ({ value, unit: "s" });
|
|
162
|
+
const count = (value: number) => ({ value, unit: "count" });
|
|
163
|
+
|
|
164
|
+
// a heartbeat each 6 h, in one lane: a bound of 10 s for each beat, in the drain of 15 s, and a backoff from 10 s to 5 min
|
|
165
|
+
const heartbeat = workerLoop(
|
|
166
|
+
{ worker_sleep: s(6 * 3600), unit_processing_time: s(10), retry_base: s(10), retry_cap: s(300), close_bound: s(1), concurrency: count(1) },
|
|
167
|
+
{
|
|
168
|
+
setup: () => openClient(),
|
|
169
|
+
iteration: ({ state, signal }) => state.beat(signal),
|
|
170
|
+
teardown: (state) => state?.close(),
|
|
171
|
+
onError: (error, failures) => log.warn({ error, failures }, "the heartbeat failed"),
|
|
172
|
+
},
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// the jobs of a table, 4 at a time: a poll each 2 s, and the policy of A11
|
|
176
|
+
const jobs = queueDrain(
|
|
177
|
+
{ queue_poll_interval: s(2), unit_processing_time: s(10), retry_base: s(1), retry_cap: s(30), close_bound: s(1), concurrency: count(4) },
|
|
178
|
+
{
|
|
179
|
+
source: (signal) => db.claimNextJob(signal), // a job, or null
|
|
180
|
+
handle: (job, { signal, delivery }) => runPipeline(job, { signal, delivery }),
|
|
181
|
+
settle: {
|
|
182
|
+
ack: (job) => db.markDone(job),
|
|
183
|
+
delivery: (job) => job.attempts, // the count of the table, 1 at the first take
|
|
184
|
+
requeue: (job) => db.markQueued(job),
|
|
185
|
+
reject: (job, error) => db.markFailed(job, error),
|
|
186
|
+
},
|
|
187
|
+
redelivery: redeliverOnce("reject", (error) => error instanceof InvalidJobError),
|
|
188
|
+
request: { controller: "jobs", route: "RUN" },
|
|
189
|
+
},
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const app = service("app")
|
|
193
|
+
.dependency("db", { system: null, knobs: {}, operations: {} }, { connect: () => db.connect(), close: () => db.end() })
|
|
194
|
+
.worker("heartbeat", heartbeat.knobs, heartbeat)
|
|
195
|
+
.consumer("jobs", jobs.knobs, jobs)
|
|
196
|
+
.profile(kubernetes({ grace: s(30), preStop: s(0) }))
|
|
197
|
+
.readiness({ type: "debounce", interval: s(5), failAfter: 3, recoverAfter: 1 })
|
|
198
|
+
.liveness({ type: "window", failAfter: s(45) })
|
|
199
|
+
.startup(s(60));
|
|
200
|
+
// a job that the shutdown cuts goes back on the queue, because its route can go again
|
|
201
|
+
app.controller("jobs").requires("db").route("RUN", { idempotent: true });
|
|
202
|
+
|
|
203
|
+
// O20: the lease of the queue, 30 s, against a job of 10 s
|
|
204
|
+
const scenario = { platform: {}, probes: { readiness: s(30), liveness: s(30) }, transports: { jobs: { "lease_duration@broker": s(30) } } };
|
|
205
|
+
const findings = lint(app.describe(), scenario, rules);
|
|
206
|
+
const problems = [...heartbeat.problems(app.describe()), ...jobs.problems(app.describe())];
|
|
207
|
+
const life = await app.run();
|
|
208
|
+
```
|
|
209
|
+
|
|
125
210
|
## Profiles
|
|
126
211
|
|
|
127
212
|
A profile is a part of a declaration as data: the phases, the cooperative window, the policies and the rules of the shutdown. `svc.profile(profile)` declares each field of a profile, as the method of that field does. The runtime still has no defaults, because the caller gives the profile. The caller changes a field with a spread, and `describe()` shows the values.
|
|
@@ -311,7 +396,7 @@ const life = await app.run(); // the process exits after SIGTERM, with 0 or 1
|
|
|
311
396
|
`@mark1russell7/service/testing` has the seams that a test of a service needs:
|
|
312
397
|
|
|
313
398
|
- `fakeClock()` gives a clock that moves only on `advance(ms)`. Pass it to `start({ clock })`, and the timing of the runtime is exact and repeatable. `block(ms)` moves the clock and starts no timer, as a blocked event loop does.
|
|
314
|
-
- `fakeProcess({ flush, clock })` gives a process port that takes signals and process triggers from a test (`signal(name)`, `trigger(name)`). It keeps each exit code in place of an exit. `flush` is its flush of the output, and `log` gives the time of each flush and each exit.
|
|
399
|
+
- `fakeProcess({ flush, clock })` gives a process port that takes signals and process triggers from a test (`signal(name)`, `trigger(name)`). It keeps each exit code in place of an exit. `flush` is its flush of the output, and `log` gives the time of each flush and each exit. `holds` is the count of holds of the process, and `heldAtExit` gives that count at each exit. `message(value)` sends a message of the IPC channel.
|
|
315
400
|
- `seeded(seed)` gives a seeded random number generator for the jitter (`start({ random })`).
|
|
316
401
|
- `scriptedCheck(behavior)` gives a check whose behavior a test sets: `"ok"`, `"error"`, `"indeterminate"`, `"throw"`, `"timeout"` or `"hang"`.
|
|
317
402
|
- `quietServer()` starts a TCP echo server that can go quiet. Its connections stay open while it is quiet, as with a frozen peer.
|
|
@@ -454,16 +539,22 @@ for await (const event of stream) {
|
|
|
454
539
|
- **The Redis client.** These tests use a fake ioredis and a fake clock. They cover the declaration, the three outcomes, and the commands that the client sends again or not. They also cover the join, the backoff, the watchdog, the close, the checks and the manifestations. Five scenarios replay against the document of the client.
|
|
455
540
|
- **F6 for Redis.** The fault matrix crosses each manifestation (a refusal, a frozen process, a black hole, a death without a close, and an app stall), each gating posture, two timings and two jitters. That is 60 cases on the fake. Each case holds the trip and the release of its policy to `failSpan` and `recoverSpan`. Five more cases shut the service down during each manifestation, and each one exits 0.
|
|
456
541
|
- **The timers.** These tests use a fake clock, and they cover the hooks of the heartbeat and its two kinds of ping. A property test holds the death of a silent peer to `detectionTime` in 300 seeded cases. Another one compares the heartbeat with a WebSocket heartbeat that sends one ping at a time, on 300 schedules of pongs. The backoff gives the delays of three earlier backoffs, number for number, and each delay is in its band.
|
|
457
|
-
- **The entries.** The timer entries reach no other package, no `node:` module and no zod. No entry of the service reaches zod, also through `quantity` and the replay of the algebra. The root reaches no `node:` module. The HTTP edge reaches only `node:http`, `node:net` and `quantity`.
|
|
542
|
+
- **The entries.** The timer entries reach no other package, no `node:` module and no zod. No entry of the service reaches zod, also through `quantity` and the replay of the algebra. The root reaches no `node:` module. The HTTP edge reaches only `node:http`, `node:net` and `quantity`. The worker loop and the queue drain reach only `quantity`.
|
|
458
543
|
- **The requests.** A unary work is a stream of one item, and a stream gives each item when its reader asks. The drain waits for an open stream. The cooperative abort ends a stream through `return()`, and a cut or a failure after the first item ends in-band. A reader that stops early stops the work. The function of a route pattern decides the answer of each cut request.
|
|
459
544
|
- **The options of the runner.** These tests cover the requests of a transport while the startup runs, the policy of a repeat signal, the profiles and the process triggers. They also cover the exit codes, a first Ctrl-C that aborts at once, and the flush of the output.
|
|
460
545
|
- **The composition.** Two components, each with a transport, a dependency and a check, share one service and one `run()`. The parts start and stop in their sequence, and the process exits one time.
|
|
461
|
-
- **The process port.** The port of Node, on a fake host: the signals, the end of stdin
|
|
546
|
+
- **The process port.** The port of Node, on a fake host: the signals, the end of stdin and the close of the IPC channel. The tests also cover the messages, the hold, the flush and the exit. The lifecycle holds its fake process from its start to its exit.
|
|
462
547
|
- **The HTTP edge.** The transport, the mounts, the upgrades, the probes and `handle` run on fake servers and the fake clock, with no socket. A service under `run()` drains a held request, and its probes answer through the drain. A request during the warm-up gets 503, and a stream ends in-band after its first item.
|
|
463
548
|
- **The headers of the outcomes.** Each path of the request tracker gets its status. Each 503 gets `Retry-After`: the refusals, the retryable cut and the cut of a unary stream. No 500 gets it.
|
|
464
549
|
- **The headers on the port.** The transport adds `Retry-After` to each 503 of a probe and of a handler that writes its own status, in each form of `writeHead`. It takes `Retry-After` from each 500. The probe server gives its 503s the time of its knob.
|
|
465
550
|
- **The conformance of a transport.** A reference transport passes each case, and each of nine mutations of it fails the case of its part. The HTTP transport passes each case on a fake server, for its requests and its upgrades.
|
|
551
|
+
- **The worker loop.** These tests use the fake clock. They cover the sleep, `"more"`, the backoff and its jitter, `wake()`, the stop of the intake and the drain with its cooperative abort. They also cover the bound of an iteration, the lanes, the setup, the teardown, the close bound and the law (O20).
|
|
552
|
+
- **The queue drain.** These tests cover the slots, the poll, the policy of A11 and the count of deliveries. They also cover a job that goes back at the stop, an async iterable and a failed take. A cut job of each kind of route, the close bound and the refusals of the service have tests too.
|
|
553
|
+
- **The conformance of the helpers.** The worker loop passes each case of the kit with three lanes, and the queue drain with three slots. Each one passes with the request tracking and without it. With one lane, the loop passes each case that holds one work.
|
|
554
|
+
- **The IPC message.** A message that matches a declared shape starts the staged shutdown on the fake process, with the exit code of its signal. A second message obeys the policy of a repeat signal.
|
|
466
555
|
- **On real sockets.** `HTTP_IT=1` starts the tests of the HTTP edge on the loopback. They cover the conformance on the fake clock and on the real clock, keep-alive and the stop of the intake. They also cover the upgrades, the probe server and a service after SIGTERM. They measure time, thus only CI starts them (`http.yml`), on Node 22.19, 24 and 26. CI keeps the raw records of each job as an artifact.
|
|
556
|
+
- **On the real clock.** `WORKERS_IT=1` starts the kit of the worker loop and of the queue drain on the real clock, in the jobs of `http.yml`.
|
|
557
|
+
- **A real child process.** `PROCESS_IT=1` starts a child under `run()` with an IPC channel, on Linux and on Windows (`process.yml`). The message `shutdown` stops it in stages, with the code 143. A control shows that a SIGTERM of the parent does that only on Linux. A child whose only part is a worker on the real clock stays alive through the hold, and on Linux a SIGTERM gives its teardown.
|
|
467
558
|
- **Against a real Redis.** `REDIS_IT=1` starts the tests of the client against Redis 7.4. `REDIS_F6=1` starts the F6 matrix in the VM of Docker. The injector of `@mark1russell7/harness` makes the network faults, and an agent in the namespaces of Redis makes the process faults. All of them use one monotonic clock.
|
|
468
559
|
|
|
469
560
|
`pnpm --filter @mark1russell7/service build` writes the package for npm into `build/`.
|
|
@@ -1,5 +1,5 @@
|
|
|
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
|
|
2
|
+
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-DjEpgpRk.js";
|
|
3
3
|
import { Extent, Quantity } from "@mark1russell7/quantity";
|
|
4
4
|
//#region src/profiles.d.ts
|
|
5
5
|
/**
|
|
@@ -21,6 +21,8 @@ interface Profile {
|
|
|
21
21
|
readonly cooperativeFloor?: CooperativeFloor | undefined;
|
|
22
22
|
readonly onRepeatSignal?: RepeatSignal | undefined;
|
|
23
23
|
readonly triggers?: readonly ProcessTrigger[] | undefined;
|
|
24
|
+
/** These messages of the IPC channel start the shutdown, as `svc.trigger("ipc-message", message)` declares each one. */
|
|
25
|
+
readonly messages?: readonly MessageTrigger[] | undefined;
|
|
24
26
|
readonly exitCodes?: ExitCodes | undefined;
|
|
25
27
|
}
|
|
26
28
|
/**
|
|
@@ -161,6 +163,11 @@ interface ServiceBuilder {
|
|
|
161
163
|
onRepeatSignal(policy: RepeatSignal): ServiceBuilder;
|
|
162
164
|
/** This method declares process triggers that start the shutdown, in addition to the exit signals. */
|
|
163
165
|
trigger(...triggers: ProcessTrigger[]): ServiceBuilder;
|
|
166
|
+
/**
|
|
167
|
+
* This method declares a message of the IPC channel that starts the shutdown, as data. With `as`,
|
|
168
|
+
* the message is that exit signal: `svc.trigger("ipc-message", { match: "shutdown", as: "SIGTERM" })`.
|
|
169
|
+
*/
|
|
170
|
+
trigger(trigger: "ipc-message", message: MessageTrigger): ServiceBuilder;
|
|
164
171
|
/** This method declares the exit code of a clean shutdown that a trigger starts, in place of 0. */
|
|
165
172
|
exitCode(trigger: ExitTrigger, code: number): ServiceBuilder;
|
|
166
173
|
/** This method declares each field of a profile, as the method of that field does. */
|
|
@@ -194,4 +201,4 @@ interface ServiceBuilder {
|
|
|
194
201
|
declare const service: (name: string) => ServiceBuilder;
|
|
195
202
|
//#endregion
|
|
196
203
|
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 };
|
|
197
|
-
//# sourceMappingURL=builder-
|
|
204
|
+
//# sourceMappingURL=builder-BzjhWk2E.d.ts.map
|
package/build/http.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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-
|
|
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-DjEpgpRk.js";
|
|
3
3
|
import { Extent, Quantity } from "@mark1russell7/quantity";
|
|
4
4
|
import { ListenOptions } from "node:net";
|
|
5
5
|
import { IncomingMessage, Server, ServerEventMap, ServerResponse } from "node:http";
|
package/build/http.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { realClock } from "./clock.js";
|
|
2
|
-
import {
|
|
2
|
+
import { r as millisecondsOf$1 } from "./knobs-BxG-c7Zb.js";
|
|
3
|
+
import { valueIn } from "@mark1russell7/quantity";
|
|
3
4
|
import { createServer } from "node:http";
|
|
4
5
|
//#region src/http/transport.ts
|
|
5
6
|
/** This function gives the path of a request target, without its query and fragment. */
|
|
@@ -15,15 +16,7 @@ const pathOf = (url) => {
|
|
|
15
16
|
}
|
|
16
17
|
};
|
|
17
18
|
/** This function gives a knob in milliseconds, or infinity for a time without a bound. */
|
|
18
|
-
const millisecondsOf = (knobs, name, owner = "httpTransport") =>
|
|
19
|
-
const amount = knobs[name];
|
|
20
|
-
if (amount === void 0) throw new Error(`${owner} needs the knob ${name}: it has no defaults`);
|
|
21
|
-
if (!convertible(amount.unit, "s")) throw new Error(`the knob ${name} is not a time`);
|
|
22
|
-
if (amount.value === "infinite") return Number.POSITIVE_INFINITY;
|
|
23
|
-
const value = valueIn(amount, "ms");
|
|
24
|
-
if (!(value >= 0)) throw new Error(`the knob ${name} is less than zero`);
|
|
25
|
-
return value;
|
|
26
|
-
};
|
|
19
|
+
const millisecondsOf = (knobs, name, owner = "httpTransport") => millisecondsOf$1(knobs, name, owner);
|
|
27
20
|
/**
|
|
28
21
|
* This function gives the `Retry-After` of the knob `server_retry_after`, in whole seconds, rounded
|
|
29
22
|
* up. The knob is necessary, and it has a bound.
|
package/build/http.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.js","names":[],"sources":["../src/http/transport.ts","../src/http/probes.ts","../src/http/outcome.ts"],"sourcesContent":["import type { Extent, Quantity } from \"@mark1russell7/quantity\";\nimport { convertible, valueIn } 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 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 const amount = knobs[name];\n if (amount === undefined) throw new Error(`${owner} needs the knob ${name}: it has no defaults`);\n if (!convertible(amount.unit, \"s\")) throw new Error(`the knob ${name} is not a time`);\n if (amount.value === \"infinite\") return Number.POSITIVE_INFINITY;\n const value = valueIn(amount, \"ms\");\n if (!(value >= 0)) throw new Error(`the knob ${name} is less than zero`);\n return value;\n};\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 { 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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"}
|