@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/build/index.js CHANGED
@@ -1,109 +1,969 @@
1
- import { compare, convertExact, convertible, quantity, rationals } from "@mark1russell7/quantity";
2
- //#region src/timing.ts
3
- const seconds = (amount) => convertExact(rationals.fromNumber(amount.value), amount.unit, "s");
4
- const out = (value) => quantity(rationals.toNumber(value), "s");
5
- const fraction = (jitter) => {
6
- if (!(jitter >= 0 && jitter < 1)) throw new RangeError(`jitter is a fraction below 1, not ${jitter}`);
7
- return rationals.fromNumber(jitter);
8
- };
9
- const count = (polls, what) => {
10
- if (!Number.isInteger(polls) || polls < 1) throw new RangeError(`${what} is a whole number of polls, not ${polls}`);
11
- return rationals.rational(BigInt(polls));
1
+ import { i as recoverSpan, n as asWindow, r as failSpan, t as asDebounce } from "./timing-DxZ4nkQg.js";
2
+ import { n as mayReissue, r as operationProblems, t as realClock } from "./clock-DlJFFZlX.js";
3
+ import { convertExact, convertible, quantity, rationals, valueIn } from "@mark1russell7/quantity";
4
+ import { arr, fromStates } from "@mark1russell7/algebra";
5
+ //#region src/description.ts
6
+ /**
7
+ * This is the format of a service as data: what `describe()` exports, and nothing starts. The
8
+ * timer algebra and the ordering law read it. Each timer has a unit. Each knob comes from the
9
+ * declaration, because the runtime has no defaults. A bound that nobody gives is "no bound", not
10
+ * zero.
11
+ */
12
+ const FORMAT = "mark1russell7.service/1";
13
+ //#endregion
14
+ //#region src/latch.ts
15
+ const ms = (amount) => valueIn(amount, "ms");
16
+ /**
17
+ * This function makes a latch. `tripped` is its state at the start: a readiness latch starts
18
+ * tripped, and a liveness latch starts released. The latch starts `changed` each time it trips or
19
+ * releases, and it gives each transition to `trace`. A debounce has the states `released` and
20
+ * `tripped`. A window also has the states `failing` and `recovering`, while one of its timers runs.
21
+ */
22
+ const latch = (timing, clock, tripped, changed, trace) => {
23
+ let state = tripped;
24
+ let since = clock.now();
25
+ let streak = 0;
26
+ let fails = 0;
27
+ let passes = 0;
28
+ let failingSince = null;
29
+ let passingSince = null;
30
+ let failTimer = null;
31
+ let passTimer = null;
32
+ /** This function gives the name of the state, as the machine documents name it. */
33
+ const named = () => state ? passingSince === null ? "tripped" : "recovering" : failingSince === null ? "released" : "failing";
34
+ /** This function does one step of the machine, and gives its transition to the trace. */
35
+ const step = (on, change) => {
36
+ const from = named();
37
+ change();
38
+ trace?.(from, named(), on);
39
+ };
40
+ const set = (next) => {
41
+ if (next === state) return;
42
+ state = next;
43
+ since = clock.now();
44
+ changed();
45
+ };
46
+ const stopFail = () => {
47
+ failTimer?.();
48
+ failTimer = null;
49
+ failingSince = null;
50
+ };
51
+ const stopPass = () => {
52
+ passTimer?.();
53
+ passTimer = null;
54
+ passingSince = null;
55
+ };
56
+ const debounce = (result) => {
57
+ if (timing.type !== "debounce") return;
58
+ if (result.outcome === "ok") {
59
+ passes += 1;
60
+ fails = 0;
61
+ if (state && passes >= timing.recoverAfter) set(false);
62
+ } else {
63
+ fails += 1;
64
+ passes = 0;
65
+ if (!state && fails >= timing.failAfter) set(true);
66
+ }
67
+ };
68
+ const windowed = (result) => {
69
+ if (timing.type !== "window") return;
70
+ if (result.outcome === "ok") {
71
+ stopFail();
72
+ if (!state) return;
73
+ if (timing.recoverAfter === void 0) {
74
+ set(false);
75
+ return;
76
+ }
77
+ if (passingSince !== null) return;
78
+ const wait = ms(timing.recoverAfter);
79
+ if (wait <= 0) {
80
+ stopPass();
81
+ set(false);
82
+ return;
83
+ }
84
+ passingSince = result.at;
85
+ passTimer = clock.after(wait, () => step("window.recovered", () => {
86
+ passTimer = null;
87
+ passingSince = null;
88
+ set(false);
89
+ }));
90
+ return;
91
+ }
92
+ stopPass();
93
+ if (state || failingSince !== null) return;
94
+ const wait = ms(timing.failAfter);
95
+ if (wait <= 0) {
96
+ stopFail();
97
+ set(true);
98
+ return;
99
+ }
100
+ failingSince = result.at;
101
+ failTimer = clock.after(wait, () => step("window.failed", () => {
102
+ failTimer = null;
103
+ failingSince = null;
104
+ set(true);
105
+ }));
106
+ };
107
+ return {
108
+ get tripped() {
109
+ return state;
110
+ },
111
+ get since() {
112
+ return since;
113
+ },
114
+ get streak() {
115
+ return streak;
116
+ },
117
+ record(result) {
118
+ if (result.outcome === "indeterminate") return;
119
+ const pass = result.outcome === "ok";
120
+ step(pass ? "poll.ok" : "poll.fail", () => {
121
+ streak = pass ? Math.max(0, streak) + 1 : Math.min(0, streak) - 1;
122
+ debounce(result);
123
+ windowed(result);
124
+ });
125
+ },
126
+ stop() {
127
+ stopFail();
128
+ stopPass();
129
+ }
130
+ };
12
131
  };
13
- /** N polls with jitter take from N − 1 short intervals to N long intervals. */
14
- const debounced = (polls, interval, jitter) => [rationals.mul(rationals.sub(polls, rationals.ONE), rationals.mul(interval, rationals.sub(rationals.ONE, jitter))), rationals.mul(polls, rationals.mul(interval, rationals.add(rationals.ONE, jitter)))];
132
+ //#endregion
133
+ //#region src/poller.ts
134
+ const message$1 = (error) => error instanceof Error ? error.message : String(error);
15
135
  /**
16
- * This function gives the time that a policy takes to fail after its signal becomes bad. A
17
- * debounce of N polls takes approximately N intervals, with the jitter. A window takes its
18
- * duration, plus one interval at most for the poll that starts its sequence. A step that fails by
19
- * a timeout adds the timeout of the poll. At 3 polls of 5 s ± 20 %, the latch fails in 8–18 s on
20
- * an error, and in 12–22 s on a 4 s timeout.
136
+ * This function makes a poller. The loop is on a fixed delay from the start of each poll. Thus the
137
+ * next poll starts one interval after the current poll starts, with the jitter (±20 % is 0.2).
138
+ * The timing algebra of the declaration uses this model.
139
+ *
140
+ * Each poll makes a new measurement. While a use of the check is pending, the poller does not
141
+ * start the check again. Such a poll fails at once with the same cause, and the sequence of
142
+ * failures continues.
21
143
  */
22
- const failSpan = (timing, poll, by = "error") => {
23
- const jitter = fraction(poll.jitter);
24
- const late = by === "timeout" ? seconds(poll.timeout) : rationals.ZERO;
25
- if (timing.type === "debounce") {
26
- const [min, max] = debounced(count(timing.failAfter, "failAfter"), seconds(timing.interval), jitter);
27
- return {
28
- min: out(rationals.add(min, late)),
29
- max: out(rationals.add(max, late))
30
- };
31
- }
32
- const duration = seconds(timing.failAfter);
33
- const interval = rationals.mul(seconds(poll.interval), rationals.add(rationals.ONE, jitter));
144
+ const poller = (options) => {
145
+ const { clock, poll, run } = options;
146
+ const interval = valueIn(poll.interval, "ms");
147
+ const timeout = valueIn(poll.timeout, "ms");
148
+ let last = null;
149
+ let next = null;
150
+ let stopped = false;
151
+ let looping = false;
152
+ let pending = null;
153
+ const active = /* @__PURE__ */ new Set();
154
+ /** This function gives the name of the state, as the machine document of the poller names it. */
155
+ const named = () => stopped ? "stopped" : pending !== null ? pending.late ? "hung" : "calling" : looping ? "waiting" : "idle";
156
+ const step = (on, change) => {
157
+ const from = named();
158
+ change();
159
+ options.trace?.(from, named(), on);
160
+ };
161
+ const result = (outcome, started, fields = {}) => ({
162
+ outcome,
163
+ at: clock.now(),
164
+ took: clock.now() - started,
165
+ cause: fields.cause ?? null,
166
+ timedOut: fields.timedOut ?? false,
167
+ thrown: fields.thrown ?? false,
168
+ forced: fields.forced ?? false
169
+ });
170
+ const once = () => {
171
+ const started = clock.now();
172
+ if (options.forced()) return Promise.resolve(result("error", started, {
173
+ cause: "the force-fail list names it",
174
+ forced: true
175
+ }));
176
+ if (pending !== null) {
177
+ const cause = `its call of ${Math.round(started - pending.since)} ms ago is still pending`;
178
+ return Promise.resolve(result("error", started, { cause }));
179
+ }
180
+ const controller = new AbortController();
181
+ return new Promise((resolve) => {
182
+ let done = false;
183
+ const finish = (value) => {
184
+ if (done) return;
185
+ done = true;
186
+ active.delete(abandon);
187
+ cancel();
188
+ resolve(value);
189
+ };
190
+ const abandon = () => {
191
+ controller.abort();
192
+ finish(result("error", started, { cause: "the runtime stopped" }));
193
+ };
194
+ active.add(abandon);
195
+ const call = {
196
+ since: started,
197
+ late: false
198
+ };
199
+ pending = call;
200
+ const cancel = clock.after(timeout, () => {
201
+ controller.abort();
202
+ step("timeout", () => {
203
+ if (pending === call) call.late = true;
204
+ });
205
+ finish(result("error", started, {
206
+ cause: `no answer in ${timeout} ms`,
207
+ timedOut: true
208
+ }));
209
+ });
210
+ const settled = () => {
211
+ if (pending !== call) return;
212
+ step(call.late ? "late-answer" : "answer", () => {
213
+ pending = null;
214
+ });
215
+ };
216
+ let answer;
217
+ try {
218
+ answer = Promise.resolve(run(controller.signal));
219
+ } catch (error) {
220
+ answer = Promise.reject(error);
221
+ }
222
+ answer.then((value) => {
223
+ settled();
224
+ const outcome = value ?? "ok";
225
+ finish(result(outcome, started, outcome === "ok" ? {} : { cause: `the check gave ${outcome}` }));
226
+ }, (error) => {
227
+ settled();
228
+ finish(result("error", started, {
229
+ cause: message$1(error),
230
+ thrown: true
231
+ }));
232
+ });
233
+ });
234
+ };
235
+ const handle = (value) => {
236
+ last = value;
237
+ if (!stopped) options.record(value);
238
+ return value;
239
+ };
240
+ const jittered = () => interval * (1 + poll.jitter * (2 * options.random() - 1));
241
+ const tick = () => {
242
+ next = null;
243
+ if (stopped) return;
244
+ let polled;
245
+ step("tick", () => {
246
+ next = clock.after(jittered(), tick);
247
+ polled = once();
248
+ });
249
+ polled.then(handle);
250
+ };
34
251
  return {
35
- min: out(rationals.add(duration, late)),
36
- max: out(rationals.add(rationals.add(duration, interval), late))
252
+ get last() {
253
+ return last;
254
+ },
255
+ pollNow: () => {
256
+ let polled;
257
+ step("sweep", () => {
258
+ polled = once();
259
+ });
260
+ return polled.then(handle);
261
+ },
262
+ start(delay) {
263
+ if (stopped || next !== null) return;
264
+ step("start", () => {
265
+ looping = true;
266
+ next = clock.after(Math.max(0, delay), tick);
267
+ });
268
+ },
269
+ jittered,
270
+ stop() {
271
+ if (stopped) return;
272
+ step("stop", () => {
273
+ stopped = true;
274
+ next?.();
275
+ next = null;
276
+ });
277
+ for (const abandon of [...active]) abandon();
278
+ }
37
279
  };
38
280
  };
39
- /** This function gives the time that a policy takes to recover after its signal becomes good. */
40
- const recoverSpan = (timing, poll) => {
41
- const jitter = fraction(poll.jitter);
42
- if (timing.type === "debounce") {
43
- const [min, max] = debounced(count(timing.recoverAfter, "recoverAfter"), seconds(timing.interval), jitter);
44
- return {
45
- min: out(min),
46
- max: out(max)
281
+ //#endregion
282
+ //#region src/health.ts
283
+ /**
284
+ * This function makes the runtime of a declaration. The service has no defaults of its own: each
285
+ * timing, poll and coupling comes from the description.
286
+ */
287
+ const healthRuntime = (description, runs, options = {}) => {
288
+ const clock = options.clock ?? realClock;
289
+ const random = options.random ?? Math.random;
290
+ const forced = new Set(options.forceFail ?? []);
291
+ const startedAt = clock.now();
292
+ const budget = valueIn(description.policies.startup, "ms");
293
+ const entries = /* @__PURE__ */ new Map();
294
+ const dependencies = new Set(description.dependencies.map((dependency) => dependency.name));
295
+ const lateDependencies = /* @__PURE__ */ new Set();
296
+ const listeners = /* @__PURE__ */ new Set();
297
+ let started = false;
298
+ let draining = false;
299
+ let stopped = false;
300
+ let gate = "closed";
301
+ const tracer = (machine, instance) => (from, to, on) => options.record?.({
302
+ at: clock.now(),
303
+ machine,
304
+ instance,
305
+ from,
306
+ to,
307
+ on
308
+ });
309
+ const gateTrace = tracer("gate", description.name);
310
+ const moveGate = (to, on, detail) => {
311
+ const from = gate;
312
+ gate = to;
313
+ options.record?.({
314
+ at: clock.now(),
315
+ machine: "gate",
316
+ instance: description.name,
317
+ from,
318
+ to,
319
+ on,
320
+ ...detail === void 0 ? {} : { detail }
321
+ });
322
+ };
323
+ let budgetTimer = clock.after(budget, () => {
324
+ budgetTimer = null;
325
+ if (gate !== "open" && gate !== "failed") gateTrace(gate, gate, "startup.over-budget");
326
+ });
327
+ const changed = () => {
328
+ if (listeners.size === 0) return;
329
+ const now = verdict();
330
+ for (const listener of listeners) listener(now);
331
+ };
332
+ const add = (check, given, late) => {
333
+ const run = options.wrapCheck?.(check, given) ?? given;
334
+ const timing = check.gates === null ? null : description.policies[check.gates];
335
+ const made = timing === null ? null : latch(timing, clock, check.gates === "readiness", changed, tracer(timing.type === "window" ? "window" : "latch", check.name));
336
+ const entry = {
337
+ check,
338
+ latch: made,
339
+ poller: poller({
340
+ name: check.name,
341
+ poll: check.poll,
342
+ run,
343
+ clock,
344
+ random,
345
+ forced: () => forced.has(check.name) || check.dependency !== null && forced.has(check.dependency),
346
+ record: (result) => {
347
+ made?.record(result);
348
+ if (result.outcome === "ok" && check.dependency !== null) {
349
+ if (lateDependencies.delete(check.dependency)) changed();
350
+ }
351
+ },
352
+ trace: tracer("poller", check.name)
353
+ })
47
354
  };
355
+ entries.set(check.name, entry);
356
+ if (late && !stopped) {
357
+ entry.poller.pollNow();
358
+ entry.poller.start(entry.poller.jittered());
359
+ }
360
+ };
361
+ for (const check of description.checks) {
362
+ const run = runs.get(check.name);
363
+ if (run === void 0) throw new Error(`check ${check.name} has no function: the runtime has no defaults`);
364
+ add(check, run, false);
48
365
  }
49
- const duration = timing.recoverAfter === void 0 ? rationals.ZERO : seconds(timing.recoverAfter);
50
- const interval = rationals.mul(seconds(poll.interval), rationals.add(rationals.ONE, jitter));
366
+ const latchesOf = (gateOf, keep = () => true) => [...entries.values()].flatMap((entry) => entry.check.gates === gateOf && entry.latch !== null && keep(entry.check) ? [entry.latch] : []);
367
+ const released = (latches) => latches.every((each) => !each.tripped);
368
+ /** This function tells if a policy fails through a declared coupling from another policy. */
369
+ const coupledOff = (to, base) => description.couplings.some((coupling) => coupling.to === to && !base[coupling.from]);
370
+ const verdict = () => {
371
+ const base = {
372
+ readiness: released(latchesOf("readiness")) && lateDependencies.size === 0,
373
+ liveness: released(latchesOf("liveness"))
374
+ };
375
+ const alive = base.liveness && !coupledOff("liveness", base);
376
+ const open = started && !draining && !coupledOff("readiness", base);
377
+ const ready = open && base.readiness;
378
+ const dependencyReady = (name) => !lateDependencies.has(name) && released(latchesOf("readiness", (check) => check.dependency === name));
379
+ const own = released(latchesOf("readiness", (check) => check.dependency === null));
380
+ const dependenciesOut = {};
381
+ for (const name of dependencies) dependenciesOut[name] = dependencyReady(name);
382
+ const controllers = {};
383
+ for (const controller of description.controllers) controllers[controller.name] = open && own && controller.requires.every(dependencyReady);
384
+ const checks = {};
385
+ for (const [name, entry] of entries) checks[name] = {
386
+ gates: entry.check.gates,
387
+ dependency: entry.check.dependency,
388
+ tripped: entry.latch?.tripped ?? null,
389
+ streak: entry.latch?.streak ?? 0,
390
+ since: entry.latch?.since ?? null,
391
+ last: entry.poller.last
392
+ };
393
+ const elapsed = clock.now() - startedAt;
394
+ return {
395
+ started,
396
+ draining,
397
+ ready,
398
+ alive,
399
+ controllers,
400
+ dependencies: dependenciesOut,
401
+ checks,
402
+ startup: {
403
+ budget,
404
+ elapsed,
405
+ overBudget: !started && elapsed > budget
406
+ }
407
+ };
408
+ };
409
+ const answer = (kind, pass, controller) => ({
410
+ status: pass || kind === "report" ? 200 : 503,
411
+ contentType: "application/health+json",
412
+ body: {
413
+ status: pass ? "pass" : "fail",
414
+ service: description.name,
415
+ probe: kind,
416
+ ...controller === void 0 ? {} : { controller },
417
+ ...kind === "report" ? { verdict: verdict() } : {}
418
+ }
419
+ });
51
420
  return {
52
- min: out(duration),
53
- max: out(rationals.add(duration, interval))
421
+ verdict,
422
+ probe(kind, controller) {
423
+ const now = verdict();
424
+ if (kind === "startup") return answer(kind, now.started);
425
+ if (kind === "liveness") return answer(kind, now.alive);
426
+ if (kind === "report") return answer(kind, now.ready && now.alive);
427
+ if (controller === void 0) return answer(kind, now.ready);
428
+ const known = now.controllers[controller];
429
+ if (known === void 0) throw new Error(`${description.name} has no controller ${controller}`);
430
+ return answer(kind, known, controller);
431
+ },
432
+ subscribe(listener) {
433
+ listeners.add(listener);
434
+ return () => listeners.delete(listener);
435
+ },
436
+ forceFail(name, on) {
437
+ if (on) forced.add(name);
438
+ else forced.delete(name);
439
+ },
440
+ drain() {
441
+ if (draining) return;
442
+ draining = true;
443
+ changed();
444
+ },
445
+ stop() {
446
+ stopped = true;
447
+ budgetTimer?.();
448
+ budgetTimer = null;
449
+ for (const entry of entries.values()) {
450
+ entry.poller.stop();
451
+ entry.latch?.stop();
452
+ }
453
+ },
454
+ beginStartup() {
455
+ if (gate === "closed") moveGate("starting", "startup.begin");
456
+ },
457
+ failStartup(cause) {
458
+ if (gate === "open" || gate === "failed") return;
459
+ moveGate("failed", "startup.fail", cause);
460
+ budgetTimer?.();
461
+ budgetTimer = null;
462
+ },
463
+ async warmUp() {
464
+ if (started || stopped || gate === "failed" || gate === "sweeping") return;
465
+ moveGate("sweeping", "mark-ready");
466
+ const sweep = clock.now();
467
+ await Promise.all([...entries.values()].map((entry) => entry.poller.pollNow()));
468
+ if (stopped || gate !== "sweeping") return;
469
+ for (const entry of entries.values()) entry.poller.start(entry.poller.jittered() - (clock.now() - sweep));
470
+ started = true;
471
+ budgetTimer?.();
472
+ budgetTimer = null;
473
+ moveGate("open", "sweep.done");
474
+ changed();
475
+ },
476
+ addCheck(check, run) {
477
+ if (entries.has(check.name)) throw new Error(`${description.name} already has a check named ${check.name}`);
478
+ add(check, run, true);
479
+ changed();
480
+ },
481
+ addDependency(name) {
482
+ if (dependencies.has(name)) return;
483
+ dependencies.add(name);
484
+ lateDependencies.add(name);
485
+ changed();
486
+ }
54
487
  };
55
488
  };
56
- /** This function gives the window that a debounce approximates: N polls of an interval fail after N intervals. */
57
- const asWindow = (timing) => timing.type === "window" ? timing : {
58
- type: "window",
59
- failAfter: out(rationals.mul(count(timing.failAfter, "failAfter"), seconds(timing.interval))),
60
- recoverAfter: out(rationals.mul(count(timing.recoverAfter, "recoverAfter"), seconds(timing.interval)))
489
+ //#endregion
490
+ //#region src/process.ts
491
+ /** These are the signals that the runtime handles. */
492
+ const EXIT_SIGNALS = [
493
+ "SIGINT",
494
+ "SIGTERM",
495
+ "SIGQUIT"
496
+ ];
497
+ /** This port is the process of Node. */
498
+ const nodeProcess = {
499
+ on(signal, handler) {
500
+ const host = globalThis.process;
501
+ if (host === void 0) throw new Error("no process: give the runtime a process port");
502
+ host.on(signal, handler);
503
+ return () => {
504
+ host.off(signal, handler);
505
+ };
506
+ },
507
+ exit(code) {
508
+ const host = globalThis.process;
509
+ if (host === void 0) throw new Error("no process: give the runtime a process port");
510
+ host.exit(code);
511
+ }
61
512
  };
62
- /** This function gives the debounce that approximates a window on a poll: the polls that fit, rounded up. */
63
- const asDebounce = (timing, poll) => {
64
- if (timing.type === "debounce") return timing;
65
- const interval = seconds(poll.interval);
66
- const polls = (duration) => duration === void 0 ? 1 : Math.max(1, Number(rationals.ceil(rationals.div(duration, interval)).n));
513
+ //#endregion
514
+ //#region src/requests.ts
515
+ /**
516
+ * This function makes a tracker. `refuses` gives the reason that the service refuses a new request
517
+ * for a controller at this time, or null when it admits it.
518
+ */
519
+ const tracker = (controllers, clock, refuses) => {
520
+ const byName = new Map(controllers.map((controller) => [controller.name, controller]));
521
+ const flights = /* @__PURE__ */ new Set();
522
+ const idle = /* @__PURE__ */ new Set();
523
+ let intake = true;
524
+ const settle = (flight) => {
525
+ flights.delete(flight);
526
+ if (flights.size === 0) for (const done of [...idle]) done();
527
+ };
528
+ /** This function answers a request that the shutdown stopped, by the idempotency of its route. */
529
+ const cut = (flight, cause) => {
530
+ const operation = byName.get(flight.controller).routes[flight.route];
531
+ const since = quantity(Math.max(0, clock.now() - flight.started) / 1e3, "s");
532
+ const retryable = mayReissue(operation, since);
533
+ return {
534
+ kind: "cut",
535
+ status: retryable ? 503 : 500,
536
+ retryable,
537
+ cause
538
+ };
539
+ };
67
540
  return {
68
- type: "debounce",
69
- interval: poll.interval,
70
- failAfter: polls(seconds(timing.failAfter)),
71
- recoverAfter: polls(timing.recoverAfter === void 0 ? void 0 : seconds(timing.recoverAfter))
541
+ request(controller, route, work) {
542
+ const held = byName.get(controller);
543
+ if (held === void 0) throw new Error(`there is no controller ${controller}`);
544
+ if (held.routes[route] === void 0) throw new Error(`controller ${controller} does not declare the route ${route}`);
545
+ const refusal = intake ? refuses(controller) : "the service stopped its intake";
546
+ if (refusal !== null) return Promise.resolve({
547
+ kind: "refused",
548
+ status: 503,
549
+ retryable: true,
550
+ cause: refusal
551
+ });
552
+ return new Promise((resolve) => {
553
+ let answered = false;
554
+ const flight = {
555
+ controller,
556
+ route,
557
+ started: clock.now(),
558
+ abort: new AbortController(),
559
+ answer: (outcome) => {
560
+ if (answered) return;
561
+ answered = true;
562
+ settle(flight);
563
+ resolve(outcome);
564
+ }
565
+ };
566
+ flights.add(flight);
567
+ let promise;
568
+ try {
569
+ promise = Promise.resolve(work(flight.abort.signal));
570
+ } catch (error) {
571
+ promise = Promise.reject(error);
572
+ }
573
+ promise.then((value) => {
574
+ if (answered) return;
575
+ answered = true;
576
+ settle(flight);
577
+ resolve({
578
+ kind: "done",
579
+ value
580
+ });
581
+ }, (error) => {
582
+ if (answered) return;
583
+ const outcome = flight.abort.signal.aborted ? cut(flight, "the shutdown aborted it, and it failed") : {
584
+ kind: "failed",
585
+ error
586
+ };
587
+ answered = true;
588
+ settle(flight);
589
+ resolve(outcome);
590
+ });
591
+ });
592
+ },
593
+ inFlight(controller) {
594
+ let count = 0;
595
+ for (const flight of flights) if (controller === void 0 || flight.controller === controller) count += 1;
596
+ return count;
597
+ },
598
+ stopIntake() {
599
+ intake = false;
600
+ },
601
+ cooperativeAbort() {
602
+ for (const flight of flights) flight.abort.abort(/* @__PURE__ */ new Error("the cooperative abort of the drain"));
603
+ },
604
+ drain(deadline) {
605
+ if (flights.size === 0) return Promise.resolve();
606
+ return new Promise((resolve) => {
607
+ let finished = false;
608
+ const done = () => {
609
+ if (finished) return;
610
+ finished = true;
611
+ idle.delete(done);
612
+ cancel();
613
+ resolve();
614
+ };
615
+ idle.add(done);
616
+ const cancel = clock.after(Math.max(0, deadline - clock.now()), () => {
617
+ for (const flight of [...flights]) {
618
+ flight.abort.abort(/* @__PURE__ */ new Error("the drain reached its deadline"));
619
+ flight.answer(cut(flight, "the drain reached its deadline"));
620
+ }
621
+ done();
622
+ });
623
+ });
624
+ }
72
625
  };
73
626
  };
74
627
  //#endregion
75
- //#region src/description.ts
628
+ //#region src/lifecycle.ts
76
629
  /**
77
- * This is the format of a service as data: what `describe()` exports, and nothing starts. The
78
- * timer algebra and the ordering law read it. Each timer has a unit. Each knob comes from the
79
- * declaration, because the runtime has no defaults. A bound that nobody gives is "no bound", not
80
- * zero.
630
+ * These are the stages of the shutdown, in their sequence. A declared phase with one of these
631
+ * names gets the work of the runtime for its stage. A phase with another name gets only the work of
632
+ * its owners.
633
+ * - `fail readiness`: readiness fails at the start of the shutdown. This phase then holds for its
634
+ * cap, while the endpoints of the platform remove the service.
635
+ * - `stop intake`: each transport stops its intake, and new requests get a retryable 503.
636
+ * - `drain`: the transports and the requests in flight end, until the cap. The cooperative abort
637
+ * comes before the end of the cap, by the cooperative window.
638
+ * - `close transports`: each transport closes.
639
+ * - `stop the poller`: each check and watchdog stops. Then the signal of the service aborts.
640
+ * - `tear down dependencies`: each dependency closes, in the reverse sequence of its connection.
641
+ * - `close the health server`: the probes stop last.
81
642
  */
82
- const FORMAT = "mark1russell7.service/1";
83
- //#endregion
84
- //#region src/operation.ts
643
+ const STAGES = [
644
+ "fail readiness",
645
+ "stop intake",
646
+ "drain",
647
+ "close transports",
648
+ "stop the poller",
649
+ "tear down dependencies",
650
+ "close the health server"
651
+ ];
652
+ /** This function gives the place of a stage in the sequence, or -1 for a phase of another name. */
653
+ const stageRank = (name) => STAGES.indexOf(name);
654
+ /** The signal of the service aborts after the poller stops, and before the dependencies close. */
655
+ const ABORT_AFTER = stageRank("stop the poller");
85
656
  /**
86
- * This function tells if an operation can go again `since` its first issue. An idempotent
87
- * operation can always go again, and other operations cannot. A keyed operation can go again
88
- * while the server keeps its key.
657
+ * This function gives the time of the cooperative abort, in milliseconds after the drain starts. It
658
+ * is the cap less the cooperative window, and not less than half the cap. A window without a bound
659
+ * gives half the cap.
89
660
  */
90
- const mayReissue = (operation, since) => {
91
- const { idempotent } = operation;
92
- if (typeof idempotent === "boolean") return idempotent;
93
- return compare(since, idempotent.window) < 0;
661
+ const cooperativeAbortAt = (cap, window) => {
662
+ const before = window.value === "infinite" ? Number.POSITIVE_INFINITY : valueIn(window, "ms");
663
+ return Math.max(cap - before, cap / 2);
94
664
  };
95
- /** This function gives the problems in the declaration of an operation that `describe()` refuses. */
96
- const operationProblems = (owner, name, operation) => {
97
- const { idempotent } = operation;
98
- if (typeof idempotent === "boolean") return [];
665
+ const message = (error) => error instanceof Error ? error.message : String(error);
666
+ /**
667
+ * This function gives the problems of a declaration that the lifecycle cannot start: a stage with
668
+ * work and no declared phase. The runtime has no defaults, thus a transport without a drain phase
669
+ * has no time to drain.
670
+ */
671
+ const lifecycleProblems = (description, components) => {
672
+ const declared = new Set(description.shutdown.phases.map((phase) => phase.name));
673
+ const needs = (stage, why) => declared.has(stage) ? [] : [`${description.name} has ${why}, thus it declares the phase "${stage}"`];
99
674
  const problems = [];
100
- if (idempotent.key.trim().length === 0) problems.push(`${owner}'s ${name} names no key`);
101
- if (!convertible(idempotent.window.unit, "s")) problems.push(`${owner}'s ${name} holds its key for a span that is not a time`);
675
+ for (const transport of description.transports) if (!components.transports.has(transport.name)) problems.push(`transport ${transport.name} has no implementation: the runtime has no defaults`);
676
+ if (components.transports.size > 0 || description.controllers.length > 0) {
677
+ problems.push(...needs("stop intake", "transports or controllers"));
678
+ problems.push(...needs("drain", "transports or controllers"));
679
+ }
680
+ if (components.transports.size > 0) problems.push(...needs("close transports", "transports"));
681
+ if (description.checks.length > 0) problems.push(...needs("stop the poller", "checks"));
682
+ if (components.dependencies.size > 0) problems.push(...needs("tear down dependencies", "dependency handles"));
683
+ if (components.healthServer !== null) problems.push(...needs("close the health server", "a health server"));
102
684
  return problems;
103
685
  };
686
+ /**
687
+ * This function starts the lifecycle of a service. The startup has a sequence: the health server
688
+ * first, then each dependency, then each transport, and the startup gate last. A signal during the
689
+ * startup gets its shutdown after the startup ends. The shutdown does the declared phases in their
690
+ * sequence, each one capped. Only the lifecycle ends the process: 0 after a clean shutdown, and 1
691
+ * after an error.
692
+ */
693
+ const lifecycle = (description, runtime, components, options = {}) => {
694
+ const clock = options.clock ?? realClock;
695
+ const port = options.process ?? nodeProcess;
696
+ const record = (machine, from, to, on, detail) => options.record?.({
697
+ at: clock.now(),
698
+ machine,
699
+ instance: description.name,
700
+ from,
701
+ to,
702
+ on,
703
+ ...detail === void 0 ? {} : { detail }
704
+ });
705
+ const service = new AbortController();
706
+ const errors = [];
707
+ let state = "starting";
708
+ let exitCode = null;
709
+ let intakeStopped = false;
710
+ let shutdownStarted = false;
711
+ let failedStartup = false;
712
+ let exitState = "running";
713
+ const moveExit = (to, on, detail) => {
714
+ record("exit", exitState, to, on, detail);
715
+ exitState = to;
716
+ };
717
+ const fail = (cause) => {
718
+ errors.push(cause);
719
+ record("exit", exitState, exitState, "error", cause);
720
+ };
721
+ const requests = tracker(description.controllers, clock, (controller) => {
722
+ if (state === "starting") return "the service has not started";
723
+ if (state === "exited") return "the service has exited";
724
+ if (state === "draining") return null;
725
+ return runtime.verdict().controllers[controller] === true ? null : `controller ${controller} is not ready`;
726
+ });
727
+ let finish = () => {};
728
+ const done = new Promise((resolve) => {
729
+ finish = resolve;
730
+ });
731
+ let healthListening = false;
732
+ const connected = [];
733
+ const startedTransports = [];
734
+ const offs = EXIT_SIGNALS.map((signal) => port.on(signal, () => {
735
+ shutdown(signal);
736
+ }));
737
+ const startup = (async () => {
738
+ runtime.beginStartup();
739
+ const started = (what) => record("gate", "starting", "starting", "component.started", what);
740
+ try {
741
+ if (components.healthServer !== null) {
742
+ await components.healthServer.listen(runtime);
743
+ healthListening = true;
744
+ started("the health server");
745
+ }
746
+ for (const dependency of description.dependencies) {
747
+ const handle = components.dependencies.get(dependency.name);
748
+ if (handle === void 0) continue;
749
+ await handle.connect(service.signal);
750
+ connected.push([dependency.name, handle]);
751
+ started(`dependency ${dependency.name}`);
752
+ }
753
+ for (const transport of description.transports) {
754
+ const impl = components.transports.get(transport.name);
755
+ await impl.start(service.signal);
756
+ startedTransports.push([transport.name, impl]);
757
+ started(`${transport.kind} ${transport.name}`);
758
+ }
759
+ await runtime.warmUp();
760
+ if (state === "starting") state = "running";
761
+ } catch (error) {
762
+ const cause = `the startup failed: ${message(error)}`;
763
+ failedStartup = true;
764
+ runtime.failStartup(cause);
765
+ fail(cause);
766
+ if (exitState === "running") moveExit("stopping", "startup.fail", cause);
767
+ else record("exit", exitState, exitState, "startup.fail", cause);
768
+ }
769
+ })();
770
+ startup.then(() => {
771
+ if (failedStartup && !shutdownStarted) shutdown("the startup failed");
772
+ });
773
+ /** This function does the work of one phase, until its cap. It gives true when the work ended in time. */
774
+ const runPhase = async (phase, hold, work) => {
775
+ const cap = valueIn(phase.cap, "ms");
776
+ const deadline = clock.now() + cap;
777
+ const capped = new AbortController();
778
+ const context = {
779
+ deadline,
780
+ signal: capped.signal,
781
+ clock
782
+ };
783
+ let pending = work.length;
784
+ const all = Promise.all(work.map(async (each) => {
785
+ try {
786
+ await each(context);
787
+ } catch (error) {
788
+ fail(`the phase ${phase.name} failed: ${message(error)}`);
789
+ } finally {
790
+ pending -= 1;
791
+ }
792
+ }));
793
+ let cancelCap = () => {};
794
+ const end = new Promise((resolve) => {
795
+ cancelCap = clock.after(cap, () => resolve("cap"));
796
+ });
797
+ if (await Promise.race([all.then(() => "work"), end]) === "work" && hold) await end;
798
+ cancelCap();
799
+ capped.abort(/* @__PURE__ */ new Error(`the phase ${phase.name} reached its cap`));
800
+ return pending === 0;
801
+ };
802
+ async function shutdown(cause = "the service asked for it") {
803
+ if (shutdownStarted || exitState === "exited") {
804
+ if (exitState === "stopping") record("exit", exitState, exitState, "exit.repeat", cause);
805
+ return done;
806
+ }
807
+ shutdownStarted = true;
808
+ if (exitState === "running") moveExit("stopping", "exit.request", cause);
809
+ await startup;
810
+ state = "draining";
811
+ runtime.drain();
812
+ let drainState = "running";
813
+ const moveDrain = (to, on, detail) => {
814
+ record("drain", drainState, to, on, detail);
815
+ drainState = to;
816
+ };
817
+ const abortService = () => {
818
+ if (service.signal.aborted) return;
819
+ service.abort(/* @__PURE__ */ new Error("the service stopped"));
820
+ record("drain", drainState, drainState, "signal.abort");
821
+ };
822
+ const phases = description.shutdown.phases;
823
+ let on = "exit.request";
824
+ for (const phase of phases) {
825
+ const rank = stageRank(phase.name);
826
+ moveDrain(phase.name, on);
827
+ if (rank > ABORT_AFTER) abortService();
828
+ const work = [];
829
+ if (phase.name === "stop intake") {
830
+ work.push(() => {
831
+ intakeStopped = true;
832
+ requests.stopIntake();
833
+ });
834
+ for (const [, impl] of startedTransports) work.push(() => impl.stopIntake());
835
+ }
836
+ let cancelAbort = () => {};
837
+ if (phase.name === "drain") {
838
+ const cap = valueIn(phase.cap, "ms");
839
+ const cooperative = new AbortController();
840
+ const fireAt = cooperativeAbortAt(cap, description.shutdown.cooperativeWindow);
841
+ cancelAbort = clock.after(fireAt, () => {
842
+ cooperative.abort(/* @__PURE__ */ new Error("the cooperative abort of the drain"));
843
+ requests.cooperativeAbort();
844
+ record("drain", drainState, drainState, "drain.cooperative-abort");
845
+ });
846
+ work.push(({ deadline }) => requests.drain(deadline));
847
+ for (const [, impl] of startedTransports) work.push(({ deadline }) => impl.drain(deadline, cooperative.signal));
848
+ }
849
+ if (phase.name === "close transports") for (const [, impl] of startedTransports) work.push(() => impl.close());
850
+ if (phase.name === "stop the poller") work.push(() => {
851
+ runtime.stop();
852
+ components.stopPollers();
853
+ });
854
+ if (phase.name === "tear down dependencies") work.push(async () => {
855
+ for (const [, handle] of [...connected].reverse()) await handle.close();
856
+ });
857
+ if (phase.name === "close the health server" && healthListening && components.healthServer !== null) {
858
+ const server = components.healthServer;
859
+ work.push(() => server.close());
860
+ }
861
+ for (const owned of components.phaseWork.get(phase.name) ?? []) work.push(owned);
862
+ const inTime = await runPhase(phase, phase.name === "fail readiness", work);
863
+ cancelAbort();
864
+ if (!inTime) fail(`the phase ${phase.name} did not end in its cap of ${valueIn(phase.cap, "s")} s`);
865
+ on = inTime ? "phase.done" : "phase.cap";
866
+ }
867
+ moveDrain("done", on);
868
+ abortService();
869
+ runtime.stop();
870
+ components.stopPollers();
871
+ if (!intakeStopped) requests.stopIntake();
872
+ for (const off of offs) off();
873
+ state = "exited";
874
+ exitCode = errors.length > 0 ? 1 : 0;
875
+ moveExit("exited", "shutdown.done", `exit ${exitCode}`);
876
+ finish(exitCode);
877
+ port.exit(exitCode);
878
+ return exitCode;
879
+ }
880
+ return {
881
+ health: runtime,
882
+ get state() {
883
+ return state;
884
+ },
885
+ get exitCode() {
886
+ return exitCode;
887
+ },
888
+ get errors() {
889
+ return [...errors];
890
+ },
891
+ started: startup,
892
+ done,
893
+ signal: service.signal,
894
+ shutdown,
895
+ request: (controller, route, work) => requests.request(controller, route, work),
896
+ inFlight: (controller) => requests.inFlight(controller),
897
+ keepAlive: () => state === "running" && runtime.verdict().ready
898
+ };
899
+ };
900
+ //#endregion
901
+ //#region src/watchdog.ts
902
+ /** This function starts a watchdog on a clock. */
903
+ const watchdog = (clock, spec, trace) => {
904
+ const tick = valueIn(spec.tick, "ms");
905
+ const threshold = valueIn(spec.threshold, "ms");
906
+ if (!(tick > 0)) throw new RangeError(`watchdog ${spec.name} ticks every ${tick} ms, not above zero`);
907
+ let blocked = false;
908
+ let stopped = false;
909
+ let worst = 0;
910
+ let sincePoll = 0;
911
+ let next = null;
912
+ const named = () => stopped ? "stopped" : blocked ? "blocked" : "watching";
913
+ const arm = () => {
914
+ const due = clock.now() + tick;
915
+ next = clock.after(tick, () => {
916
+ next = null;
917
+ if (stopped) return;
918
+ const late = Math.max(0, clock.now() - due);
919
+ worst = Math.max(worst, late);
920
+ sincePoll = Math.max(sincePoll, late);
921
+ const from = named();
922
+ blocked = late > threshold;
923
+ trace?.(from, named(), "tick");
924
+ arm();
925
+ });
926
+ };
927
+ arm();
928
+ return {
929
+ check: () => {
930
+ const failed = blocked || sincePoll > threshold;
931
+ sincePoll = 0;
932
+ return failed ? "error" : "ok";
933
+ },
934
+ get worst() {
935
+ return worst;
936
+ },
937
+ get blocked() {
938
+ return blocked;
939
+ },
940
+ stop() {
941
+ if (stopped) return;
942
+ const from = named();
943
+ stopped = true;
944
+ next?.();
945
+ next = null;
946
+ trace?.(from, "stopped", "stop");
947
+ }
948
+ };
949
+ };
104
950
  //#endregion
105
951
  //#region src/builder.ts
106
952
  const isTime = (amount) => convertible(amount.unit, "s");
953
+ const above = (amount) => amount.value === "infinite" || isTime(amount) && valueIn(amount, "s") > 0;
954
+ const whole = (count) => Number.isInteger(count) && count >= 1;
955
+ /** This function gives the problems of a timing: a zero interval or count fails decoding. */
956
+ const timingProblems = (policy, timing) => {
957
+ if (timing.type === "debounce") {
958
+ const problems = [];
959
+ if (!above(timing.interval)) problems.push(`the ${policy} debounce polls at an interval that is not above zero`);
960
+ if (!whole(timing.failAfter)) problems.push(`the ${policy} debounce fails after ${timing.failAfter} polls, not a whole number above zero`);
961
+ if (!whole(timing.recoverAfter)) problems.push(`the ${policy} debounce recovers after ${timing.recoverAfter} polls, not a whole number above zero`);
962
+ return problems;
963
+ }
964
+ const negative = (amount) => amount !== void 0 && (!isTime(amount) || valueIn(amount, "s") < 0);
965
+ return negative(timing.failAfter) || negative(timing.recoverAfter) ? [`the ${policy} window is not a time of zero or more`] : [];
966
+ };
107
967
  /** This function starts the composition of a service. */
108
968
  const service = (name) => {
109
969
  if (name.trim().length === 0) throw new Error("a service needs a name");
@@ -111,6 +971,7 @@ const service = (name) => {
111
971
  const dependencies = [];
112
972
  const controllers = [];
113
973
  const checks = [];
974
+ const runs = /* @__PURE__ */ new Map();
114
975
  const couplings = [];
115
976
  const phases = [];
116
977
  let readiness;
@@ -119,6 +980,14 @@ const service = (name) => {
119
980
  let cooperative;
120
981
  let started = false;
121
982
  let partial = false;
983
+ let runtime;
984
+ let live;
985
+ let running;
986
+ const transportImpls = /* @__PURE__ */ new Map();
987
+ const handles = /* @__PURE__ */ new Map();
988
+ const phaseWork = /* @__PURE__ */ new Map();
989
+ const watchdogs = [];
990
+ let probes = null;
122
991
  /** This function stops a change to the shape of the service after the start. */
123
992
  const composing = (what) => {
124
993
  if (started) throw new Error(`${name} has started: ${what} is declared before svc.start()`);
@@ -126,7 +995,7 @@ const service = (name) => {
126
995
  const once = (list, entry, what) => {
127
996
  if (list.some((held) => held.name === entry)) throw new Error(`${name} already has a ${what} named ${entry}`);
128
997
  };
129
- const transport = (kind) => (entry, knobs) => {
998
+ const transport = (kind) => (entry, knobs, impl) => {
130
999
  composing(`a ${kind}`);
131
1000
  once(transports, entry, "transport");
132
1001
  transports.push({
@@ -134,19 +1003,67 @@ const service = (name) => {
134
1003
  kind,
135
1004
  knobs: { ...knobs }
136
1005
  });
1006
+ if (impl !== void 0) transportImpls.set(entry, impl);
137
1007
  return builder;
138
1008
  };
1009
+ /** This function makes the runtime of the declaration: its checks, and a watchdog for each spec. */
1010
+ const makeRuntime = (options) => {
1011
+ const description = builder.describe();
1012
+ const clock = options.clock ?? realClock;
1013
+ const dogs = [];
1014
+ const all = new Map(runs);
1015
+ for (const spec of watchdogs) {
1016
+ const dog = watchdog(clock, spec, (from, to, on) => options.record?.({
1017
+ at: clock.now(),
1018
+ machine: "watchdog",
1019
+ instance: spec.name,
1020
+ from,
1021
+ to,
1022
+ on
1023
+ }));
1024
+ dogs.push(dog);
1025
+ all.set(spec.name, () => dog.check());
1026
+ }
1027
+ const stopPollers = () => {
1028
+ for (const dog of dogs) dog.stop();
1029
+ };
1030
+ let made;
1031
+ try {
1032
+ made = healthRuntime(description, all, options);
1033
+ } catch (error) {
1034
+ stopPollers();
1035
+ throw error;
1036
+ }
1037
+ return {
1038
+ runtime: {
1039
+ ...made,
1040
+ stop: () => {
1041
+ made.stop();
1042
+ stopPollers();
1043
+ }
1044
+ },
1045
+ description,
1046
+ stopPollers
1047
+ };
1048
+ };
139
1049
  const builder = {
140
1050
  name,
141
- dependency(entry, spec) {
1051
+ get health() {
1052
+ return live ?? null;
1053
+ },
1054
+ dependency(entry, spec, handle) {
142
1055
  once(dependencies, entry, "dependency");
1056
+ if (handle !== void 0 && !started) handles.set(entry, handle);
143
1057
  dependencies.push({
144
1058
  name: entry,
145
1059
  ...spec,
146
1060
  knobs: { ...spec.knobs },
147
1061
  operations: { ...spec.operations }
148
1062
  });
149
- if (started) partial = true;
1063
+ if (started) {
1064
+ partial = true;
1065
+ live?.addDependency(entry);
1066
+ }
150
1067
  return builder;
151
1068
  },
152
1069
  server: transport("server"),
@@ -176,10 +1093,43 @@ const service = (name) => {
176
1093
  };
177
1094
  return handle;
178
1095
  },
179
- check(check) {
1096
+ check(check, run) {
180
1097
  once(checks, check.name, "check");
1098
+ if (started && run === void 0) throw new Error(`check ${check.name} joins after the start, thus it needs its function`);
181
1099
  checks.push({ ...check });
182
- if (started) partial = true;
1100
+ if (run !== void 0) runs.set(check.name, run);
1101
+ if (started) {
1102
+ partial = true;
1103
+ live?.addCheck({
1104
+ ...check,
1105
+ poll: { ...check.poll }
1106
+ }, run);
1107
+ }
1108
+ return builder;
1109
+ },
1110
+ healthServer(server) {
1111
+ composing("the health server");
1112
+ if (probes !== null) throw new Error(`${name} already has a health server`);
1113
+ probes = server;
1114
+ return builder;
1115
+ },
1116
+ onPhase(phase, work) {
1117
+ composing(`work for the phase ${phase}`);
1118
+ phaseWork.set(phase, [...phaseWork.get(phase) ?? [], work]);
1119
+ return builder;
1120
+ },
1121
+ watchdog(spec) {
1122
+ composing(`the watchdog ${spec.name}`);
1123
+ once(checks, spec.name, "check");
1124
+ checks.push({
1125
+ name: spec.name,
1126
+ dependency: null,
1127
+ step: "local-state",
1128
+ ownConnection: true,
1129
+ gates: spec.gates,
1130
+ poll: { ...spec.poll }
1131
+ });
1132
+ watchdogs.push(spec);
183
1133
  return builder;
184
1134
  },
185
1135
  readiness(timing) {
@@ -226,9 +1176,18 @@ const service = (name) => {
226
1176
  if (missing.length > 0) throw new Error(`${name} does not declare ${missing.join("; ")}: the runtime has no defaults`);
227
1177
  const known = new Set(dependencies.map((entry) => entry.name));
228
1178
  const problems = [];
1179
+ const policies = {
1180
+ readiness,
1181
+ liveness
1182
+ };
1183
+ for (const gate of ["readiness", "liveness"]) problems.push(...timingProblems(gate, policies[gate]));
229
1184
  for (const check of checks) {
230
1185
  if (check.dependency !== null && !known.has(check.dependency)) problems.push(`check ${check.name} exercises ${check.dependency}, which is not a dependency`);
231
1186
  if (!isTime(check.poll.interval) || !isTime(check.poll.timeout)) problems.push(`check ${check.name} polls in a unit that is not a time`);
1187
+ else if (!above(check.poll.interval) || !above(check.poll.timeout)) problems.push(`check ${check.name} polls with an interval or a timeout that is not above zero`);
1188
+ if (!(check.poll.jitter >= 0 && check.poll.jitter < 1)) problems.push(`check ${check.name} has a jitter of ${check.poll.jitter}, not a fraction below 1`);
1189
+ const timing = check.gates === null ? null : policies[check.gates];
1190
+ if (timing?.type === "debounce" && isTime(check.poll.interval) && above(timing.interval) && valueIn(check.poll.interval, "s") !== valueIn(timing.interval, "s")) problems.push(`check ${check.name} polls every ${valueIn(check.poll.interval, "s")} s, but the ${check.gates} debounce counts polls every ${valueIn(timing.interval, "s")} s`);
232
1191
  }
233
1192
  for (const controller of controllers) {
234
1193
  for (const needed of controller.requires) if (!known.has(needed)) problems.push(`controller ${controller.name} requires ${needed}, which is not a dependency`);
@@ -236,6 +1195,8 @@ const service = (name) => {
236
1195
  }
237
1196
  for (const dependency of dependencies) for (const [name, operation] of Object.entries(dependency.operations)) problems.push(...operationProblems(`dependency ${dependency.name}`, name, operation));
238
1197
  for (const phase of phases) if (!isTime(phase.cap)) problems.push(`phase ${phase.name}'s cap is not a time`);
1198
+ const ranked = phases.map((phase) => phase.name).filter((each) => stageRank(each) >= 0);
1199
+ for (let at = 1; at < ranked.length; at += 1) if (stageRank(ranked[at]) < stageRank(ranked[at - 1])) problems.push(`the phase "${ranked[at]}" comes after "${ranked[at - 1]}", but the stages go ${STAGES.join(", ")}`);
239
1200
  if (problems.length > 0) throw new Error(`${name}: ${problems.join("; ")}`);
240
1201
  const controllersOut = controllers.map((entry) => ({
241
1202
  name: entry.name,
@@ -272,10 +1233,47 @@ const service = (name) => {
272
1233
  }
273
1234
  };
274
1235
  },
275
- start() {
276
- builder.describe();
1236
+ start(options = {}) {
1237
+ if (runtime !== void 0) return runtime;
1238
+ if (running !== void 0) return Promise.reject(/* @__PURE__ */ new Error(`${name} runs its lifecycle: use its health`));
1239
+ let made;
1240
+ try {
1241
+ made = makeRuntime(options).runtime;
1242
+ } catch (error) {
1243
+ return Promise.reject(error);
1244
+ }
277
1245
  started = true;
278
- return Promise.resolve();
1246
+ live = made;
1247
+ runtime = made.warmUp().then(() => made);
1248
+ return runtime;
1249
+ },
1250
+ run(options = {}) {
1251
+ if (running !== void 0) return running;
1252
+ if (runtime !== void 0) return Promise.reject(/* @__PURE__ */ new Error(`${name} started its health only: run() starts the whole lifecycle`));
1253
+ let life;
1254
+ try {
1255
+ const description = builder.describe();
1256
+ const components = {
1257
+ transports: transportImpls,
1258
+ dependencies: handles,
1259
+ healthServer: probes,
1260
+ phaseWork,
1261
+ stopPollers: () => {}
1262
+ };
1263
+ const problems = lifecycleProblems(description, components);
1264
+ if (problems.length > 0) throw new Error(`${name}: ${problems.join("; ")}`);
1265
+ const made = makeRuntime(options);
1266
+ started = true;
1267
+ live = made.runtime;
1268
+ life = lifecycle(made.description, made.runtime, {
1269
+ ...components,
1270
+ stopPollers: made.stopPollers
1271
+ }, options);
1272
+ } catch (error) {
1273
+ return Promise.reject(error);
1274
+ }
1275
+ running = life.started.then(() => life);
1276
+ return running;
279
1277
  }
280
1278
  };
281
1279
  return builder;
@@ -448,6 +1446,456 @@ const lint = (svc, scenario, rules) => {
448
1446
  return findings;
449
1447
  };
450
1448
  //#endregion
451
- export { FORMAT, SERVICE_RULES, asDebounce, asWindow, evaluate, failSpan, inequalities, lint, mayReissue, recoverSpan, service, serviceBindings, symbolsOf, verdictOf };
1449
+ //#region src/machines.ts
1450
+ /**
1451
+ * The machines of the runtime as `mark1russell7.machine/1` documents. The documents are the
1452
+ * specification, and TypeScript implements them. A test records the transitions of the runtime
1453
+ * and replays them against each document, thus the code and the documents cannot disagree.
1454
+ */
1455
+ const SOURCE = [{
1456
+ ref: "packages/service/src (the runtime implements this document)",
1457
+ rung: "implemented"
1458
+ }];
1459
+ const base = (id, title) => ({
1460
+ format: "mark1russell7.machine/1",
1461
+ id: `service.machine.${id}`,
1462
+ title,
1463
+ layer: "self",
1464
+ version: "0.2.0",
1465
+ sources: SOURCE
1466
+ });
1467
+ const t = (id, from, on, to, extra = {}) => ({
1468
+ id,
1469
+ from,
1470
+ on,
1471
+ to,
1472
+ ...extra
1473
+ });
1474
+ /**
1475
+ * The startup gate opens one time. The lifecycle moves it from `closed` to `starting` while its
1476
+ * components start. Then one warm-up sweep of each check opens it. A component that does not start
1477
+ * fails it for good.
1478
+ */
1479
+ const GATE_MACHINE = {
1480
+ ...base("gate", "The startup gate"),
1481
+ states: {
1482
+ closed: { initial: true },
1483
+ starting: { note: "the health server, then the dependencies, then the transports" },
1484
+ sweeping: { note: "one bounded warm-up sweep of each check" },
1485
+ open: { final: true },
1486
+ failed: { final: true }
1487
+ },
1488
+ events: {
1489
+ "startup.begin": { kind: "local" },
1490
+ "component.started": { kind: "local" },
1491
+ "mark-ready": { kind: "local" },
1492
+ "sweep.done": { kind: "local" },
1493
+ "startup.fail": { kind: "local" },
1494
+ "startup.over-budget": { kind: "timer" }
1495
+ },
1496
+ knobs: { startup: {
1497
+ key: "policies.startup",
1498
+ scope: "service",
1499
+ unit: "s",
1500
+ default: null
1501
+ } },
1502
+ timers: { startup_budget: {
1503
+ duration: {
1504
+ knob: "startup",
1505
+ unit: "s"
1506
+ },
1507
+ arms: ["enter:closed"],
1508
+ cancels: ["sweep.done", "startup.fail"],
1509
+ fires: "startup.over-budget",
1510
+ note: "the gate says that it is over its budget, and the startup probe of the platform acts on it"
1511
+ } },
1512
+ transitions: [
1513
+ t("begin", "closed", "startup.begin", "starting"),
1514
+ t("component", "starting", "component.started", "starting"),
1515
+ t("mark-ready", ["closed", "starting"], "mark-ready", "sweeping"),
1516
+ t("open", "sweeping", "sweep.done", "open"),
1517
+ t("fail", [
1518
+ "closed",
1519
+ "starting",
1520
+ "sweeping"
1521
+ ], "startup.fail", "failed"),
1522
+ t("over-budget-closed", "closed", "startup.over-budget", "closed", { timer: "startup_budget" }),
1523
+ t("over-budget-starting", "starting", "startup.over-budget", "starting", { timer: "startup_budget" }),
1524
+ t("over-budget-sweeping", "sweeping", "startup.over-budget", "sweeping", { timer: "startup_budget" })
1525
+ ]
1526
+ };
1527
+ /** A debounce latch counts the polls of its check in sequence. A readiness latch starts tripped. */
1528
+ const LATCH_MACHINE = {
1529
+ ...base("latch", "The latch of a policy, as a debounce"),
1530
+ states: {
1531
+ tripped: {
1532
+ initial: true,
1533
+ note: "a readiness latch starts here; a liveness latch starts released"
1534
+ },
1535
+ released: {}
1536
+ },
1537
+ events: {
1538
+ "poll.ok": { kind: "local" },
1539
+ "poll.fail": { kind: "local" }
1540
+ },
1541
+ knobs: {
1542
+ fail_after: {
1543
+ key: "policies.<gate>.failAfter",
1544
+ scope: "check",
1545
+ unit: "count",
1546
+ default: null
1547
+ },
1548
+ recover_after: {
1549
+ key: "policies.<gate>.recoverAfter",
1550
+ scope: "check",
1551
+ unit: "count",
1552
+ default: null
1553
+ }
1554
+ },
1555
+ timers: {},
1556
+ transitions: [
1557
+ t("pass", "released", "poll.ok", "released"),
1558
+ t("fail-count", "released", "poll.fail", "released", { guard: "failures in sequence < fail_after" }),
1559
+ t("trip", "released", "poll.fail", "tripped", { guard: "failures in sequence >= fail_after" }),
1560
+ t("fail", "tripped", "poll.fail", "tripped"),
1561
+ t("pass-count", "tripped", "poll.ok", "tripped", { guard: "passes in sequence < recover_after" }),
1562
+ t("release", "tripped", "poll.ok", "released", { guard: "passes in sequence >= recover_after" })
1563
+ ]
1564
+ };
1565
+ /**
1566
+ * A window latch measures time on the monotonic clock from the first failure of a sequence, and
1567
+ * from the first pass after a trip.
1568
+ */
1569
+ const WINDOW_MACHINE = {
1570
+ ...base("window", "The latch of a policy, as a window"),
1571
+ states: {
1572
+ released: {
1573
+ initial: true,
1574
+ note: "a liveness latch starts here; a readiness latch starts tripped"
1575
+ },
1576
+ failing: { note: "the fail window runs" },
1577
+ tripped: {},
1578
+ recovering: { note: "the recover window runs" }
1579
+ },
1580
+ events: {
1581
+ "poll.ok": { kind: "local" },
1582
+ "poll.fail": { kind: "local" },
1583
+ "window.failed": { kind: "timer" },
1584
+ "window.recovered": { kind: "timer" }
1585
+ },
1586
+ knobs: {
1587
+ fail_after: {
1588
+ key: "policies.<gate>.failAfter",
1589
+ scope: "check",
1590
+ unit: "s",
1591
+ default: null
1592
+ },
1593
+ recover_after: {
1594
+ key: "policies.<gate>.recoverAfter",
1595
+ scope: "check",
1596
+ unit: "s",
1597
+ default: null
1598
+ }
1599
+ },
1600
+ timers: {
1601
+ fail_window: {
1602
+ duration: {
1603
+ knob: "fail_after",
1604
+ unit: "s"
1605
+ },
1606
+ arms: ["enter:failing"],
1607
+ cancels: ["poll.ok"],
1608
+ fires: "window.failed"
1609
+ },
1610
+ recover_window: {
1611
+ duration: {
1612
+ knob: "recover_after",
1613
+ unit: "s"
1614
+ },
1615
+ arms: ["enter:recovering"],
1616
+ cancels: ["poll.fail"],
1617
+ fires: "window.recovered"
1618
+ }
1619
+ },
1620
+ transitions: [
1621
+ t("pass", "released", "poll.ok", "released"),
1622
+ t("start-failing", "released", "poll.fail", "failing", { guard: "fail_after > 0" }),
1623
+ t("trip-at-once", "released", "poll.fail", "tripped", { guard: "fail_after = 0" }),
1624
+ t("still-failing", "failing", "poll.fail", "failing"),
1625
+ t("stop-failing", "failing", "poll.ok", "released"),
1626
+ t("trip", "failing", "window.failed", "tripped", { timer: "fail_window" }),
1627
+ t("fail", "tripped", "poll.fail", "tripped"),
1628
+ t("release-at-once", "tripped", "poll.ok", "released", { guard: "recover_after is absent or 0" }),
1629
+ t("start-recovering", "tripped", "poll.ok", "recovering", { guard: "recover_after > 0" }),
1630
+ t("still-recovering", "recovering", "poll.ok", "recovering"),
1631
+ t("stop-recovering", "recovering", "poll.fail", "tripped"),
1632
+ t("release", "recovering", "window.recovered", "released", { timer: "recover_window" })
1633
+ ]
1634
+ };
1635
+ /**
1636
+ * A poller starts one check on a jittered loop with a fixed delay. A call that outlives its timeout
1637
+ * stays pending (`hung`), and the poller does not start the check again until the call settles.
1638
+ */
1639
+ const POLLER_MACHINE = {
1640
+ ...base("poller", "The poller of a check"),
1641
+ states: {
1642
+ idle: {
1643
+ initial: true,
1644
+ note: "before the loop starts"
1645
+ },
1646
+ waiting: { note: "the loop timer runs, and no call is pending" },
1647
+ calling: { note: "a call runs, in its timeout" },
1648
+ hung: { note: "a call outlived its timeout, and it is still pending" },
1649
+ stopped: { final: true }
1650
+ },
1651
+ events: {
1652
+ start: { kind: "local" },
1653
+ sweep: { kind: "local" },
1654
+ tick: { kind: "timer" },
1655
+ answer: { kind: "local" },
1656
+ timeout: { kind: "timer" },
1657
+ "late-answer": { kind: "local" },
1658
+ stop: { kind: "local" }
1659
+ },
1660
+ knobs: {
1661
+ interval: {
1662
+ key: "checks.<name>.poll.interval",
1663
+ scope: "check",
1664
+ unit: "s",
1665
+ default: null
1666
+ },
1667
+ timeout: {
1668
+ key: "checks.<name>.poll.timeout",
1669
+ scope: "check",
1670
+ unit: "s",
1671
+ default: null
1672
+ }
1673
+ },
1674
+ timers: {
1675
+ loop: {
1676
+ duration: {
1677
+ knob: "interval",
1678
+ unit: "s"
1679
+ },
1680
+ arms: ["start", "tick"],
1681
+ cancels: ["stop"],
1682
+ fires: "tick",
1683
+ note: "the interval, with its jitter: a fixed delay from the start of each poll"
1684
+ },
1685
+ poll_timeout: {
1686
+ duration: {
1687
+ knob: "timeout",
1688
+ unit: "s"
1689
+ },
1690
+ arms: ["enter:calling"],
1691
+ cancels: ["answer", "stop"],
1692
+ fires: "timeout"
1693
+ }
1694
+ },
1695
+ transitions: [
1696
+ t("sweep", "idle", "sweep", "calling"),
1697
+ t("start", "idle", "start", "waiting"),
1698
+ t("start-while-calling", "calling", "start", "calling", { note: "a late check polls at once, then starts its loop" }),
1699
+ t("start-while-hung", "hung", "start", "hung"),
1700
+ t("tick", "waiting", "tick", "calling", { guard: "the force-fail list does not name the check" }),
1701
+ t("tick-forced", "waiting", "tick", "waiting", { guard: "the force-fail list names the check" }),
1702
+ t("tick-pending", "calling", "tick", "calling", { note: "the poll fails at once: its call is pending" }),
1703
+ t("tick-hung", "hung", "tick", "hung", { note: "the poll fails at once: its call is pending" }),
1704
+ t("answer-loop", "calling", "answer", "waiting", { guard: "the loop runs" }),
1705
+ t("answer-sweep", "calling", "answer", "idle", { guard: "the loop has not started" }),
1706
+ t("timeout", "calling", "timeout", "hung", { timer: "poll_timeout" }),
1707
+ t("late-answer-loop", "hung", "late-answer", "waiting", { guard: "the loop runs" }),
1708
+ t("late-answer-sweep", "hung", "late-answer", "idle", { guard: "the loop has not started" }),
1709
+ t("stop", [
1710
+ "idle",
1711
+ "waiting",
1712
+ "calling",
1713
+ "hung"
1714
+ ], "stop", "stopped"),
1715
+ t("settle-after-stop", "stopped", [
1716
+ "answer",
1717
+ "late-answer",
1718
+ "timeout"
1719
+ ], "stopped")
1720
+ ]
1721
+ };
1722
+ /**
1723
+ * The exit manager is the only part that ends the process. The first exit request starts the
1724
+ * shutdown, and a later one has no effect. An error makes the exit code 1.
1725
+ */
1726
+ const EXIT_MACHINE = {
1727
+ ...base("exit", "The exit manager"),
1728
+ states: {
1729
+ running: { initial: true },
1730
+ stopping: {},
1731
+ exited: { final: true }
1732
+ },
1733
+ events: {
1734
+ "exit.request": {
1735
+ kind: "app",
1736
+ note: "SIGINT, SIGTERM, SIGQUIT or shutdown()"
1737
+ },
1738
+ "exit.repeat": { kind: "app" },
1739
+ "startup.fail": { kind: "local" },
1740
+ error: { kind: "local" },
1741
+ "shutdown.done": { kind: "local" }
1742
+ },
1743
+ knobs: {},
1744
+ timers: {},
1745
+ transitions: [
1746
+ t("request", "running", "exit.request", "stopping"),
1747
+ t("startup-fail", "running", "startup.fail", "stopping"),
1748
+ t("error-running", "running", "error", "running"),
1749
+ t("repeat", "stopping", "exit.repeat", "stopping"),
1750
+ t("startup-fail-stopping", "stopping", "startup.fail", "stopping"),
1751
+ t("error", "stopping", "error", "stopping"),
1752
+ t("exit", "stopping", "shutdown.done", "exited", { note: "the exit code is 1 after an error, else 0" })
1753
+ ]
1754
+ };
1755
+ /** A watchdog measures how late its own tick comes, and a late tick shows a blocked event loop. */
1756
+ const WATCHDOG_MACHINE = {
1757
+ ...base("watchdog", "The watchdog of the event loop"),
1758
+ states: {
1759
+ watching: { initial: true },
1760
+ blocked: {},
1761
+ stopped: { final: true }
1762
+ },
1763
+ events: {
1764
+ tick: { kind: "timer" },
1765
+ stop: { kind: "local" }
1766
+ },
1767
+ knobs: {
1768
+ tick: {
1769
+ key: "watchdog.tick",
1770
+ scope: "process",
1771
+ unit: "s",
1772
+ default: null
1773
+ },
1774
+ threshold: {
1775
+ key: "watchdog.threshold",
1776
+ scope: "process",
1777
+ unit: "s",
1778
+ default: null
1779
+ }
1780
+ },
1781
+ timers: { tick: {
1782
+ duration: {
1783
+ knob: "tick",
1784
+ unit: "s"
1785
+ },
1786
+ arms: ["enter:watching", "tick"],
1787
+ cancels: ["stop"],
1788
+ fires: "tick"
1789
+ } },
1790
+ transitions: [
1791
+ t("on-time", "watching", "tick", "watching", { guard: "lateness <= threshold" }),
1792
+ t("block", "watching", "tick", "blocked", { guard: "lateness > threshold" }),
1793
+ t("still-blocked", "blocked", "tick", "blocked", { guard: "lateness > threshold" }),
1794
+ t("unblock", "blocked", "tick", "watching", { guard: "lateness <= threshold" }),
1795
+ t("stop", ["watching", "blocked"], "stop", "stopped")
1796
+ ]
1797
+ };
1798
+ /** This function gives the name of a knob for the cap of a phase: `drain` gives `drain_cap`. */
1799
+ const capKnob = (phase) => `${phase.replace(/[^A-Za-z0-9]+/g, "_")}_cap`;
1800
+ /**
1801
+ * This function gives the drain of a declaration as a machine. It has one state for each declared
1802
+ * phase, in its sequence, and each state has a timer at its cap. The drain also has the cooperative abort: the
1803
+ * cap of the drain less the cooperative window, and half that cap at the least.
1804
+ */
1805
+ const drainMachine = (description) => {
1806
+ const phases = description.shutdown.phases;
1807
+ const states = { running: { initial: true } };
1808
+ for (const phase of phases) states[phase.name] = {};
1809
+ states["done"] = { final: true };
1810
+ const knobs = Object.fromEntries([...phases.map((phase) => [capKnob(phase.name), {
1811
+ key: `shutdown.phases.${phase.name}.cap`,
1812
+ scope: "service",
1813
+ unit: "s",
1814
+ default: null
1815
+ }]), ["cooperative_window", {
1816
+ key: "shutdown.cooperativeWindow",
1817
+ scope: "service",
1818
+ unit: "s",
1819
+ default: null
1820
+ }]]);
1821
+ const timers = {};
1822
+ for (const phase of phases) timers[`${capKnob(phase.name)}_timer`] = {
1823
+ duration: {
1824
+ knob: capKnob(phase.name),
1825
+ unit: "s"
1826
+ },
1827
+ arms: [`enter:${phase.name}`],
1828
+ cancels: ["phase.done"],
1829
+ fires: "phase.cap"
1830
+ };
1831
+ if (phases.some((phase) => phase.name === "drain")) timers["cooperative_abort"] = {
1832
+ duration: {
1833
+ expr: "max(drain_cap - cooperative_window, drain_cap / 2)",
1834
+ unit: "s"
1835
+ },
1836
+ arms: ["enter:drain"],
1837
+ cancels: ["phase.done", "phase.cap"],
1838
+ fires: "drain.cooperative-abort"
1839
+ };
1840
+ const transitions = [];
1841
+ const order = [
1842
+ "running",
1843
+ ...phases.map((phase) => phase.name),
1844
+ "done"
1845
+ ];
1846
+ for (let at = 0; at + 1 < order.length; at += 1) {
1847
+ const from = order[at];
1848
+ const to = order[at + 1];
1849
+ transitions.push(at === 0 ? t(`begin`, from, "exit.request", to) : t(`after-${from}`, from, ["phase.done", "phase.cap"], to, {
1850
+ timer: `${capKnob(from)}_timer`,
1851
+ note: "phase.cap: the work of the phase did not end in its cap, and the exit code is 1"
1852
+ }));
1853
+ }
1854
+ if (phases.some((phase) => phase.name === "drain")) transitions.push(t("cooperative-abort", "drain", "drain.cooperative-abort", "drain", { timer: "cooperative_abort" }));
1855
+ for (const state of [...phases.map((phase) => phase.name), "done"]) transitions.push(t(`signal-abort-${state}`, state, "signal.abort", state));
1856
+ return {
1857
+ ...base("drain", `The drain of ${description.name}`),
1858
+ states,
1859
+ events: {
1860
+ "exit.request": { kind: "app" },
1861
+ "phase.done": { kind: "local" },
1862
+ "phase.cap": { kind: "timer" },
1863
+ "drain.cooperative-abort": { kind: "timer" },
1864
+ "signal.abort": { kind: "local" }
1865
+ },
1866
+ knobs,
1867
+ timers,
1868
+ transitions
1869
+ };
1870
+ };
1871
+ /** These are the static machines of the runtime, by the name that their events carry. */
1872
+ const MACHINES = {
1873
+ gate: GATE_MACHINE,
1874
+ latch: LATCH_MACHINE,
1875
+ window: WINDOW_MACHINE,
1876
+ poller: POLLER_MACHINE,
1877
+ exit: EXIT_MACHINE,
1878
+ watchdog: WATCHDOG_MACHINE
1879
+ };
1880
+ /**
1881
+ * This function replays the transitions of one machine of the runtime against its document. Each
1882
+ * instance starts in the state that its first transition leaves. Each transition must leave the
1883
+ * state where the replay is. The document must have a transition from that state, on that event,
1884
+ * to that state. The function gives each disagreement.
1885
+ */
1886
+ const replay = (doc, events) => {
1887
+ const problems = [];
1888
+ const at = /* @__PURE__ */ new Map();
1889
+ for (const event of events) {
1890
+ const where = at.get(event.instance) ?? event.from;
1891
+ if (doc.states[where] === void 0) problems.push(`${event.instance}: the state ${where} is not in ${doc.id}`);
1892
+ if (event.from !== where) problems.push(`${event.instance}: the runtime left ${event.from} on ${event.on}, but the replay is in ${where}`);
1893
+ if (!doc.transitions.some((each) => fromStates(doc, each.from).includes(event.from) && arr(each.on).includes(event.on) && each.to === event.to)) problems.push(`${event.instance}: ${doc.id} has no transition from ${event.from} on ${event.on} to ${event.to}`);
1894
+ at.set(event.instance, event.to);
1895
+ }
1896
+ return problems;
1897
+ };
1898
+ //#endregion
1899
+ export { EXIT_MACHINE, EXIT_SIGNALS, FORMAT, GATE_MACHINE, LATCH_MACHINE, MACHINES, POLLER_MACHINE, SERVICE_RULES, STAGES, WATCHDOG_MACHINE, WINDOW_MACHINE, asDebounce, asWindow, capKnob, cooperativeAbortAt, drainMachine, evaluate, failSpan, inequalities, lint, mayReissue, nodeProcess, realClock, recoverSpan, replay, service, serviceBindings, stageRank, symbolsOf, verdictOf, watchdog };
452
1900
 
453
1901
  //# sourceMappingURL=index.js.map