@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.
- package/README.md +61 -0
- package/build/{builder-BzjhWk2E.d.ts → builder-DyDB7zM9.d.ts} +9 -2
- package/build/http.d.ts +9 -1
- package/build/http.js +25 -1
- package/build/http.js.map +1 -1
- package/build/index.d.ts +30 -8
- package/build/index.js +117 -5
- package/build/index.js.map +1 -1
- package/build/{lifecycle-DjEpgpRk.d.ts → lifecycle-Chu9Pri2.d.ts} +7 -1
- package/build/limiter-DzTyosou.d.ts +72 -0
- package/build/limiter-wv0uXDk9.js +126 -0
- package/build/limiter-wv0uXDk9.js.map +1 -0
- package/build/limiter.d.ts +3 -0
- package/build/limiter.js +3 -0
- package/build/policy-CgS4y94U.js +187 -0
- package/build/policy-CgS4y94U.js.map +1 -0
- package/build/policy-G4QJ1ZZq.d.ts +176 -0
- package/build/redis.d.ts +2 -2
- package/build/testing.d.ts +2 -2
- package/package.json +9 -4
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { n as Clock } from "./clock-D-38uH_z.js";
|
|
2
|
+
import { o as Limiter } from "./policy-G4QJ1ZZq.js";
|
|
2
3
|
import { Extent, Quantity } from "@mark1russell7/quantity";
|
|
3
4
|
//#region src/operation.d.ts
|
|
4
5
|
/**
|
|
@@ -344,6 +345,11 @@ interface ServiceDescription {
|
|
|
344
345
|
readonly dependencies: readonly Dependency[];
|
|
345
346
|
readonly controllers: readonly Controller[];
|
|
346
347
|
readonly checks: readonly Check[];
|
|
348
|
+
/**
|
|
349
|
+
* These are the keyed limiters of the service, each with its key, its policy, its store and the
|
|
350
|
+
* bound on its keys. Without a declaration, the service has no limiters.
|
|
351
|
+
*/
|
|
352
|
+
readonly limiters?: readonly Limiter[] | undefined;
|
|
347
353
|
readonly policies: {
|
|
348
354
|
readonly readiness: Timing;
|
|
349
355
|
readonly liveness: Timing;
|
|
@@ -696,4 +702,4 @@ interface Lifecycle {
|
|
|
696
702
|
}
|
|
697
703
|
//#endregion
|
|
698
704
|
export { failSpan as $, LifecycleEvent as A, Gate as B, ProbeAnswer as C, Operation as Ct, Outcome as D, CheckFn as E, CooperativeFloor as F, Step as G, Phase as H, Coupling as I, Poll as J, Transport as K, Dependency as L, Recorder as M, Check as N, Result as O, Controller as P, asWindow as Q, ExitCodes as R, HealthVerdict as S, Idempotency as St, StartOptions as T, RepeatSignal as U, MessageTrigger as V, ServiceDescription as W, Timing as X, Span as Y, asDebounce as Z, RequestOptions as _, isExitSignal as _t, PhaseContext as a, EXIT_SIGNALS as at, CheckVerdict as b, nodeProcess as bt, STAGES as c, ExitTrigger as ct, TransportImpl as d, PROCESS_TRIGGERS as dt, heartbeatDetection as et, cooperativeAbortAt as f, ProcessHost as ft, RequestFn as g, isExitCode as gt, RequestEvent as h, embeddedProcess as ht, LifecycleState as i, RouteRequest as it, MachineName as j, Trace as k, ShutdownOptions as l, HostStream as lt, RequestEnd as m, ProcessTrigger as mt, HealthServer as n, OperationOf as nt, PhaseWork as o, EmbeddedProcess as ot, stageRank as p, ProcessPort as pt, FailsBy as q, Lifecycle as r, RoutePattern as rt, RunOptions as s, ExitSignal as st, DependencyHandle as t, recoverSpan as tt, Stage as u, MessageShape as ut, RequestStream as v, isMessageShape as vt, ProbeKind as w, mayReissue as wt, Health as x, processPortOf as xt, RequestWork as y, matchesMessage as yt, FORMAT as z };
|
|
699
|
-
//# sourceMappingURL=lifecycle-
|
|
705
|
+
//# sourceMappingURL=lifecycle-Chu9Pri2.d.ts.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { n as Clock } from "./clock-D-38uH_z.js";
|
|
2
|
+
import { a as LimitRule, c as LimiterSpec, n as Decision } from "./policy-G4QJ1ZZq.js";
|
|
3
|
+
//#region src/limiter/store.d.ts
|
|
4
|
+
/** A take request is one take of one key, with the rule and the cap of its limiter in numbers. */
|
|
5
|
+
interface TakeRequest {
|
|
6
|
+
/** This name is the limiter. A store keeps the keys of each limiter apart. */
|
|
7
|
+
readonly limiter: string;
|
|
8
|
+
readonly key: string;
|
|
9
|
+
readonly rule: LimitRule;
|
|
10
|
+
/** This count is the most keys that the store holds for the limiter. */
|
|
11
|
+
readonly maxKeys: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A limiter store keeps the state of each key, and it decides each take in one atomic step, on its
|
|
15
|
+
* own clock. Thus two takes of one key do not read the same state at any time. A store in this
|
|
16
|
+
* process uses `decide`. A store that the replicas share does the same arithmetic in one step of its
|
|
17
|
+
* own, for example one script of Redis that reads `TIME`.
|
|
18
|
+
*/
|
|
19
|
+
interface LimiterStore {
|
|
20
|
+
/** This kind tells where the counts are: in the memory of this replica, or in a store that the replicas share. */
|
|
21
|
+
readonly kind: "local" | "shared";
|
|
22
|
+
/**
|
|
23
|
+
* This method decides one take. It removes a key only when its state is equal to the state of a
|
|
24
|
+
* new key. It holds at most `maxKeys` keys for each limiter. A failure of a shared store rejects
|
|
25
|
+
* the take, and the limiter then applies the `onStoreFailure` of its spec.
|
|
26
|
+
*/
|
|
27
|
+
take(request: TakeRequest): Decision | Promise<Decision>;
|
|
28
|
+
}
|
|
29
|
+
/** A memory store is the store of one replica, on the injected monotonic clock. */
|
|
30
|
+
interface MemoryStore extends LimiterStore {
|
|
31
|
+
readonly kind: "local";
|
|
32
|
+
/** This method gives the count of keys that the store holds for a limiter. */
|
|
33
|
+
size(limiter: string): number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* This function makes the store of one replica. It reads time only through the clock. Thus a step
|
|
37
|
+
* of the wall clock does not move a count, and a fake clock gives exact decisions in a test.
|
|
38
|
+
* - The eviction is exact. A key whose bucket is full again, or whose window has no take, is equal
|
|
39
|
+
* to a new key. The store removes it at its next take, or at a sweep.
|
|
40
|
+
* - The count of keys has a bound. At the cap, a take of a new key sweeps the table, but only when
|
|
41
|
+
* a held key can be free. Each sweep reads each key one time. When no key is free, the store
|
|
42
|
+
* refuses the new key with the cause `cap`. Its `retryAfter` is at most the time until the
|
|
43
|
+
* earliest held key is free, and that time is at most `idleSpan` of the policy.
|
|
44
|
+
*/
|
|
45
|
+
declare const memoryStore: (clock: Clock) => MemoryStore;
|
|
46
|
+
/** A keyed limiter is the runtime of one declared limiter: its spec, its rule in numbers, and its store. */
|
|
47
|
+
interface KeyedLimiter {
|
|
48
|
+
readonly name: string;
|
|
49
|
+
/** This spec is the declaration, the same data that `svc.limiter(name, spec)` takes and `describe()` shows. */
|
|
50
|
+
readonly spec: LimiterSpec;
|
|
51
|
+
readonly rule: LimitRule;
|
|
52
|
+
/** This count is the most keys that the store holds for the limiter. */
|
|
53
|
+
readonly maxKeys: number;
|
|
54
|
+
/**
|
|
55
|
+
* This method takes one unit for a key, and gives the decision of the store. When a shared store
|
|
56
|
+
* fails, it gives the decision of `onStoreFailure`, with the failure as text.
|
|
57
|
+
*/
|
|
58
|
+
take(key: string): Promise<Decision>;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* This function makes the runtime of a limiter from its spec and its store. It refuses a spec with a
|
|
62
|
+
* problem, because the limiter has no defaults. It also refuses a store of the wrong kind: a spec
|
|
63
|
+
* with a shared store needs a store that the replicas share.
|
|
64
|
+
* - A failure of a shared store gives the decision of `onStoreFailure`. With `open`, the take is
|
|
65
|
+
* permitted with `remaining` 0. With `closed`, it is refused with the cause `store`.
|
|
66
|
+
* - The `retryAfter` of the cause `store` is the longest `retryAfter` of the policy.
|
|
67
|
+
* - A failure of a local store rejects the take, because a local store has no `onStoreFailure`.
|
|
68
|
+
*/
|
|
69
|
+
declare const keyedLimiter: (name: string, spec: LimiterSpec, store: LimiterStore) => KeyedLimiter;
|
|
70
|
+
//#endregion
|
|
71
|
+
export { keyedLimiter as a, TakeRequest as i, LimiterStore as n, memoryStore as o, MemoryStore as r, KeyedLimiter as t };
|
|
72
|
+
//# sourceMappingURL=limiter-DzTyosou.d.ts.map
|
|
@@ -0,0 +1,126 @@
|
|
|
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
|
+
export { memoryStore as n, keyedLimiter as t };
|
|
125
|
+
|
|
126
|
+
//# sourceMappingURL=limiter-wv0uXDk9.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"limiter-wv0uXDk9.js","names":["r"],"sources":["../src/limiter/store.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"],"mappings":"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"}
|
|
@@ -0,0 +1,3 @@
|
|
|
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
|
+
import { a as keyedLimiter, i as TakeRequest, n as LimiterStore, o as memoryStore, r as MemoryStore, t as KeyedLimiter } from "./limiter-DzTyosou.js";
|
|
3
|
+
export { type Allowed, type Decision, type KeyState, type KeyedLimiter, type LimitPolicy, type LimitRule, type Limiter, type LimiterEviction, type LimiterSpec, type LimiterStore, type LimiterStoreSpec, type MemoryStore, type RefusalCause, type Refused, type SlidingWindow, type StoreFailure, type TakeRequest, type TakeStep, type TokenBucket, decide, freeAt, idleSpan, keyedLimiter, limitRule, limiterProblems, longestRetryAfter, memoryStore, retryAfterSeconds };
|
package/build/limiter.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
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 keyedLimiter } from "./limiter-wv0uXDk9.js";
|
|
3
|
+
export { decide, freeAt, idleSpan, keyedLimiter, limitRule, limiterProblems, longestRetryAfter, memoryStore, retryAfterSeconds };
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { convertExact, convertible, rationals } from "@mark1russell7/quantity";
|
|
2
|
+
//#region src/limiter/policy.ts
|
|
3
|
+
/**
|
|
4
|
+
* This tolerance is one nanosecond, in milliseconds. A comparison of times permits it, because a
|
|
5
|
+
* rate of 3 for each second gives an interval that floating point cannot hold exactly.
|
|
6
|
+
*/
|
|
7
|
+
const TOLERANCE = 1e-6;
|
|
8
|
+
const isCount = (amount) => amount !== void 0 && convertible(amount.unit, "count");
|
|
9
|
+
const isTime = (amount) => amount !== void 0 && convertible(amount.unit, "s");
|
|
10
|
+
const isRate = (amount) => amount !== void 0 && convertible(amount.unit, "count/s");
|
|
11
|
+
/** This function tells if a unit converts, also for a unit that the quantity package does not know. */
|
|
12
|
+
const safely = (test, amount) => {
|
|
13
|
+
try {
|
|
14
|
+
return test(amount);
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const finiteAbove = (amount) => typeof amount.value === "number" && Number.isFinite(amount.value) && amount.value > 0;
|
|
20
|
+
const wholeCount = (amount) => finiteAbove(amount) && Number.isInteger(rationals.toNumber(convertExact(rationals.fromNumber(amount.value), amount.unit, "count")));
|
|
21
|
+
/**
|
|
22
|
+
* This function gives the problems of a limiter spec. `describe()` and `keyedLimiter` refuse a spec
|
|
23
|
+
* with a problem. A part that is absent is a problem, because the limiter has no defaults.
|
|
24
|
+
*/
|
|
25
|
+
const limiterProblems = (name, spec) => {
|
|
26
|
+
const owner = `limiter ${name}`;
|
|
27
|
+
if (spec === void 0 || spec === null) return [`${owner} has no spec: a limiter has no defaults`];
|
|
28
|
+
const problems = [];
|
|
29
|
+
const missing = (part) => {
|
|
30
|
+
problems.push(`${owner} does not declare ${part}: a limiter has no defaults`);
|
|
31
|
+
};
|
|
32
|
+
if (typeof spec.key !== "string") missing("what its key is (key)");
|
|
33
|
+
else if (spec.key.trim().length === 0) problems.push(`${owner} names no key`);
|
|
34
|
+
const policy = spec.policy;
|
|
35
|
+
if (policy === void 0) missing("its policy (policy)");
|
|
36
|
+
else if (policy.type === "token-bucket") {
|
|
37
|
+
if (policy.rate === void 0) missing("the rate of its bucket (policy.rate)");
|
|
38
|
+
else if (!safely(isRate, policy.rate)) problems.push(`${owner}'s rate is not a count for each unit of time, for example count/s`);
|
|
39
|
+
else if (!finiteAbove(policy.rate)) problems.push(`${owner}'s rate is not a finite rate above zero`);
|
|
40
|
+
if (policy.burst === void 0) missing("the size of its bucket (policy.burst)");
|
|
41
|
+
else if (!safely(isCount, policy.burst)) problems.push(`${owner}'s burst is not a count`);
|
|
42
|
+
else if (!wholeCount(policy.burst)) problems.push(`${owner}'s burst is not a whole count above zero`);
|
|
43
|
+
} else if (policy.type === "sliding-window") {
|
|
44
|
+
if (policy.limit === void 0) missing("the limit of its window (policy.limit)");
|
|
45
|
+
else if (!safely(isCount, policy.limit)) problems.push(`${owner}'s limit is not a count`);
|
|
46
|
+
else if (!wholeCount(policy.limit)) problems.push(`${owner}'s limit is not a whole count above zero`);
|
|
47
|
+
if (policy.window === void 0) missing("the length of its window (policy.window)");
|
|
48
|
+
else if (!safely(isTime, policy.window)) problems.push(`${owner}'s window is not a time`);
|
|
49
|
+
else if (!finiteAbove(policy.window)) problems.push(`${owner}'s window is not a finite time above zero`);
|
|
50
|
+
} else problems.push(`${owner}'s policy ${String(policy.type)} is not "token-bucket" or "sliding-window"`);
|
|
51
|
+
const store = spec.store;
|
|
52
|
+
if (store === void 0) missing(`its store, { kind: "local" } or { kind: "shared", dependency, onStoreFailure } (store)`);
|
|
53
|
+
else if (store.kind === "shared") {
|
|
54
|
+
if (typeof store.dependency !== "string" || store.dependency.trim().length === 0) problems.push(`${owner}'s shared store names no dependency`);
|
|
55
|
+
if (store.onStoreFailure === void 0) missing(`what a failure of its shared store does, "open" or "closed" (store.onStoreFailure)`);
|
|
56
|
+
else if (store.onStoreFailure !== "open" && store.onStoreFailure !== "closed") problems.push(`${owner}'s onStoreFailure ${String(store.onStoreFailure)} is not "open" or "closed"`);
|
|
57
|
+
} else if (store.kind !== "local") problems.push(`${owner}'s store ${String(store.kind)} is not "local" or "shared"`);
|
|
58
|
+
const evict = spec.evict;
|
|
59
|
+
if (evict === void 0 || evict.maxKeys === void 0) missing("the most keys that it holds (evict.maxKeys)");
|
|
60
|
+
else if (!safely(isCount, evict.maxKeys)) problems.push(`${owner}'s maxKeys is not a count`);
|
|
61
|
+
else if (!wholeCount(evict.maxKeys)) problems.push(`${owner}'s maxKeys is not a whole count above zero: the count of keys has a bound`);
|
|
62
|
+
return problems;
|
|
63
|
+
};
|
|
64
|
+
/** This function gives an amount in milliseconds, exactly where the units permit, as one number. */
|
|
65
|
+
const milliseconds = (amount) => rationals.toNumber(convertExact(rationals.fromNumber(amount.value), amount.unit, "ms"));
|
|
66
|
+
/** This function gives a count as one number. */
|
|
67
|
+
const countOf = (amount) => rationals.toNumber(convertExact(rationals.fromNumber(amount.value), amount.unit, "count"));
|
|
68
|
+
/**
|
|
69
|
+
* This function gives the rule of a policy in numbers. The interval of a bucket is exact as a
|
|
70
|
+
* rational, and it becomes one number only at the end. Thus 10 for each minute gives 6000 ms.
|
|
71
|
+
*/
|
|
72
|
+
const limitRule = (policy) => {
|
|
73
|
+
if (policy.type === "token-bucket") {
|
|
74
|
+
const perMs = convertExact(rationals.fromNumber(policy.rate.value), policy.rate.unit, "count/ms");
|
|
75
|
+
return {
|
|
76
|
+
type: "token-bucket",
|
|
77
|
+
interval: rationals.toNumber(rationals.div(rationals.ONE, perMs)),
|
|
78
|
+
burst: countOf(policy.burst)
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
type: "sliding-window",
|
|
83
|
+
limit: countOf(policy.limit),
|
|
84
|
+
window: milliseconds(policy.window)
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
/** This function gives a time in milliseconds as a quantity. */
|
|
88
|
+
const ms = (value) => ({
|
|
89
|
+
value,
|
|
90
|
+
unit: "ms"
|
|
91
|
+
});
|
|
92
|
+
/**
|
|
93
|
+
* This function gives the span after which an idle key is equal to a new key. That span is
|
|
94
|
+
* `burst / rate` for a token bucket, and one window for a sliding window. A store can remove the key
|
|
95
|
+
* after it, and no decision changes. It is also the longest `retryAfter` of a new key that the store
|
|
96
|
+
* refuses at its cap.
|
|
97
|
+
*/
|
|
98
|
+
const idleSpan = (policy) => {
|
|
99
|
+
const rule = limitRule(policy);
|
|
100
|
+
return ms(rule.type === "token-bucket" ? rule.interval * rule.burst : rule.window);
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* This function gives the longest `retryAfter` of a refusal by the policy: one interval of the
|
|
104
|
+
* bucket (`1 / rate`), or one window. A refusal at the cap of keys can be longer, up to `idleSpan`.
|
|
105
|
+
*/
|
|
106
|
+
const longestRetryAfter = (policy) => {
|
|
107
|
+
const rule = limitRule(policy);
|
|
108
|
+
return ms(rule.type === "token-bucket" ? rule.interval : rule.window);
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* This function gives a `retryAfter` in whole seconds, rounded up, as the header `Retry-After`
|
|
112
|
+
* (RFC 9110) has it. A time over a whole second by less than one nanosecond rounds down to it,
|
|
113
|
+
* because that part is an error of floating point.
|
|
114
|
+
*/
|
|
115
|
+
const retryAfterSeconds = (retryAfter) => {
|
|
116
|
+
const seconds = rationals.toNumber(convertExact(rationals.fromNumber(retryAfter.value), retryAfter.unit, "s"));
|
|
117
|
+
return Math.max(0, Math.ceil(seconds - TOLERANCE / 1e3));
|
|
118
|
+
};
|
|
119
|
+
/** This function gives the time at which a key state is equal to the state of a new key, in milliseconds. */
|
|
120
|
+
const freeAt = (rule, state) => {
|
|
121
|
+
if (state.type === "token-bucket") return state.full;
|
|
122
|
+
const last = state.stamps[state.stamps.length - 1];
|
|
123
|
+
return last === void 0 ? Number.NEGATIVE_INFINITY : last + (rule.type === "sliding-window" ? rule.window : 0);
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* This function decides one take of a key at the time `now`, in milliseconds, from the state of the
|
|
127
|
+
* key (`undefined` for a new key). It is the one decision of each store in this process. A store in
|
|
128
|
+
* another process does the same arithmetic in its own atomic step.
|
|
129
|
+
* - A token bucket (GCRA): the store lets the take in when, with the take, the bucket is full
|
|
130
|
+
* again in at most `burst` intervals. A refusal waits until one token is back.
|
|
131
|
+
* - A sliding window: the store lets the take in when fewer than `limit` takes are in the window.
|
|
132
|
+
* A refusal waits until the oldest take leaves the window.
|
|
133
|
+
*/
|
|
134
|
+
const decide = (rule, state, now) => {
|
|
135
|
+
if (rule.type === "token-bucket") {
|
|
136
|
+
const span = rule.interval * rule.burst;
|
|
137
|
+
const full = state?.type === "token-bucket" ? Math.max(state.full, now) : now;
|
|
138
|
+
const next = full + rule.interval;
|
|
139
|
+
if (next - now > span + TOLERANCE) return {
|
|
140
|
+
decision: {
|
|
141
|
+
allowed: false,
|
|
142
|
+
retryAfter: ms(next - span - now),
|
|
143
|
+
cause: "limit"
|
|
144
|
+
},
|
|
145
|
+
state: {
|
|
146
|
+
type: "token-bucket",
|
|
147
|
+
full
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
return {
|
|
151
|
+
decision: {
|
|
152
|
+
allowed: true,
|
|
153
|
+
remaining: Math.max(0, Math.floor((span - (next - now)) / rule.interval + TOLERANCE))
|
|
154
|
+
},
|
|
155
|
+
state: {
|
|
156
|
+
type: "token-bucket",
|
|
157
|
+
full: next
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const live = (state?.type === "sliding-window" ? state.stamps : []).filter((stamp) => stamp + rule.window > now + TOLERANCE);
|
|
162
|
+
if (live.length >= rule.limit) return {
|
|
163
|
+
decision: {
|
|
164
|
+
allowed: false,
|
|
165
|
+
retryAfter: ms(live[0] + rule.window - now),
|
|
166
|
+
cause: "limit"
|
|
167
|
+
},
|
|
168
|
+
state: {
|
|
169
|
+
type: "sliding-window",
|
|
170
|
+
stamps: live
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
return {
|
|
174
|
+
decision: {
|
|
175
|
+
allowed: true,
|
|
176
|
+
remaining: rule.limit - live.length - 1
|
|
177
|
+
},
|
|
178
|
+
state: {
|
|
179
|
+
type: "sliding-window",
|
|
180
|
+
stamps: [...live, now]
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
//#endregion
|
|
185
|
+
export { limiterProblems as a, limitRule as i, freeAt as n, longestRetryAfter as o, idleSpan as r, retryAfterSeconds as s, decide as t };
|
|
186
|
+
|
|
187
|
+
//# sourceMappingURL=policy-CgS4y94U.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"policy-CgS4y94U.js","names":["r"],"sources":["../src/limiter/policy.ts"],"sourcesContent":["import type { Quantity } from \"@mark1russell7/quantity\";\nimport { convertExact, convertible, rationals as r } from \"@mark1russell7/quantity\";\n\n/**\n * A token bucket gives each key a bucket of `burst` tokens. The bucket refills at `rate`, and each\n * take uses one token. Thus a key can make `burst` takes at once after an idle time, and then\n * `rate` takes in each unit of time.\n */\nexport interface TokenBucket {\n readonly type: \"token-bucket\";\n /** This rate is how fast the bucket refills: a count for each unit of time, for example `{ value: 10, unit: \"count/s\" }`. */\n readonly rate: Quantity;\n /** This count is the size of the bucket: the most takes in a sequence after an idle time. */\n readonly burst: Quantity;\n}\n\n/**\n * A sliding window gives each key at most `limit` takes in each `window`. Each take counts for one\n * window from its time, thus the window moves with the clock and has no fixed edges.\n */\nexport interface SlidingWindow {\n readonly type: \"sliding-window\";\n /** This count is the most takes of one key in one window. */\n readonly limit: Quantity;\n /** This time is the length of the window. */\n readonly window: Quantity;\n}\n\n/** A limit policy is the rule that a keyed limiter applies to each key, as data. It has no defaults. */\nexport type LimitPolicy = TokenBucket | SlidingWindow;\n\n/**\n * A store failure policy tells what a take does when a shared store fails. It has no default.\n * - `open`: the limiter lets the take in. The count is not known, thus `remaining` is 0.\n * - `closed`: the limiter refuses the take with the cause `store`. Its `retryAfter` is the longest\n * `retryAfter` of the policy (`longestRetryAfter`). Thus O29 and O30 apply to it too.\n */\nexport type StoreFailure = \"open\" | \"closed\";\n\n/**\n * A store spec tells where the counts of the keys are:\n * - `local`: in the memory of each replica. A limit L over N replicas is then L / N on each\n * replica. That sum holds only on average, when the balancer spreads the takes of each key evenly.\n * A local store does not fail, thus it has no `onStoreFailure`, and the limiter ignores one.\n * - `shared`: in a store that the replicas share, the dependency of that name. The store decides\n * each take in one atomic step, on its own clock. Thus the limit holds across the replicas.\n * `onStoreFailure` tells what a take does when the store fails.\n */\nexport type LimiterStoreSpec =\n | { readonly kind: \"local\" }\n | { readonly kind: \"shared\"; readonly dependency: string; readonly onStoreFailure: StoreFailure };\n\n/**\n * The eviction of a limiter bounds its keys. A store removes a key only when its state is equal to\n * the state of a new key. That is a bucket that is full again, or a window with no take in it.\n * Thus an eviction changes no decision.\n */\nexport interface LimiterEviction {\n /**\n * This count is the most keys that the store holds. At the cap, the store refuses a take of a new\n * key until a held key is idle for its span (`idleSpan`). A held key keeps its count.\n */\n readonly maxKeys: Quantity;\n}\n\n/** A limiter spec is the declaration of one keyed limiter. Each part is necessary, because the limiter has no defaults. */\nexport interface LimiterSpec {\n /** This text tells what the key of each take is, for example `tenant` or `principal`. The caller gives each key. */\n readonly key: string;\n readonly policy: LimitPolicy;\n readonly store: LimiterStoreSpec;\n readonly evict: LimiterEviction;\n}\n\n/** A limiter is a keyed limiter in the description of a service: its name and its spec. */\nexport interface Limiter extends LimiterSpec {\n readonly name: string;\n}\n\n/**\n * A refusal cause tells why a take is refused:\n * - `limit`: the key used its limit.\n * - `cap`: the store holds its cap of keys, and the key is new.\n * - `store`: the shared store failed, and its spec declares `onStoreFailure: \"closed\"`.\n */\nexport type RefusalCause = \"limit\" | \"cap\" | \"store\";\n\n/** A permitted take used one unit of its key. `remaining` is the count of takes that the key can make at this time. */\nexport interface Allowed {\n readonly allowed: true;\n readonly remaining: number;\n /** This text is the failure of a shared store, when `onStoreFailure: \"open\"` lets the take in. */\n readonly failure?: string | undefined;\n}\n\n/**\n * A refused take used nothing. `retryAfter` is the time until the limiter lets the same take in, when\n * no other take comes first. It is always above zero.\n */\nexport interface Refused {\n readonly allowed: false;\n readonly retryAfter: Quantity;\n readonly cause: RefusalCause;\n /** This text is the failure of a shared store, for the cause `store`. */\n readonly failure?: string | undefined;\n}\n\n/** A decision is the answer of a limiter to one take. */\nexport type Decision = Allowed | Refused;\n\n/**\n * A limit rule is a policy in numbers, which a store applies: each time in milliseconds, and each\n * count as a number. A store in another process, for example a script of Redis, gets these numbers.\n */\nexport type LimitRule =\n | {\n readonly type: \"token-bucket\";\n /** This time is the refill of one token, in milliseconds. */\n readonly interval: number;\n readonly burst: number;\n }\n | {\n readonly type: \"sliding-window\";\n readonly limit: number;\n /** This time is the length of the window, in milliseconds. */\n readonly window: number;\n };\n\n/**\n * A key state is what a store keeps for one key:\n * - A token bucket keeps the time at which its bucket is full again (`full`, in milliseconds).\n * - A sliding window keeps the time of each permitted take in the window, the oldest first.\n */\nexport type KeyState =\n | { readonly type: \"token-bucket\"; readonly full: number }\n | { readonly type: \"sliding-window\"; readonly stamps: readonly number[] };\n\n/** 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. */\nexport interface TakeStep {\n readonly decision: Decision;\n readonly state: KeyState;\n}\n\n/**\n * This tolerance is one nanosecond, in milliseconds. A comparison of times permits it, because a\n * rate of 3 for each second gives an interval that floating point cannot hold exactly.\n */\nconst TOLERANCE = 1e-6;\n\nconst isCount = (amount: Quantity | undefined): boolean => amount !== undefined && convertible(amount.unit, \"count\");\nconst isTime = (amount: Quantity | undefined): boolean => amount !== undefined && convertible(amount.unit, \"s\");\nconst isRate = (amount: Quantity | undefined): boolean => amount !== undefined && convertible(amount.unit, \"count/s\");\n/** This function tells if a unit converts, also for a unit that the quantity package does not know. */\nconst safely = (test: (amount: Quantity | undefined) => boolean, amount: Quantity | undefined): boolean => {\n try {\n return test(amount);\n } catch {\n return false;\n }\n};\nconst finiteAbove = (amount: Quantity): boolean => typeof amount.value === \"number\" && Number.isFinite(amount.value) && amount.value > 0;\nconst wholeCount = (amount: Quantity): boolean => finiteAbove(amount) && Number.isInteger(r.toNumber(convertExact(r.fromNumber(amount.value), amount.unit, \"count\")));\n\n/** A loose policy is a policy from any source, with each part optional, for the check of its parts. */\ninterface LoosePolicy {\n readonly type?: string;\n readonly rate?: Quantity;\n readonly burst?: Quantity;\n readonly limit?: Quantity;\n readonly window?: Quantity;\n}\n\n/**\n * This function gives the problems of a limiter spec. `describe()` and `keyedLimiter` refuse a spec\n * with a problem. A part that is absent is a problem, because the limiter has no defaults.\n */\nexport const limiterProblems = (name: string, spec: Partial<LimiterSpec> | undefined): string[] => {\n const owner = `limiter ${name}`;\n if (spec === undefined || (spec as unknown) === null) return [`${owner} has no spec: a limiter has no defaults`];\n const problems: string[] = [];\n const missing = (part: string): void => {\n problems.push(`${owner} does not declare ${part}: a limiter has no defaults`);\n };\n if (typeof spec.key !== \"string\") missing(\"what its key is (key)\");\n else if (spec.key.trim().length === 0) problems.push(`${owner} names no key`);\n const policy = spec.policy as LoosePolicy | undefined;\n if (policy === undefined) missing(\"its policy (policy)\");\n else if (policy.type === \"token-bucket\") {\n if (policy.rate === undefined) missing(\"the rate of its bucket (policy.rate)\");\n else if (!safely(isRate, policy.rate)) problems.push(`${owner}'s rate is not a count for each unit of time, for example count/s`);\n else if (!finiteAbove(policy.rate)) problems.push(`${owner}'s rate is not a finite rate above zero`);\n if (policy.burst === undefined) missing(\"the size of its bucket (policy.burst)\");\n else if (!safely(isCount, policy.burst)) problems.push(`${owner}'s burst is not a count`);\n else if (!wholeCount(policy.burst)) problems.push(`${owner}'s burst is not a whole count above zero`);\n } else if (policy.type === \"sliding-window\") {\n if (policy.limit === undefined) missing(\"the limit of its window (policy.limit)\");\n else if (!safely(isCount, policy.limit)) problems.push(`${owner}'s limit is not a count`);\n else if (!wholeCount(policy.limit)) problems.push(`${owner}'s limit is not a whole count above zero`);\n if (policy.window === undefined) missing(\"the length of its window (policy.window)\");\n else if (!safely(isTime, policy.window)) problems.push(`${owner}'s window is not a time`);\n else if (!finiteAbove(policy.window)) problems.push(`${owner}'s window is not a finite time above zero`);\n } else problems.push(`${owner}'s policy ${String(policy.type)} is not \"token-bucket\" or \"sliding-window\"`);\n const store = spec.store as { kind?: string; dependency?: unknown; onStoreFailure?: unknown } | undefined;\n if (store === undefined) missing(`its store, { kind: \"local\" } or { kind: \"shared\", dependency, onStoreFailure } (store)`);\n else if (store.kind === \"shared\") {\n if (typeof store.dependency !== \"string\" || store.dependency.trim().length === 0)\n problems.push(`${owner}'s shared store names no dependency`);\n if (store.onStoreFailure === undefined) missing(`what a failure of its shared store does, \"open\" or \"closed\" (store.onStoreFailure)`);\n else if (store.onStoreFailure !== \"open\" && store.onStoreFailure !== \"closed\")\n problems.push(`${owner}'s onStoreFailure ${String(store.onStoreFailure)} is not \"open\" or \"closed\"`);\n } else if (store.kind !== \"local\") problems.push(`${owner}'s store ${String(store.kind)} is not \"local\" or \"shared\"`);\n const evict = spec.evict as Partial<LimiterEviction> | undefined;\n if (evict === undefined || evict.maxKeys === undefined) missing(\"the most keys that it holds (evict.maxKeys)\");\n else if (!safely(isCount, evict.maxKeys)) problems.push(`${owner}'s maxKeys is not a count`);\n else if (!wholeCount(evict.maxKeys)) problems.push(`${owner}'s maxKeys is not a whole count above zero: the count of keys has a bound`);\n return problems;\n};\n\n/** This function gives an amount in milliseconds, exactly where the units permit, as one number. */\nconst milliseconds = (amount: Quantity): number => r.toNumber(convertExact(r.fromNumber(amount.value), amount.unit, \"ms\"));\n/** This function gives a count as one number. */\nconst countOf = (amount: Quantity): number => r.toNumber(convertExact(r.fromNumber(amount.value), amount.unit, \"count\"));\n\n/**\n * This function gives the rule of a policy in numbers. The interval of a bucket is exact as a\n * rational, and it becomes one number only at the end. Thus 10 for each minute gives 6000 ms.\n */\nexport const limitRule = (policy: LimitPolicy): LimitRule => {\n if (policy.type === \"token-bucket\") {\n const perMs = convertExact(r.fromNumber(policy.rate.value), policy.rate.unit, \"count/ms\");\n return { type: \"token-bucket\", interval: r.toNumber(r.div(r.ONE, perMs)), burst: countOf(policy.burst) };\n }\n return { type: \"sliding-window\", limit: countOf(policy.limit), window: milliseconds(policy.window) };\n};\n\n/** This function gives a time in milliseconds as a quantity. */\nconst ms = (value: number): Quantity => ({ value, unit: \"ms\" });\n\n/**\n * This function gives the span after which an idle key is equal to a new key. That span is\n * `burst / rate` for a token bucket, and one window for a sliding window. A store can remove the key\n * after it, and no decision changes. It is also the longest `retryAfter` of a new key that the store\n * refuses at its cap.\n */\nexport const idleSpan = (policy: LimitPolicy): Quantity => {\n const rule = limitRule(policy);\n return ms(rule.type === \"token-bucket\" ? rule.interval * rule.burst : rule.window);\n};\n\n/**\n * This function gives the longest `retryAfter` of a refusal by the policy: one interval of the\n * bucket (`1 / rate`), or one window. A refusal at the cap of keys can be longer, up to `idleSpan`.\n */\nexport const longestRetryAfter = (policy: LimitPolicy): Quantity => {\n const rule = limitRule(policy);\n return ms(rule.type === \"token-bucket\" ? rule.interval : rule.window);\n};\n\n/**\n * This function gives a `retryAfter` in whole seconds, rounded up, as the header `Retry-After`\n * (RFC 9110) has it. A time over a whole second by less than one nanosecond rounds down to it,\n * because that part is an error of floating point.\n */\nexport const retryAfterSeconds = (retryAfter: Quantity): number => {\n const seconds = r.toNumber(convertExact(r.fromNumber(retryAfter.value), retryAfter.unit, \"s\"));\n return Math.max(0, Math.ceil(seconds - TOLERANCE / 1000));\n};\n\n/** This function gives the time at which a key state is equal to the state of a new key, in milliseconds. */\nexport const freeAt = (rule: LimitRule, state: KeyState): number => {\n if (state.type === \"token-bucket\") return state.full;\n const last = state.stamps[state.stamps.length - 1];\n return last === undefined ? Number.NEGATIVE_INFINITY : last + (rule.type === \"sliding-window\" ? rule.window : 0);\n};\n\n/**\n * This function decides one take of a key at the time `now`, in milliseconds, from the state of the\n * key (`undefined` for a new key). It is the one decision of each store in this process. A store in\n * another process does the same arithmetic in its own atomic step.\n * - A token bucket (GCRA): the store lets the take in when, with the take, the bucket is full\n * again in at most `burst` intervals. A refusal waits until one token is back.\n * - A sliding window: the store lets the take in when fewer than `limit` takes are in the window.\n * A refusal waits until the oldest take leaves the window.\n */\nexport const decide = (rule: LimitRule, state: KeyState | undefined, now: number): TakeStep => {\n if (rule.type === \"token-bucket\") {\n const span = rule.interval * rule.burst;\n const full = state?.type === \"token-bucket\" ? Math.max(state.full, now) : now;\n const next = full + rule.interval;\n if (next - now > span + TOLERANCE)\n return { decision: { allowed: false, retryAfter: ms(next - span - now), cause: \"limit\" }, state: { type: \"token-bucket\", full } };\n const remaining = Math.max(0, Math.floor((span - (next - now)) / rule.interval + TOLERANCE));\n return { decision: { allowed: true, remaining }, state: { type: \"token-bucket\", full: next } };\n }\n const held = state?.type === \"sliding-window\" ? state.stamps : [];\n // a take counts for one window from its time: it leaves the window at stamp + window\n const live = held.filter((stamp) => stamp + rule.window > now + TOLERANCE);\n if (live.length >= rule.limit)\n return { decision: { allowed: false, retryAfter: ms(live[0]! + rule.window - now), cause: \"limit\" }, state: { type: \"sliding-window\", stamps: live } };\n return { decision: { allowed: true, remaining: rule.limit - live.length - 1 }, state: { type: \"sliding-window\", stamps: [...live, now] } };\n};\n"],"mappings":"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"}
|