@mark1russell7/service 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +70 -2
- package/build/index.d.ts +349 -0
- package/build/index.js +453 -0
- package/build/index.js.map +1 -0
- package/package.json +66 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mark Russell
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,71 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @mark1russell7/service
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A service that you declare as data before it starts. You compose it with plain calls: its dependencies, its checks, its policies and its shutdown. `describe()` gives the whole graph as data, and it starts nothing. `lint()` then applies the timer-ordering law to that graph.
|
|
4
|
+
|
|
5
|
+
This release has the declaration, its export and the law. The runtime that `start()` gives the graph to comes in a later release.
|
|
6
|
+
|
|
7
|
+
## What the package does
|
|
8
|
+
|
|
9
|
+
- **The builder.** `service(name)` gives a builder. You add each part with a call: `svc.dependency(…)`, `svc.server(…)`, `svc.controller("checkout").requires(…).route(…)`, `svc.check(…)`, the policies, the couplings and the shutdown phases.
|
|
10
|
+
- **The start.** `await svc.start()` closes the composition. A dependency or a check that joins later starts tripped, and the export is partial.
|
|
11
|
+
- **No defaults.** The runtime has no defaults of its own. Thus `describe()` refuses a service that does not declare its readiness, its liveness, its startup budget, its shutdown phases or its cooperative window. The error tells which.
|
|
12
|
+
- **One timing shape.** Each policy has a `Timing`: a debounce of polls, or a window of time. Each converts to the other (`asWindow`, `asDebounce`). `failSpan` and `recoverSpan` give the earliest and the latest time to detect a change. At 3 polls of 5 s ± 20 %, a latch fails in 8–18 s on an error, and in 12–22 s on a 4 s timeout.
|
|
13
|
+
- **Checks.** A check names the step of the connection that it examines (`dial`, `round-trip`, `checkout` and others). It also tells if it uses the connection of the application, and which policy it controls. The policy has no default.
|
|
14
|
+
- **Idempotency for each operation.** One dependency has the two kinds of operation. A Redis GET is safe to send again. An INCR that goes again after a timeout counts two times. Thus each operation of a dependency, and each route of a controller, declares if it can go again: `true`, `false`, or keyed.
|
|
15
|
+
- **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.
|
|
16
|
+
- **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).
|
|
17
|
+
- **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 rule about a client applies one time for each dependency. A `depends` finding names the symbols that have no bound.
|
|
18
|
+
|
|
19
|
+
The format of a description is `mark1russell7.service/1`.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
pnpm add @mark1russell7/service zod
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The package uses [`@mark1russell7/quantity`](https://www.npmjs.com/package/@mark1russell7/quantity) for each amount, and `quantity` uses `zod` as a peer dependency. Thus the application installs `zod`.
|
|
28
|
+
|
|
29
|
+
## Example
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { lint, service } from "@mark1russell7/service";
|
|
33
|
+
import type { Rule } from "@mark1russell7/service";
|
|
34
|
+
|
|
35
|
+
const s = (value: number) => ({ value, unit: "s" });
|
|
36
|
+
const poll = { interval: s(5), jitter: 0.2, timeout: s(4) };
|
|
37
|
+
|
|
38
|
+
const svc = service("app")
|
|
39
|
+
.dependency("db", {
|
|
40
|
+
system: null,
|
|
41
|
+
knobs: { request_timeout: s(10) },
|
|
42
|
+
operations: { SELECT: { idempotent: true }, INSERT: { idempotent: false } },
|
|
43
|
+
})
|
|
44
|
+
.check({
|
|
45
|
+
name: "db.round-trip",
|
|
46
|
+
dependency: "db",
|
|
47
|
+
step: "round-trip",
|
|
48
|
+
ownConnection: true,
|
|
49
|
+
gates: "readiness",
|
|
50
|
+
poll,
|
|
51
|
+
})
|
|
52
|
+
.readiness({ type: "debounce", interval: s(5), failAfter: 3, recoverAfter: 1 })
|
|
53
|
+
.liveness({ type: "window", failAfter: s(45) })
|
|
54
|
+
.startup(s(60))
|
|
55
|
+
.phase("fail readiness", s(2))
|
|
56
|
+
.phase("drain", s(15))
|
|
57
|
+
.cooperativeWindow(s(5));
|
|
58
|
+
|
|
59
|
+
const rules: Rule[] = []; // the rules of the law, as data
|
|
60
|
+
const scenario = {
|
|
61
|
+
platform: { "termination_grace@pod": s(30), "prestop_sleep@pod": s(5) },
|
|
62
|
+
probes: { readiness: s(30), liveness: s(30) },
|
|
63
|
+
};
|
|
64
|
+
const findings = lint(svc.describe(), scenario, rules);
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Tests
|
|
68
|
+
|
|
69
|
+
`pnpm --filter @mark1russell7/service test` starts the tests: the timing algebra, the refusals of the builder, its partial export, and the four verdicts of the law. A test in jarvis compares this evaluator with the evaluator of the research, on 185 bindings. All decisions agree.
|
|
70
|
+
|
|
71
|
+
`pnpm --filter @mark1russell7/service build` writes the package for npm into `build/`.
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { Extent, Quantity } from "@mark1russell7/quantity";
|
|
2
|
+
//#region src/timing.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* All policies have the same timing shape. A timing is a count of polls (a debounce) or a
|
|
5
|
+
* duration (a window), and each converts to the other. A long window is easier to read as a
|
|
6
|
+
* duration, and that is the only difference between the two.
|
|
7
|
+
*/
|
|
8
|
+
type Timing = {
|
|
9
|
+
readonly type: "debounce";
|
|
10
|
+
/** This interval is the time between two polls of the signal. */
|
|
11
|
+
readonly interval: Quantity;
|
|
12
|
+
/** This count is the number of failed polls in sequence before the policy fails. */
|
|
13
|
+
readonly failAfter: number;
|
|
14
|
+
/** This count is the number of passed polls in sequence before the policy recovers. */
|
|
15
|
+
readonly recoverAfter: number;
|
|
16
|
+
} | {
|
|
17
|
+
readonly type: "window";
|
|
18
|
+
/** This duration is how long the signal fails before the policy fails, on the monotonic clock. */
|
|
19
|
+
readonly failAfter: Quantity;
|
|
20
|
+
/** This duration is how long the signal passes before the policy recovers. Without it, the first pass recovers the policy. */
|
|
21
|
+
readonly recoverAfter?: Quantity | undefined;
|
|
22
|
+
};
|
|
23
|
+
/** A poll has an interval, a jitter as a fraction (±20 % is 0.2) and a timeout. */
|
|
24
|
+
interface Poll {
|
|
25
|
+
readonly interval: Quantity;
|
|
26
|
+
readonly jitter: number;
|
|
27
|
+
readonly timeout: Quantity;
|
|
28
|
+
}
|
|
29
|
+
/** A span is a range of times, from the earliest to the latest. */
|
|
30
|
+
interface Span {
|
|
31
|
+
readonly min: Quantity;
|
|
32
|
+
readonly max: Quantity;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* This type tells how a failed step answers a poll. An `"error"` step refuses at once. A
|
|
36
|
+
* `"timeout"` step (a black hole) answers only when the timeout of the poll ends. Thus it is
|
|
37
|
+
* slower by that timeout, at the two ends of the span.
|
|
38
|
+
*/
|
|
39
|
+
type FailsBy = "error" | "timeout";
|
|
40
|
+
/**
|
|
41
|
+
* This function gives the time that a policy takes to fail after its signal becomes bad. A
|
|
42
|
+
* debounce of N polls takes approximately N intervals, with the jitter. A window takes its
|
|
43
|
+
* duration, plus one interval at most for the poll that starts its sequence. A step that fails by
|
|
44
|
+
* a timeout adds the timeout of the poll. At 3 polls of 5 s ± 20 %, the latch fails in 8–18 s on
|
|
45
|
+
* an error, and in 12–22 s on a 4 s timeout.
|
|
46
|
+
*/
|
|
47
|
+
export declare const failSpan: (timing: Timing, poll: Poll, by?: FailsBy) => Span;
|
|
48
|
+
/** This function gives the time that a policy takes to recover after its signal becomes good. */
|
|
49
|
+
export declare const recoverSpan: (timing: Timing, poll: Poll) => Span;
|
|
50
|
+
/** This function gives the window that a debounce approximates: N polls of an interval fail after N intervals. */
|
|
51
|
+
export declare const asWindow: (timing: Timing) => Timing;
|
|
52
|
+
/** This function gives the debounce that approximates a window on a poll: the polls that fit, rounded up. */
|
|
53
|
+
export declare const asDebounce: (timing: Timing, poll: Poll) => Timing;
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region src/operation.d.ts
|
|
56
|
+
/**
|
|
57
|
+
* This type tells if an operation can go again after a failure. It is a property of the
|
|
58
|
+
* operation, not of the dependency. One Redis client sends a GET, which is safe to send again. It
|
|
59
|
+
* also sends an INCR, which counts two times when it goes again after a timeout. Each operation
|
|
60
|
+
* declares its idempotency:
|
|
61
|
+
* - `true`: a second issue has the same effect. Examples are an HTTP GET or PUT, a SELECT, a
|
|
62
|
+
* Redis GET or SET, and a write that the server removes when it is a copy.
|
|
63
|
+
* - `false`: the client does not send it again after it can have reached the server.
|
|
64
|
+
* - keyed: the server keeps a key for a window, as with an `Idempotency-Key` header, and it
|
|
65
|
+
* ignores a copy in that window. A copy after the window is a second operation. Thus the
|
|
66
|
+
* window is a timer, and it must be longer than all retries of the operation.
|
|
67
|
+
*/
|
|
68
|
+
type Idempotency = boolean | {
|
|
69
|
+
readonly key: string;
|
|
70
|
+
readonly window: Extent;
|
|
71
|
+
};
|
|
72
|
+
/** An operation is an endpoint that the service calls or serves, a command, or a query. */
|
|
73
|
+
interface Operation {
|
|
74
|
+
readonly idempotent: Idempotency;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* This function tells if an operation can go again `since` its first issue. An idempotent
|
|
78
|
+
* operation can always go again, and other operations cannot. A keyed operation can go again
|
|
79
|
+
* while the server keeps its key.
|
|
80
|
+
*/
|
|
81
|
+
export declare const mayReissue: (operation: Operation, since: Quantity) => boolean;
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/description.d.ts
|
|
84
|
+
/**
|
|
85
|
+
* This is the format of a service as data: what `describe()` exports, and nothing starts. The
|
|
86
|
+
* timer algebra and the ordering law read it. Each timer has a unit. Each knob comes from the
|
|
87
|
+
* declaration, because the runtime has no defaults. A bound that nobody gives is "no bound", not
|
|
88
|
+
* zero.
|
|
89
|
+
*/
|
|
90
|
+
export declare const FORMAT = "mark1russell7.service/1";
|
|
91
|
+
/** A step is a step of a connection that a check examines. A check that skips a step cannot see a failure in it. */
|
|
92
|
+
type Step = "dial" | "local-state" | "round-trip" | "checkout" | "progress" | "topology" | "latency";
|
|
93
|
+
/** A gate is the policy that the failures of a check count toward. */
|
|
94
|
+
type Gate = "readiness" | "liveness";
|
|
95
|
+
/** A check is an active check: one signal, for one step of a connection. */
|
|
96
|
+
interface Check {
|
|
97
|
+
readonly name: string;
|
|
98
|
+
/** This name is the dependency that it examines, or null when it reads the state of the process. */
|
|
99
|
+
readonly dependency: string | null;
|
|
100
|
+
readonly step: Step;
|
|
101
|
+
/** This flag tells if it uses the connection of the application. A check on its own connection cannot see a failure of that connection. */
|
|
102
|
+
readonly ownConnection: boolean;
|
|
103
|
+
/** This gate is the policy that it controls, or null for a check that only reports. It has no default. */
|
|
104
|
+
readonly gates: Gate | null;
|
|
105
|
+
readonly poll: Poll;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* A dependency is something that the service uses, with its client. Its knobs have the names that
|
|
109
|
+
* the ordering law uses: `request_timeout`, `connect_bound`, `driver_writeoff_time`,
|
|
110
|
+
* `heartbeat_interval`, `client_retry_window`. Thus the law reads them without a translation.
|
|
111
|
+
*/
|
|
112
|
+
interface Dependency {
|
|
113
|
+
readonly name: string;
|
|
114
|
+
/** This identifier is the component in the corpus (`systems.component.postgresql`), or null when the corpus does not have it. */
|
|
115
|
+
readonly system: string | null;
|
|
116
|
+
readonly knobs: Readonly<Record<string, Extent>>;
|
|
117
|
+
/**
|
|
118
|
+
* These are the operations that the service does with it, by name (`GET /v1/forecast`, `INCR`).
|
|
119
|
+
* Each operation declares if it can go again. A dependency usually has the two kinds, thus a
|
|
120
|
+
* dependency has no idempotency of its own. An operation without a declaration does not go again.
|
|
121
|
+
*/
|
|
122
|
+
readonly operations: Readonly<Record<string, Operation>>;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* A transport is something that takes work in: a server, a consumer or a worker. Each is a
|
|
126
|
+
* component with a state machine. The service is ready only while each transport takes work.
|
|
127
|
+
*/
|
|
128
|
+
interface Transport {
|
|
129
|
+
readonly name: string;
|
|
130
|
+
readonly kind: "server" | "consumer" | "worker";
|
|
131
|
+
/** These are its knobs, with the names of the law where the law has them: the `server_idle_close` of a server. */
|
|
132
|
+
readonly knobs: Readonly<Record<string, Extent>>;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* A controller is a group of routes. It is ready only while the dependencies that it needs are
|
|
136
|
+
* ready. Each route declares if its callers can send it again. When the shutdown stops a request,
|
|
137
|
+
* an idempotent route answers 503, which a caller can retry. Other routes answer 500.
|
|
138
|
+
*/
|
|
139
|
+
interface Controller {
|
|
140
|
+
readonly name: string;
|
|
141
|
+
readonly requires: readonly string[];
|
|
142
|
+
readonly routes: Readonly<Record<string, Operation>>;
|
|
143
|
+
}
|
|
144
|
+
/** A coupling is a declared edge between two policies. The algebra checks its margin. */
|
|
145
|
+
interface Coupling {
|
|
146
|
+
readonly from: Gate;
|
|
147
|
+
readonly to: Gate;
|
|
148
|
+
readonly effect: "fails";
|
|
149
|
+
}
|
|
150
|
+
/** A phase is one step of the shutdown sequence, with a cap. Its owner adds it. */
|
|
151
|
+
interface Phase {
|
|
152
|
+
readonly name: string;
|
|
153
|
+
readonly cap: Quantity;
|
|
154
|
+
}
|
|
155
|
+
/** A service description is the whole graph of a service, as data. */
|
|
156
|
+
interface ServiceDescription {
|
|
157
|
+
readonly format: typeof FORMAT;
|
|
158
|
+
readonly name: string;
|
|
159
|
+
/** This flag tells if a part joined after the start. That part starts tripped, and the export is partial. */
|
|
160
|
+
readonly partial: boolean;
|
|
161
|
+
readonly transports: readonly Transport[];
|
|
162
|
+
readonly dependencies: readonly Dependency[];
|
|
163
|
+
readonly controllers: readonly Controller[];
|
|
164
|
+
readonly checks: readonly Check[];
|
|
165
|
+
readonly policies: {
|
|
166
|
+
readonly readiness: Timing;
|
|
167
|
+
readonly liveness: Timing;
|
|
168
|
+
/** This duration is how long the service can take to start. The startup probe must agree with it. */
|
|
169
|
+
readonly startup: Quantity;
|
|
170
|
+
};
|
|
171
|
+
readonly couplings: readonly Coupling[];
|
|
172
|
+
readonly shutdown: {
|
|
173
|
+
readonly phases: readonly Phase[];
|
|
174
|
+
/** This window is how long before the end of the drain the cooperative abort occurs, or no bound. */
|
|
175
|
+
readonly cooperativeWindow: Extent;
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
//#endregion
|
|
179
|
+
//#region src/builder.d.ts
|
|
180
|
+
/**
|
|
181
|
+
* A service is composed with plain method uses before it starts. The builder is imperative, and
|
|
182
|
+
* it holds static knowledge. The service does not use an `init` function that gives registrations.
|
|
183
|
+
*
|
|
184
|
+
* const svc = service("app");
|
|
185
|
+
* svc.dependency("weather", {
|
|
186
|
+
* system: null,
|
|
187
|
+
* knobs: { request_timeout },
|
|
188
|
+
* operations: { "GET /v1/forecast": { idempotent: true } },
|
|
189
|
+
* });
|
|
190
|
+
* svc.controller("forecast").requires("weather").route("GET /api/forecast", { idempotent: true });
|
|
191
|
+
* svc.check({ name: "weather.round-trip", dependency: "weather", … });
|
|
192
|
+
* svc.describe(); // the whole graph as data, with nothing started
|
|
193
|
+
* await svc.start();
|
|
194
|
+
*
|
|
195
|
+
* The graph has one source, thus its parts cannot disagree. A dependency or a check can join
|
|
196
|
+
* after the start. It starts tripped, and the export is partial. The runtime has no defaults of
|
|
197
|
+
* its own. Thus `describe()` refuses a service that does not declare a policy, its startup budget
|
|
198
|
+
* or its shutdown, and it tells which.
|
|
199
|
+
*/
|
|
200
|
+
interface ControllerBuilder {
|
|
201
|
+
/** This method declares the dependencies that its routes need. The controller is ready only while they are ready. */
|
|
202
|
+
requires(...dependencies: string[]): ControllerBuilder;
|
|
203
|
+
/** This method declares a route that the controller serves, and if its callers can send it again. */
|
|
204
|
+
route(route: string, operation: Operation): ControllerBuilder;
|
|
205
|
+
}
|
|
206
|
+
/** A service builder composes a service, gives it as data, and starts it. */
|
|
207
|
+
interface ServiceBuilder {
|
|
208
|
+
readonly name: string;
|
|
209
|
+
dependency(name: string, spec: Omit<Dependency, "name">): ServiceBuilder;
|
|
210
|
+
server(name: string, knobs: Readonly<Record<string, Extent>>): ServiceBuilder;
|
|
211
|
+
consumer(name: string, knobs: Readonly<Record<string, Extent>>): ServiceBuilder;
|
|
212
|
+
worker(name: string, knobs: Readonly<Record<string, Extent>>): ServiceBuilder;
|
|
213
|
+
controller(name: string): ControllerBuilder;
|
|
214
|
+
check(check: Check): ServiceBuilder;
|
|
215
|
+
readiness(timing: Timing): ServiceBuilder;
|
|
216
|
+
liveness(timing: Timing): ServiceBuilder;
|
|
217
|
+
startup(budget: Quantity): ServiceBuilder;
|
|
218
|
+
couple(coupling: Coupling): ServiceBuilder;
|
|
219
|
+
phase(name: string, cap: Quantity): ServiceBuilder;
|
|
220
|
+
cooperativeWindow(window: Extent): ServiceBuilder;
|
|
221
|
+
/** This method gives the service as data, and it starts nothing. */
|
|
222
|
+
describe(): ServiceDescription;
|
|
223
|
+
/** This method closes the composition. A part that joins after it starts tripped, and it makes the export partial. */
|
|
224
|
+
start(): Promise<void>;
|
|
225
|
+
}
|
|
226
|
+
/** This function starts the composition of a service. */
|
|
227
|
+
export declare const service: (name: string) => ServiceBuilder;
|
|
228
|
+
//#endregion
|
|
229
|
+
//#region src/law.d.ts
|
|
230
|
+
/**
|
|
231
|
+
* This module is the evaluator of the timer-ordering law. Its decisions are the same as the
|
|
232
|
+
* decisions of the research evaluator. The rules are data, and the caller gives them. Each rule
|
|
233
|
+
* is one or more linear inequalities over symbols at a hop (`readiness_detection_window@pod`),
|
|
234
|
+
* with a margin. A binding maps `symbol@hop` to seconds, or to "inf" for a timer that is absent.
|
|
235
|
+
*/
|
|
236
|
+
/** A term is a coefficient times one symbol, at a hop when it has one. */
|
|
237
|
+
interface Term {
|
|
238
|
+
readonly knob: string;
|
|
239
|
+
readonly coef: number;
|
|
240
|
+
readonly at?: string | undefined;
|
|
241
|
+
}
|
|
242
|
+
/** A side is a sum of terms, plus a constant. */
|
|
243
|
+
interface Side {
|
|
244
|
+
readonly terms: readonly Term[];
|
|
245
|
+
readonly const?: number | undefined;
|
|
246
|
+
}
|
|
247
|
+
/** An inequality compares two sides, with a margin. */
|
|
248
|
+
interface Inequality {
|
|
249
|
+
readonly lhs: Side;
|
|
250
|
+
readonly op: "<" | "<=" | ">" | ">=";
|
|
251
|
+
readonly rhs: Side;
|
|
252
|
+
/** The margin is in seconds, or it is words for a margin that no binding gives ("a factor of several"). Words count as 0. */
|
|
253
|
+
readonly margin?: number | string | undefined;
|
|
254
|
+
}
|
|
255
|
+
/** A rule is one or more inequalities, with their statement in words. */
|
|
256
|
+
interface Rule {
|
|
257
|
+
readonly id: string;
|
|
258
|
+
/** This text is the rule in words. */
|
|
259
|
+
readonly rule: string;
|
|
260
|
+
/** This text is the window that a violation opens. */
|
|
261
|
+
readonly window?: string | undefined;
|
|
262
|
+
readonly expr: Inequality;
|
|
263
|
+
readonly expr_more?: readonly Inequality[] | undefined;
|
|
264
|
+
}
|
|
265
|
+
/** A bound is in seconds, or it is "inf" for an absent timer, or "off" for a disabled feature. The last two have no limit. */
|
|
266
|
+
type Bound = number | "inf" | "off";
|
|
267
|
+
/**
|
|
268
|
+
* A binding maps `symbol@hop` to its bound. Its "margin" replaces the margin of each inequality.
|
|
269
|
+
* Its "margins" replace them one by one, in sequence, as the binding of a chain can.
|
|
270
|
+
*/
|
|
271
|
+
type Binding = Readonly<Record<string, Bound | readonly number[] | undefined>> & {
|
|
272
|
+
readonly margin?: number | undefined;
|
|
273
|
+
readonly margins?: readonly number[] | undefined;
|
|
274
|
+
};
|
|
275
|
+
/** A verdict is "holds", "violated", "unbounded" (it holds only because a timer is absent) or "depends" (nothing is bound). */
|
|
276
|
+
type Verdict = "holds" | "violated" | "unbounded" | "depends";
|
|
277
|
+
/** An evaluation is the value of each side of one inequality, its margin, and if it holds. */
|
|
278
|
+
interface Evaluated {
|
|
279
|
+
readonly lhs: number | null;
|
|
280
|
+
readonly rhs: number | null;
|
|
281
|
+
readonly margin: number;
|
|
282
|
+
readonly holds: boolean | null;
|
|
283
|
+
}
|
|
284
|
+
/** This function gives the inequalities of a rule: its expr, then the others. */
|
|
285
|
+
export declare const inequalities: (rule: Rule) => readonly Inequality[];
|
|
286
|
+
/** This function gives the symbols that a rule reads, as `symbol@hop`. */
|
|
287
|
+
export declare const symbolsOf: (rule: Rule) => string[];
|
|
288
|
+
/**
|
|
289
|
+
* This function evaluates each inequality on one binding. "<" holds when lhs + margin < rhs, and
|
|
290
|
+
* ">" holds when lhs > rhs + margin. Only the inequalities with all their symbols bound apply.
|
|
291
|
+
* The others are null.
|
|
292
|
+
*/
|
|
293
|
+
export declare const evaluate: (rule: Rule, bind: Binding) => Evaluated[];
|
|
294
|
+
/**
|
|
295
|
+
* This function gives the verdict of an evaluation. When an applicable inequality fails, the
|
|
296
|
+
* verdict is "violated". When all hold only because a timer is absent, the verdict is "unbounded",
|
|
297
|
+
* because nothing then limits the window. Otherwise the verdict is "holds". When no inequality
|
|
298
|
+
* applies, the arithmetic cannot decide, and the verdict is "depends".
|
|
299
|
+
*/
|
|
300
|
+
export declare const verdictOf: (rule: Rule, bind: Binding, evaluated: readonly Evaluated[]) => Verdict;
|
|
301
|
+
//#endregion
|
|
302
|
+
//#region src/lint.d.ts
|
|
303
|
+
/**
|
|
304
|
+
* This module applies the ordering law to a declaration. The declaration binds the symbols of the
|
|
305
|
+
* service, from its primitives: the polls of its checks, the windows of its policies, its drain.
|
|
306
|
+
* The scenario binds the symbols of the platform (preStop, grace, the probes of the kubelet,
|
|
307
|
+
* propagation) and of each dependency (its failover time, the idle close of its server). Thus the
|
|
308
|
+
* service gives the inequalities, and the scenario gives the values. A rule about a client
|
|
309
|
+
* applies one time for each dependency.
|
|
310
|
+
*/
|
|
311
|
+
/** A scenario gives the values of the platform and of each dependency, by the `symbol@hop` of the law. */
|
|
312
|
+
interface Scenario {
|
|
313
|
+
/** These are the values of the platform: prestop_sleep@pod, termination_grace@pod, probe_period@pod, propagation_bound@cluster. */
|
|
314
|
+
readonly platform: Readonly<Record<string, Extent>>;
|
|
315
|
+
/**
|
|
316
|
+
* These are the probe windows of the kubelet, period × failure threshold. They add to the
|
|
317
|
+
* windows of the declaration. A probe reads the cached verdict, thus detection is the latch,
|
|
318
|
+
* then the probe.
|
|
319
|
+
*/
|
|
320
|
+
readonly probes: {
|
|
321
|
+
readonly readiness: Quantity;
|
|
322
|
+
readonly liveness: Quantity;
|
|
323
|
+
};
|
|
324
|
+
/** These are the values of each dependency, by its name: failover_time@dependency, server_idle_close@server. */
|
|
325
|
+
readonly dependencies?: Readonly<Record<string, Readonly<Record<string, Extent>>>> | undefined;
|
|
326
|
+
}
|
|
327
|
+
/** A finding is the verdict of one rule for one scope: the service, or the client of a dependency. */
|
|
328
|
+
interface Finding {
|
|
329
|
+
readonly rule: string;
|
|
330
|
+
readonly scope: string;
|
|
331
|
+
readonly verdict: Verdict;
|
|
332
|
+
readonly statement: string;
|
|
333
|
+
readonly window?: string | undefined;
|
|
334
|
+
readonly evaluated: readonly Evaluated[];
|
|
335
|
+
/** These are the symbols that have no bound. A "depends" verdict waits for them. */
|
|
336
|
+
readonly unbound: readonly string[];
|
|
337
|
+
}
|
|
338
|
+
/** These are the identifiers of the rules that each declaration gets. */
|
|
339
|
+
export declare const SERVICE_RULES: readonly string[];
|
|
340
|
+
/** This function gives what the declaration tells about the service, as symbols of the law, and the source of each. */
|
|
341
|
+
export declare const serviceBindings: (svc: ServiceDescription, scenario: Scenario) => {
|
|
342
|
+
readonly bind: Record<string, Bound>;
|
|
343
|
+
readonly from: Record<string, string>;
|
|
344
|
+
};
|
|
345
|
+
/** This function applies each service rule to a declaration and a scenario: one time for the service, and one time for each client. */
|
|
346
|
+
export declare const lint: (svc: ServiceDescription, scenario: Scenario, rules: readonly Rule[]) => Finding[];
|
|
347
|
+
//#endregion
|
|
348
|
+
export type { Binding, Bound, Check, Controller, ControllerBuilder, Coupling, Dependency, Evaluated, FailsBy, Finding, Gate, Idempotency, Inequality, Operation, Phase, Poll, Rule, Scenario, ServiceBuilder, ServiceDescription, Side, Span, Step, Term, Timing, Transport, Verdict };
|
|
349
|
+
//# sourceMappingURL=index.d.ts.map
|
package/build/index.js
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import { compare, convertExact, convertible, 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.
|
|
21
|
+
*/
|
|
22
|
+
const failSpan = (timing, poll, by = "error") => {
|
|
23
|
+
const jitter = fraction(poll.jitter);
|
|
24
|
+
const late = by === "timeout" ? seconds(poll.timeout) : rationals.ZERO;
|
|
25
|
+
if (timing.type === "debounce") {
|
|
26
|
+
const [min, max] = debounced(count(timing.failAfter, "failAfter"), seconds(timing.interval), jitter);
|
|
27
|
+
return {
|
|
28
|
+
min: out(rationals.add(min, late)),
|
|
29
|
+
max: out(rationals.add(max, late))
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const duration = seconds(timing.failAfter);
|
|
33
|
+
const interval = rationals.mul(seconds(poll.interval), rationals.add(rationals.ONE, jitter));
|
|
34
|
+
return {
|
|
35
|
+
min: out(rationals.add(duration, late)),
|
|
36
|
+
max: out(rationals.add(rationals.add(duration, interval), late))
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
/** This function gives the time that a policy takes to recover after its signal becomes good. */
|
|
40
|
+
const recoverSpan = (timing, poll) => {
|
|
41
|
+
const jitter = fraction(poll.jitter);
|
|
42
|
+
if (timing.type === "debounce") {
|
|
43
|
+
const [min, max] = debounced(count(timing.recoverAfter, "recoverAfter"), seconds(timing.interval), jitter);
|
|
44
|
+
return {
|
|
45
|
+
min: out(min),
|
|
46
|
+
max: out(max)
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const duration = timing.recoverAfter === void 0 ? rationals.ZERO : seconds(timing.recoverAfter);
|
|
50
|
+
const interval = rationals.mul(seconds(poll.interval), rationals.add(rationals.ONE, jitter));
|
|
51
|
+
return {
|
|
52
|
+
min: out(duration),
|
|
53
|
+
max: out(rationals.add(duration, interval))
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
/** This function gives the window that a debounce approximates: N polls of an interval fail after N intervals. */
|
|
57
|
+
const asWindow = (timing) => timing.type === "window" ? timing : {
|
|
58
|
+
type: "window",
|
|
59
|
+
failAfter: out(rationals.mul(count(timing.failAfter, "failAfter"), seconds(timing.interval))),
|
|
60
|
+
recoverAfter: out(rationals.mul(count(timing.recoverAfter, "recoverAfter"), seconds(timing.interval)))
|
|
61
|
+
};
|
|
62
|
+
/** This function gives the debounce that approximates a window on a poll: the polls that fit, rounded up. */
|
|
63
|
+
const asDebounce = (timing, poll) => {
|
|
64
|
+
if (timing.type === "debounce") return timing;
|
|
65
|
+
const interval = seconds(poll.interval);
|
|
66
|
+
const polls = (duration) => duration === void 0 ? 1 : Math.max(1, Number(rationals.ceil(rationals.div(duration, interval)).n));
|
|
67
|
+
return {
|
|
68
|
+
type: "debounce",
|
|
69
|
+
interval: poll.interval,
|
|
70
|
+
failAfter: polls(seconds(timing.failAfter)),
|
|
71
|
+
recoverAfter: polls(timing.recoverAfter === void 0 ? void 0 : seconds(timing.recoverAfter))
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region src/description.ts
|
|
76
|
+
/**
|
|
77
|
+
* This is the format of a service as data: what `describe()` exports, and nothing starts. The
|
|
78
|
+
* timer algebra and the ordering law read it. Each timer has a unit. Each knob comes from the
|
|
79
|
+
* declaration, because the runtime has no defaults. A bound that nobody gives is "no bound", not
|
|
80
|
+
* zero.
|
|
81
|
+
*/
|
|
82
|
+
const FORMAT = "mark1russell7.service/1";
|
|
83
|
+
//#endregion
|
|
84
|
+
//#region src/operation.ts
|
|
85
|
+
/**
|
|
86
|
+
* This function tells if an operation can go again `since` its first issue. An idempotent
|
|
87
|
+
* operation can always go again, and other operations cannot. A keyed operation can go again
|
|
88
|
+
* while the server keeps its key.
|
|
89
|
+
*/
|
|
90
|
+
const mayReissue = (operation, since) => {
|
|
91
|
+
const { idempotent } = operation;
|
|
92
|
+
if (typeof idempotent === "boolean") return idempotent;
|
|
93
|
+
return compare(since, idempotent.window) < 0;
|
|
94
|
+
};
|
|
95
|
+
/** This function gives the problems in the declaration of an operation that `describe()` refuses. */
|
|
96
|
+
const operationProblems = (owner, name, operation) => {
|
|
97
|
+
const { idempotent } = operation;
|
|
98
|
+
if (typeof idempotent === "boolean") return [];
|
|
99
|
+
const problems = [];
|
|
100
|
+
if (idempotent.key.trim().length === 0) problems.push(`${owner}'s ${name} names no key`);
|
|
101
|
+
if (!convertible(idempotent.window.unit, "s")) problems.push(`${owner}'s ${name} holds its key for a span that is not a time`);
|
|
102
|
+
return problems;
|
|
103
|
+
};
|
|
104
|
+
//#endregion
|
|
105
|
+
//#region src/builder.ts
|
|
106
|
+
const isTime = (amount) => convertible(amount.unit, "s");
|
|
107
|
+
/** This function starts the composition of a service. */
|
|
108
|
+
const service = (name) => {
|
|
109
|
+
if (name.trim().length === 0) throw new Error("a service needs a name");
|
|
110
|
+
const transports = [];
|
|
111
|
+
const dependencies = [];
|
|
112
|
+
const controllers = [];
|
|
113
|
+
const checks = [];
|
|
114
|
+
const couplings = [];
|
|
115
|
+
const phases = [];
|
|
116
|
+
let readiness;
|
|
117
|
+
let liveness;
|
|
118
|
+
let startup;
|
|
119
|
+
let cooperative;
|
|
120
|
+
let started = false;
|
|
121
|
+
let partial = false;
|
|
122
|
+
/** This function stops a change to the shape of the service after the start. */
|
|
123
|
+
const composing = (what) => {
|
|
124
|
+
if (started) throw new Error(`${name} has started: ${what} is declared before svc.start()`);
|
|
125
|
+
};
|
|
126
|
+
const once = (list, entry, what) => {
|
|
127
|
+
if (list.some((held) => held.name === entry)) throw new Error(`${name} already has a ${what} named ${entry}`);
|
|
128
|
+
};
|
|
129
|
+
const transport = (kind) => (entry, knobs) => {
|
|
130
|
+
composing(`a ${kind}`);
|
|
131
|
+
once(transports, entry, "transport");
|
|
132
|
+
transports.push({
|
|
133
|
+
name: entry,
|
|
134
|
+
kind,
|
|
135
|
+
knobs: { ...knobs }
|
|
136
|
+
});
|
|
137
|
+
return builder;
|
|
138
|
+
};
|
|
139
|
+
const builder = {
|
|
140
|
+
name,
|
|
141
|
+
dependency(entry, spec) {
|
|
142
|
+
once(dependencies, entry, "dependency");
|
|
143
|
+
dependencies.push({
|
|
144
|
+
name: entry,
|
|
145
|
+
...spec,
|
|
146
|
+
knobs: { ...spec.knobs },
|
|
147
|
+
operations: { ...spec.operations }
|
|
148
|
+
});
|
|
149
|
+
if (started) partial = true;
|
|
150
|
+
return builder;
|
|
151
|
+
},
|
|
152
|
+
server: transport("server"),
|
|
153
|
+
consumer: transport("consumer"),
|
|
154
|
+
worker: transport("worker"),
|
|
155
|
+
controller(entry) {
|
|
156
|
+
composing("a controller");
|
|
157
|
+
once(controllers, entry, "controller");
|
|
158
|
+
const held = {
|
|
159
|
+
name: entry,
|
|
160
|
+
requires: [],
|
|
161
|
+
routes: {}
|
|
162
|
+
};
|
|
163
|
+
controllers.push(held);
|
|
164
|
+
const handle = {
|
|
165
|
+
requires(...needed) {
|
|
166
|
+
composing(`what ${entry} requires`);
|
|
167
|
+
held.requires.push(...needed);
|
|
168
|
+
return handle;
|
|
169
|
+
},
|
|
170
|
+
route(served, operation) {
|
|
171
|
+
composing(`${entry}'s routes`);
|
|
172
|
+
if (held.routes[served] !== void 0) throw new Error(`${entry} already serves ${served}`);
|
|
173
|
+
held.routes[served] = { ...operation };
|
|
174
|
+
return handle;
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
return handle;
|
|
178
|
+
},
|
|
179
|
+
check(check) {
|
|
180
|
+
once(checks, check.name, "check");
|
|
181
|
+
checks.push({ ...check });
|
|
182
|
+
if (started) partial = true;
|
|
183
|
+
return builder;
|
|
184
|
+
},
|
|
185
|
+
readiness(timing) {
|
|
186
|
+
composing("readiness");
|
|
187
|
+
readiness = timing;
|
|
188
|
+
return builder;
|
|
189
|
+
},
|
|
190
|
+
liveness(timing) {
|
|
191
|
+
composing("liveness");
|
|
192
|
+
liveness = timing;
|
|
193
|
+
return builder;
|
|
194
|
+
},
|
|
195
|
+
startup(budget) {
|
|
196
|
+
composing("the startup budget");
|
|
197
|
+
startup = budget;
|
|
198
|
+
return builder;
|
|
199
|
+
},
|
|
200
|
+
couple(coupling) {
|
|
201
|
+
composing("a coupling");
|
|
202
|
+
couplings.push({ ...coupling });
|
|
203
|
+
return builder;
|
|
204
|
+
},
|
|
205
|
+
phase(entry, cap) {
|
|
206
|
+
composing("a shutdown phase");
|
|
207
|
+
once(phases, entry, "shutdown phase");
|
|
208
|
+
phases.push({
|
|
209
|
+
name: entry,
|
|
210
|
+
cap
|
|
211
|
+
});
|
|
212
|
+
return builder;
|
|
213
|
+
},
|
|
214
|
+
cooperativeWindow(window) {
|
|
215
|
+
composing("the cooperative window");
|
|
216
|
+
cooperative = window;
|
|
217
|
+
return builder;
|
|
218
|
+
},
|
|
219
|
+
describe() {
|
|
220
|
+
const missing = [];
|
|
221
|
+
if (readiness === void 0) missing.push("readiness (svc.readiness)");
|
|
222
|
+
if (liveness === void 0) missing.push("liveness (svc.liveness)");
|
|
223
|
+
if (startup === void 0) missing.push("a startup budget (svc.startup)");
|
|
224
|
+
if (phases.length === 0) missing.push("its shutdown phases (svc.phase)");
|
|
225
|
+
if (cooperative === void 0) missing.push("a cooperative window, or no bound (svc.cooperativeWindow)");
|
|
226
|
+
if (missing.length > 0) throw new Error(`${name} does not declare ${missing.join("; ")}: the runtime has no defaults`);
|
|
227
|
+
const known = new Set(dependencies.map((entry) => entry.name));
|
|
228
|
+
const problems = [];
|
|
229
|
+
for (const check of checks) {
|
|
230
|
+
if (check.dependency !== null && !known.has(check.dependency)) problems.push(`check ${check.name} exercises ${check.dependency}, which is not a dependency`);
|
|
231
|
+
if (!isTime(check.poll.interval) || !isTime(check.poll.timeout)) problems.push(`check ${check.name} polls in a unit that is not a time`);
|
|
232
|
+
}
|
|
233
|
+
for (const controller of controllers) {
|
|
234
|
+
for (const needed of controller.requires) if (!known.has(needed)) problems.push(`controller ${controller.name} requires ${needed}, which is not a dependency`);
|
|
235
|
+
for (const [route, operation] of Object.entries(controller.routes)) problems.push(...operationProblems(`controller ${controller.name}`, route, operation));
|
|
236
|
+
}
|
|
237
|
+
for (const dependency of dependencies) for (const [name, operation] of Object.entries(dependency.operations)) problems.push(...operationProblems(`dependency ${dependency.name}`, name, operation));
|
|
238
|
+
for (const phase of phases) if (!isTime(phase.cap)) problems.push(`phase ${phase.name}'s cap is not a time`);
|
|
239
|
+
if (problems.length > 0) throw new Error(`${name}: ${problems.join("; ")}`);
|
|
240
|
+
const controllersOut = controllers.map((entry) => ({
|
|
241
|
+
name: entry.name,
|
|
242
|
+
requires: [...entry.requires],
|
|
243
|
+
routes: { ...entry.routes }
|
|
244
|
+
}));
|
|
245
|
+
return {
|
|
246
|
+
format: FORMAT,
|
|
247
|
+
name,
|
|
248
|
+
partial,
|
|
249
|
+
transports: transports.map((entry) => ({
|
|
250
|
+
...entry,
|
|
251
|
+
knobs: { ...entry.knobs }
|
|
252
|
+
})),
|
|
253
|
+
dependencies: dependencies.map((entry) => ({
|
|
254
|
+
...entry,
|
|
255
|
+
knobs: { ...entry.knobs },
|
|
256
|
+
operations: { ...entry.operations }
|
|
257
|
+
})),
|
|
258
|
+
controllers: controllersOut,
|
|
259
|
+
checks: checks.map((entry) => ({
|
|
260
|
+
...entry,
|
|
261
|
+
poll: { ...entry.poll }
|
|
262
|
+
})),
|
|
263
|
+
policies: {
|
|
264
|
+
readiness,
|
|
265
|
+
liveness,
|
|
266
|
+
startup
|
|
267
|
+
},
|
|
268
|
+
couplings: couplings.map((entry) => ({ ...entry })),
|
|
269
|
+
shutdown: {
|
|
270
|
+
phases: phases.map((entry) => ({ ...entry })),
|
|
271
|
+
cooperativeWindow: cooperative
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
},
|
|
275
|
+
start() {
|
|
276
|
+
builder.describe();
|
|
277
|
+
started = true;
|
|
278
|
+
return Promise.resolve();
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
return builder;
|
|
282
|
+
};
|
|
283
|
+
//#endregion
|
|
284
|
+
//#region src/law.ts
|
|
285
|
+
const value = (bound) => bound === "inf" || bound === "off" ? Infinity : Number(bound);
|
|
286
|
+
const keyOf = (term) => term.at !== void 0 ? `${term.knob}@${term.at}` : term.knob;
|
|
287
|
+
/** This function gives the bound of a symbol, or undefined when the symbol has no bound. */
|
|
288
|
+
const boundOf$1 = (bind, key) => {
|
|
289
|
+
const bound = bind[key];
|
|
290
|
+
return typeof bound === "number" || bound === "inf" || bound === "off" ? bound : void 0;
|
|
291
|
+
};
|
|
292
|
+
/** This function gives the inequalities of a rule: its expr, then the others. */
|
|
293
|
+
const inequalities = (rule) => [rule.expr, ...rule.expr_more ?? []];
|
|
294
|
+
/** This function gives the symbols that a rule reads, as `symbol@hop`. */
|
|
295
|
+
const symbolsOf = (rule) => {
|
|
296
|
+
const out = [];
|
|
297
|
+
for (const inequality of inequalities(rule)) for (const term of [...inequality.lhs.terms, ...inequality.rhs.terms]) {
|
|
298
|
+
const key = keyOf(term);
|
|
299
|
+
if (!out.includes(key)) out.push(key);
|
|
300
|
+
}
|
|
301
|
+
return out;
|
|
302
|
+
};
|
|
303
|
+
/** This function gives the value of a side, Σ coef × symbol + const, or null when a symbol has no bound. */
|
|
304
|
+
const sideOf = (side, bind) => {
|
|
305
|
+
let total = side.const ?? 0;
|
|
306
|
+
for (const term of side.terms) {
|
|
307
|
+
const bound = boundOf$1(bind, keyOf(term));
|
|
308
|
+
if (bound === void 0) return null;
|
|
309
|
+
total += term.coef * value(bound);
|
|
310
|
+
}
|
|
311
|
+
return total;
|
|
312
|
+
};
|
|
313
|
+
/** This function gives the margin of an inequality: from the binding (one for all, or one each), else from the rule, else 0. */
|
|
314
|
+
const marginOf = (inequality, index, bind) => {
|
|
315
|
+
if (Array.isArray(bind.margins)) return Number(bind.margins[index] ?? 0);
|
|
316
|
+
if (typeof bind.margin === "number") return bind.margin;
|
|
317
|
+
return typeof inequality.margin === "number" ? inequality.margin : 0;
|
|
318
|
+
};
|
|
319
|
+
/**
|
|
320
|
+
* This function evaluates each inequality on one binding. "<" holds when lhs + margin < rhs, and
|
|
321
|
+
* ">" holds when lhs > rhs + margin. Only the inequalities with all their symbols bound apply.
|
|
322
|
+
* The others are null.
|
|
323
|
+
*/
|
|
324
|
+
const evaluate = (rule, bind) => inequalities(rule).map((inequality, index) => {
|
|
325
|
+
const lhs = sideOf(inequality.lhs, bind);
|
|
326
|
+
const rhs = sideOf(inequality.rhs, bind);
|
|
327
|
+
const margin = marginOf(inequality, index, bind);
|
|
328
|
+
if (lhs === null || rhs === null) return {
|
|
329
|
+
lhs,
|
|
330
|
+
rhs,
|
|
331
|
+
margin,
|
|
332
|
+
holds: null
|
|
333
|
+
};
|
|
334
|
+
return {
|
|
335
|
+
lhs,
|
|
336
|
+
rhs,
|
|
337
|
+
margin,
|
|
338
|
+
holds: inequality.op === "<" ? lhs + margin < rhs : inequality.op === "<=" ? lhs + margin <= rhs : inequality.op === ">" ? lhs > rhs + margin : lhs >= rhs + margin
|
|
339
|
+
};
|
|
340
|
+
});
|
|
341
|
+
/**
|
|
342
|
+
* This function gives the verdict of an evaluation. When an applicable inequality fails, the
|
|
343
|
+
* verdict is "violated". When all hold only because a timer is absent, the verdict is "unbounded",
|
|
344
|
+
* because nothing then limits the window. Otherwise the verdict is "holds". When no inequality
|
|
345
|
+
* applies, the arithmetic cannot decide, and the verdict is "depends".
|
|
346
|
+
*/
|
|
347
|
+
const verdictOf = (rule, bind, evaluated) => {
|
|
348
|
+
const applicable = evaluated.flatMap((entry, index) => entry.holds === null ? [] : [index]);
|
|
349
|
+
if (applicable.length === 0) return "depends";
|
|
350
|
+
if (applicable.some((index) => evaluated[index]?.holds === false)) return "violated";
|
|
351
|
+
const all = inequalities(rule);
|
|
352
|
+
return applicable.some((index) => {
|
|
353
|
+
const inequality = all[index];
|
|
354
|
+
return [...inequality.lhs.terms, ...inequality.rhs.terms].some((term) => {
|
|
355
|
+
const bound = boundOf$1(bind, keyOf(term));
|
|
356
|
+
return bound !== void 0 && value(bound) === Infinity;
|
|
357
|
+
});
|
|
358
|
+
}) ? "unbounded" : "holds";
|
|
359
|
+
};
|
|
360
|
+
//#endregion
|
|
361
|
+
//#region src/lint.ts
|
|
362
|
+
/** These are the identifiers of the rules that each declaration gets. */
|
|
363
|
+
const SERVICE_RULES = [
|
|
364
|
+
"O09",
|
|
365
|
+
"O10",
|
|
366
|
+
"O11",
|
|
367
|
+
"O12",
|
|
368
|
+
"O13",
|
|
369
|
+
"O14",
|
|
370
|
+
"O15",
|
|
371
|
+
"O16",
|
|
372
|
+
"O17",
|
|
373
|
+
"O18",
|
|
374
|
+
"O19",
|
|
375
|
+
"O21",
|
|
376
|
+
"O22",
|
|
377
|
+
"O23",
|
|
378
|
+
"O27"
|
|
379
|
+
];
|
|
380
|
+
/** This function gives a finite amount in seconds. */
|
|
381
|
+
const secondsOf = (amount) => rationals.toNumber(convertExact(rationals.fromNumber(amount.value), amount.unit, "s"));
|
|
382
|
+
/** This function gives an amount, or no bound, as the law binds it. */
|
|
383
|
+
const boundOf = (amount) => amount.value === "infinite" ? "inf" : secondsOf(amount);
|
|
384
|
+
/** This function gives the largest of some numbers of seconds, or undefined when there are none. */
|
|
385
|
+
const most = (values) => values.length === 0 ? void 0 : Math.max(...values);
|
|
386
|
+
/** This function gives what the declaration tells about the service, as symbols of the law, and the source of each. */
|
|
387
|
+
const serviceBindings = (svc, scenario) => {
|
|
388
|
+
const bind = {};
|
|
389
|
+
const from = {};
|
|
390
|
+
const set = (symbol, bound, said) => {
|
|
391
|
+
if (bound === void 0) return;
|
|
392
|
+
bind[symbol] = bound;
|
|
393
|
+
from[symbol] = said;
|
|
394
|
+
};
|
|
395
|
+
const gating = svc.checks.filter((check) => check.gates !== null);
|
|
396
|
+
const readinessChecks = gating.filter((check) => check.gates === "readiness");
|
|
397
|
+
const livenessChecks = gating.filter((check) => check.gates === "liveness");
|
|
398
|
+
set("check_poll_interval@service", most(gating.map((check) => secondsOf(check.poll.interval))), "the slowest gating check's interval");
|
|
399
|
+
set("check_timeout@service", most(gating.map((check) => secondsOf(check.poll.timeout))), "the longest gating check's timeout");
|
|
400
|
+
set("slowest_poll@service", most(gating.map((check) => secondsOf(check.poll.timeout) + secondsOf(check.poll.interval) * (1 + check.poll.jitter))), "a check's timeout plus one jittered interval, at its slowest");
|
|
401
|
+
const windowOf = (checks, policy) => most(checks.map((check) => secondsOf(failSpan(svc.policies[policy], check.poll, "timeout").max)));
|
|
402
|
+
const readiness = windowOf(readinessChecks, "readiness");
|
|
403
|
+
const liveness = windowOf(livenessChecks, "liveness");
|
|
404
|
+
set("readiness_detection_window@service", readiness, "the readiness latch, failing by timeout");
|
|
405
|
+
if (readiness !== void 0) set("readiness_detection_window@pod", readiness + secondsOf(scenario.probes.readiness), "the readiness latch, then the kubelet's probe");
|
|
406
|
+
if (liveness !== void 0) set("liveness_detection_window@pod", liveness + secondsOf(scenario.probes.liveness), "the liveness window, then the kubelet's probe");
|
|
407
|
+
set("startup_budget@pod", secondsOf(svc.policies.startup), "the declared startup budget");
|
|
408
|
+
set("process_drain_time@process", svc.shutdown.phases.reduce((total, phase) => total + secondsOf(phase.cap), 0), `the shutdown phases' caps, summed (${svc.shutdown.phases.map((phase) => phase.name).join(", ")})`);
|
|
409
|
+
set("cooperative_window@process", boundOf(svc.shutdown.cooperativeWindow), "the declared cooperative window");
|
|
410
|
+
for (const [symbol, amount] of Object.entries(scenario.platform)) set(symbol, boundOf(amount), "the scenario");
|
|
411
|
+
return {
|
|
412
|
+
bind,
|
|
413
|
+
from
|
|
414
|
+
};
|
|
415
|
+
};
|
|
416
|
+
/** This function gives the client knobs of a dependency at @client, and the values of the scenario at its own hops. */
|
|
417
|
+
const dependencyBindings = (dependency, scenario) => {
|
|
418
|
+
const bind = {};
|
|
419
|
+
for (const [knob, amount] of Object.entries(dependency.knobs)) bind[`${knob}@client`] = boundOf(amount);
|
|
420
|
+
for (const [symbol, amount] of Object.entries(scenario.dependencies?.[dependency.name] ?? {})) bind[symbol.includes("@") ? symbol : `${symbol}@dependency`] = boundOf(amount);
|
|
421
|
+
return bind;
|
|
422
|
+
};
|
|
423
|
+
const SCOPED = /@(client|dependency|server)$/;
|
|
424
|
+
/** This function applies each service rule to a declaration and a scenario: one time for the service, and one time for each client. */
|
|
425
|
+
const lint = (svc, scenario, rules) => {
|
|
426
|
+
const service = serviceBindings(svc, scenario).bind;
|
|
427
|
+
const findings = [];
|
|
428
|
+
for (const rule of rules) {
|
|
429
|
+
if (!SERVICE_RULES.includes(rule.id)) continue;
|
|
430
|
+
const symbols = symbolsOf(rule);
|
|
431
|
+
const scopes = symbols.some((symbol) => SCOPED.test(symbol)) ? svc.dependencies.map((dependency) => [dependency.name, {
|
|
432
|
+
...service,
|
|
433
|
+
...dependencyBindings(dependency, scenario)
|
|
434
|
+
}]) : [[svc.name, service]];
|
|
435
|
+
for (const [scope, bind] of scopes) {
|
|
436
|
+
const evaluated = evaluate(rule, bind);
|
|
437
|
+
findings.push({
|
|
438
|
+
rule: rule.id,
|
|
439
|
+
scope,
|
|
440
|
+
verdict: verdictOf(rule, bind, evaluated),
|
|
441
|
+
statement: rule.rule,
|
|
442
|
+
...rule.window !== void 0 ? { window: rule.window } : {},
|
|
443
|
+
evaluated,
|
|
444
|
+
unbound: symbols.filter((symbol) => bind[symbol] === void 0)
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return findings;
|
|
449
|
+
};
|
|
450
|
+
//#endregion
|
|
451
|
+
export { FORMAT, SERVICE_RULES, asDebounce, asWindow, evaluate, failSpan, inequalities, lint, mayReissue, recoverSpan, service, serviceBindings, symbolsOf, verdictOf };
|
|
452
|
+
|
|
453
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["r","boundOf","r"],"sources":["../src/timing.ts","../src/description.ts","../src/operation.ts","../src/builder.ts","../src/law.ts","../src/lint.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. An `\"error\"` step refuses at once. A\n * `\"timeout\"` step (a black hole) answers only when the timeout of the poll ends. Thus it is\n * slower by that timeout, at the two ends of the span.\n */\nexport type FailsBy = \"error\" | \"timeout\";\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.\n */\nexport const failSpan = (timing: Timing, poll: Poll, by: FailsBy = \"error\"): Span => {\n const jitter = fraction(poll.jitter);\n const late = by === \"timeout\" ? 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","import type { Extent, Quantity } from \"@mark1russell7/quantity\";\nimport type { Operation } from \"./operation.ts\";\nimport type { Poll, Timing } from \"./timing.ts\";\n\n/**\n * This is the format of a service as data: what `describe()` exports, and nothing starts. The\n * timer algebra and the ordering law read it. Each timer has a unit. Each knob comes from the\n * declaration, because the runtime has no defaults. A bound that nobody gives is \"no bound\", not\n * zero.\n */\nexport const FORMAT = \"mark1russell7.service/1\";\n\n/** A step is a step of a connection that a check examines. A check that skips a step cannot see a failure in it. */\nexport type Step =\n \"dial\" | \"local-state\" | \"round-trip\" | \"checkout\" | \"progress\" | \"topology\" | \"latency\";\n\n/** A gate is the policy that the failures of a check count toward. */\nexport type Gate = \"readiness\" | \"liveness\";\n\n/** A check is an active check: one signal, for one step of a connection. */\nexport interface Check {\n readonly name: string;\n /** This name is the dependency that it examines, or null when it reads the state of the process. */\n readonly dependency: string | null;\n readonly step: Step;\n /** This flag tells if it uses the connection of the application. A check on its own connection cannot see a failure of that connection. */\n readonly ownConnection: boolean;\n /** This gate is the policy that it controls, or null for a check that only reports. It has no default. */\n readonly gates: Gate | null;\n readonly poll: Poll;\n}\n\n/**\n * A dependency is something that the service uses, with its client. Its knobs have the names that\n * the ordering law uses: `request_timeout`, `connect_bound`, `driver_writeoff_time`,\n * `heartbeat_interval`, `client_retry_window`. Thus the law reads them without a translation.\n */\nexport interface Dependency {\n readonly name: string;\n /** This identifier is the component in the corpus (`systems.component.postgresql`), or null when the corpus does not have it. */\n readonly system: string | null;\n readonly knobs: Readonly<Record<string, Extent>>;\n /**\n * These are the operations that the service does with it, by name (`GET /v1/forecast`, `INCR`).\n * Each operation declares if it can go again. A dependency usually has the two kinds, thus a\n * dependency has no idempotency of its own. An operation without a declaration does not go again.\n */\n readonly operations: Readonly<Record<string, Operation>>;\n}\n\n/**\n * A transport is something that takes work in: a server, a consumer or a worker. Each is a\n * component with a state machine. The service is ready only while each transport takes work.\n */\nexport interface Transport {\n readonly name: string;\n readonly kind: \"server\" | \"consumer\" | \"worker\";\n /** These are its knobs, with the names of the law where the law has them: the `server_idle_close` of a server. */\n readonly knobs: Readonly<Record<string, Extent>>;\n}\n\n/**\n * A controller is a group of routes. It is ready only while the dependencies that it needs are\n * ready. Each route declares if its callers can send it again. When the shutdown stops a request,\n * an idempotent route answers 503, which a caller can retry. Other routes answer 500.\n */\nexport interface Controller {\n readonly name: string;\n readonly requires: readonly string[];\n readonly routes: Readonly<Record<string, Operation>>;\n}\n\n/** A coupling is a declared edge between two policies. The algebra checks its margin. */\nexport interface Coupling {\n readonly from: Gate;\n readonly to: Gate;\n readonly effect: \"fails\";\n}\n\n/** A phase is one step of the shutdown sequence, with a cap. Its owner adds it. */\nexport interface Phase {\n readonly name: string;\n readonly cap: Quantity;\n}\n\n/** A service description is the whole graph of a service, as data. */\nexport interface ServiceDescription {\n readonly format: typeof FORMAT;\n readonly name: string;\n /** This flag tells if a part joined after the start. That part starts tripped, and the export is partial. */\n readonly partial: boolean;\n readonly transports: readonly Transport[];\n readonly dependencies: readonly Dependency[];\n readonly controllers: readonly Controller[];\n readonly checks: readonly Check[];\n readonly policies: {\n readonly readiness: Timing;\n readonly liveness: Timing;\n /** This duration is how long the service can take to start. The startup probe must agree with it. */\n readonly startup: Quantity;\n };\n readonly couplings: readonly Coupling[];\n readonly shutdown: {\n readonly phases: readonly Phase[];\n /** This window is how long before the end of the drain the cooperative abort occurs, or no bound. */\n readonly cooperativeWindow: Extent;\n };\n}\n","import type { Extent, Quantity } from \"@mark1russell7/quantity\";\nimport { compare, convertible } from \"@mark1russell7/quantity\";\n\n/**\n * This type tells if an operation can go again after a failure. It is a property of the\n * operation, not of the dependency. One Redis client sends a GET, which is safe to send again. It\n * also sends an INCR, which counts two times when it goes again after a timeout. Each operation\n * declares its idempotency:\n * - `true`: a second issue has the same effect. Examples are an HTTP GET or PUT, a SELECT, a\n * Redis GET or SET, and a write that the server removes when it is a copy.\n * - `false`: the client does not send it again after it can have reached the server.\n * - keyed: the server keeps a key for a window, as with an `Idempotency-Key` header, and it\n * ignores a copy in that window. A copy after the window is a second operation. Thus the\n * window is a timer, and it must be longer than all retries of the operation.\n */\nexport type Idempotency = boolean | { readonly key: string; readonly window: Extent };\n\n/** An operation is an endpoint that the service calls or serves, a command, or a query. */\nexport interface Operation {\n readonly idempotent: Idempotency;\n}\n\n/**\n * This function tells if an operation can go again `since` its first issue. An idempotent\n * operation can always go again, and other operations cannot. A keyed operation can go again\n * while the server keeps its key.\n */\nexport const mayReissue = (operation: Operation, since: Quantity): boolean => {\n const { idempotent } = operation;\n if (typeof idempotent === \"boolean\") return idempotent;\n return compare(since, idempotent.window) < 0;\n};\n\n/** This function gives the problems in the declaration of an operation that `describe()` refuses. */\nexport const operationProblems = (owner: string, name: string, operation: Operation): string[] => {\n const { idempotent } = operation;\n if (typeof idempotent === \"boolean\") return [];\n const problems: string[] = [];\n if (idempotent.key.trim().length === 0) problems.push(`${owner}'s ${name} names no key`);\n if (!convertible(idempotent.window.unit, \"s\"))\n problems.push(`${owner}'s ${name} holds its key for a span that is not a time`);\n return problems;\n};\n","import type { Extent, Quantity } from \"@mark1russell7/quantity\";\nimport { convertible } from \"@mark1russell7/quantity\";\nimport type {\n Check,\n Controller,\n Coupling,\n Dependency,\n Phase,\n ServiceDescription,\n Transport,\n} from \"./description.ts\";\nimport { FORMAT } from \"./description.ts\";\nimport type { Operation } from \"./operation.ts\";\nimport { operationProblems } from \"./operation.ts\";\nimport type { Timing } from \"./timing.ts\";\n\n/**\n * A service is composed with plain method uses before it starts. The builder is imperative, and\n * it holds static knowledge. The service does not use an `init` function that gives registrations.\n *\n * const svc = service(\"app\");\n * svc.dependency(\"weather\", {\n * system: null,\n * knobs: { request_timeout },\n * operations: { \"GET /v1/forecast\": { idempotent: true } },\n * });\n * svc.controller(\"forecast\").requires(\"weather\").route(\"GET /api/forecast\", { idempotent: true });\n * svc.check({ name: \"weather.round-trip\", dependency: \"weather\", … });\n * svc.describe(); // the whole graph as data, with nothing started\n * await svc.start();\n *\n * The graph has one source, thus its parts cannot disagree. A dependency or a check can join\n * after the start. It starts tripped, and the export is partial. The runtime has no defaults of\n * its own. Thus `describe()` refuses a service that does not declare a policy, its startup budget\n * or its shutdown, and it tells which.\n */\nexport interface ControllerBuilder {\n /** This method declares the dependencies that its routes need. The controller is ready only while they are ready. */\n requires(...dependencies: string[]): ControllerBuilder;\n /** This method declares a route that the controller serves, and if its callers can send it again. */\n route(route: string, operation: Operation): ControllerBuilder;\n}\n\n/** A service builder composes a service, gives it as data, and starts it. */\nexport interface ServiceBuilder {\n readonly name: string;\n dependency(name: string, spec: Omit<Dependency, \"name\">): ServiceBuilder;\n server(name: string, knobs: Readonly<Record<string, Extent>>): ServiceBuilder;\n consumer(name: string, knobs: Readonly<Record<string, Extent>>): ServiceBuilder;\n worker(name: string, knobs: Readonly<Record<string, Extent>>): ServiceBuilder;\n controller(name: string): ControllerBuilder;\n check(check: Check): ServiceBuilder;\n readiness(timing: Timing): ServiceBuilder;\n liveness(timing: Timing): ServiceBuilder;\n startup(budget: Quantity): ServiceBuilder;\n couple(coupling: Coupling): ServiceBuilder;\n phase(name: string, cap: Quantity): ServiceBuilder;\n cooperativeWindow(window: Extent): ServiceBuilder;\n /** This method gives the service as data, and it starts nothing. */\n describe(): ServiceDescription;\n /** This method closes the composition. A part that joins after it starts tripped, and it makes the export partial. */\n start(): Promise<void>;\n}\n\nconst isTime = (amount: Extent): boolean => convertible(amount.unit, \"s\");\n\n/** This function starts the composition of a service. */\nexport const service = (name: string): ServiceBuilder => {\n if (name.trim().length === 0) throw new Error(\"a service needs a name\");\n const transports: Transport[] = [];\n const dependencies: Dependency[] = [];\n const controllers: Array<{\n name: string;\n requires: string[];\n routes: Record<string, Operation>;\n }> = [];\n const checks: Check[] = [];\n const couplings: Coupling[] = [];\n const phases: Phase[] = [];\n let readiness: Timing | undefined;\n let liveness: Timing | undefined;\n let startup: Quantity | undefined;\n let cooperative: Extent | undefined;\n let started = false;\n let partial = false;\n\n /** This function stops a change to the shape of the service after the start. */\n const composing = (what: string): void => {\n if (started) throw new Error(`${name} has started: ${what} is declared before svc.start()`);\n };\n const once = (list: ReadonlyArray<{ name: string }>, entry: string, what: string): void => {\n if (list.some((held) => held.name === entry))\n throw new Error(`${name} already has a ${what} named ${entry}`);\n };\n const transport =\n (kind: Transport[\"kind\"]) => (entry: string, knobs: Readonly<Record<string, Extent>>) => {\n composing(`a ${kind}`);\n once(transports, entry, \"transport\");\n transports.push({ name: entry, kind, knobs: { ...knobs } });\n return builder;\n };\n\n const builder: ServiceBuilder = {\n name,\n dependency(entry, spec) {\n once(dependencies, entry, \"dependency\");\n dependencies.push({\n name: entry,\n ...spec,\n knobs: { ...spec.knobs },\n operations: { ...spec.operations },\n });\n if (started) partial = true;\n return builder;\n },\n server: transport(\"server\"),\n consumer: transport(\"consumer\"),\n worker: transport(\"worker\"),\n controller(entry) {\n composing(\"a controller\");\n once(controllers, entry, \"controller\");\n const held = {\n name: entry,\n requires: [] as string[],\n routes: {} as Record<string, Operation>,\n };\n controllers.push(held);\n const handle: ControllerBuilder = {\n requires(...needed) {\n composing(`what ${entry} requires`);\n held.requires.push(...needed);\n return handle;\n },\n route(served, operation) {\n composing(`${entry}'s routes`);\n if (held.routes[served] !== undefined)\n throw new Error(`${entry} already serves ${served}`);\n held.routes[served] = { ...operation };\n return handle;\n },\n };\n return handle;\n },\n check(check) {\n once(checks, check.name, \"check\");\n checks.push({ ...check });\n if (started) partial = true;\n return builder;\n },\n readiness(timing) {\n composing(\"readiness\");\n readiness = timing;\n return builder;\n },\n liveness(timing) {\n composing(\"liveness\");\n liveness = timing;\n return builder;\n },\n startup(budget) {\n composing(\"the startup budget\");\n startup = budget;\n return builder;\n },\n couple(coupling) {\n composing(\"a coupling\");\n couplings.push({ ...coupling });\n return builder;\n },\n phase(entry, cap) {\n composing(\"a shutdown phase\");\n once(phases, entry, \"shutdown phase\");\n phases.push({ name: entry, cap });\n return builder;\n },\n cooperativeWindow(window) {\n composing(\"the cooperative window\");\n cooperative = window;\n return builder;\n },\n describe() {\n const missing: string[] = [];\n if (readiness === undefined) missing.push(\"readiness (svc.readiness)\");\n if (liveness === undefined) missing.push(\"liveness (svc.liveness)\");\n if (startup === undefined) missing.push(\"a startup budget (svc.startup)\");\n if (phases.length === 0) missing.push(\"its shutdown phases (svc.phase)\");\n if (cooperative === undefined)\n missing.push(\"a cooperative window, or no bound (svc.cooperativeWindow)\");\n if (missing.length > 0)\n throw new Error(\n `${name} does not declare ${missing.join(\"; \")}: the runtime has no defaults`,\n );\n const known = new Set(dependencies.map((entry) => entry.name));\n const problems: string[] = [];\n for (const check of checks) {\n if (check.dependency !== null && !known.has(check.dependency)) {\n problems.push(\n `check ${check.name} exercises ${check.dependency}, which is not a dependency`,\n );\n }\n if (!isTime(check.poll.interval) || !isTime(check.poll.timeout)) {\n problems.push(`check ${check.name} polls in a unit that is not a time`);\n }\n }\n for (const controller of controllers) {\n for (const needed of controller.requires) {\n if (!known.has(needed))\n problems.push(\n `controller ${controller.name} requires ${needed}, which is not a dependency`,\n );\n }\n for (const [route, operation] of Object.entries(controller.routes))\n problems.push(...operationProblems(`controller ${controller.name}`, route, operation));\n }\n for (const dependency of dependencies)\n for (const [name, operation] of Object.entries(dependency.operations))\n problems.push(...operationProblems(`dependency ${dependency.name}`, name, operation));\n for (const phase of phases)\n if (!isTime(phase.cap)) problems.push(`phase ${phase.name}'s cap is not a time`);\n if (problems.length > 0) throw new Error(`${name}: ${problems.join(\"; \")}`);\n const controllersOut: Controller[] = controllers.map((entry) => ({\n name: entry.name,\n requires: [...entry.requires],\n routes: { ...entry.routes },\n }));\n return {\n format: FORMAT,\n name,\n partial,\n transports: transports.map((entry) => ({ ...entry, knobs: { ...entry.knobs } })),\n dependencies: dependencies.map((entry) => ({\n ...entry,\n knobs: { ...entry.knobs },\n operations: { ...entry.operations },\n })),\n controllers: controllersOut,\n checks: checks.map((entry) => ({ ...entry, poll: { ...entry.poll } })),\n policies: { readiness: readiness!, liveness: liveness!, startup: startup! },\n couplings: couplings.map((entry) => ({ ...entry })),\n shutdown: {\n phases: phases.map((entry) => ({ ...entry })),\n cooperativeWindow: cooperative!,\n },\n };\n },\n start() {\n // the start needs the whole graph: describe() is the check\n builder.describe();\n started = true;\n return Promise.resolve();\n },\n };\n return builder;\n};\n","/**\n * This module is the evaluator of the timer-ordering law. Its decisions are the same as the\n * decisions of the research evaluator. The rules are data, and the caller gives them. Each rule\n * is one or more linear inequalities over symbols at a hop (`readiness_detection_window@pod`),\n * with a margin. A binding maps `symbol@hop` to seconds, or to \"inf\" for a timer that is absent.\n */\n\n/** A term is a coefficient times one symbol, at a hop when it has one. */\nexport interface Term {\n readonly knob: string;\n readonly coef: number;\n readonly at?: string | undefined;\n}\n\n/** A side is a sum of terms, plus a constant. */\nexport interface Side {\n readonly terms: readonly Term[];\n readonly const?: number | undefined;\n}\n\n/** An inequality compares two sides, with a margin. */\nexport interface Inequality {\n readonly lhs: Side;\n readonly op: \"<\" | \"<=\" | \">\" | \">=\";\n readonly rhs: Side;\n /** The margin is in seconds, or it is words for a margin that no binding gives (\"a factor of several\"). Words count as 0. */\n readonly margin?: number | string | undefined;\n}\n\n/** A rule is one or more inequalities, with their statement in words. */\nexport interface Rule {\n readonly id: string;\n /** This text is the rule in words. */\n readonly rule: string;\n /** This text is the window that a violation opens. */\n readonly window?: string | undefined;\n readonly expr: Inequality;\n readonly expr_more?: readonly Inequality[] | undefined;\n}\n\n/** A bound is in seconds, or it is \"inf\" for an absent timer, or \"off\" for a disabled feature. The last two have no limit. */\nexport type Bound = number | \"inf\" | \"off\";\n\n/**\n * A binding maps `symbol@hop` to its bound. Its \"margin\" replaces the margin of each inequality.\n * Its \"margins\" replace them one by one, in sequence, as the binding of a chain can.\n */\nexport type Binding = Readonly<Record<string, Bound | readonly number[] | undefined>> & {\n readonly margin?: number | undefined;\n readonly margins?: readonly number[] | undefined;\n};\n\n/** A verdict is \"holds\", \"violated\", \"unbounded\" (it holds only because a timer is absent) or \"depends\" (nothing is bound). */\nexport type Verdict = \"holds\" | \"violated\" | \"unbounded\" | \"depends\";\n\n/** An evaluation is the value of each side of one inequality, its margin, and if it holds. */\nexport interface Evaluated {\n readonly lhs: number | null;\n readonly rhs: number | null;\n readonly margin: number;\n readonly holds: boolean | null;\n}\n\nconst value = (bound: Bound): number =>\n bound === \"inf\" || bound === \"off\" ? Infinity : Number(bound);\nconst keyOf = (term: Term): string =>\n term.at !== undefined ? `${term.knob}@${term.at}` : term.knob;\n/** This function gives the bound of a symbol, or undefined when the symbol has no bound. */\nconst boundOf = (bind: Binding, key: string): Bound | undefined => {\n const bound = bind[key];\n return typeof bound === \"number\" || bound === \"inf\" || bound === \"off\" ? bound : undefined;\n};\n\n/** This function gives the inequalities of a rule: its expr, then the others. */\nexport const inequalities = (rule: Rule): readonly Inequality[] => [\n rule.expr,\n ...(rule.expr_more ?? []),\n];\n\n/** This function gives the symbols that a rule reads, as `symbol@hop`. */\nexport const symbolsOf = (rule: Rule): string[] => {\n const out: string[] = [];\n for (const inequality of inequalities(rule)) {\n for (const term of [...inequality.lhs.terms, ...inequality.rhs.terms]) {\n const key = keyOf(term);\n if (!out.includes(key)) out.push(key);\n }\n }\n return out;\n};\n\n/** This function gives the value of a side, Σ coef × symbol + const, or null when a symbol has no bound. */\nconst sideOf = (side: Side, bind: Binding): number | null => {\n let total = side.const ?? 0;\n for (const term of side.terms) {\n const bound = boundOf(bind, keyOf(term));\n if (bound === undefined) return null;\n total += term.coef * value(bound);\n }\n return total;\n};\n\n/** This function gives the margin of an inequality: from the binding (one for all, or one each), else from the rule, else 0. */\nconst marginOf = (inequality: Inequality, index: number, bind: Binding): number => {\n if (Array.isArray(bind.margins)) return Number(bind.margins[index] ?? 0);\n if (typeof bind.margin === \"number\") return bind.margin;\n return typeof inequality.margin === \"number\" ? inequality.margin : 0;\n};\n\n/**\n * This function evaluates each inequality on one binding. \"<\" holds when lhs + margin < rhs, and\n * \">\" holds when lhs > rhs + margin. Only the inequalities with all their symbols bound apply.\n * The others are null.\n */\nexport const evaluate = (rule: Rule, bind: Binding): Evaluated[] =>\n inequalities(rule).map((inequality, index) => {\n const lhs = sideOf(inequality.lhs, bind);\n const rhs = sideOf(inequality.rhs, bind);\n const margin = marginOf(inequality, index, bind);\n if (lhs === null || rhs === null) return { lhs, rhs, margin, holds: null };\n const holds =\n inequality.op === \"<\"\n ? lhs + margin < rhs\n : inequality.op === \"<=\"\n ? lhs + margin <= rhs\n : inequality.op === \">\"\n ? lhs > rhs + margin\n : lhs >= rhs + margin;\n return { lhs, rhs, margin, holds };\n });\n\n/**\n * This function gives the verdict of an evaluation. When an applicable inequality fails, the\n * verdict is \"violated\". When all hold only because a timer is absent, the verdict is \"unbounded\",\n * because nothing then limits the window. Otherwise the verdict is \"holds\". When no inequality\n * applies, the arithmetic cannot decide, and the verdict is \"depends\".\n */\nexport const verdictOf = (rule: Rule, bind: Binding, evaluated: readonly Evaluated[]): Verdict => {\n const applicable = evaluated.flatMap((entry, index) => (entry.holds === null ? [] : [index]));\n if (applicable.length === 0) return \"depends\";\n if (applicable.some((index) => evaluated[index]?.holds === false)) return \"violated\";\n const all = inequalities(rule);\n const infinite = applicable.some((index) => {\n const inequality = all[index]!;\n return [...inequality.lhs.terms, ...inequality.rhs.terms].some((term) => {\n const bound = boundOf(bind, keyOf(term));\n return bound !== undefined && value(bound) === Infinity;\n });\n });\n return infinite ? \"unbounded\" : \"holds\";\n};\n","import type { Extent, Quantity } from \"@mark1russell7/quantity\";\nimport { convertExact, rationals as r } from \"@mark1russell7/quantity\";\nimport type { Dependency, ServiceDescription } from \"./description.ts\";\nimport type { Binding, Bound, Evaluated, Rule, Verdict } from \"./law.ts\";\nimport { evaluate, symbolsOf, verdictOf } from \"./law.ts\";\nimport { failSpan } from \"./timing.ts\";\n\n/**\n * This module applies the ordering law to a declaration. The declaration binds the symbols of the\n * service, from its primitives: the polls of its checks, the windows of its policies, its drain.\n * The scenario binds the symbols of the platform (preStop, grace, the probes of the kubelet,\n * propagation) and of each dependency (its failover time, the idle close of its server). Thus the\n * service gives the inequalities, and the scenario gives the values. A rule about a client\n * applies one time for each dependency.\n */\n\n/** A scenario gives the values of the platform and of each dependency, by the `symbol@hop` of the law. */\nexport interface Scenario {\n /** These are the values of the platform: prestop_sleep@pod, termination_grace@pod, probe_period@pod, propagation_bound@cluster. */\n readonly platform: Readonly<Record<string, Extent>>;\n /**\n * These are the probe windows of the kubelet, period × failure threshold. They add to the\n * windows of the declaration. A probe reads the cached verdict, thus detection is the latch,\n * then the probe.\n */\n readonly probes: { readonly readiness: Quantity; readonly liveness: Quantity };\n /** These are the values of each dependency, by its name: failover_time@dependency, server_idle_close@server. */\n readonly dependencies?: Readonly<Record<string, Readonly<Record<string, Extent>>>> | undefined;\n}\n\n/** A finding is the verdict of one rule for one scope: the service, or the client of a dependency. */\nexport interface Finding {\n readonly rule: string;\n readonly scope: string;\n readonly verdict: Verdict;\n readonly statement: string;\n readonly window?: string | undefined;\n readonly evaluated: readonly Evaluated[];\n /** These are the symbols that have no bound. A \"depends\" verdict waits for them. */\n readonly unbound: readonly string[];\n}\n\n/** These are the identifiers of the rules that each declaration gets. */\nexport const SERVICE_RULES: readonly string[] = [\n \"O09\",\n \"O10\",\n \"O11\",\n \"O12\",\n \"O13\",\n \"O14\",\n \"O15\",\n \"O16\",\n \"O17\",\n \"O18\",\n \"O19\",\n \"O21\",\n \"O22\",\n \"O23\",\n \"O27\",\n];\n\n/** This function gives a finite amount in seconds. */\nconst secondsOf = (amount: Quantity): number =>\n r.toNumber(convertExact(r.fromNumber(amount.value), amount.unit, \"s\"));\n/** This function gives an amount, or no bound, as the law binds it. */\nconst boundOf = (amount: Extent): Bound =>\n amount.value === \"infinite\" ? \"inf\" : secondsOf(amount);\n/** This function gives the largest of some numbers of seconds, or undefined when there are none. */\nconst most = (values: readonly number[]): number | undefined =>\n values.length === 0 ? undefined : Math.max(...values);\n\n/** This function gives what the declaration tells about the service, as symbols of the law, and the source of each. */\nexport const serviceBindings = (\n svc: ServiceDescription,\n scenario: Scenario,\n): { readonly bind: Record<string, Bound>; readonly from: Record<string, string> } => {\n const bind: Record<string, Bound> = {};\n const from: Record<string, string> = {};\n const set = (symbol: string, bound: Bound | undefined, said: string): void => {\n if (bound === undefined) return;\n bind[symbol] = bound;\n from[symbol] = said;\n };\n const gating = svc.checks.filter((check) => check.gates !== null);\n const readinessChecks = gating.filter((check) => check.gates === \"readiness\");\n const livenessChecks = gating.filter((check) => check.gates === \"liveness\");\n set(\n \"check_poll_interval@service\",\n most(gating.map((check) => secondsOf(check.poll.interval))),\n \"the slowest gating check's interval\",\n );\n set(\n \"check_timeout@service\",\n most(gating.map((check) => secondsOf(check.poll.timeout))),\n \"the longest gating check's timeout\",\n );\n set(\n \"slowest_poll@service\",\n most(\n gating.map(\n (check) =>\n secondsOf(check.poll.timeout) + secondsOf(check.poll.interval) * (1 + check.poll.jitter),\n ),\n ),\n \"a check's timeout plus one jittered interval, at its slowest\",\n );\n // the window of a policy is the time that its latch takes to fail when its slowest check times out\n const windowOf = (checks: typeof gating, policy: \"readiness\" | \"liveness\"): number | undefined =>\n most(\n checks.map((check) => secondsOf(failSpan(svc.policies[policy], check.poll, \"timeout\").max)),\n );\n const readiness = windowOf(readinessChecks, \"readiness\");\n const liveness = windowOf(livenessChecks, \"liveness\");\n set(\"readiness_detection_window@service\", readiness, \"the readiness latch, failing by timeout\");\n if (readiness !== undefined) {\n set(\n \"readiness_detection_window@pod\",\n readiness + secondsOf(scenario.probes.readiness),\n \"the readiness latch, then the kubelet's probe\",\n );\n }\n if (liveness !== undefined) {\n set(\n \"liveness_detection_window@pod\",\n liveness + secondsOf(scenario.probes.liveness),\n \"the liveness window, then the kubelet's probe\",\n );\n }\n set(\"startup_budget@pod\", secondsOf(svc.policies.startup), \"the declared startup budget\");\n set(\n \"process_drain_time@process\",\n svc.shutdown.phases.reduce((total, phase) => total + secondsOf(phase.cap), 0),\n `the shutdown phases' caps, summed (${svc.shutdown.phases.map((phase) => phase.name).join(\", \")})`,\n );\n set(\n \"cooperative_window@process\",\n boundOf(svc.shutdown.cooperativeWindow),\n \"the declared cooperative window\",\n );\n for (const [symbol, amount] of Object.entries(scenario.platform))\n set(symbol, boundOf(amount), \"the scenario\");\n return { bind, from };\n};\n\n/** This function gives the client knobs of a dependency at @client, and the values of the scenario at its own hops. */\nconst dependencyBindings = (dependency: Dependency, scenario: Scenario): Record<string, Bound> => {\n const bind: Record<string, Bound> = {};\n for (const [knob, amount] of Object.entries(dependency.knobs))\n bind[`${knob}@client`] = boundOf(amount);\n for (const [symbol, amount] of Object.entries(scenario.dependencies?.[dependency.name] ?? {})) {\n bind[symbol.includes(\"@\") ? symbol : `${symbol}@dependency`] = boundOf(amount);\n }\n return bind;\n};\n\nconst SCOPED = /@(client|dependency|server)$/;\n\n/** This function applies each service rule to a declaration and a scenario: one time for the service, and one time for each client. */\nexport const lint = (\n svc: ServiceDescription,\n scenario: Scenario,\n rules: readonly Rule[],\n): Finding[] => {\n const service = serviceBindings(svc, scenario).bind;\n const findings: Finding[] = [];\n for (const rule of rules) {\n if (!SERVICE_RULES.includes(rule.id)) continue;\n const symbols = symbolsOf(rule);\n const scopes: Array<[string, Binding]> = symbols.some((symbol) => SCOPED.test(symbol))\n ? svc.dependencies.map((dependency) => [\n dependency.name,\n { ...service, ...dependencyBindings(dependency, scenario) },\n ])\n : [[svc.name, service]];\n for (const [scope, bind] of scopes) {\n const evaluated = evaluate(rule, bind);\n findings.push({\n rule: rule.id,\n scope,\n verdict: verdictOf(rule, bind, evaluated),\n statement: rule.rule,\n ...(rule.window !== undefined ? { window: rule.window } : {}),\n evaluated,\n unbound: symbols.filter((symbol) => bind[symbol] === undefined),\n });\n }\n }\n return findings;\n};\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,69 @@
|
|
|
1
1
|
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/package",
|
|
2
3
|
"name": "@mark1russell7/service",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"service",
|
|
8
|
+
"readiness",
|
|
9
|
+
"liveness",
|
|
10
|
+
"health-check",
|
|
11
|
+
"graceful-shutdown",
|
|
12
|
+
"idempotency",
|
|
13
|
+
"timeouts",
|
|
14
|
+
"kubernetes"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://mark1russell7.github.io/systems/",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/mark1russell7/systems.git",
|
|
20
|
+
"directory": "packages/service"
|
|
21
|
+
},
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/mark1russell7/systems/issues"
|
|
24
|
+
},
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"author": "Mark Russell (https://github.com/mark1russell7)",
|
|
27
|
+
"type": "module",
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"main": "./build/index.js",
|
|
30
|
+
"types": "./build/index.d.ts",
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./build/index.d.ts",
|
|
34
|
+
"default": "./build/index.js"
|
|
35
|
+
},
|
|
36
|
+
"./package.json": "./package.json"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"build",
|
|
40
|
+
"README.md",
|
|
41
|
+
"LICENSE"
|
|
42
|
+
],
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=22.19"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@mark1russell7/quantity": "^0.1.0"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"zod": "^4.1.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@mark1russell7/cue": "github:mark1russell7/cue",
|
|
54
|
+
"@types/node": "^26.6.4",
|
|
55
|
+
"tsdown": "^0.23.1",
|
|
56
|
+
"typescript": "^7.0.2",
|
|
57
|
+
"vitest": "^5.0.3",
|
|
58
|
+
"zod": "^4.6.6"
|
|
59
|
+
},
|
|
60
|
+
"publishConfig": {
|
|
61
|
+
"access": "public"
|
|
62
|
+
},
|
|
63
|
+
"scripts": {
|
|
64
|
+
"build": "tsdown",
|
|
65
|
+
"test": "vitest run",
|
|
66
|
+
"test:watch": "vitest",
|
|
67
|
+
"typecheck": "tsc -p tsconfig.json --noEmit --emitDeclarationOnly false && tsc -p tsconfig.test.json"
|
|
68
|
+
}
|
|
6
69
|
}
|