@mark1russell7/service 0.5.1 → 0.5.3

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
@@ -2,7 +2,9 @@ import { platformClock, realClock } from "./clock.js";
2
2
  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
+ import { a as limiterProblems, i as limitRule, n as freeAt, o as longestRetryAfter, r as idleSpan, s as retryAfterSeconds, t as decide } from "./policy-CgS4y94U.js";
5
6
  import { backoffBand, backoffDelay, backoffProblems } from "./backoff.js";
7
+ import { n as memoryStore, t as keyedLimiter } from "./limiter-wv0uXDk9.js";
6
8
  import { n as countOf, r as millisecondsOf, t as boundedMillisecondsOf } from "./knobs-BxG-c7Zb.js";
7
9
  import { convertExact, convertible, quantity, rationals, valueIn } from "@mark1russell7/quantity";
8
10
  import { replay } from "@mark1russell7/algebra/replay";
@@ -616,6 +618,33 @@ const processPortOf = (find) => {
616
618
  };
617
619
  /** This port is the process of Node. */
618
620
  const nodeProcess = processPortOf(() => globalThis.process);
621
+ /**
622
+ * This function gives a process port for a lifecycle in a process that another part owns. That
623
+ * part is a script, a test of an application, or a library entry. The port adds no handler of a signal or of
624
+ * a trigger, it does not hold the process, and its `exit` only records the code. The caller stops
625
+ * the lifecycle with `life.shutdown()`. A process with a service of its own gives its parts to
626
+ * that service, and does not use this port.
627
+ */
628
+ const embeddedProcess = () => {
629
+ let code = null;
630
+ let resolve = () => {};
631
+ return {
632
+ on() {
633
+ return () => {};
634
+ },
635
+ exit(requested) {
636
+ if (code !== null) return;
637
+ code = requested;
638
+ resolve(requested);
639
+ },
640
+ get exitCode() {
641
+ return code;
642
+ },
643
+ exited: new Promise((done) => {
644
+ resolve = done;
645
+ })
646
+ };
647
+ };
619
648
  //#endregion
620
649
  //#region src/routes.ts
621
650
  const NAME = /^[A-Za-z_][A-Za-z0-9_]*/;
@@ -1400,6 +1429,7 @@ const service = (name) => {
1400
1429
  const choices = /* @__PURE__ */ new Map();
1401
1430
  const checks = [];
1402
1431
  const runs = /* @__PURE__ */ new Map();
1432
+ const limiters = [];
1403
1433
  const couplings = [];
1404
1434
  const phases = [];
1405
1435
  let readiness;
@@ -1560,6 +1590,19 @@ const service = (name) => {
1560
1590
  }
1561
1591
  return builder;
1562
1592
  },
1593
+ limiter(entry, spec) {
1594
+ composing(`the limiter ${entry}`);
1595
+ once(limiters, entry, "limiter");
1596
+ const given = spec ?? {};
1597
+ limiters.push({
1598
+ name: entry,
1599
+ ...given,
1600
+ ...given.policy === void 0 ? {} : { policy: { ...given.policy } },
1601
+ ...given.store === void 0 ? {} : { store: { ...given.store } },
1602
+ ...given.evict === void 0 ? {} : { evict: { ...given.evict } }
1603
+ });
1604
+ return builder;
1605
+ },
1563
1606
  healthServer(server) {
1564
1607
  composing("the health server");
1565
1608
  if (probes !== null) throw new Error(`${name} already has a health server`);
@@ -1689,6 +1732,11 @@ const service = (name) => {
1689
1732
  for (const [route, operation] of Object.entries(controller.routes)) problems.push(...operationProblems(`controller ${controller.name}`, route, operation));
1690
1733
  for (const declared of controller.patterns) problems.push(...routePatternProblems(`controller ${controller.name}`, declared));
1691
1734
  }
1735
+ for (const limiter of limiters) {
1736
+ const { name: entry, ...spec } = limiter;
1737
+ problems.push(...limiterProblems(entry, spec));
1738
+ if (limiter.store?.kind === "shared" && typeof limiter.store.dependency === "string" && !known.has(limiter.store.dependency)) problems.push(`limiter ${entry} keeps its counts in ${limiter.store.dependency}, which is not a dependency`);
1739
+ }
1692
1740
  for (const dependency of dependencies) for (const [name, operation] of Object.entries(dependency.operations)) problems.push(...operationProblems(`dependency ${dependency.name}`, name, operation));
1693
1741
  for (const phase of phases) if (!isTime$1(phase.cap)) problems.push(`phase ${phase.name}'s cap is not a time`);
1694
1742
  problems.push(...shutdownProblems(floor, repeat, triggers, exitCodes, messages));
@@ -1722,6 +1770,12 @@ const service = (name) => {
1722
1770
  ...entry,
1723
1771
  poll: { ...entry.poll }
1724
1772
  })),
1773
+ ...limiters.length === 0 ? {} : { limiters: limiters.map((entry) => ({
1774
+ ...entry,
1775
+ policy: { ...entry.policy },
1776
+ store: { ...entry.store },
1777
+ evict: { maxKeys: { ...entry.evict.maxKeys } }
1778
+ })) },
1725
1779
  policies: {
1726
1780
  readiness,
1727
1781
  liveness,
@@ -1883,8 +1937,74 @@ const SERVICE_RULES = [
1883
1937
  "O21",
1884
1938
  "O22",
1885
1939
  "O23",
1886
- "O27"
1940
+ "O27",
1941
+ "O29",
1942
+ "O30"
1887
1943
  ];
1944
+ /**
1945
+ * These are the two rules of the law for a keyed limiter, as data. The caller of `lint()` gives
1946
+ * them, as it gives each other rule. Each one applies one time for each limiter.
1947
+ * - O29: the longest `Retry-After` of a limiter is at most the retry window of its caller. Its
1948
+ * second inequality applies the same bound to a new key at the cap of keys.
1949
+ * Otherwise the caller stops before the limit lets its call in, and a refusal becomes a failure.
1950
+ * - O30: the least delay of a caller before it sends a refused call again is at least the longest
1951
+ * `Retry-After` of the policy. Otherwise a caller that does not read the header sends again too
1952
+ * early, and the limiter refuses each early call again.
1953
+ *
1954
+ * The identifiers O29 and O30 are provisional. The corpus of the law in jarvis (R3, track B) has 28
1955
+ * rules, O01 to O28. It does not have these two rules yet, thus they take the next two identifiers.
1956
+ */
1957
+ const LIMITER_RULES = [{
1958
+ id: "O29",
1959
+ rule: "the longest Retry-After of a limiter, at its cap of keys too, is at most the retry window of its caller",
1960
+ window: "the caller stops before the limit lets its call in, thus a refusal becomes a failure",
1961
+ expr: {
1962
+ lhs: { terms: [{
1963
+ knob: "retry_after",
1964
+ coef: 1,
1965
+ at: "limiter"
1966
+ }] },
1967
+ op: "<=",
1968
+ rhs: { terms: [{
1969
+ knob: "client_retry_window",
1970
+ coef: 1,
1971
+ at: "caller"
1972
+ }] },
1973
+ margin: 0
1974
+ },
1975
+ expr_more: [{
1976
+ lhs: { terms: [{
1977
+ knob: "retry_after_at_cap",
1978
+ coef: 1,
1979
+ at: "limiter"
1980
+ }] },
1981
+ op: "<=",
1982
+ rhs: { terms: [{
1983
+ knob: "client_retry_window",
1984
+ coef: 1,
1985
+ at: "caller"
1986
+ }] },
1987
+ margin: 0
1988
+ }]
1989
+ }, {
1990
+ id: "O30",
1991
+ rule: "the least delay of a caller before it sends a refused call again is at least the longest Retry-After of the limiter",
1992
+ window: "a caller that does not read Retry-After sends again too early, and the limiter refuses each early call again",
1993
+ expr: {
1994
+ lhs: { terms: [{
1995
+ knob: "retry_delay",
1996
+ coef: 1,
1997
+ at: "caller"
1998
+ }] },
1999
+ op: ">=",
2000
+ rhs: { terms: [{
2001
+ knob: "retry_after",
2002
+ coef: 1,
2003
+ at: "limiter"
2004
+ }] },
2005
+ margin: 0
2006
+ }
2007
+ }];
1888
2008
  /** This function gives a finite amount in seconds. */
1889
2009
  const secondsOf = (amount) => rationals.toNumber(convertExact(rationals.fromNumber(amount.value), amount.unit, "s"));
1890
2010
  /** This function gives an amount, or no bound, as the law binds it. */
@@ -1960,8 +2080,24 @@ const transportBindings = (transport, hop, scenario) => {
1960
2080
  for (const [symbol, amount] of Object.entries(scenario.transports?.[transport.name] ?? {})) bind[symbol] = boundOf(amount);
1961
2081
  return bind;
1962
2082
  };
2083
+ /**
2084
+ * This function gives the symbols of a limiter, and the values of the scenario for its callers.
2085
+ * - `retry_after@limiter` is the longest `Retry-After` of a refusal by the policy, in whole seconds.
2086
+ * It is one interval of the bucket, or one window, as the HTTP edge sends it.
2087
+ * - `retry_after_at_cap@limiter` is the longest `Retry-After` of a new key at the cap of keys. It is
2088
+ * the idle span, `burst / rate` or one window.
2089
+ */
2090
+ const limiterBindings = (limiter, scenario) => {
2091
+ const bind = {
2092
+ "retry_after@limiter": retryAfterSeconds(longestRetryAfter(limiter.policy)),
2093
+ "retry_after_at_cap@limiter": retryAfterSeconds(idleSpan(limiter.policy))
2094
+ };
2095
+ for (const [symbol, amount] of Object.entries(scenario.limiters?.[limiter.name] ?? {})) bind[symbol.includes("@") ? symbol : `${symbol}@caller`] = boundOf(amount);
2096
+ return bind;
2097
+ };
1963
2098
  const SCOPED = /@(client|dependency|server)$/;
1964
2099
  const TRANSPORT_SCOPED = /@consumer$/;
2100
+ const LIMITER_SCOPED = /@(limiter|caller)$/;
1965
2101
  /** This identifier is the rule of the findings about a field that the declaration does not give. */
1966
2102
  const DECLARATION_RULE = "declaration";
1967
2103
  /**
@@ -1980,8 +2116,8 @@ const declarationFindings = (svc) => svc.shutdown.onRepeatSignal === void 0 ? [{
1980
2116
  }] : [];
1981
2117
  /**
1982
2118
  * This function applies each service rule to a declaration and a scenario. A rule applies one time
1983
- * for the service, for each client, or for each consumer and worker. The function also gives a
1984
- * finding for each necessary field that the description does not give (`DECLARATION_RULE`).
2119
+ * for the service, for each client, for each consumer and worker, or for each limiter. The function
2120
+ * also gives a finding for each necessary field that the description does not give (`DECLARATION_RULE`).
1985
2121
  */
1986
2122
  const lint = (svc, scenario, rules) => {
1987
2123
  const service = serviceBindings(svc, scenario).bind;
@@ -1989,7 +2125,10 @@ const lint = (svc, scenario, rules) => {
1989
2125
  for (const rule of rules) {
1990
2126
  if (!SERVICE_RULES.includes(rule.id)) continue;
1991
2127
  const symbols = symbolsOf(rule);
1992
- const scopes = symbols.some((symbol) => TRANSPORT_SCOPED.test(symbol)) ? svc.transports.flatMap((transport) => {
2128
+ const scopes = symbols.some((symbol) => LIMITER_SCOPED.test(symbol)) ? (svc.limiters ?? []).map((limiter) => [limiter.name, {
2129
+ ...service,
2130
+ ...limiterBindings(limiter, scenario)
2131
+ }]) : symbols.some((symbol) => TRANSPORT_SCOPED.test(symbol)) ? svc.transports.flatMap((transport) => {
1993
2132
  const hop = TRANSPORT_HOPS[transport.kind];
1994
2133
  return hop === null ? [] : [[transport.name, {
1995
2134
  ...service,
@@ -3225,6 +3364,6 @@ const MACHINES = {
3225
3364
  watchdog: WATCHDOG_MACHINE
3226
3365
  };
3227
3366
  //#endregion
3228
- 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, isMessageShape, kubernetes, lint, localDev, matchesMessage, mayReissue, nodeProcess, platformClock, processPortOf, queueDrain, realClock, recoverSpan, redeliverOnce, replay, service, serviceBindings, stageRank, symbolsOf, verdictOf, watchdog, workerLoop };
3367
+ export { DECLARATION_RULE, EXIT_MACHINE, EXIT_SIGNALS, FORMAT, GATE_MACHINE, LATCH_MACHINE, LIMITER_RULES, MACHINES, POLLER_MACHINE, PROCESS_TRIGGERS, SERVICE_RULES, STAGES, 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, limiterProblems, lint, localDev, longestRetryAfter, matchesMessage, mayReissue, memoryStore, nodeProcess, platformClock, processPortOf, queueDrain, realClock, recoverSpan, redeliverOnce, replay, retryAfterSeconds, service, serviceBindings, stageRank, symbolsOf, verdictOf, watchdog, workerLoop };
3229
3368
 
3230
3369
  //# sourceMappingURL=index.js.map