@mark1russell7/service 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.js CHANGED
@@ -3,6 +3,7 @@ import { detectionTime, heartbeat } from "./heartbeat.js";
3
3
  import { a as recoverSpan, i as heartbeatDetection, n as asWindow, r as failSpan, t as asDebounce } from "./timing-DGRi3XCI.js";
4
4
  import { n as operationProblems, t as mayReissue } from "./operation-DL5RxFds.js";
5
5
  import { backoffBand, backoffDelay, backoffProblems } from "./backoff.js";
6
+ import { n as countOf, r as millisecondsOf, t as boundedMillisecondsOf } from "./knobs-BxG-c7Zb.js";
6
7
  import { convertExact, convertible, quantity, rationals, valueIn } from "@mark1russell7/quantity";
7
8
  import { replay } from "@mark1russell7/algebra/replay";
8
9
  //#region src/description.ts
@@ -498,11 +499,37 @@ const EXIT_SIGNALS = [
498
499
  "SIGQUIT"
499
500
  ];
500
501
  /** These are the process triggers that a declaration can opt in to. */
501
- const PROCESS_TRIGGERS = ["stdin-close", "ipc-close"];
502
+ const PROCESS_TRIGGERS = [
503
+ "stdin-close",
504
+ "ipc-close",
505
+ "ipc-message"
506
+ ];
502
507
  /** This function tells if a number is an exit code of a process: a whole number from 0 to 255. */
503
508
  const isExitCode = (code) => Number.isInteger(code) && code >= 0 && code <= 255;
504
509
  /** This function tells if a trigger is an exit signal. A repeat signal obeys `onRepeatSignal`. */
505
510
  const isExitSignal = (trigger) => EXIT_SIGNALS.includes(trigger);
511
+ /** This function tells if a value is a message shape: a value of JSON, with no function and no undefined. */
512
+ const isMessageShape = (value) => {
513
+ if (value === null || typeof value === "string" || typeof value === "boolean") return true;
514
+ if (typeof value === "number") return Number.isFinite(value);
515
+ if (Array.isArray(value)) return value.every(isMessageShape);
516
+ if (typeof value !== "object") return false;
517
+ const prototype = Object.getPrototypeOf(value);
518
+ return (prototype === Object.prototype || prototype === null) && Object.values(value).every(isMessageShape);
519
+ };
520
+ /** This function tells if a message of the IPC channel matches a shape. */
521
+ const matchesMessage = (shape, message) => {
522
+ if (shape === null || typeof shape !== "object") return Object.is(shape, message);
523
+ if (Array.isArray(shape)) {
524
+ const items = shape;
525
+ return Array.isArray(message) && message.length === items.length && items.every((item, at) => matchesMessage(item, message[at]));
526
+ }
527
+ if (message === null || typeof message !== "object" || Array.isArray(message)) return false;
528
+ const fields = message;
529
+ return Object.entries(shape).every(([field, value]) => Object.hasOwn(fields, field) && matchesMessage(value, fields[field]));
530
+ };
531
+ /** This time is the interval of the timer of a hold, the most that a timer of Node accepts. */
532
+ const HOLD_INTERVAL = 2147483647;
506
533
  /** This function adds a handler to some events of an emitter. The handler operates one time at most. */
507
534
  const once = (emitter, events, handler) => {
508
535
  let called = false;
@@ -556,6 +583,13 @@ const processPortOf = (find) => {
556
583
  return once(input, ["end", "close"], handler);
557
584
  }
558
585
  if (trigger === "ipc-close") return once(host, ["disconnect"], handler);
586
+ if (trigger === "ipc-message") {
587
+ const receive = (message) => handler(message);
588
+ host.on("message", receive);
589
+ return () => {
590
+ host.off("message", receive);
591
+ };
592
+ }
559
593
  host.on(trigger, handler);
560
594
  return () => {
561
595
  host.off(trigger, handler);
@@ -565,6 +599,16 @@ const processPortOf = (find) => {
565
599
  const host = hostProcess();
566
600
  await Promise.all([written(host.stdout), written(host.stderr)]);
567
601
  },
602
+ hold() {
603
+ hostProcess();
604
+ const timer = setInterval(() => {}, HOLD_INTERVAL);
605
+ let held = true;
606
+ return () => {
607
+ if (!held) return;
608
+ held = false;
609
+ clearInterval(timer);
610
+ };
611
+ },
568
612
  exit(code) {
569
613
  hostProcess().exit(code);
570
614
  }
@@ -572,6 +616,33 @@ const processPortOf = (find) => {
572
616
  };
573
617
  /** This port is the process of Node. */
574
618
  const nodeProcess = processPortOf(() => globalThis.process);
619
+ /**
620
+ * This function gives a process port for a lifecycle in a process that another part owns. That
621
+ * part is a script, a test of an application, or a library entry. The port adds no handler of a signal or of
622
+ * a trigger, it does not hold the process, and its `exit` only records the code. The caller stops
623
+ * the lifecycle with `life.shutdown()`. A process with a service of its own gives its parts to
624
+ * that service, and does not use this port.
625
+ */
626
+ const embeddedProcess = () => {
627
+ let code = null;
628
+ let resolve = () => {};
629
+ return {
630
+ on() {
631
+ return () => {};
632
+ },
633
+ exit(requested) {
634
+ if (code !== null) return;
635
+ code = requested;
636
+ resolve(requested);
637
+ },
638
+ get exitCode() {
639
+ return code;
640
+ },
641
+ exited: new Promise((done) => {
642
+ resolve = done;
643
+ })
644
+ };
645
+ };
575
646
  //#endregion
576
647
  //#region src/routes.ts
577
648
  const NAME = /^[A-Za-z_][A-Za-z0-9_]*/;
@@ -648,7 +719,7 @@ const compilePattern = (pattern) => {
648
719
  };
649
720
  //#endregion
650
721
  //#region src/requests.ts
651
- const isAsyncIterable = (value) => value !== null && (typeof value === "object" || typeof value === "function") && typeof value[Symbol.asyncIterator] === "function";
722
+ const isAsyncIterable$1 = (value) => value !== null && (typeof value === "object" || typeof value === "function") && typeof value[Symbol.asyncIterator] === "function";
652
723
  const isPromiseLike = (value) => value !== null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
653
724
  /** This function gives a stream of events from a function that gives the next event. */
654
725
  const streamOf = (next, stop) => {
@@ -793,7 +864,7 @@ const tracker = (controllers, clock, refuses, choices = /* @__PURE__ */ new Map(
793
864
  /** This function takes the result of the work: a stream, or one value. */
794
865
  const take = (result) => {
795
866
  if (end !== null) return;
796
- if (isAsyncIterable(result)) {
867
+ if (isAsyncIterable$1(result)) {
797
868
  source = result[Symbol.asyncIterator]();
798
869
  if (abort.signal.aborted) closeSource();
799
870
  } else {
@@ -804,7 +875,7 @@ const tracker = (controllers, clock, refuses, choices = /* @__PURE__ */ new Map(
804
875
  let ready;
805
876
  try {
806
877
  const result = work(abort.signal);
807
- if (isAsyncIterable(result) || !isPromiseLike(result)) {
878
+ if (isAsyncIterable$1(result) || !isPromiseLike(result)) {
808
879
  take(result);
809
880
  ready = Promise.resolve();
810
881
  } else ready = Promise.resolve(result).then(take, failed);
@@ -1039,7 +1110,8 @@ const lifecycle = (description, runtime, components, options = {}, bind = () =>
1039
1110
  startupEnded = resolve;
1040
1111
  });
1041
1112
  let offs = [];
1042
- /** This function ends the process with a code, one time, and removes each handler of a trigger. */
1113
+ let release = () => {};
1114
+ /** This function ends the process with a code, one time, and removes each handler of a trigger and the hold. */
1043
1115
  const end = (code, on) => {
1044
1116
  state = "exited";
1045
1117
  exitCode = code;
@@ -1047,22 +1119,34 @@ const lifecycle = (description, runtime, components, options = {}, bind = () =>
1047
1119
  offs = [];
1048
1120
  moveExit("exited", on, `exit ${code}`);
1049
1121
  finish(code);
1122
+ release();
1050
1123
  port.exit(code);
1051
1124
  };
1052
- /** This function receives a trigger of the process: the first one starts the shutdown. */
1053
- const triggered = (trigger) => {
1125
+ /**
1126
+ * This function receives a trigger of the process: the first one starts the shutdown. The cause
1127
+ * names the trigger, and a message that stands for a signal tells it.
1128
+ */
1129
+ const triggered = (trigger, cause = trigger) => {
1054
1130
  if (exitState === "exited") return;
1055
1131
  if (shutdownStarted || exitState === "stopping") {
1056
1132
  if (isExitSignal(trigger) && repeatPolicy === "exit") {
1057
- fail(`${trigger} came again while the shutdown ran, and the declaration exits at once`);
1133
+ fail(`${cause} came again while the shutdown ran, and the declaration exits at once`);
1058
1134
  end(1, "exit.repeat");
1059
1135
  return;
1060
1136
  }
1061
- record("exit", exitState, exitState, "exit.repeat", trigger);
1137
+ record("exit", exitState, exitState, "exit.repeat", cause);
1062
1138
  return;
1063
1139
  }
1064
1140
  const code = exitCodes[trigger];
1065
- shutdown(trigger, code === void 0 ? {} : { code });
1141
+ shutdown(cause, code === void 0 ? {} : { code });
1142
+ };
1143
+ const messages = description.shutdown.messages ?? [];
1144
+ /** This function receives a message of the IPC channel. The first declared shape that it matches gives its trigger. */
1145
+ const received = (message) => {
1146
+ const found = messages.find((each) => matchesMessage(each.match, message));
1147
+ if (found === void 0) return;
1148
+ if (found.as === void 0) triggered("ipc-message");
1149
+ else triggered(found.as, `${found.as} (a message of the IPC channel)`);
1066
1150
  };
1067
1151
  /** This function does the work of one phase, until its cap. It gives true when the work ended in time. */
1068
1152
  const runPhase = async (phase, hold, work) => {
@@ -1208,7 +1292,8 @@ const lifecycle = (description, runtime, components, options = {}, bind = () =>
1208
1292
  keepAlive: () => state === "running" && runtime.verdict().ready
1209
1293
  };
1210
1294
  bind(life);
1211
- offs = [...EXIT_SIGNALS, ...description.shutdown.triggers ?? []].map((trigger) => port.on(trigger, () => triggered(trigger)));
1295
+ release = port.hold?.() ?? (() => {});
1296
+ offs = [...EXIT_SIGNALS, ...description.shutdown.triggers ?? []].map((trigger) => port.on(trigger, trigger === "ipc-message" ? received : () => triggered(trigger)));
1212
1297
  (async () => {
1213
1298
  const started = (what) => record("gate", "starting", "starting", "component.started", what);
1214
1299
  try {
@@ -1301,8 +1386,8 @@ const watchdog = (clock, spec, trace) => {
1301
1386
  };
1302
1387
  //#endregion
1303
1388
  //#region src/builder.ts
1304
- const isTime = (amount) => convertible(amount.unit, "s");
1305
- const above = (amount) => amount.value === "infinite" || isTime(amount) && valueIn(amount, "s") > 0;
1389
+ const isTime$1 = (amount) => convertible(amount.unit, "s");
1390
+ const above = (amount) => amount.value === "infinite" || isTime$1(amount) && valueIn(amount, "s") > 0;
1306
1391
  const whole = (count) => Number.isInteger(count) && count >= 1;
1307
1392
  /** This function gives the problems of a timing: a zero interval or count fails decoding. */
1308
1393
  const timingProblems = (policy, timing) => {
@@ -1313,15 +1398,20 @@ const timingProblems = (policy, timing) => {
1313
1398
  if (!whole(timing.recoverAfter)) problems.push(`the ${policy} debounce recovers after ${timing.recoverAfter} polls, not a whole number above zero`);
1314
1399
  return problems;
1315
1400
  }
1316
- const negative = (amount) => amount !== void 0 && (!isTime(amount) || valueIn(amount, "s") < 0);
1401
+ const negative = (amount) => amount !== void 0 && (!isTime$1(amount) || valueIn(amount, "s") < 0);
1317
1402
  return negative(timing.failAfter) || negative(timing.recoverAfter) ? [`the ${policy} window is not a time of zero or more`] : [];
1318
1403
  };
1319
1404
  /** This function gives the problems of the rules of the shutdown: an unknown name, or an exit code that a process cannot give. */
1320
- const shutdownProblems = (floor, repeat, triggers, exitCodes) => {
1405
+ const shutdownProblems = (floor, repeat, triggers, exitCodes, messages) => {
1321
1406
  const problems = [];
1322
1407
  if (floor !== void 0 && floor !== "half" && floor !== "none") problems.push(`the cooperative floor ${String(floor)} is not "half" or "none"`);
1323
1408
  if (repeat !== void 0 && repeat !== "ignore" && repeat !== "exit") problems.push(`the policy of a repeat signal ${String(repeat)} is not "ignore" or "exit"`);
1324
1409
  for (const trigger of triggers) if (!PROCESS_TRIGGERS.includes(trigger)) problems.push(`the trigger ${String(trigger)} is not a process trigger (${PROCESS_TRIGGERS.join(", ")})`);
1410
+ if (triggers.includes("ipc-message") && messages.length === 0) problems.push(`the trigger ipc-message has no message to match: declare it with svc.trigger("ipc-message", { match })`);
1411
+ for (const message of messages) {
1412
+ if (!isMessageShape(message.match)) problems.push(`the match of a message trigger is not a value of JSON: ${String(message.match)}`);
1413
+ if (message.as !== void 0 && !isExitSignal(message.as)) problems.push(`a message trigger stands for ${String(message.as)}, not an exit signal (${EXIT_SIGNALS.join(", ")})`);
1414
+ }
1325
1415
  for (const [trigger, code] of Object.entries(exitCodes)) {
1326
1416
  if (!(EXIT_SIGNALS.includes(trigger) || triggers.includes(trigger))) problems.push(`the exit code of ${trigger} names no exit signal and no declared trigger`);
1327
1417
  if (code === void 0 || !isExitCode(code)) problems.push(`the exit code of ${trigger} is ${String(code)}, not a whole number from 0 to 255`);
@@ -1346,6 +1436,7 @@ const service = (name) => {
1346
1436
  let floor;
1347
1437
  let repeat;
1348
1438
  const triggers = [];
1439
+ const messages = [];
1349
1440
  const exitCodes = {};
1350
1441
  let started = false;
1351
1442
  let partial = false;
@@ -1567,7 +1658,13 @@ const service = (name) => {
1567
1658
  },
1568
1659
  trigger(...given) {
1569
1660
  composing("a trigger");
1570
- for (const each of given) if (!triggers.includes(each)) triggers.push(each);
1661
+ const [first, message] = given;
1662
+ if (first === "ipc-message" && typeof message === "object" && message !== null) {
1663
+ if (!triggers.includes("ipc-message")) triggers.push("ipc-message");
1664
+ messages.push({ ...message });
1665
+ return builder;
1666
+ }
1667
+ for (const each of given) if (typeof each === "string" && !triggers.includes(each)) triggers.push(each);
1571
1668
  return builder;
1572
1669
  },
1573
1670
  exitCode(trigger, code) {
@@ -1586,6 +1683,7 @@ const service = (name) => {
1586
1683
  if (given.cooperativeFloor !== void 0) builder.cooperativeFloor(given.cooperativeFloor);
1587
1684
  if (given.onRepeatSignal !== void 0) builder.onRepeatSignal(given.onRepeatSignal);
1588
1685
  if (given.triggers !== void 0) builder.trigger(...given.triggers);
1686
+ for (const message of given.messages ?? []) builder.trigger("ipc-message", message);
1589
1687
  for (const [trigger, code] of Object.entries(given.exitCodes ?? {})) if (code !== void 0) builder.exitCode(trigger, code);
1590
1688
  return builder;
1591
1689
  },
@@ -1607,11 +1705,11 @@ const service = (name) => {
1607
1705
  for (const gate of ["readiness", "liveness"]) problems.push(...timingProblems(gate, policies[gate]));
1608
1706
  for (const check of checks) {
1609
1707
  if (check.dependency !== null && !known.has(check.dependency)) problems.push(`check ${check.name} exercises ${check.dependency}, which is not a dependency`);
1610
- if (!isTime(check.poll.interval) || !isTime(check.poll.timeout)) problems.push(`check ${check.name} polls in a unit that is not a time`);
1708
+ 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`);
1611
1709
  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`);
1612
1710
  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`);
1613
1711
  const timing = check.gates === null ? null : policies[check.gates];
1614
- 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`);
1712
+ 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`);
1615
1713
  }
1616
1714
  for (const controller of controllers) {
1617
1715
  for (const needed of controller.requires) if (!known.has(needed)) problems.push(`controller ${controller.name} requires ${needed}, which is not a dependency`);
@@ -1619,8 +1717,8 @@ const service = (name) => {
1619
1717
  for (const declared of controller.patterns) problems.push(...routePatternProblems(`controller ${controller.name}`, declared));
1620
1718
  }
1621
1719
  for (const dependency of dependencies) for (const [name, operation] of Object.entries(dependency.operations)) problems.push(...operationProblems(`dependency ${dependency.name}`, name, operation));
1622
- for (const phase of phases) if (!isTime(phase.cap)) problems.push(`phase ${phase.name}'s cap is not a time`);
1623
- problems.push(...shutdownProblems(floor, repeat, triggers, exitCodes));
1720
+ for (const phase of phases) if (!isTime$1(phase.cap)) problems.push(`phase ${phase.name}'s cap is not a time`);
1721
+ problems.push(...shutdownProblems(floor, repeat, triggers, exitCodes, messages));
1624
1722
  const ranked = phases.map((phase) => phase.name).filter((each) => stageRank(each) >= 0);
1625
1723
  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(", ")}`);
1626
1724
  if (problems.length > 0) throw new Error(`${name}: ${problems.join("; ")}`);
@@ -1663,6 +1761,7 @@ const service = (name) => {
1663
1761
  ...floor === void 0 ? {} : { cooperativeFloor: floor },
1664
1762
  onRepeatSignal: repeat,
1665
1763
  ...triggers.length === 0 ? {} : { triggers: [...triggers] },
1764
+ ...messages.length === 0 ? {} : { messages: messages.map((each) => ({ ...each })) },
1666
1765
  ...Object.keys(exitCodes).length === 0 ? {} : { exitCodes: { ...exitCodes } }
1667
1766
  }
1668
1767
  };
@@ -1807,6 +1906,7 @@ const SERVICE_RULES = [
1807
1906
  "O17",
1808
1907
  "O18",
1809
1908
  "O19",
1909
+ "O20",
1810
1910
  "O21",
1811
1911
  "O22",
1812
1912
  "O23",
@@ -1816,6 +1916,14 @@ const SERVICE_RULES = [
1816
1916
  const secondsOf = (amount) => rationals.toNumber(convertExact(rationals.fromNumber(amount.value), amount.unit, "s"));
1817
1917
  /** This function gives an amount, or no bound, as the law binds it. */
1818
1918
  const boundOf = (amount) => amount.value === "infinite" ? "inf" : secondsOf(amount);
1919
+ /** This function tells if a unit is a time. A unit that the package does not know is not a time. */
1920
+ const isTime = (unit) => {
1921
+ try {
1922
+ return convertible(unit, "s");
1923
+ } catch {
1924
+ return false;
1925
+ }
1926
+ };
1819
1927
  /** This function gives the largest of some numbers of seconds, or undefined when there are none. */
1820
1928
  const most = (values) => values.length === 0 ? void 0 : Math.max(...values);
1821
1929
  /** This function gives what the declaration tells about the service, as symbols of the law, and the source of each. */
@@ -1857,7 +1965,30 @@ const dependencyBindings = (dependency, scenario) => {
1857
1965
  for (const [symbol, amount] of Object.entries(scenario.dependencies?.[dependency.name] ?? {})) bind[symbol.includes("@") ? symbol : `${symbol}@dependency`] = boundOf(amount);
1858
1966
  return bind;
1859
1967
  };
1968
+ /**
1969
+ * These are the hops of the law at which the knobs of a transport bind, by its kind. A worker does
1970
+ * its units as a consumer does, thus both bind at `consumer` (O20). A server binds at no hop yet.
1971
+ */
1972
+ const TRANSPORT_HOPS = {
1973
+ server: null,
1974
+ consumer: "consumer",
1975
+ worker: "consumer"
1976
+ };
1977
+ /**
1978
+ * This function gives the time knobs of a transport at the hop of its kind, and the values of the
1979
+ * scenario for its source. A knob with a hop in its name binds at that hop. A count binds no symbol.
1980
+ */
1981
+ const transportBindings = (transport, hop, scenario) => {
1982
+ const bind = {};
1983
+ for (const [knob, amount] of Object.entries(transport.knobs)) {
1984
+ if (!isTime(amount.unit)) continue;
1985
+ bind[knob.includes("@") ? knob : `${knob}@${hop}`] = boundOf(amount);
1986
+ }
1987
+ for (const [symbol, amount] of Object.entries(scenario.transports?.[transport.name] ?? {})) bind[symbol] = boundOf(amount);
1988
+ return bind;
1989
+ };
1860
1990
  const SCOPED = /@(client|dependency|server)$/;
1991
+ const TRANSPORT_SCOPED = /@consumer$/;
1861
1992
  /** This identifier is the rule of the findings about a field that the declaration does not give. */
1862
1993
  const DECLARATION_RULE = "declaration";
1863
1994
  /**
@@ -1875,9 +2006,9 @@ const declarationFindings = (svc) => svc.shutdown.onRepeatSignal === void 0 ? [{
1875
2006
  unbound: ["shutdown.onRepeatSignal"]
1876
2007
  }] : [];
1877
2008
  /**
1878
- * This function applies each service rule to a declaration and a scenario: one time for the
1879
- * service, and one time for each client. It also gives a finding for each necessary field that the
1880
- * description does not give (`DECLARATION_RULE`).
2009
+ * This function applies each service rule to a declaration and a scenario. A rule applies one time
2010
+ * for the service, for each client, or for each consumer and worker. The function also gives a
2011
+ * finding for each necessary field that the description does not give (`DECLARATION_RULE`).
1881
2012
  */
1882
2013
  const lint = (svc, scenario, rules) => {
1883
2014
  const service = serviceBindings(svc, scenario).bind;
@@ -1885,7 +2016,13 @@ const lint = (svc, scenario, rules) => {
1885
2016
  for (const rule of rules) {
1886
2017
  if (!SERVICE_RULES.includes(rule.id)) continue;
1887
2018
  const symbols = symbolsOf(rule);
1888
- const scopes = symbols.some((symbol) => SCOPED.test(symbol)) ? svc.dependencies.map((dependency) => [dependency.name, {
2019
+ const scopes = symbols.some((symbol) => TRANSPORT_SCOPED.test(symbol)) ? svc.transports.flatMap((transport) => {
2020
+ const hop = TRANSPORT_HOPS[transport.kind];
2021
+ return hop === null ? [] : [[transport.name, {
2022
+ ...service,
2023
+ ...transportBindings(transport, hop, scenario)
2024
+ }]];
2025
+ }) : symbols.some((symbol) => SCOPED.test(symbol)) ? svc.dependencies.map((dependency) => [dependency.name, {
1889
2026
  ...service,
1890
2027
  ...dependencyBindings(dependency, scenario)
1891
2028
  }]) : [[svc.name, service]];
@@ -2022,6 +2159,650 @@ const kubernetes = (pod) => {
2022
2159
  };
2023
2160
  };
2024
2161
  //#endregion
2162
+ //#region src/units.ts
2163
+ /** This function gives the cause of an abort as text. */
2164
+ const causeOf = (signal) => signal.reason instanceof Error ? signal.reason.message : String(signal.reason ?? "the unit got an abort");
2165
+ /**
2166
+ * This function does one unit of work, and gives its end. With a request, the unit goes through
2167
+ * `life.request`, thus the service counts it, drains it and can cut it. A throw after the abort of
2168
+ * the transport is a cut, not a failure (S6 A10). A throw after the bound is a failure.
2169
+ */
2170
+ const runUnit = async (run) => {
2171
+ const { clock, bound, abort, life, request, unit, work } = run;
2172
+ const timeout = new AbortController();
2173
+ 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`))) : () => {};
2174
+ const call = async (tracker) => work(AbortSignal.any(tracker === void 0 ? [abort.signal, timeout.signal] : [
2175
+ abort.signal,
2176
+ timeout.signal,
2177
+ tracker
2178
+ ]));
2179
+ const thrown = (error) => {
2180
+ if (abort.signal.aborted) return {
2181
+ kind: "cut",
2182
+ cause: causeOf(abort.signal),
2183
+ retryable: false
2184
+ };
2185
+ if (timeout.signal.aborted) return {
2186
+ kind: "failed",
2187
+ error: timeout.signal.reason
2188
+ };
2189
+ return {
2190
+ kind: "failed",
2191
+ error
2192
+ };
2193
+ };
2194
+ try {
2195
+ if (request === void 0) try {
2196
+ return {
2197
+ kind: "done",
2198
+ value: await call()
2199
+ };
2200
+ } catch (error) {
2201
+ return thrown(error);
2202
+ }
2203
+ let collected;
2204
+ try {
2205
+ const route = typeof request.route === "string" ? request.route : request.route(unit);
2206
+ const detail = request.detail?.(unit);
2207
+ collected = life.request(request.controller, route, (signal) => call(signal).then((value) => ({ value })), detail === void 0 ? {} : { detail }).collect();
2208
+ } catch (error) {
2209
+ return {
2210
+ kind: "failed",
2211
+ error
2212
+ };
2213
+ }
2214
+ const { items, end } = await collected;
2215
+ if (end.kind === "done") return {
2216
+ kind: "done",
2217
+ value: items[0]?.value
2218
+ };
2219
+ if (end.kind === "failed") return thrown(end.error);
2220
+ if (end.kind === "refused") return {
2221
+ kind: "refused",
2222
+ cause: end.cause
2223
+ };
2224
+ return {
2225
+ kind: "cut",
2226
+ cause: end.cause,
2227
+ retryable: end.retryable
2228
+ };
2229
+ } finally {
2230
+ cancel();
2231
+ }
2232
+ };
2233
+ /** This function makes a flight set on a clock. */
2234
+ const flights = (clock) => {
2235
+ const units = /* @__PURE__ */ new Set();
2236
+ const idle = /* @__PURE__ */ new Set();
2237
+ const lower = /* @__PURE__ */ new Set();
2238
+ let aborted = null;
2239
+ const settle = () => {
2240
+ for (const done of [...lower]) done();
2241
+ lower.clear();
2242
+ if (units.size === 0) for (const done of [...idle]) done();
2243
+ };
2244
+ const abort = (reason) => {
2245
+ aborted ??= { reason };
2246
+ for (const unit of [...units]) if (!unit.signal.aborted) unit.abort(aborted.reason);
2247
+ };
2248
+ return {
2249
+ get size() {
2250
+ return units.size;
2251
+ },
2252
+ add(unit) {
2253
+ units.add(unit);
2254
+ if (aborted !== null && !unit.signal.aborted) unit.abort(aborted.reason);
2255
+ return () => {
2256
+ if (units.delete(unit)) settle();
2257
+ };
2258
+ },
2259
+ abort,
2260
+ clear() {
2261
+ units.clear();
2262
+ settle();
2263
+ },
2264
+ drain(deadline, signal) {
2265
+ const cooperative = () => abort(signal.reason);
2266
+ if (signal.aborted) cooperative();
2267
+ else signal.addEventListener("abort", cooperative, { once: true });
2268
+ if (units.size === 0 || deadline <= clock.now()) {
2269
+ signal.removeEventListener("abort", cooperative);
2270
+ return Promise.resolve();
2271
+ }
2272
+ return new Promise((resolve) => {
2273
+ let cancel = () => {};
2274
+ const done = () => {
2275
+ idle.delete(done);
2276
+ cancel();
2277
+ signal.removeEventListener("abort", cooperative);
2278
+ resolve();
2279
+ };
2280
+ idle.add(done);
2281
+ cancel = clock.after(Math.max(0, deadline - clock.now()), done);
2282
+ });
2283
+ },
2284
+ less() {
2285
+ return new Promise((resolve) => lower.add(resolve));
2286
+ },
2287
+ release() {
2288
+ for (const done of [...lower]) done();
2289
+ lower.clear();
2290
+ }
2291
+ };
2292
+ };
2293
+ /** This function makes a sleeper on a clock. */
2294
+ const sleeper = (clock) => {
2295
+ let wakes = 0;
2296
+ let stopped = false;
2297
+ const sleeps = /* @__PURE__ */ new Set();
2298
+ const backoffs = /* @__PURE__ */ new Set();
2299
+ const wait = (ms, into) => stopped ? Promise.resolve() : new Promise((resolve) => {
2300
+ let cancel = () => {};
2301
+ const done = () => {
2302
+ if (!into.delete(done)) return;
2303
+ cancel();
2304
+ resolve();
2305
+ };
2306
+ into.add(done);
2307
+ if (Number.isFinite(ms)) cancel = clock.after(ms, done);
2308
+ });
2309
+ return {
2310
+ get wakes() {
2311
+ return wakes;
2312
+ },
2313
+ sleep(ms, since) {
2314
+ if (since !== wakes) return Promise.resolve();
2315
+ return wait(ms, sleeps);
2316
+ },
2317
+ backoff(ms) {
2318
+ return wait(ms, backoffs);
2319
+ },
2320
+ wake() {
2321
+ wakes += 1;
2322
+ const ended = sleeps.size;
2323
+ for (const done of [...sleeps]) done();
2324
+ return ended;
2325
+ },
2326
+ stop() {
2327
+ stopped = true;
2328
+ for (const done of [...sleeps, ...backoffs]) done();
2329
+ }
2330
+ };
2331
+ };
2332
+ /** This function waits for a work until a bound on the clock, in milliseconds. */
2333
+ const settleWithin = (clock, ms, work) => new Promise((resolve) => {
2334
+ const cancel = Number.isFinite(ms) ? clock.after(ms, () => resolve({ kind: "bound" })) : () => {};
2335
+ work.then(() => {
2336
+ cancel();
2337
+ resolve({ kind: "done" });
2338
+ }, (error) => {
2339
+ cancel();
2340
+ resolve({
2341
+ kind: "failed",
2342
+ error
2343
+ });
2344
+ });
2345
+ });
2346
+ /**
2347
+ * This function gives the backoff of the knobs of a loop. The first delay is `retry_base`, and each
2348
+ * delay is two times longer, up to `retry_cap`. Each delay has a jitter up to the base, as in the Redis client.
2349
+ */
2350
+ const retryOf = (knobs, owner) => {
2351
+ const base = boundedMillisecondsOf(knobs, "retry_base", owner);
2352
+ const backoff = {
2353
+ base,
2354
+ factor: 2,
2355
+ cap: boundedMillisecondsOf(knobs, "retry_cap", owner),
2356
+ jitter: {
2357
+ kind: "add",
2358
+ amount: base
2359
+ }
2360
+ };
2361
+ const problems = backoffProblems(backoff);
2362
+ if (problems.length > 0) throw new Error(`${owner}: the retry_base and retry_cap give no backoff: ${problems.join(", ")}`);
2363
+ return backoff;
2364
+ };
2365
+ /**
2366
+ * This function gives the problems of the bounds of a loop in a declaration. The close bound fits
2367
+ * 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).
2368
+ */
2369
+ const boundProblems = (owner, closeBound, unitBound, description) => {
2370
+ const capOf = (name) => {
2371
+ const phase = description.shutdown.phases.find((each) => each.name === name);
2372
+ return phase === void 0 ? void 0 : valueIn(phase.cap, "ms");
2373
+ };
2374
+ const found = [];
2375
+ const close = capOf("close transports");
2376
+ if (close === void 0) found.push(`${owner} needs the phase "close transports" to close`);
2377
+ else if (closeBound >= close) found.push(`the close_bound of ${owner}, ${closeBound} ms, does not fit in the cap of "close transports", ${close} ms`);
2378
+ const drain = capOf("drain");
2379
+ if (drain === void 0) found.push(`${owner} needs the phase "drain" to end its units`);
2380
+ 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`);
2381
+ else if (unitBound >= drain) found.push(`the unit_processing_time of ${owner}, ${unitBound} ms, does not fit in the cap of "drain", ${drain} ms`);
2382
+ return found;
2383
+ };
2384
+ //#endregion
2385
+ //#region src/worker.ts
2386
+ const OWNER$1 = "workerLoop";
2387
+ /**
2388
+ * This function makes a worker loop as a transport (S6 A10). It has a setup, an iteration and a
2389
+ * teardown, and each lane does its iterations one at a time.
2390
+ * - The start does the setup. A failed setup gets its teardown, and the start throws.
2391
+ * - The first iteration of each lane comes after the startup ends. Each sleep and each backoff is on the clock.
2392
+ * - `stopIntake` starts no new iteration, and it ends each sleep and each backoff.
2393
+ * - `drain` waits for each iteration in flight until the deadline, and gives it the cooperative
2394
+ * abort. A throw after that abort is not a failure.
2395
+ * - `close` aborts each iteration, waits for its end, and does the teardown, all in `close_bound`.
2396
+ */
2397
+ const workerLoop = (knobs, options) => {
2398
+ const sleep = millisecondsOf(knobs, "worker_sleep", OWNER$1);
2399
+ const bound = millisecondsOf(knobs, "unit_processing_time", OWNER$1);
2400
+ const backoff = retryOf(knobs, OWNER$1);
2401
+ const closeBound = boundedMillisecondsOf(knobs, "close_bound", OWNER$1);
2402
+ const lanes = countOf(knobs, "concurrency", OWNER$1);
2403
+ if (!Number.isFinite(lanes)) throw new Error(`${OWNER$1} needs a concurrency with a bound: each lane is a loop`);
2404
+ const clock = options.clock ?? realClock;
2405
+ const random = options.random ?? Math.random;
2406
+ const running = flights(clock);
2407
+ const rests = sleeper(clock);
2408
+ const intake = new AbortController();
2409
+ const pending = /* @__PURE__ */ new Set();
2410
+ let started = false;
2411
+ let setup = null;
2412
+ let state;
2413
+ let tornDown = null;
2414
+ let closing = null;
2415
+ const report = (error, failures) => {
2416
+ try {
2417
+ options.onError?.(error, failures);
2418
+ } catch {}
2419
+ };
2420
+ /** This function does the teardown one time, after the setup ends, and only when the setup started. */
2421
+ const teardown = () => {
2422
+ if (tornDown !== null) return tornDown;
2423
+ const release = options.teardown;
2424
+ const made = setup;
2425
+ tornDown = made === null || release === void 0 ? Promise.resolve() : made.then((given) => release(given), () => release(void 0));
2426
+ return tornDown;
2427
+ };
2428
+ const keep = (work) => {
2429
+ pending.add(work);
2430
+ const forget = () => {
2431
+ pending.delete(work);
2432
+ };
2433
+ work.then(forget, forget);
2434
+ return work;
2435
+ };
2436
+ /** This function is one lane: an iteration, then its sleep or its backoff, until the intake stops. */
2437
+ const lane = async (index, life) => {
2438
+ let failures = 0;
2439
+ let since = rests.wakes;
2440
+ await rests.sleep(0, since);
2441
+ for (;;) {
2442
+ if (intake.signal.aborted) return;
2443
+ since = rests.wakes;
2444
+ const abort = new AbortController();
2445
+ const remove = running.add(abort);
2446
+ const count = failures;
2447
+ const end = await runUnit({
2448
+ clock,
2449
+ bound,
2450
+ abort,
2451
+ life,
2452
+ request: options.request,
2453
+ unit: index,
2454
+ work: (signal) => keep((async () => options.iteration({
2455
+ signal,
2456
+ intake: intake.signal,
2457
+ state,
2458
+ lane: index,
2459
+ failures: count,
2460
+ life
2461
+ }))())
2462
+ });
2463
+ remove();
2464
+ if (end.kind === "cut") return;
2465
+ if (end.kind === "done") {
2466
+ failures = 0;
2467
+ await rests.sleep(end.value === "more" ? 0 : sleep, end.value === "more" ? rests.wakes : since);
2468
+ continue;
2469
+ }
2470
+ failures += 1;
2471
+ report(end.kind === "failed" ? end.error : /* @__PURE__ */ new Error(`the service refused the iteration: ${end.cause}`), failures);
2472
+ await rests.backoff(backoffDelay(backoff, failures, random));
2473
+ }
2474
+ };
2475
+ return {
2476
+ knobs: { ...knobs },
2477
+ async start(signal, life) {
2478
+ if (closing !== null) throw new Error(`${OWNER$1}: a closed loop does not start again`);
2479
+ if (started) throw new Error(`${OWNER$1}: the loop started already`);
2480
+ started = true;
2481
+ const make = options.setup;
2482
+ setup = make === void 0 ? Promise.resolve(void 0) : new Promise((resolve) => resolve(make(signal)));
2483
+ try {
2484
+ await setup.then((given) => {
2485
+ state = given;
2486
+ });
2487
+ } catch (error) {
2488
+ await settleWithin(clock, closeBound, teardown());
2489
+ throw error;
2490
+ }
2491
+ life.started.then(() => {
2492
+ if (intake.signal.aborted || life.state !== "running") return;
2493
+ for (let index = 0; index < lanes; index += 1) lane(index, life).catch((error) => report(error, 0));
2494
+ });
2495
+ },
2496
+ stopIntake() {
2497
+ if (intake.signal.aborted) return;
2498
+ intake.abort(/* @__PURE__ */ new Error(`${OWNER$1} stopped its intake`));
2499
+ rests.stop();
2500
+ },
2501
+ drain(deadline, signal) {
2502
+ return running.drain(deadline, signal);
2503
+ },
2504
+ inFlight: () => running.size,
2505
+ wake() {
2506
+ if (intake.signal.aborted) return false;
2507
+ rests.wake();
2508
+ return true;
2509
+ },
2510
+ close() {
2511
+ if (closing !== null) return closing;
2512
+ if (!intake.signal.aborted) intake.abort(/* @__PURE__ */ new Error(`${OWNER$1} closed`));
2513
+ rests.stop();
2514
+ running.abort(/* @__PURE__ */ new Error(`${OWNER$1} closed`));
2515
+ running.clear();
2516
+ closing = (async () => {
2517
+ const began = clock.now();
2518
+ const ended = await settleWithin(clock, closeBound, Promise.allSettled([...pending]));
2519
+ const down = await settleWithin(clock, Math.max(0, closeBound - (clock.now() - began)), teardown());
2520
+ if (ended.kind === "bound") throw new Error(`${OWNER$1}: an iteration did not end in the close_bound of ${closeBound} ms after its abort`);
2521
+ if (down.kind === "failed") throw down.error;
2522
+ if (down.kind === "bound") throw new Error(`${OWNER$1}: the teardown did not end in the close_bound of ${closeBound} ms`);
2523
+ })();
2524
+ return closing;
2525
+ },
2526
+ problems(description) {
2527
+ return boundProblems(OWNER$1, closeBound, bound, description);
2528
+ }
2529
+ };
2530
+ };
2531
+ //#endregion
2532
+ //#region src/queue.ts
2533
+ /**
2534
+ * This function gives the redelivery policy of S6 A11. A poison message gets `ack`. Each other job
2535
+ * goes back on the queue after its first failure, one time. After a later failure, `then` gives the
2536
+ * action: the declared policy of the queue.
2537
+ */
2538
+ const redeliverOnce = (then, poison) => (failure) => {
2539
+ if (poison?.(failure.error, failure.job) === true) return "ack";
2540
+ if (failure.delivery <= 1) return "requeue";
2541
+ return typeof then === "function" ? then(failure) : then;
2542
+ };
2543
+ const OWNER = "queueDrain";
2544
+ const ACTIONS = [
2545
+ "requeue",
2546
+ "reject",
2547
+ "ack"
2548
+ ];
2549
+ const STOPPED = Symbol("the intake stopped");
2550
+ const isAsyncIterable = (value) => value !== null && (typeof value === "object" || typeof value === "function") && typeof value[Symbol.asyncIterator] === "function";
2551
+ /**
2552
+ * This function makes a queue drain as a transport (S6 A11). One taker takes the jobs from the
2553
+ * source while a slot is free, and each job operates in its slot.
2554
+ * - A success gets `ack`. A failure gets the action of the redelivery policy.
2555
+ * - `stopIntake` takes no new job, and a job that the drain took and did not start goes back (`requeue`, `unstarted`).
2556
+ * - `drain` waits for each job in flight until the deadline, and gives it the cooperative abort.
2557
+ * - `close` aborts each job, waits for its end, and closes the source, all in `close_bound`. A job
2558
+ * that does not end in the bound gets `reject`.
2559
+ */
2560
+ const queueDrain = (knobs, options) => {
2561
+ const poll = millisecondsOf(knobs, "queue_poll_interval", OWNER);
2562
+ const bound = millisecondsOf(knobs, "unit_processing_time", OWNER);
2563
+ const backoff = retryOf(knobs, OWNER);
2564
+ const closeBound = boundedMillisecondsOf(knobs, "close_bound", OWNER);
2565
+ const concurrency = countOf(knobs, "concurrency", OWNER);
2566
+ const { settle } = options;
2567
+ const clock = options.clock ?? realClock;
2568
+ const random = options.random ?? Math.random;
2569
+ const running = flights(clock);
2570
+ const rests = sleeper(clock);
2571
+ const intake = new AbortController();
2572
+ const stopped = new Promise((resolve) => intake.signal.addEventListener("abort", () => resolve(STOPPED), { once: true }));
2573
+ const pending = /* @__PURE__ */ new Set();
2574
+ const active = /* @__PURE__ */ new Set();
2575
+ let refusals = 0;
2576
+ let pausedUntil = Number.NEGATIVE_INFINITY;
2577
+ let cancelSource = () => {};
2578
+ let started = false;
2579
+ let closing = null;
2580
+ const report = (error, job) => {
2581
+ try {
2582
+ options.onError?.(error, job);
2583
+ } catch {}
2584
+ };
2585
+ const keep = (work) => {
2586
+ pending.add(work);
2587
+ const forget = () => {
2588
+ pending.delete(work);
2589
+ };
2590
+ work.then(forget, forget);
2591
+ };
2592
+ /** This function tells the queue the end of a job, one time. A throw of the queue is a failure. */
2593
+ const once = async (unit, tell) => {
2594
+ if (unit.settled) return;
2595
+ unit.settled = true;
2596
+ try {
2597
+ await tell();
2598
+ } catch (error) {
2599
+ report(error, unit.job);
2600
+ }
2601
+ };
2602
+ const handBack = (job) => keep((async () => {
2603
+ try {
2604
+ await settle.requeue(job, "unstarted");
2605
+ } catch (error) {
2606
+ report(error, job);
2607
+ }
2608
+ })());
2609
+ /** This function gives the action of the policy for a failed job. A policy that throws, or that gives no action, rejects the job. */
2610
+ const actionOf = (job, error, delivery) => {
2611
+ try {
2612
+ const action = options.redelivery({
2613
+ job,
2614
+ error,
2615
+ delivery
2616
+ });
2617
+ if (ACTIONS.includes(action)) return action;
2618
+ report(/* @__PURE__ */ new Error(`the redelivery policy gave ${String(action)}, not requeue, reject or ack`), job);
2619
+ } catch (thrown) {
2620
+ report(thrown, job);
2621
+ }
2622
+ return "reject";
2623
+ };
2624
+ const settleEnd = (unit, end, delivery) => {
2625
+ const { job } = unit;
2626
+ if (end.kind === "done") return once(unit, () => settle.ack(job));
2627
+ if (end.kind === "refused") {
2628
+ report(/* @__PURE__ */ new Error(`the service refused the job: ${end.cause}`), job);
2629
+ return once(unit, () => settle.requeue(job, "unstarted"));
2630
+ }
2631
+ if (end.kind === "cut") {
2632
+ if (end.retryable) return once(unit, () => settle.requeue(job, "cut"));
2633
+ const cut = new Error(end.cause);
2634
+ return once(unit, () => settle.reject(job, cut));
2635
+ }
2636
+ const { error } = end;
2637
+ report(error, job);
2638
+ const action = actionOf(job, error, delivery);
2639
+ if (action === "requeue") return once(unit, () => settle.requeue(job, "failed"));
2640
+ return once(unit, () => action === "ack" ? settle.ack(job) : settle.reject(job, error));
2641
+ };
2642
+ /** This function starts one job in a slot. The slot stays in flight until the queue has the end of the job. */
2643
+ const runJob = (job, life) => {
2644
+ const unit = {
2645
+ job,
2646
+ settled: false
2647
+ };
2648
+ active.add(unit);
2649
+ const abort = new AbortController();
2650
+ const remove = running.add(abort);
2651
+ let delivery;
2652
+ try {
2653
+ delivery = settle.delivery(job);
2654
+ } catch (error) {
2655
+ report(error, job);
2656
+ delivery = Number.POSITIVE_INFINITY;
2657
+ }
2658
+ keep((async () => {
2659
+ const end = await runUnit({
2660
+ clock,
2661
+ bound,
2662
+ abort,
2663
+ life,
2664
+ request: options.request,
2665
+ unit: job,
2666
+ work: (signal) => options.handle(job, {
2667
+ signal,
2668
+ intake: intake.signal,
2669
+ delivery,
2670
+ life
2671
+ })
2672
+ });
2673
+ if (end.kind === "refused") {
2674
+ refusals += 1;
2675
+ pausedUntil = clock.now() + backoffDelay(backoff, refusals, random);
2676
+ } else refusals = 0;
2677
+ await settleEnd(unit, end, delivery);
2678
+ })().finally(() => {
2679
+ active.delete(unit);
2680
+ remove();
2681
+ }));
2682
+ };
2683
+ /** This function gives the take of the source. */
2684
+ const takeOf = () => {
2685
+ const source = options.source;
2686
+ if (isAsyncIterable(source)) {
2687
+ const iterator = source[Symbol.asyncIterator]();
2688
+ cancelSource = () => {
2689
+ try {
2690
+ Promise.resolve(iterator.return?.()).catch(() => {});
2691
+ } catch {}
2692
+ };
2693
+ return async () => {
2694
+ const step = await iterator.next();
2695
+ return step.done === true ? { kind: "end" } : {
2696
+ kind: "job",
2697
+ job: step.value
2698
+ };
2699
+ };
2700
+ }
2701
+ const pull = source;
2702
+ return () => new Promise((resolve) => resolve(pull(intake.signal))).then((job) => job === null || job === void 0 ? { kind: "none" } : {
2703
+ kind: "job",
2704
+ job
2705
+ });
2706
+ };
2707
+ /** This function is the taker: it takes a job while a slot is free, until the intake stops or the source ends. */
2708
+ const taker = async (life) => {
2709
+ const take = takeOf();
2710
+ let failures = 0;
2711
+ for (;;) {
2712
+ if (intake.signal.aborted) return;
2713
+ if (running.size >= concurrency) {
2714
+ await running.less();
2715
+ continue;
2716
+ }
2717
+ const pause = pausedUntil - clock.now();
2718
+ if (pause > 0) {
2719
+ await rests.backoff(pause);
2720
+ continue;
2721
+ }
2722
+ const since = rests.wakes;
2723
+ const taking = take().catch((error) => ({
2724
+ kind: "error",
2725
+ error
2726
+ }));
2727
+ const first = await Promise.race([taking, stopped]);
2728
+ if (first === STOPPED || intake.signal.aborted) {
2729
+ if (first === STOPPED) taking.then((late) => {
2730
+ if (late.kind === "job") handBack(late.job);
2731
+ });
2732
+ else if (first.kind === "job") handBack(first.job);
2733
+ return;
2734
+ }
2735
+ if (first.kind === "error") {
2736
+ failures += 1;
2737
+ report(first.error, void 0);
2738
+ await rests.backoff(backoffDelay(backoff, failures, random));
2739
+ continue;
2740
+ }
2741
+ failures = 0;
2742
+ if (first.kind === "end") {
2743
+ report(/* @__PURE__ */ new Error(`the source of ${OWNER} ended while its intake was open: it takes no more jobs`), void 0);
2744
+ return;
2745
+ }
2746
+ if (first.kind === "none") {
2747
+ await rests.sleep(poll, since);
2748
+ continue;
2749
+ }
2750
+ runJob(first.job, life);
2751
+ }
2752
+ };
2753
+ const stopTaking = (cause) => {
2754
+ if (intake.signal.aborted) return;
2755
+ intake.abort(new Error(cause));
2756
+ rests.stop();
2757
+ running.release();
2758
+ cancelSource();
2759
+ };
2760
+ return {
2761
+ knobs: { ...knobs },
2762
+ start(_signal, life) {
2763
+ if (closing !== null) throw new Error(`${OWNER}: a closed drain does not start again`);
2764
+ if (started) throw new Error(`${OWNER}: the drain started already`);
2765
+ started = true;
2766
+ life.started.then(() => {
2767
+ if (intake.signal.aborted || life.state !== "running") return;
2768
+ taker(life).catch((error) => report(error, void 0));
2769
+ });
2770
+ },
2771
+ stopIntake() {
2772
+ stopTaking(`${OWNER} stopped its intake`);
2773
+ },
2774
+ drain(deadline, signal) {
2775
+ return running.drain(deadline, signal);
2776
+ },
2777
+ inFlight: () => running.size,
2778
+ wake() {
2779
+ if (intake.signal.aborted) return false;
2780
+ rests.wake();
2781
+ return true;
2782
+ },
2783
+ close() {
2784
+ if (closing !== null) return closing;
2785
+ stopTaking(`${OWNER} closed`);
2786
+ running.abort(/* @__PURE__ */ new Error(`${OWNER} closed`));
2787
+ running.clear();
2788
+ closing = (async () => {
2789
+ const began = clock.now();
2790
+ const ended = await settleWithin(clock, closeBound, Promise.allSettled([...pending]));
2791
+ if (ended.kind === "bound") for (const unit of [...active]) once(unit, () => settle.reject(unit.job, /* @__PURE__ */ new Error(`${OWNER} closed with the job in flight`)));
2792
+ const shut = options.close;
2793
+ const down = await settleWithin(clock, Math.max(0, closeBound - (clock.now() - began)), shut === void 0 ? Promise.resolve() : (async () => shut())());
2794
+ if (ended.kind === "bound") throw new Error(`${OWNER}: a job did not end in the close_bound of ${closeBound} ms after its abort`);
2795
+ if (down.kind === "failed") throw down.error;
2796
+ if (down.kind === "bound") throw new Error(`${OWNER}: the close of the source did not end in the close_bound of ${closeBound} ms`);
2797
+ })();
2798
+ return closing;
2799
+ },
2800
+ problems(description) {
2801
+ return boundProblems(OWNER, closeBound, bound, description);
2802
+ }
2803
+ };
2804
+ };
2805
+ //#endregion
2025
2806
  //#region src/machines.ts
2026
2807
  /**
2027
2808
  * The machines of the runtime as `mark1russell7.machine/1` documents. The documents are the
@@ -2471,6 +3252,6 @@ const MACHINES = {
2471
3252
  watchdog: WATCHDOG_MACHINE
2472
3253
  };
2473
3254
  //#endregion
2474
- export { DECLARATION_RULE, EXIT_MACHINE, EXIT_SIGNALS, FORMAT, GATE_MACHINE, LATCH_MACHINE, MACHINES, POLLER_MACHINE, PROCESS_TRIGGERS, SERVICE_RULES, STAGES, WATCHDOG_MACHINE, WINDOW_MACHINE, asDebounce, asWindow, backoffBand, backoffDelay, backoffProblems, capKnob, cooperativeAbortAt, daemon, detectionTime, drainMachine, ephemeral, evaluate, failSpan, heartbeat, heartbeatDetection, inequalities, isExitCode, isExitSignal, kubernetes, lint, localDev, mayReissue, nodeProcess, platformClock, processPortOf, realClock, recoverSpan, replay, service, serviceBindings, stageRank, symbolsOf, verdictOf, watchdog };
3255
+ export { DECLARATION_RULE, EXIT_MACHINE, EXIT_SIGNALS, FORMAT, GATE_MACHINE, LATCH_MACHINE, MACHINES, POLLER_MACHINE, PROCESS_TRIGGERS, SERVICE_RULES, STAGES, WATCHDOG_MACHINE, WINDOW_MACHINE, asDebounce, asWindow, backoffBand, backoffDelay, backoffProblems, capKnob, cooperativeAbortAt, daemon, detectionTime, drainMachine, embeddedProcess, ephemeral, evaluate, failSpan, heartbeat, heartbeatDetection, inequalities, isExitCode, isExitSignal, isMessageShape, kubernetes, lint, localDev, matchesMessage, mayReissue, nodeProcess, platformClock, processPortOf, queueDrain, realClock, recoverSpan, redeliverOnce, replay, service, serviceBindings, stageRank, symbolsOf, verdictOf, watchdog, workerLoop };
2475
3256
 
2476
3257
  //# sourceMappingURL=index.js.map