@mark1russell7/service 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -2
- package/build/backoff-B-jGWVDm.d.ts +48 -0
- package/build/backoff.d.ts +2 -0
- package/build/backoff.js +70 -0
- package/build/backoff.js.map +1 -0
- package/build/{builder-BFxOuULH.d.ts → builder-DnfUDW9u.d.ts} +8 -22
- package/build/clock-D-38uH_z.d.ts +37 -0
- package/build/clock.d.ts +2 -0
- package/build/clock.js +30 -0
- package/build/clock.js.map +1 -0
- package/build/heartbeat.d.ts +70 -0
- package/build/heartbeat.js +103 -0
- package/build/heartbeat.js.map +1 -0
- package/build/index.d.ts +6 -9
- package/build/index.js +12 -23
- package/build/index.js.map +1 -1
- package/build/{clock-DlJFFZlX.js → operation-DL5RxFds.js} +2 -16
- package/build/operation-DL5RxFds.js.map +1 -0
- package/build/redis.d.ts +7 -6
- package/build/redis.js +49 -47
- package/build/redis.js.map +1 -1
- package/build/testing.d.ts +2 -1
- package/build/testing.js +1 -1
- package/build/{timing-DxZ4nkQg.js → timing-DGRi3XCI.js} +12 -3
- package/build/timing-DGRi3XCI.js.map +1 -0
- package/package.json +17 -5
- package/build/clock-DlJFFZlX.js.map +0 -1
- package/build/timing-DxZ4nkQg.js.map +0 -1
package/README.md
CHANGED
|
@@ -14,6 +14,7 @@ This release has the declaration, its export, the law, the health runtime and th
|
|
|
14
14
|
- **Keyed operations.** A keyed operation has the window that the server keeps its key for. `mayReissue` answers from the declaration of an operation. An operation without a declaration does not go again.
|
|
15
15
|
- **The law.** `evaluate` and `verdictOf` decide one rule of the timer-ordering law on one binding. A rule is data: linear inequalities over `symbol@hop`, with a margin. A verdict is `holds`, `violated`, `unbounded` (it holds only because a timer is absent) or `depends` (nothing is bound).
|
|
16
16
|
- **The linter.** `lint(description, scenario, rules)` applies the service rules (`SERVICE_RULES`) to a declaration. The declaration binds the symbols of the service: the polls of its checks, the windows of its policies, its drain. The scenario binds the symbols of the platform and of each dependency. A `depends` finding names the symbols that have no bound.
|
|
17
|
+
- **The hold.** The cap of the `fail readiness` phase binds `readiness_hold@process`, and a service without that phase holds for zero. The hold keeps the listener open after the signal, as a preStop sleep does. Thus O04 can compare the sum of the two with the propagation of the endpoints.
|
|
17
18
|
|
|
18
19
|
The format of a description is `mark1russell7.service/1`.
|
|
19
20
|
|
|
@@ -50,10 +51,49 @@ The format of a description is `mark1russell7.service/1`.
|
|
|
50
51
|
|
|
51
52
|
## The machines
|
|
52
53
|
|
|
53
|
-
The machines of the runtime are `mark1russell7.machine/1` documents, which `@mark1russell7/algebra` reads. They are the gate, the latch (a debounce), the window, the poller, the exit manager and the watchdog. `drainMachine(description)` gives the drain of a declaration, with one state for each phase. Each machine gives its transitions to `run({ record })` or `start({ record })`. `replay(doc, events)` gives each transition that the document does not permit.
|
|
54
|
+
The machines of the runtime are `mark1russell7.machine/1` documents, which `@mark1russell7/algebra` reads. They are the gate, the latch (a debounce), the window, the poller, the exit manager and the watchdog. `drainMachine(description)` gives the drain of a declaration, with one state for each phase. Each machine gives its transitions to `run({ record })` or `start({ record })`. `replay(doc, events)` gives each transition that the document does not permit. It is the replay of `@mark1russell7/algebra`, which reads a machine of any name.
|
|
54
55
|
|
|
55
56
|
The tests replay the real runtime against each document. They also compare each timer of the documents, from the algebra, with the time that the runtime keeps.
|
|
56
57
|
|
|
58
|
+
## The timers
|
|
59
|
+
|
|
60
|
+
The clock, the heartbeat and the backoff have their own entries. Each entry imports no `node:` module, no zod and no other package. Thus a client in a browser can use them. Each entry is less than 1 kB of gzip. The root of the package also gives each function.
|
|
61
|
+
|
|
62
|
+
| Entry | Exports |
|
|
63
|
+
| --- | --- |
|
|
64
|
+
| `@mark1russell7/service/clock` | `Clock`, `Cancel`, `realClock`, `platformClock` |
|
|
65
|
+
| `@mark1russell7/service/heartbeat` | `heartbeat`, `detectionTime` |
|
|
66
|
+
| `@mark1russell7/service/backoff` | `backoffDelay`, `backoffBand`, `backoffProblems` |
|
|
67
|
+
|
|
68
|
+
- **The clock.** `platformClock({ unref })` gives a clock on `performance.now()` and `setTimeout`. With `unref: true`, its timers do not keep a Node process alive. `realClock` is that clock. Each cancel also has `unref`, for one timer.
|
|
69
|
+
- **The heartbeat.** `heartbeat(options)` sends a ping each interval, and a timeout bounds each ping. A ping without an answer at its timeout is a miss. The last of `misses` misses in sequence starts `onDead`.
|
|
70
|
+
- **Two kinds of ping.** A ping gives its answer as a promise, as a request and its reply do. Or the answer comes later through `answer()`, as a pong message does.
|
|
71
|
+
- **The hooks of a heartbeat.** At each tick, `due` tells if the tick sends a ping. `live` tells if a miss counts. `turn` waits one turn of the event loop before the heartbeat counts. Without `overlap`, the heartbeat sends one ping at a time.
|
|
72
|
+
- **The detection time.** `detectionTime(interval, timeout, misses)` gives the most time to find a silent peer: `misses` intervals plus a timeout. `heartbeatDetection` gives that time as an amount for the law.
|
|
73
|
+
- **The backoff.** `backoffDelay(policy, attempt, random)` gives the delay before a try: `base × factor^(attempt − 1)`, at most `cap`, with a jitter. The first try is 1.
|
|
74
|
+
- **The jitter.** The jitter `subtract` takes a random part of the delay away, and a fraction of 1 is the full jitter. The jitter `add` adds a random time up to an amount.
|
|
75
|
+
- **The band.** `backoffBand(policy, attempt)` gives the least and the most delay of one try. Without a try, it covers each try. The law reads the most delay from it. `backoffProblems(policy)` names each value that the delays cannot use.
|
|
76
|
+
- **Milliseconds.** These functions take times in milliseconds, as the clock does. The declaration and the law keep their amounts (`Quantity`).
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { backoffDelay } from "@mark1russell7/service/backoff";
|
|
80
|
+
import { heartbeat } from "@mark1russell7/service/heartbeat";
|
|
81
|
+
|
|
82
|
+
const beat = heartbeat({
|
|
83
|
+
interval: 30_000,
|
|
84
|
+
timeout: 10_000,
|
|
85
|
+
misses: 2,
|
|
86
|
+
ping: () => socket.send(JSON.stringify({ type: "ping" })),
|
|
87
|
+
onDead: () => socket.close(),
|
|
88
|
+
});
|
|
89
|
+
socket.addEventListener("message", (event) => {
|
|
90
|
+
if (JSON.parse(event.data).type === "pong") beat.answer();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// full jitter: a random time from 0 to the backoff
|
|
94
|
+
const delay = backoffDelay({ base: 1000, factor: 1.5, cap: 30_000, jitter: { kind: "subtract", fraction: 1 } }, attempt);
|
|
95
|
+
```
|
|
96
|
+
|
|
57
97
|
## The testing subpath
|
|
58
98
|
|
|
59
99
|
`@mark1russell7/service/testing` has the seams that a test of a service needs:
|
|
@@ -71,10 +111,11 @@ The tests replay the real runtime against each document. They also compare each
|
|
|
71
111
|
|
|
72
112
|
- **The declaration.** `redisClient(name, spec)` declares its knobs by the names of the ordering law: `connect_bound`, `request_timeout`, `driver_writeoff_time`, `heartbeat_interval`, `client_retry_window` and others. Each knob is necessary, and a bound that nobody gives is "infinite". The client derives `heartbeat_detection_time` from its watchdog. Thus `lint()` evaluates the rules of the client (O13, O15, O17, O21, O27) on the declaration.
|
|
73
113
|
- **The settings of ioredis.** The client gives ioredis each timer from the declaration. It turns off the offline queue, and it does not let ioredis send a command again. Thus a command while the client is not ready fails at once, and the command did not leave the client.
|
|
114
|
+
- **The retry strategy.** `retryBackoff(knobs)` gives the backoff of the tries to connect: from `retry_base`, doubling to `retry_cap`, each delay with a jitter up to the base. The tries stop at the end of `client_retry_window`.
|
|
74
115
|
- **Three outcomes.** `send(command, args)` gives `ok`, `error` or `indeterminate`. An error is a reply of the server, or a command that did not leave the client. An indeterminate command can have reached the server. When its connection closes, each command in flight becomes indeterminate at once, not at its timeout.
|
|
75
116
|
- **Idempotency for each command.** A table declares the idempotency of each common command, and a declaration can change it. The client sends an idempotent command again one time at most, in the bound of the request. It does not send again a command that is not idempotent, or that the table does not name.
|
|
76
117
|
- **The join.** At each `ready`, the client records `CLIENT ID` and the socket of the connection (`joins`). The prober joins its kernel records to the view of the server with it.
|
|
77
|
-
- **The watchdog.** A `PING` on the connection of the application, each interval. An error reply is an answer. After the misses of the declaration, the watchdog closes the connection, and the client connects again. The watchdog acts only on a ready connection, and it starts again a client whose retry window ended.
|
|
118
|
+
- **The watchdog.** A `heartbeat` of `PING` commands on the connection of the application, each interval. An error reply is an answer. After the misses of the declaration, the watchdog closes the connection, and the client connects again. The watchdog acts only on a ready connection, and it starts again a client whose retry window ended.
|
|
78
119
|
- **The checks.** `checks({ poll, steps })` gives a check for each step: `local-state`, `round-trip` (a `PING` on the connection of the application) and `dial` (a new connection). Each one controls readiness, liveness or nothing. `attach(svc, options)` declares the dependency, its handle and its checks.
|
|
79
120
|
- **The close.** At the shutdown, the client sends `QUIT`, and it disconnects at its `close_bound`. Thus a silent server cannot hold the phase past its cap.
|
|
80
121
|
- **The manifestations.** `meets` lists the manifestations that the client can meet, and the detectors of each, with their times from the knobs. `unwatched(meets)` lists each manifestation and situation that no detector finds.
|
|
@@ -171,6 +212,8 @@ const outcome = await life.request("orders", "POST /orders", (signal) => createO
|
|
|
171
212
|
- **The contract.** The contract of a sample declaration passes. The contract also finds a blind dependency, a check whose timeout is too short, and a check that names the wrong dependency.
|
|
172
213
|
- **The Redis client.** These tests use a fake ioredis and a fake clock. They cover the declaration, the three outcomes, and the commands that the client sends again or not. They also cover the join, the backoff, the watchdog, the close, the checks and the manifestations. Five scenarios replay against the document of the client.
|
|
173
214
|
- **F6 for Redis.** The fault matrix crosses each manifestation (a refusal, a frozen process, a black hole, a death without a close, and an app stall), each gating posture, two timings and two jitters. That is 60 cases on the fake. Each case holds the trip and the release of its policy to `failSpan` and `recoverSpan`. Five more cases shut the service down during each manifestation, and each one exits 0.
|
|
215
|
+
- **The timers.** These tests use a fake clock, and they cover the hooks of the heartbeat and its two kinds of ping. A property test holds the death of a silent peer to `detectionTime` in 300 seeded cases. Another one compares the heartbeat with a WebSocket heartbeat that sends one ping at a time, on 300 schedules of pongs. The backoff gives the delays of three earlier backoffs, number for number, and each delay is in its band.
|
|
216
|
+
- **The entries.** The timer entries reach no other package, no `node:` module and no zod.
|
|
174
217
|
- **Against a real Redis.** `REDIS_IT=1` starts the tests of the client against Redis 7.4. `REDIS_F6=1` starts the F6 matrix in the VM of Docker. The injector of `@mark1russell7/harness` makes the network faults, and an agent in the namespaces of Redis makes the process faults. All of them use one monotonic clock.
|
|
175
218
|
|
|
176
219
|
`pnpm --filter @mark1russell7/service build` writes the package for npm into `build/`.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
//#region src/backoff.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* A jitter is the random part of each delay. The random number `r` is from 0 up to 1.
|
|
4
|
+
* - `none`: the delay is the backoff.
|
|
5
|
+
* - `subtract`: the delay is `backoff × (1 − fraction × r)`. A fraction of 1 is the full jitter: a delay from 0 to the backoff.
|
|
6
|
+
* - `add`: the delay is `backoff + amount × r`, with the amount in milliseconds.
|
|
7
|
+
*/
|
|
8
|
+
type Jitter = {
|
|
9
|
+
readonly kind: "none";
|
|
10
|
+
} | {
|
|
11
|
+
readonly kind: "subtract";
|
|
12
|
+
readonly fraction: number;
|
|
13
|
+
} | {
|
|
14
|
+
readonly kind: "add";
|
|
15
|
+
readonly amount: number;
|
|
16
|
+
};
|
|
17
|
+
/** A backoff policy gives the delay before each try. Each time is in milliseconds. */
|
|
18
|
+
interface Backoff {
|
|
19
|
+
/** This time is the backoff of the first try. */
|
|
20
|
+
readonly base: number;
|
|
21
|
+
/** This number multiplies the backoff from one try to the next. The default is 2. */
|
|
22
|
+
readonly factor?: number | undefined;
|
|
23
|
+
/** This time is the most that a backoff can be, before its jitter. The default is no limit. */
|
|
24
|
+
readonly cap?: number | undefined;
|
|
25
|
+
/** This jitter gives the random part of each delay. The default is `none`. */
|
|
26
|
+
readonly jitter?: Jitter | undefined;
|
|
27
|
+
}
|
|
28
|
+
/** A band is the least and the most delay that a policy gives, in milliseconds. */
|
|
29
|
+
interface DelayBand {
|
|
30
|
+
readonly low: number;
|
|
31
|
+
readonly high: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* This function gives the delay before a try, in milliseconds. The first try is 1. The delay is
|
|
35
|
+
* the backoff of the try with its jitter, and not below zero. The `random` function gives numbers from 0 up to 1.
|
|
36
|
+
*/
|
|
37
|
+
declare const backoffDelay: (policy: Backoff, attempt: number, random?: () => number) => number;
|
|
38
|
+
/**
|
|
39
|
+
* This function gives the band of the delay before a try: the least and the most delay that the
|
|
40
|
+
* jitter can give. Without a try, the band covers each try. The ordering law reads the most delay
|
|
41
|
+
* of a policy from it.
|
|
42
|
+
*/
|
|
43
|
+
declare const backoffBand: (policy: Backoff, attempt?: number) => DelayBand;
|
|
44
|
+
/** This function gives the problems of a policy: each value that the delays cannot use. */
|
|
45
|
+
declare const backoffProblems: (policy: Backoff) => string[];
|
|
46
|
+
//#endregion
|
|
47
|
+
export { backoffDelay as a, backoffBand as i, DelayBand as n, backoffProblems as o, Jitter as r, Backoff as t };
|
|
48
|
+
//# sourceMappingURL=backoff-B-jGWVDm.d.ts.map
|
package/build/backoff.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
//#region src/backoff.ts
|
|
2
|
+
const NONE = { kind: "none" };
|
|
3
|
+
/** This function gives the backoff of a try, before its jitter: `base × factor^(attempt − 1)`, at most the cap. */
|
|
4
|
+
const backoffOf = (policy, attempt) => {
|
|
5
|
+
const grown = policy.base === 0 ? 0 : policy.base * (policy.factor ?? 2) ** (attempt - 1);
|
|
6
|
+
return Math.min(grown, policy.cap ?? Number.POSITIVE_INFINITY);
|
|
7
|
+
};
|
|
8
|
+
/** This function gives the band of one backoff after its jitter. */
|
|
9
|
+
const jittered = (backoff, jitter) => {
|
|
10
|
+
if (jitter.kind === "subtract") return {
|
|
11
|
+
low: backoff * (1 - jitter.fraction),
|
|
12
|
+
high: backoff
|
|
13
|
+
};
|
|
14
|
+
if (jitter.kind === "add") return {
|
|
15
|
+
low: backoff,
|
|
16
|
+
high: backoff + jitter.amount
|
|
17
|
+
};
|
|
18
|
+
return {
|
|
19
|
+
low: backoff,
|
|
20
|
+
high: backoff
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
const tryNumber = (attempt) => {
|
|
24
|
+
if (!Number.isInteger(attempt) || attempt < 1) throw new RangeError(`a try is a whole number from 1, not ${attempt}`);
|
|
25
|
+
return attempt;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* This function gives the delay before a try, in milliseconds. The first try is 1. The delay is
|
|
29
|
+
* the backoff of the try with its jitter, and not below zero. The `random` function gives numbers from 0 up to 1.
|
|
30
|
+
*/
|
|
31
|
+
const backoffDelay = (policy, attempt, random = Math.random) => {
|
|
32
|
+
const backoff = backoffOf(policy, tryNumber(attempt));
|
|
33
|
+
const jitter = policy.jitter ?? NONE;
|
|
34
|
+
if (jitter.kind === "subtract") return Math.max(0, backoff * (1 - jitter.fraction * random()));
|
|
35
|
+
if (jitter.kind === "add") return Math.max(0, backoff + random() * jitter.amount);
|
|
36
|
+
return Math.max(0, backoff);
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* This function gives the band of the delay before a try: the least and the most delay that the
|
|
40
|
+
* jitter can give. Without a try, the band covers each try. The ordering law reads the most delay
|
|
41
|
+
* of a policy from it.
|
|
42
|
+
*/
|
|
43
|
+
const backoffBand = (policy, attempt) => {
|
|
44
|
+
const jitter = policy.jitter ?? NONE;
|
|
45
|
+
if (attempt !== void 0) return jittered(backoffOf(policy, tryNumber(attempt)), jitter);
|
|
46
|
+
const factor = policy.factor ?? 2;
|
|
47
|
+
const first = backoffOf(policy, 1);
|
|
48
|
+
const most = factor > 1 && policy.base > 0 ? policy.cap ?? Number.POSITIVE_INFINITY : first;
|
|
49
|
+
return {
|
|
50
|
+
low: jittered(factor < 1 ? 0 : first, jitter).low,
|
|
51
|
+
high: jittered(most, jitter).high
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
/** This function gives the problems of a policy: each value that the delays cannot use. */
|
|
55
|
+
const backoffProblems = (policy) => {
|
|
56
|
+
const problems = [];
|
|
57
|
+
const factor = policy.factor ?? 2;
|
|
58
|
+
const cap = policy.cap ?? Number.POSITIVE_INFINITY;
|
|
59
|
+
if (!(Number.isFinite(policy.base) && policy.base >= 0)) problems.push(`the base ${policy.base} is not a time of zero or more`);
|
|
60
|
+
if (!(Number.isFinite(factor) && factor >= 1)) problems.push(`the factor ${factor} is not a number of 1 or more`);
|
|
61
|
+
if (!(cap >= policy.base)) problems.push(`the cap ${cap} is below the base ${policy.base}`);
|
|
62
|
+
const jitter = policy.jitter ?? NONE;
|
|
63
|
+
if (jitter.kind === "subtract" && !(jitter.fraction >= 0 && jitter.fraction <= 1)) problems.push(`the fraction ${jitter.fraction} of the jitter is not from 0 to 1`);
|
|
64
|
+
if (jitter.kind === "add" && !(Number.isFinite(jitter.amount) && jitter.amount >= 0)) problems.push(`the amount ${jitter.amount} of the jitter is not a time of zero or more`);
|
|
65
|
+
return problems;
|
|
66
|
+
};
|
|
67
|
+
//#endregion
|
|
68
|
+
export { backoffBand, backoffDelay, backoffProblems };
|
|
69
|
+
|
|
70
|
+
//# sourceMappingURL=backoff.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backoff.js","names":[],"sources":["../src/backoff.ts"],"sourcesContent":["// @mark1russell7/service/backoff: the delays of an exponential backoff with a jitter, and their\n// band for the ordering law, with no other part of the package. The entry imports nothing. Thus a\n// client in a browser can use it.\n\n/**\n * A jitter is the random part of each delay. The random number `r` is from 0 up to 1.\n * - `none`: the delay is the backoff.\n * - `subtract`: the delay is `backoff × (1 − fraction × r)`. A fraction of 1 is the full jitter: a delay from 0 to the backoff.\n * - `add`: the delay is `backoff + amount × r`, with the amount in milliseconds.\n */\nexport type Jitter =\n | { readonly kind: \"none\" }\n | { readonly kind: \"subtract\"; readonly fraction: number }\n | { readonly kind: \"add\"; readonly amount: number };\n\n/** A backoff policy gives the delay before each try. Each time is in milliseconds. */\nexport interface Backoff {\n /** This time is the backoff of the first try. */\n readonly base: number;\n /** This number multiplies the backoff from one try to the next. The default is 2. */\n readonly factor?: number | undefined;\n /** This time is the most that a backoff can be, before its jitter. The default is no limit. */\n readonly cap?: number | undefined;\n /** This jitter gives the random part of each delay. The default is `none`. */\n readonly jitter?: Jitter | undefined;\n}\n\n/** A band is the least and the most delay that a policy gives, in milliseconds. */\nexport interface DelayBand {\n readonly low: number;\n readonly high: number;\n}\n\nconst NONE: Jitter = { kind: \"none\" };\n\n/** This function gives the backoff of a try, before its jitter: `base × factor^(attempt − 1)`, at most the cap. */\nconst backoffOf = (policy: Backoff, attempt: number): number => {\n const grown = policy.base === 0 ? 0 : policy.base * (policy.factor ?? 2) ** (attempt - 1);\n return Math.min(grown, policy.cap ?? Number.POSITIVE_INFINITY);\n};\n\n/** This function gives the band of one backoff after its jitter. */\nconst jittered = (backoff: number, jitter: Jitter): DelayBand => {\n if (jitter.kind === \"subtract\") return { low: backoff * (1 - jitter.fraction), high: backoff };\n if (jitter.kind === \"add\") return { low: backoff, high: backoff + jitter.amount };\n return { low: backoff, high: backoff };\n};\n\nconst tryNumber = (attempt: number): number => {\n if (!Number.isInteger(attempt) || attempt < 1) throw new RangeError(`a try is a whole number from 1, not ${attempt}`);\n return attempt;\n};\n\n/**\n * This function gives the delay before a try, in milliseconds. The first try is 1. The delay is\n * the backoff of the try with its jitter, and not below zero. The `random` function gives numbers from 0 up to 1.\n */\nexport const backoffDelay = (policy: Backoff, attempt: number, random: () => number = Math.random): number => {\n const backoff = backoffOf(policy, tryNumber(attempt));\n const jitter = policy.jitter ?? NONE;\n if (jitter.kind === \"subtract\") return Math.max(0, backoff * (1 - jitter.fraction * random()));\n if (jitter.kind === \"add\") return Math.max(0, backoff + random() * jitter.amount);\n return Math.max(0, backoff);\n};\n\n/**\n * This function gives the band of the delay before a try: the least and the most delay that the\n * jitter can give. Without a try, the band covers each try. The ordering law reads the most delay\n * of a policy from it.\n */\nexport const backoffBand = (policy: Backoff, attempt?: number): DelayBand => {\n const jitter = policy.jitter ?? NONE;\n if (attempt !== undefined) return jittered(backoffOf(policy, tryNumber(attempt)), jitter);\n // a factor above 1 grows the backoff to the cap, a factor of 1 keeps it, and a factor below 1 shrinks it to zero\n const factor = policy.factor ?? 2;\n const first = backoffOf(policy, 1);\n const most = factor > 1 && policy.base > 0 ? (policy.cap ?? Number.POSITIVE_INFINITY) : first;\n const least = factor < 1 ? 0 : first;\n return { low: jittered(least, jitter).low, high: jittered(most, jitter).high };\n};\n\n/** This function gives the problems of a policy: each value that the delays cannot use. */\nexport const backoffProblems = (policy: Backoff): string[] => {\n const problems: string[] = [];\n const factor = policy.factor ?? 2;\n const cap = policy.cap ?? Number.POSITIVE_INFINITY;\n if (!(Number.isFinite(policy.base) && policy.base >= 0)) problems.push(`the base ${policy.base} is not a time of zero or more`);\n if (!(Number.isFinite(factor) && factor >= 1)) problems.push(`the factor ${factor} is not a number of 1 or more`);\n if (!(cap >= policy.base)) problems.push(`the cap ${cap} is below the base ${policy.base}`);\n const jitter = policy.jitter ?? NONE;\n if (jitter.kind === \"subtract\" && !(jitter.fraction >= 0 && jitter.fraction <= 1))\n problems.push(`the fraction ${jitter.fraction} of the jitter is not from 0 to 1`);\n if (jitter.kind === \"add\" && !(Number.isFinite(jitter.amount) && jitter.amount >= 0))\n problems.push(`the amount ${jitter.amount} of the jitter is not a time of zero or more`);\n return problems;\n};\n"],"mappings":";AAiCA,MAAM,OAAe,EAAE,MAAM,OAAO;;AAGpC,MAAM,aAAa,QAAiB,YAA4B;CAC9D,MAAM,QAAQ,OAAO,SAAS,IAAI,IAAI,OAAO,QAAQ,OAAO,UAAU,OAAO,UAAU;CACvF,OAAO,KAAK,IAAI,OAAO,OAAO,OAAO,OAAO,iBAAiB;AAC/D;;AAGA,MAAM,YAAY,SAAiB,WAA8B;CAC/D,IAAI,OAAO,SAAS,YAAY,OAAO;EAAE,KAAK,WAAW,IAAI,OAAO;EAAW,MAAM;CAAQ;CAC7F,IAAI,OAAO,SAAS,OAAO,OAAO;EAAE,KAAK;EAAS,MAAM,UAAU,OAAO;CAAO;CAChF,OAAO;EAAE,KAAK;EAAS,MAAM;CAAQ;AACvC;AAEA,MAAM,aAAa,YAA4B;CAC7C,IAAI,CAAC,OAAO,UAAU,OAAO,KAAK,UAAU,GAAG,MAAM,IAAI,WAAW,uCAAuC,SAAS;CACpH,OAAO;AACT;;;;;AAMA,MAAa,gBAAgB,QAAiB,SAAiB,SAAuB,KAAK,WAAmB;CAC5G,MAAM,UAAU,UAAU,QAAQ,UAAU,OAAO,CAAC;CACpD,MAAM,SAAS,OAAO,UAAU;CAChC,IAAI,OAAO,SAAS,YAAY,OAAO,KAAK,IAAI,GAAG,WAAW,IAAI,OAAO,WAAW,OAAO,EAAE;CAC7F,IAAI,OAAO,SAAS,OAAO,OAAO,KAAK,IAAI,GAAG,UAAU,OAAO,IAAI,OAAO,MAAM;CAChF,OAAO,KAAK,IAAI,GAAG,OAAO;AAC5B;;;;;;AAOA,MAAa,eAAe,QAAiB,YAAgC;CAC3E,MAAM,SAAS,OAAO,UAAU;CAChC,IAAI,YAAY,KAAA,GAAW,OAAO,SAAS,UAAU,QAAQ,UAAU,OAAO,CAAC,GAAG,MAAM;CAExF,MAAM,SAAS,OAAO,UAAU;CAChC,MAAM,QAAQ,UAAU,QAAQ,CAAC;CACjC,MAAM,OAAO,SAAS,KAAK,OAAO,OAAO,IAAK,OAAO,OAAO,OAAO,oBAAqB;CAExF,OAAO;EAAE,KAAK,SADA,SAAS,IAAI,IAAI,OACD,MAAM,CAAC,CAAC;EAAK,MAAM,SAAS,MAAM,MAAM,CAAC,CAAC;CAAK;AAC/E;;AAGA,MAAa,mBAAmB,WAA8B;CAC5D,MAAM,WAAqB,CAAC;CAC5B,MAAM,SAAS,OAAO,UAAU;CAChC,MAAM,MAAM,OAAO,OAAO,OAAO;CACjC,IAAI,EAAE,OAAO,SAAS,OAAO,IAAI,KAAK,OAAO,QAAQ,IAAI,SAAS,KAAK,YAAY,OAAO,KAAK,+BAA+B;CAC9H,IAAI,EAAE,OAAO,SAAS,MAAM,KAAK,UAAU,IAAI,SAAS,KAAK,cAAc,OAAO,8BAA8B;CAChH,IAAI,EAAE,OAAO,OAAO,OAAO,SAAS,KAAK,WAAW,IAAI,qBAAqB,OAAO,MAAM;CAC1F,MAAM,SAAS,OAAO,UAAU;CAChC,IAAI,OAAO,SAAS,cAAc,EAAE,OAAO,YAAY,KAAK,OAAO,YAAY,IAC7E,SAAS,KAAK,gBAAgB,OAAO,SAAS,kCAAkC;CAClF,IAAI,OAAO,SAAS,SAAS,EAAE,OAAO,SAAS,OAAO,MAAM,KAAK,OAAO,UAAU,IAChF,SAAS,KAAK,cAAc,OAAO,OAAO,6CAA6C;CACzF,OAAO;AACT"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { n as Clock } from "./clock-D-38uH_z.js";
|
|
1
2
|
import { Extent, Quantity } from "@mark1russell7/quantity";
|
|
2
3
|
//#region src/timing.d.ts
|
|
3
4
|
/**
|
|
@@ -57,6 +58,11 @@ declare const recoverSpan: (timing: Timing, poll: Poll) => Span;
|
|
|
57
58
|
declare const asWindow: (timing: Timing) => Timing;
|
|
58
59
|
/** This function gives the debounce that approximates a window on a poll: the polls that fit, rounded up. */
|
|
59
60
|
declare const asDebounce: (timing: Timing, poll: Poll) => Timing;
|
|
61
|
+
/**
|
|
62
|
+
* This function gives the most time that a heartbeat takes to find a silent peer, as an amount
|
|
63
|
+
* for the ordering law (`detectionTime`). An interval of "infinite" is a heartbeat that is off.
|
|
64
|
+
*/
|
|
65
|
+
declare const heartbeatDetection: (interval: Extent, timeout: Quantity, misses: number) => Extent;
|
|
60
66
|
//#endregion
|
|
61
67
|
//#region src/operation.d.ts
|
|
62
68
|
/**
|
|
@@ -182,26 +188,6 @@ interface ServiceDescription {
|
|
|
182
188
|
};
|
|
183
189
|
}
|
|
184
190
|
//#endregion
|
|
185
|
-
//#region src/clock.d.ts
|
|
186
|
-
/** This function stops a timer that `Clock.after` started. A second use does nothing. */
|
|
187
|
-
type Cancel = () => void;
|
|
188
|
-
/**
|
|
189
|
-
* A clock gives the monotonic time in milliseconds, and it starts a function after a delay. The
|
|
190
|
-
* runtime reads time only through a clock. Thus a test can give a fake clock, and the timing of
|
|
191
|
-
* the runtime is then exact and repeatable.
|
|
192
|
-
*/
|
|
193
|
-
interface Clock {
|
|
194
|
-
/** This method gives the monotonic time, in milliseconds. A step of the wall clock does not move it. */
|
|
195
|
-
now(): number;
|
|
196
|
-
/** This method starts `run` after `ms` milliseconds, and gives the function that stops it. */
|
|
197
|
-
after(ms: number, run: () => void): Cancel;
|
|
198
|
-
}
|
|
199
|
-
/**
|
|
200
|
-
* This clock reads `performance.now()` and uses `setTimeout`. Its timers do not keep a Node
|
|
201
|
-
* process alive, thus a timer that is not stopped cannot stop the exit of the process.
|
|
202
|
-
*/
|
|
203
|
-
declare const realClock: Clock;
|
|
204
|
-
//#endregion
|
|
205
191
|
//#region src/events.d.ts
|
|
206
192
|
/**
|
|
207
193
|
* A machine name names one machine of the runtime. Each machine has a document in `machines.ts`,
|
|
@@ -603,5 +589,5 @@ interface ServiceBuilder {
|
|
|
603
589
|
/** This function starts the composition of a service. */
|
|
604
590
|
declare const service: (name: string) => ServiceBuilder;
|
|
605
591
|
//#endregion
|
|
606
|
-
export {
|
|
607
|
-
//# sourceMappingURL=builder-
|
|
592
|
+
export { Timing as $, StartOptions as A, Dependency as B, ProcessPort as C, HealthVerdict as D, Health as E, MachineName as F, Step as G, Gate as H, Recorder as I, Operation as J, Transport as K, Check as L, Outcome as M, Result as N, ProbeAnswer as O, LifecycleEvent as P, Span as Q, Controller as R, ExitSignal as S, CheckVerdict as T, Phase as U, FORMAT as V, ServiceDescription as W, FailsBy as X, mayReissue as Y, Poll as Z, cooperativeAbortAt as _, WatchdogSpec as a, RequestOutcome as b, HealthServer as c, PhaseContext as d, asDebounce as et, PhaseWork as f, TransportImpl as g, Stage as h, Watchdog as i, recoverSpan as it, CheckFn as j, ProbeKind as k, Lifecycle as l, STAGES as m, ServiceBuilder as n, failSpan as nt, watchdog as o, RunOptions as p, Idempotency as q, service as r, heartbeatDetection as rt, DependencyHandle as s, ControllerBuilder as t, asWindow as tt, LifecycleState as u, stageRank as v, nodeProcess as w, EXIT_SIGNALS as x, RequestFn as y, Coupling as z };
|
|
593
|
+
//# sourceMappingURL=builder-DnfUDW9u.d.ts.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
//#region src/clock.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* This function stops a timer that `Clock.after` started. A second use does nothing. A clock of
|
|
4
|
+
* the platform also gives `unref`: after it, the timer does not keep a Node process alive.
|
|
5
|
+
*/
|
|
6
|
+
type Cancel = (() => void) & {
|
|
7
|
+
readonly unref?: (() => void) | undefined;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* A clock gives the monotonic time in milliseconds, and it starts a function after a delay. The
|
|
11
|
+
* runtime reads time only through a clock. Thus a test can give a fake clock, and the timing of
|
|
12
|
+
* the runtime is then exact and repeatable.
|
|
13
|
+
*/
|
|
14
|
+
interface Clock {
|
|
15
|
+
/** This method gives the monotonic time, in milliseconds. A step of the wall clock does not move it. */
|
|
16
|
+
now(): number;
|
|
17
|
+
/** This method starts `run` after `ms` milliseconds, and gives the function that stops it. */
|
|
18
|
+
after(ms: number, run: () => void): Cancel;
|
|
19
|
+
}
|
|
20
|
+
/** These options make a clock of the platform. */
|
|
21
|
+
interface PlatformClockOptions {
|
|
22
|
+
/** True makes each timer not keep a Node process alive. The default is false, as with `setTimeout`. */
|
|
23
|
+
readonly unref?: boolean | undefined;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* This function makes a clock of the platform: `performance.now()` and `setTimeout`. Each cancel
|
|
27
|
+
* that it gives also has `unref`.
|
|
28
|
+
*/
|
|
29
|
+
declare const platformClock: (options?: PlatformClockOptions) => Clock;
|
|
30
|
+
/**
|
|
31
|
+
* This clock reads `performance.now()` and uses `setTimeout`. Its timers do not keep a Node
|
|
32
|
+
* process alive, thus a timer that is not stopped cannot stop the exit of the process.
|
|
33
|
+
*/
|
|
34
|
+
declare const realClock: Clock;
|
|
35
|
+
//#endregion
|
|
36
|
+
export { realClock as a, platformClock as i, Clock as n, PlatformClockOptions as r, Cancel as t };
|
|
37
|
+
//# sourceMappingURL=clock-D-38uH_z.d.ts.map
|
package/build/clock.d.ts
ADDED
package/build/clock.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
//#region src/clock.ts
|
|
2
|
+
/** This function gives the monotonic time: `performance.now()`, or `Date.now()` on a platform without `performance`. */
|
|
3
|
+
const monotonic = () => {
|
|
4
|
+
const performance = globalThis.performance;
|
|
5
|
+
return performance === void 0 ? Date.now() : performance.now();
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* This function makes a clock of the platform: `performance.now()` and `setTimeout`. Each cancel
|
|
9
|
+
* that it gives also has `unref`.
|
|
10
|
+
*/
|
|
11
|
+
const platformClock = (options = {}) => ({
|
|
12
|
+
now: monotonic,
|
|
13
|
+
after(ms, run) {
|
|
14
|
+
const timer = setTimeout(run, Math.max(0, ms));
|
|
15
|
+
const unref = () => {
|
|
16
|
+
timer.unref?.();
|
|
17
|
+
};
|
|
18
|
+
if (options.unref === true) unref();
|
|
19
|
+
return Object.assign(() => clearTimeout(timer), { unref });
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
/**
|
|
23
|
+
* This clock reads `performance.now()` and uses `setTimeout`. Its timers do not keep a Node
|
|
24
|
+
* process alive, thus a timer that is not stopped cannot stop the exit of the process.
|
|
25
|
+
*/
|
|
26
|
+
const realClock = /* @__PURE__ */ platformClock({ unref: true });
|
|
27
|
+
//#endregion
|
|
28
|
+
export { platformClock, realClock };
|
|
29
|
+
|
|
30
|
+
//# sourceMappingURL=clock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clock.js","names":[],"sources":["../src/clock.ts"],"sourcesContent":["// @mark1russell7/service/clock: the clock of the runtime, with no other part of the package. It\n// imports no `node:` module and no zod. Thus a client in a browser can use it.\n\n/**\n * This function stops a timer that `Clock.after` started. A second use does nothing. A clock of\n * the platform also gives `unref`: after it, the timer does not keep a Node process alive.\n */\nexport type Cancel = (() => void) & { readonly unref?: (() => void) | undefined };\n\n/**\n * A clock gives the monotonic time in milliseconds, and it starts a function after a delay. The\n * runtime reads time only through a clock. Thus a test can give a fake clock, and the timing of\n * the runtime is then exact and repeatable.\n */\nexport interface Clock {\n /** This method gives the monotonic time, in milliseconds. A step of the wall clock does not move it. */\n now(): number;\n /** This method starts `run` after `ms` milliseconds, and gives the function that stops it. */\n after(ms: number, run: () => void): Cancel;\n}\n\n/** These options make a clock of the platform. */\nexport interface PlatformClockOptions {\n /** True makes each timer not keep a Node process alive. The default is false, as with `setTimeout`. */\n readonly unref?: boolean | undefined;\n}\n\n/** This function gives the monotonic time: `performance.now()`, or `Date.now()` on a platform without `performance`. */\nconst monotonic = (): number => {\n const performance = (globalThis as { performance?: { now(): number } }).performance;\n return performance === undefined ? Date.now() : performance.now();\n};\n\n/**\n * This function makes a clock of the platform: `performance.now()` and `setTimeout`. Each cancel\n * that it gives also has `unref`.\n */\nexport const platformClock = (options: PlatformClockOptions = {}): Clock => ({\n now: monotonic,\n after(ms, run) {\n const timer = setTimeout(run, Math.max(0, ms));\n const unref = (): void => {\n (timer as { unref?: () => void }).unref?.();\n };\n if (options.unref === true) unref();\n return Object.assign(() => clearTimeout(timer), { unref });\n },\n});\n\n/**\n * This clock reads `performance.now()` and uses `setTimeout`. Its timers do not keep a Node\n * process alive, thus a timer that is not stopped cannot stop the exit of the process.\n */\nexport const realClock: Clock = /* @__PURE__ */ platformClock({ unref: true });\n"],"mappings":";;AA4BA,MAAM,kBAA0B;CAC9B,MAAM,cAAe,WAAmD;CACxE,OAAO,gBAAgB,KAAA,IAAY,KAAK,IAAI,IAAI,YAAY,IAAI;AAClE;;;;;AAMA,MAAa,iBAAiB,UAAgC,CAAC,OAAc;CAC3E,KAAK;CACL,MAAM,IAAI,KAAK;EACb,MAAM,QAAQ,WAAW,KAAK,KAAK,IAAI,GAAG,EAAE,CAAC;EAC7C,MAAM,cAAoB;GACxB,MAAkC,QAAQ;EAC5C;EACA,IAAI,QAAQ,UAAU,MAAM,MAAM;EAClC,OAAO,OAAO,aAAa,aAAa,KAAK,GAAG,EAAE,MAAM,CAAC;CAC3D;AACF;;;;;AAMA,MAAa,YAAmC,8BAAc,EAAE,OAAO,KAAK,CAAC"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { n as Clock } from "./clock-D-38uH_z.js";
|
|
2
|
+
//#region src/heartbeat.d.ts
|
|
3
|
+
/** These options make a heartbeat. Each time is in milliseconds on the clock. */
|
|
4
|
+
export interface HeartbeatOptions {
|
|
5
|
+
/**
|
|
6
|
+
* This time is the interval of the ticks, from the start of one tick to the start of the next.
|
|
7
|
+
* A time that is not finite starts no tick.
|
|
8
|
+
*/
|
|
9
|
+
readonly interval: number;
|
|
10
|
+
/** This time bounds each ping. A time that is not finite gives a ping without a timeout. */
|
|
11
|
+
readonly timeout: number;
|
|
12
|
+
/** This count is the number of misses in sequence that make the peer dead. It is a whole number above zero. */
|
|
13
|
+
readonly misses: number;
|
|
14
|
+
/**
|
|
15
|
+
* This function sends one ping. The signal aborts at the timeout of the ping and at `stop()`.
|
|
16
|
+
* - A promise gives the answer: true when the peer answered, and false when it did not.
|
|
17
|
+
* - No value tells that the answer comes later through `answer()`, for example as a pong message.
|
|
18
|
+
* - A throw or a rejection is a miss.
|
|
19
|
+
*/
|
|
20
|
+
readonly ping: (signal: AbortSignal) => Promise<boolean> | void;
|
|
21
|
+
/** This function receives the death of the peer: the last of `misses` misses in sequence. The count of misses is then zero again. */
|
|
22
|
+
readonly onDead: () => void;
|
|
23
|
+
/** This function receives each miss, with the count of misses in sequence. */
|
|
24
|
+
readonly onMiss?: ((misses: number) => void) | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* This function operates at each tick, before the ping. The tick sends a ping only when it gives
|
|
27
|
+
* true. A client can also act at the tick, for example to connect again. Without it, each tick sends a ping.
|
|
28
|
+
*/
|
|
29
|
+
readonly due?: (() => boolean) | undefined;
|
|
30
|
+
/** This function tells if a miss counts. A miss does not count while it gives false. Without it, each miss counts. */
|
|
31
|
+
readonly live?: (() => boolean) | undefined;
|
|
32
|
+
/** True lets a tick send a ping while an earlier ping waits for its answer. The default is false: one ping at a time. */
|
|
33
|
+
readonly overlap?: boolean | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* This function waits one turn of the event loop. The heartbeat waits it before it counts an
|
|
36
|
+
* answer or a miss, thus it reads a reply that is in the socket first. Without it, the heartbeat counts at once.
|
|
37
|
+
*/
|
|
38
|
+
readonly turn?: (() => Promise<void>) | undefined;
|
|
39
|
+
/** This clock gives the ticks and the timeouts. The default is `realClock`. */
|
|
40
|
+
readonly clock?: Clock | undefined;
|
|
41
|
+
}
|
|
42
|
+
/** A heartbeat is a loop of pings that runs until `stop()`. */
|
|
43
|
+
export interface Heartbeat {
|
|
44
|
+
/** This method tells the heartbeat that the peer answered. Each ping that waits gets its answer, and the count of misses is zero. */
|
|
45
|
+
answer(): void;
|
|
46
|
+
/** This method stops the ticks and the timeouts, and it aborts each ping that waits. A second use does nothing. */
|
|
47
|
+
stop(): void;
|
|
48
|
+
/** This count is the number of misses in the current sequence. */
|
|
49
|
+
readonly misses: number;
|
|
50
|
+
/** This count is the number of pings that wait for their answer. */
|
|
51
|
+
readonly waiting: number;
|
|
52
|
+
readonly stopped: boolean;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* This function starts a heartbeat. The first tick comes one interval after the start. Each tick
|
|
56
|
+
* starts the next one first, thus the ticks keep their interval while a ping waits. A ping that has
|
|
57
|
+
* no answer at its timeout is a miss. The last miss of `misses` in sequence starts `onDead`.
|
|
58
|
+
*/
|
|
59
|
+
export declare const heartbeat: (options: HeartbeatOptions) => Heartbeat;
|
|
60
|
+
/**
|
|
61
|
+
* This function gives the most time that a heartbeat takes to find a silent peer, in milliseconds.
|
|
62
|
+
* The first ping without an answer starts one interval after the silence at the latest. The last
|
|
63
|
+
* of `misses` pings then fails at its timeout: `misses` intervals plus a timeout. An interval that
|
|
64
|
+
* is not finite gives infinity.
|
|
65
|
+
*
|
|
66
|
+
* The time holds when each tick sends a ping: with `overlap`, or with a timeout below the interval.
|
|
67
|
+
*/
|
|
68
|
+
export declare const detectionTime: (interval: number, timeout: number, misses: number) => number;
|
|
69
|
+
//#endregion
|
|
70
|
+
//# sourceMappingURL=heartbeat.d.ts.map
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { realClock } from "./clock.js";
|
|
2
|
+
//#region src/heartbeat.ts
|
|
3
|
+
/**
|
|
4
|
+
* This function starts a heartbeat. The first tick comes one interval after the start. Each tick
|
|
5
|
+
* starts the next one first, thus the ticks keep their interval while a ping waits. A ping that has
|
|
6
|
+
* no answer at its timeout is a miss. The last miss of `misses` in sequence starts `onDead`.
|
|
7
|
+
*/
|
|
8
|
+
const heartbeat = (options) => {
|
|
9
|
+
const { interval, timeout, misses: limit, clock = realClock } = options;
|
|
10
|
+
if (!Number.isInteger(limit) || limit < 1) throw new RangeError(`a heartbeat needs a count of misses that is a whole number above zero, not ${limit}`);
|
|
11
|
+
let misses = 0;
|
|
12
|
+
let stopped = false;
|
|
13
|
+
let next = null;
|
|
14
|
+
const pings = /* @__PURE__ */ new Set();
|
|
15
|
+
const count = (answered) => {
|
|
16
|
+
if (stopped) return;
|
|
17
|
+
if (answered) {
|
|
18
|
+
misses = 0;
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (options.live !== void 0 && !options.live()) return;
|
|
22
|
+
misses += 1;
|
|
23
|
+
options.onMiss?.(misses);
|
|
24
|
+
if (misses >= limit) {
|
|
25
|
+
misses = 0;
|
|
26
|
+
options.onDead();
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const send = () => {
|
|
30
|
+
const controller = new AbortController();
|
|
31
|
+
const entry = {
|
|
32
|
+
controller,
|
|
33
|
+
timer: null,
|
|
34
|
+
settle: (answered) => {
|
|
35
|
+
if (!pings.delete(entry)) return;
|
|
36
|
+
entry.timer?.();
|
|
37
|
+
const done = () => count(answered);
|
|
38
|
+
if (options.turn === void 0) done();
|
|
39
|
+
else options.turn().then(done, done);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
pings.add(entry);
|
|
43
|
+
if (Number.isFinite(timeout)) entry.timer = clock.after(timeout, () => {
|
|
44
|
+
entry.settle(false);
|
|
45
|
+
controller.abort();
|
|
46
|
+
});
|
|
47
|
+
let result;
|
|
48
|
+
try {
|
|
49
|
+
result = options.ping(controller.signal);
|
|
50
|
+
} catch {
|
|
51
|
+
entry.settle(false);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (result !== void 0) result.then((answered) => entry.settle(answered === true), () => entry.settle(false));
|
|
55
|
+
};
|
|
56
|
+
const tick = () => {
|
|
57
|
+
next = clock.after(interval, tick);
|
|
58
|
+
if (options.due !== void 0 && !options.due()) return;
|
|
59
|
+
if (stopped || options.overlap !== true && pings.size > 0) return;
|
|
60
|
+
send();
|
|
61
|
+
};
|
|
62
|
+
if (Number.isFinite(interval)) next = clock.after(interval, tick);
|
|
63
|
+
return {
|
|
64
|
+
answer() {
|
|
65
|
+
if (stopped) return;
|
|
66
|
+
misses = 0;
|
|
67
|
+
for (const entry of [...pings]) entry.settle(true);
|
|
68
|
+
},
|
|
69
|
+
stop() {
|
|
70
|
+
if (stopped) return;
|
|
71
|
+
stopped = true;
|
|
72
|
+
next?.();
|
|
73
|
+
next = null;
|
|
74
|
+
for (const entry of [...pings]) {
|
|
75
|
+
pings.delete(entry);
|
|
76
|
+
entry.timer?.();
|
|
77
|
+
entry.controller.abort();
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
get misses() {
|
|
81
|
+
return misses;
|
|
82
|
+
},
|
|
83
|
+
get waiting() {
|
|
84
|
+
return pings.size;
|
|
85
|
+
},
|
|
86
|
+
get stopped() {
|
|
87
|
+
return stopped;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* This function gives the most time that a heartbeat takes to find a silent peer, in milliseconds.
|
|
93
|
+
* The first ping without an answer starts one interval after the silence at the latest. The last
|
|
94
|
+
* of `misses` pings then fails at its timeout: `misses` intervals plus a timeout. An interval that
|
|
95
|
+
* is not finite gives infinity.
|
|
96
|
+
*
|
|
97
|
+
* The time holds when each tick sends a ping: with `overlap`, or with a timeout below the interval.
|
|
98
|
+
*/
|
|
99
|
+
const detectionTime = (interval, timeout, misses) => Number.isFinite(interval) ? misses * interval + timeout : Number.POSITIVE_INFINITY;
|
|
100
|
+
//#endregion
|
|
101
|
+
export { detectionTime, heartbeat };
|
|
102
|
+
|
|
103
|
+
//# sourceMappingURL=heartbeat.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"heartbeat.js","names":[],"sources":["../src/heartbeat.ts"],"sourcesContent":["// @mark1russell7/service/heartbeat: a heartbeat on a clock, with no other part of the package. A\n// ping starts each interval, each ping has a timeout, and a count of misses in sequence finds a\n// dead peer. The entry imports no `node:` module and no zod. Thus a client in a browser can use it.\n\nimport type { Cancel, Clock } from \"./clock.ts\";\nimport { realClock } from \"./clock.ts\";\n\n/** These options make a heartbeat. Each time is in milliseconds on the clock. */\nexport interface HeartbeatOptions {\n /**\n * This time is the interval of the ticks, from the start of one tick to the start of the next.\n * A time that is not finite starts no tick.\n */\n readonly interval: number;\n /** This time bounds each ping. A time that is not finite gives a ping without a timeout. */\n readonly timeout: number;\n /** This count is the number of misses in sequence that make the peer dead. It is a whole number above zero. */\n readonly misses: number;\n /**\n * This function sends one ping. The signal aborts at the timeout of the ping and at `stop()`.\n * - A promise gives the answer: true when the peer answered, and false when it did not.\n * - No value tells that the answer comes later through `answer()`, for example as a pong message.\n * - A throw or a rejection is a miss.\n */\n readonly ping: (signal: AbortSignal) => Promise<boolean> | void;\n /** This function receives the death of the peer: the last of `misses` misses in sequence. The count of misses is then zero again. */\n readonly onDead: () => void;\n /** This function receives each miss, with the count of misses in sequence. */\n readonly onMiss?: ((misses: number) => void) | undefined;\n /**\n * This function operates at each tick, before the ping. The tick sends a ping only when it gives\n * true. A client can also act at the tick, for example to connect again. Without it, each tick sends a ping.\n */\n readonly due?: (() => boolean) | undefined;\n /** This function tells if a miss counts. A miss does not count while it gives false. Without it, each miss counts. */\n readonly live?: (() => boolean) | undefined;\n /** True lets a tick send a ping while an earlier ping waits for its answer. The default is false: one ping at a time. */\n readonly overlap?: boolean | undefined;\n /**\n * This function waits one turn of the event loop. The heartbeat waits it before it counts an\n * answer or a miss, thus it reads a reply that is in the socket first. Without it, the heartbeat counts at once.\n */\n readonly turn?: (() => Promise<void>) | undefined;\n /** This clock gives the ticks and the timeouts. The default is `realClock`. */\n readonly clock?: Clock | undefined;\n}\n\n/** A heartbeat is a loop of pings that runs until `stop()`. */\nexport interface Heartbeat {\n /** This method tells the heartbeat that the peer answered. Each ping that waits gets its answer, and the count of misses is zero. */\n answer(): void;\n /** This method stops the ticks and the timeouts, and it aborts each ping that waits. A second use does nothing. */\n stop(): void;\n /** This count is the number of misses in the current sequence. */\n readonly misses: number;\n /** This count is the number of pings that wait for their answer. */\n readonly waiting: number;\n readonly stopped: boolean;\n}\n\ninterface Waiting {\n readonly settle: (answered: boolean) => void;\n readonly controller: AbortController;\n timer: Cancel | null;\n}\n\n/**\n * This function starts a heartbeat. The first tick comes one interval after the start. Each tick\n * starts the next one first, thus the ticks keep their interval while a ping waits. A ping that has\n * no answer at its timeout is a miss. The last miss of `misses` in sequence starts `onDead`.\n */\nexport const heartbeat = (options: HeartbeatOptions): Heartbeat => {\n const { interval, timeout, misses: limit, clock = realClock } = options;\n if (!Number.isInteger(limit) || limit < 1)\n throw new RangeError(`a heartbeat needs a count of misses that is a whole number above zero, not ${limit}`);\n let misses = 0;\n let stopped = false;\n let next: Cancel | null = null;\n const pings = new Set<Waiting>();\n\n const count = (answered: boolean): void => {\n if (stopped) return;\n if (answered) {\n misses = 0;\n return;\n }\n if (options.live !== undefined && !options.live()) return;\n misses += 1;\n options.onMiss?.(misses);\n if (misses >= limit) {\n misses = 0;\n options.onDead();\n }\n };\n\n const send = (): void => {\n const controller = new AbortController();\n const entry: Waiting = {\n controller,\n timer: null,\n settle: (answered) => {\n if (!pings.delete(entry)) return;\n entry.timer?.();\n const done = (): void => count(answered);\n if (options.turn === undefined) done();\n else void options.turn().then(done, done);\n },\n };\n pings.add(entry);\n if (Number.isFinite(timeout))\n entry.timer = clock.after(timeout, () => {\n entry.settle(false);\n controller.abort();\n });\n let result: Promise<boolean> | void;\n try {\n result = options.ping(controller.signal);\n } catch {\n entry.settle(false);\n return;\n }\n if (result !== undefined)\n result.then(\n (answered) => entry.settle(answered === true),\n () => entry.settle(false),\n );\n };\n\n const tick = (): void => {\n next = clock.after(interval, tick);\n if (options.due !== undefined && !options.due()) return;\n if (stopped || (options.overlap !== true && pings.size > 0)) return;\n send();\n };\n if (Number.isFinite(interval)) next = clock.after(interval, tick);\n\n return {\n answer() {\n if (stopped) return;\n misses = 0;\n for (const entry of [...pings]) entry.settle(true);\n },\n stop() {\n if (stopped) return;\n stopped = true;\n next?.();\n next = null;\n for (const entry of [...pings]) {\n pings.delete(entry);\n entry.timer?.();\n entry.controller.abort();\n }\n },\n get misses() {\n return misses;\n },\n get waiting() {\n return pings.size;\n },\n get stopped() {\n return stopped;\n },\n };\n};\n\n/**\n * This function gives the most time that a heartbeat takes to find a silent peer, in milliseconds.\n * The first ping without an answer starts one interval after the silence at the latest. The last\n * of `misses` pings then fails at its timeout: `misses` intervals plus a timeout. An interval that\n * is not finite gives infinity.\n *\n * The time holds when each tick sends a ping: with `overlap`, or with a timeout below the interval.\n */\nexport const detectionTime = (interval: number, timeout: number, misses: number): number =>\n Number.isFinite(interval) ? misses * interval + timeout : Number.POSITIVE_INFINITY;\n"],"mappings":";;;;;;;AAuEA,MAAa,aAAa,YAAyC;CACjE,MAAM,EAAE,UAAU,SAAS,QAAQ,OAAO,QAAQ,cAAc;CAChE,IAAI,CAAC,OAAO,UAAU,KAAK,KAAK,QAAQ,GACtC,MAAM,IAAI,WAAW,8EAA8E,OAAO;CAC5G,IAAI,SAAS;CACb,IAAI,UAAU;CACd,IAAI,OAAsB;CAC1B,MAAM,wBAAQ,IAAI,IAAa;CAE/B,MAAM,SAAS,aAA4B;EACzC,IAAI,SAAS;EACb,IAAI,UAAU;GACZ,SAAS;GACT;EACF;EACA,IAAI,QAAQ,SAAS,KAAA,KAAa,CAAC,QAAQ,KAAK,GAAG;EACnD,UAAU;EACV,QAAQ,SAAS,MAAM;EACvB,IAAI,UAAU,OAAO;GACnB,SAAS;GACT,QAAQ,OAAO;EACjB;CACF;CAEA,MAAM,aAAmB;EACvB,MAAM,aAAa,IAAI,gBAAgB;EACvC,MAAM,QAAiB;GACrB;GACA,OAAO;GACP,SAAS,aAAa;IACpB,IAAI,CAAC,MAAM,OAAO,KAAK,GAAG;IAC1B,MAAM,QAAQ;IACd,MAAM,aAAmB,MAAM,QAAQ;IACvC,IAAI,QAAQ,SAAS,KAAA,GAAW,KAAK;SAChC,QAAa,KAAK,CAAC,CAAC,KAAK,MAAM,IAAI;GAC1C;EACF;EACA,MAAM,IAAI,KAAK;EACf,IAAI,OAAO,SAAS,OAAO,GACzB,MAAM,QAAQ,MAAM,MAAM,eAAe;GACvC,MAAM,OAAO,KAAK;GAClB,WAAW,MAAM;EACnB,CAAC;EACH,IAAI;EACJ,IAAI;GACF,SAAS,QAAQ,KAAK,WAAW,MAAM;EACzC,QAAQ;GACN,MAAM,OAAO,KAAK;GAClB;EACF;EACA,IAAI,WAAW,KAAA,GACb,OAAO,MACJ,aAAa,MAAM,OAAO,aAAa,IAAI,SACtC,MAAM,OAAO,KAAK,CAC1B;CACJ;CAEA,MAAM,aAAmB;EACvB,OAAO,MAAM,MAAM,UAAU,IAAI;EACjC,IAAI,QAAQ,QAAQ,KAAA,KAAa,CAAC,QAAQ,IAAI,GAAG;EACjD,IAAI,WAAY,QAAQ,YAAY,QAAQ,MAAM,OAAO,GAAI;EAC7D,KAAK;CACP;CACA,IAAI,OAAO,SAAS,QAAQ,GAAG,OAAO,MAAM,MAAM,UAAU,IAAI;CAEhE,OAAO;EACL,SAAS;GACP,IAAI,SAAS;GACb,SAAS;GACT,KAAK,MAAM,SAAS,CAAC,GAAG,KAAK,GAAG,MAAM,OAAO,IAAI;EACnD;EACA,OAAO;GACL,IAAI,SAAS;GACb,UAAU;GACV,OAAO;GACP,OAAO;GACP,KAAK,MAAM,SAAS,CAAC,GAAG,KAAK,GAAG;IAC9B,MAAM,OAAO,KAAK;IAClB,MAAM,QAAQ;IACd,MAAM,WAAW,MAAM;GACzB;EACF;EACA,IAAI,SAAS;GACX,OAAO;EACT;EACA,IAAI,UAAU;GACZ,OAAO,MAAM;EACf;EACA,IAAI,UAAU;GACZ,OAAO;EACT;CACF;AACF;;;;;;;;;AAUA,MAAa,iBAAiB,UAAkB,SAAiB,WAC/D,OAAO,SAAS,QAAQ,IAAI,SAAS,WAAW,UAAU,OAAO"}
|
package/build/index.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as backoffDelay, i as backoffBand, n as DelayBand, o as backoffProblems, r as Jitter, t as Backoff } from "./backoff-B-jGWVDm.js";
|
|
2
|
+
import { a as realClock, i as platformClock, n as Clock, r as PlatformClockOptions, t as Cancel } from "./clock-D-38uH_z.js";
|
|
3
|
+
import { Heartbeat, HeartbeatOptions, detectionTime, heartbeat } from "./heartbeat.js";
|
|
4
|
+
import { $ as Timing, A as StartOptions, B as Dependency, C as ProcessPort, D as HealthVerdict, E as Health, F as MachineName, G as Step, H as Gate, I as Recorder, J as Operation, K as Transport, L as Check, M as Outcome, N as Result, O as ProbeAnswer, P as LifecycleEvent, Q as Span, R as Controller, S as ExitSignal, T as CheckVerdict, U as Phase, V as FORMAT, W as ServiceDescription, X as FailsBy, Y as mayReissue, Z as Poll, _ as cooperativeAbortAt, a as WatchdogSpec, b as RequestOutcome, c as HealthServer, d as PhaseContext, et as asDebounce, f as PhaseWork, g as TransportImpl, h as Stage, i as Watchdog, it as recoverSpan, j as CheckFn, k as ProbeKind, l as Lifecycle, m as STAGES, n as ServiceBuilder, nt as failSpan, o as watchdog, p as RunOptions, q as Idempotency, r as service, rt as heartbeatDetection, s as DependencyHandle, t as ControllerBuilder, tt as asWindow, u as LifecycleState, v as stageRank, w as nodeProcess, x as EXIT_SIGNALS, y as RequestFn, z as Coupling } from "./builder-DnfUDW9u.js";
|
|
2
5
|
import { Extent, Quantity } from "@mark1russell7/quantity";
|
|
6
|
+
import { replay } from "@mark1russell7/algebra/replay";
|
|
3
7
|
import { Machine } from "@mark1russell7/algebra";
|
|
4
8
|
//#region src/law.d.ts
|
|
5
9
|
/**
|
|
@@ -156,13 +160,6 @@ export declare const capKnob: (phase: string) => string;
|
|
|
156
160
|
export declare const drainMachine: (description: ServiceDescription) => Machine;
|
|
157
161
|
/** These are the static machines of the runtime, by the name that their events carry. */
|
|
158
162
|
export declare const MACHINES: Readonly<Record<Exclude<MachineName, "drain" | "client">, Machine>>;
|
|
159
|
-
/**
|
|
160
|
-
* This function replays the transitions of one machine of the runtime against its document. Each
|
|
161
|
-
* instance starts in the state that its first transition leaves. Each transition must leave the
|
|
162
|
-
* state where the replay is. The document must have a transition from that state, on that event,
|
|
163
|
-
* to that state. The function gives each disagreement.
|
|
164
|
-
*/
|
|
165
|
-
export declare const replay: (doc: Machine, events: readonly LifecycleEvent[]) => string[];
|
|
166
163
|
//#endregion
|
|
167
|
-
export { type Binding, type Bound, type Cancel, type Check, type CheckFn, type CheckVerdict, type Clock, type Controller, type ControllerBuilder, type Coupling, type Dependency, type DependencyHandle, EXIT_SIGNALS, type Evaluated, type ExitSignal, FORMAT, type FailsBy, type Finding, type Gate, type Health, type HealthServer, type HealthVerdict, type Idempotency, type Inequality, type Lifecycle, type LifecycleEvent, type LifecycleState, type MachineName, type Operation, type Outcome, type Phase, type PhaseContext, type PhaseWork, type Poll, type ProbeAnswer, type ProbeKind, type ProcessPort, type Recorder, type RequestFn, type RequestOutcome, type Result, type Rule, type RunOptions, STAGES, type Scenario, type ServiceBuilder, type ServiceDescription, type Side, type Span, type Stage, type StartOptions, type Step, type Term, type Timing, type Transport, type TransportImpl, type Verdict, type Watchdog, type WatchdogSpec, asDebounce, asWindow, cooperativeAbortAt, failSpan, mayReissue, nodeProcess, realClock, recoverSpan, service, stageRank, watchdog };
|
|
164
|
+
export { type Backoff, type Binding, type Bound, type Cancel, type Check, type CheckFn, type CheckVerdict, type Clock, type Controller, type ControllerBuilder, type Coupling, type DelayBand, type Dependency, type DependencyHandle, EXIT_SIGNALS, type Evaluated, type ExitSignal, FORMAT, type FailsBy, type Finding, type Gate, type Health, type HealthServer, type HealthVerdict, type Heartbeat, type HeartbeatOptions, type Idempotency, type Inequality, type Jitter, type Lifecycle, type LifecycleEvent, type LifecycleState, type MachineName, type Operation, type Outcome, type Phase, type PhaseContext, type PhaseWork, type PlatformClockOptions, type Poll, type ProbeAnswer, type ProbeKind, type ProcessPort, type Recorder, type RequestFn, type RequestOutcome, type Result, type Rule, type RunOptions, STAGES, type Scenario, type ServiceBuilder, type ServiceDescription, type Side, type Span, type Stage, type StartOptions, type Step, type Term, type Timing, type Transport, type TransportImpl, type Verdict, type Watchdog, type WatchdogSpec, asDebounce, asWindow, backoffBand, backoffDelay, backoffProblems, cooperativeAbortAt, detectionTime, failSpan, heartbeat, heartbeatDetection, mayReissue, nodeProcess, platformClock, realClock, recoverSpan, replay, service, stageRank, watchdog };
|
|
168
165
|
//# sourceMappingURL=index.d.ts.map
|