@chriscdn/file-cache 0.0.12 → 0.0.14

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.
@@ -0,0 +1,142 @@
1
+ var path = require('path');
2
+ var fs = require('fs');
3
+ var pathExists = require('path-exists');
4
+ var sha1 = require('sha1');
5
+ var promiseSemaphore = require('@chriscdn/promise-semaphore');
6
+ var touch = require('touch');
7
+ var findNuke = require('@chriscdn/find-nuke');
8
+ var duration = require('@chriscdn/duration');
9
+
10
+ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
11
+
12
+ var path__default = /*#__PURE__*/_interopDefaultLegacy(path);
13
+ var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs);
14
+ var sha1__default = /*#__PURE__*/_interopDefaultLegacy(sha1);
15
+ var touch__default = /*#__PURE__*/_interopDefaultLegacy(touch);
16
+
17
+ class FileCache {
18
+ constructor({
19
+ cachePath,
20
+ autoCreateCachePath,
21
+ cb,
22
+ ext,
23
+ ttl,
24
+ cleanupInterval,
25
+ resolveCacheKey
26
+ }) {
27
+ this._cachePath = void 0;
28
+ this._cb = void 0;
29
+ this._ttl = void 0;
30
+ this._cleanupInterval = void 0;
31
+ this._resolveCacheKey = void 0;
32
+ this._ext = void 0;
33
+ this._intervalId = null;
34
+ this._semaphore = new promiseSemaphore.Semaphore();
35
+ this._cleanupGroupSemaphore = new promiseSemaphore.GroupSemaphore();
36
+ this._cachePath = cachePath;
37
+ this._cb = cb;
38
+ this._ext = ext;
39
+ this._ttl = ttl;
40
+ this._cleanupInterval = cleanupInterval != null ? cleanupInterval : duration.Duration.toMilliseconds({
41
+ days: 1
42
+ });
43
+ this._resolveCacheKey = resolveCacheKey != null ? resolveCacheKey : args => JSON.stringify(args);
44
+ // Startup code
45
+ if (pathExists.pathExistsSync(cachePath)) ; else if (autoCreateCachePath) {
46
+ fs__default["default"].mkdirSync(cachePath, {
47
+ recursive: true
48
+ });
49
+ } else {
50
+ throw new Error("💥 FileCache error: cachePath does not exist.");
51
+ }
52
+ // fire and forget
53
+ this.cleanup();
54
+ this._intervalId = setInterval(this.cleanup.bind(this), this._cleanupInterval);
55
+ }
56
+ /**
57
+ * Resolves the filename for the cache based on the provided arguments.
58
+ *
59
+ * Uses the provided `resolveFileName` function if available, or falls back to
60
+ * generating a hash from the arguments to create a unique file name.
61
+ */
62
+ async resolveFileName(args) {
63
+ const [name, ext] = await Promise.all([this._resolveCacheKey(args), this._ext(args)]);
64
+ const resolvedFileName = sha1__default["default"](JSON.stringify([name, ext]));
65
+ return path__default["default"].format({
66
+ name: resolvedFileName,
67
+ ext
68
+ });
69
+ }
70
+ /**
71
+ * Retrieves a cached file for the given arguments.
72
+ * If the file doesn't exist, it is generated by the callback and stored.
73
+ *
74
+ * Concurrency-safe, ensuring no conflicts when multiple requests are made
75
+ * for the same file.
76
+ *
77
+ * @param args The arguments used to determine the file's identity and generation
78
+ * @returns The path to the cached or newly created file
79
+ */
80
+ async getFile(args) {
81
+ const filePath = await this.resolveFilePath(args);
82
+ try {
83
+ await Promise.all([this._cleanupGroupSemaphore.acquire("getFile"), this._semaphore.acquire(filePath)]);
84
+ if (await pathExists.pathExists(filePath)) {
85
+ // fire and forget
86
+ touch__default["default"](filePath);
87
+ } else {
88
+ await fs__default["default"].promises.mkdir(path__default["default"].dirname(filePath), {
89
+ recursive: true
90
+ });
91
+ await this._cb(filePath, args, this);
92
+ }
93
+ } finally {
94
+ this._semaphore.release(filePath);
95
+ this._cleanupGroupSemaphore.release("getFile");
96
+ }
97
+ return filePath;
98
+ }
99
+ /**
100
+ * Resolves the full file path for the cached file based on the provided arguments.
101
+ *
102
+ * @param args The arguments used to resolve the file path
103
+ * @returns The full path to the cached file
104
+ */
105
+ async resolveFilePath(args) {
106
+ const fileName = await this.resolveFileName(args);
107
+ const filePath = path__default["default"].resolve(this._cachePath, fileName[0], fileName[1], fileName[2], fileName);
108
+ return filePath;
109
+ }
110
+ /**
111
+ * Stops the background cleanup process, but does not delete any cached files.
112
+ */
113
+ destroy() {
114
+ if (this._intervalId) {
115
+ clearInterval(this._intervalId);
116
+ this._intervalId = null;
117
+ }
118
+ }
119
+ async has(args) {
120
+ const filePath = await this.resolveFilePath(args);
121
+ return await pathExists.pathExists(filePath);
122
+ }
123
+ /**
124
+ * Runs a cleanup pass to remove files older than 1 day from the cache.
125
+ * This is called periodically, but can be triggered manually.
126
+ */
127
+ async cleanup() {
128
+ try {
129
+ await this._cleanupGroupSemaphore.acquire("cleanup");
130
+ await findNuke.findNuke(this._cachePath, {
131
+ olderThan: this._ttl,
132
+ verbose: true,
133
+ deleteEmptyDirectories: true
134
+ });
135
+ } finally {
136
+ this._cleanupGroupSemaphore.release("cleanup");
137
+ }
138
+ }
139
+ }
140
+
141
+ exports.FileCache = FileCache;
142
+ //# sourceMappingURL=file-cache.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-cache.cjs","sources":["../src/index.ts"],"sourcesContent":["import path from \"path\";\nimport fs from \"fs\";\nimport { pathExists, pathExistsSync } from \"path-exists\";\nimport sha1 from \"sha1\";\nimport { GroupSemaphore, Semaphore } from \"@chriscdn/promise-semaphore\";\nimport touch from \"touch\";\nimport { findNuke } from \"@chriscdn/find-nuke\";\nimport { Duration } from \"@chriscdn/duration\";\n\ntype DirectoryPath = string;\ntype FileName = 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, args: T, cache: FileCache<T>) => Promise<void>;\n ext: (args: T) => string | Promise<string>;\n resolveCacheKey?: (args: T) => string | Promise<string>;\n ttl: Milliseconds;\n cleanupInterval?: Milliseconds;\n};\n\nclass FileCache<T extends Record<string, any>> {\n private _cachePath: FileCacheOptions<T>[\"cachePath\"];\n private _cb: FileCacheOptions<T>[\"cb\"];\n private _ttl: FileCacheOptions<T>[\"ttl\"];\n private _cleanupInterval: FileCacheOptions<T>[\"cleanupInterval\"];\n private _resolveCacheKey: Required<\n FileCacheOptions<T>\n >[\"resolveCacheKey\"];\n private _ext: FileCacheOptions<T>[\"ext\"];\n private _intervalId: ReturnType<typeof setInterval> | null = null;\n private _semaphore: Semaphore = new Semaphore();\n private _cleanupGroupSemaphore: GroupSemaphore = new GroupSemaphore();\n\n constructor(\n {\n cachePath,\n autoCreateCachePath,\n cb,\n ext,\n ttl,\n cleanupInterval,\n resolveCacheKey,\n }: FileCacheOptions<T>,\n ) {\n this._cachePath = cachePath;\n this._cb = cb;\n this._ext = ext;\n this._ttl = ttl;\n this._cleanupInterval = cleanupInterval ??\n Duration.toMilliseconds({ days: 1 });\n\n this._resolveCacheKey = resolveCacheKey ??\n ((args: T) => (JSON.stringify(args)));\n\n // Startup code\n if ((pathExistsSync(cachePath))) {\n // we're good\n } else if (autoCreateCachePath) {\n fs.mkdirSync(cachePath, { recursive: true });\n } else {\n throw new Error(\"💥 FileCache error: cachePath does not exist.\");\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 * 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 async resolveFileName(args: T): Promise<FileName> {\n const [name, ext] = await Promise.all([\n this._resolveCacheKey(args),\n this._ext(args),\n ]);\n\n const resolvedFileName = sha1(JSON.stringify([name, ext]));\n\n return path.format({\n name: resolvedFileName,\n ext,\n }) as FileName;\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 = await this.resolveFilePath(args);\n\n try {\n await Promise.all([\n this._cleanupGroupSemaphore.acquire(\"getFile\"),\n this._semaphore.acquire(filePath),\n ]);\n\n if (await pathExists(filePath)) {\n // fire and forget\n touch(filePath);\n } else {\n await fs.promises.mkdir(path.dirname(filePath), { recursive: true });\n await this._cb(filePath, args, this);\n }\n } finally {\n this._semaphore.release(filePath);\n this._cleanupGroupSemaphore.release(\"getFile\");\n }\n\n return filePath;\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 async resolveFilePath(args: T): Promise<FilePath> {\n const fileName = await this.resolveFileName(args);\n\n const filePath = path.resolve(\n this._cachePath,\n fileName[0],\n fileName[1],\n fileName[2],\n fileName,\n );\n\n return filePath;\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 async has(args: T): Promise<boolean> {\n const filePath = await this.resolveFilePath(args);\n return await pathExists(filePath);\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._cleanupGroupSemaphore.acquire(\"cleanup\");\n await findNuke(this._cachePath, {\n olderThan: this._ttl,\n verbose: true,\n deleteEmptyDirectories: true,\n });\n } finally {\n this._cleanupGroupSemaphore.release(\"cleanup\");\n }\n }\n}\n\nexport { type DirectoryPath, FileCache, type FileName, type FilePath };\n"],"names":["FileCache","constructor","cachePath","autoCreateCachePath","cb","ext","ttl","cleanupInterval","resolveCacheKey","_cachePath","_cb","_ttl","_cleanupInterval","_resolveCacheKey","_ext","_intervalId","_semaphore","Semaphore","_cleanupGroupSemaphore","GroupSemaphore","Duration","toMilliseconds","days","args","JSON","stringify","pathExistsSync","fs","mkdirSync","recursive","Error","cleanup","setInterval","bind","resolveFileName","name","Promise","all","resolvedFileName","sha1","path","format","getFile","filePath","resolveFilePath","acquire","pathExists","touch","promises","mkdir","dirname","release","fileName","resolve","destroy","clearInterval","has","findNuke","olderThan","verbose","deleteEmptyDirectories"],"mappings":"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"}
@@ -0,0 +1,135 @@
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+ import { pathExistsSync, pathExists } from 'path-exists';
4
+ import sha1 from 'sha1';
5
+ import { Semaphore, GroupSemaphore } from '@chriscdn/promise-semaphore';
6
+ import touch from 'touch';
7
+ import { findNuke } from '@chriscdn/find-nuke';
8
+ import { Duration } from '@chriscdn/duration';
9
+
10
+ class FileCache {
11
+ constructor({
12
+ cachePath,
13
+ autoCreateCachePath,
14
+ cb,
15
+ ext,
16
+ ttl,
17
+ cleanupInterval,
18
+ resolveCacheKey
19
+ }) {
20
+ this._cachePath = void 0;
21
+ this._cb = void 0;
22
+ this._ttl = void 0;
23
+ this._cleanupInterval = void 0;
24
+ this._resolveCacheKey = void 0;
25
+ this._ext = void 0;
26
+ this._intervalId = null;
27
+ this._semaphore = new Semaphore();
28
+ this._cleanupGroupSemaphore = new GroupSemaphore();
29
+ this._cachePath = cachePath;
30
+ this._cb = cb;
31
+ this._ext = ext;
32
+ this._ttl = ttl;
33
+ this._cleanupInterval = cleanupInterval != null ? cleanupInterval : Duration.toMilliseconds({
34
+ days: 1
35
+ });
36
+ this._resolveCacheKey = resolveCacheKey != null ? resolveCacheKey : args => JSON.stringify(args);
37
+ // Startup code
38
+ if (pathExistsSync(cachePath)) ; else if (autoCreateCachePath) {
39
+ fs.mkdirSync(cachePath, {
40
+ recursive: true
41
+ });
42
+ } else {
43
+ throw new Error("💥 FileCache error: cachePath does not exist.");
44
+ }
45
+ // fire and forget
46
+ this.cleanup();
47
+ this._intervalId = setInterval(this.cleanup.bind(this), this._cleanupInterval);
48
+ }
49
+ /**
50
+ * Resolves the filename for the cache based on the provided arguments.
51
+ *
52
+ * Uses the provided `resolveFileName` function if available, or falls back to
53
+ * generating a hash from the arguments to create a unique file name.
54
+ */
55
+ async resolveFileName(args) {
56
+ const [name, ext] = await Promise.all([this._resolveCacheKey(args), this._ext(args)]);
57
+ const resolvedFileName = sha1(JSON.stringify([name, ext]));
58
+ return path.format({
59
+ name: resolvedFileName,
60
+ ext
61
+ });
62
+ }
63
+ /**
64
+ * Retrieves a cached file for the given arguments.
65
+ * If the file doesn't exist, it is generated by the callback and stored.
66
+ *
67
+ * Concurrency-safe, ensuring no conflicts when multiple requests are made
68
+ * for the same file.
69
+ *
70
+ * @param args The arguments used to determine the file's identity and generation
71
+ * @returns The path to the cached or newly created file
72
+ */
73
+ async getFile(args) {
74
+ const filePath = await this.resolveFilePath(args);
75
+ try {
76
+ await Promise.all([this._cleanupGroupSemaphore.acquire("getFile"), this._semaphore.acquire(filePath)]);
77
+ if (await pathExists(filePath)) {
78
+ // fire and forget
79
+ touch(filePath);
80
+ } else {
81
+ await fs.promises.mkdir(path.dirname(filePath), {
82
+ recursive: true
83
+ });
84
+ await this._cb(filePath, args, this);
85
+ }
86
+ } finally {
87
+ this._semaphore.release(filePath);
88
+ this._cleanupGroupSemaphore.release("getFile");
89
+ }
90
+ return filePath;
91
+ }
92
+ /**
93
+ * Resolves the full file path for the cached file based on the provided arguments.
94
+ *
95
+ * @param args The arguments used to resolve the file path
96
+ * @returns The full path to the cached file
97
+ */
98
+ async resolveFilePath(args) {
99
+ const fileName = await this.resolveFileName(args);
100
+ const filePath = path.resolve(this._cachePath, fileName[0], fileName[1], fileName[2], fileName);
101
+ return filePath;
102
+ }
103
+ /**
104
+ * Stops the background cleanup process, but does not delete any cached files.
105
+ */
106
+ destroy() {
107
+ if (this._intervalId) {
108
+ clearInterval(this._intervalId);
109
+ this._intervalId = null;
110
+ }
111
+ }
112
+ async has(args) {
113
+ const filePath = await this.resolveFilePath(args);
114
+ return await pathExists(filePath);
115
+ }
116
+ /**
117
+ * Runs a cleanup pass to remove files older than 1 day from the cache.
118
+ * This is called periodically, but can be triggered manually.
119
+ */
120
+ async cleanup() {
121
+ try {
122
+ await this._cleanupGroupSemaphore.acquire("cleanup");
123
+ await findNuke(this._cachePath, {
124
+ olderThan: this._ttl,
125
+ verbose: true,
126
+ deleteEmptyDirectories: true
127
+ });
128
+ } finally {
129
+ this._cleanupGroupSemaphore.release("cleanup");
130
+ }
131
+ }
132
+ }
133
+
134
+ export { FileCache };
135
+ //# sourceMappingURL=file-cache.modern.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-cache.modern.js","sources":["../src/index.ts"],"sourcesContent":["import path from \"path\";\nimport fs from \"fs\";\nimport { pathExists, pathExistsSync } from \"path-exists\";\nimport sha1 from \"sha1\";\nimport { GroupSemaphore, Semaphore } from \"@chriscdn/promise-semaphore\";\nimport touch from \"touch\";\nimport { findNuke } from \"@chriscdn/find-nuke\";\nimport { Duration } from \"@chriscdn/duration\";\n\ntype DirectoryPath = string;\ntype FileName = 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, args: T, cache: FileCache<T>) => Promise<void>;\n ext: (args: T) => string | Promise<string>;\n resolveCacheKey?: (args: T) => string | Promise<string>;\n ttl: Milliseconds;\n cleanupInterval?: Milliseconds;\n};\n\nclass FileCache<T extends Record<string, any>> {\n private _cachePath: FileCacheOptions<T>[\"cachePath\"];\n private _cb: FileCacheOptions<T>[\"cb\"];\n private _ttl: FileCacheOptions<T>[\"ttl\"];\n private _cleanupInterval: FileCacheOptions<T>[\"cleanupInterval\"];\n private _resolveCacheKey: Required<\n FileCacheOptions<T>\n >[\"resolveCacheKey\"];\n private _ext: FileCacheOptions<T>[\"ext\"];\n private _intervalId: ReturnType<typeof setInterval> | null = null;\n private _semaphore: Semaphore = new Semaphore();\n private _cleanupGroupSemaphore: GroupSemaphore = new GroupSemaphore();\n\n constructor(\n {\n cachePath,\n autoCreateCachePath,\n cb,\n ext,\n ttl,\n cleanupInterval,\n resolveCacheKey,\n }: FileCacheOptions<T>,\n ) {\n this._cachePath = cachePath;\n this._cb = cb;\n this._ext = ext;\n this._ttl = ttl;\n this._cleanupInterval = cleanupInterval ??\n Duration.toMilliseconds({ days: 1 });\n\n this._resolveCacheKey = resolveCacheKey ??\n ((args: T) => (JSON.stringify(args)));\n\n // Startup code\n if ((pathExistsSync(cachePath))) {\n // we're good\n } else if (autoCreateCachePath) {\n fs.mkdirSync(cachePath, { recursive: true });\n } else {\n throw new Error(\"💥 FileCache error: cachePath does not exist.\");\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 * 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 async resolveFileName(args: T): Promise<FileName> {\n const [name, ext] = await Promise.all([\n this._resolveCacheKey(args),\n this._ext(args),\n ]);\n\n const resolvedFileName = sha1(JSON.stringify([name, ext]));\n\n return path.format({\n name: resolvedFileName,\n ext,\n }) as FileName;\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 = await this.resolveFilePath(args);\n\n try {\n await Promise.all([\n this._cleanupGroupSemaphore.acquire(\"getFile\"),\n this._semaphore.acquire(filePath),\n ]);\n\n if (await pathExists(filePath)) {\n // fire and forget\n touch(filePath);\n } else {\n await fs.promises.mkdir(path.dirname(filePath), { recursive: true });\n await this._cb(filePath, args, this);\n }\n } finally {\n this._semaphore.release(filePath);\n this._cleanupGroupSemaphore.release(\"getFile\");\n }\n\n return filePath;\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 async resolveFilePath(args: T): Promise<FilePath> {\n const fileName = await this.resolveFileName(args);\n\n const filePath = path.resolve(\n this._cachePath,\n fileName[0],\n fileName[1],\n fileName[2],\n fileName,\n );\n\n return filePath;\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 async has(args: T): Promise<boolean> {\n const filePath = await this.resolveFilePath(args);\n return await pathExists(filePath);\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._cleanupGroupSemaphore.acquire(\"cleanup\");\n await findNuke(this._cachePath, {\n olderThan: this._ttl,\n verbose: true,\n deleteEmptyDirectories: true,\n });\n } finally {\n this._cleanupGroupSemaphore.release(\"cleanup\");\n }\n }\n}\n\nexport { type DirectoryPath, FileCache, type FileName, type FilePath };\n"],"names":["FileCache","constructor","cachePath","autoCreateCachePath","cb","ext","ttl","cleanupInterval","resolveCacheKey","_cachePath","_cb","_ttl","_cleanupInterval","_resolveCacheKey","_ext","_intervalId","_semaphore","Semaphore","_cleanupGroupSemaphore","GroupSemaphore","Duration","toMilliseconds","days","args","JSON","stringify","pathExistsSync","fs","mkdirSync","recursive","Error","cleanup","setInterval","bind","resolveFileName","name","Promise","all","resolvedFileName","sha1","path","format","getFile","filePath","resolveFilePath","acquire","pathExists","touch","promises","mkdir","dirname","release","fileName","resolve","destroy","clearInterval","has","findNuke","olderThan","verbose","deleteEmptyDirectories"],"mappings":"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"}
@@ -0,0 +1,135 @@
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+ import { pathExistsSync, pathExists } from 'path-exists';
4
+ import sha1 from 'sha1';
5
+ import { Semaphore, GroupSemaphore } from '@chriscdn/promise-semaphore';
6
+ import touch from 'touch';
7
+ import { findNuke } from '@chriscdn/find-nuke';
8
+ import { Duration } from '@chriscdn/duration';
9
+
10
+ class FileCache {
11
+ constructor({
12
+ cachePath,
13
+ autoCreateCachePath,
14
+ cb,
15
+ ext,
16
+ ttl,
17
+ cleanupInterval,
18
+ resolveCacheKey
19
+ }) {
20
+ this._cachePath = void 0;
21
+ this._cb = void 0;
22
+ this._ttl = void 0;
23
+ this._cleanupInterval = void 0;
24
+ this._resolveCacheKey = void 0;
25
+ this._ext = void 0;
26
+ this._intervalId = null;
27
+ this._semaphore = new Semaphore();
28
+ this._cleanupGroupSemaphore = new GroupSemaphore();
29
+ this._cachePath = cachePath;
30
+ this._cb = cb;
31
+ this._ext = ext;
32
+ this._ttl = ttl;
33
+ this._cleanupInterval = cleanupInterval != null ? cleanupInterval : Duration.toMilliseconds({
34
+ days: 1
35
+ });
36
+ this._resolveCacheKey = resolveCacheKey != null ? resolveCacheKey : args => JSON.stringify(args);
37
+ // Startup code
38
+ if (pathExistsSync(cachePath)) ; else if (autoCreateCachePath) {
39
+ fs.mkdirSync(cachePath, {
40
+ recursive: true
41
+ });
42
+ } else {
43
+ throw new Error("💥 FileCache error: cachePath does not exist.");
44
+ }
45
+ // fire and forget
46
+ this.cleanup();
47
+ this._intervalId = setInterval(this.cleanup.bind(this), this._cleanupInterval);
48
+ }
49
+ /**
50
+ * Resolves the filename for the cache based on the provided arguments.
51
+ *
52
+ * Uses the provided `resolveFileName` function if available, or falls back to
53
+ * generating a hash from the arguments to create a unique file name.
54
+ */
55
+ async resolveFileName(args) {
56
+ const [name, ext] = await Promise.all([this._resolveCacheKey(args), this._ext(args)]);
57
+ const resolvedFileName = sha1(JSON.stringify([name, ext]));
58
+ return path.format({
59
+ name: resolvedFileName,
60
+ ext
61
+ });
62
+ }
63
+ /**
64
+ * Retrieves a cached file for the given arguments.
65
+ * If the file doesn't exist, it is generated by the callback and stored.
66
+ *
67
+ * Concurrency-safe, ensuring no conflicts when multiple requests are made
68
+ * for the same file.
69
+ *
70
+ * @param args The arguments used to determine the file's identity and generation
71
+ * @returns The path to the cached or newly created file
72
+ */
73
+ async getFile(args) {
74
+ const filePath = await this.resolveFilePath(args);
75
+ try {
76
+ await Promise.all([this._cleanupGroupSemaphore.acquire("getFile"), this._semaphore.acquire(filePath)]);
77
+ if (await pathExists(filePath)) {
78
+ // fire and forget
79
+ touch(filePath);
80
+ } else {
81
+ await fs.promises.mkdir(path.dirname(filePath), {
82
+ recursive: true
83
+ });
84
+ await this._cb(filePath, args, this);
85
+ }
86
+ } finally {
87
+ this._semaphore.release(filePath);
88
+ this._cleanupGroupSemaphore.release("getFile");
89
+ }
90
+ return filePath;
91
+ }
92
+ /**
93
+ * Resolves the full file path for the cached file based on the provided arguments.
94
+ *
95
+ * @param args The arguments used to resolve the file path
96
+ * @returns The full path to the cached file
97
+ */
98
+ async resolveFilePath(args) {
99
+ const fileName = await this.resolveFileName(args);
100
+ const filePath = path.resolve(this._cachePath, fileName[0], fileName[1], fileName[2], fileName);
101
+ return filePath;
102
+ }
103
+ /**
104
+ * Stops the background cleanup process, but does not delete any cached files.
105
+ */
106
+ destroy() {
107
+ if (this._intervalId) {
108
+ clearInterval(this._intervalId);
109
+ this._intervalId = null;
110
+ }
111
+ }
112
+ async has(args) {
113
+ const filePath = await this.resolveFilePath(args);
114
+ return await pathExists(filePath);
115
+ }
116
+ /**
117
+ * Runs a cleanup pass to remove files older than 1 day from the cache.
118
+ * This is called periodically, but can be triggered manually.
119
+ */
120
+ async cleanup() {
121
+ try {
122
+ await this._cleanupGroupSemaphore.acquire("cleanup");
123
+ await findNuke(this._cachePath, {
124
+ olderThan: this._ttl,
125
+ verbose: true,
126
+ deleteEmptyDirectories: true
127
+ });
128
+ } finally {
129
+ this._cleanupGroupSemaphore.release("cleanup");
130
+ }
131
+ }
132
+ }
133
+
134
+ export { FileCache };
135
+ //# sourceMappingURL=file-cache.module.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-cache.module.js","sources":["../src/index.ts"],"sourcesContent":["import path from \"path\";\nimport fs from \"fs\";\nimport { pathExists, pathExistsSync } from \"path-exists\";\nimport sha1 from \"sha1\";\nimport { GroupSemaphore, Semaphore } from \"@chriscdn/promise-semaphore\";\nimport touch from \"touch\";\nimport { findNuke } from \"@chriscdn/find-nuke\";\nimport { Duration } from \"@chriscdn/duration\";\n\ntype DirectoryPath = string;\ntype FileName = 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, args: T, cache: FileCache<T>) => Promise<void>;\n ext: (args: T) => string | Promise<string>;\n resolveCacheKey?: (args: T) => string | Promise<string>;\n ttl: Milliseconds;\n cleanupInterval?: Milliseconds;\n};\n\nclass FileCache<T extends Record<string, any>> {\n private _cachePath: FileCacheOptions<T>[\"cachePath\"];\n private _cb: FileCacheOptions<T>[\"cb\"];\n private _ttl: FileCacheOptions<T>[\"ttl\"];\n private _cleanupInterval: FileCacheOptions<T>[\"cleanupInterval\"];\n private _resolveCacheKey: Required<\n FileCacheOptions<T>\n >[\"resolveCacheKey\"];\n private _ext: FileCacheOptions<T>[\"ext\"];\n private _intervalId: ReturnType<typeof setInterval> | null = null;\n private _semaphore: Semaphore = new Semaphore();\n private _cleanupGroupSemaphore: GroupSemaphore = new GroupSemaphore();\n\n constructor(\n {\n cachePath,\n autoCreateCachePath,\n cb,\n ext,\n ttl,\n cleanupInterval,\n resolveCacheKey,\n }: FileCacheOptions<T>,\n ) {\n this._cachePath = cachePath;\n this._cb = cb;\n this._ext = ext;\n this._ttl = ttl;\n this._cleanupInterval = cleanupInterval ??\n Duration.toMilliseconds({ days: 1 });\n\n this._resolveCacheKey = resolveCacheKey ??\n ((args: T) => (JSON.stringify(args)));\n\n // Startup code\n if ((pathExistsSync(cachePath))) {\n // we're good\n } else if (autoCreateCachePath) {\n fs.mkdirSync(cachePath, { recursive: true });\n } else {\n throw new Error(\"💥 FileCache error: cachePath does not exist.\");\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 * 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 async resolveFileName(args: T): Promise<FileName> {\n const [name, ext] = await Promise.all([\n this._resolveCacheKey(args),\n this._ext(args),\n ]);\n\n const resolvedFileName = sha1(JSON.stringify([name, ext]));\n\n return path.format({\n name: resolvedFileName,\n ext,\n }) as FileName;\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 = await this.resolveFilePath(args);\n\n try {\n await Promise.all([\n this._cleanupGroupSemaphore.acquire(\"getFile\"),\n this._semaphore.acquire(filePath),\n ]);\n\n if (await pathExists(filePath)) {\n // fire and forget\n touch(filePath);\n } else {\n await fs.promises.mkdir(path.dirname(filePath), { recursive: true });\n await this._cb(filePath, args, this);\n }\n } finally {\n this._semaphore.release(filePath);\n this._cleanupGroupSemaphore.release(\"getFile\");\n }\n\n return filePath;\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 async resolveFilePath(args: T): Promise<FilePath> {\n const fileName = await this.resolveFileName(args);\n\n const filePath = path.resolve(\n this._cachePath,\n fileName[0],\n fileName[1],\n fileName[2],\n fileName,\n );\n\n return filePath;\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 async has(args: T): Promise<boolean> {\n const filePath = await this.resolveFilePath(args);\n return await pathExists(filePath);\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._cleanupGroupSemaphore.acquire(\"cleanup\");\n await findNuke(this._cachePath, {\n olderThan: this._ttl,\n verbose: true,\n deleteEmptyDirectories: true,\n });\n } finally {\n this._cleanupGroupSemaphore.release(\"cleanup\");\n }\n }\n}\n\nexport { type DirectoryPath, FileCache, type FileName, type FilePath };\n"],"names":["FileCache","constructor","cachePath","autoCreateCachePath","cb","ext","ttl","cleanupInterval","resolveCacheKey","_cachePath","_cb","_ttl","_cleanupInterval","_resolveCacheKey","_ext","_intervalId","_semaphore","Semaphore","_cleanupGroupSemaphore","GroupSemaphore","Duration","toMilliseconds","days","args","JSON","stringify","pathExistsSync","fs","mkdirSync","recursive","Error","cleanup","setInterval","bind","resolveFileName","name","Promise","all","resolvedFileName","sha1","path","format","getFile","filePath","resolveFilePath","acquire","pathExists","touch","promises","mkdir","dirname","release","fileName","resolve","destroy","clearInterval","has","findNuke","olderThan","verbose","deleteEmptyDirectories"],"mappings":"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"}
package/lib/index.d.ts CHANGED
@@ -7,7 +7,7 @@ export type FileCacheOptions<T extends Record<string, any>> = {
7
7
  autoCreateCachePath?: boolean;
8
8
  cb: (filePath: FilePath, args: T, cache: FileCache<T>) => Promise<void>;
9
9
  ext: (args: T) => string | Promise<string>;
10
- resolveCacheFileKey?: (args: T) => string | Promise<string>;
10
+ resolveCacheKey?: (args: T) => string | Promise<string>;
11
11
  ttl: Milliseconds;
12
12
  cleanupInterval?: Milliseconds;
13
13
  };
@@ -16,17 +16,19 @@ declare class FileCache<T extends Record<string, any>> {
16
16
  private _cb;
17
17
  private _ttl;
18
18
  private _cleanupInterval;
19
- private _resolveCacheFileKey;
19
+ private _resolveCacheKey;
20
20
  private _ext;
21
21
  private _intervalId;
22
22
  private _semaphore;
23
23
  private _cleanupGroupSemaphore;
24
- constructor({ cachePath, autoCreateCachePath, cb, ext, ttl, cleanupInterval, resolveCacheFileKey, }: FileCacheOptions<T>);
24
+ constructor({ cachePath, autoCreateCachePath, cb, ext, ttl, cleanupInterval, resolveCacheKey, }: FileCacheOptions<T>);
25
25
  /**
26
- * Runs a cleanup pass to remove files older than 1 day from the cache.
27
- * This is called periodically, but can be triggered manually.
26
+ * Resolves the filename for the cache based on the provided arguments.
27
+ *
28
+ * Uses the provided `resolveFileName` function if available, or falls back to
29
+ * generating a hash from the arguments to create a unique file name.
28
30
  */
29
- cleanup(): Promise<void>;
31
+ private resolveFileName;
30
32
  /**
31
33
  * Retrieves a cached file for the given arguments.
32
34
  * If the file doesn't exist, it is generated by the callback and stored.
@@ -38,31 +40,22 @@ declare class FileCache<T extends Record<string, any>> {
38
40
  * @returns The path to the cached or newly created file
39
41
  */
40
42
  getFile(args: T): Promise<FilePath>;
41
- has(args: T): Promise<boolean>;
42
43
  /**
43
- * Deletes a file from the cache if it exists.
44
+ * Resolves the full file path for the cached file based on the provided arguments.
44
45
  *
45
- * @param args The arguments used to determine the file's identity
46
- * @returns True if the file was deleted, false if it didn't exist
46
+ * @param args The arguments used to resolve the file path
47
+ * @returns The full path to the cached file
47
48
  */
48
- expire(args: T): Promise<boolean>;
49
+ resolveFilePath(args: T): Promise<FilePath>;
49
50
  /**
50
51
  * Stops the background cleanup process, but does not delete any cached files.
51
52
  */
52
53
  destroy(): void;
54
+ has(args: T): Promise<boolean>;
53
55
  /**
54
- * Resolves the filename for the cache based on the provided arguments.
55
- *
56
- * Uses the provided `resolveFileName` function if available, or falls back to
57
- * generating a hash from the arguments to create a unique file name.
58
- */
59
- private resolveFileName;
60
- /**
61
- * Resolves the full file path for the cached file based on the provided arguments.
62
- *
63
- * @param args The arguments used to resolve the file path
64
- * @returns The full path to the cached file
56
+ * Runs a cleanup pass to remove files older than 1 day from the cache.
57
+ * This is called periodically, but can be triggered manually.
65
58
  */
66
- resolveFilePath(args: T): Promise<FilePath>;
59
+ cleanup(): Promise<void>;
67
60
  }
68
61
  export { type DirectoryPath, FileCache, type FileName, type FilePath };
package/package.json CHANGED
@@ -1,25 +1,27 @@
1
1
  {
2
2
  "name": "@chriscdn/file-cache",
3
- "version": "0.0.12",
4
- "description": "File cache ",
3
+ "version": "0.0.14",
4
+ "description": "File cache",
5
5
  "repository": "https://github.com/chriscdn/file-cache",
6
6
  "author": "Christopher Meyer <chris@schwiiz.org>",
7
7
  "license": "MIT",
8
8
  "type": "module",
9
9
  "source": "./src/index.ts",
10
- "module": "./lib/index.js",
11
- "types": "./lib/index.d.ts",
10
+ "main": "./lib/file-cache.cjs",
11
+ "module": "./lib/file-cache.module.js",
12
12
  "exports": {
13
- "default": "./lib/index.js"
13
+ "types": "./lib/index.d.ts",
14
+ "require": "./lib/file-cache.cjs",
15
+ "default": "./lib/file-cache.modern.js"
14
16
  },
17
+ "types": "./lib/index.d.ts",
15
18
  "scripts": {
16
- "build": "rimraf ./lib/ && tsc",
19
+ "build": "rm -rf ./lib/ && microbundle --target node --format modern,esm,cjs",
17
20
  "test": "vitest"
18
21
  },
19
22
  "dependencies": {
20
23
  "@chriscdn/duration": "^1.0.3",
21
24
  "@chriscdn/find-nuke": "^0.0.5",
22
- "@chriscdn/memoize": "^1.0.10",
23
25
  "@chriscdn/promise-semaphore": "^3.1.1",
24
26
  "path-exists": "^5.0.0",
25
27
  "sha1": "^1.1.1",
@@ -27,10 +29,13 @@
27
29
  "touch": "^3.1.1"
28
30
  },
29
31
  "devDependencies": {
30
- "@tsconfig/node22": "^22.0.2",
31
32
  "@types/sha1": "^1.1.5",
32
33
  "@types/touch": "^3.1.5",
33
- "typescript": "^5.9.2",
34
- "vitest": "^3.2.4"
35
- }
34
+ "microbundle": "^0.15.1",
35
+ "typescript": "^5.9.3",
36
+ "vitest": "^4.0.5"
37
+ },
38
+ "files": [
39
+ "lib"
40
+ ]
36
41
  }
package/.gitattributes DELETED
@@ -1 +0,0 @@
1
- *.pdf filter=lfs diff=lfs merge=lfs -text
@@ -1,45 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { FileCache } from "../src";
3
- import { Duration } from "@chriscdn/duration";
4
- import { promises as fs } from "fs";
5
- import temp from "temp";
6
- import { pathExists } from "path-exists";
7
-
8
- const pause = async (ms) => new Promise((resolve) => setTimeout(resolve, ms));
9
-
10
- const testPDFFile = "./__tests__/pdfs/lorem.pdf";
11
-
12
- describe("Cache Expiration", async () => {
13
- const cache = new FileCache({
14
- cachePath: await temp.mkdir("file-cache-test"),
15
- cb: async (filePath, { a }: { a: number }) => {
16
- await fs.copyFile(
17
- testPDFFile,
18
- filePath,
19
- );
20
- },
21
- ext: () => ".pdf",
22
- ttl: Duration.toMilliseconds({ seconds: 1 }),
23
- cleanupInterval: Duration.toMilliseconds({ seconds: 2 }),
24
- });
25
-
26
- it("sync", async () => {
27
- //
28
- const filePath3 = await cache.getFile({ a: 3 });
29
-
30
- // test if filePath exists
31
- expect(await pathExists(filePath3)).toBe(true);
32
-
33
- // wait a second
34
- await pause(Duration.toMilliseconds({ seconds: 1 }));
35
-
36
- // should still exist
37
- expect(await pathExists(filePath3)).toBe(true);
38
-
39
- // wait 2 seconds
40
- await pause(Duration.toMilliseconds({ seconds: 2 }));
41
-
42
- // should not longer exist due to cleanup
43
- expect(await pathExists(filePath3)).toBe(false);
44
- });
45
- });
Binary file
package/lib/index.js DELETED
@@ -1,164 +0,0 @@
1
- import path from "path";
2
- import fs from "fs";
3
- import { pathExists, pathExistsSync } from "path-exists";
4
- import sha1 from "sha1";
5
- import { GroupSemaphore, Semaphore } from "@chriscdn/promise-semaphore";
6
- import touch from "touch";
7
- import { findNuke } from "@chriscdn/find-nuke";
8
- import { Duration } from "@chriscdn/duration";
9
- import { Memoize } from "@chriscdn/memoize";
10
- const fsp = fs.promises;
11
- const deleteFile = async (filepath) => await fs.promises.unlink(filepath);
12
- class FileCache {
13
- _cachePath;
14
- _cb;
15
- _ttl;
16
- _cleanupInterval;
17
- _resolveCacheFileKey;
18
- _ext;
19
- _intervalId = null;
20
- _semaphore = new Semaphore();
21
- _cleanupGroupSemaphore = new GroupSemaphore();
22
- // private _fileNameResover(args:T) =>
23
- constructor({ cachePath, autoCreateCachePath, cb, ext, ttl, cleanupInterval, resolveCacheFileKey, }) {
24
- this._cachePath = cachePath;
25
- this._cb = cb;
26
- this._ext = ext;
27
- this._ttl = ttl;
28
- this._cleanupInterval = cleanupInterval ??
29
- Duration.toMilliseconds({ days: 1 });
30
- this._resolveCacheFileKey = resolveCacheFileKey ??
31
- ((args) => sha1(JSON.stringify(args)));
32
- if ((pathExistsSync(cachePath))) {
33
- // we're good
34
- }
35
- else if (autoCreateCachePath) {
36
- fs.mkdirSync(cachePath, { recursive: true });
37
- }
38
- else {
39
- throw new Error("💥 FileCache error: cachePath does not exist.");
40
- }
41
- // fire and forget
42
- this.cleanup();
43
- this._intervalId = setInterval(this.cleanup.bind(this), this._cleanupInterval);
44
- this.resolveFileName = Memoize(this.resolveFileName.bind(this));
45
- this.resolveFilePath = Memoize(this.resolveFilePath.bind(this));
46
- }
47
- /**
48
- * Runs a cleanup pass to remove files older than 1 day from the cache.
49
- * This is called periodically, but can be triggered manually.
50
- */
51
- async cleanup() {
52
- try {
53
- await this._cleanupGroupSemaphore.acquire("cleanup");
54
- await findNuke(this._cachePath, {
55
- olderThan: this._ttl,
56
- verbose: true,
57
- deleteEmptyDirectories: true,
58
- });
59
- }
60
- finally {
61
- this._cleanupGroupSemaphore.release("cleanup");
62
- }
63
- }
64
- /**
65
- * Retrieves a cached file for the given arguments.
66
- * If the file doesn't exist, it is generated by the callback and stored.
67
- *
68
- * Concurrency-safe, ensuring no conflicts when multiple requests are made
69
- * for the same file.
70
- *
71
- * @param args The arguments used to determine the file's identity and generation
72
- * @returns The path to the cached or newly created file
73
- */
74
- async getFile(args) {
75
- const filePath = await this.resolveFilePath(args);
76
- try {
77
- await Promise.all([
78
- this._cleanupGroupSemaphore.acquire("getFile"),
79
- this._semaphore.acquire(filePath),
80
- ]);
81
- if (await pathExists(filePath)) {
82
- // fire and forget
83
- touch(filePath);
84
- }
85
- else {
86
- await fsp.mkdir(path.dirname(filePath), { recursive: true });
87
- await this._cb(filePath, args, this);
88
- }
89
- }
90
- finally {
91
- this._semaphore.release(filePath);
92
- this._cleanupGroupSemaphore.release("getFile");
93
- }
94
- return filePath;
95
- }
96
- async has(args) {
97
- const filePath = await this.resolveFilePath(args);
98
- return await pathExists(filePath);
99
- }
100
- /**
101
- * Deletes a file from the cache if it exists.
102
- *
103
- * @param args The arguments used to determine the file's identity
104
- * @returns True if the file was deleted, false if it didn't exist
105
- */
106
- async expire(args) {
107
- const filePath = await this.resolveFilePath(args);
108
- try {
109
- await Promise.all([
110
- this._cleanupGroupSemaphore.acquire("expire"),
111
- this._semaphore.acquire(filePath),
112
- ]);
113
- if (await pathExists(filePath)) {
114
- await deleteFile(filePath);
115
- return true;
116
- }
117
- else {
118
- return false;
119
- }
120
- }
121
- finally {
122
- this._semaphore.release(filePath);
123
- this._cleanupGroupSemaphore.release("expire");
124
- }
125
- }
126
- /**
127
- * Stops the background cleanup process, but does not delete any cached files.
128
- */
129
- destroy() {
130
- if (this._intervalId) {
131
- clearInterval(this._intervalId);
132
- this._intervalId = null;
133
- }
134
- }
135
- /**
136
- * Resolves the filename for the cache based on the provided arguments.
137
- *
138
- * Uses the provided `resolveFileName` function if available, or falls back to
139
- * generating a hash from the arguments to create a unique file name.
140
- */
141
- async resolveFileName(args) {
142
- const [name, ext] = await Promise.all([
143
- this._resolveCacheFileKey(args),
144
- this._ext(args),
145
- ]);
146
- return path.format({
147
- name,
148
- ext,
149
- });
150
- }
151
- /**
152
- * Resolves the full file path for the cached file based on the provided arguments.
153
- *
154
- * @param args The arguments used to resolve the file path
155
- * @returns The full path to the cached file
156
- */
157
- async resolveFilePath(args) {
158
- const fileName = await this.resolveFileName(args);
159
- const filePath = path.resolve(this._cachePath, fileName[0], fileName[1], fileName[2], fileName);
160
- return filePath;
161
- }
162
- }
163
- export { FileCache };
164
- //# sourceMappingURL=index.js.map
package/lib/index.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/src/index.ts DELETED
@@ -1,225 +0,0 @@
1
- import path from "path";
2
- import fs from "fs";
3
- import { pathExists, pathExistsSync } from "path-exists";
4
- import sha1 from "sha1";
5
- import { GroupSemaphore, Semaphore } from "@chriscdn/promise-semaphore";
6
- import touch from "touch";
7
- import { findNuke } from "@chriscdn/find-nuke";
8
- import { Duration } from "@chriscdn/duration";
9
-
10
- import { Memoize } from "@chriscdn/memoize";
11
-
12
- const fsp = fs.promises;
13
-
14
- type DirectoryPath = string;
15
- type FileName = string;
16
- type FilePath = string;
17
- type Milliseconds = number;
18
-
19
- const deleteFile = async (filepath: FilePath) =>
20
- await fs.promises.unlink(filepath);
21
-
22
- export type FileCacheOptions<T extends Record<string, any>> = {
23
- cachePath: DirectoryPath;
24
- autoCreateCachePath?: boolean;
25
- cb: (filePath: FilePath, args: T, cache: FileCache<T>) => Promise<void>;
26
- ext: (args: T) => string | Promise<string>;
27
- resolveCacheFileKey?: (args: T) => string | Promise<string>;
28
- ttl: Milliseconds;
29
- cleanupInterval?: Milliseconds;
30
- };
31
-
32
- class FileCache<T extends Record<string, any>> {
33
- private _cachePath: FileCacheOptions<T>["cachePath"];
34
-
35
- private _cb: FileCacheOptions<T>["cb"];
36
-
37
- private _ttl: FileCacheOptions<T>["ttl"];
38
- private _cleanupInterval: FileCacheOptions<T>["cleanupInterval"];
39
-
40
- private _resolveCacheFileKey: Required<
41
- FileCacheOptions<T>
42
- >["resolveCacheFileKey"];
43
- private _ext: FileCacheOptions<T>["ext"];
44
-
45
- private _intervalId: ReturnType<typeof setInterval> | null = null;
46
-
47
- private _semaphore: Semaphore = new Semaphore();
48
- private _cleanupGroupSemaphore: GroupSemaphore = new GroupSemaphore();
49
-
50
- // private _fileNameResover(args:T) =>
51
-
52
- constructor(
53
- {
54
- cachePath,
55
- autoCreateCachePath,
56
- cb,
57
- ext,
58
- ttl,
59
- cleanupInterval,
60
- resolveCacheFileKey,
61
- }: FileCacheOptions<T>,
62
- ) {
63
- this._cachePath = cachePath;
64
- this._cb = cb;
65
- this._ext = ext;
66
- this._ttl = ttl;
67
- this._cleanupInterval = cleanupInterval ??
68
- Duration.toMilliseconds({ days: 1 });
69
-
70
- this._resolveCacheFileKey = resolveCacheFileKey ??
71
- ((args: T) => sha1(JSON.stringify(args)));
72
-
73
- if ((pathExistsSync(cachePath))) {
74
- // we're good
75
- } else if (autoCreateCachePath) {
76
- fs.mkdirSync(cachePath, { recursive: true });
77
- } else {
78
- throw new Error("💥 FileCache error: cachePath does not exist.");
79
- }
80
-
81
- // fire and forget
82
- this.cleanup();
83
-
84
- this._intervalId = setInterval(
85
- this.cleanup.bind(this),
86
- this._cleanupInterval,
87
- );
88
-
89
- this.resolveFileName = Memoize(this.resolveFileName.bind(this));
90
- this.resolveFilePath = Memoize(this.resolveFilePath.bind(this));
91
- }
92
-
93
- /**
94
- * Runs a cleanup pass to remove files older than 1 day from the cache.
95
- * This is called periodically, but can be triggered manually.
96
- */
97
- async cleanup() {
98
- try {
99
- await this._cleanupGroupSemaphore.acquire("cleanup");
100
- await findNuke(this._cachePath, {
101
- olderThan: this._ttl,
102
- verbose: true,
103
- deleteEmptyDirectories: true,
104
- });
105
- } finally {
106
- this._cleanupGroupSemaphore.release("cleanup");
107
- }
108
- }
109
-
110
- /**
111
- * Retrieves a cached file for the given arguments.
112
- * If the file doesn't exist, it is generated by the callback and stored.
113
- *
114
- * Concurrency-safe, ensuring no conflicts when multiple requests are made
115
- * for the same file.
116
- *
117
- * @param args The arguments used to determine the file's identity and generation
118
- * @returns The path to the cached or newly created file
119
- */
120
- async getFile(args: T): Promise<FilePath> {
121
- const filePath = await this.resolveFilePath(args);
122
-
123
- try {
124
- await Promise.all([
125
- this._cleanupGroupSemaphore.acquire("getFile"),
126
- this._semaphore.acquire(filePath),
127
- ]);
128
-
129
- if (await pathExists(filePath)) {
130
- // fire and forget
131
- touch(filePath);
132
- } else {
133
- await fsp.mkdir(path.dirname(filePath), { recursive: true });
134
- await this._cb(filePath, args, this);
135
- }
136
- } finally {
137
- this._semaphore.release(filePath);
138
- this._cleanupGroupSemaphore.release("getFile");
139
- }
140
-
141
- return filePath;
142
- }
143
-
144
- async has(args: T): Promise<boolean> {
145
- const filePath = await this.resolveFilePath(args);
146
- return await pathExists(filePath);
147
- }
148
-
149
- /**
150
- * Deletes a file from the cache if it exists.
151
- *
152
- * @param args The arguments used to determine the file's identity
153
- * @returns True if the file was deleted, false if it didn't exist
154
- */
155
- async expire(args: T): Promise<boolean> {
156
- const filePath = await this.resolveFilePath(args);
157
-
158
- try {
159
- await Promise.all([
160
- this._cleanupGroupSemaphore.acquire("expire"),
161
- this._semaphore.acquire(filePath),
162
- ]);
163
-
164
- if (await pathExists(filePath)) {
165
- await deleteFile(filePath);
166
- return true;
167
- } else {
168
- return false;
169
- }
170
- } finally {
171
- this._semaphore.release(filePath);
172
- this._cleanupGroupSemaphore.release("expire");
173
- }
174
- }
175
-
176
- /**
177
- * Stops the background cleanup process, but does not delete any cached files.
178
- */
179
- destroy() {
180
- if (this._intervalId) {
181
- clearInterval(this._intervalId);
182
- this._intervalId = null;
183
- }
184
- }
185
-
186
- /**
187
- * Resolves the filename for the cache based on the provided arguments.
188
- *
189
- * Uses the provided `resolveFileName` function if available, or falls back to
190
- * generating a hash from the arguments to create a unique file name.
191
- */
192
- private async resolveFileName(args: T): Promise<FileName> {
193
- const [name, ext] = await Promise.all([
194
- this._resolveCacheFileKey(args),
195
- this._ext(args),
196
- ]);
197
-
198
- return path.format({
199
- name,
200
- ext,
201
- }) as FileName;
202
- }
203
-
204
- /**
205
- * Resolves the full file path for the cached file based on the provided arguments.
206
- *
207
- * @param args The arguments used to resolve the file path
208
- * @returns The full path to the cached file
209
- */
210
- async resolveFilePath(args: T): Promise<FilePath> {
211
- const fileName = await this.resolveFileName(args);
212
-
213
- const filePath = path.resolve(
214
- this._cachePath,
215
- fileName[0],
216
- fileName[1],
217
- fileName[2],
218
- fileName,
219
- );
220
-
221
- return filePath;
222
- }
223
- }
224
-
225
- export { type DirectoryPath, FileCache, type FileName, type FilePath };
package/tsconfig.json DELETED
@@ -1,11 +0,0 @@
1
- {
2
- "extends": "@tsconfig/node22/tsconfig.json",
3
- "compilerOptions": {
4
- "allowSyntheticDefaultImports": true,
5
- "outDir": "./lib",
6
- "declaration": true,
7
- "declarationDir": "./lib",
8
- "sourceMap": true
9
- },
10
- "include": ["./src/**/*"]
11
- }