webspresso 0.0.104 → 0.0.105
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/.agents/AGENTS.md +6 -0
- package/.agents/ARCHITECTURE.md +23 -0
- package/.agents/CACHE.md +147 -0
- package/.agents/CLI.md +1 -1
- package/.agents/EARLY_HINTS.md +120 -0
- package/.agents/HEALTH.md +118 -0
- package/.agents/LIMITS.md +109 -0
- package/.agents/ORM.md +155 -1
- package/.agents/PLUGINS.md +28 -0
- package/.agents/RESILIENCE.md +235 -0
- package/AGENTS.md +6 -0
- package/README.md +2 -2
- package/bin/commands/add-docker.js +6 -6
- package/bin/commands/dev.js +1 -1
- package/bin/commands/new.js +5 -5
- package/bin/commands/openapi.js +1 -1
- package/bin/commands/start.js +1 -1
- package/bin/utils/project.js +2 -2
- package/core/auth/hash.js +5 -4
- package/core/auth/jwt.js +5 -54
- package/core/auth/magic-link.js +4 -5
- package/core/auth/manager.js +3 -5
- package/core/auth/oauth.js +8 -17
- package/core/auth/totp.js +2 -1
- package/core/cache/byte-size.js +123 -0
- package/core/cache/index.js +21 -0
- package/core/cache/lru.js +477 -0
- package/core/cache/tagged-lru.js +346 -0
- package/core/commands/registry.js +6 -20
- package/core/early-hints/index.js +267 -0
- package/core/errors/domain.js +48 -0
- package/core/errors/http.js +31 -0
- package/core/errors/index.js +10 -0
- package/core/errors/normalize.js +88 -2
- package/core/events/discovery.js +6 -24
- package/core/events/dispatcher.js +2 -7
- package/core/health/index.js +41 -0
- package/core/health/middleware.js +213 -0
- package/core/health/normalize.js +140 -0
- package/core/health/probes.js +203 -0
- package/core/limits/index.js +34 -0
- package/core/limits/middleware.js +317 -0
- package/core/limits/normalize.js +222 -0
- package/core/orm/batch.js +70 -0
- package/core/orm/cache/fingerprint.js +2 -2
- package/core/orm/cache/memory-provider.js +27 -110
- package/core/orm/index.js +71 -8
- package/core/orm/json-fields.js +22 -8
- package/core/orm/model.js +10 -0
- package/core/orm/query-builder.js +63 -9
- package/core/orm/repository.js +318 -19
- package/core/orm/schema-helpers.js +8 -10
- package/core/orm/scopes.js +321 -1
- package/core/orm/transaction.js +10 -2
- package/core/orm/utils.js +4 -86
- package/core/queue/adapters/database.js +4 -8
- package/core/queue/manager.js +55 -41
- package/core/resilience/circuit-breaker.js +391 -0
- package/core/resilience/errors.js +63 -0
- package/core/resilience/index.js +53 -0
- package/core/resilience/registry.js +97 -0
- package/core/resilience/retry.js +203 -0
- package/core/schema-cache.js +9 -2
- package/core/shutdown/http-adapter.js +4 -1
- package/core/shutdown/shutdown-manager.js +120 -19
- package/core/utils/array.js +12 -0
- package/core/utils/base64url.js +8 -0
- package/core/utils/crypto.js +118 -0
- package/core/utils/ensure-leading-slash.js +6 -0
- package/core/utils/escape-regexp.js +7 -0
- package/core/utils/fetch.js +6 -5
- package/core/utils/fs.js +144 -0
- package/core/utils/hmac.js +6 -0
- package/core/utils/json.js +98 -0
- package/core/utils/object.js +106 -0
- package/core/utils/random.js +9 -0
- package/core/utils/regex.js +77 -0
- package/core/utils/sha256.js +6 -0
- package/core/utils/string.js +87 -0
- package/core/utils/strip-trailing-slash.js +6 -0
- package/core/utils/time.js +96 -0
- package/core/utils/timeout.js +348 -0
- package/core/utils/url.js +106 -0
- package/core/utils/walk.js +7 -0
- package/index.d.ts +616 -1
- package/index.js +148 -0
- package/package.json +1 -2
- package/plugins/admin-panel/core/api-extensions.js +2 -5
- package/plugins/admin-panel/index.js +1 -1
- package/plugins/admin-panel/modules/menu.js +1 -1
- package/plugins/api-key/crypto.js +3 -3
- package/plugins/api-key/scopes.js +5 -7
- package/plugins/audit-log/api-handlers.js +6 -15
- package/plugins/audit-log/parse.js +3 -3
- package/plugins/csrf/index.js +5 -9
- package/plugins/dashboard/app.js +2 -2
- package/plugins/dashboard/index.js +5 -5
- package/plugins/data-exchange/record-selection.js +2 -5
- package/plugins/email/api-handlers.js +5 -18
- package/plugins/email/auth-bridge.js +3 -2
- package/plugins/health-check.js +2 -1
- package/plugins/index.js +2 -0
- package/plugins/lazy.js +5 -6
- package/plugins/mcp/discovery.js +18 -17
- package/plugins/polar/src/api.js +2 -1
- package/plugins/polar/src/webhooks.js +5 -6
- package/plugins/rate-limit/index.js +3 -1
- package/plugins/redirect/index.js +9 -7
- package/plugins/rls/index.js +170 -0
- package/plugins/site-analytics/tracking.js +2 -2
- package/plugins/sitemap.js +3 -2
- package/plugins/swagger.js +4 -3
- package/plugins/upload/index.js +4 -3
- package/plugins/upload/orphan-pruner.js +4 -3
- package/plugins/webhooks/crypto.js +6 -11
- package/plugins/webhooks/dispatcher.js +8 -8
- package/plugins/webhooks/index.js +6 -0
- package/plugins/webhooks/matcher.js +7 -11
- package/plugins/webhooks/sender.js +8 -11
- package/src/app-context.js +41 -5
- package/src/dev/banner.js +6 -2
- package/src/file-router.js +1 -3
- package/src/helpers.js +159 -24
- package/src/modules/module-discovery.js +1 -9
- package/src/pages/page-loader.js +53 -1
- package/src/plugin-manager.js +90 -17
- package/src/server.js +136 -25
- package/src/services/discovery.js +19 -40
- package/src/services/executor.js +86 -25
- package/src/services/memoize.js +3 -25
- package/src/services/registry.js +6 -0
- package/src/template/precompile.js +8 -19
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso - Zero-Dependency High-Performance LRU Cache
|
|
5
|
+
* O(1) Least-Recently-Used in-memory cache with memory byte budget, TTL expiration,
|
|
6
|
+
* stampede protection (thundering herd prevention), and detailed observability metrics.
|
|
7
|
+
* @module core/cache/lru
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { approximateByteSize } = require('./byte-size');
|
|
11
|
+
const { parseDurationMs } = require('../utils/time');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Deep clone utility to isolate cached values if cloning is requested
|
|
15
|
+
* @param {*} value
|
|
16
|
+
* @returns {*}
|
|
17
|
+
*/
|
|
18
|
+
function safeClone(value) {
|
|
19
|
+
if (value === null || typeof value !== 'object') {
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
if (typeof structuredClone === 'function') {
|
|
23
|
+
try {
|
|
24
|
+
return structuredClone(value);
|
|
25
|
+
} catch {
|
|
26
|
+
// Fallback if structuredClone fails on non-serializable elements
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (Array.isArray(value)) {
|
|
30
|
+
return value.map(item => safeClone(item));
|
|
31
|
+
}
|
|
32
|
+
if (value instanceof Date) {
|
|
33
|
+
return new Date(value.getTime());
|
|
34
|
+
}
|
|
35
|
+
const cloned = {};
|
|
36
|
+
for (const [k, v] of Object.entries(value)) {
|
|
37
|
+
cloned[k] = safeClone(v);
|
|
38
|
+
}
|
|
39
|
+
return cloned;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @typedef {Object} LruCacheOptions
|
|
44
|
+
* @property {number} [maxEntries=1000] - Maximum entry count (0 = unlimited count)
|
|
45
|
+
* @property {number} [maxBytes=0] - Maximum byte capacity (0 = disabled)
|
|
46
|
+
* @property {number} [maxMemoryMb=0] - Maximum megabytes capacity (converts to bytes)
|
|
47
|
+
* @property {string|number} [defaultTtl=0] - Default TTL in ms or string duration ('5m', '1h', 0 = no expiry)
|
|
48
|
+
* @property {function(*, string): number} [sizeCalculation] - Custom byte sizer function
|
|
49
|
+
* @property {boolean} [clone=false] - Whether to deep-clone items on read/write
|
|
50
|
+
* @property {function(string, *, 'evict'|'expire'|'delete'|'clear'): void} [onEvict] - Eviction callback
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
class LruCache {
|
|
54
|
+
/**
|
|
55
|
+
* @param {LruCacheOptions} [options]
|
|
56
|
+
*/
|
|
57
|
+
constructor(options = {}) {
|
|
58
|
+
this.maxEntries = typeof options.maxEntries === 'number' ? Math.max(0, options.maxEntries) : 1000;
|
|
59
|
+
|
|
60
|
+
// Memory budget configuration
|
|
61
|
+
if (typeof options.maxMemoryMb === 'number' && options.maxMemoryMb > 0) {
|
|
62
|
+
this.maxBytes = Math.floor(options.maxMemoryMb * 1024 * 1024);
|
|
63
|
+
} else if (typeof options.maxBytes === 'number' && options.maxBytes > 0) {
|
|
64
|
+
this.maxBytes = Math.floor(options.maxBytes);
|
|
65
|
+
} else {
|
|
66
|
+
this.maxBytes = 0; // 0 = unlimited bytes
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
this.defaultTtlMs = options.defaultTtl ? parseDurationMs(options.defaultTtl, 0) : 0;
|
|
70
|
+
this.sizeCalculation = typeof options.sizeCalculation === 'function' ? options.sizeCalculation : null;
|
|
71
|
+
this.clone = options.clone === true;
|
|
72
|
+
this.onEvict = typeof options.onEvict === 'function' ? options.onEvict : null;
|
|
73
|
+
|
|
74
|
+
/** @type {Map<string, { value: *, size: number, expiresAt: number }>} */
|
|
75
|
+
this._store = new Map();
|
|
76
|
+
|
|
77
|
+
/** @type {Map<string, Promise<*>>} In-flight promises for stampede protection */
|
|
78
|
+
this._inFlight = new Map();
|
|
79
|
+
|
|
80
|
+
this.currentBytes = 0;
|
|
81
|
+
|
|
82
|
+
// Observability metrics
|
|
83
|
+
this.hits = 0;
|
|
84
|
+
this.misses = 0;
|
|
85
|
+
this.sets = 0;
|
|
86
|
+
this.evictions = 0;
|
|
87
|
+
this.expirations = 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Number of active entries stored in the cache
|
|
92
|
+
* @type {number}
|
|
93
|
+
*/
|
|
94
|
+
get size() {
|
|
95
|
+
return this._store.size;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Hit rate ratio (0.0 to 1.0)
|
|
100
|
+
* @type {number}
|
|
101
|
+
*/
|
|
102
|
+
get hitRate() {
|
|
103
|
+
const total = this.hits + this.misses;
|
|
104
|
+
return total > 0 ? this.hits / total : 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Calculate size of an item
|
|
109
|
+
* @private
|
|
110
|
+
*/
|
|
111
|
+
_calcSize(value, key, explicitSize) {
|
|
112
|
+
if (typeof explicitSize === 'number' && explicitSize >= 0) {
|
|
113
|
+
return explicitSize;
|
|
114
|
+
}
|
|
115
|
+
if (this.sizeCalculation) {
|
|
116
|
+
return Math.max(0, this.sizeCalculation(value, key));
|
|
117
|
+
}
|
|
118
|
+
return Math.max(0, approximateByteSize(value));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Check if an entry has expired
|
|
123
|
+
* @private
|
|
124
|
+
*/
|
|
125
|
+
_isExpired(entry) {
|
|
126
|
+
return entry.expiresAt > 0 && Date.now() > entry.expiresAt;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Remove an entry and invoke eviction handler
|
|
131
|
+
* @private
|
|
132
|
+
*/
|
|
133
|
+
_removeEntry(key, entry, reason) {
|
|
134
|
+
this._store.delete(key);
|
|
135
|
+
this.currentBytes = Math.max(0, this.currentBytes - entry.size);
|
|
136
|
+
|
|
137
|
+
if (this.onEvict) {
|
|
138
|
+
try {
|
|
139
|
+
this.onEvict(key, entry.value, reason);
|
|
140
|
+
} catch (err) {
|
|
141
|
+
// Eviction callbacks must never crash the cache operations
|
|
142
|
+
process.nextTick(() => {
|
|
143
|
+
console.error(`[LruCache] onEvict callback failed for key "${key}":`, err);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Evict the least recently used item (the first key in Map insertion order)
|
|
151
|
+
* @private
|
|
152
|
+
*/
|
|
153
|
+
_evictLru() {
|
|
154
|
+
const oldestKey = this._store.keys().next().value;
|
|
155
|
+
if (oldestKey === undefined) return false;
|
|
156
|
+
|
|
157
|
+
const entry = this._store.get(oldestKey);
|
|
158
|
+
if (!entry) return false;
|
|
159
|
+
|
|
160
|
+
this.evictions++;
|
|
161
|
+
this._removeEntry(oldestKey, entry, 'evict');
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Retrieve an item from the cache. Updates its LRU position to most-recently used.
|
|
167
|
+
*
|
|
168
|
+
* @param {string} key
|
|
169
|
+
* @returns {*} Cached value or undefined
|
|
170
|
+
*/
|
|
171
|
+
get(key) {
|
|
172
|
+
const entry = this._store.get(key);
|
|
173
|
+
|
|
174
|
+
if (!entry) {
|
|
175
|
+
this.misses++;
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (this._isExpired(entry)) {
|
|
180
|
+
this.expirations++;
|
|
181
|
+
this.misses++;
|
|
182
|
+
this._removeEntry(key, entry, 'expire');
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Refresh LRU position by deleting and re-inserting at the end of the Map
|
|
187
|
+
this._store.delete(key);
|
|
188
|
+
this._store.set(key, entry);
|
|
189
|
+
|
|
190
|
+
this.hits++;
|
|
191
|
+
return this.clone ? safeClone(entry.value) : entry.value;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Read value without updating LRU position
|
|
196
|
+
*
|
|
197
|
+
* @param {string} key
|
|
198
|
+
* @returns {*}
|
|
199
|
+
*/
|
|
200
|
+
peek(key) {
|
|
201
|
+
const entry = this._store.get(key);
|
|
202
|
+
if (!entry) return undefined;
|
|
203
|
+
|
|
204
|
+
if (this._isExpired(entry)) {
|
|
205
|
+
this.expirations++;
|
|
206
|
+
this._removeEntry(key, entry, 'expire');
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return this.clone ? safeClone(entry.value) : entry.value;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Check if a key exists and is not expired
|
|
215
|
+
*
|
|
216
|
+
* @param {string} key
|
|
217
|
+
* @returns {boolean}
|
|
218
|
+
*/
|
|
219
|
+
has(key) {
|
|
220
|
+
const entry = this._store.get(key);
|
|
221
|
+
if (!entry) return false;
|
|
222
|
+
|
|
223
|
+
if (this._isExpired(entry)) {
|
|
224
|
+
this.expirations++;
|
|
225
|
+
this._removeEntry(key, entry, 'expire');
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Store a key/value pair in the cache
|
|
234
|
+
*
|
|
235
|
+
* @param {string} key
|
|
236
|
+
* @param {*} value
|
|
237
|
+
* @param {Object} [options]
|
|
238
|
+
* @param {number|string} [options.ttl] - TTL duration string or ms
|
|
239
|
+
* @param {number} [options.ttlMs] - TTL duration in ms
|
|
240
|
+
* @param {number} [options.size] - Explicit byte size
|
|
241
|
+
* @returns {this}
|
|
242
|
+
*/
|
|
243
|
+
set(key, value, options = {}) {
|
|
244
|
+
const entrySize = this._calcSize(value, key, options.size);
|
|
245
|
+
|
|
246
|
+
// If a single entry exceeds the entire byte budget, it cannot fit
|
|
247
|
+
if (this.maxBytes > 0 && entrySize > this.maxBytes) {
|
|
248
|
+
return this;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// If key already exists, subtract its previous size before calculating space
|
|
252
|
+
const existing = this._store.get(key);
|
|
253
|
+
if (existing) {
|
|
254
|
+
this._removeEntry(key, existing, 'delete');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Capacity enforcement loop: evict LRU entries until under entry and byte limits
|
|
258
|
+
while (
|
|
259
|
+
(this.maxEntries > 0 && this._store.size >= this.maxEntries) ||
|
|
260
|
+
(this.maxBytes > 0 && (this.currentBytes + entrySize) > this.maxBytes)
|
|
261
|
+
) {
|
|
262
|
+
const evicted = this._evictLru();
|
|
263
|
+
if (!evicted) break;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
let ttlMs = this.defaultTtlMs;
|
|
267
|
+
if (typeof options.ttlMs === 'number') {
|
|
268
|
+
ttlMs = Math.max(0, options.ttlMs);
|
|
269
|
+
} else if (options.ttl !== undefined) {
|
|
270
|
+
ttlMs = parseDurationMs(options.ttl, 0);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const expiresAt = ttlMs > 0 ? Date.now() + ttlMs : 0;
|
|
274
|
+
const storedValue = this.clone ? safeClone(value) : value;
|
|
275
|
+
|
|
276
|
+
const entry = {
|
|
277
|
+
value: storedValue,
|
|
278
|
+
size: entrySize,
|
|
279
|
+
expiresAt,
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
this._store.set(key, entry);
|
|
283
|
+
this.currentBytes += entrySize;
|
|
284
|
+
this.sets++;
|
|
285
|
+
|
|
286
|
+
return this;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Delete an entry by key
|
|
291
|
+
*
|
|
292
|
+
* @param {string} key
|
|
293
|
+
* @returns {boolean} True if deleted, false if not found
|
|
294
|
+
*/
|
|
295
|
+
delete(key) {
|
|
296
|
+
const entry = this._store.get(key);
|
|
297
|
+
if (!entry) return false;
|
|
298
|
+
|
|
299
|
+
this._removeEntry(key, entry, 'delete');
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Alias for delete
|
|
305
|
+
*/
|
|
306
|
+
del(key) {
|
|
307
|
+
return this.delete(key);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Purge all items from the cache
|
|
312
|
+
* @returns {this}
|
|
313
|
+
*/
|
|
314
|
+
clear() {
|
|
315
|
+
if (this.onEvict) {
|
|
316
|
+
for (const [k, entry] of this._store.entries()) {
|
|
317
|
+
try {
|
|
318
|
+
this.onEvict(k, entry.value, 'clear');
|
|
319
|
+
} catch {
|
|
320
|
+
// Ignore clear errors
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
this._store.clear();
|
|
325
|
+
this._inFlight.clear();
|
|
326
|
+
this.currentBytes = 0;
|
|
327
|
+
return this;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Sweeps and removes all expired entries actively
|
|
332
|
+
* @returns {number} Number of expired items pruned
|
|
333
|
+
*/
|
|
334
|
+
pruneExpired() {
|
|
335
|
+
let count = 0;
|
|
336
|
+
const now = Date.now();
|
|
337
|
+
for (const [key, entry] of this._store.entries()) {
|
|
338
|
+
if (entry.expiresAt > 0 && now > entry.expiresAt) {
|
|
339
|
+
this.expirations++;
|
|
340
|
+
this._removeEntry(key, entry, 'expire');
|
|
341
|
+
count++;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return count;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Thundering herd / cache stampede protection.
|
|
349
|
+
* If item exists, returns it. If not, calls asyncFactory once and deduplicates
|
|
350
|
+
* concurrent requests awaiting the exact same key.
|
|
351
|
+
*
|
|
352
|
+
* @param {string} key
|
|
353
|
+
* @param {function(string): Promise<*>} asyncFactory
|
|
354
|
+
* @param {Object} [options] - Options passed to .set()
|
|
355
|
+
* @returns {Promise<*>}
|
|
356
|
+
*/
|
|
357
|
+
async getOrSet(key, asyncFactory, options = {}) {
|
|
358
|
+
// 1. Check immediate cache hit
|
|
359
|
+
const cached = this.get(key);
|
|
360
|
+
if (cached !== undefined) {
|
|
361
|
+
return cached;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// 2. Check if another concurrent request is already fetching this key
|
|
365
|
+
if (this._inFlight.has(key)) {
|
|
366
|
+
const inFlightVal = await this._inFlight.get(key);
|
|
367
|
+
return this.clone ? safeClone(inFlightVal) : inFlightVal;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// 3. Launch execution and track in-flight promise
|
|
371
|
+
const executionPromise = (async () => {
|
|
372
|
+
try {
|
|
373
|
+
return await asyncFactory(key);
|
|
374
|
+
} finally {
|
|
375
|
+
this._inFlight.delete(key);
|
|
376
|
+
}
|
|
377
|
+
})();
|
|
378
|
+
|
|
379
|
+
this._inFlight.set(key, executionPromise);
|
|
380
|
+
|
|
381
|
+
let result;
|
|
382
|
+
try {
|
|
383
|
+
result = await executionPromise;
|
|
384
|
+
} catch (err) {
|
|
385
|
+
// Do not cache rejected promises / errors
|
|
386
|
+
throw err;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
this.set(key, result, options);
|
|
390
|
+
return this.clone ? safeClone(result) : result;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Retrieve telemetry snapshot
|
|
395
|
+
*/
|
|
396
|
+
getStats() {
|
|
397
|
+
return {
|
|
398
|
+
entries: this._store.size,
|
|
399
|
+
currentBytes: this.currentBytes,
|
|
400
|
+
maxEntries: this.maxEntries,
|
|
401
|
+
maxBytes: this.maxBytes,
|
|
402
|
+
hits: this.hits,
|
|
403
|
+
misses: this.misses,
|
|
404
|
+
hitRate: this.hitRate,
|
|
405
|
+
sets: this.sets,
|
|
406
|
+
evictions: this.evictions,
|
|
407
|
+
expirations: this.expirations,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Reset observability counters
|
|
413
|
+
*/
|
|
414
|
+
resetStats() {
|
|
415
|
+
this.hits = 0;
|
|
416
|
+
this.misses = 0;
|
|
417
|
+
this.sets = 0;
|
|
418
|
+
this.evictions = 0;
|
|
419
|
+
this.expirations = 0;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Iterate over keys
|
|
424
|
+
*/
|
|
425
|
+
keys() {
|
|
426
|
+
return this._store.keys();
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Iterate over values
|
|
431
|
+
*/
|
|
432
|
+
*values() {
|
|
433
|
+
const now = Date.now();
|
|
434
|
+
for (const entry of this._store.values()) {
|
|
435
|
+
if (!this._isExpired(entry)) {
|
|
436
|
+
yield this.clone ? safeClone(entry.value) : entry.value;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Iterate over [key, value] pairs
|
|
443
|
+
*/
|
|
444
|
+
*entries() {
|
|
445
|
+
const now = Date.now();
|
|
446
|
+
for (const [key, entry] of this._store.entries()) {
|
|
447
|
+
if (!this._isExpired(entry)) {
|
|
448
|
+
yield [key, this.clone ? safeClone(entry.value) : entry.value];
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
[Symbol.iterator]() {
|
|
454
|
+
return this.entries();
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
forEach(callback, thisArg) {
|
|
458
|
+
for (const [key, val] of this.entries()) {
|
|
459
|
+
callback.call(thisArg, val, key, this);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Factory helper
|
|
466
|
+
* @param {LruCacheOptions} [options]
|
|
467
|
+
* @returns {LruCache}
|
|
468
|
+
*/
|
|
469
|
+
function createLruCache(options) {
|
|
470
|
+
return new LruCache(options);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
module.exports = {
|
|
474
|
+
LruCache,
|
|
475
|
+
createLruCache,
|
|
476
|
+
safeClone,
|
|
477
|
+
};
|