@mark1russell7/service 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing.js","names":[],"sources":["../src/fakes.ts","../src/contract.ts","../src/testing.ts"],"sourcesContent":["// The fakes of the runtime for tests: a fake clock gives exact, repeatable timing, a seeded random\n// gives the same jitter on each run, and a fake process takes signals and keeps the exit codes.\n\nimport type { Cancel, Clock } from \"./clock.ts\";\nimport type { ExitSignal, ProcessPort } from \"./process.ts\";\n\n/** A fake clock moves only when a test tells it to. */\nexport interface FakeClock extends Clock {\n /** This method moves the clock forward by `ms`, and it starts each timer that comes due, in sequence. */\n advance(ms: number): Promise<void>;\n /**\n * This method moves the clock forward by `ms` and starts no timer, as a blocked event loop does.\n * The next `advance` then starts each timer that came due during the block, late.\n */\n block(ms: number): void;\n /** This count is the number of timers that are not stopped and not called. */\n readonly pending: number;\n}\n\n/** This function lets the promises that are ready continue, before the clock moves again. */\nconst settle = async (): Promise<void> => {\n for (let turn = 0; turn < 64; turn += 1) await Promise.resolve();\n};\n\n/** This function makes a fake clock. Its time starts at `start` milliseconds. */\nexport const fakeClock = (start = 0): FakeClock => {\n let now = start;\n let sequence = 0;\n const timers: Array<{ at: number; order: number; run: () => void; live: boolean }> = [];\n const due = (end: number) => {\n let best: (typeof timers)[number] | undefined;\n for (const timer of timers)\n if (timer.live && timer.at <= end && (best === undefined || timer.at < best.at || (timer.at === best.at && timer.order < best.order)))\n best = timer;\n return best;\n };\n return {\n now: () => now,\n after(ms, run): Cancel {\n const timer = { at: now + Math.max(0, ms), order: sequence++, run, live: true };\n timers.push(timer);\n return () => {\n timer.live = false;\n };\n },\n async advance(ms) {\n // a monotonic clock does not go back\n const end = now + Math.max(0, ms);\n await settle();\n for (;;) {\n const timer = due(end);\n if (timer === undefined) break;\n timer.live = false;\n // a timer that came due during a block runs late, at the time of the clock\n now = Math.max(now, timer.at);\n timer.run();\n await settle();\n }\n now = Math.max(now, end);\n for (let at = timers.length - 1; at >= 0; at -= 1) if (!timers[at]!.live) timers.splice(at, 1);\n await settle();\n },\n block(ms) {\n now += Math.max(0, ms);\n },\n get pending() {\n return timers.filter((timer) => timer.live).length;\n },\n };\n};\n\n/** A fake process receives signals from a test, and keeps each exit code in place of an exit. */\nexport interface FakeProcess extends ProcessPort {\n /** This method sends a signal to each handler of it. */\n signal(name: ExitSignal): void;\n /** These are the exit codes, in sequence. A correct runtime exits one time. */\n readonly exits: readonly number[];\n /** This count is the number of handlers that are registered. */\n readonly handlers: number;\n}\n\n/** This function makes a fake process. */\nexport const fakeProcess = (): FakeProcess => {\n const handlers = new Map<ExitSignal, Set<() => void>>();\n const exits: number[] = [];\n return {\n on(signal, handler) {\n const set = handlers.get(signal) ?? new Set();\n set.add(handler);\n handlers.set(signal, set);\n return () => {\n set.delete(handler);\n };\n },\n exit(code) {\n exits.push(code);\n },\n signal(name) {\n for (const handler of [...(handlers.get(name) ?? [])]) handler();\n },\n get exits() {\n return [...exits];\n },\n get handlers() {\n let count = 0;\n for (const set of handlers.values()) count += set.size;\n return count;\n },\n };\n};\n\n/** This function gives a seeded random number generator (mulberry32), with numbers in [0, 1). */\nexport const seeded = (seed: number): (() => number) => {\n let state = seed >>> 0;\n return () => {\n state = (state + 0x6d2b79f5) >>> 0;\n let t = state;\n t = Math.imul(t ^ (t >>> 15), t | 1);\n t ^= t + Math.imul(t ^ (t >>> 7), t | 61);\n return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n };\n};\n","import { valueIn } from \"@mark1russell7/quantity\";\nimport type { ServiceBuilder } from \"./builder.ts\";\nimport type { Check, ServiceDescription } from \"./description.ts\";\nimport type { FakeClock } from \"./fakes.ts\";\nimport { fakeClock, seeded } from \"./fakes.ts\";\nimport type { Health, HealthVerdict, StartOptions } from \"./health.ts\";\nimport { failSpan, recoverSpan } from \"./timing.ts\";\n\n/** A contract case is one generated case of the contract test of a declaration. It throws on a failure. */\nexport interface ContractCase {\n readonly name: string;\n run(): Promise<void>;\n}\n\n/** These options tune the contract test. */\nexport interface ContractOptions {\n /** This number is the latency that a degraded dependency adds to each check, in milliseconds. The default of the contract is 100. */\n readonly degradeBy?: number | undefined;\n /** This number is the seed of the jitter. */\n readonly seed?: number | undefined;\n}\n\nconst ms = (amount: { readonly value: number; readonly unit: string }): number => valueIn(amount, \"ms\");\n\n/** The part of a verdict that the contract compares: what a caller and the platform see. */\nconst visible = (verdict: HealthVerdict) => ({\n ready: verdict.ready,\n alive: verdict.alive,\n controllers: verdict.controllers,\n dependencies: verdict.dependencies,\n});\n\nconst same = (expected: unknown, actual: unknown): boolean => JSON.stringify(expected) === JSON.stringify(actual);\n\nconst failure = (what: string, expected: unknown, actual: unknown): Error =>\n new Error(`${what}: expected ${JSON.stringify(expected)}, but the verdict was ${JSON.stringify(actual)}`);\n\n/**\n * This function generates the contract test of a declaration. `declare` gives a new builder with\n * the declaration and the function of each check, each time. The checks must pass at rest. The\n * cases are:\n * - The service at rest: each controller and each dependency is ready, and the service is alive.\n * - Each dependency unavailable: its polls fail at once (the force-fail list). The dependency and\n * each controller that needs it become not ready, and the other controllers stay ready. Liveness\n * stays, unless a liveness check exercises the dependency.\n * - Each dependency degraded: each call of its checks takes the latency longer. Nothing can fail.\n * - Each dependency recovers: after it is unavailable, the service is again at rest.\n * - A negative control: a client that the declaration does not name fails, and nothing changes.\n *\n * A dependency that no check exercises is a finding: the service cannot see it fail.\n */\nexport const contractCases = (declare: () => ServiceBuilder, options: ContractOptions = {}): ContractCase[] => {\n const degradeBy = options.degradeBy ?? 100;\n const seed = options.seed ?? 1;\n const description: ServiceDescription = declare().describe();\n const service = description.name;\n const gating = description.checks.filter((check) => check.gates !== null);\n const timingOf = (check: Check) => description.policies[check.gates!];\n const failMax = (check: Check): number => ms(failSpan(timingOf(check), check.poll, \"error\").max);\n const recoverMax = (check: Check): number => ms(recoverSpan(timingOf(check), check.poll).max);\n const most = (values: readonly number[]): number => values.reduce((a, b) => Math.max(a, b), 0);\n const settleTime = most(gating.filter((check) => check.gates === \"readiness\").map(recoverMax)) + 1;\n const quietTime = most(gating.map(failMax)) + 1;\n\n /** This function starts the declaration on a fake clock, and moves it to rest. */\n const boot = async (\n extra: (clock: FakeClock) => Partial<StartOptions> = () => ({}),\n ): Promise<{ health: Health; clock: FakeClock; rest: ReturnType<typeof visible> }> => {\n const clock = fakeClock();\n // the warm-up sweep can wait on the clock (a degraded check), thus the clock moves while it starts\n const starting = declare().start({ clock, random: seeded(seed), ...extra(clock) });\n let started = false;\n void starting.then(\n () => (started = true),\n () => (started = true),\n );\n for (let step = 0; step < 1000 && !started; step += 1) await clock.advance(10);\n const health = await starting;\n await clock.advance(settleTime);\n return { health, clock, rest: visible(health.verdict()) };\n };\n const stop = (health: Health): void => health.stop();\n\n const cases: ContractCase[] = [\n {\n name: `${service}: at rest, each controller and each dependency is ready, and the service is alive`,\n async run() {\n const { health, rest } = await boot();\n try {\n const expected = {\n ready: true,\n alive: true,\n controllers: Object.fromEntries(description.controllers.map((each) => [each.name, true])),\n dependencies: Object.fromEntries(description.dependencies.map((each) => [each.name, true])),\n };\n if (!same(expected, rest)) throw failure(\"at rest\", expected, rest);\n } finally {\n stop(health);\n }\n },\n },\n ];\n\n for (const dependency of description.dependencies) {\n const on = gating.filter((check) => check.dependency === dependency.name);\n const readiness = on.filter((check) => check.gates === \"readiness\");\n const liveness = on.filter((check) => check.gates === \"liveness\");\n const unavailable = (rest: ReturnType<typeof visible>) => ({\n ready: readiness.length === 0 ? rest.ready : false,\n alive: liveness.length === 0 ? rest.alive : false,\n controllers: Object.fromEntries(\n description.controllers.map((each) => [\n each.name,\n each.requires.includes(dependency.name) && readiness.length > 0 ? false : rest.controllers[each.name],\n ]),\n ),\n dependencies: { ...rest.dependencies, [dependency.name]: readiness.length === 0 },\n });\n\n cases.push({\n name: `${service}: ${dependency.name} unavailable`,\n async run() {\n if (on.length === 0)\n throw new Error(`no check that gates a policy exercises ${dependency.name}: the service cannot see it fail`);\n const { health, clock, rest } = await boot();\n try {\n health.forceFail(dependency.name, true);\n await clock.advance(most(on.map(failMax)) + 1);\n const now = visible(health.verdict());\n const expected = unavailable(rest);\n if (!same(expected, now)) throw failure(`${dependency.name} unavailable`, expected, now);\n } finally {\n stop(health);\n }\n },\n });\n\n cases.push({\n name: `${service}: ${dependency.name} degraded by ${degradeBy} ms, and nothing fails`,\n async run() {\n const tight = on.filter((check) => ms(check.poll.timeout) <= degradeBy);\n if (tight.length > 0)\n throw new Error(\n `${tight.map((check) => `${check.name} times out in ${ms(check.poll.timeout)} ms`).join(\", \")}: a latency of ${degradeBy} ms fails it`,\n );\n const { health, clock, rest } = await boot((at) => ({\n wrapCheck: (check, run) =>\n check.dependency !== dependency.name\n ? run\n : (signal) => new Promise((resolve) => at.after(degradeBy, () => resolve(run(signal)))),\n }));\n try {\n await clock.advance(quietTime + most(on.map((check) => ms(check.poll.interval))));\n const now = visible(health.verdict());\n if (!same(rest, now)) throw failure(`${dependency.name} degraded`, rest, now);\n } finally {\n stop(health);\n }\n },\n });\n\n cases.push({\n name: `${service}: ${dependency.name} recovers`,\n async run() {\n if (on.length === 0)\n throw new Error(`no check that gates a policy exercises ${dependency.name}: the service cannot see it recover`);\n const { health, clock, rest } = await boot();\n try {\n health.forceFail(dependency.name, true);\n await clock.advance(most(on.map(failMax)) + 1);\n health.forceFail(dependency.name, false);\n await clock.advance(most(on.map(recoverMax)) + 1);\n const now = visible(health.verdict());\n if (!same(rest, now)) throw failure(`${dependency.name} recovered`, rest, now);\n } finally {\n stop(health);\n }\n },\n });\n }\n\n const never = `${service}.client-never-registered`;\n cases.push({\n name: `${service}: a client never registered fails, and nothing changes (the negative control)`,\n async run() {\n if (description.dependencies.some((each) => each.name === never))\n throw new Error(`the declaration has a dependency named ${never}`);\n const { health, clock, rest } = await boot();\n try {\n health.forceFail(never, true);\n await clock.advance(quietTime);\n const now = visible(health.verdict());\n if (!same(rest, now)) throw failure(\"the negative control\", rest, now);\n } finally {\n stop(health);\n }\n // and a check on a client never registered is refused before the start\n let refused = false;\n try {\n declare()\n .check({\n name: `${never}.check`,\n dependency: never,\n step: \"round-trip\",\n ownConnection: true,\n gates: null,\n poll: gating[0]?.poll ?? { interval: { value: 1, unit: \"s\" }, jitter: 0, timeout: { value: 0.5, unit: \"s\" } },\n })\n .describe();\n } catch {\n refused = true;\n }\n if (!refused) throw new Error(`the declaration accepted a check on ${never}, which is not a dependency`);\n },\n });\n\n return cases;\n};\n","// @mark1russell7/service/testing: the seams of the runtime for tests. A fake clock gives exact,\n// repeatable timing. A seeded random gives the same jitter on each run. A fake process takes\n// signals and keeps the exit codes. A quiet server answers until it goes quiet, and then it holds\n// its connections open without an answer. `contractCases` generates the contract test of a\n// declaration.\n\nimport type { AddressInfo, Server, Socket } from \"node:net\";\nimport { createServer } from \"node:net\";\nimport type { Outcome } from \"./latch.ts\";\nimport type { CheckFn } from \"./poller.ts\";\n\nexport type { FakeClock, FakeProcess } from \"./fakes.ts\";\nexport { fakeClock, fakeProcess, seeded } from \"./fakes.ts\";\nexport type { ContractCase, ContractOptions } from \"./contract.ts\";\nexport { contractCases } from \"./contract.ts\";\n\n/**\n * A behavior is what a scripted check does when the runtime starts it:\n * - `\"ok\"`, `\"error\"` and `\"indeterminate\"` give that outcome at once.\n * - `\"throw\"` throws at once.\n * - `\"timeout\"` gives no answer until the runtime aborts the use at the timeout of the poll.\n * - `\"hang\"` gives no answer, and it ignores the abort, until `heal()`.\n */\nexport type Behavior = Outcome | \"throw\" | \"timeout\" | \"hang\";\n\n/** A scripted check is a check function whose behavior a test sets. */\nexport interface ScriptedCheck {\n readonly run: CheckFn;\n /** This behavior applies to each later call. */\n behavior: Behavior;\n /** This count is the number of calls of the check. */\n readonly calls: number;\n /** This count is the number of calls that are pending. */\n readonly hung: number;\n /** This method ends each hung call with a pass, as when a driver gets its answer late. */\n heal(): void;\n}\n\n/** This function makes a scripted check with a behavior at the start. */\nexport const scriptedCheck = (behavior: Behavior = \"ok\"): ScriptedCheck => {\n let calls = 0;\n const hung = new Set<() => void>();\n const script: ScriptedCheck = {\n behavior,\n get calls() {\n return calls;\n },\n get hung() {\n return hung.size;\n },\n heal() {\n for (const end of [...hung]) end();\n },\n run: (signal) => {\n calls += 1;\n const now = script.behavior;\n if (now === \"throw\") throw new Error(\"the scripted check threw\");\n if (now === \"timeout\")\n return new Promise<Outcome>((_, reject) =>\n signal.addEventListener(\"abort\", () => reject(new Error(\"aborted at the timeout\"))),\n );\n if (now === \"hang\")\n return new Promise<Outcome>((resolve) => {\n const end = (): void => {\n hung.delete(end);\n resolve(\"ok\");\n };\n hung.add(end);\n });\n return now;\n },\n };\n return script;\n};\n\n/** A quiet server is a TCP echo server that can stop its answers and keep its connections open. */\nexport interface QuietServer {\n readonly port: number;\n /** This method stops the answers. The connections stay open, and the server reads and discards the data. */\n quiet(): void;\n /** This method starts the answers again. */\n speak(): void;\n /** This count is the number of open connections. */\n readonly connections: number;\n close(): Promise<void>;\n}\n\n/**\n * This function starts a quiet server on the loopback address. A check against it passes while it\n * answers. When it goes quiet, the check sees a connection that is open but silent. A frozen peer\n * and a black hole that keeps the connection look the same.\n */\nexport const quietServer = async (): Promise<QuietServer> => {\n let silent = false;\n const sockets = new Set<Socket>();\n const server: Server = createServer((socket) => {\n sockets.add(socket);\n socket.on(\"close\", () => sockets.delete(socket));\n socket.on(\"error\", () => sockets.delete(socket));\n socket.on(\"data\", (chunk) => {\n if (!silent) socket.write(chunk);\n });\n });\n await new Promise<void>((resolve, reject) => {\n server.once(\"error\", reject);\n server.listen(0, \"127.0.0.1\", () => resolve());\n });\n return {\n port: (server.address() as AddressInfo).port,\n quiet: () => {\n silent = true;\n },\n speak: () => {\n silent = false;\n },\n get connections() {\n return sockets.size;\n },\n close: () =>\n new Promise<void>((resolve) => {\n for (const socket of sockets) socket.destroy();\n server.close(() => resolve());\n }),\n };\n};\n"],"mappings":"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"}
@@ -0,0 +1,79 @@
1
+ import { convertExact, quantity, rationals } from "@mark1russell7/quantity";
2
+ //#region src/timing.ts
3
+ const seconds = (amount) => convertExact(rationals.fromNumber(amount.value), amount.unit, "s");
4
+ const out = (value) => quantity(rationals.toNumber(value), "s");
5
+ const fraction = (jitter) => {
6
+ if (!(jitter >= 0 && jitter < 1)) throw new RangeError(`jitter is a fraction below 1, not ${jitter}`);
7
+ return rationals.fromNumber(jitter);
8
+ };
9
+ const count = (polls, what) => {
10
+ if (!Number.isInteger(polls) || polls < 1) throw new RangeError(`${what} is a whole number of polls, not ${polls}`);
11
+ return rationals.rational(BigInt(polls));
12
+ };
13
+ /** N polls with jitter take from N − 1 short intervals to N long intervals. */
14
+ const debounced = (polls, interval, jitter) => [rationals.mul(rationals.sub(polls, rationals.ONE), rationals.mul(interval, rationals.sub(rationals.ONE, jitter))), rationals.mul(polls, rationals.mul(interval, rationals.add(rationals.ONE, jitter)))];
15
+ /**
16
+ * This function gives the time that a policy takes to fail after its signal becomes bad. A
17
+ * debounce of N polls takes approximately N intervals, with the jitter. A window takes its
18
+ * duration, plus one interval at most for the poll that starts its sequence. A step that fails by
19
+ * a timeout adds the timeout of the poll. At 3 polls of 5 s ± 20 %, the latch fails in 8–18 s on
20
+ * an error, and in 12–22 s on a 4 s timeout. A step that hangs adds the timeout only where its
21
+ * first failure ends the sequence: a window, or a debounce of one poll.
22
+ */
23
+ const failSpan = (timing, poll, by = "error") => {
24
+ const jitter = fraction(poll.jitter);
25
+ const first = timing.type === "window" || timing.failAfter === 1;
26
+ const late = by === "timeout" || by === "hang" && first ? seconds(poll.timeout) : rationals.ZERO;
27
+ if (timing.type === "debounce") {
28
+ const [min, max] = debounced(count(timing.failAfter, "failAfter"), seconds(timing.interval), jitter);
29
+ return {
30
+ min: out(rationals.add(min, late)),
31
+ max: out(rationals.add(max, late))
32
+ };
33
+ }
34
+ const duration = seconds(timing.failAfter);
35
+ const interval = rationals.mul(seconds(poll.interval), rationals.add(rationals.ONE, jitter));
36
+ return {
37
+ min: out(rationals.add(duration, late)),
38
+ max: out(rationals.add(rationals.add(duration, interval), late))
39
+ };
40
+ };
41
+ /** This function gives the time that a policy takes to recover after its signal becomes good. */
42
+ const recoverSpan = (timing, poll) => {
43
+ const jitter = fraction(poll.jitter);
44
+ if (timing.type === "debounce") {
45
+ const [min, max] = debounced(count(timing.recoverAfter, "recoverAfter"), seconds(timing.interval), jitter);
46
+ return {
47
+ min: out(min),
48
+ max: out(max)
49
+ };
50
+ }
51
+ const duration = timing.recoverAfter === void 0 ? rationals.ZERO : seconds(timing.recoverAfter);
52
+ const interval = rationals.mul(seconds(poll.interval), rationals.add(rationals.ONE, jitter));
53
+ return {
54
+ min: out(duration),
55
+ max: out(rationals.add(duration, interval))
56
+ };
57
+ };
58
+ /** This function gives the window that a debounce approximates: N polls of an interval fail after N intervals. */
59
+ const asWindow = (timing) => timing.type === "window" ? timing : {
60
+ type: "window",
61
+ failAfter: out(rationals.mul(count(timing.failAfter, "failAfter"), seconds(timing.interval))),
62
+ recoverAfter: out(rationals.mul(count(timing.recoverAfter, "recoverAfter"), seconds(timing.interval)))
63
+ };
64
+ /** This function gives the debounce that approximates a window on a poll: the polls that fit, rounded up. */
65
+ const asDebounce = (timing, poll) => {
66
+ if (timing.type === "debounce") return timing;
67
+ const interval = seconds(poll.interval);
68
+ const polls = (duration) => duration === void 0 ? 1 : Math.max(1, Number(rationals.ceil(rationals.div(duration, interval)).n));
69
+ return {
70
+ type: "debounce",
71
+ interval: poll.interval,
72
+ failAfter: polls(seconds(timing.failAfter)),
73
+ recoverAfter: polls(timing.recoverAfter === void 0 ? void 0 : seconds(timing.recoverAfter))
74
+ };
75
+ };
76
+ //#endregion
77
+ export { recoverSpan as i, asWindow as n, failSpan as r, asDebounce as t };
78
+
79
+ //# sourceMappingURL=timing-DxZ4nkQg.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timing-DxZ4nkQg.js","names":["r"],"sources":["../src/timing.ts"],"sourcesContent":["import type { Quantity, Rational } from \"@mark1russell7/quantity\";\nimport { convertExact, quantity, rationals as r } from \"@mark1russell7/quantity\";\n\n/**\n * All policies have the same timing shape. A timing is a count of polls (a debounce) or a\n * duration (a window), and each converts to the other. A long window is easier to read as a\n * duration, and that is the only difference between the two.\n */\nexport type Timing =\n | {\n readonly type: \"debounce\";\n /** This interval is the time between two polls of the signal. */\n readonly interval: Quantity;\n /** This count is the number of failed polls in sequence before the policy fails. */\n readonly failAfter: number;\n /** This count is the number of passed polls in sequence before the policy recovers. */\n readonly recoverAfter: number;\n }\n | {\n readonly type: \"window\";\n /** This duration is how long the signal fails before the policy fails, on the monotonic clock. */\n readonly failAfter: Quantity;\n /** This duration is how long the signal passes before the policy recovers. Without it, the first pass recovers the policy. */\n readonly recoverAfter?: Quantity | undefined;\n };\n\n/** A poll has an interval, a jitter as a fraction (±20 % is 0.2) and a timeout. */\nexport interface Poll {\n readonly interval: Quantity;\n readonly jitter: number;\n readonly timeout: Quantity;\n}\n\n/** A span is a range of times, from the earliest to the latest. */\nexport interface Span {\n readonly min: Quantity;\n readonly max: Quantity;\n}\n\n/**\n * This type tells how a failed step answers a poll.\n * - An `\"error\"` step refuses at once.\n * - A `\"timeout\"` step (a black hole) answers only when the timeout of the poll ends. Each poll\n * makes a new call, and each call ends at its timeout. Thus the step is slower by that timeout,\n * at the two ends of the span.\n * - A `\"hang\"` step makes a use that does not end. The first poll fails at its timeout. While the\n * use stays pending, the runtime does not start the check again, and each later poll fails at\n * once. Thus a debounce of two polls or more fails as fast as on an error.\n */\nexport type FailsBy = \"error\" | \"timeout\" | \"hang\";\n\nconst seconds = (amount: Quantity): Rational =>\n convertExact(r.fromNumber(amount.value), amount.unit, \"s\");\nconst out = (value: Rational): Quantity => quantity(r.toNumber(value), \"s\");\nconst fraction = (jitter: number): Rational => {\n if (!(jitter >= 0 && jitter < 1))\n throw new RangeError(`jitter is a fraction below 1, not ${jitter}`);\n return r.fromNumber(jitter);\n};\nconst count = (polls: number, what: string): Rational => {\n if (!Number.isInteger(polls) || polls < 1)\n throw new RangeError(`${what} is a whole number of polls, not ${polls}`);\n return r.rational(BigInt(polls));\n};\n\n/** N polls with jitter take from N − 1 short intervals to N long intervals. */\nconst debounced = (polls: Rational, interval: Rational, jitter: Rational): [Rational, Rational] => [\n r.mul(r.sub(polls, r.ONE), r.mul(interval, r.sub(r.ONE, jitter))),\n r.mul(polls, r.mul(interval, r.add(r.ONE, jitter))),\n];\n\n/**\n * This function gives the time that a policy takes to fail after its signal becomes bad. A\n * debounce of N polls takes approximately N intervals, with the jitter. A window takes its\n * duration, plus one interval at most for the poll that starts its sequence. A step that fails by\n * a timeout adds the timeout of the poll. At 3 polls of 5 s ± 20 %, the latch fails in 8–18 s on\n * an error, and in 12–22 s on a 4 s timeout. A step that hangs adds the timeout only where its\n * first failure ends the sequence: a window, or a debounce of one poll.\n */\nexport const failSpan = (timing: Timing, poll: Poll, by: FailsBy = \"error\"): Span => {\n const jitter = fraction(poll.jitter);\n const first = timing.type === \"window\" || timing.failAfter === 1;\n const late =\n by === \"timeout\" || (by === \"hang\" && first) ? seconds(poll.timeout) : r.ZERO;\n if (timing.type === \"debounce\") {\n const [min, max] = debounced(\n count(timing.failAfter, \"failAfter\"),\n seconds(timing.interval),\n jitter,\n );\n return { min: out(r.add(min, late)), max: out(r.add(max, late)) };\n }\n const duration = seconds(timing.failAfter);\n const interval = r.mul(seconds(poll.interval), r.add(r.ONE, jitter));\n return { min: out(r.add(duration, late)), max: out(r.add(r.add(duration, interval), late)) };\n};\n\n/** This function gives the time that a policy takes to recover after its signal becomes good. */\nexport const recoverSpan = (timing: Timing, poll: Poll): Span => {\n const jitter = fraction(poll.jitter);\n if (timing.type === \"debounce\") {\n const [min, max] = debounced(\n count(timing.recoverAfter, \"recoverAfter\"),\n seconds(timing.interval),\n jitter,\n );\n return { min: out(min), max: out(max) };\n }\n const duration = timing.recoverAfter === undefined ? r.ZERO : seconds(timing.recoverAfter);\n const interval = r.mul(seconds(poll.interval), r.add(r.ONE, jitter));\n return { min: out(duration), max: out(r.add(duration, interval)) };\n};\n\n/** This function gives the window that a debounce approximates: N polls of an interval fail after N intervals. */\nexport const asWindow = (timing: Timing): Timing =>\n timing.type === \"window\"\n ? timing\n : {\n type: \"window\",\n failAfter: out(r.mul(count(timing.failAfter, \"failAfter\"), seconds(timing.interval))),\n recoverAfter: out(\n r.mul(count(timing.recoverAfter, \"recoverAfter\"), seconds(timing.interval)),\n ),\n };\n\n/** This function gives the debounce that approximates a window on a poll: the polls that fit, rounded up. */\nexport const asDebounce = (timing: Timing, poll: Poll): Timing => {\n if (timing.type === \"debounce\") return timing;\n const interval = seconds(poll.interval);\n const polls = (duration: Rational | undefined): number =>\n duration === undefined ? 1 : Math.max(1, Number(r.ceil(r.div(duration, interval)).n));\n return {\n type: \"debounce\",\n interval: poll.interval,\n failAfter: polls(seconds(timing.failAfter)),\n recoverAfter: polls(\n timing.recoverAfter === undefined ? undefined : seconds(timing.recoverAfter),\n ),\n };\n};\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package",
3
3
  "name": "@mark1russell7/service",
4
- "version": "0.1.0",
5
- "description": "A service declared as data before it starts: its dependencies, checks, policies and shutdown, with the timer-ordering law checked against the declaration.",
4
+ "version": "0.2.0",
5
+ "description": "A service declared as data before it starts, and its runtime: checks on jittered loops, a latch for each policy, probes from the cache, the startup in its sequence, and a staged, capped drain. The timer-ordering law checks the declaration.",
6
6
  "keywords": [
7
7
  "service",
8
8
  "readiness",
@@ -33,6 +33,14 @@
33
33
  "types": "./build/index.d.ts",
34
34
  "default": "./build/index.js"
35
35
  },
36
+ "./testing": {
37
+ "types": "./build/testing.d.ts",
38
+ "default": "./build/testing.js"
39
+ },
40
+ "./redis": {
41
+ "types": "./build/redis.d.ts",
42
+ "default": "./build/redis.js"
43
+ },
36
44
  "./package.json": "./package.json"
37
45
  },
38
46
  "files": [
@@ -44,14 +52,24 @@
44
52
  "node": ">=22.19"
45
53
  },
46
54
  "dependencies": {
55
+ "@mark1russell7/algebra": "^0.2.0",
47
56
  "@mark1russell7/quantity": "^0.1.0"
48
57
  },
49
58
  "peerDependencies": {
59
+ "ioredis": "^6.0.0",
50
60
  "zod": "^4.1.0"
51
61
  },
62
+ "peerDependenciesMeta": {
63
+ "ioredis": {
64
+ "optional": true
65
+ }
66
+ },
52
67
  "devDependencies": {
53
68
  "@mark1russell7/cue": "github:mark1russell7/cue",
69
+ "@mark1russell7/harness": "^0.2.0",
54
70
  "@types/node": "^26.6.4",
71
+ "ioredis": "6.0.0",
72
+ "testcontainers": "12.2.0",
55
73
  "tsdown": "^0.23.1",
56
74
  "typescript": "^7.0.2",
57
75
  "vitest": "^5.0.3",