@mark1russell7/service 0.5.6 → 0.5.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -3
- package/build/http.d.ts +19 -3
- package/build/http.js +32 -8
- package/build/http.js.map +1 -1
- package/build/index.d.ts +2 -2
- package/build/index.js +2 -2
- package/build/limiter-DCDdzVwe.d.ts +45 -0
- package/build/limiter-pkBZUp-t.js +402 -0
- package/build/limiter-pkBZUp-t.js.map +1 -0
- package/build/limiter.d.ts +2 -1
- package/build/limiter.js +2 -2
- package/package.json +2 -2
- package/build/limiter-wv0uXDk9.js +0 -126
- package/build/limiter-wv0uXDk9.js.map +0 -1
package/build/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { n as operationProblems, t as mayReissue } from "./operation-DL5RxFds.js
|
|
|
5
5
|
import { a as limiterProblems, i as limitRule, n as freeAt, o as longestRetryAfter, r as idleSpan, s as retryAfterSeconds, t as decide } from "./policy-CgS4y94U.js";
|
|
6
6
|
import { a as lateHolds, l as takeOutside, n as countOf, r as millisecondsOf, s as ownStream, t as boundedMillisecondsOf } from "./knobs-D2ClI68I.js";
|
|
7
7
|
import { backoffBand, backoffDelay, backoffProblems } from "./backoff.js";
|
|
8
|
-
import { n as memoryStore, t as
|
|
8
|
+
import { a as keyedLimiter, i as limiterStateOf, n as TOKEN_BUCKET_MACHINE, o as memoryStore, r as limiterMachine, t as SLIDING_WINDOW_MACHINE } from "./limiter-pkBZUp-t.js";
|
|
9
9
|
import { convertExact, convertible, quantity, rationals, valueIn } from "@mark1russell7/quantity";
|
|
10
10
|
import { replay } from "@mark1russell7/algebra/replay";
|
|
11
11
|
//#region src/description.ts
|
|
@@ -3389,6 +3389,6 @@ const MACHINES = {
|
|
|
3389
3389
|
watchdog: WATCHDOG_MACHINE
|
|
3390
3390
|
};
|
|
3391
3391
|
//#endregion
|
|
3392
|
-
export { DECLARATION_RULE, EXIT_MACHINE, EXIT_SIGNALS, FORMAT, GATE_MACHINE, LATCH_MACHINE, LIMITER_RULES, MACHINES, POLLER_MACHINE, PROCESS_TRIGGERS, SERVICE_RULES, STAGES, WATCHDOG_MACHINE, WINDOW_MACHINE, asDebounce, asWindow, backoffBand, backoffDelay, backoffProblems, capKnob, cooperativeAbortAt, daemon, decide, detectionTime, drainMachine, embeddedProcess, ephemeral, evaluate, failSpan, freeAt, heartbeat, heartbeatDetection, idleSpan, inequalities, isExitCode, isExitSignal, isMessageShape, keyedLimiter, kubernetes, limitRule, limiterProblems, lint, localDev, longestRetryAfter, matchesMessage, mayReissue, memoryStore, nodeProcess, platformClock, processPortOf, queueDrain, realClock, recoverSpan, redeliverOnce, replay, retryAfterSeconds, service, serviceBindings, stageRank, symbolsOf, verdictOf, watchdog, workerLoop };
|
|
3392
|
+
export { DECLARATION_RULE, EXIT_MACHINE, EXIT_SIGNALS, FORMAT, GATE_MACHINE, LATCH_MACHINE, LIMITER_RULES, MACHINES, POLLER_MACHINE, PROCESS_TRIGGERS, SERVICE_RULES, SLIDING_WINDOW_MACHINE, STAGES, TOKEN_BUCKET_MACHINE, WATCHDOG_MACHINE, WINDOW_MACHINE, asDebounce, asWindow, backoffBand, backoffDelay, backoffProblems, capKnob, cooperativeAbortAt, daemon, decide, detectionTime, drainMachine, embeddedProcess, ephemeral, evaluate, failSpan, freeAt, heartbeat, heartbeatDetection, idleSpan, inequalities, isExitCode, isExitSignal, isMessageShape, keyedLimiter, kubernetes, limitRule, limiterMachine, limiterProblems, limiterStateOf, lint, localDev, longestRetryAfter, matchesMessage, mayReissue, memoryStore, nodeProcess, platformClock, processPortOf, queueDrain, realClock, recoverSpan, redeliverOnce, replay, retryAfterSeconds, service, serviceBindings, stageRank, symbolsOf, verdictOf, watchdog, workerLoop };
|
|
3393
3393
|
|
|
3394
3394
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { a as LimitRule, i as LimitPolicy, r as KeyState } from "./policy-G4QJ1ZZq.js";
|
|
2
|
+
import "./store-DzTyosou.js";
|
|
3
|
+
import { Machine } from "@mark1russell7/algebra";
|
|
4
|
+
//#region src/limiter/machine.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* A limiter state is the state of one key of a keyed limiter, as its machine document names it:
|
|
7
|
+
* - `new`: the store holds no state for the key, or a state that is equal to the state of a new key.
|
|
8
|
+
* - `open`: the key holds a count, and its next take is permitted.
|
|
9
|
+
* - `limited`: the next take of the key is refused with the cause `limit`, until a unit is back.
|
|
10
|
+
*/
|
|
11
|
+
type LimiterState = "new" | "open" | "limited";
|
|
12
|
+
/**
|
|
13
|
+
* A token bucket of one key (GCRA), as a `mark1russell7.machine/1` document. The bucket holds
|
|
14
|
+
* `burst` tokens, and it gets one token back in each interval, `1 / rate`. The store keeps one time
|
|
15
|
+
* for the key: when its bucket is full again.
|
|
16
|
+
* - A take is permitted when, with the take, the bucket is full again in at most `burst` intervals.
|
|
17
|
+
* - A refused take waits until one token is back: at most one interval. That interval is the
|
|
18
|
+
* longest `Retry-After` of the policy (`retry_after@limiter` of the law).
|
|
19
|
+
* - After `burst / rate` with no take, the bucket is full, and the key is equal to a new key. That
|
|
20
|
+
* span is the idle span, and the longest wait of a new key at the cap of keys
|
|
21
|
+
* (`retry_after_at_cap@limiter`).
|
|
22
|
+
*/
|
|
23
|
+
declare const TOKEN_BUCKET_MACHINE: Machine;
|
|
24
|
+
/**
|
|
25
|
+
* A sliding window of one key, as a `mark1russell7.machine/1` document. The store keeps the time of
|
|
26
|
+
* each permitted take of the key in the window, and each take counts for one window from its time.
|
|
27
|
+
* - A take is permitted when fewer than `limit` takes are in the window.
|
|
28
|
+
* - A refused take waits until the oldest take leaves the window: at most one window. That window is
|
|
29
|
+
* the longest `Retry-After` of the policy (`retry_after@limiter` of the law).
|
|
30
|
+
* - One window after the last take, no take is in the window, and the key is equal to a new key.
|
|
31
|
+
* That window is the idle span, and the longest wait of a new key at the cap of keys
|
|
32
|
+
* (`retry_after_at_cap@limiter`).
|
|
33
|
+
*/
|
|
34
|
+
declare const SLIDING_WINDOW_MACHINE: Machine;
|
|
35
|
+
/** This function gives the machine document of the policy of a limiter. */
|
|
36
|
+
declare const limiterMachine: (policy: LimitPolicy) => Machine;
|
|
37
|
+
/**
|
|
38
|
+
* This function gives the state of a key of a store as its machine document names it, at the time
|
|
39
|
+
* `now` in milliseconds. `state` is what the store keeps for the key, or `undefined` when it keeps
|
|
40
|
+
* nothing. A key equal to a new key is `new`, and a key whose next take `decide` refuses is `limited`.
|
|
41
|
+
*/
|
|
42
|
+
declare const limiterStateOf: (rule: LimitRule, state: KeyState | undefined, now: number) => LimiterState;
|
|
43
|
+
//#endregion
|
|
44
|
+
export { limiterStateOf as a, limiterMachine as i, SLIDING_WINDOW_MACHINE as n, TOKEN_BUCKET_MACHINE as r, LimiterState as t };
|
|
45
|
+
//# sourceMappingURL=limiter-DCDdzVwe.d.ts.map
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import { a as limiterProblems, i as limitRule, n as freeAt, o as longestRetryAfter, t as decide } from "./policy-CgS4y94U.js";
|
|
2
|
+
import { convertExact, rationals } from "@mark1russell7/quantity";
|
|
3
|
+
//#region src/limiter/store.ts
|
|
4
|
+
const ms = (value) => ({
|
|
5
|
+
value,
|
|
6
|
+
unit: "ms"
|
|
7
|
+
});
|
|
8
|
+
/**
|
|
9
|
+
* This function makes the store of one replica. It reads time only through the clock. Thus a step
|
|
10
|
+
* of the wall clock does not move a count, and a fake clock gives exact decisions in a test.
|
|
11
|
+
* - The eviction is exact. A key whose bucket is full again, or whose window has no take, is equal
|
|
12
|
+
* to a new key. The store removes it at its next take, or at a sweep.
|
|
13
|
+
* - The count of keys has a bound. At the cap, a take of a new key sweeps the table, but only when
|
|
14
|
+
* a held key can be free. Each sweep reads each key one time. When no key is free, the store
|
|
15
|
+
* refuses the new key with the cause `cap`. Its `retryAfter` is at most the time until the
|
|
16
|
+
* earliest held key is free, and that time is at most `idleSpan` of the policy.
|
|
17
|
+
*/
|
|
18
|
+
const memoryStore = (clock) => {
|
|
19
|
+
const tables = /* @__PURE__ */ new Map();
|
|
20
|
+
const sweep = (table, rule, now) => {
|
|
21
|
+
let next = Number.POSITIVE_INFINITY;
|
|
22
|
+
for (const [key, state] of table.states) {
|
|
23
|
+
const free = freeAt(rule, state);
|
|
24
|
+
if (free <= now) table.states.delete(key);
|
|
25
|
+
else next = Math.min(next, free);
|
|
26
|
+
}
|
|
27
|
+
table.next = next;
|
|
28
|
+
};
|
|
29
|
+
return {
|
|
30
|
+
kind: "local",
|
|
31
|
+
take({ limiter, key, rule, maxKeys }) {
|
|
32
|
+
const now = clock.now();
|
|
33
|
+
let table = tables.get(limiter);
|
|
34
|
+
if (table === void 0) {
|
|
35
|
+
table = {
|
|
36
|
+
states: /* @__PURE__ */ new Map(),
|
|
37
|
+
next: Number.POSITIVE_INFINITY
|
|
38
|
+
};
|
|
39
|
+
tables.set(limiter, table);
|
|
40
|
+
}
|
|
41
|
+
let state = table.states.get(key);
|
|
42
|
+
if (state !== void 0 && freeAt(rule, state) <= now) {
|
|
43
|
+
table.states.delete(key);
|
|
44
|
+
state = void 0;
|
|
45
|
+
}
|
|
46
|
+
if (state === void 0 && table.states.size >= maxKeys) {
|
|
47
|
+
if (table.next <= now) sweep(table, rule, now);
|
|
48
|
+
if (table.states.size >= maxKeys) return {
|
|
49
|
+
allowed: false,
|
|
50
|
+
retryAfter: ms(table.next - now),
|
|
51
|
+
cause: "cap"
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const step = decide(rule, state, now);
|
|
55
|
+
if (state === void 0) {
|
|
56
|
+
table.states.set(key, step.state);
|
|
57
|
+
table.next = Math.min(table.next, freeAt(rule, step.state));
|
|
58
|
+
} else table.states.set(key, step.state);
|
|
59
|
+
return step.decision;
|
|
60
|
+
},
|
|
61
|
+
size(limiter) {
|
|
62
|
+
return tables.get(limiter)?.states.size ?? 0;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
/** This function gives the text of a failure. */
|
|
67
|
+
const failureOf = (error) => error instanceof Error ? error.message : String(error);
|
|
68
|
+
/**
|
|
69
|
+
* This function makes the runtime of a limiter from its spec and its store. It refuses a spec with a
|
|
70
|
+
* problem, because the limiter has no defaults. It also refuses a store of the wrong kind: a spec
|
|
71
|
+
* with a shared store needs a store that the replicas share.
|
|
72
|
+
* - A failure of a shared store gives the decision of `onStoreFailure`. With `open`, the take is
|
|
73
|
+
* permitted with `remaining` 0. With `closed`, it is refused with the cause `store`.
|
|
74
|
+
* - The `retryAfter` of the cause `store` is the longest `retryAfter` of the policy.
|
|
75
|
+
* - A failure of a local store rejects the take, because a local store has no `onStoreFailure`.
|
|
76
|
+
*/
|
|
77
|
+
const keyedLimiter = (name, spec, store) => {
|
|
78
|
+
const problems = limiterProblems(name, spec);
|
|
79
|
+
if (problems.length > 0) throw new Error(problems.join("; "));
|
|
80
|
+
if (store.kind !== spec.store.kind) throw new Error(`limiter ${name} declares a ${spec.store.kind} store, but the store that it got is ${store.kind}`);
|
|
81
|
+
const rule = limitRule(spec.policy);
|
|
82
|
+
const maxKeys = rationals.toNumber(convertExact(rationals.fromNumber(spec.evict.maxKeys.value), spec.evict.maxKeys.unit, "count"));
|
|
83
|
+
const onStoreFailure = spec.store.kind === "shared" ? spec.store.onStoreFailure : void 0;
|
|
84
|
+
const closedAfter = longestRetryAfter(spec.policy);
|
|
85
|
+
const take = async (key) => {
|
|
86
|
+
const request = {
|
|
87
|
+
limiter: name,
|
|
88
|
+
key,
|
|
89
|
+
rule,
|
|
90
|
+
maxKeys
|
|
91
|
+
};
|
|
92
|
+
if (onStoreFailure === void 0) return store.take(request);
|
|
93
|
+
try {
|
|
94
|
+
return await store.take(request);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
const failure = failureOf(error);
|
|
97
|
+
if (onStoreFailure === "open") return {
|
|
98
|
+
allowed: true,
|
|
99
|
+
remaining: 0,
|
|
100
|
+
failure
|
|
101
|
+
};
|
|
102
|
+
return {
|
|
103
|
+
allowed: false,
|
|
104
|
+
retryAfter: { ...closedAfter },
|
|
105
|
+
cause: "store",
|
|
106
|
+
failure
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
return {
|
|
111
|
+
name,
|
|
112
|
+
spec: {
|
|
113
|
+
key: spec.key,
|
|
114
|
+
policy: { ...spec.policy },
|
|
115
|
+
store: { ...spec.store },
|
|
116
|
+
evict: { maxKeys: { ...spec.evict.maxKeys } }
|
|
117
|
+
},
|
|
118
|
+
rule,
|
|
119
|
+
maxKeys,
|
|
120
|
+
take
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
//#endregion
|
|
124
|
+
//#region src/limiter/machine.ts
|
|
125
|
+
const t = (id, from, on, to, extra = {}) => ({
|
|
126
|
+
id,
|
|
127
|
+
from,
|
|
128
|
+
on,
|
|
129
|
+
to,
|
|
130
|
+
...extra
|
|
131
|
+
});
|
|
132
|
+
const count = (key) => ({
|
|
133
|
+
key,
|
|
134
|
+
scope: "limiter",
|
|
135
|
+
unit: "count",
|
|
136
|
+
default: null
|
|
137
|
+
});
|
|
138
|
+
/** These are the states of a key that a store can hold. A store fails in each of them. */
|
|
139
|
+
const HELD = [
|
|
140
|
+
"new",
|
|
141
|
+
"open",
|
|
142
|
+
"limited"
|
|
143
|
+
];
|
|
144
|
+
/** These are the parts of each document of a limiter that do not change with its policy. */
|
|
145
|
+
const COMMON = {
|
|
146
|
+
format: "mark1russell7.machine/1",
|
|
147
|
+
layer: "self",
|
|
148
|
+
version: "0.1.0",
|
|
149
|
+
sources: [
|
|
150
|
+
{
|
|
151
|
+
ref: "packages/service/src/limiter (the runtime implements this document: decide, memoryStore, keyedLimiter)",
|
|
152
|
+
rung: "implemented"
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
ref: "RFC 6585 §4: 429 Too Many Requests, which can carry Retry-After",
|
|
156
|
+
rung: "normative"
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
ref: "RFC 9110 §10.2.3: Retry-After, as delay-seconds or an HTTP-date",
|
|
160
|
+
rung: "normative"
|
|
161
|
+
}
|
|
162
|
+
],
|
|
163
|
+
conditions: {
|
|
164
|
+
"store.local": "the counts are in the memory of one replica: a limit L over N replicas is L / N on each replica, and that sum holds only on average",
|
|
165
|
+
"store.shared": "the counts are in a store that the replicas share: one atomic step on the clock of the store decides each take"
|
|
166
|
+
},
|
|
167
|
+
states: {
|
|
168
|
+
new: {
|
|
169
|
+
initial: true,
|
|
170
|
+
note: "the store holds no state for the key, or a state equal to a new key. The store can remove the key"
|
|
171
|
+
},
|
|
172
|
+
open: { note: "the key holds a count, and its next take is permitted" },
|
|
173
|
+
limited: { note: "the next take is refused with the cause limit, until a unit is back" }
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
/** These are the events of each document of a limiter. */
|
|
177
|
+
const EVENTS = {
|
|
178
|
+
take: {
|
|
179
|
+
kind: "app",
|
|
180
|
+
note: "one call of take(key): one request of the key. A permitted take uses one unit"
|
|
181
|
+
},
|
|
182
|
+
"unit.back": {
|
|
183
|
+
kind: "timer",
|
|
184
|
+
note: "the next take of a limited key is permitted again"
|
|
185
|
+
},
|
|
186
|
+
idle: {
|
|
187
|
+
kind: "timer",
|
|
188
|
+
note: "the key is equal to a new key. The store can remove it, and no decision changes"
|
|
189
|
+
},
|
|
190
|
+
"store.fail": {
|
|
191
|
+
kind: "local",
|
|
192
|
+
note: "a shared store fails the take. The state of the key does not change"
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
/** These are the knobs of each document of a limiter that do not change with its policy. */
|
|
196
|
+
const KNOBS = {
|
|
197
|
+
max_keys: count("limiters.<name>.evict.maxKeys"),
|
|
198
|
+
on_store_failure: {
|
|
199
|
+
key: "limiters.<name>.store.onStoreFailure",
|
|
200
|
+
scope: "limiter",
|
|
201
|
+
unit: null,
|
|
202
|
+
default: null
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
/**
|
|
206
|
+
* These are the transitions of each document of a limiter that do not change with its policy. They
|
|
207
|
+
* are the refusal of a limited key, a new key at the cap of keys, and a failure of a shared store.
|
|
208
|
+
*/
|
|
209
|
+
const COMMON_TRANSITIONS = [
|
|
210
|
+
t("refuse", "limited", "take", "limited", {
|
|
211
|
+
emits: ["429 limit"],
|
|
212
|
+
note: "Retry-After is the time until the next take is permitted, in whole seconds, rounded up. The count of the key does not change"
|
|
213
|
+
}),
|
|
214
|
+
t("cap", "new", "take", "new", {
|
|
215
|
+
guard: "the store holds max_keys keys for the limiter, and no held key is equal to a new key",
|
|
216
|
+
emits: ["429 cap"],
|
|
217
|
+
note: "Retry-After is the time until the earliest held key is free: at most the idle span. A held key keeps its count"
|
|
218
|
+
}),
|
|
219
|
+
...HELD.map((state) => t(`store-failed-${state}`, state, "store.fail", state, {
|
|
220
|
+
guard: "the store is shared",
|
|
221
|
+
emits: ["allow, or 429 store"],
|
|
222
|
+
note: "on_store_failure open lets the take in, with remaining 0. closed refuses it with the cause store, and its Retry-After is the longest of the policy"
|
|
223
|
+
}))
|
|
224
|
+
];
|
|
225
|
+
/**
|
|
226
|
+
* A token bucket of one key (GCRA), as a `mark1russell7.machine/1` document. The bucket holds
|
|
227
|
+
* `burst` tokens, and it gets one token back in each interval, `1 / rate`. The store keeps one time
|
|
228
|
+
* for the key: when its bucket is full again.
|
|
229
|
+
* - A take is permitted when, with the take, the bucket is full again in at most `burst` intervals.
|
|
230
|
+
* - A refused take waits until one token is back: at most one interval. That interval is the
|
|
231
|
+
* longest `Retry-After` of the policy (`retry_after@limiter` of the law).
|
|
232
|
+
* - After `burst / rate` with no take, the bucket is full, and the key is equal to a new key. That
|
|
233
|
+
* span is the idle span, and the longest wait of a new key at the cap of keys
|
|
234
|
+
* (`retry_after_at_cap@limiter`).
|
|
235
|
+
*/
|
|
236
|
+
const TOKEN_BUCKET_MACHINE = {
|
|
237
|
+
...COMMON,
|
|
238
|
+
id: "service.machine.limiter.token-bucket",
|
|
239
|
+
title: "A token bucket of one key of the keyed limiter",
|
|
240
|
+
events: EVENTS,
|
|
241
|
+
knobs: {
|
|
242
|
+
rate: {
|
|
243
|
+
key: "limiters.<name>.policy.rate",
|
|
244
|
+
scope: "limiter",
|
|
245
|
+
unit: "count/s",
|
|
246
|
+
default: null
|
|
247
|
+
},
|
|
248
|
+
burst: count("limiters.<name>.policy.burst"),
|
|
249
|
+
...KNOBS
|
|
250
|
+
},
|
|
251
|
+
timers: {
|
|
252
|
+
token: {
|
|
253
|
+
duration: {
|
|
254
|
+
expr: "1 / rate",
|
|
255
|
+
unit: "s"
|
|
256
|
+
},
|
|
257
|
+
early: {
|
|
258
|
+
expr: "1 / rate",
|
|
259
|
+
unit: "s"
|
|
260
|
+
},
|
|
261
|
+
arms: ["enter:limited"],
|
|
262
|
+
cancels: ["idle"],
|
|
263
|
+
fires: "unit.back",
|
|
264
|
+
note: "one token comes back at most one interval after the take that used the last token. The Retry-After of each refusal is the time to this firing"
|
|
265
|
+
},
|
|
266
|
+
full: {
|
|
267
|
+
duration: {
|
|
268
|
+
expr: "burst / rate",
|
|
269
|
+
unit: "s"
|
|
270
|
+
},
|
|
271
|
+
early: {
|
|
272
|
+
expr: "(burst - 1) / rate",
|
|
273
|
+
unit: "s"
|
|
274
|
+
},
|
|
275
|
+
arms: ["take"],
|
|
276
|
+
fires: "idle",
|
|
277
|
+
note: "the bucket is full again from one to burst intervals after a permitted take. A refused take does not move it"
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
transitions: [
|
|
281
|
+
t("first", "new", "take", "open", {
|
|
282
|
+
guard: "burst > 1",
|
|
283
|
+
emits: ["allow"]
|
|
284
|
+
}),
|
|
285
|
+
t("first-last", "new", "take", "limited", {
|
|
286
|
+
guard: "burst = 1",
|
|
287
|
+
emits: ["allow"]
|
|
288
|
+
}),
|
|
289
|
+
t("take", "open", "take", "open", {
|
|
290
|
+
guard: "a token remains after the take",
|
|
291
|
+
emits: ["allow"]
|
|
292
|
+
}),
|
|
293
|
+
t("take-last", "open", "take", "limited", {
|
|
294
|
+
guard: "the take uses the last token",
|
|
295
|
+
emits: ["allow"]
|
|
296
|
+
}),
|
|
297
|
+
...COMMON_TRANSITIONS,
|
|
298
|
+
t("token-back", "limited", "unit.back", "open", {
|
|
299
|
+
timer: "token",
|
|
300
|
+
guard: "burst > 1"
|
|
301
|
+
}),
|
|
302
|
+
t("full", "open", "idle", "new", { timer: "full" }),
|
|
303
|
+
t("full-at-once", "limited", "idle", "new", {
|
|
304
|
+
timer: "full",
|
|
305
|
+
guard: "burst = 1: the one token is back when the bucket is full"
|
|
306
|
+
})
|
|
307
|
+
]
|
|
308
|
+
};
|
|
309
|
+
/**
|
|
310
|
+
* A sliding window of one key, as a `mark1russell7.machine/1` document. The store keeps the time of
|
|
311
|
+
* each permitted take of the key in the window, and each take counts for one window from its time.
|
|
312
|
+
* - A take is permitted when fewer than `limit` takes are in the window.
|
|
313
|
+
* - A refused take waits until the oldest take leaves the window: at most one window. That window is
|
|
314
|
+
* the longest `Retry-After` of the policy (`retry_after@limiter` of the law).
|
|
315
|
+
* - One window after the last take, no take is in the window, and the key is equal to a new key.
|
|
316
|
+
* That window is the idle span, and the longest wait of a new key at the cap of keys
|
|
317
|
+
* (`retry_after_at_cap@limiter`).
|
|
318
|
+
*/
|
|
319
|
+
const SLIDING_WINDOW_MACHINE = {
|
|
320
|
+
...COMMON,
|
|
321
|
+
id: "service.machine.limiter.sliding-window",
|
|
322
|
+
title: "A sliding window of one key of the keyed limiter",
|
|
323
|
+
events: EVENTS,
|
|
324
|
+
knobs: {
|
|
325
|
+
limit: count("limiters.<name>.policy.limit"),
|
|
326
|
+
window: {
|
|
327
|
+
key: "limiters.<name>.policy.window",
|
|
328
|
+
scope: "limiter",
|
|
329
|
+
unit: "s",
|
|
330
|
+
default: null
|
|
331
|
+
},
|
|
332
|
+
...KNOBS
|
|
333
|
+
},
|
|
334
|
+
timers: {
|
|
335
|
+
oldest: {
|
|
336
|
+
duration: {
|
|
337
|
+
knob: "window",
|
|
338
|
+
unit: "s"
|
|
339
|
+
},
|
|
340
|
+
early: {
|
|
341
|
+
knob: "window",
|
|
342
|
+
unit: "s"
|
|
343
|
+
},
|
|
344
|
+
arms: ["enter:limited"],
|
|
345
|
+
cancels: ["idle"],
|
|
346
|
+
fires: "unit.back",
|
|
347
|
+
note: "the oldest take leaves the window at most one window after the take that filled it. The Retry-After of each refusal is the time to this firing"
|
|
348
|
+
},
|
|
349
|
+
last: {
|
|
350
|
+
duration: {
|
|
351
|
+
knob: "window",
|
|
352
|
+
unit: "s"
|
|
353
|
+
},
|
|
354
|
+
arms: ["take"],
|
|
355
|
+
fires: "idle",
|
|
356
|
+
note: "the last permitted take leaves the window one window after its time. A refused take does not move it"
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
transitions: [
|
|
360
|
+
t("first", "new", "take", "open", {
|
|
361
|
+
guard: "limit > 1",
|
|
362
|
+
emits: ["allow"]
|
|
363
|
+
}),
|
|
364
|
+
t("first-last", "new", "take", "limited", {
|
|
365
|
+
guard: "limit = 1",
|
|
366
|
+
emits: ["allow"]
|
|
367
|
+
}),
|
|
368
|
+
t("take", "open", "take", "open", {
|
|
369
|
+
guard: "fewer than limit - 1 takes are in the window before the take",
|
|
370
|
+
emits: ["allow"]
|
|
371
|
+
}),
|
|
372
|
+
t("take-last", "open", "take", "limited", {
|
|
373
|
+
guard: "the take is the limit-th take in the window",
|
|
374
|
+
emits: ["allow"]
|
|
375
|
+
}),
|
|
376
|
+
...COMMON_TRANSITIONS,
|
|
377
|
+
t("oldest-leaves", "limited", "unit.back", "open", {
|
|
378
|
+
timer: "oldest",
|
|
379
|
+
guard: "a later take stays in the window"
|
|
380
|
+
}),
|
|
381
|
+
t("last-leaves", "open", "idle", "new", { timer: "last" }),
|
|
382
|
+
t("all-leave", "limited", "idle", "new", {
|
|
383
|
+
timer: "last",
|
|
384
|
+
guard: "each take of the window has one time: limit = 1, or each take came at once"
|
|
385
|
+
})
|
|
386
|
+
]
|
|
387
|
+
};
|
|
388
|
+
/** This function gives the machine document of the policy of a limiter. */
|
|
389
|
+
const limiterMachine = (policy) => policy.type === "token-bucket" ? TOKEN_BUCKET_MACHINE : SLIDING_WINDOW_MACHINE;
|
|
390
|
+
/**
|
|
391
|
+
* This function gives the state of a key of a store as its machine document names it, at the time
|
|
392
|
+
* `now` in milliseconds. `state` is what the store keeps for the key, or `undefined` when it keeps
|
|
393
|
+
* nothing. A key equal to a new key is `new`, and a key whose next take `decide` refuses is `limited`.
|
|
394
|
+
*/
|
|
395
|
+
const limiterStateOf = (rule, state, now) => {
|
|
396
|
+
if (state === void 0 || freeAt(rule, state) <= now) return "new";
|
|
397
|
+
return decide(rule, state, now).decision.allowed ? "open" : "limited";
|
|
398
|
+
};
|
|
399
|
+
//#endregion
|
|
400
|
+
export { keyedLimiter as a, limiterStateOf as i, TOKEN_BUCKET_MACHINE as n, memoryStore as o, limiterMachine as r, SLIDING_WINDOW_MACHINE as t };
|
|
401
|
+
|
|
402
|
+
//# sourceMappingURL=limiter-pkBZUp-t.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"limiter-pkBZUp-t.js","names":["r"],"sources":["../src/limiter/store.ts","../src/limiter/machine.ts"],"sourcesContent":["import type { Quantity } from \"@mark1russell7/quantity\";\nimport { convertExact, rationals as r } from \"@mark1russell7/quantity\";\nimport type { Clock } from \"../clock.ts\";\nimport type { Decision, KeyState, LimiterSpec, LimitRule } from \"./policy.ts\";\nimport { decide, freeAt, limitRule, limiterProblems, longestRetryAfter } from \"./policy.ts\";\n\n/** A take request is one take of one key, with the rule and the cap of its limiter in numbers. */\nexport interface TakeRequest {\n /** This name is the limiter. A store keeps the keys of each limiter apart. */\n readonly limiter: string;\n readonly key: string;\n readonly rule: LimitRule;\n /** This count is the most keys that the store holds for the limiter. */\n readonly maxKeys: number;\n}\n\n/**\n * A limiter store keeps the state of each key, and it decides each take in one atomic step, on its\n * own clock. Thus two takes of one key do not read the same state at any time. A store in this\n * process uses `decide`. A store that the replicas share does the same arithmetic in one step of its\n * own, for example one script of Redis that reads `TIME`.\n */\nexport interface LimiterStore {\n /** This kind tells where the counts are: in the memory of this replica, or in a store that the replicas share. */\n readonly kind: \"local\" | \"shared\";\n /**\n * This method decides one take. It removes a key only when its state is equal to the state of a\n * new key. It holds at most `maxKeys` keys for each limiter. A failure of a shared store rejects\n * the take, and the limiter then applies the `onStoreFailure` of its spec.\n */\n take(request: TakeRequest): Decision | Promise<Decision>;\n}\n\n/** A memory store is the store of one replica, on the injected monotonic clock. */\nexport interface MemoryStore extends LimiterStore {\n readonly kind: \"local\";\n /** This method gives the count of keys that the store holds for a limiter. */\n size(limiter: string): number;\n}\n\n/** A table is the keys of one limiter in a memory store. */\ninterface Table {\n readonly states: Map<string, KeyState>;\n /**\n * This time is at or before the earliest time at which a held key is equal to a new key. An\n * insert lowers it, and a sweep sets it again. A take of a held key only moves the time of that\n * key later, thus the bound stays true.\n */\n next: number;\n}\n\nconst ms = (value: number): Quantity => ({ value, unit: \"ms\" });\n\n/**\n * This function makes the store of one replica. It reads time only through the clock. Thus a step\n * of the wall clock does not move a count, and a fake clock gives exact decisions in a test.\n * - The eviction is exact. A key whose bucket is full again, or whose window has no take, is equal\n * to a new key. The store removes it at its next take, or at a sweep.\n * - The count of keys has a bound. At the cap, a take of a new key sweeps the table, but only when\n * a held key can be free. Each sweep reads each key one time. When no key is free, the store\n * refuses the new key with the cause `cap`. Its `retryAfter` is at most the time until the\n * earliest held key is free, and that time is at most `idleSpan` of the policy.\n */\nexport const memoryStore = (clock: Clock): MemoryStore => {\n const tables = new Map<string, Table>();\n const sweep = (table: Table, rule: LimitRule, now: number): void => {\n let next = Number.POSITIVE_INFINITY;\n for (const [key, state] of table.states) {\n const free = freeAt(rule, state);\n if (free <= now) table.states.delete(key);\n else next = Math.min(next, free);\n }\n table.next = next;\n };\n return {\n kind: \"local\",\n take({ limiter, key, rule, maxKeys }) {\n const now = clock.now();\n let table = tables.get(limiter);\n if (table === undefined) {\n table = { states: new Map(), next: Number.POSITIVE_INFINITY };\n tables.set(limiter, table);\n }\n let state = table.states.get(key);\n // a key that is equal to a new key goes: the decision is the same, and it frees its slot\n if (state !== undefined && freeAt(rule, state) <= now) {\n table.states.delete(key);\n state = undefined;\n }\n if (state === undefined && table.states.size >= maxKeys) {\n if (table.next <= now) sweep(table, rule, now);\n if (table.states.size >= maxKeys) return { allowed: false, retryAfter: ms(table.next - now), cause: \"cap\" };\n }\n const step = decide(rule, state, now);\n if (state === undefined) {\n // a new key is never refused by its policy: a full bucket, or an empty window\n table.states.set(key, step.state);\n table.next = Math.min(table.next, freeAt(rule, step.state));\n } else table.states.set(key, step.state);\n return step.decision;\n },\n size(limiter) {\n return tables.get(limiter)?.states.size ?? 0;\n },\n };\n};\n\n/** A keyed limiter is the runtime of one declared limiter: its spec, its rule in numbers, and its store. */\nexport interface KeyedLimiter {\n readonly name: string;\n /** This spec is the declaration, the same data that `svc.limiter(name, spec)` takes and `describe()` shows. */\n readonly spec: LimiterSpec;\n readonly rule: LimitRule;\n /** This count is the most keys that the store holds for the limiter. */\n readonly maxKeys: number;\n /**\n * This method takes one unit for a key, and gives the decision of the store. When a shared store\n * fails, it gives the decision of `onStoreFailure`, with the failure as text.\n */\n take(key: string): Promise<Decision>;\n}\n\n/** This function gives the text of a failure. */\nconst failureOf = (error: unknown): string => (error instanceof Error ? error.message : String(error));\n\n/**\n * This function makes the runtime of a limiter from its spec and its store. It refuses a spec with a\n * problem, because the limiter has no defaults. It also refuses a store of the wrong kind: a spec\n * with a shared store needs a store that the replicas share.\n * - A failure of a shared store gives the decision of `onStoreFailure`. With `open`, the take is\n * permitted with `remaining` 0. With `closed`, it is refused with the cause `store`.\n * - The `retryAfter` of the cause `store` is the longest `retryAfter` of the policy.\n * - A failure of a local store rejects the take, because a local store has no `onStoreFailure`.\n */\nexport const keyedLimiter = (name: string, spec: LimiterSpec, store: LimiterStore): KeyedLimiter => {\n const problems = limiterProblems(name, spec);\n if (problems.length > 0) throw new Error(problems.join(\"; \"));\n if (store.kind !== spec.store.kind)\n throw new Error(`limiter ${name} declares a ${spec.store.kind} store, but the store that it got is ${store.kind}`);\n const rule = limitRule(spec.policy);\n const maxKeys = r.toNumber(convertExact(r.fromNumber(spec.evict.maxKeys.value), spec.evict.maxKeys.unit, \"count\"));\n const onStoreFailure = spec.store.kind === \"shared\" ? spec.store.onStoreFailure : undefined;\n const closedAfter = longestRetryAfter(spec.policy);\n const take = async (key: string): Promise<Decision> => {\n const request: TakeRequest = { limiter: name, key, rule, maxKeys };\n if (onStoreFailure === undefined) return store.take(request);\n try {\n return await store.take(request);\n } catch (error) {\n const failure = failureOf(error);\n if (onStoreFailure === \"open\") return { allowed: true, remaining: 0, failure };\n return { allowed: false, retryAfter: { ...closedAfter }, cause: \"store\", failure };\n }\n };\n return {\n name,\n spec: {\n key: spec.key,\n policy: { ...spec.policy },\n store: { ...spec.store },\n evict: { maxKeys: { ...spec.evict.maxKeys } },\n },\n rule,\n maxKeys,\n take,\n };\n};\n","import type { Machine, TransitionDef } from \"@mark1russell7/algebra\";\nimport type { KeyState, LimitPolicy, LimitRule } from \"./policy.ts\";\nimport { decide, freeAt } from \"./policy.ts\";\n\nconst t = (id: string, from: string | readonly string[], on: string | readonly string[], to: string, extra: Partial<TransitionDef> = {}): TransitionDef => ({\n id,\n from,\n on,\n to,\n ...extra,\n});\nconst count = (key: string) => ({ key, scope: \"limiter\", unit: \"count\", default: null }) as const;\n\n/**\n * A limiter state is the state of one key of a keyed limiter, as its machine document names it:\n * - `new`: the store holds no state for the key, or a state that is equal to the state of a new key.\n * - `open`: the key holds a count, and its next take is permitted.\n * - `limited`: the next take of the key is refused with the cause `limit`, until a unit is back.\n */\nexport type LimiterState = \"new\" | \"open\" | \"limited\";\n\n/** These are the states of a key that a store can hold. A store fails in each of them. */\nconst HELD = [\"new\", \"open\", \"limited\"] as const;\n\n/** These are the parts of each document of a limiter that do not change with its policy. */\nconst COMMON = {\n format: \"mark1russell7.machine/1\",\n layer: \"self\",\n version: \"0.1.0\",\n sources: [\n { ref: \"packages/service/src/limiter (the runtime implements this document: decide, memoryStore, keyedLimiter)\", rung: \"implemented\" },\n { ref: \"RFC 6585 §4: 429 Too Many Requests, which can carry Retry-After\", rung: \"normative\" },\n { ref: \"RFC 9110 §10.2.3: Retry-After, as delay-seconds or an HTTP-date\", rung: \"normative\" },\n ],\n conditions: {\n \"store.local\": \"the counts are in the memory of one replica: a limit L over N replicas is L / N on each replica, and that sum holds only on average\",\n \"store.shared\": \"the counts are in a store that the replicas share: one atomic step on the clock of the store decides each take\",\n },\n states: {\n new: { initial: true, note: \"the store holds no state for the key, or a state equal to a new key. The store can remove the key\" },\n open: { note: \"the key holds a count, and its next take is permitted\" },\n limited: { note: \"the next take is refused with the cause limit, until a unit is back\" },\n },\n} as const;\n\n/** These are the events of each document of a limiter. */\nconst EVENTS = {\n take: { kind: \"app\", note: \"one call of take(key): one request of the key. A permitted take uses one unit\" },\n \"unit.back\": { kind: \"timer\", note: \"the next take of a limited key is permitted again\" },\n idle: { kind: \"timer\", note: \"the key is equal to a new key. The store can remove it, and no decision changes\" },\n \"store.fail\": { kind: \"local\", note: \"a shared store fails the take. The state of the key does not change\" },\n} as const;\n\n/** These are the knobs of each document of a limiter that do not change with its policy. */\nconst KNOBS = {\n max_keys: count(\"limiters.<name>.evict.maxKeys\"),\n on_store_failure: { key: \"limiters.<name>.store.onStoreFailure\", scope: \"limiter\", unit: null, default: null },\n} as const;\n\n/**\n * These are the transitions of each document of a limiter that do not change with its policy. They\n * are the refusal of a limited key, a new key at the cap of keys, and a failure of a shared store.\n */\nconst COMMON_TRANSITIONS: readonly TransitionDef[] = [\n t(\"refuse\", \"limited\", \"take\", \"limited\", {\n emits: [\"429 limit\"],\n note: \"Retry-After is the time until the next take is permitted, in whole seconds, rounded up. The count of the key does not change\",\n }),\n t(\"cap\", \"new\", \"take\", \"new\", {\n guard: \"the store holds max_keys keys for the limiter, and no held key is equal to a new key\",\n emits: [\"429 cap\"],\n note: \"Retry-After is the time until the earliest held key is free: at most the idle span. A held key keeps its count\",\n }),\n ...HELD.map((state) =>\n t(`store-failed-${state}`, state, \"store.fail\", state, {\n guard: \"the store is shared\",\n emits: [\"allow, or 429 store\"],\n note: \"on_store_failure open lets the take in, with remaining 0. closed refuses it with the cause store, and its Retry-After is the longest of the policy\",\n }),\n ),\n];\n\n/**\n * A token bucket of one key (GCRA), as a `mark1russell7.machine/1` document. The bucket holds\n * `burst` tokens, and it gets one token back in each interval, `1 / rate`. The store keeps one time\n * for the key: when its bucket is full again.\n * - A take is permitted when, with the take, the bucket is full again in at most `burst` intervals.\n * - A refused take waits until one token is back: at most one interval. That interval is the\n * longest `Retry-After` of the policy (`retry_after@limiter` of the law).\n * - After `burst / rate` with no take, the bucket is full, and the key is equal to a new key. That\n * span is the idle span, and the longest wait of a new key at the cap of keys\n * (`retry_after_at_cap@limiter`).\n */\nexport const TOKEN_BUCKET_MACHINE: Machine = {\n ...COMMON,\n id: \"service.machine.limiter.token-bucket\",\n title: \"A token bucket of one key of the keyed limiter\",\n events: EVENTS,\n knobs: {\n rate: { key: \"limiters.<name>.policy.rate\", scope: \"limiter\", unit: \"count/s\", default: null },\n burst: count(\"limiters.<name>.policy.burst\"),\n ...KNOBS,\n },\n timers: {\n token: {\n duration: { expr: \"1 / rate\", unit: \"s\" },\n early: { expr: \"1 / rate\", unit: \"s\" },\n arms: [\"enter:limited\"],\n cancels: [\"idle\"],\n fires: \"unit.back\",\n note: \"one token comes back at most one interval after the take that used the last token. The Retry-After of each refusal is the time to this firing\",\n },\n full: {\n duration: { expr: \"burst / rate\", unit: \"s\" },\n early: { expr: \"(burst - 1) / rate\", unit: \"s\" },\n arms: [\"take\"],\n fires: \"idle\",\n note: \"the bucket is full again from one to burst intervals after a permitted take. A refused take does not move it\",\n },\n },\n transitions: [\n t(\"first\", \"new\", \"take\", \"open\", { guard: \"burst > 1\", emits: [\"allow\"] }),\n t(\"first-last\", \"new\", \"take\", \"limited\", { guard: \"burst = 1\", emits: [\"allow\"] }),\n t(\"take\", \"open\", \"take\", \"open\", { guard: \"a token remains after the take\", emits: [\"allow\"] }),\n t(\"take-last\", \"open\", \"take\", \"limited\", { guard: \"the take uses the last token\", emits: [\"allow\"] }),\n ...COMMON_TRANSITIONS,\n t(\"token-back\", \"limited\", \"unit.back\", \"open\", { timer: \"token\", guard: \"burst > 1\" }),\n t(\"full\", \"open\", \"idle\", \"new\", { timer: \"full\" }),\n t(\"full-at-once\", \"limited\", \"idle\", \"new\", {\n timer: \"full\",\n guard: \"burst = 1: the one token is back when the bucket is full\",\n }),\n ],\n};\n\n/**\n * A sliding window of one key, as a `mark1russell7.machine/1` document. The store keeps the time of\n * each permitted take of the key in the window, and each take counts for one window from its time.\n * - A take is permitted when fewer than `limit` takes are in the window.\n * - A refused take waits until the oldest take leaves the window: at most one window. That window is\n * the longest `Retry-After` of the policy (`retry_after@limiter` of the law).\n * - One window after the last take, no take is in the window, and the key is equal to a new key.\n * That window is the idle span, and the longest wait of a new key at the cap of keys\n * (`retry_after_at_cap@limiter`).\n */\nexport const SLIDING_WINDOW_MACHINE: Machine = {\n ...COMMON,\n id: \"service.machine.limiter.sliding-window\",\n title: \"A sliding window of one key of the keyed limiter\",\n events: EVENTS,\n knobs: {\n limit: count(\"limiters.<name>.policy.limit\"),\n window: { key: \"limiters.<name>.policy.window\", scope: \"limiter\", unit: \"s\", default: null },\n ...KNOBS,\n },\n timers: {\n oldest: {\n duration: { knob: \"window\", unit: \"s\" },\n early: { knob: \"window\", unit: \"s\" },\n arms: [\"enter:limited\"],\n cancels: [\"idle\"],\n fires: \"unit.back\",\n note: \"the oldest take leaves the window at most one window after the take that filled it. The Retry-After of each refusal is the time to this firing\",\n },\n last: {\n duration: { knob: \"window\", unit: \"s\" },\n arms: [\"take\"],\n fires: \"idle\",\n note: \"the last permitted take leaves the window one window after its time. A refused take does not move it\",\n },\n },\n transitions: [\n t(\"first\", \"new\", \"take\", \"open\", { guard: \"limit > 1\", emits: [\"allow\"] }),\n t(\"first-last\", \"new\", \"take\", \"limited\", { guard: \"limit = 1\", emits: [\"allow\"] }),\n t(\"take\", \"open\", \"take\", \"open\", { guard: \"fewer than limit - 1 takes are in the window before the take\", emits: [\"allow\"] }),\n t(\"take-last\", \"open\", \"take\", \"limited\", { guard: \"the take is the limit-th take in the window\", emits: [\"allow\"] }),\n ...COMMON_TRANSITIONS,\n t(\"oldest-leaves\", \"limited\", \"unit.back\", \"open\", { timer: \"oldest\", guard: \"a later take stays in the window\" }),\n t(\"last-leaves\", \"open\", \"idle\", \"new\", { timer: \"last\" }),\n t(\"all-leave\", \"limited\", \"idle\", \"new\", {\n timer: \"last\",\n guard: \"each take of the window has one time: limit = 1, or each take came at once\",\n }),\n ],\n};\n\n/** This function gives the machine document of the policy of a limiter. */\nexport const limiterMachine = (policy: LimitPolicy): Machine =>\n policy.type === \"token-bucket\" ? TOKEN_BUCKET_MACHINE : SLIDING_WINDOW_MACHINE;\n\n/**\n * This function gives the state of a key of a store as its machine document names it, at the time\n * `now` in milliseconds. `state` is what the store keeps for the key, or `undefined` when it keeps\n * nothing. A key equal to a new key is `new`, and a key whose next take `decide` refuses is `limited`.\n */\nexport const limiterStateOf = (rule: LimitRule, state: KeyState | undefined, now: number): LimiterState => {\n if (state === undefined || freeAt(rule, state) <= now) return \"new\";\n return decide(rule, state, now).decision.allowed ? \"open\" : \"limited\";\n};\n"],"mappings":"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"}
|
package/build/limiter.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import { S as retryAfterSeconds, _ as freeAt, a as LimitRule, b as limiterProblems, c as LimiterSpec, d as Refused, f as SlidingWindow, g as decide, h as TokenBucket, i as LimitPolicy, l as LimiterStoreSpec, m as TakeStep, n as Decision, o as Limiter, p as StoreFailure, r as KeyState, s as LimiterEviction, t as Allowed, u as RefusalCause, v as idleSpan, x as longestRetryAfter, y as limitRule } from "./policy-G4QJ1ZZq.js";
|
|
2
2
|
import { a as keyedLimiter, i as TakeRequest, n as LimiterStore, o as memoryStore, r as MemoryStore, t as KeyedLimiter } from "./store-DzTyosou.js";
|
|
3
|
-
|
|
3
|
+
import { a as limiterStateOf, i as limiterMachine, n as SLIDING_WINDOW_MACHINE, r as TOKEN_BUCKET_MACHINE, t as LimiterState } from "./limiter-DCDdzVwe.js";
|
|
4
|
+
export { type Allowed, type Decision, type KeyState, type KeyedLimiter, type LimitPolicy, type LimitRule, type Limiter, type LimiterEviction, type LimiterSpec, type LimiterState, type LimiterStore, type LimiterStoreSpec, type MemoryStore, type RefusalCause, type Refused, SLIDING_WINDOW_MACHINE, type SlidingWindow, type StoreFailure, TOKEN_BUCKET_MACHINE, type TakeRequest, type TakeStep, type TokenBucket, decide, freeAt, idleSpan, keyedLimiter, limitRule, limiterMachine, limiterProblems, limiterStateOf, longestRetryAfter, memoryStore, retryAfterSeconds };
|
package/build/limiter.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { a as limiterProblems, i as limitRule, n as freeAt, o as longestRetryAfter, r as idleSpan, s as retryAfterSeconds, t as decide } from "./policy-CgS4y94U.js";
|
|
2
|
-
import { n as memoryStore, t as
|
|
3
|
-
export { decide, freeAt, idleSpan, keyedLimiter, limitRule, limiterProblems, longestRetryAfter, memoryStore, retryAfterSeconds };
|
|
2
|
+
import { a as keyedLimiter, i as limiterStateOf, n as TOKEN_BUCKET_MACHINE, o as memoryStore, r as limiterMachine, t as SLIDING_WINDOW_MACHINE } from "./limiter-pkBZUp-t.js";
|
|
3
|
+
export { SLIDING_WINDOW_MACHINE, TOKEN_BUCKET_MACHINE, decide, freeAt, idleSpan, keyedLimiter, limitRule, limiterMachine, limiterProblems, limiterStateOf, longestRetryAfter, memoryStore, retryAfterSeconds };
|
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.
|
|
4
|
+
"version": "0.5.8",
|
|
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",
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
},
|
|
91
91
|
"devDependencies": {
|
|
92
92
|
"@mark1russell7/cue": "github:mark1russell7/cue",
|
|
93
|
-
"@mark1russell7/harness": "^0.11.
|
|
93
|
+
"@mark1russell7/harness": "^0.11.23",
|
|
94
94
|
"@types/node": "^26.6.4",
|
|
95
95
|
"ioredis": "6.0.0",
|
|
96
96
|
"testcontainers": "12.2.0",
|