@mark1russell7/service 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +120 -15
- package/build/builder-BFxOuULH.d.ts +607 -0
- package/build/clock-DlJFFZlX.js +39 -0
- package/build/clock-DlJFFZlX.js.map +1 -0
- package/build/index.d.ts +47 -228
- package/build/index.js +1536 -88
- package/build/index.js.map +1 -1
- package/build/redis.d.ts +294 -0
- package/build/redis.js +853 -0
- package/build/redis.js.map +1 -0
- package/build/testing.d.ts +102 -0
- package/build/testing.js +349 -0
- package/build/testing.js.map +1 -0
- package/build/timing-DxZ4nkQg.js +79 -0
- package/build/timing-DxZ4nkQg.js.map +1 -0
- package/package.json +20 -2
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
import { Extent, Quantity } from "@mark1russell7/quantity";
|
|
2
|
+
//#region src/timing.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* All policies have the same timing shape. A timing is a count of polls (a debounce) or a
|
|
5
|
+
* duration (a window), and each converts to the other. A long window is easier to read as a
|
|
6
|
+
* duration, and that is the only difference between the two.
|
|
7
|
+
*/
|
|
8
|
+
type Timing = {
|
|
9
|
+
readonly type: "debounce";
|
|
10
|
+
/** This interval is the time between two polls of the signal. */
|
|
11
|
+
readonly interval: Quantity;
|
|
12
|
+
/** This count is the number of failed polls in sequence before the policy fails. */
|
|
13
|
+
readonly failAfter: number;
|
|
14
|
+
/** This count is the number of passed polls in sequence before the policy recovers. */
|
|
15
|
+
readonly recoverAfter: number;
|
|
16
|
+
} | {
|
|
17
|
+
readonly type: "window";
|
|
18
|
+
/** This duration is how long the signal fails before the policy fails, on the monotonic clock. */
|
|
19
|
+
readonly failAfter: Quantity;
|
|
20
|
+
/** This duration is how long the signal passes before the policy recovers. Without it, the first pass recovers the policy. */
|
|
21
|
+
readonly recoverAfter?: Quantity | undefined;
|
|
22
|
+
};
|
|
23
|
+
/** A poll has an interval, a jitter as a fraction (±20 % is 0.2) and a timeout. */
|
|
24
|
+
interface Poll {
|
|
25
|
+
readonly interval: Quantity;
|
|
26
|
+
readonly jitter: number;
|
|
27
|
+
readonly timeout: Quantity;
|
|
28
|
+
}
|
|
29
|
+
/** A span is a range of times, from the earliest to the latest. */
|
|
30
|
+
interface Span {
|
|
31
|
+
readonly min: Quantity;
|
|
32
|
+
readonly max: Quantity;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* This type tells how a failed step answers a poll.
|
|
36
|
+
* - An `"error"` step refuses at once.
|
|
37
|
+
* - A `"timeout"` step (a black hole) answers only when the timeout of the poll ends. Each poll
|
|
38
|
+
* makes a new call, and each call ends at its timeout. Thus the step is slower by that timeout,
|
|
39
|
+
* at the two ends of the span.
|
|
40
|
+
* - A `"hang"` step makes a use that does not end. The first poll fails at its timeout. While the
|
|
41
|
+
* use stays pending, the runtime does not start the check again, and each later poll fails at
|
|
42
|
+
* once. Thus a debounce of two polls or more fails as fast as on an error.
|
|
43
|
+
*/
|
|
44
|
+
type FailsBy = "error" | "timeout" | "hang";
|
|
45
|
+
/**
|
|
46
|
+
* This function gives the time that a policy takes to fail after its signal becomes bad. A
|
|
47
|
+
* debounce of N polls takes approximately N intervals, with the jitter. A window takes its
|
|
48
|
+
* duration, plus one interval at most for the poll that starts its sequence. A step that fails by
|
|
49
|
+
* a timeout adds the timeout of the poll. At 3 polls of 5 s ± 20 %, the latch fails in 8–18 s on
|
|
50
|
+
* an error, and in 12–22 s on a 4 s timeout. A step that hangs adds the timeout only where its
|
|
51
|
+
* first failure ends the sequence: a window, or a debounce of one poll.
|
|
52
|
+
*/
|
|
53
|
+
declare const failSpan: (timing: Timing, poll: Poll, by?: FailsBy) => Span;
|
|
54
|
+
/** This function gives the time that a policy takes to recover after its signal becomes good. */
|
|
55
|
+
declare const recoverSpan: (timing: Timing, poll: Poll) => Span;
|
|
56
|
+
/** This function gives the window that a debounce approximates: N polls of an interval fail after N intervals. */
|
|
57
|
+
declare const asWindow: (timing: Timing) => Timing;
|
|
58
|
+
/** This function gives the debounce that approximates a window on a poll: the polls that fit, rounded up. */
|
|
59
|
+
declare const asDebounce: (timing: Timing, poll: Poll) => Timing;
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/operation.d.ts
|
|
62
|
+
/**
|
|
63
|
+
* This type tells if an operation can go again after a failure. It is a property of the
|
|
64
|
+
* operation, not of the dependency. One Redis client sends a GET, which is safe to send again. It
|
|
65
|
+
* also sends an INCR, which counts two times when it goes again after a timeout. Each operation
|
|
66
|
+
* declares its idempotency:
|
|
67
|
+
* - `true`: a second issue has the same effect. Examples are an HTTP GET or PUT, a SELECT, a
|
|
68
|
+
* Redis GET or SET, and a write that the server removes when it is a copy.
|
|
69
|
+
* - `false`: the client does not send it again after it can have reached the server.
|
|
70
|
+
* - keyed: the server keeps a key for a window, as with an `Idempotency-Key` header, and it
|
|
71
|
+
* ignores a copy in that window. A copy after the window is a second operation. Thus the
|
|
72
|
+
* window is a timer, and it must be longer than all retries of the operation.
|
|
73
|
+
*/
|
|
74
|
+
type Idempotency = boolean | {
|
|
75
|
+
readonly key: string;
|
|
76
|
+
readonly window: Extent;
|
|
77
|
+
};
|
|
78
|
+
/** An operation is an endpoint that the service calls or serves, a command, or a query. */
|
|
79
|
+
interface Operation {
|
|
80
|
+
readonly idempotent: Idempotency;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* This function tells if an operation can go again `since` its first issue. An idempotent
|
|
84
|
+
* operation can always go again, and other operations cannot. A keyed operation can go again
|
|
85
|
+
* while the server keeps its key.
|
|
86
|
+
*/
|
|
87
|
+
declare const mayReissue: (operation: Operation, since: Quantity) => boolean;
|
|
88
|
+
//#endregion
|
|
89
|
+
//#region src/description.d.ts
|
|
90
|
+
/**
|
|
91
|
+
* This is the format of a service as data: what `describe()` exports, and nothing starts. The
|
|
92
|
+
* timer algebra and the ordering law read it. Each timer has a unit. Each knob comes from the
|
|
93
|
+
* declaration, because the runtime has no defaults. A bound that nobody gives is "no bound", not
|
|
94
|
+
* zero.
|
|
95
|
+
*/
|
|
96
|
+
declare const FORMAT = "mark1russell7.service/1";
|
|
97
|
+
/** A step is a step of a connection that a check examines. A check that skips a step cannot see a failure in it. */
|
|
98
|
+
type Step = "dial" | "local-state" | "round-trip" | "checkout" | "progress" | "topology" | "latency";
|
|
99
|
+
/** A gate is the policy that the failures of a check count toward. */
|
|
100
|
+
type Gate = "readiness" | "liveness";
|
|
101
|
+
/** A check is an active check: one signal, for one step of a connection. */
|
|
102
|
+
interface Check {
|
|
103
|
+
readonly name: string;
|
|
104
|
+
/** This name is the dependency that it examines, or null when it reads the state of the process. */
|
|
105
|
+
readonly dependency: string | null;
|
|
106
|
+
readonly step: Step;
|
|
107
|
+
/** This flag tells if it uses the connection of the application. A check on its own connection cannot see a failure of that connection. */
|
|
108
|
+
readonly ownConnection: boolean;
|
|
109
|
+
/** This gate is the policy that it controls, or null for a check that only reports. It has no default. */
|
|
110
|
+
readonly gates: Gate | null;
|
|
111
|
+
readonly poll: Poll;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* A dependency is something that the service uses, with its client. Its knobs have the names that
|
|
115
|
+
* the ordering law uses: `request_timeout`, `connect_bound`, `driver_writeoff_time`,
|
|
116
|
+
* `heartbeat_interval`, `client_retry_window`. Thus the law reads them without a translation.
|
|
117
|
+
*/
|
|
118
|
+
interface Dependency {
|
|
119
|
+
readonly name: string;
|
|
120
|
+
/** This identifier is the component in the corpus (`systems.component.postgresql`), or null when the corpus does not have it. */
|
|
121
|
+
readonly system: string | null;
|
|
122
|
+
readonly knobs: Readonly<Record<string, Extent>>;
|
|
123
|
+
/**
|
|
124
|
+
* These are the operations that the service does with it, by name (`GET /v1/forecast`, `INCR`).
|
|
125
|
+
* Each operation declares if it can go again. A dependency usually has the two kinds, thus a
|
|
126
|
+
* dependency has no idempotency of its own. An operation without a declaration does not go again.
|
|
127
|
+
*/
|
|
128
|
+
readonly operations: Readonly<Record<string, Operation>>;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* A transport is something that takes work in: a server, a consumer or a worker. Each is a
|
|
132
|
+
* component with a state machine. The service is ready only while each transport takes work.
|
|
133
|
+
*/
|
|
134
|
+
interface Transport {
|
|
135
|
+
readonly name: string;
|
|
136
|
+
readonly kind: "server" | "consumer" | "worker";
|
|
137
|
+
/** These are its knobs, with the names of the law where the law has them: the `server_idle_close` of a server. */
|
|
138
|
+
readonly knobs: Readonly<Record<string, Extent>>;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* A controller is a group of routes. It is ready only while the dependencies that it needs are
|
|
142
|
+
* ready. Each route declares if its callers can send it again. When the shutdown stops a request,
|
|
143
|
+
* an idempotent route answers 503, which a caller can retry. Other routes answer 500.
|
|
144
|
+
*/
|
|
145
|
+
interface Controller {
|
|
146
|
+
readonly name: string;
|
|
147
|
+
readonly requires: readonly string[];
|
|
148
|
+
readonly routes: Readonly<Record<string, Operation>>;
|
|
149
|
+
}
|
|
150
|
+
/** A coupling is a declared edge between two policies. The algebra checks its margin. */
|
|
151
|
+
interface Coupling {
|
|
152
|
+
readonly from: Gate;
|
|
153
|
+
readonly to: Gate;
|
|
154
|
+
readonly effect: "fails";
|
|
155
|
+
}
|
|
156
|
+
/** A phase is one step of the shutdown sequence, with a cap. Its owner adds it. */
|
|
157
|
+
interface Phase {
|
|
158
|
+
readonly name: string;
|
|
159
|
+
readonly cap: Quantity;
|
|
160
|
+
}
|
|
161
|
+
/** A service description is the whole graph of a service, as data. */
|
|
162
|
+
interface ServiceDescription {
|
|
163
|
+
readonly format: typeof FORMAT;
|
|
164
|
+
readonly name: string;
|
|
165
|
+
/** This flag tells if a part joined after the start. That part starts tripped, and the export is partial. */
|
|
166
|
+
readonly partial: boolean;
|
|
167
|
+
readonly transports: readonly Transport[];
|
|
168
|
+
readonly dependencies: readonly Dependency[];
|
|
169
|
+
readonly controllers: readonly Controller[];
|
|
170
|
+
readonly checks: readonly Check[];
|
|
171
|
+
readonly policies: {
|
|
172
|
+
readonly readiness: Timing;
|
|
173
|
+
readonly liveness: Timing;
|
|
174
|
+
/** This duration is how long the service can take to start. The startup probe must agree with it. */
|
|
175
|
+
readonly startup: Quantity;
|
|
176
|
+
};
|
|
177
|
+
readonly couplings: readonly Coupling[];
|
|
178
|
+
readonly shutdown: {
|
|
179
|
+
readonly phases: readonly Phase[];
|
|
180
|
+
/** This window is how long before the end of the drain the cooperative abort occurs, or no bound. */
|
|
181
|
+
readonly cooperativeWindow: Extent;
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
//#endregion
|
|
185
|
+
//#region src/clock.d.ts
|
|
186
|
+
/** This function stops a timer that `Clock.after` started. A second use does nothing. */
|
|
187
|
+
type Cancel = () => void;
|
|
188
|
+
/**
|
|
189
|
+
* A clock gives the monotonic time in milliseconds, and it starts a function after a delay. The
|
|
190
|
+
* runtime reads time only through a clock. Thus a test can give a fake clock, and the timing of
|
|
191
|
+
* the runtime is then exact and repeatable.
|
|
192
|
+
*/
|
|
193
|
+
interface Clock {
|
|
194
|
+
/** This method gives the monotonic time, in milliseconds. A step of the wall clock does not move it. */
|
|
195
|
+
now(): number;
|
|
196
|
+
/** This method starts `run` after `ms` milliseconds, and gives the function that stops it. */
|
|
197
|
+
after(ms: number, run: () => void): Cancel;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* This clock reads `performance.now()` and uses `setTimeout`. Its timers do not keep a Node
|
|
201
|
+
* process alive, thus a timer that is not stopped cannot stop the exit of the process.
|
|
202
|
+
*/
|
|
203
|
+
declare const realClock: Clock;
|
|
204
|
+
//#endregion
|
|
205
|
+
//#region src/events.d.ts
|
|
206
|
+
/**
|
|
207
|
+
* A machine name names one machine of the runtime. Each machine has a document in `machines.ts`,
|
|
208
|
+
* which names the same states and events. The document of a client is with its client, for example
|
|
209
|
+
* `REDIS_CLIENT_MACHINE`.
|
|
210
|
+
*/
|
|
211
|
+
type MachineName = "gate" | "latch" | "window" | "poller" | "drain" | "exit" | "watchdog" | "client";
|
|
212
|
+
/**
|
|
213
|
+
* A lifecycle event is one transition of one machine of the runtime, on the monotonic clock. The
|
|
214
|
+
* runtime stamps each planned event: the gate, each phase of the drain, and the exit. Thus a
|
|
215
|
+
* window that a measurement times starts at the event.
|
|
216
|
+
*/
|
|
217
|
+
interface LifecycleEvent {
|
|
218
|
+
/** This time is when the transition occurred, in milliseconds on the monotonic clock. */
|
|
219
|
+
readonly at: number;
|
|
220
|
+
readonly machine: MachineName;
|
|
221
|
+
/** This name identifies one machine among others of its kind: a check, or the service. */
|
|
222
|
+
readonly instance: string;
|
|
223
|
+
readonly from: string;
|
|
224
|
+
readonly to: string;
|
|
225
|
+
readonly on: string;
|
|
226
|
+
/** This text gives more about the event, for example the cause of a failure. */
|
|
227
|
+
readonly detail?: string | undefined;
|
|
228
|
+
}
|
|
229
|
+
/** A recorder receives each lifecycle event. */
|
|
230
|
+
type Recorder = (event: LifecycleEvent) => void;
|
|
231
|
+
//#endregion
|
|
232
|
+
//#region src/latch.d.ts
|
|
233
|
+
/**
|
|
234
|
+
* An outcome is the result of one poll of a check. Only `"ok"` passes. An `"indeterminate"`
|
|
235
|
+
* outcome measured nothing: it does not pass, and it does not fail.
|
|
236
|
+
*/
|
|
237
|
+
type Outcome = "ok" | "error" | "indeterminate";
|
|
238
|
+
/** A result is one poll of a check, as the runtime records it. */
|
|
239
|
+
interface Result {
|
|
240
|
+
readonly outcome: Outcome;
|
|
241
|
+
/** This time is when the result came, on the monotonic clock, in milliseconds. */
|
|
242
|
+
readonly at: number;
|
|
243
|
+
/** This time is how long the poll took, in milliseconds. */
|
|
244
|
+
readonly took: number;
|
|
245
|
+
/** This text tells why the poll did not pass, or it is null. */
|
|
246
|
+
readonly cause: string | null;
|
|
247
|
+
/** This flag tells if the poll failed at its timeout. */
|
|
248
|
+
readonly timedOut: boolean;
|
|
249
|
+
/** This flag tells if the check threw. A throw is an error with a flag. */
|
|
250
|
+
readonly thrown: boolean;
|
|
251
|
+
/** This flag tells if the force-fail list made the poll fail. */
|
|
252
|
+
readonly forced: boolean;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* A trace receives each transition of a machine of the runtime: the state before, the state after,
|
|
256
|
+
* and the event. The machine documents of the lifecycle name the same states and events, thus a
|
|
257
|
+
* test can replay a trace against its document.
|
|
258
|
+
*/
|
|
259
|
+
type Trace = (from: string, to: string, on: string) => void;
|
|
260
|
+
//#endregion
|
|
261
|
+
//#region src/poller.d.ts
|
|
262
|
+
/**
|
|
263
|
+
* A check function measures one step of a connection. It ends with nothing or `"ok"` for a pass,
|
|
264
|
+
* or with `"error"` or `"indeterminate"`. A throw is an error with a flag. The runtime aborts the
|
|
265
|
+
* signal at the timeout of the poll.
|
|
266
|
+
*/
|
|
267
|
+
type CheckFn = (signal: AbortSignal) => Outcome | void | Promise<Outcome | void>;
|
|
268
|
+
//#endregion
|
|
269
|
+
//#region src/health.d.ts
|
|
270
|
+
/** These options start the runtime. Without them, it uses the real clock and `Math.random`. */
|
|
271
|
+
interface StartOptions {
|
|
272
|
+
readonly clock?: Clock | undefined;
|
|
273
|
+
/** This function gives a number in [0, 1) for the jitter of each poll. */
|
|
274
|
+
readonly random?: (() => number) | undefined;
|
|
275
|
+
/** This list names the checks or dependencies whose polls fail at once (a seam for tests). */
|
|
276
|
+
readonly forceFail?: readonly string[] | undefined;
|
|
277
|
+
/** This function receives each transition of each machine of the runtime. */
|
|
278
|
+
readonly record?: Recorder | undefined;
|
|
279
|
+
/**
|
|
280
|
+
* This function can replace the function of a check, for example to add a delay to each call
|
|
281
|
+
* (a seam for tests: a degraded dependency). It gives the function to use.
|
|
282
|
+
*/
|
|
283
|
+
readonly wrapCheck?: ((check: Check, run: CheckFn) => CheckFn) | undefined;
|
|
284
|
+
}
|
|
285
|
+
/** A check verdict is the cached state of one check, for the probes and the report. */
|
|
286
|
+
interface CheckVerdict {
|
|
287
|
+
readonly gates: Gate | null;
|
|
288
|
+
readonly dependency: string | null;
|
|
289
|
+
/** This flag tells if the latch of the check is tripped, or it is null for a check that only reports. */
|
|
290
|
+
readonly tripped: boolean | null;
|
|
291
|
+
readonly streak: number;
|
|
292
|
+
/** This time is when the latch last changed, in milliseconds on the monotonic clock. */
|
|
293
|
+
readonly since: number | null;
|
|
294
|
+
readonly last: Result | null;
|
|
295
|
+
}
|
|
296
|
+
/** A verdict is the state of the service, from its cache. Nothing in it starts a check. */
|
|
297
|
+
interface HealthVerdict {
|
|
298
|
+
/** This flag becomes true one time, after the warm-up sweep. */
|
|
299
|
+
readonly started: boolean;
|
|
300
|
+
readonly draining: boolean;
|
|
301
|
+
/** This flag tells if the service can take new work: started, not draining, and each readiness latch released. */
|
|
302
|
+
readonly ready: boolean;
|
|
303
|
+
/** This flag tells if each liveness latch is released. */
|
|
304
|
+
readonly alive: boolean;
|
|
305
|
+
/** These flags tell if each controller can take new work, from the dependencies that it needs. */
|
|
306
|
+
readonly controllers: Readonly<Record<string, boolean>>;
|
|
307
|
+
readonly dependencies: Readonly<Record<string, boolean>>;
|
|
308
|
+
readonly checks: Readonly<Record<string, CheckVerdict>>;
|
|
309
|
+
readonly startup: {
|
|
310
|
+
readonly budget: number;
|
|
311
|
+
readonly elapsed: number;
|
|
312
|
+
readonly overBudget: boolean;
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
/** A probe kind is the question that a probe asks: the startup, the readiness, the liveness, or the whole report. */
|
|
316
|
+
type ProbeKind = "startup" | "readiness" | "liveness" | "report";
|
|
317
|
+
/** A probe answer is a status and a body, for any HTTP framework or none. */
|
|
318
|
+
interface ProbeAnswer {
|
|
319
|
+
readonly status: 200 | 503;
|
|
320
|
+
readonly contentType: "application/health+json";
|
|
321
|
+
readonly body: {
|
|
322
|
+
readonly status: "pass" | "fail";
|
|
323
|
+
readonly service: string;
|
|
324
|
+
readonly probe: ProbeKind;
|
|
325
|
+
readonly controller?: string | undefined;
|
|
326
|
+
readonly verdict?: HealthVerdict | undefined;
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
/** The health runtime of a started service. It reads its timing and its checks from the declaration. */
|
|
330
|
+
interface Health {
|
|
331
|
+
verdict(): HealthVerdict;
|
|
332
|
+
/**
|
|
333
|
+
* This method answers a probe from the cache, in constant time, and it starts no check. A
|
|
334
|
+
* readiness probe with a controller asks about that controller. The report always answers 200.
|
|
335
|
+
*/
|
|
336
|
+
probe(kind: ProbeKind, controller?: string): ProbeAnswer;
|
|
337
|
+
/** This method gives each change of the verdict to `listener`, and gives the function that stops it. */
|
|
338
|
+
subscribe(listener: (verdict: HealthVerdict) => void): () => void;
|
|
339
|
+
/** This method adds a name to the force-fail list, or removes it. */
|
|
340
|
+
forceFail(name: string, on: boolean): void;
|
|
341
|
+
/** This method fails readiness for good. The drain of the lifecycle starts with it. */
|
|
342
|
+
drain(): void;
|
|
343
|
+
/** This method stops each poller and each timer of the runtime. */
|
|
344
|
+
stop(): void;
|
|
345
|
+
}
|
|
346
|
+
//#endregion
|
|
347
|
+
//#region src/process.d.ts
|
|
348
|
+
/** An exit signal is a signal that asks the process to stop: each one starts the shutdown. */
|
|
349
|
+
type ExitSignal = "SIGINT" | "SIGTERM" | "SIGQUIT";
|
|
350
|
+
/** These are the signals that the runtime handles. */
|
|
351
|
+
declare const EXIT_SIGNALS: readonly ExitSignal[];
|
|
352
|
+
/**
|
|
353
|
+
* A process port is the part of the process that the runtime uses: its exit signals and its exit.
|
|
354
|
+
* The runtime owns the handlers, because Node as PID 1 without a handler ignores SIGTERM. Only the
|
|
355
|
+
* runtime ends the process, and library code does not. A test gives a fake port, sends signals,
|
|
356
|
+
* and reads the exit code.
|
|
357
|
+
*/
|
|
358
|
+
interface ProcessPort {
|
|
359
|
+
/** This method adds a handler for a signal, and gives the function that removes it. */
|
|
360
|
+
on(signal: ExitSignal, handler: () => void): () => void;
|
|
361
|
+
/** This method ends the process with a code: 0 after a clean shutdown, and 1 after an error. */
|
|
362
|
+
exit(code: number): void;
|
|
363
|
+
}
|
|
364
|
+
/** This port is the process of Node. */
|
|
365
|
+
declare const nodeProcess: ProcessPort;
|
|
366
|
+
//#endregion
|
|
367
|
+
//#region src/requests.d.ts
|
|
368
|
+
/**
|
|
369
|
+
* A request outcome is what a request of a controller came to, for any HTTP framework or none:
|
|
370
|
+
* - `done`: the work gave a value.
|
|
371
|
+
* - `failed`: the work failed for a cause of its own. The adapter answers it as it answers any
|
|
372
|
+
* error of the route.
|
|
373
|
+
* - `refused`: the service did not admit the request. A refused request did not start, thus its
|
|
374
|
+
* caller can send it again: 503, retryable.
|
|
375
|
+
* - `cut`: the shutdown stopped the request. A route whose callers can send it again answers 503,
|
|
376
|
+
* retryable. Each other route answers 500, not retryable, because the work can have had its
|
|
377
|
+
* effect.
|
|
378
|
+
*/
|
|
379
|
+
type RequestOutcome<T> = {
|
|
380
|
+
readonly kind: "done";
|
|
381
|
+
readonly value: T;
|
|
382
|
+
} | {
|
|
383
|
+
readonly kind: "failed";
|
|
384
|
+
readonly error: unknown;
|
|
385
|
+
} | {
|
|
386
|
+
readonly kind: "refused";
|
|
387
|
+
readonly status: 503;
|
|
388
|
+
readonly retryable: true;
|
|
389
|
+
readonly cause: string;
|
|
390
|
+
} | {
|
|
391
|
+
readonly kind: "cut";
|
|
392
|
+
readonly status: 503 | 500;
|
|
393
|
+
readonly retryable: boolean;
|
|
394
|
+
readonly cause: string;
|
|
395
|
+
};
|
|
396
|
+
/** A request function does the work of one request. It receives a signal that the shutdown aborts. */
|
|
397
|
+
type RequestFn<T> = (signal: AbortSignal) => Promise<T> | T;
|
|
398
|
+
//#endregion
|
|
399
|
+
//#region src/lifecycle.d.ts
|
|
400
|
+
/**
|
|
401
|
+
* A transport is a source of work: a server, a consumer or a worker. Each has one contract. It
|
|
402
|
+
* starts, stops its intake, and drains to a deadline without a throw. It counts its work in flight,
|
|
403
|
+
* and it closes. A second close has no effect.
|
|
404
|
+
*/
|
|
405
|
+
interface TransportImpl {
|
|
406
|
+
start(signal: AbortSignal): Promise<void> | void;
|
|
407
|
+
stopIntake(): Promise<void> | void;
|
|
408
|
+
/**
|
|
409
|
+
* This method lets the work in flight end until `deadline` (milliseconds on the monotonic
|
|
410
|
+
* clock). The signal is the cooperative abort, which comes before the deadline.
|
|
411
|
+
*/
|
|
412
|
+
drain(deadline: number, signal: AbortSignal): Promise<void> | void;
|
|
413
|
+
inFlight(): number;
|
|
414
|
+
close(): Promise<void> | void;
|
|
415
|
+
}
|
|
416
|
+
/** A dependency handle connects to a dependency at the start, and closes the connection at the end. */
|
|
417
|
+
interface DependencyHandle {
|
|
418
|
+
connect(signal: AbortSignal): Promise<void> | void;
|
|
419
|
+
close(): Promise<void> | void;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* A health server answers the probes, on its own port. It starts first, thus the probes answer
|
|
423
|
+
* while the dependencies connect, and it closes last.
|
|
424
|
+
*/
|
|
425
|
+
interface HealthServer {
|
|
426
|
+
listen(health: Health): Promise<void> | void;
|
|
427
|
+
close(): Promise<void> | void;
|
|
428
|
+
}
|
|
429
|
+
/** A phase context is what a phase gives its work: its deadline, its signal and the clock. */
|
|
430
|
+
interface PhaseContext {
|
|
431
|
+
/** This time is the end of the cap of the phase, in milliseconds on the monotonic clock. */
|
|
432
|
+
readonly deadline: number;
|
|
433
|
+
/** This signal aborts at the end of the cap of the phase. */
|
|
434
|
+
readonly signal: AbortSignal;
|
|
435
|
+
readonly clock: Clock;
|
|
436
|
+
}
|
|
437
|
+
/** A phase work is a step of the shutdown that an owner adds to a phase. */
|
|
438
|
+
type PhaseWork = (context: PhaseContext) => Promise<void> | void;
|
|
439
|
+
/**
|
|
440
|
+
* These are the stages of the shutdown, in their sequence. A declared phase with one of these
|
|
441
|
+
* names gets the work of the runtime for its stage. A phase with another name gets only the work of
|
|
442
|
+
* its owners.
|
|
443
|
+
* - `fail readiness`: readiness fails at the start of the shutdown. This phase then holds for its
|
|
444
|
+
* cap, while the endpoints of the platform remove the service.
|
|
445
|
+
* - `stop intake`: each transport stops its intake, and new requests get a retryable 503.
|
|
446
|
+
* - `drain`: the transports and the requests in flight end, until the cap. The cooperative abort
|
|
447
|
+
* comes before the end of the cap, by the cooperative window.
|
|
448
|
+
* - `close transports`: each transport closes.
|
|
449
|
+
* - `stop the poller`: each check and watchdog stops. Then the signal of the service aborts.
|
|
450
|
+
* - `tear down dependencies`: each dependency closes, in the reverse sequence of its connection.
|
|
451
|
+
* - `close the health server`: the probes stop last.
|
|
452
|
+
*/
|
|
453
|
+
declare const STAGES: readonly ["fail readiness", "stop intake", "drain", "close transports", "stop the poller", "tear down dependencies", "close the health server"];
|
|
454
|
+
type Stage = (typeof STAGES)[number];
|
|
455
|
+
/** This function gives the place of a stage in the sequence, or -1 for a phase of another name. */
|
|
456
|
+
declare const stageRank: (name: string) => number;
|
|
457
|
+
/**
|
|
458
|
+
* This function gives the time of the cooperative abort, in milliseconds after the drain starts. It
|
|
459
|
+
* is the cap less the cooperative window, and not less than half the cap. A window without a bound
|
|
460
|
+
* gives half the cap.
|
|
461
|
+
*/
|
|
462
|
+
declare const cooperativeAbortAt: (cap: number, window: Extent) => number;
|
|
463
|
+
/** These options run the lifecycle. Without them, it uses the real clock and the process of Node. */
|
|
464
|
+
interface RunOptions extends StartOptions {
|
|
465
|
+
readonly process?: ProcessPort | undefined;
|
|
466
|
+
}
|
|
467
|
+
/** A lifecycle state tells where the service is in its life. */
|
|
468
|
+
type LifecycleState = "starting" | "running" | "draining" | "exited";
|
|
469
|
+
/** The lifecycle of a running service. */
|
|
470
|
+
interface Lifecycle {
|
|
471
|
+
readonly health: Health;
|
|
472
|
+
readonly state: LifecycleState;
|
|
473
|
+
/** This code is the exit code, or null before the exit. */
|
|
474
|
+
readonly exitCode: number | null;
|
|
475
|
+
/** These texts are the errors that make the exit code 1. */
|
|
476
|
+
readonly errors: readonly string[];
|
|
477
|
+
/** This promise ends when the startup ends: the service runs, or its startup failed. */
|
|
478
|
+
readonly started: Promise<void>;
|
|
479
|
+
/** This promise gives the exit code when the shutdown ends. */
|
|
480
|
+
readonly done: Promise<number>;
|
|
481
|
+
/** This signal aborts after the poller stops: the background work of the service stops with it. */
|
|
482
|
+
readonly signal: AbortSignal;
|
|
483
|
+
/** This method asks for the shutdown, as an exit signal does. A second request has no effect. */
|
|
484
|
+
shutdown(cause?: string): Promise<number>;
|
|
485
|
+
/**
|
|
486
|
+
* This method does one request of a controller, on a route that the controller declares. The
|
|
487
|
+
* service refuses it while it cannot take new work, and the shutdown can cut it.
|
|
488
|
+
*/
|
|
489
|
+
request<T>(controller: string, route: string, work: RequestFn<T>): Promise<RequestOutcome<T>>;
|
|
490
|
+
inFlight(controller?: string): number;
|
|
491
|
+
/**
|
|
492
|
+
* This method tells if a response can keep its connection open. It is false while the service is
|
|
493
|
+
* not ready, thus each response then closes its connection.
|
|
494
|
+
*/
|
|
495
|
+
keepAlive(): boolean;
|
|
496
|
+
}
|
|
497
|
+
//#endregion
|
|
498
|
+
//#region src/watchdog.d.ts
|
|
499
|
+
/**
|
|
500
|
+
* A watchdog spec declares a watchdog of the event loop. The service declares it as a check whose
|
|
501
|
+
* function the watchdog gives: the process's own wedge, which a liveness window reads.
|
|
502
|
+
*/
|
|
503
|
+
interface WatchdogSpec {
|
|
504
|
+
readonly name: string;
|
|
505
|
+
/** This time is the interval of the tick of the watchdog. */
|
|
506
|
+
readonly tick: Quantity;
|
|
507
|
+
/** This time is the most that a tick can be late before the loop counts as blocked. */
|
|
508
|
+
readonly threshold: Quantity;
|
|
509
|
+
/** This gate is the policy that the check controls, or null for a check that only reports. */
|
|
510
|
+
readonly gates: Gate | null;
|
|
511
|
+
readonly poll: Poll;
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* A watchdog measures how late its own tick comes. A blocked event loop delays each timer, thus a
|
|
515
|
+
* tick that comes later than the threshold shows the block. Its states are `watching`, `blocked`
|
|
516
|
+
* and `stopped`.
|
|
517
|
+
*/
|
|
518
|
+
interface Watchdog {
|
|
519
|
+
/** This function is the check: it fails when a tick since the last poll came late, or the loop is still blocked. */
|
|
520
|
+
readonly check: () => Outcome;
|
|
521
|
+
/** This time is the most that a tick was late, in milliseconds, since the start. */
|
|
522
|
+
readonly worst: number;
|
|
523
|
+
readonly blocked: boolean;
|
|
524
|
+
stop(): void;
|
|
525
|
+
}
|
|
526
|
+
/** This function starts a watchdog on a clock. */
|
|
527
|
+
declare const watchdog: (clock: Clock, spec: WatchdogSpec, trace?: Trace) => Watchdog;
|
|
528
|
+
//#endregion
|
|
529
|
+
//#region src/builder.d.ts
|
|
530
|
+
/**
|
|
531
|
+
* A service is composed with plain method uses before it starts. The builder is imperative, and
|
|
532
|
+
* it holds static knowledge. The service does not use an `init` function that gives registrations.
|
|
533
|
+
*
|
|
534
|
+
* const svc = service("app");
|
|
535
|
+
* svc.dependency("weather", {
|
|
536
|
+
* system: null,
|
|
537
|
+
* knobs: { request_timeout },
|
|
538
|
+
* operations: { "GET /v1/forecast": { idempotent: true } },
|
|
539
|
+
* });
|
|
540
|
+
* svc.controller("forecast").requires("weather").route("GET /api/forecast", { idempotent: true });
|
|
541
|
+
* svc.check({ name: "weather.round-trip", dependency: "weather", … }, roundTrip);
|
|
542
|
+
* svc.describe(); // the whole graph as data, with nothing started
|
|
543
|
+
* const health = await svc.start();
|
|
544
|
+
*
|
|
545
|
+
* The graph has one source, thus its parts cannot disagree. A dependency or a check can join
|
|
546
|
+
* after the start. It starts tripped, and the export is partial. The runtime has no defaults of
|
|
547
|
+
* its own. Thus `describe()` refuses a service that does not declare a policy, its startup budget
|
|
548
|
+
* or its shutdown, and it tells which.
|
|
549
|
+
*
|
|
550
|
+
* `start()` starts only the health runtime. `run()` starts the whole lifecycle: the startup in its
|
|
551
|
+
* sequence, the exit signals, the capped phases of the shutdown, and the exit of the process.
|
|
552
|
+
*/
|
|
553
|
+
interface ControllerBuilder {
|
|
554
|
+
/** This method declares the dependencies that its routes need. The controller is ready only while they are ready. */
|
|
555
|
+
requires(...dependencies: string[]): ControllerBuilder;
|
|
556
|
+
/** This method declares a route that the controller serves, and if its callers can send it again. */
|
|
557
|
+
route(route: string, operation: Operation): ControllerBuilder;
|
|
558
|
+
}
|
|
559
|
+
/** A service builder composes a service, gives it as data, and starts it. */
|
|
560
|
+
interface ServiceBuilder {
|
|
561
|
+
readonly name: string;
|
|
562
|
+
/** This method declares a dependency, and the handle that connects to it and closes it, if it has one. */
|
|
563
|
+
dependency(name: string, spec: Omit<Dependency, "name">, handle?: DependencyHandle): ServiceBuilder;
|
|
564
|
+
/** This method declares a server, and its implementation. `run()` needs the implementation. */
|
|
565
|
+
server(name: string, knobs: Readonly<Record<string, Extent>>, impl?: TransportImpl): ServiceBuilder;
|
|
566
|
+
consumer(name: string, knobs: Readonly<Record<string, Extent>>, impl?: TransportImpl): ServiceBuilder;
|
|
567
|
+
worker(name: string, knobs: Readonly<Record<string, Extent>>, impl?: TransportImpl): ServiceBuilder;
|
|
568
|
+
/** This method gives the server of the probes. It starts first and closes last. */
|
|
569
|
+
healthServer(server: HealthServer): ServiceBuilder;
|
|
570
|
+
/** This method adds work to a declared phase of the shutdown: the owner of the work registers it. */
|
|
571
|
+
onPhase(phase: string, work: PhaseWork): ServiceBuilder;
|
|
572
|
+
/** This method declares a watchdog of the event loop, as a check on the state of the process. */
|
|
573
|
+
watchdog(spec: WatchdogSpec): ServiceBuilder;
|
|
574
|
+
controller(name: string): ControllerBuilder;
|
|
575
|
+
/** This method declares a check, and the function that measures its step. `start()` needs the function. */
|
|
576
|
+
check(check: Check, run?: CheckFn): ServiceBuilder;
|
|
577
|
+
readiness(timing: Timing): ServiceBuilder;
|
|
578
|
+
liveness(timing: Timing): ServiceBuilder;
|
|
579
|
+
startup(budget: Quantity): ServiceBuilder;
|
|
580
|
+
couple(coupling: Coupling): ServiceBuilder;
|
|
581
|
+
phase(name: string, cap: Quantity): ServiceBuilder;
|
|
582
|
+
cooperativeWindow(window: Extent): ServiceBuilder;
|
|
583
|
+
/** This method gives the service as data, and it starts nothing. */
|
|
584
|
+
describe(): ServiceDescription;
|
|
585
|
+
/**
|
|
586
|
+
* This method closes the composition, starts the health runtime, and gives it after one warm-up
|
|
587
|
+
* sweep of each check. A part that joins after it starts tripped, and it makes the export
|
|
588
|
+
* partial. A second use gives the same runtime.
|
|
589
|
+
*/
|
|
590
|
+
start(options?: StartOptions): Promise<Health>;
|
|
591
|
+
/**
|
|
592
|
+
* This method starts the whole lifecycle and gives it when its startup ends: the service operates,
|
|
593
|
+
* or its startup failed and it shuts down. It refuses a declaration that it cannot start: a transport
|
|
594
|
+
* without its implementation, or a stage with work and no declared phase.
|
|
595
|
+
*/
|
|
596
|
+
run(options?: RunOptions): Promise<Lifecycle>;
|
|
597
|
+
/**
|
|
598
|
+
* This value is the health runtime from the time that `start()` begins, or null before. Thus the
|
|
599
|
+
* probes can answer during the warm-up sweep: the startup probe fails until the sweep ends.
|
|
600
|
+
*/
|
|
601
|
+
readonly health: Health | null;
|
|
602
|
+
}
|
|
603
|
+
/** This function starts the composition of a service. */
|
|
604
|
+
declare const service: (name: string) => ServiceBuilder;
|
|
605
|
+
//#endregion
|
|
606
|
+
export { FailsBy as $, StartOptions as A, Check as B, ProcessPort as C, HealthVerdict as D, Health as E, MachineName as F, Gate as G, Coupling as H, Recorder as I, Step as J, Phase as K, Cancel as L, Outcome as M, Result as N, ProbeAnswer as O, LifecycleEvent as P, mayReissue as Q, Clock as R, ExitSignal as S, CheckVerdict as T, Dependency as U, Controller as V, FORMAT as W, Idempotency as X, Transport as Y, Operation as Z, cooperativeAbortAt as _, WatchdogSpec as a, failSpan as at, RequestOutcome as b, HealthServer as c, PhaseContext as d, Poll as et, PhaseWork as f, TransportImpl as g, Stage as h, Watchdog as i, asWindow as it, CheckFn as j, ProbeKind as k, Lifecycle as l, STAGES as m, ServiceBuilder as n, Timing as nt, watchdog as o, recoverSpan as ot, RunOptions as p, ServiceDescription as q, service as r, asDebounce as rt, DependencyHandle as s, ControllerBuilder as t, Span as tt, LifecycleState as u, stageRank as v, nodeProcess as w, EXIT_SIGNALS as x, RequestFn as y, realClock as z };
|
|
607
|
+
//# sourceMappingURL=builder-BFxOuULH.d.ts.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { compare, convertible } from "@mark1russell7/quantity";
|
|
2
|
+
//#region src/operation.ts
|
|
3
|
+
/**
|
|
4
|
+
* This function tells if an operation can go again `since` its first issue. An idempotent
|
|
5
|
+
* operation can always go again, and other operations cannot. A keyed operation can go again
|
|
6
|
+
* while the server keeps its key.
|
|
7
|
+
*/
|
|
8
|
+
const mayReissue = (operation, since) => {
|
|
9
|
+
const { idempotent } = operation;
|
|
10
|
+
if (typeof idempotent === "boolean") return idempotent;
|
|
11
|
+
return compare(since, idempotent.window) < 0;
|
|
12
|
+
};
|
|
13
|
+
/** This function gives the problems in the declaration of an operation that `describe()` refuses. */
|
|
14
|
+
const operationProblems = (owner, name, operation) => {
|
|
15
|
+
const { idempotent } = operation;
|
|
16
|
+
if (typeof idempotent === "boolean") return [];
|
|
17
|
+
const problems = [];
|
|
18
|
+
if (idempotent.key.trim().length === 0) problems.push(`${owner}'s ${name} names no key`);
|
|
19
|
+
if (!convertible(idempotent.window.unit, "s")) problems.push(`${owner}'s ${name} holds its key for a span that is not a time`);
|
|
20
|
+
return problems;
|
|
21
|
+
};
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/clock.ts
|
|
24
|
+
/**
|
|
25
|
+
* This clock reads `performance.now()` and uses `setTimeout`. Its timers do not keep a Node
|
|
26
|
+
* process alive, thus a timer that is not stopped cannot stop the exit of the process.
|
|
27
|
+
*/
|
|
28
|
+
const realClock = {
|
|
29
|
+
now: () => performance.now(),
|
|
30
|
+
after(ms, run) {
|
|
31
|
+
const timer = setTimeout(run, Math.max(0, ms));
|
|
32
|
+
timer.unref?.();
|
|
33
|
+
return () => clearTimeout(timer);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
//#endregion
|
|
37
|
+
export { mayReissue as n, operationProblems as r, realClock as t };
|
|
38
|
+
|
|
39
|
+
//# sourceMappingURL=clock-DlJFFZlX.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clock-DlJFFZlX.js","names":[],"sources":["../src/operation.ts","../src/clock.ts"],"sourcesContent":["import type { Extent, Quantity } from \"@mark1russell7/quantity\";\nimport { compare, convertible } from \"@mark1russell7/quantity\";\n\n/**\n * This type tells if an operation can go again after a failure. It is a property of the\n * operation, not of the dependency. One Redis client sends a GET, which is safe to send again. It\n * also sends an INCR, which counts two times when it goes again after a timeout. Each operation\n * declares its idempotency:\n * - `true`: a second issue has the same effect. Examples are an HTTP GET or PUT, a SELECT, a\n * Redis GET or SET, and a write that the server removes when it is a copy.\n * - `false`: the client does not send it again after it can have reached the server.\n * - keyed: the server keeps a key for a window, as with an `Idempotency-Key` header, and it\n * ignores a copy in that window. A copy after the window is a second operation. Thus the\n * window is a timer, and it must be longer than all retries of the operation.\n */\nexport type Idempotency = boolean | { readonly key: string; readonly window: Extent };\n\n/** An operation is an endpoint that the service calls or serves, a command, or a query. */\nexport interface Operation {\n readonly idempotent: Idempotency;\n}\n\n/**\n * This function tells if an operation can go again `since` its first issue. An idempotent\n * operation can always go again, and other operations cannot. A keyed operation can go again\n * while the server keeps its key.\n */\nexport const mayReissue = (operation: Operation, since: Quantity): boolean => {\n const { idempotent } = operation;\n if (typeof idempotent === \"boolean\") return idempotent;\n return compare(since, idempotent.window) < 0;\n};\n\n/** This function gives the problems in the declaration of an operation that `describe()` refuses. */\nexport const operationProblems = (owner: string, name: string, operation: Operation): string[] => {\n const { idempotent } = operation;\n if (typeof idempotent === \"boolean\") return [];\n const problems: string[] = [];\n if (idempotent.key.trim().length === 0) problems.push(`${owner}'s ${name} names no key`);\n if (!convertible(idempotent.window.unit, \"s\"))\n problems.push(`${owner}'s ${name} holds its key for a span that is not a time`);\n return problems;\n};\n","/** This function stops a timer that `Clock.after` started. A second use does nothing. */\nexport type Cancel = () => void;\n\n/**\n * A clock gives the monotonic time in milliseconds, and it starts a function after a delay. The\n * runtime reads time only through a clock. Thus a test can give a fake clock, and the timing of\n * the runtime is then exact and repeatable.\n */\nexport interface Clock {\n /** This method gives the monotonic time, in milliseconds. A step of the wall clock does not move it. */\n now(): number;\n /** This method starts `run` after `ms` milliseconds, and gives the function that stops it. */\n after(ms: number, run: () => void): Cancel;\n}\n\n/**\n * This clock reads `performance.now()` and uses `setTimeout`. Its timers do not keep a Node\n * process alive, thus a timer that is not stopped cannot stop the exit of the process.\n */\nexport const realClock: Clock = {\n now: () => performance.now(),\n after(ms, run) {\n const timer = setTimeout(run, Math.max(0, ms));\n (timer as { unref?: () => void }).unref?.();\n return () => clearTimeout(timer);\n },\n};\n"],"mappings":";;;;;;;AA2BA,MAAa,cAAc,WAAsB,UAA6B;CAC5E,MAAM,EAAE,eAAe;CACvB,IAAI,OAAO,eAAe,WAAW,OAAO;CAC5C,OAAO,QAAQ,OAAO,WAAW,MAAM,IAAI;AAC7C;;AAGA,MAAa,qBAAqB,OAAe,MAAc,cAAmC;CAChG,MAAM,EAAE,eAAe;CACvB,IAAI,OAAO,eAAe,WAAW,OAAO,CAAC;CAC7C,MAAM,WAAqB,CAAC;CAC5B,IAAI,WAAW,IAAI,KAAK,CAAC,CAAC,WAAW,GAAG,SAAS,KAAK,GAAG,MAAM,KAAK,KAAK,cAAc;CACvF,IAAI,CAAC,YAAY,WAAW,OAAO,MAAM,GAAG,GAC1C,SAAS,KAAK,GAAG,MAAM,KAAK,KAAK,6CAA6C;CAChF,OAAO;AACT;;;;;;;ACvBA,MAAa,YAAmB;CAC9B,WAAW,YAAY,IAAI;CAC3B,MAAM,IAAI,KAAK;EACb,MAAM,QAAQ,WAAW,KAAK,KAAK,IAAI,GAAG,EAAE,CAAC;EAC7C,MAAkC,QAAQ;EAC1C,aAAa,aAAa,KAAK;CACjC;AACF"}
|