@synstack/fs-cache 1.4.3 → 1.4.4
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 +103 -0
- package/dist/fs-cache.index.cjs +3 -3
- package/dist/fs-cache.index.cjs.map +1 -1
- package/dist/fs-cache.index.d.cts +3 -1
- package/dist/fs-cache.index.d.ts +3 -1
- package/dist/fs-cache.index.js +2 -3
- package/dist/fs-cache.index.js.map +1 -1
- package/package.json +5 -4
- package/src/fs-cache.index.ts +1 -0
package/README.md
CHANGED
@@ -1 +1,104 @@
|
|
1
1
|
# @synstack/fs-cache
|
2
|
+
|
3
|
+
Human-friendly file system caching
|
4
|
+
|
5
|
+
> [!WARNING]
|
6
|
+
> This package is included in the [@synstack/synscript](https://github.com/pAIrprogio/synscript) package. It is not recommended to install both packages at the same time.
|
7
|
+
|
8
|
+
## What is it for?
|
9
|
+
|
10
|
+
Sometimes you need to cache expensive function results between program runs. This package makes it easy to cache function outputs to disk with type safety while keeping the file system clean and cache entries readable.
|
11
|
+
|
12
|
+
```typescript
|
13
|
+
import { fsCache } from "@synstack/fs-cache";
|
14
|
+
|
15
|
+
// Create a cache in the .cache/expensive/function.json file
|
16
|
+
const cache = fsCache(".cache").key(["expensive", "function"]).pretty(true);
|
17
|
+
|
18
|
+
// Cache an expensive function
|
19
|
+
const cachedExpensiveFunction = cache.fn(expensiveFunction);
|
20
|
+
```
|
21
|
+
|
22
|
+
## Caching philosophy
|
23
|
+
|
24
|
+
The idea is to generate cache entries without poluting the file system with expired entries. This requires a two-step matching process:
|
25
|
+
|
26
|
+
- First file is matched based on the key defined with `key()`
|
27
|
+
- If the file exists, a second check is done based on input parameters and can be customized with `signatureFn()` which defaults to [`deepEqual`](#Deep-Equality)
|
28
|
+
- If it matches, the output is returned from the cache
|
29
|
+
- If it doesn't match or the file doesn't exist, the function is called, the output is cached, and then returned
|
30
|
+
|
31
|
+
## Installation
|
32
|
+
|
33
|
+
```bash
|
34
|
+
# Using npm
|
35
|
+
npm install @synstack/fs-cache
|
36
|
+
|
37
|
+
# Using yarn
|
38
|
+
yarn add @synstack/fs-cache
|
39
|
+
|
40
|
+
# Using pnpm
|
41
|
+
pnpm add @synstack/fs-cache
|
42
|
+
```
|
43
|
+
|
44
|
+
## Features
|
45
|
+
|
46
|
+
### [@synstack/fs](../fs/README.md) interoperability
|
47
|
+
|
48
|
+
Cache directory can be initialized using an `FsDir` instance:
|
49
|
+
|
50
|
+
```typescript
|
51
|
+
import { dir } from "@synstack/fs";
|
52
|
+
|
53
|
+
const cacheDir = dir(".cache");
|
54
|
+
const cache = fsCache(cacheDir);
|
55
|
+
```
|
56
|
+
|
57
|
+
### Function Caching
|
58
|
+
|
59
|
+
Cache expensive function results with type safety:
|
60
|
+
|
61
|
+
```typescript
|
62
|
+
import { fsCache } from "@synstack/fs-cache";
|
63
|
+
|
64
|
+
const cache = fsCache(".cache");
|
65
|
+
|
66
|
+
// Cache with static key
|
67
|
+
const cachedFn = cache.key(["myFunction"]).fn((x: number) => x * x);
|
68
|
+
```
|
69
|
+
|
70
|
+
### Cache Control
|
71
|
+
|
72
|
+
Fine-grained control over cache behavior:
|
73
|
+
|
74
|
+
```typescript
|
75
|
+
// Pretty-print cached JSON
|
76
|
+
const cache = fsCache(".cache").pretty(true);
|
77
|
+
|
78
|
+
// Custom cache signature generation
|
79
|
+
const cache2 = fsCache(".cache")
|
80
|
+
.signatureFn((arg: string) => arg.toLowerCase())
|
81
|
+
.key(["normalized"]);
|
82
|
+
|
83
|
+
// Lock cached values
|
84
|
+
await cache.lock(true, ["key"]); // Prevent updates
|
85
|
+
await cache.lock(false, ["key"]); // Allow updates
|
86
|
+
|
87
|
+
// Manual cache operations
|
88
|
+
const [status, value] = await cache.get(["key"]);
|
89
|
+
await cache.set(["key"], "value");
|
90
|
+
|
91
|
+
// Set a default cache value if the file doesn't exist
|
92
|
+
await cache.setDefault(["key"], "default");
|
93
|
+
```
|
94
|
+
|
95
|
+
## Deep Equality
|
96
|
+
|
97
|
+
A custom deep equality function is provided.
|
98
|
+
The only custom behavior implemented, is that objects with undefined values are considered equal to objects with matching missing values.
|
99
|
+
|
100
|
+
```typescript
|
101
|
+
import { deepEqual } from "@synstack/fs-cache";
|
102
|
+
|
103
|
+
deepEqual({ a: undefined }, {}); // true
|
104
|
+
```
|
package/dist/fs-cache.index.cjs
CHANGED
@@ -21,13 +21,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
21
21
|
var fs_cache_index_exports = {};
|
22
22
|
__export(fs_cache_index_exports, {
|
23
23
|
FsCache: () => FsCache,
|
24
|
+
deepEqual: () => deepEqual,
|
24
25
|
fsCache: () => fsCache
|
25
26
|
});
|
26
27
|
module.exports = __toCommonJS(fs_cache_index_exports);
|
27
28
|
|
28
|
-
// src/fs-cache.lib.ts
|
29
|
-
var import_fs = require("@synstack/fs");
|
30
|
-
|
31
29
|
// src/deepEqual.lib.ts
|
32
30
|
function deepEqual(obj1, obj2) {
|
33
31
|
if (obj1 === obj2) {
|
@@ -55,6 +53,7 @@ function isPrimitive(obj) {
|
|
55
53
|
}
|
56
54
|
|
57
55
|
// src/fs-cache.lib.ts
|
56
|
+
var import_fs = require("@synstack/fs");
|
58
57
|
var FsCache = class _FsCache {
|
59
58
|
_config;
|
60
59
|
constructor(config) {
|
@@ -143,6 +142,7 @@ var fsCache = FsCache.cwd;
|
|
143
142
|
// Annotate the CommonJS export names for ESM import in node:
|
144
143
|
0 && (module.exports = {
|
145
144
|
FsCache,
|
145
|
+
deepEqual,
|
146
146
|
fsCache
|
147
147
|
});
|
148
148
|
//# sourceMappingURL=fs-cache.index.cjs.map
|
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"sources":["../src/fs-cache.index.ts","../src/
|
1
|
+
{"version":3,"sources":["../src/fs-cache.index.ts","../src/deepEqual.lib.ts","../src/fs-cache.lib.ts"],"sourcesContent":["export * from \"./deepEqual.lib.ts\";\nexport * from \"./fs-cache.lib.ts\";\n","export function deepEqual(obj1: any, obj2: any) {\n if (obj1 === obj2) {\n return true;\n }\n\n if (isPrimitive(obj1) && isPrimitive(obj2)) {\n return obj1 === obj2;\n }\n\n if (obj1 == null || obj2 == null) {\n return false;\n }\n\n const keys1 = Object.keys(obj1 as {}).filter((k) => obj1[k] !== undefined);\n const keys2 = Object.keys(obj2 as {}).filter((k) => obj2[k] !== undefined);\n\n if (keys1.length !== keys2.length) return false;\n\n for (const key of keys1) {\n if (!keys2.includes(key)) return false;\n if (!deepEqual(obj1[key], obj2[key])) {\n return false;\n }\n }\n\n return true;\n}\n\n//check if value is primitive\nfunction isPrimitive(obj: any) {\n return obj !== Object(obj);\n}\n","import { dir, FsDir } from \"@synstack/fs\";\nimport { type OneToN } from \"../../shared/src/ts.utils.ts\";\nimport { deepEqual } from \"./deepEqual.lib.ts\";\n\ntype $Partial<T> = Partial<T>;\n\ninterface CacheValue<TValue = any> {\n isLocked?: boolean;\n input: any;\n output: TValue;\n}\n\ndeclare namespace FsCache {\n export type KeyFn<TFnArgs extends any[] = any[]> =\n | string\n | ((...args: TFnArgs) => string);\n\n export type Key<TFnArgs extends any[] = any[]> = OneToN<KeyFn<TFnArgs>>;\n\n export type SignatureFn<TFnArgs extends any[] = any[]> = (\n ...args: TFnArgs\n ) => any;\n\n interface Options<TFnArgs extends any[] = any[]> {\n cwd: string;\n key: Key<TFnArgs>;\n signatureFn?: SignatureFn<TFnArgs>;\n pretty?: boolean;\n }\n\n namespace Options {\n export type Partial = $Partial<Options>;\n }\n}\n\nexport class FsCache<\n TConfig extends FsCache.Options.Partial = FsCache.Options.Partial,\n> {\n private readonly _config: TConfig;\n\n private constructor(config: TConfig) {\n this._config = {\n ...config,\n } as TConfig & { signatureFn: FsCache.SignatureFn };\n }\n\n // eslint-disable-next-line @typescript-eslint/require-await\n private async serializeInput<TFnArgs extends any[]>(...args: TFnArgs) {\n if (this._config.signatureFn) return this._config.signatureFn(...args);\n return args;\n }\n\n public static cwd(this: void, cwd: string | FsDir) {\n if (cwd instanceof FsDir) return new FsCache({ cwd: cwd.path });\n return new FsCache({ cwd });\n }\n\n public key<TFnArgs extends any[], TKey extends FsCache.Key<TFnArgs>>(\n key: TKey,\n ) {\n return new FsCache({ ...this._config, key });\n }\n\n public signatureFn<TFnArgs extends any[]>(\n signatureFn: FsCache.SignatureFn<TFnArgs>,\n ) {\n return new FsCache({ ...this._config, signatureFn });\n }\n\n public pretty(pretty: boolean) {\n return new FsCache({ ...this._config, pretty });\n }\n\n private static keyToRelativePath(key: FsCache.Key, args: any[]) {\n // eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n return `./${key.map((k) => (k instanceof Function ? k(...args) : k)).join(\"/\")}.json`;\n }\n\n public async lock<TFnArgs extends any[]>(\n this: FsCache<FsCache.Options<TFnArgs>>,\n isLocked: boolean,\n args: TFnArgs,\n ) {\n const relativePath = FsCache.keyToRelativePath(this._config.key, args);\n const cacheDir = dir(this._config.cwd);\n const file = cacheDir.file(relativePath);\n\n if (!(await file.exists())) {\n console.warn(\n \"[fs-cache] Failed to update lock on non-existing file\",\n file.path,\n );\n return;\n }\n\n const value = await file.read.json<CacheValue>();\n await file.write.json({\n ...value,\n isLocked,\n });\n }\n\n public async get<TFnArgs extends any[], TValue = any>(\n this: FsCache<FsCache.Options<TFnArgs>>,\n args: TFnArgs,\n ): Promise<[\"miss\", null] | [\"hit\", TValue]> {\n const relativePath = FsCache.keyToRelativePath(this._config.key, args);\n const cacheDir = dir(this._config.cwd);\n\n if (!(await cacheDir.file(relativePath).exists())) return [\"miss\", null];\n\n const value = await cacheDir\n .file(relativePath)\n .read.json<CacheValue<TValue>>();\n\n if (value === null) return [\"miss\", null];\n\n if (\n value.isLocked ||\n deepEqual(value.input, await this.serializeInput(...args))\n )\n return [\"hit\", value.output];\n\n return [\"miss\", null];\n }\n\n public async setDefault<TFnArgs extends any[], TValue = any>(\n this: FsCache<FsCache.Options<TFnArgs>>,\n args: TFnArgs,\n defaultValue: TValue,\n ) {\n const [status] = await this.get<TFnArgs, TValue>(args);\n if (status === \"hit\") return false;\n return this.set(args, defaultValue);\n }\n\n public async set<TFnArgs extends any[]>(\n this: FsCache<FsCache.Options<TFnArgs>>,\n args: TFnArgs,\n value: any,\n ) {\n const relativePath = FsCache.keyToRelativePath(this._config.key, args);\n const file = dir(this._config.cwd).file(relativePath);\n return file.write.text(\n JSON.stringify(\n {\n input: await this.serializeInput(...args),\n output: value,\n } satisfies CacheValue,\n null,\n this._config.pretty ? 2 : undefined,\n ),\n );\n }\n\n public fn<TFnArgs extends any[], TFnOutput>(\n this: FsCache<FsCache.Options<TFnArgs>>,\n fn: (...args: TFnArgs) => TFnOutput,\n ): (...args: TFnArgs) => Promise<Awaited<TFnOutput>> {\n return async (...args: TFnArgs): Promise<Awaited<TFnOutput>> => {\n const [status, value] = await this.get<TFnArgs, TFnOutput>(args);\n if (status === \"hit\") return value as Awaited<TFnOutput>;\n\n const output = await Promise.resolve(fn(...args));\n await this.set(args, output);\n return output;\n };\n }\n}\n\nexport const fsCache = FsCache.cwd;\n"],"mappings":"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","names":[]}
|
@@ -1,5 +1,7 @@
|
|
1
1
|
import { FsDir } from '@synstack/fs';
|
2
2
|
|
3
|
+
declare function deepEqual(obj1: any, obj2: any): boolean;
|
4
|
+
|
3
5
|
/**
|
4
6
|
* One or more values
|
5
7
|
*/
|
@@ -45,4 +47,4 @@ declare class FsCache<TConfig extends FsCache.Options.Partial = FsCache.Options.
|
|
45
47
|
}
|
46
48
|
declare const fsCache: typeof FsCache.cwd;
|
47
49
|
|
48
|
-
export { FsCache, fsCache };
|
50
|
+
export { FsCache, deepEqual, fsCache };
|
package/dist/fs-cache.index.d.ts
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
import { FsDir } from '@synstack/fs';
|
2
2
|
|
3
|
+
declare function deepEqual(obj1: any, obj2: any): boolean;
|
4
|
+
|
3
5
|
/**
|
4
6
|
* One or more values
|
5
7
|
*/
|
@@ -45,4 +47,4 @@ declare class FsCache<TConfig extends FsCache.Options.Partial = FsCache.Options.
|
|
45
47
|
}
|
46
48
|
declare const fsCache: typeof FsCache.cwd;
|
47
49
|
|
48
|
-
export { FsCache, fsCache };
|
50
|
+
export { FsCache, deepEqual, fsCache };
|
package/dist/fs-cache.index.js
CHANGED
@@ -1,6 +1,3 @@
|
|
1
|
-
// src/fs-cache.lib.ts
|
2
|
-
import { dir, FsDir } from "@synstack/fs";
|
3
|
-
|
4
1
|
// src/deepEqual.lib.ts
|
5
2
|
function deepEqual(obj1, obj2) {
|
6
3
|
if (obj1 === obj2) {
|
@@ -28,6 +25,7 @@ function isPrimitive(obj) {
|
|
28
25
|
}
|
29
26
|
|
30
27
|
// src/fs-cache.lib.ts
|
28
|
+
import { dir, FsDir } from "@synstack/fs";
|
31
29
|
var FsCache = class _FsCache {
|
32
30
|
_config;
|
33
31
|
constructor(config) {
|
@@ -115,6 +113,7 @@ var FsCache = class _FsCache {
|
|
115
113
|
var fsCache = FsCache.cwd;
|
116
114
|
export {
|
117
115
|
FsCache,
|
116
|
+
deepEqual,
|
118
117
|
fsCache
|
119
118
|
};
|
120
119
|
//# sourceMappingURL=fs-cache.index.js.map
|
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"sources":["../src/
|
1
|
+
{"version":3,"sources":["../src/deepEqual.lib.ts","../src/fs-cache.lib.ts"],"sourcesContent":["export function deepEqual(obj1: any, obj2: any) {\n if (obj1 === obj2) {\n return true;\n }\n\n if (isPrimitive(obj1) && isPrimitive(obj2)) {\n return obj1 === obj2;\n }\n\n if (obj1 == null || obj2 == null) {\n return false;\n }\n\n const keys1 = Object.keys(obj1 as {}).filter((k) => obj1[k] !== undefined);\n const keys2 = Object.keys(obj2 as {}).filter((k) => obj2[k] !== undefined);\n\n if (keys1.length !== keys2.length) return false;\n\n for (const key of keys1) {\n if (!keys2.includes(key)) return false;\n if (!deepEqual(obj1[key], obj2[key])) {\n return false;\n }\n }\n\n return true;\n}\n\n//check if value is primitive\nfunction isPrimitive(obj: any) {\n return obj !== Object(obj);\n}\n","import { dir, FsDir } from \"@synstack/fs\";\nimport { type OneToN } from \"../../shared/src/ts.utils.ts\";\nimport { deepEqual } from \"./deepEqual.lib.ts\";\n\ntype $Partial<T> = Partial<T>;\n\ninterface CacheValue<TValue = any> {\n isLocked?: boolean;\n input: any;\n output: TValue;\n}\n\ndeclare namespace FsCache {\n export type KeyFn<TFnArgs extends any[] = any[]> =\n | string\n | ((...args: TFnArgs) => string);\n\n export type Key<TFnArgs extends any[] = any[]> = OneToN<KeyFn<TFnArgs>>;\n\n export type SignatureFn<TFnArgs extends any[] = any[]> = (\n ...args: TFnArgs\n ) => any;\n\n interface Options<TFnArgs extends any[] = any[]> {\n cwd: string;\n key: Key<TFnArgs>;\n signatureFn?: SignatureFn<TFnArgs>;\n pretty?: boolean;\n }\n\n namespace Options {\n export type Partial = $Partial<Options>;\n }\n}\n\nexport class FsCache<\n TConfig extends FsCache.Options.Partial = FsCache.Options.Partial,\n> {\n private readonly _config: TConfig;\n\n private constructor(config: TConfig) {\n this._config = {\n ...config,\n } as TConfig & { signatureFn: FsCache.SignatureFn };\n }\n\n // eslint-disable-next-line @typescript-eslint/require-await\n private async serializeInput<TFnArgs extends any[]>(...args: TFnArgs) {\n if (this._config.signatureFn) return this._config.signatureFn(...args);\n return args;\n }\n\n public static cwd(this: void, cwd: string | FsDir) {\n if (cwd instanceof FsDir) return new FsCache({ cwd: cwd.path });\n return new FsCache({ cwd });\n }\n\n public key<TFnArgs extends any[], TKey extends FsCache.Key<TFnArgs>>(\n key: TKey,\n ) {\n return new FsCache({ ...this._config, key });\n }\n\n public signatureFn<TFnArgs extends any[]>(\n signatureFn: FsCache.SignatureFn<TFnArgs>,\n ) {\n return new FsCache({ ...this._config, signatureFn });\n }\n\n public pretty(pretty: boolean) {\n return new FsCache({ ...this._config, pretty });\n }\n\n private static keyToRelativePath(key: FsCache.Key, args: any[]) {\n // eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n return `./${key.map((k) => (k instanceof Function ? k(...args) : k)).join(\"/\")}.json`;\n }\n\n public async lock<TFnArgs extends any[]>(\n this: FsCache<FsCache.Options<TFnArgs>>,\n isLocked: boolean,\n args: TFnArgs,\n ) {\n const relativePath = FsCache.keyToRelativePath(this._config.key, args);\n const cacheDir = dir(this._config.cwd);\n const file = cacheDir.file(relativePath);\n\n if (!(await file.exists())) {\n console.warn(\n \"[fs-cache] Failed to update lock on non-existing file\",\n file.path,\n );\n return;\n }\n\n const value = await file.read.json<CacheValue>();\n await file.write.json({\n ...value,\n isLocked,\n });\n }\n\n public async get<TFnArgs extends any[], TValue = any>(\n this: FsCache<FsCache.Options<TFnArgs>>,\n args: TFnArgs,\n ): Promise<[\"miss\", null] | [\"hit\", TValue]> {\n const relativePath = FsCache.keyToRelativePath(this._config.key, args);\n const cacheDir = dir(this._config.cwd);\n\n if (!(await cacheDir.file(relativePath).exists())) return [\"miss\", null];\n\n const value = await cacheDir\n .file(relativePath)\n .read.json<CacheValue<TValue>>();\n\n if (value === null) return [\"miss\", null];\n\n if (\n value.isLocked ||\n deepEqual(value.input, await this.serializeInput(...args))\n )\n return [\"hit\", value.output];\n\n return [\"miss\", null];\n }\n\n public async setDefault<TFnArgs extends any[], TValue = any>(\n this: FsCache<FsCache.Options<TFnArgs>>,\n args: TFnArgs,\n defaultValue: TValue,\n ) {\n const [status] = await this.get<TFnArgs, TValue>(args);\n if (status === \"hit\") return false;\n return this.set(args, defaultValue);\n }\n\n public async set<TFnArgs extends any[]>(\n this: FsCache<FsCache.Options<TFnArgs>>,\n args: TFnArgs,\n value: any,\n ) {\n const relativePath = FsCache.keyToRelativePath(this._config.key, args);\n const file = dir(this._config.cwd).file(relativePath);\n return file.write.text(\n JSON.stringify(\n {\n input: await this.serializeInput(...args),\n output: value,\n } satisfies CacheValue,\n null,\n this._config.pretty ? 2 : undefined,\n ),\n );\n }\n\n public fn<TFnArgs extends any[], TFnOutput>(\n this: FsCache<FsCache.Options<TFnArgs>>,\n fn: (...args: TFnArgs) => TFnOutput,\n ): (...args: TFnArgs) => Promise<Awaited<TFnOutput>> {\n return async (...args: TFnArgs): Promise<Awaited<TFnOutput>> => {\n const [status, value] = await this.get<TFnArgs, TFnOutput>(args);\n if (status === \"hit\") return value as Awaited<TFnOutput>;\n\n const output = await Promise.resolve(fn(...args));\n await this.set(args, output);\n return output;\n };\n }\n}\n\nexport const fsCache = FsCache.cwd;\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
@@ -4,11 +4,12 @@
|
|
4
4
|
"publishConfig": {
|
5
5
|
"access": "public"
|
6
6
|
},
|
7
|
-
"version": "1.4.
|
7
|
+
"version": "1.4.4",
|
8
8
|
"description": "Human-friendly file system caching",
|
9
9
|
"keywords": [
|
10
10
|
"cache",
|
11
|
-
"memoize"
|
11
|
+
"memoize",
|
12
|
+
"file-system"
|
12
13
|
],
|
13
14
|
"author": {
|
14
15
|
"name": "pAIrprog",
|
@@ -43,7 +44,7 @@
|
|
43
44
|
}
|
44
45
|
},
|
45
46
|
"dependencies": {
|
46
|
-
"@synstack/fs": "1.4.
|
47
|
+
"@synstack/fs": "1.4.3"
|
47
48
|
},
|
48
49
|
"devDependencies": {
|
49
50
|
"@types/node": "^22.10.1",
|
@@ -55,5 +56,5 @@
|
|
55
56
|
"!src/**/*.test.ts",
|
56
57
|
"dist/**/*"
|
57
58
|
],
|
58
|
-
"gitHead": "
|
59
|
+
"gitHead": "2868de5f8935b959b5b5faa6606a2fa9fcad27ab"
|
59
60
|
}
|
package/src/fs-cache.index.ts
CHANGED