@mark1russell7/service 0.5.2 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,176 @@
1
+ import { Quantity } from "@mark1russell7/quantity";
2
+ //#region src/limiter/policy.d.ts
3
+ /**
4
+ * A token bucket gives each key a bucket of `burst` tokens. The bucket refills at `rate`, and each
5
+ * take uses one token. Thus a key can make `burst` takes at once after an idle time, and then
6
+ * `rate` takes in each unit of time.
7
+ */
8
+ interface TokenBucket {
9
+ readonly type: "token-bucket";
10
+ /** This rate is how fast the bucket refills: a count for each unit of time, for example `{ value: 10, unit: "count/s" }`. */
11
+ readonly rate: Quantity;
12
+ /** This count is the size of the bucket: the most takes in a sequence after an idle time. */
13
+ readonly burst: Quantity;
14
+ }
15
+ /**
16
+ * A sliding window gives each key at most `limit` takes in each `window`. Each take counts for one
17
+ * window from its time, thus the window moves with the clock and has no fixed edges.
18
+ */
19
+ interface SlidingWindow {
20
+ readonly type: "sliding-window";
21
+ /** This count is the most takes of one key in one window. */
22
+ readonly limit: Quantity;
23
+ /** This time is the length of the window. */
24
+ readonly window: Quantity;
25
+ }
26
+ /** A limit policy is the rule that a keyed limiter applies to each key, as data. It has no defaults. */
27
+ type LimitPolicy = TokenBucket | SlidingWindow;
28
+ /**
29
+ * A store failure policy tells what a take does when a shared store fails. It has no default.
30
+ * - `open`: the limiter lets the take in. The count is not known, thus `remaining` is 0.
31
+ * - `closed`: the limiter refuses the take with the cause `store`. Its `retryAfter` is the longest
32
+ * `retryAfter` of the policy (`longestRetryAfter`). Thus O29 and O30 apply to it too.
33
+ */
34
+ type StoreFailure = "open" | "closed";
35
+ /**
36
+ * A store spec tells where the counts of the keys are:
37
+ * - `local`: in the memory of each replica. A limit L over N replicas is then L / N on each
38
+ * replica. That sum holds only on average, when the balancer spreads the takes of each key evenly.
39
+ * A local store does not fail, thus it has no `onStoreFailure`, and the limiter ignores one.
40
+ * - `shared`: in a store that the replicas share, the dependency of that name. The store decides
41
+ * each take in one atomic step, on its own clock. Thus the limit holds across the replicas.
42
+ * `onStoreFailure` tells what a take does when the store fails.
43
+ */
44
+ type LimiterStoreSpec = {
45
+ readonly kind: "local";
46
+ } | {
47
+ readonly kind: "shared";
48
+ readonly dependency: string;
49
+ readonly onStoreFailure: StoreFailure;
50
+ };
51
+ /**
52
+ * The eviction of a limiter bounds its keys. A store removes a key only when its state is equal to
53
+ * the state of a new key. That is a bucket that is full again, or a window with no take in it.
54
+ * Thus an eviction changes no decision.
55
+ */
56
+ interface LimiterEviction {
57
+ /**
58
+ * This count is the most keys that the store holds. At the cap, the store refuses a take of a new
59
+ * key until a held key is idle for its span (`idleSpan`). A held key keeps its count.
60
+ */
61
+ readonly maxKeys: Quantity;
62
+ }
63
+ /** A limiter spec is the declaration of one keyed limiter. Each part is necessary, because the limiter has no defaults. */
64
+ interface LimiterSpec {
65
+ /** This text tells what the key of each take is, for example `tenant` or `principal`. The caller gives each key. */
66
+ readonly key: string;
67
+ readonly policy: LimitPolicy;
68
+ readonly store: LimiterStoreSpec;
69
+ readonly evict: LimiterEviction;
70
+ }
71
+ /** A limiter is a keyed limiter in the description of a service: its name and its spec. */
72
+ interface Limiter extends LimiterSpec {
73
+ readonly name: string;
74
+ }
75
+ /**
76
+ * A refusal cause tells why a take is refused:
77
+ * - `limit`: the key used its limit.
78
+ * - `cap`: the store holds its cap of keys, and the key is new.
79
+ * - `store`: the shared store failed, and its spec declares `onStoreFailure: "closed"`.
80
+ */
81
+ type RefusalCause = "limit" | "cap" | "store";
82
+ /** A permitted take used one unit of its key. `remaining` is the count of takes that the key can make at this time. */
83
+ interface Allowed {
84
+ readonly allowed: true;
85
+ readonly remaining: number;
86
+ /** This text is the failure of a shared store, when `onStoreFailure: "open"` lets the take in. */
87
+ readonly failure?: string | undefined;
88
+ }
89
+ /**
90
+ * A refused take used nothing. `retryAfter` is the time until the limiter lets the same take in, when
91
+ * no other take comes first. It is always above zero.
92
+ */
93
+ interface Refused {
94
+ readonly allowed: false;
95
+ readonly retryAfter: Quantity;
96
+ readonly cause: RefusalCause;
97
+ /** This text is the failure of a shared store, for the cause `store`. */
98
+ readonly failure?: string | undefined;
99
+ }
100
+ /** A decision is the answer of a limiter to one take. */
101
+ type Decision = Allowed | Refused;
102
+ /**
103
+ * A limit rule is a policy in numbers, which a store applies: each time in milliseconds, and each
104
+ * count as a number. A store in another process, for example a script of Redis, gets these numbers.
105
+ */
106
+ type LimitRule = {
107
+ readonly type: "token-bucket";
108
+ /** This time is the refill of one token, in milliseconds. */
109
+ readonly interval: number;
110
+ readonly burst: number;
111
+ } | {
112
+ readonly type: "sliding-window";
113
+ readonly limit: number;
114
+ /** This time is the length of the window, in milliseconds. */
115
+ readonly window: number;
116
+ };
117
+ /**
118
+ * A key state is what a store keeps for one key:
119
+ * - A token bucket keeps the time at which its bucket is full again (`full`, in milliseconds).
120
+ * - A sliding window keeps the time of each permitted take in the window, the oldest first.
121
+ */
122
+ type KeyState = {
123
+ readonly type: "token-bucket";
124
+ readonly full: number;
125
+ } | {
126
+ readonly type: "sliding-window";
127
+ readonly stamps: readonly number[];
128
+ };
129
+ /** A step is the decision of one take, and the state of its key after the take. A refusal keeps the count of the key. */
130
+ interface TakeStep {
131
+ readonly decision: Decision;
132
+ readonly state: KeyState;
133
+ }
134
+ /**
135
+ * This function gives the problems of a limiter spec. `describe()` and `keyedLimiter` refuse a spec
136
+ * with a problem. A part that is absent is a problem, because the limiter has no defaults.
137
+ */
138
+ declare const limiterProblems: (name: string, spec: Partial<LimiterSpec> | undefined) => string[];
139
+ /**
140
+ * This function gives the rule of a policy in numbers. The interval of a bucket is exact as a
141
+ * rational, and it becomes one number only at the end. Thus 10 for each minute gives 6000 ms.
142
+ */
143
+ declare const limitRule: (policy: LimitPolicy) => LimitRule;
144
+ /**
145
+ * This function gives the span after which an idle key is equal to a new key. That span is
146
+ * `burst / rate` for a token bucket, and one window for a sliding window. A store can remove the key
147
+ * after it, and no decision changes. It is also the longest `retryAfter` of a new key that the store
148
+ * refuses at its cap.
149
+ */
150
+ declare const idleSpan: (policy: LimitPolicy) => Quantity;
151
+ /**
152
+ * This function gives the longest `retryAfter` of a refusal by the policy: one interval of the
153
+ * bucket (`1 / rate`), or one window. A refusal at the cap of keys can be longer, up to `idleSpan`.
154
+ */
155
+ declare const longestRetryAfter: (policy: LimitPolicy) => Quantity;
156
+ /**
157
+ * This function gives a `retryAfter` in whole seconds, rounded up, as the header `Retry-After`
158
+ * (RFC 9110) has it. A time over a whole second by less than one nanosecond rounds down to it,
159
+ * because that part is an error of floating point.
160
+ */
161
+ declare const retryAfterSeconds: (retryAfter: Quantity) => number;
162
+ /** This function gives the time at which a key state is equal to the state of a new key, in milliseconds. */
163
+ declare const freeAt: (rule: LimitRule, state: KeyState) => number;
164
+ /**
165
+ * This function decides one take of a key at the time `now`, in milliseconds, from the state of the
166
+ * key (`undefined` for a new key). It is the one decision of each store in this process. A store in
167
+ * another process does the same arithmetic in its own atomic step.
168
+ * - A token bucket (GCRA): the store lets the take in when, with the take, the bucket is full
169
+ * again in at most `burst` intervals. A refusal waits until one token is back.
170
+ * - A sliding window: the store lets the take in when fewer than `limit` takes are in the window.
171
+ * A refusal waits until the oldest take leaves the window.
172
+ */
173
+ declare const decide: (rule: LimitRule, state: KeyState | undefined, now: number) => TakeStep;
174
+ //#endregion
175
+ export { retryAfterSeconds as S, freeAt as _, LimitRule as a, limiterProblems as b, LimiterSpec as c, Refused as d, SlidingWindow as f, decide as g, TokenBucket as h, LimitPolicy as i, LimiterStoreSpec as l, TakeStep as m, Decision as n, Limiter as o, StoreFailure as p, KeyState as r, LimiterEviction as s, Allowed as t, RefusalCause as u, idleSpan as v, longestRetryAfter as x, limitRule as y };
176
+ //# sourceMappingURL=policy-G4QJ1ZZq.d.ts.map
package/build/redis.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { t as Backoff } from "./backoff-B-jGWVDm.js";
2
2
  import { n as Clock } from "./clock-D-38uH_z.js";
3
- import { B as Gate, Ct as Operation, E as CheckFn, G as Step, J as Poll, L as Dependency, M as Recorder, N as Check, W as ServiceDescription, et as heartbeatDetection, t as DependencyHandle } from "./lifecycle-DjEpgpRk.js";
4
- import { n as ServiceBuilder } from "./builder-BzjhWk2E.js";
3
+ import { B as Gate, Ct as Operation, E as CheckFn, G as Step, J as Poll, L as Dependency, M as Recorder, N as Check, W as ServiceDescription, et as heartbeatDetection, t as DependencyHandle } from "./lifecycle-Chu9Pri2.js";
4
+ import { n as ServiceBuilder } from "./builder-DyDB7zM9.js";
5
5
  import { Extent, Quantity } from "@mark1russell7/quantity";
6
6
  import { Machine } from "@mark1russell7/algebra";
7
7
  //#region src/redis/connection.d.ts
@@ -1,6 +1,6 @@
1
1
  import { n as Clock } from "./clock-D-38uH_z.js";
2
- import { D as Outcome, E as CheckFn, ct as ExitTrigger, d as TransportImpl, pt as ProcessPort, r as Lifecycle, st as ExitSignal } from "./lifecycle-DjEpgpRk.js";
3
- import { n as ServiceBuilder } from "./builder-BzjhWk2E.js";
2
+ import { D as Outcome, E as CheckFn, ct as ExitTrigger, d as TransportImpl, pt as ProcessPort, r as Lifecycle, st as ExitSignal } from "./lifecycle-Chu9Pri2.js";
3
+ import { n as ServiceBuilder } from "./builder-DyDB7zM9.js";
4
4
  //#region src/fakes.d.ts
5
5
  /** A fake clock moves only when a test tells it to. */
6
6
  interface FakeClock extends Clock {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package",
3
3
  "name": "@mark1russell7/service",
4
- "version": "0.5.2",
4
+ "version": "0.5.3",
5
5
  "description": "A service declared as data before it starts, and its runtime: checks on jittered loops, a latch for each policy, probes from the cache, the startup in its sequence, and a staged, capped drain. The timer-ordering law checks the declaration. Its clock, heartbeat and backoff also work alone, in a browser too.",
6
6
  "keywords": [
7
7
  "service",
@@ -11,7 +11,8 @@
11
11
  "graceful-shutdown",
12
12
  "idempotency",
13
13
  "timeouts",
14
- "kubernetes"
14
+ "kubernetes",
15
+ "rate-limit"
15
16
  ],
16
17
  "homepage": "https://mark1russell7.github.io/systems/",
17
18
  "repository": {
@@ -57,6 +58,10 @@
57
58
  "types": "./build/http.d.ts",
58
59
  "default": "./build/http.js"
59
60
  },
61
+ "./limiter": {
62
+ "types": "./build/limiter.d.ts",
63
+ "default": "./build/limiter.js"
64
+ },
60
65
  "./package.json": "./package.json"
61
66
  },
62
67
  "files": [
@@ -68,7 +73,7 @@
68
73
  "node": ">=22.19"
69
74
  },
70
75
  "dependencies": {
71
- "@mark1russell7/algebra": "^0.7.1",
76
+ "@mark1russell7/algebra": "^0.7.2",
72
77
  "@mark1russell7/quantity": "^0.2.0"
73
78
  },
74
79
  "peerDependencies": {
@@ -85,7 +90,7 @@
85
90
  },
86
91
  "devDependencies": {
87
92
  "@mark1russell7/cue": "github:mark1russell7/cue",
88
- "@mark1russell7/harness": "^0.11.4",
93
+ "@mark1russell7/harness": "^0.11.18",
89
94
  "@types/node": "^26.6.4",
90
95
  "ioredis": "6.0.0",
91
96
  "testcontainers": "12.2.0",