Skip to content

Commit eb29853

Browse files
authored
feat: add prune() method (#85)
1 parent 01b8f5b commit eb29853

13 files changed

Lines changed: 182 additions & 29 deletions

File tree

.changeset/weak-parts-pull.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'bentocache': minor
3+
---
4+
5+
Add `prune()` method for cache drivers without native TTL support as an alternative to `pruneInterval` strategy

docs/content/docs/methods.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,14 @@ Clear the cache. This will delete all the keys in the cache if called from the "
250250
await bento.clear();
251251
```
252252

253+
## prune
254+
255+
Prunes the cache by removing expired entries. This is useful for drivers that do not have native TTL support, such as File and Database drivers. On drivers with native TTL support, this is typically a noop.
256+
257+
```ts
258+
await bento.prune();
259+
```
260+
253261
## disconnect
254262

255263
Disconnect from the cache. This will close the connection to the cache server, if applicable.

packages/bentocache/src/bento_cache.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,16 @@ export class BentoCache<KnownCaches extends Record<string, BentoStore>> implemen
241241
return this.use().clear(options)
242242
}
243243

244+
/**
245+
* Manually prune expired cache entries
246+
*
247+
* For drivers with native TTL support, this is typically a noop
248+
* For drivers without native TTL (PostgreSQL, File), this will remove expired entries
249+
*/
250+
prune(): Promise<void> {
251+
return this.use().prune?.() ?? Promise.resolve()
252+
}
253+
244254
/**
245255
* Remove all items from all caches
246256
*/

packages/bentocache/src/cache/cache.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,16 @@ export class Cache implements CacheProvider {
267267
this.#stack.emit(cacheEvents.cleared(this.name))
268268
}
269269

270+
/**
271+
* Manually prune expired cache entries
272+
*
273+
* For drivers with native TTL support, this is typically a noop
274+
* For drivers without native TTL (PostgreSQL, File), this will remove expired entries
275+
*/
276+
prune(): Promise<void> {
277+
return this.#stack.l2?.prune() ?? Promise.resolve()
278+
}
279+
270280
/**
271281
* Closes the connection to the cache
272282
*/

packages/bentocache/src/cache/facades/remote_cache.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,13 @@ export class RemoteCache {
154154
})
155155
}
156156

157+
/**
158+
* Manually prune expired cache entries
159+
*/
160+
prune(): Promise<void> {
161+
return this.#driver.prune?.() ?? Promise.resolve()
162+
}
163+
157164
/**
158165
* Disconnect from the remote cache
159166
*/

packages/bentocache/src/drivers/database/database.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,4 +166,12 @@ export class DatabaseDriver extends BaseDriver implements CacheDriver<true> {
166166

167167
await this.#adapter.disconnect()
168168
}
169+
170+
/**
171+
* Manually prune expired cache entries.
172+
*/
173+
async prune() {
174+
await this.#initializer()
175+
await this.#adapter.pruneExpiredEntries()
176+
}
169177
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// @ts-check
2+
3+
import { join } from 'node:path'
4+
import { readdir, unlink, readFile } from 'node:fs/promises'
5+
6+
/**
7+
* Read the file content and delete it if it's expired
8+
* @param {object} options
9+
* @param {string} options.filePath
10+
* @param {(err: { filePath: string, error: any }) => void} [options.onError]
11+
*/
12+
async function deleteFileIfExpired({ filePath, onError }) {
13+
try {
14+
const content = await readFile(filePath, 'utf-8')
15+
const [, expiresAt] = JSON.parse(content)
16+
17+
const expiry = new Date(expiresAt).getTime()
18+
if (+expiry === -1) return
19+
20+
if (expiry < Date.now()) await unlink(filePath)
21+
} catch (error) {
22+
if (onError) onError({ filePath, error })
23+
}
24+
}
25+
26+
/**
27+
* Get recursive list of files in the cache directory and delete expired files
28+
* @param {object} options
29+
* @param {string} options.directory
30+
* @param {(err: { filePath: string, error: any }) => void} [options.onError]
31+
*/
32+
export async function pruneExpiredFiles({ directory, onError }) {
33+
const dirEntries = await readdir(directory, { recursive: true, withFileTypes: true })
34+
35+
for (const dirEntry of dirEntries) {
36+
if (dirEntry.isDirectory()) continue
37+
38+
const filePath = join(dirEntry.parentPath, dirEntry.name)
39+
await deleteFileIfExpired({ filePath, onError })
40+
}
41+
}

packages/bentocache/src/drivers/file/cleaner_worker.js

Lines changed: 6 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,20 @@
11
// @ts-check
22

3-
import { join } from 'node:path'
4-
import { readdir, unlink, readFile } from 'node:fs/promises'
53
import { parentPort, workerData } from 'node:worker_threads'
64

5+
import { pruneExpiredFiles } from './cleaner.js'
6+
77
const directory = workerData.directory
88
const pruneIntervalInMs = workerData.pruneInterval
99

10-
/**
11-
* Read the file content and delete it if it's expired
12-
*
13-
* @param {string} filePath
14-
*/
15-
async function deleteFileIfExpired(filePath) {
16-
const content = await readFile(filePath, 'utf-8')
17-
const [, expiresAt] = JSON.parse(content)
18-
19-
const expiry = new Date(expiresAt).getTime()
20-
if (+expiry === -1) return
21-
22-
if (expiry < Date.now()) {
23-
await unlink(filePath)
24-
}
25-
}
26-
2710
/**
2811
* Get recursive list of files in the cache directory and delete expired files
2912
*/
3013
async function prune() {
31-
const dirEntries = await readdir(directory, { recursive: true, withFileTypes: true })
32-
33-
for (const dirEntry of dirEntries) {
34-
if (dirEntry.isDirectory()) continue
35-
36-
const filePath = join(dirEntry.path, dirEntry.name)
37-
await deleteFileIfExpired(filePath).catch((error) => {
38-
parentPort?.postMessage({ type: 'error', error })
39-
})
40-
}
14+
await pruneExpiredFiles({
15+
directory,
16+
onError: (err) => parentPort?.postMessage({ type: 'error', error: err }),
17+
})
4118
}
4219

4320
setInterval(async () => {

packages/bentocache/src/drivers/file/file.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,4 +234,19 @@ export class FileDriver extends BaseDriver implements CacheDriver {
234234
async disconnect() {
235235
await this.#cleanerWorker?.terminate()
236236
}
237+
238+
/**
239+
* Manually prune expired cache entries by scanning the cache directory
240+
* and removing files that have expired.
241+
*/
242+
async prune() {
243+
const cacheExists = await this.#pathExists(this.#directory)
244+
if (!cacheExists) return
245+
246+
const { pruneExpiredFiles } = await import('./cleaner.js')
247+
await pruneExpiredFiles({
248+
directory: this.#directory,
249+
onError: (err) => this.config.logger?.error(err),
250+
})
251+
}
237252
}

packages/bentocache/src/types/driver.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ export interface CacheDriver<Async extends boolean = true> {
4646
* Some drivers may not need this
4747
*/
4848
disconnect(): PromiseOr<void, Async>
49+
50+
/**
51+
* Manually prune expired cache entries
52+
*
53+
* For drivers with native TTL support, this is typically a noop
54+
* For drivers without native TTL (PostgreSQL, File), this will remove expired entries
55+
*/
56+
prune?(): PromiseOr<void, Async>
4957
}
5058

5159
/**

0 commit comments

Comments
 (0)