@chriscdn/file-cache 0.0.3 → 0.0.5
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/README.md +2 -3
- package/__tests__/file-cache.test.ts +1 -1
- package/lib/index.d.ts +3 -4
- package/lib/index.js +25 -32
- package/lib/index.js.map +1 -7
- package/package.json +5 -6
- package/src/index.ts +33 -38
- package/tsconfig.json +3 -7
- package/esbuild.mjs +0 -19
package/README.md
CHANGED
|
@@ -61,9 +61,8 @@ const options: FileCacheOptions<MyCallbackArgs> = {
|
|
|
61
61
|
await downloadAndConvertToJPG(url, filePath);
|
|
62
62
|
},
|
|
63
63
|
|
|
64
|
-
// Specifies the file extension for
|
|
65
|
-
|
|
66
|
-
ext: ".jpg",
|
|
64
|
+
// Specifies the file extension for the cached file. Can be synchronous or asynchronous.
|
|
65
|
+
ext: (context: MyCallbackArgs) => ".jpg",
|
|
67
66
|
|
|
68
67
|
// Determines the time-to-live (TTL) of a cached file, in milliseconds,
|
|
69
68
|
// based on when it was last accessed.
|
package/lib/index.d.ts
CHANGED
|
@@ -5,9 +5,9 @@ export type FileCacheOptions<T extends Record<string, any>> = {
|
|
|
5
5
|
cachePath: DirectoryPath;
|
|
6
6
|
autoCreateCachePath?: boolean;
|
|
7
7
|
cb: (filePath: FilePath, context: T, cache: FileCache<T>) => Promise<void>;
|
|
8
|
-
ext: string
|
|
9
|
-
cleanupInterval?: Milliseconds;
|
|
8
|
+
ext: (context: T) => string | Promise<string>;
|
|
10
9
|
ttl: Milliseconds;
|
|
10
|
+
cleanupInterval?: Milliseconds;
|
|
11
11
|
};
|
|
12
12
|
declare class FileCache<T extends Record<string, any>> {
|
|
13
13
|
private _cachePath;
|
|
@@ -60,7 +60,6 @@ declare class FileCache<T extends Record<string, any>> {
|
|
|
60
60
|
* @param args The arguments used to resolve the file path
|
|
61
61
|
* @returns The full path to the cached file
|
|
62
62
|
*/
|
|
63
|
-
resolveFilePath(args: T): FilePath
|
|
64
|
-
resolveCreateFilePath(args: T): Promise<FilePath>;
|
|
63
|
+
resolveFilePath(args: T): Promise<FilePath>;
|
|
65
64
|
}
|
|
66
65
|
export { type DirectoryPath, FileCache, type FilePath };
|
package/lib/index.js
CHANGED
|
@@ -7,10 +7,10 @@ import touch from "touch";
|
|
|
7
7
|
import { findNuke } from "@chriscdn/find-nuke";
|
|
8
8
|
import { Duration } from "@chriscdn/duration";
|
|
9
9
|
import { rimraf } from "rimraf";
|
|
10
|
+
import { Memoize } from "@chriscdn/memoize";
|
|
10
11
|
const fsp = fs.promises;
|
|
11
12
|
class FileCache {
|
|
12
13
|
_cachePath;
|
|
13
|
-
// private _resolveFileName: FileCacheOptions<T>["resolveFileName"];
|
|
14
14
|
_cb;
|
|
15
15
|
_ext;
|
|
16
16
|
_ttl;
|
|
@@ -20,30 +20,25 @@ class FileCache {
|
|
|
20
20
|
_cleanupSemaphore = new Semaphore();
|
|
21
21
|
constructor({ cachePath, autoCreateCachePath, cb, ext, ttl, cleanupInterval }) {
|
|
22
22
|
this._cachePath = cachePath;
|
|
23
|
-
// this._resolveFileName = resolveFileName;
|
|
24
23
|
this._cb = cb;
|
|
25
24
|
this._ext = ext;
|
|
26
25
|
this._ttl = ttl;
|
|
27
26
|
this._cleanupInterval = cleanupInterval ??
|
|
28
27
|
Duration.toMilliseconds({ days: 1 });
|
|
29
|
-
(async () => {
|
|
30
|
-
if (!(await pathExists(cachePath))) {
|
|
31
|
-
throw new Error("💥 FileCache error: cachePath does not exist.");
|
|
32
|
-
// process.exit(1); // or throw new Error() if you want to let it bubble
|
|
33
|
-
}
|
|
34
|
-
})();
|
|
35
28
|
if ((pathExistsSync(cachePath))) {
|
|
29
|
+
// we're good
|
|
36
30
|
}
|
|
37
31
|
else if (autoCreateCachePath) {
|
|
38
32
|
fs.mkdirSync(cachePath, { recursive: true });
|
|
39
33
|
}
|
|
40
34
|
else {
|
|
41
35
|
throw new Error("💥 FileCache error: cachePath does not exist.");
|
|
42
|
-
// process.exit(1); // or throw new Error() if you want to let it bubble
|
|
43
36
|
}
|
|
44
37
|
// fire and forget
|
|
45
38
|
this.cleanup();
|
|
46
39
|
this._intervalId = setInterval(this.cleanup.bind(this), this._cleanupInterval);
|
|
40
|
+
this.resolveFileName = Memoize(this.resolveFileName.bind(this));
|
|
41
|
+
this.resolveFilePath = Memoize(this.resolveFilePath.bind(this));
|
|
47
42
|
}
|
|
48
43
|
/**
|
|
49
44
|
* Runs a cleanup pass to remove files older than 1 day from the cache.
|
|
@@ -73,18 +68,18 @@ class FileCache {
|
|
|
73
68
|
* @returns The path to the cached or newly created file
|
|
74
69
|
*/
|
|
75
70
|
async getFile(args) {
|
|
76
|
-
const filePath = this.resolveFilePath(args);
|
|
71
|
+
const filePath = await this.resolveFilePath(args);
|
|
77
72
|
try {
|
|
78
73
|
await Promise.all([
|
|
79
74
|
this._semaphore.acquire(filePath),
|
|
80
|
-
this._cleanupSemaphore.wait(),
|
|
75
|
+
this._cleanupSemaphore.wait(), // wait for any cleanup task to end
|
|
81
76
|
]);
|
|
82
77
|
if (await pathExists(filePath)) {
|
|
83
78
|
// fire and forget
|
|
84
79
|
touch(filePath);
|
|
85
80
|
}
|
|
86
81
|
else {
|
|
87
|
-
await
|
|
82
|
+
await fsp.mkdir(path.dirname(filePath), { recursive: true });
|
|
88
83
|
await this._cb(filePath, args, this);
|
|
89
84
|
}
|
|
90
85
|
}
|
|
@@ -94,7 +89,7 @@ class FileCache {
|
|
|
94
89
|
return filePath;
|
|
95
90
|
}
|
|
96
91
|
async has(args) {
|
|
97
|
-
const filePath = this.resolveFilePath(args);
|
|
92
|
+
const filePath = await this.resolveFilePath(args);
|
|
98
93
|
return await pathExists(filePath);
|
|
99
94
|
}
|
|
100
95
|
/**
|
|
@@ -104,13 +99,19 @@ class FileCache {
|
|
|
104
99
|
* @returns True if the file was deleted, false if it didn't exist
|
|
105
100
|
*/
|
|
106
101
|
async expire(args) {
|
|
107
|
-
const filePath = this.resolveFilePath(args);
|
|
108
|
-
|
|
109
|
-
await
|
|
110
|
-
|
|
102
|
+
const filePath = await this.resolveFilePath(args);
|
|
103
|
+
try {
|
|
104
|
+
await this._semaphore.acquire(filePath);
|
|
105
|
+
if (await pathExists(filePath)) {
|
|
106
|
+
await rimraf(filePath);
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
111
112
|
}
|
|
112
|
-
|
|
113
|
-
|
|
113
|
+
finally {
|
|
114
|
+
this._semaphore.release(filePath);
|
|
114
115
|
}
|
|
115
116
|
}
|
|
116
117
|
/**
|
|
@@ -128,16 +129,12 @@ class FileCache {
|
|
|
128
129
|
* Uses the provided `resolveFileName` function if available, or falls back to
|
|
129
130
|
* generating a hash from the arguments to create a unique file name.
|
|
130
131
|
*/
|
|
131
|
-
resolveFileName(args) {
|
|
132
|
+
async resolveFileName(args) {
|
|
132
133
|
const baseName = sha1(JSON.stringify(args));
|
|
133
134
|
return path.format({
|
|
134
|
-
// dir: path.dirname(filePath),
|
|
135
135
|
name: baseName,
|
|
136
|
-
ext: this._ext,
|
|
136
|
+
ext: await this._ext(args),
|
|
137
137
|
});
|
|
138
|
-
// return this._resolveFileName
|
|
139
|
-
// ? this._resolveFileName(args)
|
|
140
|
-
// : `${sha1(JSON.stringify(args, Object.keys(args).sort()))}${this._ext}`;
|
|
141
138
|
}
|
|
142
139
|
/**
|
|
143
140
|
* Resolves the full file path for the cached file based on the provided arguments.
|
|
@@ -145,17 +142,13 @@ class FileCache {
|
|
|
145
142
|
* @param args The arguments used to resolve the file path
|
|
146
143
|
* @returns The full path to the cached file
|
|
147
144
|
*/
|
|
148
|
-
resolveFilePath(args) {
|
|
149
|
-
const fileName = this.resolveFileName(args);
|
|
145
|
+
async resolveFilePath(args) {
|
|
146
|
+
const fileName = await this.resolveFileName(args);
|
|
150
147
|
const filePath = path.resolve(this._cachePath, fileName[0], fileName[1], fileName[2],
|
|
151
148
|
// fileName[3],
|
|
152
149
|
fileName);
|
|
153
150
|
return filePath;
|
|
154
151
|
}
|
|
155
|
-
async resolveCreateFilePath(args) {
|
|
156
|
-
const filePath = this.resolveFilePath(args);
|
|
157
|
-
await fsp.mkdir(path.dirname(filePath), { recursive: true });
|
|
158
|
-
return filePath;
|
|
159
|
-
}
|
|
160
152
|
}
|
|
161
153
|
export { FileCache };
|
|
154
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
CHANGED
|
@@ -1,7 +1 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../src/index.ts"],
|
|
4
|
-
"sourcesContent": ["import path from \"path\";\nimport fs from \"fs\";\nimport { pathExists, pathExistsSync } from \"path-exists\";\nimport sha1 from \"sha1\";\nimport Semaphore from \"@chriscdn/promise-semaphore\";\nimport touch from \"touch\";\nimport { findNuke } from \"@chriscdn/find-nuke\";\nimport { Duration } from \"@chriscdn/duration\";\nimport { rimraf } from \"rimraf\";\n\nconst fsp = fs.promises;\n\ntype DirectoryPath = string;\ntype FilePath = string;\ntype Milliseconds = number;\n\nexport type FileCacheOptions<T extends Record<string, any>> = {\n cachePath: DirectoryPath;\n autoCreateCachePath?: boolean;\n cb: (filePath: FilePath, context: T, cache: FileCache<T>) => Promise<void>;\n ext: string;\n cleanupInterval?: Milliseconds;\n ttl: Milliseconds;\n // resolveFileName?: (args: T) => FilePath;\n};\n\nclass FileCache<T extends Record<string, any>> {\n private _cachePath: FileCacheOptions<T>[\"cachePath\"];\n // private _resolveFileName: FileCacheOptions<T>[\"resolveFileName\"];\n private _cb: FileCacheOptions<T>[\"cb\"];\n private _ext: FileCacheOptions<T>[\"ext\"];\n private _ttl: FileCacheOptions<T>[\"ttl\"];\n private _cleanupInterval: FileCacheOptions<T>[\"cleanupInterval\"];\n\n private _intervalId: ReturnType<typeof setInterval> | null = null;\n\n private _semaphore: Semaphore = new Semaphore();\n private _cleanupSemaphore: Semaphore = new Semaphore();\n\n constructor(\n { cachePath, autoCreateCachePath, cb, ext, ttl, cleanupInterval }:\n FileCacheOptions<T>,\n ) {\n this._cachePath = cachePath;\n // this._resolveFileName = resolveFileName;\n this._cb = cb;\n this._ext = ext;\n this._ttl = ttl;\n this._cleanupInterval = cleanupInterval ??\n Duration.toMilliseconds({ days: 1 });\n\n (async () => {\n if (!(await pathExists(cachePath))) {\n throw new Error(\"\uD83D\uDCA5 FileCache error: cachePath does not exist.\");\n // process.exit(1); // or throw new Error() if you want to let it bubble\n }\n })();\n\n if ((pathExistsSync(cachePath))) {\n } else if (autoCreateCachePath) {\n fs.mkdirSync(cachePath, { recursive: true });\n } else {\n throw new Error(\"\uD83D\uDCA5 FileCache error: cachePath does not exist.\");\n // process.exit(1); // or throw new Error() if you want to let it bubble\n }\n\n // fire and forget\n this.cleanup();\n\n this._intervalId = setInterval(\n this.cleanup.bind(this),\n this._cleanupInterval,\n );\n }\n\n /**\n * Runs a cleanup pass to remove files older than 1 day from the cache.\n * This is called periodically, but can be triggered manually.\n */\n async cleanup() {\n try {\n await this._cleanupSemaphore.acquire();\n await findNuke(this._cachePath, {\n olderThan: this._ttl,\n verbose: true,\n deleteEmptyDirectories: true,\n });\n } finally {\n this._cleanupSemaphore.release();\n }\n }\n\n /**\n * Retrieves a cached file for the given arguments.\n * If the file doesn't exist, it is generated by the callback and stored.\n *\n * Concurrency-safe, ensuring no conflicts when multiple requests are made\n * for the same file.\n *\n * @param args The arguments used to determine the file's identity and generation\n * @returns The path to the cached or newly created file\n */\n async getFile(args: T): Promise<FilePath> {\n const filePath = this.resolveFilePath(args);\n\n try {\n await Promise.all([\n this._semaphore.acquire(filePath),\n this._cleanupSemaphore.wait(),\n ]);\n\n if (await pathExists(filePath)) {\n // fire and forget\n touch(filePath);\n } else {\n await fs.mkdirSync(path.dirname(filePath), { recursive: true });\n await this._cb(filePath, args, this);\n }\n } finally {\n this._semaphore.release(filePath);\n }\n\n return filePath;\n }\n\n async has(args: T): Promise<boolean> {\n const filePath = this.resolveFilePath(args);\n return await pathExists(filePath);\n }\n\n /**\n * Deletes a file from the cache if it exists.\n *\n * @param args The arguments used to determine the file's identity\n * @returns True if the file was deleted, false if it didn't exist\n */\n async expire(args: T): Promise<boolean> {\n const filePath = this.resolveFilePath(args);\n\n if (await pathExists(filePath)) {\n await rimraf(filePath);\n return true;\n } else {\n return false;\n }\n }\n\n /**\n * Stops the background cleanup process, but does not delete any cached files.\n */\n destroy() {\n if (this._intervalId) {\n clearInterval(this._intervalId);\n this._intervalId = null;\n }\n }\n\n /**\n * Resolves the filename for the cache based on the provided arguments.\n *\n * Uses the provided `resolveFileName` function if available, or falls back to\n * generating a hash from the arguments to create a unique file name.\n */\n private resolveFileName(args: T): string {\n const baseName = sha1(JSON.stringify(args));\n\n return path.format({\n // dir: path.dirname(filePath),\n name: baseName,\n ext: this._ext,\n });\n\n // return this._resolveFileName\n // ? this._resolveFileName(args)\n // : `${sha1(JSON.stringify(args, Object.keys(args).sort()))}${this._ext}`;\n }\n\n /**\n * Resolves the full file path for the cached file based on the provided arguments.\n *\n * @param args The arguments used to resolve the file path\n * @returns The full path to the cached file\n */\n resolveFilePath(args: T): FilePath {\n const fileName = this.resolveFileName(args);\n\n const filePath = path.resolve(\n this._cachePath,\n fileName[0],\n fileName[1],\n fileName[2],\n // fileName[3],\n fileName,\n );\n\n return filePath;\n }\n\n async resolveCreateFilePath(args: T): Promise<FilePath> {\n const filePath = this.resolveFilePath(args);\n\n await fsp.mkdir(path.dirname(filePath), { recursive: true });\n\n return filePath;\n }\n}\n\nexport { type DirectoryPath, FileCache, type FilePath };\n"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": []
|
|
7
|
-
}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chriscdn/file-cache",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "File cache ",
|
|
5
5
|
"repository": "https://github.com/chriscdn/file-cache",
|
|
6
6
|
"author": "Christopher Meyer <chris@schwiiz.org>",
|
|
@@ -10,18 +10,17 @@
|
|
|
10
10
|
"module": "./lib/index.js",
|
|
11
11
|
"types": "./lib/index.d.ts",
|
|
12
12
|
"exports": {
|
|
13
|
-
"types": "./lib/index.d.ts",
|
|
14
13
|
"default": "./lib/index.js"
|
|
15
14
|
},
|
|
16
15
|
"scripts": {
|
|
17
|
-
"build": "rimraf ./lib/ &&
|
|
18
|
-
"dev": "microbundle watch",
|
|
16
|
+
"build": "rimraf ./lib/ && tsc",
|
|
19
17
|
"test": "vitest"
|
|
20
18
|
},
|
|
21
19
|
"dependencies": {
|
|
22
20
|
"@chriscdn/duration": "^1.0.3",
|
|
23
21
|
"@chriscdn/find-nuke": "^0.0.4",
|
|
24
|
-
"@chriscdn/
|
|
22
|
+
"@chriscdn/memoize": "^1.0.9",
|
|
23
|
+
"@chriscdn/promise-semaphore": "^2.0.12",
|
|
25
24
|
"path-exists": "^5.0.0",
|
|
26
25
|
"rimraf": "^6.0.1",
|
|
27
26
|
"sha1": "^1.1.1",
|
|
@@ -29,9 +28,9 @@
|
|
|
29
28
|
"touch": "^3.1.1"
|
|
30
29
|
},
|
|
31
30
|
"devDependencies": {
|
|
31
|
+
"@tsconfig/node22": "^22.0.1",
|
|
32
32
|
"@types/sha1": "^1.1.5",
|
|
33
33
|
"@types/touch": "^3.1.5",
|
|
34
|
-
"esbuild": "^0.25.3",
|
|
35
34
|
"typescript": "^5.8.3",
|
|
36
35
|
"vitest": "^3.1.2"
|
|
37
36
|
}
|
package/src/index.ts
CHANGED
|
@@ -7,7 +7,7 @@ import touch from "touch";
|
|
|
7
7
|
import { findNuke } from "@chriscdn/find-nuke";
|
|
8
8
|
import { Duration } from "@chriscdn/duration";
|
|
9
9
|
import { rimraf } from "rimraf";
|
|
10
|
-
|
|
10
|
+
import { Memoize } from "@chriscdn/memoize";
|
|
11
11
|
const fsp = fs.promises;
|
|
12
12
|
|
|
13
13
|
type DirectoryPath = string;
|
|
@@ -18,15 +18,14 @@ export type FileCacheOptions<T extends Record<string, any>> = {
|
|
|
18
18
|
cachePath: DirectoryPath;
|
|
19
19
|
autoCreateCachePath?: boolean;
|
|
20
20
|
cb: (filePath: FilePath, context: T, cache: FileCache<T>) => Promise<void>;
|
|
21
|
-
ext: string
|
|
22
|
-
cleanupInterval?: Milliseconds;
|
|
21
|
+
ext: (context: T) => string | Promise<string>;
|
|
23
22
|
ttl: Milliseconds;
|
|
24
|
-
|
|
23
|
+
cleanupInterval?: Milliseconds;
|
|
25
24
|
};
|
|
26
25
|
|
|
27
26
|
class FileCache<T extends Record<string, any>> {
|
|
28
27
|
private _cachePath: FileCacheOptions<T>["cachePath"];
|
|
29
|
-
|
|
28
|
+
|
|
30
29
|
private _cb: FileCacheOptions<T>["cb"];
|
|
31
30
|
private _ext: FileCacheOptions<T>["ext"];
|
|
32
31
|
private _ttl: FileCacheOptions<T>["ttl"];
|
|
@@ -42,26 +41,18 @@ class FileCache<T extends Record<string, any>> {
|
|
|
42
41
|
FileCacheOptions<T>,
|
|
43
42
|
) {
|
|
44
43
|
this._cachePath = cachePath;
|
|
45
|
-
// this._resolveFileName = resolveFileName;
|
|
46
44
|
this._cb = cb;
|
|
47
45
|
this._ext = ext;
|
|
48
46
|
this._ttl = ttl;
|
|
49
47
|
this._cleanupInterval = cleanupInterval ??
|
|
50
48
|
Duration.toMilliseconds({ days: 1 });
|
|
51
49
|
|
|
52
|
-
(async () => {
|
|
53
|
-
if (!(await pathExists(cachePath))) {
|
|
54
|
-
throw new Error("💥 FileCache error: cachePath does not exist.");
|
|
55
|
-
// process.exit(1); // or throw new Error() if you want to let it bubble
|
|
56
|
-
}
|
|
57
|
-
})();
|
|
58
|
-
|
|
59
50
|
if ((pathExistsSync(cachePath))) {
|
|
51
|
+
// we're good
|
|
60
52
|
} else if (autoCreateCachePath) {
|
|
61
53
|
fs.mkdirSync(cachePath, { recursive: true });
|
|
62
54
|
} else {
|
|
63
55
|
throw new Error("💥 FileCache error: cachePath does not exist.");
|
|
64
|
-
// process.exit(1); // or throw new Error() if you want to let it bubble
|
|
65
56
|
}
|
|
66
57
|
|
|
67
58
|
// fire and forget
|
|
@@ -71,6 +62,9 @@ class FileCache<T extends Record<string, any>> {
|
|
|
71
62
|
this.cleanup.bind(this),
|
|
72
63
|
this._cleanupInterval,
|
|
73
64
|
);
|
|
65
|
+
|
|
66
|
+
this.resolveFileName = Memoize(this.resolveFileName.bind(this));
|
|
67
|
+
this.resolveFilePath = Memoize(this.resolveFilePath.bind(this));
|
|
74
68
|
}
|
|
75
69
|
|
|
76
70
|
/**
|
|
@@ -101,19 +95,19 @@ class FileCache<T extends Record<string, any>> {
|
|
|
101
95
|
* @returns The path to the cached or newly created file
|
|
102
96
|
*/
|
|
103
97
|
async getFile(args: T): Promise<FilePath> {
|
|
104
|
-
const filePath = this.resolveFilePath(args);
|
|
98
|
+
const filePath = await this.resolveFilePath(args);
|
|
105
99
|
|
|
106
100
|
try {
|
|
107
101
|
await Promise.all([
|
|
108
102
|
this._semaphore.acquire(filePath),
|
|
109
|
-
this._cleanupSemaphore.wait(),
|
|
103
|
+
this._cleanupSemaphore.wait(), // wait for any cleanup task to end
|
|
110
104
|
]);
|
|
111
105
|
|
|
112
106
|
if (await pathExists(filePath)) {
|
|
113
107
|
// fire and forget
|
|
114
108
|
touch(filePath);
|
|
115
109
|
} else {
|
|
116
|
-
await
|
|
110
|
+
await fsp.mkdir(path.dirname(filePath), { recursive: true });
|
|
117
111
|
await this._cb(filePath, args, this);
|
|
118
112
|
}
|
|
119
113
|
} finally {
|
|
@@ -124,7 +118,7 @@ class FileCache<T extends Record<string, any>> {
|
|
|
124
118
|
}
|
|
125
119
|
|
|
126
120
|
async has(args: T): Promise<boolean> {
|
|
127
|
-
const filePath = this.resolveFilePath(args);
|
|
121
|
+
const filePath = await this.resolveFilePath(args);
|
|
128
122
|
return await pathExists(filePath);
|
|
129
123
|
}
|
|
130
124
|
|
|
@@ -135,13 +129,19 @@ class FileCache<T extends Record<string, any>> {
|
|
|
135
129
|
* @returns True if the file was deleted, false if it didn't exist
|
|
136
130
|
*/
|
|
137
131
|
async expire(args: T): Promise<boolean> {
|
|
138
|
-
const filePath = this.resolveFilePath(args);
|
|
132
|
+
const filePath = await this.resolveFilePath(args);
|
|
139
133
|
|
|
140
|
-
|
|
141
|
-
await
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
134
|
+
try {
|
|
135
|
+
await this._semaphore.acquire(filePath);
|
|
136
|
+
|
|
137
|
+
if (await pathExists(filePath)) {
|
|
138
|
+
await rimraf(filePath);
|
|
139
|
+
return true;
|
|
140
|
+
} else {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
} finally {
|
|
144
|
+
this._semaphore.release(filePath);
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
147
|
|
|
@@ -161,18 +161,13 @@ class FileCache<T extends Record<string, any>> {
|
|
|
161
161
|
* Uses the provided `resolveFileName` function if available, or falls back to
|
|
162
162
|
* generating a hash from the arguments to create a unique file name.
|
|
163
163
|
*/
|
|
164
|
-
private resolveFileName(args: T): string {
|
|
164
|
+
private async resolveFileName(args: T): Promise<string> {
|
|
165
165
|
const baseName = sha1(JSON.stringify(args));
|
|
166
166
|
|
|
167
167
|
return path.format({
|
|
168
|
-
// dir: path.dirname(filePath),
|
|
169
168
|
name: baseName,
|
|
170
|
-
ext: this._ext,
|
|
169
|
+
ext: await this._ext(args),
|
|
171
170
|
});
|
|
172
|
-
|
|
173
|
-
// return this._resolveFileName
|
|
174
|
-
// ? this._resolveFileName(args)
|
|
175
|
-
// : `${sha1(JSON.stringify(args, Object.keys(args).sort()))}${this._ext}`;
|
|
176
171
|
}
|
|
177
172
|
|
|
178
173
|
/**
|
|
@@ -181,8 +176,8 @@ class FileCache<T extends Record<string, any>> {
|
|
|
181
176
|
* @param args The arguments used to resolve the file path
|
|
182
177
|
* @returns The full path to the cached file
|
|
183
178
|
*/
|
|
184
|
-
resolveFilePath(args: T): FilePath {
|
|
185
|
-
const fileName = this.resolveFileName(args);
|
|
179
|
+
async resolveFilePath(args: T): Promise<FilePath> {
|
|
180
|
+
const fileName = await this.resolveFileName(args);
|
|
186
181
|
|
|
187
182
|
const filePath = path.resolve(
|
|
188
183
|
this._cachePath,
|
|
@@ -196,13 +191,13 @@ class FileCache<T extends Record<string, any>> {
|
|
|
196
191
|
return filePath;
|
|
197
192
|
}
|
|
198
193
|
|
|
199
|
-
async resolveCreateFilePath(args: T): Promise<FilePath> {
|
|
200
|
-
|
|
194
|
+
// async resolveCreateFilePath(args: T): Promise<FilePath> {
|
|
195
|
+
// const filePath = this.resolveFilePath(args);
|
|
201
196
|
|
|
202
|
-
|
|
197
|
+
// await fsp.mkdir(path.dirname(filePath), { recursive: true });
|
|
203
198
|
|
|
204
|
-
|
|
205
|
-
}
|
|
199
|
+
// return filePath;
|
|
200
|
+
// }
|
|
206
201
|
}
|
|
207
202
|
|
|
208
203
|
export { type DirectoryPath, FileCache, type FilePath };
|
package/tsconfig.json
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
+
"extends": "@tsconfig/node22/tsconfig.json",
|
|
2
3
|
"compilerOptions": {
|
|
3
|
-
"target": "ESNext",
|
|
4
|
-
"lib": ["ESNext"],
|
|
5
|
-
"module": "ESNext",
|
|
6
|
-
"moduleResolution": "node",
|
|
7
|
-
"strict": true,
|
|
8
4
|
"allowSyntheticDefaultImports": true,
|
|
9
5
|
"outDir": "./lib",
|
|
10
|
-
"declaration": true,
|
|
6
|
+
"declaration": true,
|
|
11
7
|
"declarationDir": "./lib",
|
|
12
|
-
"
|
|
8
|
+
"sourceMap": true
|
|
13
9
|
},
|
|
14
10
|
"include": ["./src/**/*"]
|
|
15
11
|
}
|
package/esbuild.mjs
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { build } from "esbuild";
|
|
2
|
-
|
|
3
|
-
import pkg from "./package.json" with { type: "json" };
|
|
4
|
-
|
|
5
|
-
const external = [
|
|
6
|
-
...Object.keys(pkg.dependencies || {}),
|
|
7
|
-
...Object.keys(pkg.devDependencies || {}),
|
|
8
|
-
];
|
|
9
|
-
|
|
10
|
-
build({
|
|
11
|
-
entryPoints: ["./src/index.ts"],
|
|
12
|
-
bundle: true,
|
|
13
|
-
platform: "node",
|
|
14
|
-
format: "esm", // or 'esm' if you prefer
|
|
15
|
-
outfile: "./lib/index.js",
|
|
16
|
-
external,
|
|
17
|
-
minify: false,
|
|
18
|
-
sourcemap: true,
|
|
19
|
-
}).catch(() => process.exit(1));
|