safe-memory-layer 1.0.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 +21 -0
- package/README.md +501 -0
- package/dist/index.cjs +953 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +650 -0
- package/dist/index.d.ts +650 -0
- package/dist/index.js +938 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
- package/src/Entry.ts +83 -0
- package/src/Events.ts +138 -0
- package/src/MemoryStore.ts +554 -0
- package/src/Scheduler.ts +120 -0
- package/src/Stats.ts +138 -0
- package/src/WeakMemoryStore.ts +229 -0
- package/src/index.ts +57 -0
- package/src/types.ts +106 -0
- package/src/utils/featureDetection.ts +52 -0
- package/src/utils/timer.ts +93 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,938 @@
|
|
|
1
|
+
// src/utils/timer.ts
|
|
2
|
+
function createTimer(fn, delay) {
|
|
3
|
+
let cancelled = false;
|
|
4
|
+
let timerId;
|
|
5
|
+
const wrappedFn = () => {
|
|
6
|
+
if (cancelled) return;
|
|
7
|
+
timerId = void 0;
|
|
8
|
+
try {
|
|
9
|
+
fn();
|
|
10
|
+
} catch {
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
timerId = setTimeout(wrappedFn, delay);
|
|
14
|
+
return {
|
|
15
|
+
cancel: () => {
|
|
16
|
+
cancelled = true;
|
|
17
|
+
if (timerId !== void 0) {
|
|
18
|
+
clearTimeout(timerId);
|
|
19
|
+
timerId = void 0;
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
get cancelled() {
|
|
23
|
+
return cancelled;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function createInterval(fn, interval) {
|
|
28
|
+
let cancelled = false;
|
|
29
|
+
let timerId;
|
|
30
|
+
const wrappedFn = () => {
|
|
31
|
+
if (cancelled) return;
|
|
32
|
+
try {
|
|
33
|
+
fn();
|
|
34
|
+
} catch {
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
timerId = setInterval(wrappedFn, interval);
|
|
38
|
+
return {
|
|
39
|
+
cancel: () => {
|
|
40
|
+
cancelled = true;
|
|
41
|
+
if (timerId !== void 0) {
|
|
42
|
+
clearInterval(timerId);
|
|
43
|
+
timerId = void 0;
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
get cancelled() {
|
|
47
|
+
return cancelled;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/Scheduler.ts
|
|
53
|
+
var Scheduler = class {
|
|
54
|
+
/** The cleanup interval timer handle. */
|
|
55
|
+
#timer = null;
|
|
56
|
+
/** The interval in milliseconds between cleanup runs. */
|
|
57
|
+
#interval;
|
|
58
|
+
/** The cleanup function to call on each interval. */
|
|
59
|
+
#cleanupFn;
|
|
60
|
+
/** The onEmpty callback, called when cleanup finds the store empty. */
|
|
61
|
+
#onEmpty;
|
|
62
|
+
/** Whether the scheduler is currently running. */
|
|
63
|
+
#running = false;
|
|
64
|
+
/**
|
|
65
|
+
* Creates a new Scheduler.
|
|
66
|
+
*
|
|
67
|
+
* @param cleanupFn - Function to call for cleanup. Should return count of removed entries.
|
|
68
|
+
* @param interval - Interval in milliseconds between cleanup runs.
|
|
69
|
+
* @param onEmpty - Optional callback when cleanup finds the store empty.
|
|
70
|
+
*/
|
|
71
|
+
constructor(cleanupFn, interval, onEmpty) {
|
|
72
|
+
this.#cleanupFn = cleanupFn;
|
|
73
|
+
this.#interval = interval;
|
|
74
|
+
this.#onEmpty = onEmpty;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Starts the scheduler if it's not already running.
|
|
78
|
+
* Does nothing if already started.
|
|
79
|
+
*/
|
|
80
|
+
start() {
|
|
81
|
+
if (this.#running || this.#timer !== null) return;
|
|
82
|
+
this.#running = true;
|
|
83
|
+
this.#timer = createInterval(() => {
|
|
84
|
+
this.#tick();
|
|
85
|
+
}, this.#interval);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Stops the scheduler if running.
|
|
89
|
+
* Safe to call multiple times.
|
|
90
|
+
*/
|
|
91
|
+
stop() {
|
|
92
|
+
this.#running = false;
|
|
93
|
+
if (this.#timer !== null) {
|
|
94
|
+
this.#timer.cancel();
|
|
95
|
+
this.#timer = null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Restarts the scheduler (stops then starts).
|
|
100
|
+
*/
|
|
101
|
+
restart() {
|
|
102
|
+
this.stop();
|
|
103
|
+
this.start();
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Whether the scheduler is currently running.
|
|
107
|
+
*/
|
|
108
|
+
get running() {
|
|
109
|
+
return this.#running;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Performs a single cleanup tick.
|
|
113
|
+
* Stops the scheduler if the store is empty.
|
|
114
|
+
*/
|
|
115
|
+
#tick() {
|
|
116
|
+
const removed = this.#cleanupFn();
|
|
117
|
+
if (removed === 0) {
|
|
118
|
+
this.stop();
|
|
119
|
+
this.#onEmpty?.();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Disposes the scheduler, stopping it and releasing all references.
|
|
124
|
+
*/
|
|
125
|
+
dispose() {
|
|
126
|
+
this.stop();
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// src/Entry.ts
|
|
131
|
+
function createEntry(key, value, options = {}, now = Date.now()) {
|
|
132
|
+
const ttl = options.ttl;
|
|
133
|
+
const expiresAt = ttl !== void 0 ? now + ttl : void 0;
|
|
134
|
+
return {
|
|
135
|
+
value,
|
|
136
|
+
key,
|
|
137
|
+
expiresAt,
|
|
138
|
+
lastAccessed: now,
|
|
139
|
+
createdAt: now
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function isExpired(entry, now = Date.now()) {
|
|
143
|
+
return entry.expiresAt !== void 0 && now >= entry.expiresAt;
|
|
144
|
+
}
|
|
145
|
+
var lruCounter = 0;
|
|
146
|
+
function touchEntry(entry, now = Date.now()) {
|
|
147
|
+
entry.lastAccessed = now * 1e4 + lruCounter++ % 1e4;
|
|
148
|
+
}
|
|
149
|
+
function getRemainingTTL(entry, now = Date.now()) {
|
|
150
|
+
if (entry.expiresAt === void 0) return void 0;
|
|
151
|
+
const remaining = entry.expiresAt - now;
|
|
152
|
+
return remaining > 0 ? remaining : 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/Stats.ts
|
|
156
|
+
var Stats = class {
|
|
157
|
+
/** Total number of entries that have expired. */
|
|
158
|
+
#expired = 0;
|
|
159
|
+
/** Total number of entries that have been deleted. */
|
|
160
|
+
#deleted = 0;
|
|
161
|
+
/** Total number of cleanup cycles completed. */
|
|
162
|
+
#cleaned = 0;
|
|
163
|
+
/** Timestamp when the store was created. */
|
|
164
|
+
#createdAt;
|
|
165
|
+
/** Current number of entries in the store. */
|
|
166
|
+
#entries = 0;
|
|
167
|
+
/**
|
|
168
|
+
* Creates a new Stats instance.
|
|
169
|
+
*
|
|
170
|
+
* @param createdAt - Timestamp when the store was created.
|
|
171
|
+
*/
|
|
172
|
+
constructor(createdAt) {
|
|
173
|
+
this.#createdAt = createdAt;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Increments the expired counter.
|
|
177
|
+
*/
|
|
178
|
+
incrementExpired() {
|
|
179
|
+
this.#expired++;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Increments the deleted counter.
|
|
183
|
+
*/
|
|
184
|
+
incrementDeleted() {
|
|
185
|
+
this.#deleted++;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Increments the cleaned counter.
|
|
189
|
+
*/
|
|
190
|
+
incrementCleaned() {
|
|
191
|
+
this.#cleaned++;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Sets the current number of entries.
|
|
195
|
+
*
|
|
196
|
+
* @param count - The current entry count.
|
|
197
|
+
*/
|
|
198
|
+
setEntries(count) {
|
|
199
|
+
this.#entries = count;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Returns the current statistics snapshot.
|
|
203
|
+
*
|
|
204
|
+
* @param currentEntries - Current number of entries (overrides internal count).
|
|
205
|
+
* @returns A snapshot of the current statistics.
|
|
206
|
+
*/
|
|
207
|
+
snapshot(currentEntries) {
|
|
208
|
+
const uptime = Date.now() - this.#createdAt;
|
|
209
|
+
const entries = currentEntries ?? this.#entries;
|
|
210
|
+
return {
|
|
211
|
+
entries,
|
|
212
|
+
expired: this.#expired,
|
|
213
|
+
deleted: this.#deleted,
|
|
214
|
+
cleaned: this.#cleaned,
|
|
215
|
+
uptime,
|
|
216
|
+
memoryEstimate: this.estimateMemory(entries)
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Creates a cleanup stats object.
|
|
221
|
+
*
|
|
222
|
+
* @param removed - Number of entries removed.
|
|
223
|
+
* @param totalBefore - Total entries before cleanup.
|
|
224
|
+
* @param totalAfter - Total entries after cleanup.
|
|
225
|
+
* @param duration - Duration of cleanup in ms.
|
|
226
|
+
* @returns A CleanupStats object.
|
|
227
|
+
*/
|
|
228
|
+
createCleanupStats(removed, totalBefore, totalAfter, duration) {
|
|
229
|
+
return {
|
|
230
|
+
removed,
|
|
231
|
+
totalBefore,
|
|
232
|
+
totalAfter,
|
|
233
|
+
duration
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Estimates memory usage based on entry count.
|
|
238
|
+
* This is a rough approximation for monitoring purposes.
|
|
239
|
+
*
|
|
240
|
+
* @param entryCount - Number of entries.
|
|
241
|
+
* @returns Estimated memory usage in bytes.
|
|
242
|
+
*/
|
|
243
|
+
estimateMemory(entryCount) {
|
|
244
|
+
return entryCount * 200;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Resets all statistics.
|
|
248
|
+
*/
|
|
249
|
+
reset() {
|
|
250
|
+
this.#expired = 0;
|
|
251
|
+
this.#deleted = 0;
|
|
252
|
+
this.#cleaned = 0;
|
|
253
|
+
this.#entries = 0;
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// src/Events.ts
|
|
258
|
+
var EventEmitter = class {
|
|
259
|
+
/** Registered event listeners. */
|
|
260
|
+
#listeners = {};
|
|
261
|
+
/**
|
|
262
|
+
* Registers a listener for the expire event.
|
|
263
|
+
*
|
|
264
|
+
* @param callback - Function to call when an entry expires.
|
|
265
|
+
*/
|
|
266
|
+
onExpire(callback) {
|
|
267
|
+
this.#listeners.expire ??= [];
|
|
268
|
+
this.#listeners.expire.push(callback);
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Registers a listener for the delete event.
|
|
272
|
+
*
|
|
273
|
+
* @param callback - Function to call when an entry is deleted.
|
|
274
|
+
*/
|
|
275
|
+
onDelete(callback) {
|
|
276
|
+
this.#listeners.delete ??= [];
|
|
277
|
+
this.#listeners.delete.push(callback);
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Registers a listener for the cleanup event.
|
|
281
|
+
*
|
|
282
|
+
* @param callback - Function to call after cleanup completes.
|
|
283
|
+
*/
|
|
284
|
+
onCleanup(callback) {
|
|
285
|
+
this.#listeners.cleanup ??= [];
|
|
286
|
+
this.#listeners.cleanup.push(callback);
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Emits an expire event to all registered listeners.
|
|
290
|
+
* Errors in callbacks are isolated and logged.
|
|
291
|
+
*
|
|
292
|
+
* @param key - The key of the expired entry.
|
|
293
|
+
* @param value - The value of the expired entry.
|
|
294
|
+
*/
|
|
295
|
+
emitExpire(key, value) {
|
|
296
|
+
const listeners = this.#listeners.expire;
|
|
297
|
+
if (listeners === void 0 || listeners.length === 0) return;
|
|
298
|
+
for (const callback of listeners) {
|
|
299
|
+
try {
|
|
300
|
+
callback(key, value);
|
|
301
|
+
} catch {
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Emits a delete event to all registered listeners.
|
|
307
|
+
* Errors in callbacks are isolated and logged.
|
|
308
|
+
*
|
|
309
|
+
* @param key - The key of the deleted entry.
|
|
310
|
+
* @param value - The value of the deleted entry.
|
|
311
|
+
*/
|
|
312
|
+
emitDelete(key, value) {
|
|
313
|
+
const listeners = this.#listeners.delete;
|
|
314
|
+
if (listeners === void 0 || listeners.length === 0) return;
|
|
315
|
+
for (const callback of listeners) {
|
|
316
|
+
try {
|
|
317
|
+
callback(key, value);
|
|
318
|
+
} catch {
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Emits a cleanup event to all registered listeners.
|
|
324
|
+
* Errors in callbacks are isolated and logged.
|
|
325
|
+
*
|
|
326
|
+
* @param stats - The cleanup statistics.
|
|
327
|
+
*/
|
|
328
|
+
emitCleanup(stats) {
|
|
329
|
+
const listeners = this.#listeners.cleanup;
|
|
330
|
+
if (listeners === void 0 || listeners.length === 0) return;
|
|
331
|
+
for (const callback of listeners) {
|
|
332
|
+
try {
|
|
333
|
+
callback(stats);
|
|
334
|
+
} catch {
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Removes all event listeners.
|
|
340
|
+
*/
|
|
341
|
+
removeAllListeners() {
|
|
342
|
+
this.#listeners = {};
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Disposes the event emitter, removing all listeners.
|
|
346
|
+
*/
|
|
347
|
+
dispose() {
|
|
348
|
+
this.removeAllListeners();
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
// src/utils/featureDetection.ts
|
|
353
|
+
function detectFeatures() {
|
|
354
|
+
return {
|
|
355
|
+
finalizationRegistry: typeof globalThis.FinalizationRegistry !== "undefined"
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
var cachedFeatures;
|
|
359
|
+
function getFeatures() {
|
|
360
|
+
if (cachedFeatures === void 0) {
|
|
361
|
+
cachedFeatures = detectFeatures();
|
|
362
|
+
}
|
|
363
|
+
return cachedFeatures;
|
|
364
|
+
}
|
|
365
|
+
function createSafeFinalizationRegistry(cleanup) {
|
|
366
|
+
const features = getFeatures();
|
|
367
|
+
if (features.finalizationRegistry) {
|
|
368
|
+
try {
|
|
369
|
+
return new FinalizationRegistry(cleanup);
|
|
370
|
+
} catch {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// src/MemoryStore.ts
|
|
378
|
+
var DEFAULT_CLEANUP_INTERVAL = 1e4;
|
|
379
|
+
var DEFAULT_AUTO_DISPOSE_DELAY = 6e4;
|
|
380
|
+
var MemoryStore = class {
|
|
381
|
+
/** Internal map of entries. */
|
|
382
|
+
#map = /* @__PURE__ */ new Map();
|
|
383
|
+
/** Store configuration options. */
|
|
384
|
+
#options;
|
|
385
|
+
/** Statistics tracker. */
|
|
386
|
+
#stats;
|
|
387
|
+
/** Event emitter for store events. */
|
|
388
|
+
#events;
|
|
389
|
+
/** Cleanup scheduler. */
|
|
390
|
+
#scheduler;
|
|
391
|
+
/** Auto-dispose timer handle. */
|
|
392
|
+
#autoDisposeTimer = null;
|
|
393
|
+
/** Whether the store has been disposed. */
|
|
394
|
+
#disposed = false;
|
|
395
|
+
/** Timestamp when the store was created. */
|
|
396
|
+
#createdAt;
|
|
397
|
+
/** Timestamp when the store became empty (for auto-dispose). */
|
|
398
|
+
#emptySince = null;
|
|
399
|
+
// Used for auto-dispose timing
|
|
400
|
+
/**
|
|
401
|
+
* Creates a new MemoryStore instance.
|
|
402
|
+
*
|
|
403
|
+
* @param options - Configuration options for the store.
|
|
404
|
+
*
|
|
405
|
+
* @example
|
|
406
|
+
* ```ts
|
|
407
|
+
* const store = new MemoryStore({
|
|
408
|
+
* defaultTTL: 60000,
|
|
409
|
+
* cleanupInterval: 5000,
|
|
410
|
+
* autoCleanup: true,
|
|
411
|
+
* maxEntries: 1000,
|
|
412
|
+
* maxEntriesStrategy: "LRU"
|
|
413
|
+
* });
|
|
414
|
+
* ```
|
|
415
|
+
*/
|
|
416
|
+
constructor(options = {}) {
|
|
417
|
+
const opts = options;
|
|
418
|
+
this.#options = {
|
|
419
|
+
defaultTTL: opts.defaultTTL,
|
|
420
|
+
cleanupInterval: opts.cleanupInterval ?? DEFAULT_CLEANUP_INTERVAL,
|
|
421
|
+
autoCleanup: opts.autoCleanup ?? true,
|
|
422
|
+
autoDispose: opts.autoDispose ?? false,
|
|
423
|
+
autoDisposeDelay: opts.autoDisposeDelay ?? DEFAULT_AUTO_DISPOSE_DELAY,
|
|
424
|
+
maxEntries: opts.maxEntries,
|
|
425
|
+
maxEntriesStrategy: opts.maxEntriesStrategy ?? "reject",
|
|
426
|
+
onExpire: opts.onExpire,
|
|
427
|
+
onDelete: opts.onDelete,
|
|
428
|
+
onCleanup: opts.onCleanup
|
|
429
|
+
};
|
|
430
|
+
this.#createdAt = Date.now();
|
|
431
|
+
this.#stats = new Stats(this.#createdAt);
|
|
432
|
+
this.#events = new EventEmitter();
|
|
433
|
+
if (this.#options.onExpire !== void 0) {
|
|
434
|
+
this.#events.onExpire(this.#options.onExpire);
|
|
435
|
+
}
|
|
436
|
+
if (this.#options.onDelete !== void 0) {
|
|
437
|
+
this.#events.onDelete(this.#options.onDelete);
|
|
438
|
+
}
|
|
439
|
+
if (this.#options.onCleanup !== void 0) {
|
|
440
|
+
this.#events.onCleanup(this.#options.onCleanup);
|
|
441
|
+
}
|
|
442
|
+
this.#scheduler = new Scheduler(
|
|
443
|
+
() => this.#runCleanup(),
|
|
444
|
+
this.#options.cleanupInterval,
|
|
445
|
+
() => this.#onSchedulerEmpty()
|
|
446
|
+
);
|
|
447
|
+
if (this.#options.autoCleanup) {
|
|
448
|
+
this.#scheduler.start();
|
|
449
|
+
}
|
|
450
|
+
this.#setupFinalizationRegistry();
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Sets up FinalizationRegistry for weak reference tracking.
|
|
454
|
+
* This is used only for monitoring, not for core functionality.
|
|
455
|
+
*/
|
|
456
|
+
#setupFinalizationRegistry() {
|
|
457
|
+
const registry = createSafeFinalizationRegistry(() => {
|
|
458
|
+
});
|
|
459
|
+
if (registry !== null) {
|
|
460
|
+
this._registry = registry;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Stores a value with the given key.
|
|
465
|
+
*
|
|
466
|
+
* @param key - The key to store the value under.
|
|
467
|
+
* @param value - The value to store.
|
|
468
|
+
* @param options - Optional settings (e.g., TTL).
|
|
469
|
+
* @returns True if the value was stored, false if rejected (e.g., max entries reached).
|
|
470
|
+
*
|
|
471
|
+
* @example
|
|
472
|
+
* ```ts
|
|
473
|
+
* store.set("token", token, { ttl: 60000 });
|
|
474
|
+
* ```
|
|
475
|
+
*/
|
|
476
|
+
set(key, value, options = {}) {
|
|
477
|
+
this.#checkDisposed();
|
|
478
|
+
this.#clearAutoDisposeTimer();
|
|
479
|
+
if (this.#options.maxEntries !== void 0 && !this.#map.has(key)) {
|
|
480
|
+
if (this.#map.size >= this.#options.maxEntries) {
|
|
481
|
+
const strategy = this.#options.maxEntriesStrategy;
|
|
482
|
+
if (strategy === "reject") {
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
if (strategy === "FIFO") {
|
|
486
|
+
const firstKey = this.#map.keys().next().value;
|
|
487
|
+
if (firstKey !== void 0) {
|
|
488
|
+
this.delete(firstKey);
|
|
489
|
+
}
|
|
490
|
+
} else if (strategy === "LRU") {
|
|
491
|
+
let lruKey;
|
|
492
|
+
let lruTime = Infinity;
|
|
493
|
+
for (const [k, entry2] of this.#map) {
|
|
494
|
+
if (entry2.lastAccessed < lruTime) {
|
|
495
|
+
lruTime = entry2.lastAccessed;
|
|
496
|
+
lruKey = k;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
if (lruKey !== void 0) {
|
|
500
|
+
this.delete(lruKey);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
const now = Date.now();
|
|
506
|
+
const ttl = options.ttl ?? this.#options.defaultTTL;
|
|
507
|
+
const entry = createEntry(
|
|
508
|
+
key,
|
|
509
|
+
value,
|
|
510
|
+
ttl !== void 0 ? { ttl } : {},
|
|
511
|
+
now
|
|
512
|
+
);
|
|
513
|
+
this.#map.set(key, entry);
|
|
514
|
+
if (!this.#scheduler.running) {
|
|
515
|
+
this.#scheduler.start();
|
|
516
|
+
}
|
|
517
|
+
return true;
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Retrieves a value by key.
|
|
521
|
+
*
|
|
522
|
+
* @param key - The key to look up.
|
|
523
|
+
* @returns The value if found and not expired, undefined otherwise.
|
|
524
|
+
*
|
|
525
|
+
* @example
|
|
526
|
+
* ```ts
|
|
527
|
+
* const user = store.get("user:123");
|
|
528
|
+
* ```
|
|
529
|
+
*/
|
|
530
|
+
get(key) {
|
|
531
|
+
this.#checkDisposed();
|
|
532
|
+
const entry = this.#map.get(key);
|
|
533
|
+
if (entry === void 0) return void 0;
|
|
534
|
+
if (isExpired(entry)) {
|
|
535
|
+
this.#map.delete(key);
|
|
536
|
+
this.#stats.incrementExpired();
|
|
537
|
+
this.#events.emitExpire(entry.key, entry.value);
|
|
538
|
+
return void 0;
|
|
539
|
+
}
|
|
540
|
+
touchEntry(entry);
|
|
541
|
+
return entry.value;
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Checks if a key exists in the store and is not expired.
|
|
545
|
+
*
|
|
546
|
+
* @param key - The key to check.
|
|
547
|
+
* @returns True if the key exists and is not expired.
|
|
548
|
+
*/
|
|
549
|
+
has(key) {
|
|
550
|
+
this.#checkDisposed();
|
|
551
|
+
const entry = this.#map.get(key);
|
|
552
|
+
if (entry === void 0) return false;
|
|
553
|
+
if (isExpired(entry)) {
|
|
554
|
+
this.#map.delete(key);
|
|
555
|
+
this.#stats.incrementExpired();
|
|
556
|
+
this.#events.emitExpire(entry.key, entry.value);
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
return true;
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Deletes a value by key.
|
|
563
|
+
*
|
|
564
|
+
* @param key - The key to delete.
|
|
565
|
+
* @returns True if the key was present, false otherwise.
|
|
566
|
+
*/
|
|
567
|
+
delete(key) {
|
|
568
|
+
this.#checkDisposed();
|
|
569
|
+
const entry = this.#map.get(key);
|
|
570
|
+
if (entry === void 0) return false;
|
|
571
|
+
this.#map.delete(key);
|
|
572
|
+
this.#stats.incrementDeleted();
|
|
573
|
+
this.#events.emitDelete(entry.key, entry.value);
|
|
574
|
+
if (this.#map.size === 0) {
|
|
575
|
+
this.#onEmpty();
|
|
576
|
+
}
|
|
577
|
+
return true;
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Removes all entries from the store.
|
|
581
|
+
*/
|
|
582
|
+
clear() {
|
|
583
|
+
this.#checkDisposed();
|
|
584
|
+
for (const entry of this.#map.values()) {
|
|
585
|
+
this.#events.emitDelete(entry.key, entry.value);
|
|
586
|
+
this.#stats.incrementDeleted();
|
|
587
|
+
}
|
|
588
|
+
this.#map.clear();
|
|
589
|
+
this.#onEmpty();
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Returns the number of entries in the store.
|
|
593
|
+
* Excludes expired entries.
|
|
594
|
+
*/
|
|
595
|
+
get size() {
|
|
596
|
+
if (this.#disposed) return 0;
|
|
597
|
+
let count = 0;
|
|
598
|
+
const now = Date.now();
|
|
599
|
+
for (const entry of this.#map.values()) {
|
|
600
|
+
if (!isExpired(entry, now)) {
|
|
601
|
+
count++;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return count;
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Returns an iterator of all keys.
|
|
608
|
+
*/
|
|
609
|
+
keys() {
|
|
610
|
+
this.#checkDisposed();
|
|
611
|
+
return this.#map.keys();
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Returns an iterator of all values.
|
|
615
|
+
*/
|
|
616
|
+
values() {
|
|
617
|
+
this.#checkDisposed();
|
|
618
|
+
return this.#getNonExpiredValues();
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Returns an iterator of all [key, value] pairs.
|
|
622
|
+
*/
|
|
623
|
+
entries() {
|
|
624
|
+
this.#checkDisposed();
|
|
625
|
+
return this.#getNonExpiredEntries();
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Returns an iterator for the store (for...of support).
|
|
629
|
+
*/
|
|
630
|
+
[Symbol.iterator]() {
|
|
631
|
+
return this.entries();
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Returns an iterator of non-expired values.
|
|
635
|
+
*/
|
|
636
|
+
*#getNonExpiredValues() {
|
|
637
|
+
const now = Date.now();
|
|
638
|
+
for (const entry of this.#map.values()) {
|
|
639
|
+
if (!isExpired(entry, now)) {
|
|
640
|
+
yield entry.value;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Returns an iterator of non-expired entries.
|
|
646
|
+
*/
|
|
647
|
+
*#getNonExpiredEntries() {
|
|
648
|
+
const now = Date.now();
|
|
649
|
+
for (const entry of this.#map.values()) {
|
|
650
|
+
if (!isExpired(entry, now)) {
|
|
651
|
+
yield [entry.key, entry.value];
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Runs a cleanup cycle, removing all expired entries.
|
|
657
|
+
*
|
|
658
|
+
* @returns The number of entries removed.
|
|
659
|
+
*/
|
|
660
|
+
cleanup() {
|
|
661
|
+
this.#checkDisposed();
|
|
662
|
+
return this.#runCleanup();
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Runs the cleanup cycle.
|
|
666
|
+
* This is called by the scheduler and can be called manually.
|
|
667
|
+
*
|
|
668
|
+
* @returns The number of entries removed.
|
|
669
|
+
*/
|
|
670
|
+
#runCleanup() {
|
|
671
|
+
const startTime = Date.now();
|
|
672
|
+
const totalBefore = this.#map.size;
|
|
673
|
+
let removed = 0;
|
|
674
|
+
const now = Date.now();
|
|
675
|
+
for (const [entryKey, entry] of this.#map) {
|
|
676
|
+
if (isExpired(entry, now)) {
|
|
677
|
+
this.#map.delete(entryKey);
|
|
678
|
+
this.#stats.incrementExpired();
|
|
679
|
+
this.#events.emitExpire(entry.key, entry.value);
|
|
680
|
+
removed++;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
if (this.#map.size === 0 && this.#emptySince !== null) ;
|
|
684
|
+
const totalAfter = this.#map.size;
|
|
685
|
+
const duration = Date.now() - startTime;
|
|
686
|
+
this.#stats.incrementCleaned();
|
|
687
|
+
const cleanupStats = this.#stats.createCleanupStats(
|
|
688
|
+
removed,
|
|
689
|
+
totalBefore,
|
|
690
|
+
totalAfter,
|
|
691
|
+
duration
|
|
692
|
+
);
|
|
693
|
+
this.#events.emitCleanup(cleanupStats);
|
|
694
|
+
if (this.#map.size === 0) {
|
|
695
|
+
this.#onEmpty();
|
|
696
|
+
}
|
|
697
|
+
return removed;
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Compacts the store by removing expired entries.
|
|
701
|
+
* Alias for cleanup().
|
|
702
|
+
*/
|
|
703
|
+
compact() {
|
|
704
|
+
return this.cleanup();
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Returns statistics about the store.
|
|
708
|
+
*
|
|
709
|
+
* @returns A snapshot of the current statistics.
|
|
710
|
+
*/
|
|
711
|
+
stats() {
|
|
712
|
+
this.#checkDisposed();
|
|
713
|
+
return this.#stats.snapshot(this.#map.size);
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Disposes the store, stopping all timers and releasing references.
|
|
717
|
+
* After disposal, the store cannot be used.
|
|
718
|
+
*/
|
|
719
|
+
dispose() {
|
|
720
|
+
if (this.#disposed) return;
|
|
721
|
+
this.#disposed = true;
|
|
722
|
+
this.#scheduler.dispose();
|
|
723
|
+
this.#clearAutoDisposeTimer();
|
|
724
|
+
this.#map.clear();
|
|
725
|
+
this.#events.dispose();
|
|
726
|
+
delete this._registry;
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Checks if the store has been disposed.
|
|
730
|
+
*/
|
|
731
|
+
get disposed() {
|
|
732
|
+
return this.#disposed;
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Called when the store becomes empty.
|
|
736
|
+
*/
|
|
737
|
+
#onEmpty() {
|
|
738
|
+
this.#emptySince = Date.now();
|
|
739
|
+
if (this.#options.autoDispose) {
|
|
740
|
+
this.#startAutoDisposeTimer();
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Starts the auto-dispose timer.
|
|
745
|
+
*/
|
|
746
|
+
#startAutoDisposeTimer() {
|
|
747
|
+
this.#clearAutoDisposeTimer();
|
|
748
|
+
this.#autoDisposeTimer = createTimer(() => {
|
|
749
|
+
if (this.#map.size === 0 && !this.#disposed) {
|
|
750
|
+
this.dispose();
|
|
751
|
+
}
|
|
752
|
+
}, this.#options.autoDisposeDelay);
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Clears the auto-dispose timer.
|
|
756
|
+
*/
|
|
757
|
+
#clearAutoDisposeTimer() {
|
|
758
|
+
if (this.#autoDisposeTimer !== null) {
|
|
759
|
+
this.#autoDisposeTimer.cancel();
|
|
760
|
+
this.#autoDisposeTimer = null;
|
|
761
|
+
}
|
|
762
|
+
this.#emptySince = null;
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Called when the scheduler finds the store empty.
|
|
766
|
+
*/
|
|
767
|
+
#onSchedulerEmpty() {
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Checks if the store has been disposed and throws if so.
|
|
771
|
+
*/
|
|
772
|
+
#checkDisposed() {
|
|
773
|
+
if (this.#disposed) {
|
|
774
|
+
throw new Error("MemoryStore has been disposed");
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
// src/WeakMemoryStore.ts
|
|
780
|
+
var WeakMemoryStore = class {
|
|
781
|
+
/** Internal WeakMap for storage. */
|
|
782
|
+
#map = /* @__PURE__ */ new WeakMap();
|
|
783
|
+
/** Store configuration options. */
|
|
784
|
+
#options;
|
|
785
|
+
/** Statistics tracker. */
|
|
786
|
+
#deleted = 0;
|
|
787
|
+
/** Timestamp when the store was created. */
|
|
788
|
+
#createdAt;
|
|
789
|
+
/** Whether the store has been disposed. */
|
|
790
|
+
#disposed = false;
|
|
791
|
+
/**
|
|
792
|
+
* Creates a new WeakMemoryStore instance.
|
|
793
|
+
*
|
|
794
|
+
* @param options - Configuration options for the store.
|
|
795
|
+
*
|
|
796
|
+
* @example
|
|
797
|
+
* ```ts
|
|
798
|
+
* const store = new WeakMemoryStore({
|
|
799
|
+
* onDelete: (value) => console.log("Deleted:", value)
|
|
800
|
+
* });
|
|
801
|
+
* ```
|
|
802
|
+
*/
|
|
803
|
+
constructor(options = {}) {
|
|
804
|
+
this.#options = options;
|
|
805
|
+
this.#createdAt = Date.now();
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Stores a value with the given object key.
|
|
809
|
+
*
|
|
810
|
+
* @param key - The object key to store the value under. Must be an object.
|
|
811
|
+
* @param value - The value to store.
|
|
812
|
+
* @returns True if the value was stored.
|
|
813
|
+
*
|
|
814
|
+
* @example
|
|
815
|
+
* ```ts
|
|
816
|
+
* const obj = { id: 1 };
|
|
817
|
+
* weakStore.set(obj, { name: "Alice" });
|
|
818
|
+
* ```
|
|
819
|
+
*/
|
|
820
|
+
set(key, value) {
|
|
821
|
+
this.#checkDisposed();
|
|
822
|
+
if (typeof key !== "object" || key === null) {
|
|
823
|
+
throw new TypeError("WeakMemoryStore keys must be objects");
|
|
824
|
+
}
|
|
825
|
+
this.#map.set(key, value);
|
|
826
|
+
return true;
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Retrieves a value by object key.
|
|
830
|
+
*
|
|
831
|
+
* @param key - The object key to look up.
|
|
832
|
+
* @returns The value if found, undefined otherwise.
|
|
833
|
+
*
|
|
834
|
+
* @example
|
|
835
|
+
* ```ts
|
|
836
|
+
* const user = weakStore.get(obj);
|
|
837
|
+
* ```
|
|
838
|
+
*/
|
|
839
|
+
get(key) {
|
|
840
|
+
this.#checkDisposed();
|
|
841
|
+
if (typeof key !== "object" || key === null) {
|
|
842
|
+
return void 0;
|
|
843
|
+
}
|
|
844
|
+
return this.#map.get(key);
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Checks if a key exists in the store.
|
|
848
|
+
*
|
|
849
|
+
* @param key - The object key to check.
|
|
850
|
+
* @returns True if the key exists.
|
|
851
|
+
*/
|
|
852
|
+
has(key) {
|
|
853
|
+
this.#checkDisposed();
|
|
854
|
+
if (typeof key !== "object" || key === null) {
|
|
855
|
+
return false;
|
|
856
|
+
}
|
|
857
|
+
return this.#map.has(key);
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Deletes a value by object key.
|
|
861
|
+
*
|
|
862
|
+
* @param key - The object key to delete.
|
|
863
|
+
* @returns True if the key was present, false otherwise.
|
|
864
|
+
*/
|
|
865
|
+
delete(key) {
|
|
866
|
+
this.#checkDisposed();
|
|
867
|
+
if (typeof key !== "object" || key === null) {
|
|
868
|
+
return false;
|
|
869
|
+
}
|
|
870
|
+
const value = this.#map.get(key);
|
|
871
|
+
if (value === void 0) return false;
|
|
872
|
+
this.#map.delete(key);
|
|
873
|
+
this.#deleted++;
|
|
874
|
+
if (this.#options.onDelete !== void 0) {
|
|
875
|
+
try {
|
|
876
|
+
this.#options.onDelete(value);
|
|
877
|
+
} catch {
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return true;
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Removes all entries from the store.
|
|
884
|
+
* Note: This does not prevent garbage collection of the keys.
|
|
885
|
+
*/
|
|
886
|
+
clear() {
|
|
887
|
+
this.#checkDisposed();
|
|
888
|
+
this.#map = /* @__PURE__ */ new WeakMap();
|
|
889
|
+
this.#deleted++;
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Returns the number of deleted entries.
|
|
893
|
+
* Note: Cannot return current size (WeakMap limitation).
|
|
894
|
+
*/
|
|
895
|
+
get deleted() {
|
|
896
|
+
return this.#deleted;
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* Returns statistics about the store.
|
|
900
|
+
* Note: entries count is not available for WeakMap.
|
|
901
|
+
*
|
|
902
|
+
* @returns A snapshot of the current statistics.
|
|
903
|
+
*/
|
|
904
|
+
stats() {
|
|
905
|
+
this.#checkDisposed();
|
|
906
|
+
return {
|
|
907
|
+
deleted: this.#deleted,
|
|
908
|
+
uptime: Date.now() - this.#createdAt
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Disposes the store, releasing the WeakMap reference.
|
|
913
|
+
* After disposal, the store cannot be used.
|
|
914
|
+
*/
|
|
915
|
+
dispose() {
|
|
916
|
+
if (this.#disposed) return;
|
|
917
|
+
this.#disposed = true;
|
|
918
|
+
this.#map = /* @__PURE__ */ new WeakMap();
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* Checks if the store has been disposed.
|
|
922
|
+
*/
|
|
923
|
+
get disposed() {
|
|
924
|
+
return this.#disposed;
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* Checks if the store has been disposed and throws if so.
|
|
928
|
+
*/
|
|
929
|
+
#checkDisposed() {
|
|
930
|
+
if (this.#disposed) {
|
|
931
|
+
throw new Error("WeakMemoryStore has been disposed");
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
};
|
|
935
|
+
|
|
936
|
+
export { EventEmitter, MemoryStore, Scheduler, Stats, WeakMemoryStore, createEntry, createInterval, createSafeFinalizationRegistry, createTimer, detectFeatures, getFeatures, getRemainingTTL, isExpired, touchEntry };
|
|
937
|
+
//# sourceMappingURL=index.js.map
|
|
938
|
+
//# sourceMappingURL=index.js.map
|