@synstack/fs-cache 1.2.4 → 1.3.0

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.
@@ -58,7 +58,14 @@ function isPrimitive(obj) {
58
58
  var FsCache = class _FsCache {
59
59
  _config;
60
60
  constructor(config) {
61
- this._config = config;
61
+ this._config = {
62
+ ...config
63
+ };
64
+ }
65
+ // eslint-disable-next-line @typescript-eslint/require-await
66
+ async serializeInput(...args) {
67
+ if (this._config.signatureFn) return this._config.signatureFn(...args);
68
+ return args;
62
69
  }
63
70
  static cwd(cwd) {
64
71
  if (cwd instanceof import_fs.FsDir) return new _FsCache({ cwd: cwd.path });
@@ -67,6 +74,9 @@ var FsCache = class _FsCache {
67
74
  key(key) {
68
75
  return new _FsCache({ ...this._config, key });
69
76
  }
77
+ signatureFn(signatureFn) {
78
+ return new _FsCache({ ...this._config, signatureFn });
79
+ }
70
80
  pretty(pretty) {
71
81
  return new _FsCache({ ...this._config, pretty });
72
82
  }
@@ -86,8 +96,8 @@ var FsCache = class _FsCache {
86
96
  }
87
97
  const value = await file.read.json();
88
98
  await file.write.json({
89
- isLocked,
90
- ...value
99
+ ...value,
100
+ isLocked
91
101
  });
92
102
  }
93
103
  async get(args) {
@@ -96,7 +106,7 @@ var FsCache = class _FsCache {
96
106
  if (!await cacheDir.file(relativePath).exists()) return ["miss", null];
97
107
  const value = await cacheDir.file(relativePath).read.json();
98
108
  if (value === null) return ["miss", null];
99
- if (value.isLocked || deepEqual(value.input, args))
109
+ if (value.isLocked || deepEqual(value.input, await this.serializeInput(...args)))
100
110
  return ["hit", value.output];
101
111
  return ["miss", null];
102
112
  }
@@ -110,7 +120,10 @@ var FsCache = class _FsCache {
110
120
  const file = (0, import_fs.dir)(this._config.cwd).file(relativePath);
111
121
  return file.write.text(
112
122
  JSON.stringify(
113
- { input: args, output: value },
123
+ {
124
+ input: await this.serializeInput(...args),
125
+ output: value
126
+ },
114
127
  null,
115
128
  this._config.pretty ? 2 : void 0
116
129
  )
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/fs-cache.index.ts","../src/fs-cache.lib.ts","../src/deepEqual.lib.ts"],"sourcesContent":["export * from \"./fs-cache.lib.ts\";\n","import { type OneToN } from \"../../shared/src/ts.utils.ts\";\nimport { dir, FsDir } from \"@synstack/fs\";\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 export type Key<TFnArgs extends any[] = any[]> = OneToN<KeyFn<TFnArgs>>;\n\n interface Options<TFnArgs extends any[] = any[]> {\n cwd: string;\n key: Key<TFnArgs>;\n pretty?: boolean;\n }\n\n namespace Options {\n export type Partial = $Partial<Options>;\n }\n}\n\nexport class FsCache<TConfig extends FsCache.Options.Partial> {\n private readonly _config: TConfig;\n\n private constructor(config: TConfig) {\n this._config = config;\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 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 isLocked,\n ...value,\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 (value.isLocked || deepEqual(value.input, args))\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 { input: args, output: value } 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) => Promise<TFnOutput> | TFnOutput,\n ): (...args: TFnArgs) => Promise<TFnOutput> {\n return async (...args: TFnArgs): Promise<TFnOutput> => {\n const [status, value] = await this.get<TFnArgs, TFnOutput>(args);\n if (status === \"hit\") return value;\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","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"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/fs-cache.index.ts","../src/fs-cache.lib.ts","../src/deepEqual.lib.ts"],"sourcesContent":["export * from \"./fs-cache.lib.ts\";\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<TConfig extends FsCache.Options.Partial> {\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","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"],"mappings":"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","names":[]}
@@ -9,9 +9,11 @@ type $Partial<T> = Partial<T>;
9
9
  declare namespace FsCache {
10
10
  type KeyFn<TFnArgs extends any[] = any[]> = string | ((...args: TFnArgs) => string);
11
11
  type Key<TFnArgs extends any[] = any[]> = OneToN<KeyFn<TFnArgs>>;
12
+ type SignatureFn<TFnArgs extends any[] = any[]> = (...args: TFnArgs) => any;
12
13
  interface Options<TFnArgs extends any[] = any[]> {
13
14
  cwd: string;
14
15
  key: Key<TFnArgs>;
16
+ signatureFn?: SignatureFn<TFnArgs>;
15
17
  pretty?: boolean;
16
18
  }
17
19
  namespace Options {
@@ -21,12 +23,16 @@ declare namespace FsCache {
21
23
  declare class FsCache<TConfig extends FsCache.Options.Partial> {
22
24
  private readonly _config;
23
25
  private constructor();
26
+ private serializeInput;
24
27
  static cwd(this: void, cwd: string | FsDir): FsCache<{
25
28
  cwd: string;
26
29
  }>;
27
30
  key<TFnArgs extends any[], TKey extends FsCache.Key<TFnArgs>>(key: TKey): FsCache<TConfig & {
28
31
  key: TKey;
29
32
  }>;
33
+ signatureFn<TFnArgs extends any[]>(signatureFn: FsCache.SignatureFn<TFnArgs>): FsCache<TConfig & {
34
+ signatureFn: FsCache.SignatureFn<TFnArgs>;
35
+ }>;
30
36
  pretty(pretty: boolean): FsCache<TConfig & {
31
37
  pretty: boolean;
32
38
  }>;
@@ -35,7 +41,7 @@ declare class FsCache<TConfig extends FsCache.Options.Partial> {
35
41
  get<TFnArgs extends any[], TValue = any>(this: FsCache<FsCache.Options<TFnArgs>>, args: TFnArgs): Promise<["miss", null] | ["hit", TValue]>;
36
42
  setDefault<TFnArgs extends any[], TValue = any>(this: FsCache<FsCache.Options<TFnArgs>>, args: TFnArgs, defaultValue: TValue): Promise<false | void>;
37
43
  set<TFnArgs extends any[]>(this: FsCache<FsCache.Options<TFnArgs>>, args: TFnArgs, value: any): Promise<void>;
38
- fn<TFnArgs extends any[], TFnOutput>(this: FsCache<FsCache.Options<TFnArgs>>, fn: (...args: TFnArgs) => Promise<TFnOutput> | TFnOutput): (...args: TFnArgs) => Promise<TFnOutput>;
44
+ fn<TFnArgs extends any[], TFnOutput>(this: FsCache<FsCache.Options<TFnArgs>>, fn: (...args: TFnArgs) => TFnOutput): (...args: TFnArgs) => Promise<Awaited<TFnOutput>>;
39
45
  }
40
46
  declare const fsCache: typeof FsCache.cwd;
41
47
 
@@ -9,9 +9,11 @@ type $Partial<T> = Partial<T>;
9
9
  declare namespace FsCache {
10
10
  type KeyFn<TFnArgs extends any[] = any[]> = string | ((...args: TFnArgs) => string);
11
11
  type Key<TFnArgs extends any[] = any[]> = OneToN<KeyFn<TFnArgs>>;
12
+ type SignatureFn<TFnArgs extends any[] = any[]> = (...args: TFnArgs) => any;
12
13
  interface Options<TFnArgs extends any[] = any[]> {
13
14
  cwd: string;
14
15
  key: Key<TFnArgs>;
16
+ signatureFn?: SignatureFn<TFnArgs>;
15
17
  pretty?: boolean;
16
18
  }
17
19
  namespace Options {
@@ -21,12 +23,16 @@ declare namespace FsCache {
21
23
  declare class FsCache<TConfig extends FsCache.Options.Partial> {
22
24
  private readonly _config;
23
25
  private constructor();
26
+ private serializeInput;
24
27
  static cwd(this: void, cwd: string | FsDir): FsCache<{
25
28
  cwd: string;
26
29
  }>;
27
30
  key<TFnArgs extends any[], TKey extends FsCache.Key<TFnArgs>>(key: TKey): FsCache<TConfig & {
28
31
  key: TKey;
29
32
  }>;
33
+ signatureFn<TFnArgs extends any[]>(signatureFn: FsCache.SignatureFn<TFnArgs>): FsCache<TConfig & {
34
+ signatureFn: FsCache.SignatureFn<TFnArgs>;
35
+ }>;
30
36
  pretty(pretty: boolean): FsCache<TConfig & {
31
37
  pretty: boolean;
32
38
  }>;
@@ -35,7 +41,7 @@ declare class FsCache<TConfig extends FsCache.Options.Partial> {
35
41
  get<TFnArgs extends any[], TValue = any>(this: FsCache<FsCache.Options<TFnArgs>>, args: TFnArgs): Promise<["miss", null] | ["hit", TValue]>;
36
42
  setDefault<TFnArgs extends any[], TValue = any>(this: FsCache<FsCache.Options<TFnArgs>>, args: TFnArgs, defaultValue: TValue): Promise<false | void>;
37
43
  set<TFnArgs extends any[]>(this: FsCache<FsCache.Options<TFnArgs>>, args: TFnArgs, value: any): Promise<void>;
38
- fn<TFnArgs extends any[], TFnOutput>(this: FsCache<FsCache.Options<TFnArgs>>, fn: (...args: TFnArgs) => Promise<TFnOutput> | TFnOutput): (...args: TFnArgs) => Promise<TFnOutput>;
44
+ fn<TFnArgs extends any[], TFnOutput>(this: FsCache<FsCache.Options<TFnArgs>>, fn: (...args: TFnArgs) => TFnOutput): (...args: TFnArgs) => Promise<Awaited<TFnOutput>>;
39
45
  }
40
46
  declare const fsCache: typeof FsCache.cwd;
41
47
 
@@ -31,7 +31,14 @@ function isPrimitive(obj) {
31
31
  var FsCache = class _FsCache {
32
32
  _config;
33
33
  constructor(config) {
34
- this._config = config;
34
+ this._config = {
35
+ ...config
36
+ };
37
+ }
38
+ // eslint-disable-next-line @typescript-eslint/require-await
39
+ async serializeInput(...args) {
40
+ if (this._config.signatureFn) return this._config.signatureFn(...args);
41
+ return args;
35
42
  }
36
43
  static cwd(cwd) {
37
44
  if (cwd instanceof FsDir) return new _FsCache({ cwd: cwd.path });
@@ -40,6 +47,9 @@ var FsCache = class _FsCache {
40
47
  key(key) {
41
48
  return new _FsCache({ ...this._config, key });
42
49
  }
50
+ signatureFn(signatureFn) {
51
+ return new _FsCache({ ...this._config, signatureFn });
52
+ }
43
53
  pretty(pretty) {
44
54
  return new _FsCache({ ...this._config, pretty });
45
55
  }
@@ -59,8 +69,8 @@ var FsCache = class _FsCache {
59
69
  }
60
70
  const value = await file.read.json();
61
71
  await file.write.json({
62
- isLocked,
63
- ...value
72
+ ...value,
73
+ isLocked
64
74
  });
65
75
  }
66
76
  async get(args) {
@@ -69,7 +79,7 @@ var FsCache = class _FsCache {
69
79
  if (!await cacheDir.file(relativePath).exists()) return ["miss", null];
70
80
  const value = await cacheDir.file(relativePath).read.json();
71
81
  if (value === null) return ["miss", null];
72
- if (value.isLocked || deepEqual(value.input, args))
82
+ if (value.isLocked || deepEqual(value.input, await this.serializeInput(...args)))
73
83
  return ["hit", value.output];
74
84
  return ["miss", null];
75
85
  }
@@ -83,7 +93,10 @@ var FsCache = class _FsCache {
83
93
  const file = dir(this._config.cwd).file(relativePath);
84
94
  return file.write.text(
85
95
  JSON.stringify(
86
- { input: args, output: value },
96
+ {
97
+ input: await this.serializeInput(...args),
98
+ output: value
99
+ },
87
100
  null,
88
101
  this._config.pretty ? 2 : void 0
89
102
  )
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/fs-cache.lib.ts","../src/deepEqual.lib.ts"],"sourcesContent":["import { type OneToN } from \"../../shared/src/ts.utils.ts\";\nimport { dir, FsDir } from \"@synstack/fs\";\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 export type Key<TFnArgs extends any[] = any[]> = OneToN<KeyFn<TFnArgs>>;\n\n interface Options<TFnArgs extends any[] = any[]> {\n cwd: string;\n key: Key<TFnArgs>;\n pretty?: boolean;\n }\n\n namespace Options {\n export type Partial = $Partial<Options>;\n }\n}\n\nexport class FsCache<TConfig extends FsCache.Options.Partial> {\n private readonly _config: TConfig;\n\n private constructor(config: TConfig) {\n this._config = config;\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 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 isLocked,\n ...value,\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 (value.isLocked || deepEqual(value.input, args))\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 { input: args, output: value } 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) => Promise<TFnOutput> | TFnOutput,\n ): (...args: TFnArgs) => Promise<TFnOutput> {\n return async (...args: TFnArgs): Promise<TFnOutput> => {\n const [status, value] = await this.get<TFnArgs, TFnOutput>(args);\n if (status === \"hit\") return value;\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","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"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/fs-cache.lib.ts","../src/deepEqual.lib.ts"],"sourcesContent":["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<TConfig extends FsCache.Options.Partial> {\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","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"],"mappings":";AAAA,SAAS,KAAK,aAAa;;;ACApB,SAAS,UAAU,MAAW,MAAW;AAC9C,MAAI,SAAS,MAAM;AACjB,WAAO;AAAA,EACT;AAEA,MAAI,YAAY,IAAI,KAAK,YAAY,IAAI,GAAG;AAC1C,WAAO,SAAS;AAAA,EAClB;AAEA,MAAI,QAAQ,QAAQ,QAAQ,MAAM;AAChC,WAAO;AAAA,EACT;AAEA,QAAM,QAAQ,OAAO,KAAK,IAAU,EAAE,OAAO,CAAC,MAAM,KAAK,CAAC,MAAM,MAAS;AACzE,QAAM,QAAQ,OAAO,KAAK,IAAU,EAAE,OAAO,CAAC,MAAM,KAAK,CAAC,MAAM,MAAS;AAEzE,MAAI,MAAM,WAAW,MAAM,OAAQ,QAAO;AAE1C,aAAW,OAAO,OAAO;AACvB,QAAI,CAAC,MAAM,SAAS,GAAG,EAAG,QAAO;AACjC,QAAI,CAAC,UAAU,KAAK,GAAG,GAAG,KAAK,GAAG,CAAC,GAAG;AACpC,aAAO;AAAA,IACT;AAAA,EACF;AAEA,SAAO;AACT;AAGA,SAAS,YAAY,KAAU;AAC7B,SAAO,QAAQ,OAAO,GAAG;AAC3B;;;ADIO,IAAM,UAAN,MAAM,SAAiD;AAAA,EAC3C;AAAA,EAET,YAAY,QAAiB;AACnC,SAAK,UAAU;AAAA,MACb,GAAG;AAAA,IACL;AAAA,EACF;AAAA;AAAA,EAGA,MAAc,kBAAyC,MAAe;AACpE,QAAI,KAAK,QAAQ,YAAa,QAAO,KAAK,QAAQ,YAAY,GAAG,IAAI;AACrE,WAAO;AAAA,EACT;AAAA,EAEA,OAAc,IAAgB,KAAqB;AACjD,QAAI,eAAe,MAAO,QAAO,IAAI,SAAQ,EAAE,KAAK,IAAI,KAAK,CAAC;AAC9D,WAAO,IAAI,SAAQ,EAAE,IAAI,CAAC;AAAA,EAC5B;AAAA,EAEO,IACL,KACA;AACA,WAAO,IAAI,SAAQ,EAAE,GAAG,KAAK,SAAS,IAAI,CAAC;AAAA,EAC7C;AAAA,EAEO,YACL,aACA;AACA,WAAO,IAAI,SAAQ,EAAE,GAAG,KAAK,SAAS,YAAY,CAAC;AAAA,EACrD;AAAA,EAEO,OAAO,QAAiB;AAC7B,WAAO,IAAI,SAAQ,EAAE,GAAG,KAAK,SAAS,OAAO,CAAC;AAAA,EAChD;AAAA,EAEA,OAAe,kBAAkB,KAAkB,MAAa;AAE9D,WAAO,KAAK,IAAI,IAAI,CAAC,MAAO,aAAa,WAAW,EAAE,GAAG,IAAI,IAAI,CAAE,EAAE,KAAK,GAAG,CAAC;AAAA,EAChF;AAAA,EAEA,MAAa,KAEX,UACA,MACA;AACA,UAAM,eAAe,SAAQ,kBAAkB,KAAK,QAAQ,KAAK,IAAI;AACrE,UAAM,WAAW,IAAI,KAAK,QAAQ,GAAG;AACrC,UAAM,OAAO,SAAS,KAAK,YAAY;AAEvC,QAAI,CAAE,MAAM,KAAK,OAAO,GAAI;AAC1B,cAAQ;AAAA,QACN;AAAA,QACA,KAAK;AAAA,MACP;AACA;AAAA,IACF;AAEA,UAAM,QAAQ,MAAM,KAAK,KAAK,KAAiB;AAC/C,UAAM,KAAK,MAAM,KAAK;AAAA,MACpB,GAAG;AAAA,MACH;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEA,MAAa,IAEX,MAC2C;AAC3C,UAAM,eAAe,SAAQ,kBAAkB,KAAK,QAAQ,KAAK,IAAI;AACrE,UAAM,WAAW,IAAI,KAAK,QAAQ,GAAG;AAErC,QAAI,CAAE,MAAM,SAAS,KAAK,YAAY,EAAE,OAAO,EAAI,QAAO,CAAC,QAAQ,IAAI;AAEvE,UAAM,QAAQ,MAAM,SACjB,KAAK,YAAY,EACjB,KAAK,KAAyB;AAEjC,QAAI,UAAU,KAAM,QAAO,CAAC,QAAQ,IAAI;AAExC,QACE,MAAM,YACN,UAAU,MAAM,OAAO,MAAM,KAAK,eAAe,GAAG,IAAI,CAAC;AAEzD,aAAO,CAAC,OAAO,MAAM,MAAM;AAE7B,WAAO,CAAC,QAAQ,IAAI;AAAA,EACtB;AAAA,EAEA,MAAa,WAEX,MACA,cACA;AACA,UAAM,CAAC,MAAM,IAAI,MAAM,KAAK,IAAqB,IAAI;AACrD,QAAI,WAAW,MAAO,QAAO;AAC7B,WAAO,KAAK,IAAI,MAAM,YAAY;AAAA,EACpC;AAAA,EAEA,MAAa,IAEX,MACA,OACA;AACA,UAAM,eAAe,SAAQ,kBAAkB,KAAK,QAAQ,KAAK,IAAI;AACrE,UAAM,OAAO,IAAI,KAAK,QAAQ,GAAG,EAAE,KAAK,YAAY;AACpD,WAAO,KAAK,MAAM;AAAA,MAChB,KAAK;AAAA,QACH;AAAA,UACE,OAAO,MAAM,KAAK,eAAe,GAAG,IAAI;AAAA,UACxC,QAAQ;AAAA,QACV;AAAA,QACA;AAAA,QACA,KAAK,QAAQ,SAAS,IAAI;AAAA,MAC5B;AAAA,IACF;AAAA,EACF;AAAA,EAEO,GAEL,IACmD;AACnD,WAAO,UAAU,SAA+C;AAC9D,YAAM,CAAC,QAAQ,KAAK,IAAI,MAAM,KAAK,IAAwB,IAAI;AAC/D,UAAI,WAAW,MAAO,QAAO;AAE7B,YAAM,SAAS,MAAM,QAAQ,QAAQ,GAAG,GAAG,IAAI,CAAC;AAChD,YAAM,KAAK,IAAI,MAAM,MAAM;AAC3B,aAAO;AAAA,IACT;AAAA,EACF;AACF;AAEO,IAAM,UAAU,QAAQ;","names":[]}
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
- "version": "1.2.4",
7
+ "version": "1.3.0",
8
8
  "description": "Human-friendly file system caching",
9
9
  "keywords": [
10
10
  "cache",
@@ -43,10 +43,10 @@
43
43
  }
44
44
  },
45
45
  "dependencies": {
46
- "@synstack/fs": "1.3.4"
46
+ "@synstack/fs": "1.3.5"
47
47
  },
48
48
  "devDependencies": {
49
- "@types/node": "^22.9.3",
49
+ "@types/node": "^22.10.1",
50
50
  "tsup": "^8.3.5",
51
51
  "typescript": "^5.7.2"
52
52
  },
@@ -55,5 +55,5 @@
55
55
  "!src/**/*.test.ts",
56
56
  "dist/**/*"
57
57
  ],
58
- "gitHead": "47113f21d2bb9f2b8be606f1a1aa6f331018d53d"
58
+ "gitHead": "34bad542796c3d22efe0b4f7f4aabb05ec72aebd"
59
59
  }
@@ -1,5 +1,5 @@
1
- import { type OneToN } from "../../shared/src/ts.utils.ts";
2
1
  import { dir, FsDir } from "@synstack/fs";
2
+ import { type OneToN } from "../../shared/src/ts.utils.ts";
3
3
  import { deepEqual } from "./deepEqual.lib.ts";
4
4
 
5
5
  type $Partial<T> = Partial<T>;
@@ -14,11 +14,17 @@ declare namespace FsCache {
14
14
  export type KeyFn<TFnArgs extends any[] = any[]> =
15
15
  | string
16
16
  | ((...args: TFnArgs) => string);
17
+
17
18
  export type Key<TFnArgs extends any[] = any[]> = OneToN<KeyFn<TFnArgs>>;
18
19
 
20
+ export type SignatureFn<TFnArgs extends any[] = any[]> = (
21
+ ...args: TFnArgs
22
+ ) => any;
23
+
19
24
  interface Options<TFnArgs extends any[] = any[]> {
20
25
  cwd: string;
21
26
  key: Key<TFnArgs>;
27
+ signatureFn?: SignatureFn<TFnArgs>;
22
28
  pretty?: boolean;
23
29
  }
24
30
 
@@ -31,7 +37,15 @@ export class FsCache<TConfig extends FsCache.Options.Partial> {
31
37
  private readonly _config: TConfig;
32
38
 
33
39
  private constructor(config: TConfig) {
34
- this._config = config;
40
+ this._config = {
41
+ ...config,
42
+ } as TConfig & { signatureFn: FsCache.SignatureFn };
43
+ }
44
+
45
+ // eslint-disable-next-line @typescript-eslint/require-await
46
+ private async serializeInput<TFnArgs extends any[]>(...args: TFnArgs) {
47
+ if (this._config.signatureFn) return this._config.signatureFn(...args);
48
+ return args;
35
49
  }
36
50
 
37
51
  public static cwd(this: void, cwd: string | FsDir) {
@@ -45,6 +59,12 @@ export class FsCache<TConfig extends FsCache.Options.Partial> {
45
59
  return new FsCache({ ...this._config, key });
46
60
  }
47
61
 
62
+ public signatureFn<TFnArgs extends any[]>(
63
+ signatureFn: FsCache.SignatureFn<TFnArgs>,
64
+ ) {
65
+ return new FsCache({ ...this._config, signatureFn });
66
+ }
67
+
48
68
  public pretty(pretty: boolean) {
49
69
  return new FsCache({ ...this._config, pretty });
50
70
  }
@@ -73,8 +93,8 @@ export class FsCache<TConfig extends FsCache.Options.Partial> {
73
93
 
74
94
  const value = await file.read.json<CacheValue>();
75
95
  await file.write.json({
76
- isLocked,
77
96
  ...value,
97
+ isLocked,
78
98
  });
79
99
  }
80
100
 
@@ -93,7 +113,10 @@ export class FsCache<TConfig extends FsCache.Options.Partial> {
93
113
 
94
114
  if (value === null) return ["miss", null];
95
115
 
96
- if (value.isLocked || deepEqual(value.input, args))
116
+ if (
117
+ value.isLocked ||
118
+ deepEqual(value.input, await this.serializeInput(...args))
119
+ )
97
120
  return ["hit", value.output];
98
121
 
99
122
  return ["miss", null];
@@ -118,7 +141,10 @@ export class FsCache<TConfig extends FsCache.Options.Partial> {
118
141
  const file = dir(this._config.cwd).file(relativePath);
119
142
  return file.write.text(
120
143
  JSON.stringify(
121
- { input: args, output: value } satisfies CacheValue,
144
+ {
145
+ input: await this.serializeInput(...args),
146
+ output: value,
147
+ } satisfies CacheValue,
122
148
  null,
123
149
  this._config.pretty ? 2 : undefined,
124
150
  ),
@@ -127,11 +153,11 @@ export class FsCache<TConfig extends FsCache.Options.Partial> {
127
153
 
128
154
  public fn<TFnArgs extends any[], TFnOutput>(
129
155
  this: FsCache<FsCache.Options<TFnArgs>>,
130
- fn: (...args: TFnArgs) => Promise<TFnOutput> | TFnOutput,
131
- ): (...args: TFnArgs) => Promise<TFnOutput> {
132
- return async (...args: TFnArgs): Promise<TFnOutput> => {
156
+ fn: (...args: TFnArgs) => TFnOutput,
157
+ ): (...args: TFnArgs) => Promise<Awaited<TFnOutput>> {
158
+ return async (...args: TFnArgs): Promise<Awaited<TFnOutput>> => {
133
159
  const [status, value] = await this.get<TFnArgs, TFnOutput>(args);
134
- if (status === "hit") return value;
160
+ if (status === "hit") return value as Awaited<TFnOutput>;
135
161
 
136
162
  const output = await Promise.resolve(fn(...args));
137
163
  await this.set(args, output);