@mark1russell7/service 0.5.10 → 0.5.12

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
@@ -720,6 +720,248 @@ const compilePattern = (pattern) => {
720
720
  };
721
721
  };
722
722
  //#endregion
723
+ //#region src/units.ts
724
+ /** This function gives the cause of an abort as text. */
725
+ const causeOf = (signal) => signal.reason instanceof Error ? signal.reason.message : String(signal.reason ?? "the unit got an abort");
726
+ /**
727
+ * This function does one unit of work, and gives its end. With a request, the unit goes through
728
+ * `life.request`, thus the service counts it, drains it and can cut it. A throw after the abort of
729
+ * the transport is a cut, not a failure (S6 A10). A throw after the bound is a failure.
730
+ */
731
+ const runUnit = async (run) => {
732
+ const { clock, bound, abort, life, request, unit, work } = run;
733
+ const timeout = new AbortController();
734
+ const cancel = Number.isFinite(bound) ? clock.after(bound, () => timeout.abort(/* @__PURE__ */ new Error(`the unit did not end in its unit_processing_time of ${bound} ms`))) : () => {};
735
+ const call = async (tracker) => work(AbortSignal.any(tracker === void 0 ? [abort.signal, timeout.signal] : [
736
+ abort.signal,
737
+ timeout.signal,
738
+ tracker
739
+ ]));
740
+ const thrown = (error) => {
741
+ if (abort.signal.aborted) return {
742
+ kind: "cut",
743
+ cause: causeOf(abort.signal),
744
+ retryable: false
745
+ };
746
+ if (timeout.signal.aborted) return {
747
+ kind: "failed",
748
+ error: timeout.signal.reason
749
+ };
750
+ return {
751
+ kind: "failed",
752
+ error
753
+ };
754
+ };
755
+ try {
756
+ if (request === void 0) try {
757
+ return {
758
+ kind: "done",
759
+ value: await call()
760
+ };
761
+ } catch (error) {
762
+ return thrown(error);
763
+ }
764
+ let collected;
765
+ try {
766
+ const route = typeof request.route === "string" ? request.route : request.route(unit);
767
+ const detail = request.detail?.(unit);
768
+ collected = life.request(request.controller, route, (signal) => call(signal).then((value) => ({ value })), detail === void 0 ? {} : { detail }).collect();
769
+ } catch (error) {
770
+ return {
771
+ kind: "failed",
772
+ error
773
+ };
774
+ }
775
+ const { items, end } = await collected;
776
+ if (end.kind === "done") return {
777
+ kind: "done",
778
+ value: items[0]?.value
779
+ };
780
+ if (end.kind === "failed") return thrown(end.error);
781
+ if (end.kind === "refused") return {
782
+ kind: "refused",
783
+ cause: end.cause
784
+ };
785
+ return {
786
+ kind: "cut",
787
+ cause: end.cause,
788
+ retryable: end.retryable
789
+ };
790
+ } finally {
791
+ cancel();
792
+ }
793
+ };
794
+ /** This function makes a flight set on a clock. */
795
+ const flights = (clock) => {
796
+ const units = /* @__PURE__ */ new Set();
797
+ const idle = /* @__PURE__ */ new Set();
798
+ const lower = /* @__PURE__ */ new Set();
799
+ let aborted = null;
800
+ const settle = () => {
801
+ for (const done of [...lower]) done();
802
+ lower.clear();
803
+ if (units.size === 0) for (const done of [...idle]) done();
804
+ };
805
+ const abort = (reason) => {
806
+ aborted ??= { reason };
807
+ for (const unit of [...units]) if (!unit.signal.aborted) unit.abort(aborted.reason);
808
+ };
809
+ return {
810
+ get size() {
811
+ return units.size;
812
+ },
813
+ add(unit) {
814
+ units.add(unit);
815
+ if (aborted !== null && !unit.signal.aborted) unit.abort(aborted.reason);
816
+ return () => {
817
+ if (units.delete(unit)) settle();
818
+ };
819
+ },
820
+ abort,
821
+ clear() {
822
+ units.clear();
823
+ settle();
824
+ },
825
+ drain(deadline, signal) {
826
+ const cooperative = () => abort(signal.reason);
827
+ if (signal.aborted) cooperative();
828
+ else signal.addEventListener("abort", cooperative, { once: true });
829
+ if (units.size === 0 || deadline <= clock.now()) {
830
+ signal.removeEventListener("abort", cooperative);
831
+ return Promise.resolve();
832
+ }
833
+ return new Promise((resolve) => {
834
+ let cancel = () => {};
835
+ const done = () => {
836
+ idle.delete(done);
837
+ cancel();
838
+ signal.removeEventListener("abort", cooperative);
839
+ resolve();
840
+ };
841
+ idle.add(done);
842
+ cancel = clock.after(Math.max(0, deadline - clock.now()), done);
843
+ });
844
+ },
845
+ less() {
846
+ return new Promise((resolve) => lower.add(resolve));
847
+ },
848
+ release() {
849
+ for (const done of [...lower]) done();
850
+ lower.clear();
851
+ }
852
+ };
853
+ };
854
+ /** This function makes a sleeper on a clock. */
855
+ const sleeper = (clock) => {
856
+ let wakes = 0;
857
+ let stopped = false;
858
+ const sleeps = /* @__PURE__ */ new Set();
859
+ const backoffs = /* @__PURE__ */ new Set();
860
+ const wait = (ms, into) => stopped ? Promise.resolve() : new Promise((resolve) => {
861
+ let cancel = () => {};
862
+ const done = () => {
863
+ if (!into.delete(done)) return;
864
+ cancel();
865
+ resolve();
866
+ };
867
+ into.add(done);
868
+ if (Number.isFinite(ms)) cancel = clock.after(ms, done);
869
+ });
870
+ return {
871
+ get wakes() {
872
+ return wakes;
873
+ },
874
+ sleep(ms, since) {
875
+ if (since !== wakes) return Promise.resolve();
876
+ return wait(ms, sleeps);
877
+ },
878
+ backoff(ms) {
879
+ return wait(ms, backoffs);
880
+ },
881
+ wake() {
882
+ wakes += 1;
883
+ const ended = sleeps.size;
884
+ for (const done of [...sleeps]) done();
885
+ return ended;
886
+ },
887
+ stop() {
888
+ stopped = true;
889
+ for (const done of [...sleeps, ...backoffs]) done();
890
+ }
891
+ };
892
+ };
893
+ /** This function waits for a work until a bound on the clock, in milliseconds. */
894
+ const settleWithin = (clock, ms, work) => new Promise((resolve) => {
895
+ const cancel = Number.isFinite(ms) ? clock.after(ms, () => resolve({ kind: "bound" })) : () => {};
896
+ work.then(() => {
897
+ cancel();
898
+ resolve({ kind: "done" });
899
+ }, (error) => {
900
+ cancel();
901
+ resolve({
902
+ kind: "failed",
903
+ error
904
+ });
905
+ });
906
+ });
907
+ /**
908
+ * This function gives the value of a promise, or `stop` when the signal aborts first. Each wait adds
909
+ * its own listener, and removes it when the promise settles. A race against one pending promise
910
+ * keeps each settled value until that promise ends.
911
+ */
912
+ const untilAbort = (promise, signal, stop) => {
913
+ if (signal.aborted) return Promise.race([promise, Promise.resolve(stop)]);
914
+ return new Promise((resolve, reject) => {
915
+ const onAbort = () => resolve(stop);
916
+ signal.addEventListener("abort", onAbort, { once: true });
917
+ promise.then((value) => {
918
+ signal.removeEventListener("abort", onAbort);
919
+ resolve(value);
920
+ }, (error) => {
921
+ signal.removeEventListener("abort", onAbort);
922
+ reject(error);
923
+ });
924
+ });
925
+ };
926
+ /**
927
+ * This function gives the backoff of the knobs of a loop. The first delay is `retry_base`, and each
928
+ * delay is two times longer, up to `retry_cap`. Each delay has a jitter up to the base, as in the Redis client.
929
+ */
930
+ const retryOf = (knobs, owner) => {
931
+ const base = boundedMillisecondsOf(knobs, "retry_base", owner);
932
+ const backoff = {
933
+ base,
934
+ factor: 2,
935
+ cap: boundedMillisecondsOf(knobs, "retry_cap", owner),
936
+ jitter: {
937
+ kind: "add",
938
+ amount: base
939
+ }
940
+ };
941
+ const problems = backoffProblems(backoff);
942
+ if (problems.length > 0) throw new Error(`${owner}: the retry_base and retry_cap give no backoff: ${problems.join(", ")}`);
943
+ return backoff;
944
+ };
945
+ /**
946
+ * This function gives the problems of the bounds of a loop in a declaration. The close bound fits
947
+ * in the cap of `close transports`. The bound of a unit fits in the cap of `drain`, or the drain can cut a unit (S6 A10).
948
+ */
949
+ const boundProblems = (owner, closeBound, unitBound, description) => {
950
+ const capOf = (name) => {
951
+ const phase = description.shutdown.phases.find((each) => each.name === name);
952
+ return phase === void 0 ? void 0 : valueIn(phase.cap, "ms");
953
+ };
954
+ const found = [];
955
+ const close = capOf("close transports");
956
+ if (close === void 0) found.push(`${owner} needs the phase "close transports" to close`);
957
+ else if (closeBound >= close) found.push(`the close_bound of ${owner}, ${closeBound} ms, does not fit in the cap of "close transports", ${close} ms`);
958
+ const drain = capOf("drain");
959
+ if (drain === void 0) found.push(`${owner} needs the phase "drain" to end its units`);
960
+ else if (!Number.isFinite(unitBound)) found.push(`the unit_processing_time of ${owner} has no bound, thus the drain can cut a unit at its cap of ${drain} ms`);
961
+ else if (unitBound >= drain) found.push(`the unit_processing_time of ${owner}, ${unitBound} ms, does not fit in the cap of "drain", ${drain} ms`);
962
+ return found;
963
+ };
964
+ //#endregion
723
965
  //#region src/requests.ts
724
966
  const isAsyncIterable$1 = (value) => value !== null && (typeof value === "object" || typeof value === "function") && typeof value[Symbol.asyncIterator] === "function";
725
967
  const isPromiseLike = (value) => value !== null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
@@ -815,10 +1057,8 @@ const tracker = (controllers, clock, refuses, choices = /* @__PURE__ */ new Map(
815
1057
  let returned = false;
816
1058
  let source = null;
817
1059
  let unary = null;
818
- let interrupt = () => {};
819
- const interrupted = new Promise((resolve) => {
820
- interrupt = () => resolve("interrupted");
821
- });
1060
+ const interruption = new AbortController();
1061
+ const interrupt = () => interruption.abort();
822
1062
  const cutEnd = (cause) => {
823
1063
  const since = quantity(Math.max(0, clock.now() - started) / 1e3, "s");
824
1064
  const retryable = mayReissue(operation, since);
@@ -887,7 +1127,7 @@ const tracker = (controllers, clock, refuses, choices = /* @__PURE__ */ new Map(
887
1127
  }
888
1128
  const next = async () => {
889
1129
  if (ended) return null;
890
- if (end === null) await Promise.race([ready, interrupted]);
1130
+ if (end === null) await untilAbort(ready, interruption.signal, "interrupted");
891
1131
  if (end === null && unary !== null) {
892
1132
  const { value } = unary;
893
1133
  unary = null;
@@ -905,7 +1145,7 @@ const tracker = (controllers, clock, refuses, choices = /* @__PURE__ */ new Map(
905
1145
  const pending = source.next();
906
1146
  let step;
907
1147
  try {
908
- step = await Promise.race([pending, interrupted]);
1148
+ step = await untilAbort(Promise.resolve(pending), interruption.signal, "interrupted");
909
1149
  } catch (error) {
910
1150
  failed(error);
911
1151
  step = "interrupted";
@@ -1416,6 +1656,25 @@ const watchdog = (clock, spec, trace) => {
1416
1656
  const isTime$1 = (amount) => convertible(amount.unit, "s");
1417
1657
  const above = (amount) => amount.value === "infinite" || isTime$1(amount) && valueIn(amount, "s") > 0;
1418
1658
  const whole = (count) => Number.isInteger(count) && count >= 1;
1659
+ /** This function copies a check, with its poll and the loops that mark it. */
1660
+ const copyCheck = (check) => ({
1661
+ ...check,
1662
+ poll: { ...check.poll },
1663
+ ...check.marks === void 0 ? {} : { marks: {
1664
+ loops: [...check.marks.loops],
1665
+ stall: { ...check.marks.stall }
1666
+ } }
1667
+ });
1668
+ /** This function gives the problems of the loops that mark a check. `lint()` compares the stall time with the loops. */
1669
+ const marksProblems = (check) => {
1670
+ const marks = check.marks;
1671
+ if (marks === void 0) return [];
1672
+ const problems = [];
1673
+ if (check.step !== "progress") problems.push(`check ${check.name} names the loops that mark it, but its step is ${check.step}, not progress`);
1674
+ if (marks.loops.length === 0) problems.push(`check ${check.name} names no loop that marks it`);
1675
+ if (typeof marks.stall.value !== "number" || !above(marks.stall)) problems.push(`check ${check.name} has a stall time that is not a time above zero`);
1676
+ return problems;
1677
+ };
1419
1678
  /** This function gives the problems of a timing: a zero interval or count fails decoding. */
1420
1679
  const timingProblems = (policy, timing) => {
1421
1680
  if (timing.type === "debounce") {
@@ -1604,14 +1863,11 @@ const service = (name) => {
1604
1863
  check(check, run) {
1605
1864
  once(checks, check.name, "check");
1606
1865
  if (started && run === void 0) throw new Error(`check ${check.name} joins after the start, thus it needs its function`);
1607
- checks.push({ ...check });
1866
+ checks.push(copyCheck(check));
1608
1867
  if (run !== void 0) runs.set(check.name, run);
1609
1868
  if (started) {
1610
1869
  partial = true;
1611
- live?.addCheck({
1612
- ...check,
1613
- poll: { ...check.poll }
1614
- }, run);
1870
+ live?.addCheck(copyCheck(check), run);
1615
1871
  }
1616
1872
  return builder;
1617
1873
  },
@@ -1749,6 +2005,7 @@ const service = (name) => {
1749
2005
  if (!isTime$1(check.poll.interval) || !isTime$1(check.poll.timeout)) problems.push(`check ${check.name} polls in a unit that is not a time`);
1750
2006
  else if (!above(check.poll.interval) || !above(check.poll.timeout)) problems.push(`check ${check.name} polls with an interval or a timeout that is not above zero`);
1751
2007
  if (!(check.poll.jitter >= 0 && check.poll.jitter < 1)) problems.push(`check ${check.name} has a jitter of ${check.poll.jitter}, not a fraction below 1`);
2008
+ problems.push(...marksProblems(check));
1752
2009
  const timing = check.gates === null ? null : policies[check.gates];
1753
2010
  if (timing?.type === "debounce" && isTime$1(check.poll.interval) && above(timing.interval) && valueIn(check.poll.interval, "s") !== valueIn(timing.interval, "s")) problems.push(`check ${check.name} polls every ${valueIn(check.poll.interval, "s")} s, but the ${check.gates} debounce counts polls every ${valueIn(timing.interval, "s")} s`);
1754
2011
  }
@@ -1791,10 +2048,7 @@ const service = (name) => {
1791
2048
  operations: { ...entry.operations }
1792
2049
  })),
1793
2050
  controllers: controllersOut,
1794
- checks: checks.map((entry) => ({
1795
- ...entry,
1796
- poll: { ...entry.poll }
1797
- })),
2051
+ checks: checks.map(copyCheck),
1798
2052
  ...limiters.length === 0 ? {} : { limiters: limiters.map((entry) => ({
1799
2053
  ...entry,
1800
2054
  policy: { ...entry.policy },
@@ -1901,10 +2155,16 @@ const marginOf = (inequality, index, bind) => {
1901
2155
  if (typeof bind.margin === "number") return bind.margin;
1902
2156
  return typeof inequality.margin === "number" ? inequality.margin : 0;
1903
2157
  };
2158
+ /** This function tells if an inequality reads a symbol with no limit: "inf", "off" or an infinite number. */
2159
+ const readsAbsent = (inequality, bind) => [...inequality.lhs.terms, ...inequality.rhs.terms].some((term) => {
2160
+ const bound = boundOf$1(bind, keyOf(term));
2161
+ return bound !== void 0 && value(bound) === Infinity;
2162
+ });
1904
2163
  /**
1905
2164
  * This function evaluates each inequality on one binding. "<" holds when lhs + margin < rhs, and
1906
2165
  * ">" holds when lhs > rhs + margin. Only the inequalities with all their symbols bound apply.
1907
- * The others are null.
2166
+ * The others are null. Absent timers can leave no order: the same infinity on each side, or both
2167
+ * signs of infinity on one side. Then the inequality holds only because a timer is absent.
1908
2168
  */
1909
2169
  const evaluate = (rule, bind) => inequalities(rule).map((inequality, index) => {
1910
2170
  const lhs = sideOf(inequality.lhs, bind);
@@ -1916,6 +2176,12 @@ const evaluate = (rule, bind) => inequalities(rule).map((inequality, index) => {
1916
2176
  margin,
1917
2177
  holds: null
1918
2178
  };
2179
+ if ((Number.isNaN(lhs) || Number.isNaN(rhs) || lhs === rhs && !Number.isFinite(lhs)) && readsAbsent(inequality, bind)) return {
2180
+ lhs,
2181
+ rhs,
2182
+ margin,
2183
+ holds: true
2184
+ };
1919
2185
  return {
1920
2186
  lhs,
1921
2187
  rhs,
@@ -1934,13 +2200,7 @@ const verdictOf = (rule, bind, evaluated) => {
1934
2200
  if (applicable.length === 0) return "depends";
1935
2201
  if (applicable.some((index) => evaluated[index]?.holds === false)) return "violated";
1936
2202
  const all = inequalities(rule);
1937
- return applicable.some((index) => {
1938
- const inequality = all[index];
1939
- return [...inequality.lhs.terms, ...inequality.rhs.terms].some((term) => {
1940
- const bound = boundOf$1(bind, keyOf(term));
1941
- return bound !== void 0 && value(bound) === Infinity;
1942
- });
1943
- }) ? "unbounded" : "holds";
2203
+ return applicable.some((index) => readsAbsent(all[index], bind)) ? "unbounded" : "holds";
1944
2204
  };
1945
2205
  //#endregion
1946
2206
  //#region src/lint.ts
@@ -2139,14 +2399,144 @@ const declarationFindings = (svc) => svc.shutdown.onRepeatSignal === void 0 ? [{
2139
2399
  evaluated: [],
2140
2400
  unbound: ["shutdown.onRepeatSignal"]
2141
2401
  }] : [];
2402
+ /** This identifier is the rule of the findings about the stall time of a progress check (`Check.marks`). */
2403
+ const PROGRESS_RULE = "progress";
2404
+ /**
2405
+ * This rule compares the stall time of a progress check with the longest gap between two marks of
2406
+ * the slowest loop that it names. A shorter stall time trips the check while each loop turns on time.
2407
+ */
2408
+ const PROGRESS_LAW = {
2409
+ id: PROGRESS_RULE,
2410
+ rule: "the stall time of a progress check is at least the longest gap between two marks of the slowest loop that it names",
2411
+ window: "a loop that turns on time can trip the check",
2412
+ expr: {
2413
+ lhs: { terms: [{
2414
+ knob: "stall",
2415
+ coef: 1,
2416
+ at: "check"
2417
+ }] },
2418
+ op: ">=",
2419
+ rhs: { terms: [{
2420
+ knob: "mark_gap",
2421
+ coef: 1,
2422
+ at: "loop"
2423
+ }] },
2424
+ margin: 0
2425
+ }
2426
+ };
2427
+ /** These are the knobs of a worker that bound the gap between two of its marks. */
2428
+ const WORKER_GAP_KNOBS = [
2429
+ "worker_sleep",
2430
+ "unit_processing_time",
2431
+ "retry_base",
2432
+ "retry_cap"
2433
+ ];
2434
+ /**
2435
+ * This function gives the longest gap between two marks of each loop with a name. The rule counts
2436
+ * a mark at the end of each turn of the loop, also a turn that fails.
2437
+ * - A check polls on a fixed delay from the start of each poll, and each poll ends at its timeout
2438
+ * at the latest. Thus the gap is `interval × (1 + jitter) + timeout`.
2439
+ * - After an iteration, a lane of a worker waits its sleep, or its backoff after a failure. The
2440
+ * longest backoff is `retry_cap` plus its jitter of up to `retry_base`. The next iteration then
2441
+ * takes up to `unit_processing_time`. Thus the gap is the longer wait plus that bound.
2442
+ *
2443
+ * A sleep or a bound without a limit gives a gap without a limit. An iteration that does not end
2444
+ * at its abort can take more time, and the rule does not bound it.
2445
+ */
2446
+ const gapsOf = (svc, loop) => {
2447
+ const gaps = svc.checks.filter((check) => check.name === loop).map((check) => ({
2448
+ loop,
2449
+ seconds: secondsOf(check.poll.interval) * (1 + check.poll.jitter) + secondsOf(check.poll.timeout),
2450
+ unbound: []
2451
+ }));
2452
+ for (const worker of svc.transports) {
2453
+ if (worker.kind !== "worker" || worker.name !== loop) continue;
2454
+ const knob = (name) => {
2455
+ const amount = worker.knobs[name];
2456
+ if (amount === void 0 || !isTime(amount.unit)) return void 0;
2457
+ return amount.value === "infinite" ? Infinity : secondsOf(amount);
2458
+ };
2459
+ const unbound = WORKER_GAP_KNOBS.filter((name) => knob(name) === void 0).map((name) => `${loop}.${name}`);
2460
+ if (unbound.length > 0) {
2461
+ gaps.push({
2462
+ loop,
2463
+ seconds: void 0,
2464
+ unbound
2465
+ });
2466
+ continue;
2467
+ }
2468
+ const [sleep, unit, base, cap] = WORKER_GAP_KNOBS.map((name) => knob(name));
2469
+ const backoff = Number.isFinite(base) && Number.isFinite(cap) ? backoffBand({
2470
+ base,
2471
+ factor: 2,
2472
+ cap,
2473
+ jitter: {
2474
+ kind: "add",
2475
+ amount: base
2476
+ }
2477
+ }).high : Infinity;
2478
+ gaps.push({
2479
+ loop,
2480
+ seconds: Math.max(sleep, backoff) + unit,
2481
+ unbound: []
2482
+ });
2483
+ }
2484
+ return gaps;
2485
+ };
2486
+ /**
2487
+ * This function gives the findings of each check that names the loops that mark it. A name that is
2488
+ * not a check or a worker of the service gets a finding of its own, with the verdict `depends`.
2489
+ * Without `marks`, a check gets no finding.
2490
+ */
2491
+ const progressFindings = (svc) => {
2492
+ const findings = [];
2493
+ const finding = (check, fields) => ({
2494
+ rule: PROGRESS_RULE,
2495
+ scope: check.name,
2496
+ statement: PROGRESS_LAW.rule,
2497
+ window: PROGRESS_LAW.window,
2498
+ ...fields
2499
+ });
2500
+ for (const check of svc.checks) {
2501
+ const marks = check.marks;
2502
+ if (marks === void 0) continue;
2503
+ const gaps = [];
2504
+ for (const loop of marks.loops) {
2505
+ const found = gapsOf(svc, loop);
2506
+ if (found.length > 0) gaps.push(...found);
2507
+ else findings.push(finding(check, {
2508
+ verdict: "depends",
2509
+ statement: "each loop that a progress check names is a check or a worker of the service",
2510
+ window: `the check waits for the marks of ${loop}, and the service has no loop with that name`,
2511
+ evaluated: [],
2512
+ unbound: [loop]
2513
+ }));
2514
+ }
2515
+ if (gaps.length === 0) continue;
2516
+ const unbound = [...isTime(marks.stall.unit) ? [] : [`${check.name}.marks.stall`], ...gaps.flatMap((gap) => gap.unbound)];
2517
+ const slowest = gaps.reduce((worst, gap) => (gap.seconds ?? -Infinity) > (worst.seconds ?? -Infinity) ? gap : worst);
2518
+ const bind = {};
2519
+ if (isTime(marks.stall.unit)) bind["stall@check"] = boundOf(marks.stall);
2520
+ if (unbound.length === 0 && slowest.seconds !== void 0) bind["mark_gap@loop"] = Number.isFinite(slowest.seconds) ? slowest.seconds : "inf";
2521
+ const evaluated = evaluate(PROGRESS_LAW, bind);
2522
+ findings.push(finding(check, {
2523
+ verdict: verdictOf(PROGRESS_LAW, bind, evaluated),
2524
+ ...unbound.length === 0 ? { statement: `${PROGRESS_LAW.rule} (${slowest.loop})` } : {},
2525
+ evaluated,
2526
+ unbound
2527
+ }));
2528
+ }
2529
+ return findings;
2530
+ };
2142
2531
  /**
2143
2532
  * This function applies each service rule to a declaration and a scenario. A rule applies one time
2144
2533
  * for the service, for each client, for each consumer and worker, or for each limiter. The function
2145
2534
  * also gives a finding for each necessary field that the description does not give (`DECLARATION_RULE`).
2535
+ * Each progress check that names its loops also gets its findings (`PROGRESS_RULE`).
2146
2536
  */
2147
2537
  const lint = (svc, scenario, rules) => {
2148
2538
  const service = serviceBindings(svc, scenario).bind;
2149
- const findings = [...declarationFindings(svc)];
2539
+ const findings = [...declarationFindings(svc), ...progressFindings(svc)];
2150
2540
  for (const rule of rules) {
2151
2541
  if (!SERVICE_RULES.includes(rule.id)) continue;
2152
2542
  const symbols = symbolsOf(rule);
@@ -2296,229 +2686,6 @@ const kubernetes = (pod) => {
2296
2686
  };
2297
2687
  };
2298
2688
  //#endregion
2299
- //#region src/units.ts
2300
- /** This function gives the cause of an abort as text. */
2301
- const causeOf = (signal) => signal.reason instanceof Error ? signal.reason.message : String(signal.reason ?? "the unit got an abort");
2302
- /**
2303
- * This function does one unit of work, and gives its end. With a request, the unit goes through
2304
- * `life.request`, thus the service counts it, drains it and can cut it. A throw after the abort of
2305
- * the transport is a cut, not a failure (S6 A10). A throw after the bound is a failure.
2306
- */
2307
- const runUnit = async (run) => {
2308
- const { clock, bound, abort, life, request, unit, work } = run;
2309
- const timeout = new AbortController();
2310
- const cancel = Number.isFinite(bound) ? clock.after(bound, () => timeout.abort(/* @__PURE__ */ new Error(`the unit did not end in its unit_processing_time of ${bound} ms`))) : () => {};
2311
- const call = async (tracker) => work(AbortSignal.any(tracker === void 0 ? [abort.signal, timeout.signal] : [
2312
- abort.signal,
2313
- timeout.signal,
2314
- tracker
2315
- ]));
2316
- const thrown = (error) => {
2317
- if (abort.signal.aborted) return {
2318
- kind: "cut",
2319
- cause: causeOf(abort.signal),
2320
- retryable: false
2321
- };
2322
- if (timeout.signal.aborted) return {
2323
- kind: "failed",
2324
- error: timeout.signal.reason
2325
- };
2326
- return {
2327
- kind: "failed",
2328
- error
2329
- };
2330
- };
2331
- try {
2332
- if (request === void 0) try {
2333
- return {
2334
- kind: "done",
2335
- value: await call()
2336
- };
2337
- } catch (error) {
2338
- return thrown(error);
2339
- }
2340
- let collected;
2341
- try {
2342
- const route = typeof request.route === "string" ? request.route : request.route(unit);
2343
- const detail = request.detail?.(unit);
2344
- collected = life.request(request.controller, route, (signal) => call(signal).then((value) => ({ value })), detail === void 0 ? {} : { detail }).collect();
2345
- } catch (error) {
2346
- return {
2347
- kind: "failed",
2348
- error
2349
- };
2350
- }
2351
- const { items, end } = await collected;
2352
- if (end.kind === "done") return {
2353
- kind: "done",
2354
- value: items[0]?.value
2355
- };
2356
- if (end.kind === "failed") return thrown(end.error);
2357
- if (end.kind === "refused") return {
2358
- kind: "refused",
2359
- cause: end.cause
2360
- };
2361
- return {
2362
- kind: "cut",
2363
- cause: end.cause,
2364
- retryable: end.retryable
2365
- };
2366
- } finally {
2367
- cancel();
2368
- }
2369
- };
2370
- /** This function makes a flight set on a clock. */
2371
- const flights = (clock) => {
2372
- const units = /* @__PURE__ */ new Set();
2373
- const idle = /* @__PURE__ */ new Set();
2374
- const lower = /* @__PURE__ */ new Set();
2375
- let aborted = null;
2376
- const settle = () => {
2377
- for (const done of [...lower]) done();
2378
- lower.clear();
2379
- if (units.size === 0) for (const done of [...idle]) done();
2380
- };
2381
- const abort = (reason) => {
2382
- aborted ??= { reason };
2383
- for (const unit of [...units]) if (!unit.signal.aborted) unit.abort(aborted.reason);
2384
- };
2385
- return {
2386
- get size() {
2387
- return units.size;
2388
- },
2389
- add(unit) {
2390
- units.add(unit);
2391
- if (aborted !== null && !unit.signal.aborted) unit.abort(aborted.reason);
2392
- return () => {
2393
- if (units.delete(unit)) settle();
2394
- };
2395
- },
2396
- abort,
2397
- clear() {
2398
- units.clear();
2399
- settle();
2400
- },
2401
- drain(deadline, signal) {
2402
- const cooperative = () => abort(signal.reason);
2403
- if (signal.aborted) cooperative();
2404
- else signal.addEventListener("abort", cooperative, { once: true });
2405
- if (units.size === 0 || deadline <= clock.now()) {
2406
- signal.removeEventListener("abort", cooperative);
2407
- return Promise.resolve();
2408
- }
2409
- return new Promise((resolve) => {
2410
- let cancel = () => {};
2411
- const done = () => {
2412
- idle.delete(done);
2413
- cancel();
2414
- signal.removeEventListener("abort", cooperative);
2415
- resolve();
2416
- };
2417
- idle.add(done);
2418
- cancel = clock.after(Math.max(0, deadline - clock.now()), done);
2419
- });
2420
- },
2421
- less() {
2422
- return new Promise((resolve) => lower.add(resolve));
2423
- },
2424
- release() {
2425
- for (const done of [...lower]) done();
2426
- lower.clear();
2427
- }
2428
- };
2429
- };
2430
- /** This function makes a sleeper on a clock. */
2431
- const sleeper = (clock) => {
2432
- let wakes = 0;
2433
- let stopped = false;
2434
- const sleeps = /* @__PURE__ */ new Set();
2435
- const backoffs = /* @__PURE__ */ new Set();
2436
- const wait = (ms, into) => stopped ? Promise.resolve() : new Promise((resolve) => {
2437
- let cancel = () => {};
2438
- const done = () => {
2439
- if (!into.delete(done)) return;
2440
- cancel();
2441
- resolve();
2442
- };
2443
- into.add(done);
2444
- if (Number.isFinite(ms)) cancel = clock.after(ms, done);
2445
- });
2446
- return {
2447
- get wakes() {
2448
- return wakes;
2449
- },
2450
- sleep(ms, since) {
2451
- if (since !== wakes) return Promise.resolve();
2452
- return wait(ms, sleeps);
2453
- },
2454
- backoff(ms) {
2455
- return wait(ms, backoffs);
2456
- },
2457
- wake() {
2458
- wakes += 1;
2459
- const ended = sleeps.size;
2460
- for (const done of [...sleeps]) done();
2461
- return ended;
2462
- },
2463
- stop() {
2464
- stopped = true;
2465
- for (const done of [...sleeps, ...backoffs]) done();
2466
- }
2467
- };
2468
- };
2469
- /** This function waits for a work until a bound on the clock, in milliseconds. */
2470
- const settleWithin = (clock, ms, work) => new Promise((resolve) => {
2471
- const cancel = Number.isFinite(ms) ? clock.after(ms, () => resolve({ kind: "bound" })) : () => {};
2472
- work.then(() => {
2473
- cancel();
2474
- resolve({ kind: "done" });
2475
- }, (error) => {
2476
- cancel();
2477
- resolve({
2478
- kind: "failed",
2479
- error
2480
- });
2481
- });
2482
- });
2483
- /**
2484
- * This function gives the backoff of the knobs of a loop. The first delay is `retry_base`, and each
2485
- * delay is two times longer, up to `retry_cap`. Each delay has a jitter up to the base, as in the Redis client.
2486
- */
2487
- const retryOf = (knobs, owner) => {
2488
- const base = boundedMillisecondsOf(knobs, "retry_base", owner);
2489
- const backoff = {
2490
- base,
2491
- factor: 2,
2492
- cap: boundedMillisecondsOf(knobs, "retry_cap", owner),
2493
- jitter: {
2494
- kind: "add",
2495
- amount: base
2496
- }
2497
- };
2498
- const problems = backoffProblems(backoff);
2499
- if (problems.length > 0) throw new Error(`${owner}: the retry_base and retry_cap give no backoff: ${problems.join(", ")}`);
2500
- return backoff;
2501
- };
2502
- /**
2503
- * This function gives the problems of the bounds of a loop in a declaration. The close bound fits
2504
- * in the cap of `close transports`. The bound of a unit fits in the cap of `drain`, or the drain can cut a unit (S6 A10).
2505
- */
2506
- const boundProblems = (owner, closeBound, unitBound, description) => {
2507
- const capOf = (name) => {
2508
- const phase = description.shutdown.phases.find((each) => each.name === name);
2509
- return phase === void 0 ? void 0 : valueIn(phase.cap, "ms");
2510
- };
2511
- const found = [];
2512
- const close = capOf("close transports");
2513
- if (close === void 0) found.push(`${owner} needs the phase "close transports" to close`);
2514
- else if (closeBound >= close) found.push(`the close_bound of ${owner}, ${closeBound} ms, does not fit in the cap of "close transports", ${close} ms`);
2515
- const drain = capOf("drain");
2516
- if (drain === void 0) found.push(`${owner} needs the phase "drain" to end its units`);
2517
- else if (!Number.isFinite(unitBound)) found.push(`the unit_processing_time of ${owner} has no bound, thus the drain can cut a unit at its cap of ${drain} ms`);
2518
- else if (unitBound >= drain) found.push(`the unit_processing_time of ${owner}, ${unitBound} ms, does not fit in the cap of "drain", ${drain} ms`);
2519
- return found;
2520
- };
2521
- //#endregion
2522
2689
  //#region src/worker.ts
2523
2690
  const OWNER$1 = "workerLoop";
2524
2691
  /**
@@ -2706,7 +2873,6 @@ const queueDrain = (knobs, options) => {
2706
2873
  const running = flights(clock);
2707
2874
  const rests = sleeper(clock);
2708
2875
  const intake = new AbortController();
2709
- const stopped = new Promise((resolve) => intake.signal.addEventListener("abort", () => resolve(STOPPED), { once: true }));
2710
2876
  const pending = /* @__PURE__ */ new Set();
2711
2877
  const active = /* @__PURE__ */ new Set();
2712
2878
  let refusals = 0;
@@ -2861,7 +3027,7 @@ const queueDrain = (knobs, options) => {
2861
3027
  kind: "error",
2862
3028
  error
2863
3029
  }));
2864
- const first = await Promise.race([taking, stopped]);
3030
+ const first = await untilAbort(taking, intake.signal, STOPPED);
2865
3031
  if (first === STOPPED || intake.signal.aborted) {
2866
3032
  if (first === STOPPED) taking.then((late) => {
2867
3033
  if (late.kind === "job") handBack(late.job);
@@ -3389,6 +3555,6 @@ const MACHINES = {
3389
3555
  watchdog: WATCHDOG_MACHINE
3390
3556
  };
3391
3557
  //#endregion
3392
- export { DECLARATION_RULE, EXIT_MACHINE, EXIT_SIGNALS, FORMAT, GATE_MACHINE, LATCH_MACHINE, LIMITER_RULES, MACHINES, POLLER_MACHINE, PROCESS_TRIGGERS, SERVICE_RULES, SLIDING_WINDOW_MACHINE, STAGES, TOKEN_BUCKET_MACHINE, WATCHDOG_MACHINE, WINDOW_MACHINE, asDebounce, asWindow, backoffBand, backoffDelay, backoffProblems, capKnob, cooperativeAbortAt, daemon, decide, detectionTime, drainMachine, embeddedProcess, ephemeral, evaluate, failSpan, freeAt, heartbeat, heartbeatDetection, idleSpan, inequalities, isExitCode, isExitSignal, isMessageShape, keyedLimiter, kubernetes, limitRule, limiterMachine, limiterProblems, limiterStateOf, lint, localDev, longestRetryAfter, matchesMessage, mayReissue, memoryStore, nodeProcess, platformClock, processPortOf, queueDrain, realClock, recoverSpan, redeliverOnce, replay, retryAfterSeconds, service, serviceBindings, stageRank, symbolsOf, verdictOf, watchdog, workerLoop };
3558
+ export { DECLARATION_RULE, EXIT_MACHINE, EXIT_SIGNALS, FORMAT, GATE_MACHINE, LATCH_MACHINE, LIMITER_RULES, MACHINES, POLLER_MACHINE, PROCESS_TRIGGERS, PROGRESS_RULE, SERVICE_RULES, SLIDING_WINDOW_MACHINE, STAGES, TOKEN_BUCKET_MACHINE, WATCHDOG_MACHINE, WINDOW_MACHINE, asDebounce, asWindow, backoffBand, backoffDelay, backoffProblems, capKnob, cooperativeAbortAt, daemon, decide, detectionTime, drainMachine, embeddedProcess, ephemeral, evaluate, failSpan, freeAt, heartbeat, heartbeatDetection, idleSpan, inequalities, isExitCode, isExitSignal, isMessageShape, keyedLimiter, kubernetes, limitRule, limiterMachine, limiterProblems, limiterStateOf, lint, localDev, longestRetryAfter, matchesMessage, mayReissue, memoryStore, nodeProcess, platformClock, processPortOf, queueDrain, realClock, recoverSpan, redeliverOnce, replay, retryAfterSeconds, service, serviceBindings, stageRank, symbolsOf, verdictOf, watchdog, workerLoop };
3393
3559
 
3394
3560
  //# sourceMappingURL=index.js.map