@push.rocks/smartregistry 2.3.0 → 2.5.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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.smartregistry.d.ts +33 -2
- package/dist_ts/classes.smartregistry.js +38 -5
- package/dist_ts/core/classes.authmanager.d.ts +30 -80
- package/dist_ts/core/classes.authmanager.js +63 -337
- package/dist_ts/core/classes.defaultauthprovider.d.ts +78 -0
- package/dist_ts/core/classes.defaultauthprovider.js +311 -0
- package/dist_ts/core/classes.registrystorage.d.ts +70 -4
- package/dist_ts/core/classes.registrystorage.js +165 -5
- package/dist_ts/core/index.d.ts +3 -0
- package/dist_ts/core/index.js +7 -2
- package/dist_ts/core/interfaces.auth.d.ts +83 -0
- package/dist_ts/core/interfaces.auth.js +2 -0
- package/dist_ts/core/interfaces.core.d.ts +35 -0
- package/dist_ts/core/interfaces.storage.d.ts +120 -0
- package/dist_ts/core/interfaces.storage.js +2 -0
- package/dist_ts/upstream/classes.baseupstream.d.ts +2 -2
- package/dist_ts/upstream/classes.baseupstream.js +16 -14
- package/dist_ts/upstream/classes.upstreamcache.d.ts +69 -22
- package/dist_ts/upstream/classes.upstreamcache.js +207 -50
- package/package.json +1 -1
- package/readme.md +225 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.smartregistry.ts +39 -4
- package/ts/core/classes.authmanager.ts +74 -412
- package/ts/core/classes.defaultauthprovider.ts +393 -0
- package/ts/core/classes.registrystorage.ts +199 -5
- package/ts/core/index.ts +8 -1
- package/ts/core/interfaces.auth.ts +91 -0
- package/ts/core/interfaces.core.ts +39 -0
- package/ts/core/interfaces.storage.ts +130 -0
- package/ts/upstream/classes.baseupstream.ts +20 -15
- package/ts/upstream/classes.upstreamcache.ts +256 -53
|
@@ -4,9 +4,23 @@ import type {
|
|
|
4
4
|
IUpstreamFetchContext,
|
|
5
5
|
} from './interfaces.upstream.js';
|
|
6
6
|
import { DEFAULT_CACHE_CONFIG } from './interfaces.upstream.js';
|
|
7
|
+
import type { IStorageBackend } from '../core/interfaces.core.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
|
-
*
|
|
10
|
+
* Cache metadata stored alongside cache entries.
|
|
11
|
+
*/
|
|
12
|
+
interface ICacheMetadata {
|
|
13
|
+
contentType: string;
|
|
14
|
+
headers: Record<string, string>;
|
|
15
|
+
cachedAt: string;
|
|
16
|
+
expiresAt?: string;
|
|
17
|
+
etag?: string;
|
|
18
|
+
upstreamId: string;
|
|
19
|
+
upstreamUrl: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* S3-backed upstream cache with in-memory hot layer.
|
|
10
24
|
*
|
|
11
25
|
* Features:
|
|
12
26
|
* - TTL-based expiration
|
|
@@ -14,26 +28,45 @@ import { DEFAULT_CACHE_CONFIG } from './interfaces.upstream.js';
|
|
|
14
28
|
* - Negative caching (404s)
|
|
15
29
|
* - Content-type aware caching
|
|
16
30
|
* - ETag support for conditional requests
|
|
31
|
+
* - Multi-upstream support via URL-based cache paths
|
|
32
|
+
* - Persistent S3 storage with in-memory hot layer
|
|
17
33
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
34
|
+
* Cache paths are structured as:
|
|
35
|
+
* cache/{escaped-upstream-url}/{protocol}:{method}:{path}
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```typescript
|
|
39
|
+
* // In-memory only (default)
|
|
40
|
+
* const cache = new UpstreamCache(config);
|
|
41
|
+
*
|
|
42
|
+
* // With S3 persistence
|
|
43
|
+
* const cache = new UpstreamCache(config, 10000, storage);
|
|
44
|
+
* ```
|
|
20
45
|
*/
|
|
21
46
|
export class UpstreamCache {
|
|
22
|
-
/**
|
|
23
|
-
private readonly
|
|
47
|
+
/** In-memory hot cache */
|
|
48
|
+
private readonly memoryCache: Map<string, ICacheEntry> = new Map();
|
|
24
49
|
|
|
25
50
|
/** Configuration */
|
|
26
51
|
private readonly config: IUpstreamCacheConfig;
|
|
27
52
|
|
|
28
|
-
/** Maximum cache entries
|
|
29
|
-
private readonly
|
|
53
|
+
/** Maximum in-memory cache entries */
|
|
54
|
+
private readonly maxMemoryEntries: number;
|
|
55
|
+
|
|
56
|
+
/** S3 storage backend (optional) */
|
|
57
|
+
private readonly storage?: IStorageBackend;
|
|
30
58
|
|
|
31
59
|
/** Cleanup interval handle */
|
|
32
60
|
private cleanupInterval: ReturnType<typeof setInterval> | null = null;
|
|
33
61
|
|
|
34
|
-
constructor(
|
|
62
|
+
constructor(
|
|
63
|
+
config?: Partial<IUpstreamCacheConfig>,
|
|
64
|
+
maxMemoryEntries: number = 10000,
|
|
65
|
+
storage?: IStorageBackend
|
|
66
|
+
) {
|
|
35
67
|
this.config = { ...DEFAULT_CACHE_CONFIG, ...config };
|
|
36
|
-
this.
|
|
68
|
+
this.maxMemoryEntries = maxMemoryEntries;
|
|
69
|
+
this.storage = storage;
|
|
37
70
|
|
|
38
71
|
// Start periodic cleanup if caching is enabled
|
|
39
72
|
if (this.config.enabled) {
|
|
@@ -48,17 +81,36 @@ export class UpstreamCache {
|
|
|
48
81
|
return this.config.enabled;
|
|
49
82
|
}
|
|
50
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Check if S3 storage is configured.
|
|
86
|
+
*/
|
|
87
|
+
public hasStorage(): boolean {
|
|
88
|
+
return !!this.storage;
|
|
89
|
+
}
|
|
90
|
+
|
|
51
91
|
/**
|
|
52
92
|
* Get cached entry for a request context.
|
|
93
|
+
* Checks memory first, then falls back to S3.
|
|
53
94
|
* Returns null if not found or expired (unless stale-while-revalidate).
|
|
54
95
|
*/
|
|
55
|
-
public get(context: IUpstreamFetchContext): ICacheEntry | null {
|
|
96
|
+
public async get(context: IUpstreamFetchContext, upstreamUrl?: string): Promise<ICacheEntry | null> {
|
|
56
97
|
if (!this.config.enabled) {
|
|
57
98
|
return null;
|
|
58
99
|
}
|
|
59
100
|
|
|
60
|
-
const key = this.buildCacheKey(context);
|
|
61
|
-
|
|
101
|
+
const key = this.buildCacheKey(context, upstreamUrl);
|
|
102
|
+
|
|
103
|
+
// Check memory cache first
|
|
104
|
+
let entry = this.memoryCache.get(key);
|
|
105
|
+
|
|
106
|
+
// If not in memory and we have storage, check S3
|
|
107
|
+
if (!entry && this.storage) {
|
|
108
|
+
entry = await this.loadFromStorage(key);
|
|
109
|
+
if (entry) {
|
|
110
|
+
// Promote to memory cache
|
|
111
|
+
this.memoryCache.set(key, entry);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
62
114
|
|
|
63
115
|
if (!entry) {
|
|
64
116
|
return null;
|
|
@@ -78,7 +130,10 @@ export class UpstreamCache {
|
|
|
78
130
|
}
|
|
79
131
|
}
|
|
80
132
|
// Entry is too old, remove it
|
|
81
|
-
this.
|
|
133
|
+
this.memoryCache.delete(key);
|
|
134
|
+
if (this.storage) {
|
|
135
|
+
await this.deleteFromStorage(key).catch(() => {});
|
|
136
|
+
}
|
|
82
137
|
return null;
|
|
83
138
|
}
|
|
84
139
|
|
|
@@ -86,26 +141,27 @@ export class UpstreamCache {
|
|
|
86
141
|
}
|
|
87
142
|
|
|
88
143
|
/**
|
|
89
|
-
* Store a response in the cache.
|
|
144
|
+
* Store a response in the cache (memory and optionally S3).
|
|
90
145
|
*/
|
|
91
|
-
public set(
|
|
146
|
+
public async set(
|
|
92
147
|
context: IUpstreamFetchContext,
|
|
93
148
|
data: Buffer,
|
|
94
149
|
contentType: string,
|
|
95
150
|
headers: Record<string, string>,
|
|
96
151
|
upstreamId: string,
|
|
152
|
+
upstreamUrl: string,
|
|
97
153
|
options?: ICacheSetOptions,
|
|
98
|
-
): void {
|
|
154
|
+
): Promise<void> {
|
|
99
155
|
if (!this.config.enabled) {
|
|
100
156
|
return;
|
|
101
157
|
}
|
|
102
158
|
|
|
103
|
-
// Enforce max entries limit
|
|
104
|
-
if (this.
|
|
159
|
+
// Enforce max memory entries limit
|
|
160
|
+
if (this.memoryCache.size >= this.maxMemoryEntries) {
|
|
105
161
|
this.evictOldest();
|
|
106
162
|
}
|
|
107
163
|
|
|
108
|
-
const key = this.buildCacheKey(context);
|
|
164
|
+
const key = this.buildCacheKey(context, upstreamUrl);
|
|
109
165
|
const now = new Date();
|
|
110
166
|
|
|
111
167
|
// Determine TTL based on content type
|
|
@@ -122,18 +178,24 @@ export class UpstreamCache {
|
|
|
122
178
|
stale: false,
|
|
123
179
|
};
|
|
124
180
|
|
|
125
|
-
|
|
181
|
+
// Store in memory
|
|
182
|
+
this.memoryCache.set(key, entry);
|
|
183
|
+
|
|
184
|
+
// Store in S3 if available
|
|
185
|
+
if (this.storage) {
|
|
186
|
+
await this.saveToStorage(key, entry, upstreamUrl).catch(() => {});
|
|
187
|
+
}
|
|
126
188
|
}
|
|
127
189
|
|
|
128
190
|
/**
|
|
129
191
|
* Store a negative cache entry (404 response).
|
|
130
192
|
*/
|
|
131
|
-
public setNegative(context: IUpstreamFetchContext, upstreamId: string): void {
|
|
193
|
+
public async setNegative(context: IUpstreamFetchContext, upstreamId: string, upstreamUrl: string): Promise<void> {
|
|
132
194
|
if (!this.config.enabled || this.config.negativeCacheTtlSeconds <= 0) {
|
|
133
195
|
return;
|
|
134
196
|
}
|
|
135
197
|
|
|
136
|
-
const key = this.buildCacheKey(context);
|
|
198
|
+
const key = this.buildCacheKey(context, upstreamUrl);
|
|
137
199
|
const now = new Date();
|
|
138
200
|
|
|
139
201
|
const entry: ICacheEntry = {
|
|
@@ -146,34 +208,47 @@ export class UpstreamCache {
|
|
|
146
208
|
stale: false,
|
|
147
209
|
};
|
|
148
210
|
|
|
149
|
-
this.
|
|
211
|
+
this.memoryCache.set(key, entry);
|
|
212
|
+
|
|
213
|
+
if (this.storage) {
|
|
214
|
+
await this.saveToStorage(key, entry, upstreamUrl).catch(() => {});
|
|
215
|
+
}
|
|
150
216
|
}
|
|
151
217
|
|
|
152
218
|
/**
|
|
153
219
|
* Check if there's a negative cache entry for this context.
|
|
154
220
|
*/
|
|
155
|
-
public hasNegative(context: IUpstreamFetchContext): boolean {
|
|
156
|
-
const entry = this.get(context);
|
|
221
|
+
public async hasNegative(context: IUpstreamFetchContext, upstreamUrl?: string): Promise<boolean> {
|
|
222
|
+
const entry = await this.get(context, upstreamUrl);
|
|
157
223
|
return entry !== null && entry.data.length === 0;
|
|
158
224
|
}
|
|
159
225
|
|
|
160
226
|
/**
|
|
161
227
|
* Invalidate a specific cache entry.
|
|
162
228
|
*/
|
|
163
|
-
public invalidate(context: IUpstreamFetchContext): boolean {
|
|
164
|
-
const key = this.buildCacheKey(context);
|
|
165
|
-
|
|
229
|
+
public async invalidate(context: IUpstreamFetchContext, upstreamUrl?: string): Promise<boolean> {
|
|
230
|
+
const key = this.buildCacheKey(context, upstreamUrl);
|
|
231
|
+
const deleted = this.memoryCache.delete(key);
|
|
232
|
+
|
|
233
|
+
if (this.storage) {
|
|
234
|
+
await this.deleteFromStorage(key).catch(() => {});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return deleted;
|
|
166
238
|
}
|
|
167
239
|
|
|
168
240
|
/**
|
|
169
241
|
* Invalidate all entries matching a pattern.
|
|
170
242
|
* Useful for invalidating all versions of a package.
|
|
171
243
|
*/
|
|
172
|
-
public invalidatePattern(pattern: RegExp): number {
|
|
244
|
+
public async invalidatePattern(pattern: RegExp): Promise<number> {
|
|
173
245
|
let count = 0;
|
|
174
|
-
for (const key of this.
|
|
246
|
+
for (const key of this.memoryCache.keys()) {
|
|
175
247
|
if (pattern.test(key)) {
|
|
176
|
-
this.
|
|
248
|
+
this.memoryCache.delete(key);
|
|
249
|
+
if (this.storage) {
|
|
250
|
+
await this.deleteFromStorage(key).catch(() => {});
|
|
251
|
+
}
|
|
177
252
|
count++;
|
|
178
253
|
}
|
|
179
254
|
}
|
|
@@ -183,11 +258,14 @@ export class UpstreamCache {
|
|
|
183
258
|
/**
|
|
184
259
|
* Invalidate all entries from a specific upstream.
|
|
185
260
|
*/
|
|
186
|
-
public invalidateUpstream(upstreamId: string): number {
|
|
261
|
+
public async invalidateUpstream(upstreamId: string): Promise<number> {
|
|
187
262
|
let count = 0;
|
|
188
|
-
for (const [key, entry] of this.
|
|
263
|
+
for (const [key, entry] of this.memoryCache.entries()) {
|
|
189
264
|
if (entry.upstreamId === upstreamId) {
|
|
190
|
-
this.
|
|
265
|
+
this.memoryCache.delete(key);
|
|
266
|
+
if (this.storage) {
|
|
267
|
+
await this.deleteFromStorage(key).catch(() => {});
|
|
268
|
+
}
|
|
191
269
|
count++;
|
|
192
270
|
}
|
|
193
271
|
}
|
|
@@ -195,10 +273,13 @@ export class UpstreamCache {
|
|
|
195
273
|
}
|
|
196
274
|
|
|
197
275
|
/**
|
|
198
|
-
* Clear all cache entries.
|
|
276
|
+
* Clear all cache entries (memory and S3).
|
|
199
277
|
*/
|
|
200
|
-
public clear(): void {
|
|
201
|
-
this.
|
|
278
|
+
public async clear(): Promise<void> {
|
|
279
|
+
this.memoryCache.clear();
|
|
280
|
+
|
|
281
|
+
// Note: S3 cleanup would require listing and deleting all cache/* objects
|
|
282
|
+
// This is left as a future enhancement for bulk cleanup
|
|
202
283
|
}
|
|
203
284
|
|
|
204
285
|
/**
|
|
@@ -211,7 +292,7 @@ export class UpstreamCache {
|
|
|
211
292
|
let totalSize = 0;
|
|
212
293
|
const now = new Date();
|
|
213
294
|
|
|
214
|
-
for (const entry of this.
|
|
295
|
+
for (const entry of this.memoryCache.values()) {
|
|
215
296
|
totalSize += entry.data.length;
|
|
216
297
|
|
|
217
298
|
if (entry.data.length === 0) {
|
|
@@ -224,13 +305,14 @@ export class UpstreamCache {
|
|
|
224
305
|
}
|
|
225
306
|
|
|
226
307
|
return {
|
|
227
|
-
totalEntries: this.
|
|
308
|
+
totalEntries: this.memoryCache.size,
|
|
228
309
|
freshEntries: freshCount,
|
|
229
310
|
staleEntries: staleCount,
|
|
230
311
|
negativeEntries: negativeCount,
|
|
231
312
|
totalSizeBytes: totalSize,
|
|
232
|
-
maxEntries: this.
|
|
313
|
+
maxEntries: this.maxMemoryEntries,
|
|
233
314
|
enabled: this.config.enabled,
|
|
315
|
+
hasStorage: !!this.storage,
|
|
234
316
|
};
|
|
235
317
|
}
|
|
236
318
|
|
|
@@ -244,17 +326,136 @@ export class UpstreamCache {
|
|
|
244
326
|
}
|
|
245
327
|
}
|
|
246
328
|
|
|
329
|
+
// ========================================================================
|
|
330
|
+
// Storage Methods
|
|
331
|
+
// ========================================================================
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Build storage path for a cache key.
|
|
335
|
+
* Escapes upstream URL for safe use in S3 paths.
|
|
336
|
+
*/
|
|
337
|
+
private buildStoragePath(key: string): string {
|
|
338
|
+
return `cache/${key}`;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Build storage path for cache metadata.
|
|
343
|
+
*/
|
|
344
|
+
private buildMetadataPath(key: string): string {
|
|
345
|
+
return `cache/${key}.meta`;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Load a cache entry from S3 storage.
|
|
350
|
+
*/
|
|
351
|
+
private async loadFromStorage(key: string): Promise<ICacheEntry | null> {
|
|
352
|
+
if (!this.storage) return null;
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
const dataPath = this.buildStoragePath(key);
|
|
356
|
+
const metaPath = this.buildMetadataPath(key);
|
|
357
|
+
|
|
358
|
+
// Load data and metadata in parallel
|
|
359
|
+
const [data, metaBuffer] = await Promise.all([
|
|
360
|
+
this.storage.getObject(dataPath),
|
|
361
|
+
this.storage.getObject(metaPath),
|
|
362
|
+
]);
|
|
363
|
+
|
|
364
|
+
if (!data || !metaBuffer) {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const meta: ICacheMetadata = JSON.parse(metaBuffer.toString('utf-8'));
|
|
369
|
+
|
|
370
|
+
return {
|
|
371
|
+
data,
|
|
372
|
+
contentType: meta.contentType,
|
|
373
|
+
headers: meta.headers,
|
|
374
|
+
cachedAt: new Date(meta.cachedAt),
|
|
375
|
+
expiresAt: meta.expiresAt ? new Date(meta.expiresAt) : undefined,
|
|
376
|
+
etag: meta.etag,
|
|
377
|
+
upstreamId: meta.upstreamId,
|
|
378
|
+
stale: false,
|
|
379
|
+
};
|
|
380
|
+
} catch {
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Save a cache entry to S3 storage.
|
|
387
|
+
*/
|
|
388
|
+
private async saveToStorage(key: string, entry: ICacheEntry, upstreamUrl: string): Promise<void> {
|
|
389
|
+
if (!this.storage) return;
|
|
390
|
+
|
|
391
|
+
const dataPath = this.buildStoragePath(key);
|
|
392
|
+
const metaPath = this.buildMetadataPath(key);
|
|
393
|
+
|
|
394
|
+
const meta: ICacheMetadata = {
|
|
395
|
+
contentType: entry.contentType,
|
|
396
|
+
headers: entry.headers,
|
|
397
|
+
cachedAt: entry.cachedAt.toISOString(),
|
|
398
|
+
expiresAt: entry.expiresAt?.toISOString(),
|
|
399
|
+
etag: entry.etag,
|
|
400
|
+
upstreamId: entry.upstreamId,
|
|
401
|
+
upstreamUrl,
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
// Save data and metadata in parallel
|
|
405
|
+
await Promise.all([
|
|
406
|
+
this.storage.putObject(dataPath, entry.data),
|
|
407
|
+
this.storage.putObject(metaPath, Buffer.from(JSON.stringify(meta), 'utf-8')),
|
|
408
|
+
]);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Delete a cache entry from S3 storage.
|
|
413
|
+
*/
|
|
414
|
+
private async deleteFromStorage(key: string): Promise<void> {
|
|
415
|
+
if (!this.storage) return;
|
|
416
|
+
|
|
417
|
+
const dataPath = this.buildStoragePath(key);
|
|
418
|
+
const metaPath = this.buildMetadataPath(key);
|
|
419
|
+
|
|
420
|
+
await Promise.all([
|
|
421
|
+
this.storage.deleteObject(dataPath).catch(() => {}),
|
|
422
|
+
this.storage.deleteObject(metaPath).catch(() => {}),
|
|
423
|
+
]);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// ========================================================================
|
|
427
|
+
// Helper Methods
|
|
428
|
+
// ========================================================================
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Escape a URL for safe use in storage paths.
|
|
432
|
+
*/
|
|
433
|
+
private escapeUrl(url: string): string {
|
|
434
|
+
// Remove protocol prefix and escape special characters
|
|
435
|
+
return url
|
|
436
|
+
.replace(/^https?:\/\//, '')
|
|
437
|
+
.replace(/[\/\\:*?"<>|]/g, '_')
|
|
438
|
+
.replace(/__+/g, '_');
|
|
439
|
+
}
|
|
440
|
+
|
|
247
441
|
/**
|
|
248
442
|
* Build a unique cache key for a request context.
|
|
443
|
+
* Includes escaped upstream URL for multi-upstream support.
|
|
249
444
|
*/
|
|
250
|
-
private buildCacheKey(context: IUpstreamFetchContext): string {
|
|
445
|
+
private buildCacheKey(context: IUpstreamFetchContext, upstreamUrl?: string): string {
|
|
251
446
|
// Include method, protocol, path, and sorted query params
|
|
252
447
|
const queryString = Object.keys(context.query)
|
|
253
448
|
.sort()
|
|
254
449
|
.map(k => `${k}=${context.query[k]}`)
|
|
255
450
|
.join('&');
|
|
256
451
|
|
|
257
|
-
|
|
452
|
+
const baseKey = `${context.protocol}:${context.method}:${context.path}${queryString ? '?' + queryString : ''}`;
|
|
453
|
+
|
|
454
|
+
if (upstreamUrl) {
|
|
455
|
+
return `${this.escapeUrl(upstreamUrl)}/${baseKey}`;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return baseKey;
|
|
258
459
|
}
|
|
259
460
|
|
|
260
461
|
/**
|
|
@@ -333,27 +534,27 @@ export class UpstreamCache {
|
|
|
333
534
|
*/
|
|
334
535
|
private evictOldest(): void {
|
|
335
536
|
// Evict 10% of max entries
|
|
336
|
-
const evictCount = Math.ceil(this.
|
|
537
|
+
const evictCount = Math.ceil(this.maxMemoryEntries * 0.1);
|
|
337
538
|
let evicted = 0;
|
|
338
539
|
|
|
339
540
|
// First, try to evict stale entries
|
|
340
541
|
const now = new Date();
|
|
341
|
-
for (const [key, entry] of this.
|
|
542
|
+
for (const [key, entry] of this.memoryCache.entries()) {
|
|
342
543
|
if (evicted >= evictCount) break;
|
|
343
544
|
if (entry.stale || (entry.expiresAt && entry.expiresAt < now)) {
|
|
344
|
-
this.
|
|
545
|
+
this.memoryCache.delete(key);
|
|
345
546
|
evicted++;
|
|
346
547
|
}
|
|
347
548
|
}
|
|
348
549
|
|
|
349
550
|
// If not enough evicted, evict oldest by cachedAt
|
|
350
551
|
if (evicted < evictCount) {
|
|
351
|
-
const entries = Array.from(this.
|
|
552
|
+
const entries = Array.from(this.memoryCache.entries())
|
|
352
553
|
.sort((a, b) => a[1].cachedAt.getTime() - b[1].cachedAt.getTime());
|
|
353
554
|
|
|
354
555
|
for (const [key] of entries) {
|
|
355
556
|
if (evicted >= evictCount) break;
|
|
356
|
-
this.
|
|
557
|
+
this.memoryCache.delete(key);
|
|
357
558
|
evicted++;
|
|
358
559
|
}
|
|
359
560
|
}
|
|
@@ -375,17 +576,17 @@ export class UpstreamCache {
|
|
|
375
576
|
}
|
|
376
577
|
|
|
377
578
|
/**
|
|
378
|
-
* Remove all expired entries.
|
|
579
|
+
* Remove all expired entries from memory cache.
|
|
379
580
|
*/
|
|
380
581
|
private cleanup(): void {
|
|
381
582
|
const now = new Date();
|
|
382
583
|
const staleDeadline = new Date(now.getTime() - this.config.staleMaxAgeSeconds * 1000);
|
|
383
584
|
|
|
384
|
-
for (const [key, entry] of this.
|
|
585
|
+
for (const [key, entry] of this.memoryCache.entries()) {
|
|
385
586
|
if (entry.expiresAt) {
|
|
386
587
|
// Remove if past stale deadline
|
|
387
588
|
if (entry.expiresAt < staleDeadline) {
|
|
388
|
-
this.
|
|
589
|
+
this.memoryCache.delete(key);
|
|
389
590
|
}
|
|
390
591
|
}
|
|
391
592
|
}
|
|
@@ -406,7 +607,7 @@ export interface ICacheSetOptions {
|
|
|
406
607
|
* Cache statistics.
|
|
407
608
|
*/
|
|
408
609
|
export interface ICacheStats {
|
|
409
|
-
/** Total number of cached entries */
|
|
610
|
+
/** Total number of cached entries in memory */
|
|
410
611
|
totalEntries: number;
|
|
411
612
|
/** Number of fresh (non-expired) entries */
|
|
412
613
|
freshEntries: number;
|
|
@@ -414,10 +615,12 @@ export interface ICacheStats {
|
|
|
414
615
|
staleEntries: number;
|
|
415
616
|
/** Number of negative cache entries */
|
|
416
617
|
negativeEntries: number;
|
|
417
|
-
/** Total size of cached data in bytes */
|
|
618
|
+
/** Total size of cached data in bytes (memory only) */
|
|
418
619
|
totalSizeBytes: number;
|
|
419
|
-
/** Maximum allowed entries */
|
|
620
|
+
/** Maximum allowed memory entries */
|
|
420
621
|
maxEntries: number;
|
|
421
622
|
/** Whether caching is enabled */
|
|
422
623
|
enabled: boolean;
|
|
624
|
+
/** Whether S3 storage is configured */
|
|
625
|
+
hasStorage: boolean;
|
|
423
626
|
}
|