erlc-v2 1.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +17 -0
- package/README.md +286 -0
- package/index.d.ts +195 -0
- package/index.js +1 -0
- package/index.mjs +14 -0
- package/package.json +36 -0
- package/src/Client.js +353 -0
- package/src/cache/Cache.js +55 -0
- package/src/errors/ERLCAPIError.js +11 -0
- package/src/errors/ERLCError.js +9 -0
- package/src/errors/ERLCHttpError.js +13 -0
- package/src/errors/InvalidGlobalKeyError.js +5 -0
- package/src/errors/KeyBannedError.js +5 -0
- package/src/errors/KeyExpiredError.js +5 -0
- package/src/errors/ModuleOutOfDateError.js +5 -0
- package/src/errors/RateLimitError.js +11 -0
- package/src/errors/RestrictedError.js +5 -0
- package/src/errors/ServerOfflineError.js +5 -0
- package/src/errors/index.js +12 -0
- package/src/events/Poller.js +312 -0
- package/src/events/diff.js +119 -0
- package/src/index.js +7 -0
- package/src/rest/RateLimiter.js +163 -0
- package/src/rest/RequestManager.js +306 -0
- package/src/util/constants.js +55 -0
- package/src/util/errors.js +79 -0
- package/src/util/logger.js +39 -0
- package/src/util/normalize.js +25 -0
- package/src/util/options.js +25 -0
- package/src/util/stableStringify.js +22 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
const {
|
|
2
|
+
diffByKey,
|
|
3
|
+
getPlayerKey,
|
|
4
|
+
getVehicleKey,
|
|
5
|
+
areEqual,
|
|
6
|
+
buildLogSignature,
|
|
7
|
+
parseLogCommand,
|
|
8
|
+
} = require("./diff");
|
|
9
|
+
|
|
10
|
+
class SeenSignatures {
|
|
11
|
+
constructor({ ttlMs = 0, maxEntries = 5000 } = {}) {
|
|
12
|
+
this.ttlMs = ttlMs;
|
|
13
|
+
this.maxEntries = maxEntries;
|
|
14
|
+
this.map = new Map();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
_prune() {
|
|
18
|
+
if (this.ttlMs > 0) {
|
|
19
|
+
const now = Date.now();
|
|
20
|
+
for (const [key, expiresAt] of this.map.entries()) {
|
|
21
|
+
if (expiresAt <= now) this.map.delete(key);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
while (this.map.size > this.maxEntries) {
|
|
25
|
+
const oldest = this.map.keys().next().value;
|
|
26
|
+
this.map.delete(oldest);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
seed(signature) {
|
|
31
|
+
this._prune();
|
|
32
|
+
this.map.set(signature, this.ttlMs > 0 ? Date.now() + this.ttlMs : 0);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
checkAndAdd(signature) {
|
|
36
|
+
this._prune();
|
|
37
|
+
if (this.map.has(signature)) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
this.map.set(signature, this.ttlMs > 0 ? Date.now() + this.ttlMs : 0);
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
class Poller {
|
|
46
|
+
constructor(client, options = {}) {
|
|
47
|
+
this.client = client;
|
|
48
|
+
this.intervalMs = Math.max(250, options.intervalMs ?? 2500);
|
|
49
|
+
this.enabled = options.enabled !== false;
|
|
50
|
+
this.bypassCache = options.bypassCache !== false;
|
|
51
|
+
this.running = false;
|
|
52
|
+
this.inFlight = false;
|
|
53
|
+
this.timer = null;
|
|
54
|
+
this.readyEmitted = false;
|
|
55
|
+
this.previous = null;
|
|
56
|
+
this.seen = {
|
|
57
|
+
kills: new SeenSignatures(),
|
|
58
|
+
joins: new SeenSignatures(),
|
|
59
|
+
commands: new SeenSignatures(),
|
|
60
|
+
modCalls: new SeenSignatures(),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
start() {
|
|
65
|
+
if (!this.enabled || this.running) return;
|
|
66
|
+
this.running = true;
|
|
67
|
+
this._schedule(0);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
stop() {
|
|
71
|
+
this.running = false;
|
|
72
|
+
if (this.timer) {
|
|
73
|
+
clearTimeout(this.timer);
|
|
74
|
+
this.timer = null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
destroy() {
|
|
79
|
+
this.stop();
|
|
80
|
+
this.previous = null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
_schedule(waitMs) {
|
|
84
|
+
if (!this.running) return;
|
|
85
|
+
if (this.timer) clearTimeout(this.timer);
|
|
86
|
+
this.timer = setTimeout(() => {
|
|
87
|
+
this.timer = null;
|
|
88
|
+
void this._tick();
|
|
89
|
+
}, waitMs);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
_primeSeen(snapshot) {
|
|
93
|
+
for (const item of snapshot.killLogs)
|
|
94
|
+
this.seen.kills.seed(buildLogSignature("kills", item));
|
|
95
|
+
for (const item of snapshot.joinLogs)
|
|
96
|
+
this.seen.joins.seed(buildLogSignature("joins", item));
|
|
97
|
+
for (const item of snapshot.commandLogs)
|
|
98
|
+
this.seen.commands.seed(buildLogSignature("commands", item));
|
|
99
|
+
for (const item of snapshot.modCalls)
|
|
100
|
+
this.seen.modCalls.seed(buildLogSignature("modCalls", item));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
_emitServerUpdate(current) {
|
|
104
|
+
const prevSummary = this.previous
|
|
105
|
+
? {
|
|
106
|
+
name: this.previous.name,
|
|
107
|
+
ownerId: this.previous.ownerId,
|
|
108
|
+
currentPlayers: this.previous.currentPlayers,
|
|
109
|
+
maxPlayers: this.previous.maxPlayers,
|
|
110
|
+
teamBalance: this.previous.teamBalance,
|
|
111
|
+
accVerifiedReq: this.previous.accVerifiedReq,
|
|
112
|
+
}
|
|
113
|
+
: null;
|
|
114
|
+
const currentSummary = {
|
|
115
|
+
name: current.name,
|
|
116
|
+
ownerId: current.ownerId,
|
|
117
|
+
currentPlayers: current.currentPlayers,
|
|
118
|
+
maxPlayers: current.maxPlayers,
|
|
119
|
+
teamBalance: current.teamBalance,
|
|
120
|
+
accVerifiedReq: current.accVerifiedReq,
|
|
121
|
+
};
|
|
122
|
+
if (!prevSummary || !areEqual(prevSummary, currentSummary)) {
|
|
123
|
+
this.client.emit("serverUpdate", {
|
|
124
|
+
previous: prevSummary,
|
|
125
|
+
current: currentSummary,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
_emitPlayerEvents(previous, current) {
|
|
131
|
+
const hasPlayersData = Array.isArray(current.raw?.Players);
|
|
132
|
+
if (hasPlayersData) {
|
|
133
|
+
const diff = diffByKey(previous.players, current.players, getPlayerKey);
|
|
134
|
+
for (const player of diff.added) {
|
|
135
|
+
this.client.emit("playerJoin", {
|
|
136
|
+
player,
|
|
137
|
+
source: "players",
|
|
138
|
+
snapshot: current,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
for (const player of diff.removed) {
|
|
142
|
+
this.client.emit("playerLeave", {
|
|
143
|
+
player,
|
|
144
|
+
source: "players",
|
|
145
|
+
snapshot: current,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
for (const entry of current.joinLogs) {
|
|
152
|
+
const signature = buildLogSignature("joins", entry);
|
|
153
|
+
if (!this.seen.joins.checkAndAdd(signature)) continue;
|
|
154
|
+
if (entry?.Join === true) {
|
|
155
|
+
this.client.emit("playerJoin", {
|
|
156
|
+
player: entry.Player,
|
|
157
|
+
log: entry,
|
|
158
|
+
source: "joinLogs",
|
|
159
|
+
snapshot: current,
|
|
160
|
+
});
|
|
161
|
+
} else if (entry?.Join === false) {
|
|
162
|
+
this.client.emit("playerLeave", {
|
|
163
|
+
player: entry.Player,
|
|
164
|
+
log: entry,
|
|
165
|
+
source: "joinLogs",
|
|
166
|
+
snapshot: current,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
_emitVehicleEvents(previous, current) {
|
|
173
|
+
const diff = diffByKey(previous.vehicles, current.vehicles, getVehicleKey);
|
|
174
|
+
for (const vehicle of diff.added) {
|
|
175
|
+
this.client.emit("vehicleSpawn", {
|
|
176
|
+
vehicle,
|
|
177
|
+
snapshot: current,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
for (const vehicle of diff.removed) {
|
|
181
|
+
this.client.emit("vehicleDespawn", {
|
|
182
|
+
vehicle,
|
|
183
|
+
snapshot: current,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
_emitQueueAndStaff(previous, current) {
|
|
189
|
+
if (!areEqual(previous.queue, current.queue)) {
|
|
190
|
+
this.client.emit("queueUpdate", {
|
|
191
|
+
previous: previous.queue,
|
|
192
|
+
current: current.queue,
|
|
193
|
+
snapshot: current,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
if (!areEqual(previous.staff, current.staff)) {
|
|
197
|
+
this.client.emit("staffUpdate", {
|
|
198
|
+
previous: previous.staff,
|
|
199
|
+
current: current.staff,
|
|
200
|
+
snapshot: current,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
_emitLogEvents(current) {
|
|
206
|
+
for (const kill of current.killLogs) {
|
|
207
|
+
const signature = buildLogSignature("kills", kill);
|
|
208
|
+
if (!this.seen.kills.checkAndAdd(signature)) continue;
|
|
209
|
+
this.client.emit("kill", {
|
|
210
|
+
kill,
|
|
211
|
+
signature,
|
|
212
|
+
snapshot: current,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
for (const modCall of current.modCalls) {
|
|
217
|
+
const signature = buildLogSignature("modCalls", modCall);
|
|
218
|
+
if (!this.seen.modCalls.checkAndAdd(signature)) continue;
|
|
219
|
+
this.client.emit("modCall", {
|
|
220
|
+
modCall,
|
|
221
|
+
signature,
|
|
222
|
+
snapshot: current,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
for (const command of current.commandLogs) {
|
|
227
|
+
const signature = buildLogSignature("commands", command);
|
|
228
|
+
if (!this.seen.commands.checkAndAdd(signature)) continue;
|
|
229
|
+
const payload = {
|
|
230
|
+
command,
|
|
231
|
+
signature,
|
|
232
|
+
snapshot: current,
|
|
233
|
+
};
|
|
234
|
+
this.client.emit("commandLog", payload);
|
|
235
|
+
|
|
236
|
+
const parsedLogCommand = parseLogCommand(command);
|
|
237
|
+
if (parsedLogCommand) {
|
|
238
|
+
this.client.emit("logCommand", {
|
|
239
|
+
...payload,
|
|
240
|
+
parsed: parsedLogCommand,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
_handleSnapshot(current) {
|
|
247
|
+
if (!this.previous) {
|
|
248
|
+
this._primeSeen(current);
|
|
249
|
+
this.previous = current;
|
|
250
|
+
if (!this.readyEmitted) {
|
|
251
|
+
this.readyEmitted = true;
|
|
252
|
+
this.client.emit("ready");
|
|
253
|
+
}
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
this._emitServerUpdate(current);
|
|
258
|
+
this._emitPlayerEvents(this.previous, current);
|
|
259
|
+
this._emitVehicleEvents(this.previous, current);
|
|
260
|
+
this._emitQueueAndStaff(this.previous, current);
|
|
261
|
+
this._emitLogEvents(current);
|
|
262
|
+
this.previous = current;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async _tick() {
|
|
266
|
+
if (!this.running || this.inFlight) return;
|
|
267
|
+
this.inFlight = true;
|
|
268
|
+
this.client.logger.debug({
|
|
269
|
+
msg: "poll_cycle_start",
|
|
270
|
+
intervalMs: this.intervalMs,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
const snapshot = await this.client.server.fetch(
|
|
275
|
+
{
|
|
276
|
+
players: true,
|
|
277
|
+
staff: true,
|
|
278
|
+
joinLogs: true,
|
|
279
|
+
queue: true,
|
|
280
|
+
killLogs: true,
|
|
281
|
+
commandLogs: true,
|
|
282
|
+
modCalls: true,
|
|
283
|
+
vehicles: true,
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
bypassCache: this.bypassCache,
|
|
287
|
+
dedupe: false,
|
|
288
|
+
},
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
this._handleSnapshot(snapshot);
|
|
292
|
+
} catch (error) {
|
|
293
|
+
if (this.running) {
|
|
294
|
+
if (this.client.listenerCount("error") > 0) {
|
|
295
|
+
this.client.emit("error", error);
|
|
296
|
+
} else {
|
|
297
|
+
this.client.logger.error({
|
|
298
|
+
msg: "poll_cycle_error",
|
|
299
|
+
error: error?.message,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
} finally {
|
|
304
|
+
this.inFlight = false;
|
|
305
|
+
if (this.running) {
|
|
306
|
+
this._schedule(this.intervalMs);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
module.exports = Poller;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const { stableStringify } = require("../util/stableStringify");
|
|
2
|
+
|
|
3
|
+
function getPlayerKey(player) {
|
|
4
|
+
if (!player) return "";
|
|
5
|
+
if (typeof player === "string") return player;
|
|
6
|
+
return String(player.Player ?? player.Name ?? "");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function getVehicleKey(vehicle) {
|
|
10
|
+
if (!vehicle) return "";
|
|
11
|
+
const name = vehicle.Name ?? "";
|
|
12
|
+
const owner = vehicle.Owner ?? "";
|
|
13
|
+
const texture = vehicle.Texture ?? "";
|
|
14
|
+
const colorHex = vehicle.ColorHex ?? "";
|
|
15
|
+
const colorName = vehicle.ColorName ?? "";
|
|
16
|
+
return `${name}|${owner}|${texture}|${colorHex}|${colorName}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function diffByKey(previous, current, keyFn) {
|
|
20
|
+
const prevList = Array.isArray(previous) ? previous : [];
|
|
21
|
+
const currList = Array.isArray(current) ? current : [];
|
|
22
|
+
|
|
23
|
+
const prevMap = new Map();
|
|
24
|
+
for (const item of prevList) {
|
|
25
|
+
prevMap.set(keyFn(item), item);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const currMap = new Map();
|
|
29
|
+
for (const item of currList) {
|
|
30
|
+
currMap.set(keyFn(item), item);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const added = [];
|
|
34
|
+
const removed = [];
|
|
35
|
+
|
|
36
|
+
for (const [key, value] of currMap.entries()) {
|
|
37
|
+
if (!prevMap.has(key)) added.push(value);
|
|
38
|
+
}
|
|
39
|
+
for (const [key, value] of prevMap.entries()) {
|
|
40
|
+
if (!currMap.has(key)) removed.push(value);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return { added, removed };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function areEqual(a, b) {
|
|
47
|
+
return stableStringify(a) === stableStringify(b);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function buildLogSignature(type, item) {
|
|
51
|
+
switch (type) {
|
|
52
|
+
case "kills":
|
|
53
|
+
return stableStringify({
|
|
54
|
+
t: item?.Timestamp ?? null,
|
|
55
|
+
killer: item?.Killer ?? "",
|
|
56
|
+
killed: item?.Killed ?? "",
|
|
57
|
+
weapon: item?.Weapon ?? "",
|
|
58
|
+
});
|
|
59
|
+
case "joins":
|
|
60
|
+
return stableStringify({
|
|
61
|
+
t: item?.Timestamp ?? null,
|
|
62
|
+
player: item?.Player ?? "",
|
|
63
|
+
join: item?.Join ?? null,
|
|
64
|
+
});
|
|
65
|
+
case "commands":
|
|
66
|
+
return stableStringify({
|
|
67
|
+
t: item?.Timestamp ?? null,
|
|
68
|
+
player: item?.Player ?? "",
|
|
69
|
+
command: item?.Command ?? "",
|
|
70
|
+
});
|
|
71
|
+
case "modCalls":
|
|
72
|
+
return stableStringify({
|
|
73
|
+
t: item?.Timestamp ?? null,
|
|
74
|
+
caller: item?.Caller ?? "",
|
|
75
|
+
moderator: item?.Moderator ?? "",
|
|
76
|
+
});
|
|
77
|
+
default:
|
|
78
|
+
return stableStringify(item);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function parseCommand(commandLogEntry) {
|
|
83
|
+
const raw = String(commandLogEntry?.Command ?? "").trim();
|
|
84
|
+
if (!raw) return null;
|
|
85
|
+
|
|
86
|
+
const segments = raw.split(/\s+/);
|
|
87
|
+
const command = String(segments[0] ?? "").toLowerCase();
|
|
88
|
+
const args = segments.slice(1).join(" ");
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
raw,
|
|
92
|
+
command,
|
|
93
|
+
args,
|
|
94
|
+
segments,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function parseLogCommand(commandLogEntry) {
|
|
99
|
+
const parsed = parseCommand(commandLogEntry);
|
|
100
|
+
if (!parsed || parsed.command !== ":log") return null;
|
|
101
|
+
|
|
102
|
+
const segments = parsed.segments.slice(1);
|
|
103
|
+
return {
|
|
104
|
+
raw: parsed.raw,
|
|
105
|
+
keyword: segments[0] ?? "",
|
|
106
|
+
args: segments.slice(1).join(" "),
|
|
107
|
+
segments,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = {
|
|
112
|
+
getPlayerKey,
|
|
113
|
+
getVehicleKey,
|
|
114
|
+
diffByKey,
|
|
115
|
+
areEqual,
|
|
116
|
+
buildLogSignature,
|
|
117
|
+
parseCommand,
|
|
118
|
+
parseLogCommand,
|
|
119
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
class RateLimiter {
|
|
2
|
+
constructor(options = {}, logger) {
|
|
3
|
+
this.enabled = options.enabled !== false;
|
|
4
|
+
this.strictSerial = options.strictSerial !== false;
|
|
5
|
+
this.perBucketConcurrency = this.strictSerial
|
|
6
|
+
? 1
|
|
7
|
+
: Math.max(1, options.perBucketConcurrency ?? 1);
|
|
8
|
+
this.globalConcurrency = this.strictSerial
|
|
9
|
+
? 1
|
|
10
|
+
: Math.max(1, options.globalConcurrency ?? 4);
|
|
11
|
+
this.logger = logger;
|
|
12
|
+
this.buckets = new Map();
|
|
13
|
+
this.globalActive = 0;
|
|
14
|
+
this.destroyed = false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
_createState(bucketId) {
|
|
18
|
+
return {
|
|
19
|
+
id: bucketId,
|
|
20
|
+
limit: Infinity,
|
|
21
|
+
remaining: Infinity,
|
|
22
|
+
resetEpochMs: 0,
|
|
23
|
+
blockedUntilMs: 0,
|
|
24
|
+
active: 0,
|
|
25
|
+
queue: [],
|
|
26
|
+
timer: null,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
_getState(bucketId) {
|
|
31
|
+
if (!this.buckets.has(bucketId)) {
|
|
32
|
+
this.buckets.set(bucketId, this._createState(bucketId));
|
|
33
|
+
}
|
|
34
|
+
return this.buckets.get(bucketId);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
_clearTimer(state) {
|
|
38
|
+
if (state.timer) {
|
|
39
|
+
clearTimeout(state.timer);
|
|
40
|
+
state.timer = null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
_scheduleTimer(state, waitMs) {
|
|
45
|
+
this._clearTimer(state);
|
|
46
|
+
state.timer = setTimeout(() => {
|
|
47
|
+
state.timer = null;
|
|
48
|
+
this._drain(state.id);
|
|
49
|
+
}, waitMs);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
_canRun(state) {
|
|
53
|
+
if (!this.enabled) return true;
|
|
54
|
+
if (this.destroyed) return false;
|
|
55
|
+
if (state.active >= this.perBucketConcurrency) return false;
|
|
56
|
+
if (this.globalActive >= this.globalConcurrency) return false;
|
|
57
|
+
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
const resetBlock =
|
|
60
|
+
Number.isFinite(state.remaining) && state.remaining <= 0
|
|
61
|
+
? state.resetEpochMs
|
|
62
|
+
: 0;
|
|
63
|
+
const blockedUntil = Math.max(state.blockedUntilMs || 0, resetBlock || 0);
|
|
64
|
+
if (blockedUntil > now) {
|
|
65
|
+
this.logger.debug({
|
|
66
|
+
msg: "rate_limit_wait",
|
|
67
|
+
bucket: state.id,
|
|
68
|
+
waitMs: blockedUntil - now,
|
|
69
|
+
});
|
|
70
|
+
this._scheduleTimer(state, Math.max(10, blockedUntil - now));
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
_drain(bucketId) {
|
|
77
|
+
const state = this._getState(bucketId);
|
|
78
|
+
if (!state.queue.length) return;
|
|
79
|
+
if (!this._canRun(state)) return;
|
|
80
|
+
|
|
81
|
+
const job = state.queue.shift();
|
|
82
|
+
state.active += 1;
|
|
83
|
+
this.globalActive += 1;
|
|
84
|
+
|
|
85
|
+
Promise.resolve()
|
|
86
|
+
.then(job.run)
|
|
87
|
+
.then(job.resolve, job.reject)
|
|
88
|
+
.finally(() => {
|
|
89
|
+
state.active -= 1;
|
|
90
|
+
this.globalActive -= 1;
|
|
91
|
+
this._drain(bucketId);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
schedule(bucketId, run) {
|
|
96
|
+
if (this.destroyed) {
|
|
97
|
+
return Promise.reject(new Error("RateLimiter destroyed"));
|
|
98
|
+
}
|
|
99
|
+
const state = this._getState(bucketId || "global");
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
state.queue.push({ run, resolve, reject });
|
|
102
|
+
this._drain(state.id);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
update(bucketId, headers = {}) {
|
|
107
|
+
const state = this._getState(bucketId || "global");
|
|
108
|
+
const limit = headers.limit != null ? Number(headers.limit) : Number.NaN;
|
|
109
|
+
const remaining =
|
|
110
|
+
headers.remaining != null ? Number(headers.remaining) : Number.NaN;
|
|
111
|
+
const resetEpochMs =
|
|
112
|
+
headers.resetEpochMs != null ? Number(headers.resetEpochMs) : Number.NaN;
|
|
113
|
+
|
|
114
|
+
if (!Number.isNaN(limit) && Number.isFinite(limit)) state.limit = limit;
|
|
115
|
+
if (!Number.isNaN(remaining) && Number.isFinite(remaining))
|
|
116
|
+
state.remaining = remaining;
|
|
117
|
+
if (!Number.isNaN(resetEpochMs) && Number.isFinite(resetEpochMs))
|
|
118
|
+
state.resetEpochMs = resetEpochMs;
|
|
119
|
+
|
|
120
|
+
if (state.remaining <= 0 && state.resetEpochMs > Date.now()) {
|
|
121
|
+
state.blockedUntilMs = Math.max(
|
|
122
|
+
state.blockedUntilMs || 0,
|
|
123
|
+
state.resetEpochMs,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
this._drain(state.id);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
block(bucketId, untilMs) {
|
|
130
|
+
const state = this._getState(bucketId || "global");
|
|
131
|
+
state.blockedUntilMs = Math.max(state.blockedUntilMs || 0, untilMs);
|
|
132
|
+
if (state.blockedUntilMs > Date.now()) {
|
|
133
|
+
const waitMs = state.blockedUntilMs - Date.now();
|
|
134
|
+
this.logger.debug({
|
|
135
|
+
msg: "rate_limit_block",
|
|
136
|
+
bucket: state.id,
|
|
137
|
+
waitMs,
|
|
138
|
+
});
|
|
139
|
+
this._scheduleTimer(state, Math.max(10, waitMs));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
clearBucket(bucketId) {
|
|
144
|
+
const state = this.buckets.get(bucketId);
|
|
145
|
+
if (!state) return;
|
|
146
|
+
this._clearTimer(state);
|
|
147
|
+
this.buckets.delete(bucketId);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
destroy(error) {
|
|
151
|
+
this.destroyed = true;
|
|
152
|
+
for (const state of this.buckets.values()) {
|
|
153
|
+
this._clearTimer(state);
|
|
154
|
+
while (state.queue.length) {
|
|
155
|
+
const job = state.queue.shift();
|
|
156
|
+
job.reject(error || new Error("RateLimiter destroyed"));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
this.buckets.clear();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
module.exports = RateLimiter;
|