@http-client-toolkit/store-memory 0.0.1
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 +15 -0
- package/README.md +103 -0
- package/lib/index.cjs +761 -0
- package/lib/index.cjs.map +1 -0
- package/lib/index.d.cts +223 -0
- package/lib/index.d.ts +223 -0
- package/lib/index.js +752 -0
- package/lib/index.js.map +1 -0
- package/package.json +78 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
import safeStringify from 'fast-safe-stringify';
|
|
2
|
+
import { randomUUID } from 'crypto';
|
|
3
|
+
import { DEFAULT_RATE_LIMIT, AdaptiveCapacityCalculator } from '@http-client-toolkit/core';
|
|
4
|
+
|
|
5
|
+
var __async = (__this, __arguments, generator) => {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
var fulfilled = (value) => {
|
|
8
|
+
try {
|
|
9
|
+
step(generator.next(value));
|
|
10
|
+
} catch (e) {
|
|
11
|
+
reject(e);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
var rejected = (value) => {
|
|
15
|
+
try {
|
|
16
|
+
step(generator.throw(value));
|
|
17
|
+
} catch (e) {
|
|
18
|
+
reject(e);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
|
|
22
|
+
step((generator = generator.apply(__this, __arguments)).next());
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
var InMemoryCacheStore = class {
|
|
26
|
+
// running total of memory usage in bytes
|
|
27
|
+
constructor(options = {}) {
|
|
28
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
29
|
+
this.totalSize = 0;
|
|
30
|
+
var _a, _b, _c, _d;
|
|
31
|
+
const cleanupIntervalMs = (_a = options.cleanupIntervalMs) != null ? _a : 6e4;
|
|
32
|
+
this.maxItems = (_b = options.maxItems) != null ? _b : 1e3;
|
|
33
|
+
this.maxMemoryBytes = (_c = options.maxMemoryBytes) != null ? _c : 50 * 1024 * 1024;
|
|
34
|
+
this.evictionRatio = (_d = options.evictionRatio) != null ? _d : 0.1;
|
|
35
|
+
if (cleanupIntervalMs > 0) {
|
|
36
|
+
this.cleanupInterval = setInterval(() => {
|
|
37
|
+
this.cleanup();
|
|
38
|
+
}, cleanupIntervalMs);
|
|
39
|
+
if (typeof this.cleanupInterval.unref === "function") {
|
|
40
|
+
this.cleanupInterval.unref();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
get(hash) {
|
|
45
|
+
return __async(this, null, function* () {
|
|
46
|
+
const item = this.cache.get(hash);
|
|
47
|
+
if (!item) {
|
|
48
|
+
return void 0;
|
|
49
|
+
}
|
|
50
|
+
if (item.expiresAt > 0 && Date.now() > item.expiresAt) {
|
|
51
|
+
this.totalSize -= item.size;
|
|
52
|
+
this.cache.delete(hash);
|
|
53
|
+
return void 0;
|
|
54
|
+
}
|
|
55
|
+
item.lastAccessed = Date.now();
|
|
56
|
+
this.cache.set(hash, item);
|
|
57
|
+
return item.value;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
set(hash, value, ttlSeconds) {
|
|
61
|
+
return __async(this, null, function* () {
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
const expiresAt = ttlSeconds === 0 ? 0 : now + ttlSeconds * 1e3;
|
|
64
|
+
const existing = this.cache.get(hash);
|
|
65
|
+
if (existing) {
|
|
66
|
+
this.totalSize -= existing.size;
|
|
67
|
+
}
|
|
68
|
+
const entrySize = this.estimateEntrySize(hash, value);
|
|
69
|
+
this.totalSize += entrySize;
|
|
70
|
+
this.cache.set(hash, {
|
|
71
|
+
value,
|
|
72
|
+
expiresAt,
|
|
73
|
+
lastAccessed: now,
|
|
74
|
+
size: entrySize
|
|
75
|
+
});
|
|
76
|
+
this.enforceMemoryLimits();
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
delete(hash) {
|
|
80
|
+
return __async(this, null, function* () {
|
|
81
|
+
const existing = this.cache.get(hash);
|
|
82
|
+
if (existing) {
|
|
83
|
+
this.totalSize -= existing.size;
|
|
84
|
+
}
|
|
85
|
+
this.cache.delete(hash);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
clear() {
|
|
89
|
+
return __async(this, null, function* () {
|
|
90
|
+
this.cache.clear();
|
|
91
|
+
this.totalSize = 0;
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Get statistics about cache usage
|
|
96
|
+
*/
|
|
97
|
+
getStats() {
|
|
98
|
+
const now = Date.now();
|
|
99
|
+
let expired = 0;
|
|
100
|
+
for (const [_hash, item] of this.cache) {
|
|
101
|
+
if (item.expiresAt > 0 && now > item.expiresAt) {
|
|
102
|
+
expired++;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const memoryUsageBytes = this.calculateMemoryUsage();
|
|
106
|
+
return {
|
|
107
|
+
totalItems: this.cache.size,
|
|
108
|
+
expired,
|
|
109
|
+
memoryUsageBytes,
|
|
110
|
+
maxItems: this.maxItems,
|
|
111
|
+
maxMemoryBytes: this.maxMemoryBytes,
|
|
112
|
+
memoryUtilization: memoryUsageBytes / this.maxMemoryBytes,
|
|
113
|
+
itemUtilization: this.cache.size / this.maxItems
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Clean up expired cache entries
|
|
118
|
+
*/
|
|
119
|
+
cleanup() {
|
|
120
|
+
const now = Date.now();
|
|
121
|
+
const toDelete = [];
|
|
122
|
+
for (const [hash, item] of this.cache) {
|
|
123
|
+
if (item.expiresAt > 0 && now > item.expiresAt) {
|
|
124
|
+
toDelete.push(hash);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
for (const hash of toDelete) {
|
|
128
|
+
const item = this.cache.get(hash);
|
|
129
|
+
if (item) {
|
|
130
|
+
this.totalSize -= item.size;
|
|
131
|
+
}
|
|
132
|
+
this.cache.delete(hash);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Enforce memory and item count limits using LRU eviction
|
|
137
|
+
*/
|
|
138
|
+
enforceMemoryLimits() {
|
|
139
|
+
if (this.cache.size > this.maxItems) {
|
|
140
|
+
this.evictLRUItems(this.cache.size - this.maxItems);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const memoryUsage = this.totalSize;
|
|
144
|
+
if (memoryUsage > this.maxMemoryBytes) {
|
|
145
|
+
const itemsToEvict = Math.max(
|
|
146
|
+
1,
|
|
147
|
+
Math.floor(this.cache.size * this.evictionRatio)
|
|
148
|
+
);
|
|
149
|
+
this.evictLRUItems(itemsToEvict);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Evict the least recently used items
|
|
154
|
+
*/
|
|
155
|
+
evictLRUItems(count) {
|
|
156
|
+
const entries = Array.from(this.cache.entries()).sort(
|
|
157
|
+
([, a], [, b]) => a.lastAccessed - b.lastAccessed
|
|
158
|
+
);
|
|
159
|
+
for (let i = 0; i < count && i < entries.length; i++) {
|
|
160
|
+
const entry = entries[i];
|
|
161
|
+
if (entry) {
|
|
162
|
+
const [key, item] = entry;
|
|
163
|
+
this.totalSize -= item.size;
|
|
164
|
+
this.cache.delete(key);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Calculate rough memory usage
|
|
170
|
+
*/
|
|
171
|
+
calculateMemoryUsage() {
|
|
172
|
+
return this.totalSize;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Get items sorted by last accessed time (for debugging/monitoring)
|
|
176
|
+
*/
|
|
177
|
+
getLRUItems(limit = 10) {
|
|
178
|
+
const entries = Array.from(this.cache.entries()).sort(([, a], [, b]) => a.lastAccessed - b.lastAccessed).slice(0, limit);
|
|
179
|
+
return entries.map(([hash, item]) => ({
|
|
180
|
+
hash,
|
|
181
|
+
lastAccessed: new Date(item.lastAccessed),
|
|
182
|
+
size: item.size
|
|
183
|
+
}));
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Destroy the cache and cleanup resources
|
|
187
|
+
*/
|
|
188
|
+
destroy() {
|
|
189
|
+
if (this.cleanupInterval) {
|
|
190
|
+
clearInterval(this.cleanupInterval);
|
|
191
|
+
this.cleanupInterval = void 0;
|
|
192
|
+
}
|
|
193
|
+
this.cache.clear();
|
|
194
|
+
this.totalSize = 0;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Estimate the size in bytes of a cache entry (key + value + metadata)
|
|
198
|
+
*/
|
|
199
|
+
estimateEntrySize(hash, value) {
|
|
200
|
+
var _a;
|
|
201
|
+
let size = 0;
|
|
202
|
+
size += hash.length * 2;
|
|
203
|
+
size += 16;
|
|
204
|
+
try {
|
|
205
|
+
const json = (_a = safeStringify(value)) != null ? _a : "";
|
|
206
|
+
size += json.length * 2;
|
|
207
|
+
} catch (e) {
|
|
208
|
+
size += 1024;
|
|
209
|
+
}
|
|
210
|
+
return size;
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
function deferred() {
|
|
214
|
+
let resolve;
|
|
215
|
+
let reject;
|
|
216
|
+
const promise = new Promise((res, rej) => {
|
|
217
|
+
resolve = res;
|
|
218
|
+
reject = rej;
|
|
219
|
+
});
|
|
220
|
+
return { promise, resolve, reject };
|
|
221
|
+
}
|
|
222
|
+
var InMemoryDedupeStore = class {
|
|
223
|
+
constructor({
|
|
224
|
+
/** Job timeout in milliseconds. Defaults to 5 minutes. */
|
|
225
|
+
jobTimeoutMs = 3e5,
|
|
226
|
+
/** Cleanup interval in milliseconds. Defaults to 1 minute. */
|
|
227
|
+
cleanupIntervalMs = 6e4
|
|
228
|
+
} = {}) {
|
|
229
|
+
this.jobs = /* @__PURE__ */ new Map();
|
|
230
|
+
this.totalJobsProcessed = 0;
|
|
231
|
+
this.destroyed = false;
|
|
232
|
+
this.jobTimeoutMs = jobTimeoutMs;
|
|
233
|
+
if (cleanupIntervalMs > 0) {
|
|
234
|
+
this.cleanupInterval = setInterval(() => {
|
|
235
|
+
this.cleanup();
|
|
236
|
+
}, cleanupIntervalMs);
|
|
237
|
+
if (typeof this.cleanupInterval.unref === "function") {
|
|
238
|
+
this.cleanupInterval.unref();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
waitFor(hash) {
|
|
243
|
+
return __async(this, null, function* () {
|
|
244
|
+
var _a;
|
|
245
|
+
if (this.destroyed) {
|
|
246
|
+
return void 0;
|
|
247
|
+
}
|
|
248
|
+
const job = this.jobs.get(hash);
|
|
249
|
+
if (!job) {
|
|
250
|
+
return void 0;
|
|
251
|
+
}
|
|
252
|
+
const jobTimedOut = this.jobTimeoutMs > 0 && Date.now() - job.createdAt > this.jobTimeoutMs;
|
|
253
|
+
if (jobTimedOut) {
|
|
254
|
+
this.cleanup();
|
|
255
|
+
return void 0;
|
|
256
|
+
}
|
|
257
|
+
if (job.completed) {
|
|
258
|
+
if (job.error) {
|
|
259
|
+
return void 0;
|
|
260
|
+
}
|
|
261
|
+
return (_a = job.result) != null ? _a : void 0;
|
|
262
|
+
}
|
|
263
|
+
try {
|
|
264
|
+
return yield job.promise;
|
|
265
|
+
} catch (e) {
|
|
266
|
+
return void 0;
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
register(hash) {
|
|
271
|
+
return __async(this, null, function* () {
|
|
272
|
+
const registration = yield this.registerOrJoin(hash);
|
|
273
|
+
return registration.jobId;
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
registerOrJoin(hash) {
|
|
277
|
+
return __async(this, null, function* () {
|
|
278
|
+
const existingJob = this.jobs.get(hash);
|
|
279
|
+
if (existingJob) {
|
|
280
|
+
return {
|
|
281
|
+
jobId: existingJob.jobId,
|
|
282
|
+
isOwner: false
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
const jobId = randomUUID();
|
|
286
|
+
const { promise, resolve, reject } = deferred();
|
|
287
|
+
const job = {
|
|
288
|
+
jobId,
|
|
289
|
+
promise,
|
|
290
|
+
resolve,
|
|
291
|
+
reject,
|
|
292
|
+
createdAt: Date.now(),
|
|
293
|
+
completed: false
|
|
294
|
+
};
|
|
295
|
+
this.jobs.set(hash, job);
|
|
296
|
+
this.totalJobsProcessed++;
|
|
297
|
+
return {
|
|
298
|
+
jobId,
|
|
299
|
+
isOwner: true
|
|
300
|
+
};
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
complete(hash, value) {
|
|
304
|
+
return __async(this, null, function* () {
|
|
305
|
+
const job = this.jobs.get(hash);
|
|
306
|
+
if (job && !job.completed) {
|
|
307
|
+
job.completed = true;
|
|
308
|
+
job.result = value;
|
|
309
|
+
job.resolve(value);
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
fail(hash, error) {
|
|
314
|
+
return __async(this, null, function* () {
|
|
315
|
+
const job = this.jobs.get(hash);
|
|
316
|
+
if (job && !job.completed) {
|
|
317
|
+
job.completed = true;
|
|
318
|
+
job.error = error;
|
|
319
|
+
job.reject(error);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
isInProgress(hash) {
|
|
324
|
+
return __async(this, null, function* () {
|
|
325
|
+
const job = this.jobs.get(hash);
|
|
326
|
+
if (!job) {
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
const isJobExpired = this.jobTimeoutMs > 0 && Date.now() - job.createdAt > this.jobTimeoutMs;
|
|
330
|
+
if (isJobExpired) {
|
|
331
|
+
this.cleanup();
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
return !job.completed;
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Get statistics about current dedupe jobs
|
|
339
|
+
*/
|
|
340
|
+
getStats() {
|
|
341
|
+
const now = Date.now();
|
|
342
|
+
let expiredJobs = 0;
|
|
343
|
+
let oldestJobAgeMs = 0;
|
|
344
|
+
let activeJobs = 0;
|
|
345
|
+
for (const [_hash, job] of this.jobs) {
|
|
346
|
+
const ageMs = now - job.createdAt;
|
|
347
|
+
if (this.jobTimeoutMs > 0 && ageMs > this.jobTimeoutMs) {
|
|
348
|
+
expiredJobs++;
|
|
349
|
+
}
|
|
350
|
+
if (ageMs > oldestJobAgeMs) {
|
|
351
|
+
oldestJobAgeMs = ageMs;
|
|
352
|
+
}
|
|
353
|
+
if (!job.completed) {
|
|
354
|
+
activeJobs++;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return {
|
|
358
|
+
activeJobs,
|
|
359
|
+
totalJobsProcessed: this.totalJobsProcessed,
|
|
360
|
+
expiredJobs,
|
|
361
|
+
oldestJobAgeMs
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Clean up expired jobs
|
|
366
|
+
*/
|
|
367
|
+
cleanup() {
|
|
368
|
+
if (this.jobTimeoutMs <= 0) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
const now = Date.now();
|
|
372
|
+
const toDelete = [];
|
|
373
|
+
for (const [hash, job] of this.jobs) {
|
|
374
|
+
if (now - job.createdAt > this.jobTimeoutMs) {
|
|
375
|
+
if (!job.completed) {
|
|
376
|
+
job.completed = true;
|
|
377
|
+
job.error = new Error(
|
|
378
|
+
"Job timeout: Request took too long to complete"
|
|
379
|
+
);
|
|
380
|
+
job.reject(job.error);
|
|
381
|
+
}
|
|
382
|
+
toDelete.push(hash);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
for (const hash of toDelete) {
|
|
386
|
+
this.jobs.delete(hash);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Clear all jobs
|
|
391
|
+
*/
|
|
392
|
+
clear() {
|
|
393
|
+
for (const [_hash, job] of this.jobs) {
|
|
394
|
+
if (!job.completed) {
|
|
395
|
+
job.completed = true;
|
|
396
|
+
job.error = new Error("DedupeStore cleared");
|
|
397
|
+
job.reject(job.error);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
this.jobs.clear();
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Destroy the store and clean up resources
|
|
404
|
+
*/
|
|
405
|
+
destroy() {
|
|
406
|
+
if (this.cleanupInterval) {
|
|
407
|
+
clearInterval(this.cleanupInterval);
|
|
408
|
+
this.cleanupInterval = void 0;
|
|
409
|
+
}
|
|
410
|
+
this.clear();
|
|
411
|
+
this.destroyed = true;
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
var InMemoryRateLimitStore = class {
|
|
415
|
+
constructor({
|
|
416
|
+
/** Global/default rate-limit config applied when a resource-specific override is not provided. */
|
|
417
|
+
defaultConfig = DEFAULT_RATE_LIMIT,
|
|
418
|
+
/** Optional per-resource overrides. */
|
|
419
|
+
resourceConfigs = /* @__PURE__ */ new Map(),
|
|
420
|
+
/** Cleanup interval in milliseconds. Defaults to 1 minute. */
|
|
421
|
+
cleanupIntervalMs = 6e4
|
|
422
|
+
} = {}) {
|
|
423
|
+
this.limits = /* @__PURE__ */ new Map();
|
|
424
|
+
this.resourceConfigs = /* @__PURE__ */ new Map();
|
|
425
|
+
this.totalRequests = 0;
|
|
426
|
+
this.defaultConfig = defaultConfig;
|
|
427
|
+
this.resourceConfigs = resourceConfigs;
|
|
428
|
+
if (cleanupIntervalMs > 0) {
|
|
429
|
+
this.cleanupInterval = setInterval(() => {
|
|
430
|
+
this.cleanup();
|
|
431
|
+
}, cleanupIntervalMs);
|
|
432
|
+
if (typeof this.cleanupInterval.unref === "function") {
|
|
433
|
+
this.cleanupInterval.unref();
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
canProceed(resource) {
|
|
438
|
+
return __async(this, null, function* () {
|
|
439
|
+
const config = this.resourceConfigs.get(resource) || this.defaultConfig;
|
|
440
|
+
const info = this.getOrCreateRateLimitInfo(resource, config);
|
|
441
|
+
this.cleanupExpiredRequests(info);
|
|
442
|
+
return info.requests.length < info.limit;
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
record(resource) {
|
|
446
|
+
return __async(this, null, function* () {
|
|
447
|
+
const config = this.resourceConfigs.get(resource) || this.defaultConfig;
|
|
448
|
+
const info = this.getOrCreateRateLimitInfo(resource, config);
|
|
449
|
+
this.cleanupExpiredRequests(info);
|
|
450
|
+
const now = Date.now();
|
|
451
|
+
info.requests.push(now);
|
|
452
|
+
this.totalRequests++;
|
|
453
|
+
info.resetTime = now + config.windowMs;
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
getStatus(resource) {
|
|
457
|
+
return __async(this, null, function* () {
|
|
458
|
+
const config = this.resourceConfigs.get(resource) || this.defaultConfig;
|
|
459
|
+
const info = this.getOrCreateRateLimitInfo(resource, config);
|
|
460
|
+
this.cleanupExpiredRequests(info);
|
|
461
|
+
return {
|
|
462
|
+
remaining: Math.max(0, info.limit - info.requests.length),
|
|
463
|
+
resetTime: new Date(info.resetTime),
|
|
464
|
+
limit: info.limit
|
|
465
|
+
};
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
reset(resource) {
|
|
469
|
+
return __async(this, null, function* () {
|
|
470
|
+
const info = this.limits.get(resource);
|
|
471
|
+
if (info) {
|
|
472
|
+
this.totalRequests = Math.max(
|
|
473
|
+
0,
|
|
474
|
+
this.totalRequests - info.requests.length
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
this.limits.delete(resource);
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
getWaitTime(resource) {
|
|
481
|
+
return __async(this, null, function* () {
|
|
482
|
+
const config = this.resourceConfigs.get(resource) || this.defaultConfig;
|
|
483
|
+
const info = this.getOrCreateRateLimitInfo(resource, config);
|
|
484
|
+
this.cleanupExpiredRequests(info);
|
|
485
|
+
if (info.limit === 0) {
|
|
486
|
+
return Math.max(0, info.resetTime - Date.now());
|
|
487
|
+
}
|
|
488
|
+
if (info.requests.length < info.limit) {
|
|
489
|
+
return 0;
|
|
490
|
+
}
|
|
491
|
+
const oldestRequest = info.requests[0];
|
|
492
|
+
if (oldestRequest === void 0) {
|
|
493
|
+
return 0;
|
|
494
|
+
}
|
|
495
|
+
const timeUntilOldestExpires = oldestRequest + config.windowMs - Date.now();
|
|
496
|
+
return Math.max(0, timeUntilOldestExpires);
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Set rate limit configuration for a specific resource
|
|
501
|
+
*/
|
|
502
|
+
setResourceConfig(resource, config) {
|
|
503
|
+
this.resourceConfigs.set(resource, config);
|
|
504
|
+
this.limits.delete(resource);
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Get rate limit configuration for a resource
|
|
508
|
+
*/
|
|
509
|
+
getResourceConfig(resource) {
|
|
510
|
+
return this.resourceConfigs.get(resource) || this.defaultConfig;
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Get statistics for all resources
|
|
514
|
+
*/
|
|
515
|
+
getStats() {
|
|
516
|
+
let activeResources = 0;
|
|
517
|
+
let rateLimitedResources = 0;
|
|
518
|
+
for (const [_resource, info] of this.limits) {
|
|
519
|
+
this.cleanupExpiredRequests(info);
|
|
520
|
+
if (info.requests.length > 0) {
|
|
521
|
+
activeResources++;
|
|
522
|
+
if (info.requests.length >= info.limit) {
|
|
523
|
+
rateLimitedResources++;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return {
|
|
528
|
+
totalResources: this.limits.size,
|
|
529
|
+
activeResources,
|
|
530
|
+
rateLimitedResources,
|
|
531
|
+
totalRequests: this.totalRequests
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Clear all rate limit data
|
|
536
|
+
*/
|
|
537
|
+
clear() {
|
|
538
|
+
this.limits.clear();
|
|
539
|
+
this.totalRequests = 0;
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Clean up expired requests for all resources
|
|
543
|
+
*/
|
|
544
|
+
cleanup() {
|
|
545
|
+
for (const [_resource, info] of this.limits) {
|
|
546
|
+
this.cleanupExpiredRequests(info);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Destroy the store and clean up resources
|
|
551
|
+
*/
|
|
552
|
+
destroy() {
|
|
553
|
+
if (this.cleanupInterval) {
|
|
554
|
+
clearInterval(this.cleanupInterval);
|
|
555
|
+
this.cleanupInterval = void 0;
|
|
556
|
+
}
|
|
557
|
+
this.clear();
|
|
558
|
+
}
|
|
559
|
+
getOrCreateRateLimitInfo(resource, config) {
|
|
560
|
+
let info = this.limits.get(resource);
|
|
561
|
+
if (!info) {
|
|
562
|
+
info = {
|
|
563
|
+
requests: [],
|
|
564
|
+
limit: config.limit,
|
|
565
|
+
windowMs: config.windowMs,
|
|
566
|
+
resetTime: Date.now() + config.windowMs
|
|
567
|
+
};
|
|
568
|
+
this.limits.set(resource, info);
|
|
569
|
+
}
|
|
570
|
+
return info;
|
|
571
|
+
}
|
|
572
|
+
cleanupExpiredRequests(info) {
|
|
573
|
+
const now = Date.now();
|
|
574
|
+
const cutoff = now - info.windowMs;
|
|
575
|
+
const initialLength = info.requests.length;
|
|
576
|
+
while (info.requests.length > 0 && info.requests[0] < cutoff) {
|
|
577
|
+
info.requests.shift();
|
|
578
|
+
}
|
|
579
|
+
const expiredCount = initialLength - info.requests.length;
|
|
580
|
+
this.totalRequests = Math.max(0, this.totalRequests - expiredCount);
|
|
581
|
+
if (info.requests.length === 0) {
|
|
582
|
+
info.resetTime = now + info.windowMs;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
var AdaptiveRateLimitStore = class {
|
|
587
|
+
constructor(options = {}) {
|
|
588
|
+
this.activityMetrics = /* @__PURE__ */ new Map();
|
|
589
|
+
this.lastCapacityUpdate = /* @__PURE__ */ new Map();
|
|
590
|
+
this.cachedCapacity = /* @__PURE__ */ new Map();
|
|
591
|
+
var _a, _b;
|
|
592
|
+
this.defaultConfig = (_a = options.defaultConfig) != null ? _a : DEFAULT_RATE_LIMIT;
|
|
593
|
+
this.resourceConfigs = (_b = options.resourceConfigs) != null ? _b : /* @__PURE__ */ new Map();
|
|
594
|
+
this.capacityCalculator = new AdaptiveCapacityCalculator(
|
|
595
|
+
options.adaptiveConfig
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
canProceed(resource, priority = "background") {
|
|
599
|
+
return __async(this, null, function* () {
|
|
600
|
+
const metrics = this.getOrCreateActivityMetrics(resource);
|
|
601
|
+
const capacity = this.calculateCurrentCapacity(resource, metrics);
|
|
602
|
+
if (priority === "background" && capacity.backgroundPaused) {
|
|
603
|
+
return false;
|
|
604
|
+
}
|
|
605
|
+
const currentUserRequests = this.getCurrentUsage(
|
|
606
|
+
resource,
|
|
607
|
+
metrics.recentUserRequests
|
|
608
|
+
);
|
|
609
|
+
const currentBackgroundRequests = this.getCurrentUsage(
|
|
610
|
+
resource,
|
|
611
|
+
metrics.recentBackgroundRequests
|
|
612
|
+
);
|
|
613
|
+
if (priority === "user") {
|
|
614
|
+
return currentUserRequests < capacity.userReserved;
|
|
615
|
+
} else {
|
|
616
|
+
return currentBackgroundRequests < capacity.backgroundMax;
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
record(resource, priority = "background") {
|
|
621
|
+
return __async(this, null, function* () {
|
|
622
|
+
const metrics = this.getOrCreateActivityMetrics(resource);
|
|
623
|
+
const now = Date.now();
|
|
624
|
+
if (priority === "user") {
|
|
625
|
+
metrics.recentUserRequests.push(now);
|
|
626
|
+
this.cleanupOldRequests(metrics.recentUserRequests);
|
|
627
|
+
} else {
|
|
628
|
+
metrics.recentBackgroundRequests.push(now);
|
|
629
|
+
this.cleanupOldRequests(metrics.recentBackgroundRequests);
|
|
630
|
+
}
|
|
631
|
+
metrics.userActivityTrend = this.capacityCalculator.calculateActivityTrend(
|
|
632
|
+
metrics.recentUserRequests
|
|
633
|
+
);
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
getStatus(resource) {
|
|
637
|
+
return __async(this, null, function* () {
|
|
638
|
+
var _a;
|
|
639
|
+
const metrics = this.getOrCreateActivityMetrics(resource);
|
|
640
|
+
const capacity = this.calculateCurrentCapacity(resource, metrics);
|
|
641
|
+
const currentUserUsage = this.getCurrentUsage(
|
|
642
|
+
resource,
|
|
643
|
+
metrics.recentUserRequests
|
|
644
|
+
);
|
|
645
|
+
const currentBackgroundUsage = this.getCurrentUsage(
|
|
646
|
+
resource,
|
|
647
|
+
metrics.recentBackgroundRequests
|
|
648
|
+
);
|
|
649
|
+
const config = (_a = this.resourceConfigs.get(resource)) != null ? _a : this.defaultConfig;
|
|
650
|
+
return {
|
|
651
|
+
remaining: capacity.userReserved - currentUserUsage + (capacity.backgroundMax - currentBackgroundUsage),
|
|
652
|
+
resetTime: new Date(Date.now() + config.windowMs),
|
|
653
|
+
limit: this.getResourceLimit(resource),
|
|
654
|
+
adaptive: {
|
|
655
|
+
userReserved: capacity.userReserved,
|
|
656
|
+
backgroundMax: capacity.backgroundMax,
|
|
657
|
+
backgroundPaused: capacity.backgroundPaused,
|
|
658
|
+
recentUserActivity: this.capacityCalculator.getRecentActivity(
|
|
659
|
+
metrics.recentUserRequests
|
|
660
|
+
),
|
|
661
|
+
reason: capacity.reason
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
reset(resource) {
|
|
667
|
+
return __async(this, null, function* () {
|
|
668
|
+
this.activityMetrics.delete(resource);
|
|
669
|
+
this.cachedCapacity.delete(resource);
|
|
670
|
+
this.lastCapacityUpdate.delete(resource);
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
getWaitTime(resource, priority = "background") {
|
|
674
|
+
return __async(this, null, function* () {
|
|
675
|
+
const canProceed = yield this.canProceed(resource, priority);
|
|
676
|
+
if (canProceed) {
|
|
677
|
+
return 0;
|
|
678
|
+
}
|
|
679
|
+
const metrics = this.getOrCreateActivityMetrics(resource);
|
|
680
|
+
const capacity = this.calculateCurrentCapacity(resource, metrics);
|
|
681
|
+
if (priority === "background" && capacity.backgroundPaused) {
|
|
682
|
+
const lastUpdate = this.lastCapacityUpdate.get(resource) || 0;
|
|
683
|
+
const nextUpdate = lastUpdate + this.capacityCalculator.config.recalculationIntervalMs;
|
|
684
|
+
return Math.max(0, nextUpdate - Date.now());
|
|
685
|
+
}
|
|
686
|
+
const monitoringWindow = this.capacityCalculator.config.monitoringWindowMs;
|
|
687
|
+
const requests = priority === "user" ? metrics.recentUserRequests : metrics.recentBackgroundRequests;
|
|
688
|
+
if (requests.length === 0) {
|
|
689
|
+
return 0;
|
|
690
|
+
}
|
|
691
|
+
const oldestRequest = Math.min(...requests);
|
|
692
|
+
const waitTime = oldestRequest + monitoringWindow - Date.now();
|
|
693
|
+
return Math.max(0, waitTime);
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
calculateCurrentCapacity(resource, metrics) {
|
|
697
|
+
const lastUpdate = this.lastCapacityUpdate.get(resource) || 0;
|
|
698
|
+
const recalcInterval = this.capacityCalculator.config.recalculationIntervalMs;
|
|
699
|
+
if (Date.now() - lastUpdate < recalcInterval) {
|
|
700
|
+
return this.cachedCapacity.get(resource) || this.getDefaultCapacity(resource);
|
|
701
|
+
}
|
|
702
|
+
const totalLimit = this.getResourceLimit(resource);
|
|
703
|
+
const capacity = this.capacityCalculator.calculateDynamicCapacity(
|
|
704
|
+
resource,
|
|
705
|
+
totalLimit,
|
|
706
|
+
metrics
|
|
707
|
+
);
|
|
708
|
+
this.cachedCapacity.set(resource, capacity);
|
|
709
|
+
this.lastCapacityUpdate.set(resource, Date.now());
|
|
710
|
+
return capacity;
|
|
711
|
+
}
|
|
712
|
+
getOrCreateActivityMetrics(resource) {
|
|
713
|
+
if (!this.activityMetrics.has(resource)) {
|
|
714
|
+
this.activityMetrics.set(resource, {
|
|
715
|
+
recentUserRequests: [],
|
|
716
|
+
recentBackgroundRequests: [],
|
|
717
|
+
userActivityTrend: "none"
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
return this.activityMetrics.get(resource);
|
|
721
|
+
}
|
|
722
|
+
getCurrentUsage(resource, requests) {
|
|
723
|
+
var _a;
|
|
724
|
+
const config = (_a = this.resourceConfigs.get(resource)) != null ? _a : this.defaultConfig;
|
|
725
|
+
const windowStart = Date.now() - config.windowMs;
|
|
726
|
+
return requests.filter((timestamp) => timestamp > windowStart).length;
|
|
727
|
+
}
|
|
728
|
+
cleanupOldRequests(requests) {
|
|
729
|
+
const cutoff = Date.now() - this.capacityCalculator.config.monitoringWindowMs;
|
|
730
|
+
while (requests.length > 0 && requests[0] < cutoff) {
|
|
731
|
+
requests.shift();
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
getResourceLimit(resource) {
|
|
735
|
+
var _a;
|
|
736
|
+
const config = (_a = this.resourceConfigs.get(resource)) != null ? _a : this.defaultConfig;
|
|
737
|
+
return config.limit;
|
|
738
|
+
}
|
|
739
|
+
getDefaultCapacity(resource) {
|
|
740
|
+
const totalLimit = this.getResourceLimit(resource);
|
|
741
|
+
return {
|
|
742
|
+
userReserved: Math.floor(totalLimit * 0.3),
|
|
743
|
+
backgroundMax: Math.floor(totalLimit * 0.7),
|
|
744
|
+
backgroundPaused: false,
|
|
745
|
+
reason: "Default capacity allocation"
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
export { AdaptiveRateLimitStore, InMemoryCacheStore, InMemoryDedupeStore, InMemoryRateLimitStore };
|
|
751
|
+
//# sourceMappingURL=index.js.map
|
|
752
|
+
//# sourceMappingURL=index.js.map
|