@happyvertical/cache 0.74.8

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/AGENT.md ADDED
@@ -0,0 +1,33 @@
1
+ # @happyvertical/cache
2
+
3
+ <!-- BEGIN AGENT:GENERATED -->
4
+ ## Purpose
5
+ Standardized caching interface supporting Memory, File, and Redis backends
6
+
7
+ ## Package Map
8
+ - Package: `@happyvertical/cache`
9
+ - Hierarchy path: `@happyvertical/sdk > packages > cache`
10
+ - Workspace position: `5 of 30` local packages
11
+ - Internal dependencies: `@happyvertical/utils`
12
+ - Internal dependents: `@happyvertical/geo`, `@happyvertical/translator`
13
+ - Knowledge graph files: `AGENT.md`, `metadata.json`, `ecosystem-manifest.json`
14
+
15
+ ## Build & Test
16
+ ```bash
17
+ pnpm --filter @happyvertical/cache build
18
+ pnpm --filter @happyvertical/cache test
19
+ pnpm --filter @happyvertical/cache clean
20
+ ```
21
+
22
+ ## Agent Correction Loops
23
+ - If module resolution or export errors mention a workspace dependency, build the dependency first (`pnpm --filter @happyvertical/utils build`) and then rerun `pnpm --filter @happyvertical/cache build`.
24
+ - If tests or exports fail after API, type, or bundle changes, run `pnpm --filter @happyvertical/cache clean` followed by `pnpm --filter @happyvertical/cache build` and `pnpm --filter @happyvertical/cache test`.
25
+ - If failures span multiple packages or Turborepo ordering looks wrong, run `pnpm build` and `pnpm typecheck` from the repo root before retrying package-scoped commands.
26
+
27
+ ## Ecosystem Relationships
28
+ - Provides: Standardized caching interface supporting Memory, File, and Redis backends
29
+ - Implements: none
30
+ - Requires: @happyvertical/utils, @aws-sdk/client-s3, @aws-sdk/credential-providers, redis
31
+ - Stability: stable (Primary package surface is described as implemented and production-oriented.)
32
+ <!-- END AGENT:GENERATED -->
33
+
package/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright <2025> <Happy Vertical Corporation>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,161 @@
1
+ # @happyvertical/cache
2
+
3
+ Unified caching interface supporting Memory, File, Redis, and S3 backends with TTL, eviction policies, batch operations, and compression. All providers implement the same `CacheProvider` interface, so you can swap backends without changing application code.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @happyvertical/cache
9
+ # Requires @happyvertical/utils as a peer
10
+ # redis and @aws-sdk/client-s3 are bundled
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```typescript
16
+ import { getCache } from '@happyvertical/cache';
17
+
18
+ const cache = await getCache({
19
+ provider: 'memory',
20
+ maxSize: 100 * 1024 * 1024,
21
+ evictionPolicy: 'lru',
22
+ });
23
+
24
+ await cache.set('user:123', { name: 'Alice' }, 3600); // TTL in seconds
25
+ const user = await cache.get('user:123');
26
+ await cache.delete('user:123');
27
+ await cache.close();
28
+ ```
29
+
30
+ ## Providers
31
+
32
+ ### Memory
33
+
34
+ In-process `Map`-based cache. No external dependencies. Data lost on restart.
35
+
36
+ ```typescript
37
+ const cache = await getCache({
38
+ provider: 'memory',
39
+ namespace: 'app',
40
+ defaultTTL: 3600, // seconds
41
+ maxSize: 100 * 1024 * 1024, // bytes (default 100MB)
42
+ maxEntries: 10000, // default 10k
43
+ evictionPolicy: 'lru', // 'lru' | 'lfu' | 'fifo'
44
+ checkPeriod: 60000, // expiration sweep interval in ms
45
+ });
46
+ ```
47
+
48
+ ### File
49
+
50
+ Stores entries as files on disk with optional gzip compression. Persists across restarts.
51
+
52
+ ```typescript
53
+ const cache = await getCache({
54
+ provider: 'file',
55
+ cacheDir: './cache', // required
56
+ compression: true, // gzip (default false)
57
+ maxSize: 500 * 1024 * 1024, // bytes (default 500MB)
58
+ fileExtension: '.cache',
59
+ checkPeriod: 300000, // cleanup interval in ms
60
+ });
61
+ ```
62
+
63
+ ### Redis
64
+
65
+ Distributed cache using the `redis` npm client. Supports optional gzip compression for large values.
66
+
67
+ ```typescript
68
+ const cache = await getCache({
69
+ provider: 'redis',
70
+ host: 'localhost',
71
+ port: 6379,
72
+ password: 'secret',
73
+ db: 0,
74
+ namespace: 'app',
75
+ enableCompression: true,
76
+ compressionThreshold: 1024, // bytes — only compress values larger than this
77
+ connectTimeout: 5000, // ms
78
+ });
79
+ ```
80
+
81
+ ### S3
82
+
83
+ Stores entries as S3 objects. Useful for CI caches that must persist between runs. Compression enabled by default to reduce egress costs.
84
+
85
+ ```typescript
86
+ const cache = await getCache({
87
+ provider: 's3',
88
+ bucket: 'my-cache-bucket',
89
+ prefix: 'cache/',
90
+ region: 'us-east-1',
91
+ compression: true, // default true
92
+ compressionThreshold: 1024, // bytes
93
+ });
94
+ ```
95
+
96
+ ## API
97
+
98
+ All providers implement `CacheProvider`:
99
+
100
+ ```typescript
101
+ interface CacheProvider {
102
+ get<T>(key: string): Promise<T | undefined>;
103
+ set<T>(key: string, value: T, ttl?: number): Promise<void>;
104
+ has(key: string): Promise<boolean>;
105
+ delete(key: string): Promise<boolean>;
106
+ clear(namespace?: string): Promise<void>;
107
+ keys(pattern?: string): Promise<string[]>;
108
+ getMany<T>(keys: string[]): Promise<Map<string, T>>;
109
+ setMany<T>(entries: Array<{ key: string; value: T; ttl?: number }>): Promise<void>;
110
+ deleteMany(keys: string[]): Promise<number>;
111
+ touch(key: string, ttl: number): Promise<boolean>;
112
+ getStats(): Promise<CacheStats>;
113
+ close(): Promise<void>;
114
+ }
115
+ ```
116
+
117
+ - **TTL** is always in seconds. Omit for no expiration.
118
+ - **`keys()`** accepts glob patterns (`user:*`, `report:2024*`).
119
+ - **`touch()`** updates TTL without modifying the value. Returns `false` if the key doesn't exist.
120
+ - **Batch ops** (`getMany`, `setMany`, `deleteMany`) reduce round-trips, especially for Redis.
121
+
122
+ ## Environment Variables
123
+
124
+ Configuration can be loaded from `HAVE_CACHE_*` environment variables via `@happyvertical/utils/loadEnvConfig`. Programmatic options always take precedence.
125
+
126
+ | Variable | Maps to | Notes |
127
+ |----------|---------|-------|
128
+ | `HAVE_CACHE_PROVIDER` | `provider` | `memory`, `file`, `redis`, `s3` |
129
+ | `HAVE_CACHE_NAMESPACE` | `namespace` | |
130
+ | `HAVE_CACHE_DEFAULT_TTL` | `defaultTTL` | seconds |
131
+ | `HAVE_CACHE_MAX_SIZE` | `maxSize` | bytes (memory/file) |
132
+ | `HAVE_CACHE_MAX_ENTRIES` | `maxEntries` | memory only |
133
+ | `HAVE_CACHE_EVICTION_POLICY` | `evictionPolicy` | memory only |
134
+ | `HAVE_CACHE_CACHE_DIR` | `cacheDir` | file only |
135
+ | `HAVE_CACHE_COMPRESSION` | `compression` | file/s3 |
136
+ | `HAVE_CACHE_HOST` | `host` | redis only |
137
+ | `HAVE_CACHE_PORT` | `port` | redis only |
138
+ | `HAVE_CACHE_PASSWORD` | `password` | redis only |
139
+ | `HAVE_CACHE_BUCKET` | `bucket` | s3 only |
140
+ | `HAVE_CACHE_PREFIX` | `prefix` | s3 only |
141
+ | `HAVE_CACHE_REGION` | `region` | s3 only |
142
+
143
+ ## Error Classes
144
+
145
+ All errors extend `CacheError(message, code, provider)`:
146
+
147
+ - `CacheKeyError` — invalid key (empty or >250 chars)
148
+ - `CacheConnectionError` — backend unreachable (Redis)
149
+ - `CacheSizeError` — entry would exceed `maxSize`
150
+ - `CacheSerializationError` — JSON serialize/deserialize failure
151
+
152
+ ## Exported Utilities
153
+
154
+ Low-level helpers re-exported from `shared/utils`:
155
+
156
+ - `isValidKey(key)` — check key length constraints
157
+ - `calculateSize(value)` — approximate JSON byte size
158
+ - `matchesPattern(pattern, str)` — glob matching
159
+ - `formatKey(namespace, key)` / `extractKey(namespace, fullKey)` — namespace prefixing
160
+ - `isExpired(expiresAt)` / `calculateExpiration(ttl)` — TTL math
161
+ - `serialize(value)` / `deserialize(json)` — JSON wrappers
@@ -0,0 +1,450 @@
1
+ import { readFile, rm, stat, mkdir, readdir, writeFile } from "node:fs/promises";
2
+ import { resolve, join } from "node:path";
3
+ import { promisify } from "node:util";
4
+ import { gunzip, gzip } from "node:zlib";
5
+ import { isValidKey, CacheKeyError, formatKey, deserialize, isExpired, CacheError, calculateExpiration, calculateSize, extractKey, matchesPattern, serialize, CacheSerializationError, CacheSizeError } from "../index.js";
6
+ const gzipAsync = promisify(gzip);
7
+ const gunzipAsync = promisify(gunzip);
8
+ class FileProvider {
9
+ cacheDir;
10
+ namespace;
11
+ defaultTTL;
12
+ maxSize;
13
+ compression;
14
+ fileExtension;
15
+ checkPeriod;
16
+ checkInterval;
17
+ stats;
18
+ constructor(options) {
19
+ this.cacheDir = resolve(options.cacheDir);
20
+ this.namespace = options.namespace;
21
+ this.defaultTTL = options.defaultTTL;
22
+ this.maxSize = options.maxSize || 500 * 1024 * 1024;
23
+ this.compression = options.compression ?? false;
24
+ this.fileExtension = options.fileExtension || ".cache";
25
+ this.checkPeriod = options.checkPeriod || 3e5;
26
+ this.stats = {
27
+ hits: 0,
28
+ misses: 0,
29
+ evictions: 0
30
+ };
31
+ this.ensureCacheDir();
32
+ this.startCleanup();
33
+ }
34
+ async get(key) {
35
+ if (!isValidKey(key)) {
36
+ throw new CacheKeyError(key, "file");
37
+ }
38
+ const fullKey = formatKey(this.namespace, key);
39
+ const filePath = this.getFilePath(fullKey);
40
+ try {
41
+ const fileContent = await readFile(filePath);
42
+ let data;
43
+ if (this.compression) {
44
+ data = await gunzipAsync(fileContent);
45
+ } else {
46
+ data = fileContent;
47
+ }
48
+ const entry = deserialize(data.toString("utf-8"));
49
+ if (isExpired(entry.expiresAt)) {
50
+ await rm(filePath, { force: true });
51
+ this.stats.misses++;
52
+ return void 0;
53
+ }
54
+ entry.hits++;
55
+ this.stats.hits++;
56
+ await this.writeEntry(filePath, entry);
57
+ return entry.value;
58
+ } catch (error) {
59
+ if (error.code === "ENOENT") {
60
+ this.stats.misses++;
61
+ return void 0;
62
+ }
63
+ throw new CacheError(
64
+ `Failed to read cache entry: ${error.message}`,
65
+ "READ_ERROR",
66
+ "file"
67
+ );
68
+ }
69
+ }
70
+ async set(key, value, ttl) {
71
+ if (!isValidKey(key)) {
72
+ throw new CacheKeyError(key, "file");
73
+ }
74
+ const fullKey = formatKey(this.namespace, key);
75
+ const filePath = this.getFilePath(fullKey);
76
+ const expiresAt = calculateExpiration(ttl ?? this.defaultTTL);
77
+ const entry = {
78
+ value,
79
+ createdAt: Date.now(),
80
+ expiresAt,
81
+ size: calculateSize(value),
82
+ hits: 0,
83
+ metadata: {
84
+ compressed: this.compression,
85
+ namespace: this.namespace
86
+ }
87
+ };
88
+ await this.evictIfNeeded(entry.size);
89
+ await this.writeEntry(filePath, entry);
90
+ }
91
+ async has(key) {
92
+ if (!isValidKey(key)) {
93
+ throw new CacheKeyError(key, "file");
94
+ }
95
+ const fullKey = formatKey(this.namespace, key);
96
+ const filePath = this.getFilePath(fullKey);
97
+ try {
98
+ const fileContent = await readFile(filePath);
99
+ let data;
100
+ if (this.compression) {
101
+ data = await gunzipAsync(fileContent);
102
+ } else {
103
+ data = fileContent;
104
+ }
105
+ const entry = deserialize(data.toString("utf-8"));
106
+ if (isExpired(entry.expiresAt)) {
107
+ await rm(filePath, { force: true });
108
+ return false;
109
+ }
110
+ return true;
111
+ } catch (error) {
112
+ if (error.code === "ENOENT") {
113
+ return false;
114
+ }
115
+ throw new CacheError(
116
+ `Failed to check cache entry: ${error.message}`,
117
+ "CHECK_ERROR",
118
+ "file"
119
+ );
120
+ }
121
+ }
122
+ async delete(key) {
123
+ if (!isValidKey(key)) {
124
+ throw new CacheKeyError(key, "file");
125
+ }
126
+ const fullKey = formatKey(this.namespace, key);
127
+ const filePath = this.getFilePath(fullKey);
128
+ try {
129
+ await rm(filePath);
130
+ return true;
131
+ } catch (error) {
132
+ if (error.code === "ENOENT") {
133
+ return false;
134
+ }
135
+ throw new CacheError(
136
+ `Failed to delete cache entry: ${error.message}`,
137
+ "DELETE_ERROR",
138
+ "file"
139
+ );
140
+ }
141
+ }
142
+ async clear(namespace) {
143
+ if (namespace) {
144
+ const prefix = this.sanitizeKey(`${namespace}:`);
145
+ const files = await this.getAllFiles();
146
+ for (const file of files) {
147
+ if (file.startsWith(prefix)) {
148
+ await rm(join(this.cacheDir, file), { force: true });
149
+ }
150
+ }
151
+ } else {
152
+ try {
153
+ await rm(this.cacheDir, { recursive: true, force: true });
154
+ await this.ensureCacheDir();
155
+ this.stats.hits = 0;
156
+ this.stats.misses = 0;
157
+ this.stats.evictions = 0;
158
+ } catch (error) {
159
+ throw new CacheError(
160
+ `Failed to clear cache: ${error.message}`,
161
+ "CLEAR_ERROR",
162
+ "file"
163
+ );
164
+ }
165
+ }
166
+ }
167
+ async keys(pattern) {
168
+ const files = await this.getAllFiles();
169
+ const keys = [];
170
+ for (const file of files) {
171
+ const key = file.replace(this.fileExtension, "");
172
+ const filePath = join(this.cacheDir, file);
173
+ try {
174
+ const fileContent = await readFile(filePath);
175
+ let data;
176
+ if (this.compression) {
177
+ data = await gunzipAsync(fileContent);
178
+ } else {
179
+ data = fileContent;
180
+ }
181
+ const entry = deserialize(data.toString("utf-8"));
182
+ if (!isExpired(entry.expiresAt)) {
183
+ const desanitized = this.desanitizeKey(key);
184
+ keys.push(extractKey(this.namespace, desanitized));
185
+ }
186
+ } catch {
187
+ }
188
+ }
189
+ if (pattern) {
190
+ return keys.filter((key) => matchesPattern(pattern, key));
191
+ }
192
+ return keys;
193
+ }
194
+ async getMany(keys) {
195
+ const result = /* @__PURE__ */ new Map();
196
+ for (const key of keys) {
197
+ const value = await this.get(key);
198
+ if (value !== void 0) {
199
+ result.set(key, value);
200
+ }
201
+ }
202
+ return result;
203
+ }
204
+ async setMany(entries) {
205
+ for (const entry of entries) {
206
+ await this.set(entry.key, entry.value, entry.ttl);
207
+ }
208
+ }
209
+ async deleteMany(keys) {
210
+ let deleted = 0;
211
+ for (const key of keys) {
212
+ const wasDeleted = await this.delete(key);
213
+ if (wasDeleted) {
214
+ deleted++;
215
+ }
216
+ }
217
+ return deleted;
218
+ }
219
+ async getStats() {
220
+ const files = await this.getAllFiles();
221
+ let totalSize = 0;
222
+ let entries = 0;
223
+ for (const file of files) {
224
+ const filePath = join(this.cacheDir, file);
225
+ try {
226
+ const stats = await stat(filePath);
227
+ totalSize += stats.size;
228
+ const fileContent = await readFile(filePath);
229
+ let data;
230
+ if (this.compression) {
231
+ data = await gunzipAsync(fileContent);
232
+ } else {
233
+ data = fileContent;
234
+ }
235
+ const entry = deserialize(data.toString("utf-8"));
236
+ if (!isExpired(entry.expiresAt)) {
237
+ entries++;
238
+ }
239
+ } catch {
240
+ }
241
+ }
242
+ const totalAccesses = this.stats.hits + this.stats.misses;
243
+ const hitRate = totalAccesses > 0 ? this.stats.hits / totalAccesses : 0;
244
+ return {
245
+ entries,
246
+ totalSize,
247
+ hits: this.stats.hits,
248
+ misses: this.stats.misses,
249
+ hitRate,
250
+ evictions: this.stats.evictions,
251
+ backend: {
252
+ type: "file",
253
+ cacheDir: this.cacheDir,
254
+ compression: this.compression,
255
+ maxSize: this.maxSize
256
+ }
257
+ };
258
+ }
259
+ async touch(key, ttl) {
260
+ if (!isValidKey(key)) {
261
+ throw new CacheKeyError(key, "file");
262
+ }
263
+ const fullKey = formatKey(this.namespace, key);
264
+ const filePath = this.getFilePath(fullKey);
265
+ try {
266
+ const fileContent = await readFile(filePath);
267
+ let data;
268
+ if (this.compression) {
269
+ data = await gunzipAsync(fileContent);
270
+ } else {
271
+ data = fileContent;
272
+ }
273
+ const entry = deserialize(data.toString("utf-8"));
274
+ if (isExpired(entry.expiresAt)) {
275
+ return false;
276
+ }
277
+ entry.expiresAt = calculateExpiration(ttl);
278
+ await this.writeEntry(filePath, entry);
279
+ return true;
280
+ } catch (error) {
281
+ if (error.code === "ENOENT") {
282
+ return false;
283
+ }
284
+ throw new CacheError(
285
+ `Failed to touch cache entry: ${error.message}`,
286
+ "TOUCH_ERROR",
287
+ "file"
288
+ );
289
+ }
290
+ }
291
+ async close() {
292
+ if (this.checkInterval) {
293
+ clearInterval(this.checkInterval);
294
+ this.checkInterval = void 0;
295
+ }
296
+ }
297
+ /**
298
+ * Ensures cache directory exists
299
+ */
300
+ async ensureCacheDir() {
301
+ try {
302
+ await mkdir(this.cacheDir, { recursive: true });
303
+ } catch (error) {
304
+ throw new CacheError(
305
+ `Failed to create cache directory: ${error.message}`,
306
+ "INIT_ERROR",
307
+ "file"
308
+ );
309
+ }
310
+ }
311
+ /**
312
+ * Gets the file path for a cache key
313
+ */
314
+ getFilePath(key) {
315
+ const sanitizedKey = this.sanitizeKey(key);
316
+ return join(this.cacheDir, `${sanitizedKey}${this.fileExtension}`);
317
+ }
318
+ /**
319
+ * Sanitizes a key for use as a filename
320
+ */
321
+ sanitizeKey(key) {
322
+ return key.replace(/[^a-zA-Z0-9_:-]/g, "_");
323
+ }
324
+ /**
325
+ * Desanitizes a filename back to the original key
326
+ */
327
+ desanitizeKey(sanitized) {
328
+ return sanitized;
329
+ }
330
+ /**
331
+ * Gets all cache file names
332
+ */
333
+ async getAllFiles() {
334
+ try {
335
+ const files = await readdir(this.cacheDir);
336
+ return files.filter((file) => file.endsWith(this.fileExtension));
337
+ } catch (error) {
338
+ if (error.code === "ENOENT") {
339
+ return [];
340
+ }
341
+ throw new CacheError(
342
+ `Failed to list cache files: ${error.message}`,
343
+ "LIST_ERROR",
344
+ "file"
345
+ );
346
+ }
347
+ }
348
+ /**
349
+ * Writes an entry to a file
350
+ */
351
+ async writeEntry(filePath, entry) {
352
+ try {
353
+ let data;
354
+ const json = serialize(entry);
355
+ data = Buffer.from(json, "utf-8");
356
+ if (this.compression) {
357
+ data = await gzipAsync(data);
358
+ }
359
+ await writeFile(filePath, data);
360
+ } catch (error) {
361
+ throw new CacheSerializationError(
362
+ `Failed to write cache entry: ${error.message}`,
363
+ "file"
364
+ );
365
+ }
366
+ }
367
+ /**
368
+ * Evicts files if size limit is exceeded
369
+ */
370
+ async evictIfNeeded(newEntrySize) {
371
+ const stats = await this.getStats();
372
+ if (stats.totalSize + newEntrySize > this.maxSize) {
373
+ await this.evict();
374
+ const updatedStats = await this.getStats();
375
+ if (updatedStats.totalSize + newEntrySize > this.maxSize) {
376
+ throw new CacheSizeError(
377
+ `Cannot cache entry: would exceed max size of ${this.maxSize} bytes`,
378
+ "file"
379
+ );
380
+ }
381
+ }
382
+ }
383
+ /**
384
+ * Evicts oldest files based on creation time
385
+ */
386
+ async evict() {
387
+ const files = await this.getAllFiles();
388
+ const filesWithStats = [];
389
+ for (const file of files) {
390
+ const filePath = join(this.cacheDir, file);
391
+ try {
392
+ const fileContent = await readFile(filePath);
393
+ let data;
394
+ if (this.compression) {
395
+ data = await gunzipAsync(fileContent);
396
+ } else {
397
+ data = fileContent;
398
+ }
399
+ const entry = deserialize(data.toString("utf-8"));
400
+ filesWithStats.push({ file, createdAt: entry.createdAt });
401
+ } catch {
402
+ }
403
+ }
404
+ filesWithStats.sort((a, b) => a.createdAt - b.createdAt);
405
+ const toRemove = Math.max(1, Math.floor(filesWithStats.length * 0.1));
406
+ for (let i = 0; i < toRemove; i++) {
407
+ const filePath = join(this.cacheDir, filesWithStats[i].file);
408
+ await rm(filePath, { force: true });
409
+ this.stats.evictions++;
410
+ }
411
+ }
412
+ /**
413
+ * Starts background cleanup of expired files
414
+ */
415
+ startCleanup() {
416
+ this.checkInterval = setInterval(() => {
417
+ this.removeExpiredFiles();
418
+ }, this.checkPeriod);
419
+ if (this.checkInterval.unref) {
420
+ this.checkInterval.unref();
421
+ }
422
+ }
423
+ /**
424
+ * Removes expired files
425
+ */
426
+ async removeExpiredFiles() {
427
+ const files = await this.getAllFiles();
428
+ for (const file of files) {
429
+ const filePath = join(this.cacheDir, file);
430
+ try {
431
+ const fileContent = await readFile(filePath);
432
+ let data;
433
+ if (this.compression) {
434
+ data = await gunzipAsync(fileContent);
435
+ } else {
436
+ data = fileContent;
437
+ }
438
+ const entry = deserialize(data.toString("utf-8"));
439
+ if (isExpired(entry.expiresAt)) {
440
+ await rm(filePath, { force: true });
441
+ }
442
+ } catch {
443
+ }
444
+ }
445
+ }
446
+ }
447
+ export {
448
+ FileProvider
449
+ };
450
+ //# sourceMappingURL=file-DyC_7WDS.js.map