-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.ts
159 lines (132 loc) · 4.57 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
/*!
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
// Common validation shorthand
const validate = (message: string, condition: unknown) => {
if (!condition) {
throw RangeError(message);
}
};
// Validate capacity metrics and translate them into flow/burst pairs
const validateCapacity = ({ min, max, window }: Capacity): [number, number] => {
validate(
'All capacity parameters must be greater than zero',
min > 0 && window > 0
);
validate(
'Maximum capacity must be greater than minimum capacity',
max > min
);
return [min / window, max - min];
};
// Validate rate metrics and translate them into flow/burst pairs
const validateRate = ({ flow, burst }: Rate): [number, number] => {
validate(
'All rate parameters must be greater than zero',
flow > 0 && burst > 0
);
return [flow, burst];
};
// Validate and sort flow/burst pairs and translate them to script parameters
const validateLimits = (...input: number[][]): number[] => {
validate(
'At least one rate or capacity metric must be specified',
input.length
);
// Sort rates by the slowest to fastest flow for consistency, or by burst
// if flow is the same (to make them easier to filter out later)
const limits = input.sort(
([flow1, burst1], [flow2, burst2]) => flow1 - flow2 || burst1 - burst2
);
// Any limit that is strictly larger than another (in both flow and burst)
// is superfluous, as the smaller limit will always be more restrictive
return limits.reduce((params, [flow, burst]) =>
burst < params[params.length - 1] ? [...params, flow, burst] : params
);
};
/**
* Create a new rate-limiter test function
* @param config Configuration options
*/
export function create({
eval: code,
evalsha: hash,
prefix = '',
backoff = x => x,
capacity = [],
rate = [],
}: Config): Test {
// Precalculate parameters not dependent on test arguments
const params = validateLimits(
...([] as Capacity[]).concat(capacity).map(validateCapacity),
...([] as Rate[]).concat(rate).map(validateRate)
);
// Execute the Lua script on the Redis client to check available capacity
return async (key, cost = 1) => {
// Translate function arguments to Redis arguments
const keys = [prefix + key];
const argv = [cost, ...params];
// Evaluate the script in the Redis cache
const [allow, value, index] =
(await hash?.('{{LUA_HASH}}', keys, argv).catch(err => {
if (!/NOSCRIPT/.test(err)) {
throw err;
}
})) || (await code('{{LUA_CODE}}', keys, argv));
// Translate the Redis response into a result object
return {
allow: allow == 1,
free: allow * value,
wait:
1 - allow &&
(cost / params[2 * index - 2]) * backoff(value / cost),
};
};
}
/** Rate-limiter instance configuration options */
export interface Config {
/** EVAL call to Redis */
eval(script: string, keys: string[], argv: unknown[]): Promise<any>;
/** EVALSHA call to Redis */
evalsha?(hash: string, keys: string[], argv: unknown[]): Promise<any>;
/** Prefix all keys with this value (default empty) */
prefix?: string;
/** Backoff scaling function (default linear) */
backoff?(denied: number): number;
/** Capacity metric(s) to limit by */
capacity?: Capacity | Capacity[];
/** Rate metric(s) to limit by */
rate?: Rate | Rate[];
}
/** A rate metric to limit by */
export interface Rate {
/** The rate at which capacity is restored (per second) */
flow: number;
/** The allowed capacity before requests start to be denied */
burst: number;
}
/** A capacity metric to limit by */
export interface Capacity {
/** Time window over which to apply this capacity (in seconds) */
window: number;
/** Minimum guaranteed capacity */
min: number;
/** Maximum tolerable capacity */
max: number;
}
/** Result type for a rate-limited action */
export interface Result {
/** Whether to allow this action */
allow: boolean;
/** Remaining free capacity */
free: number;
/** Wait this long before trying again (in seconds) */
wait: number;
}
/**
* Rate-limiter test execution function
* @param key The key against which this rate should be tested
* @param cost (default 1) The abstract cost of this operation
*/
export type Test = (key: string, cost?: number) => Promise<Result>;