cachetta 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kevin Scott
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,431 @@
1
+ # Cachetta for JavaScript/TypeScript
2
+
3
+ File-based JSON caching for JavaScript and TypeScript. Part of the [Cachetta](../../README.md) project, which provides the same caching API in both JS/TS and Python -- learn it once, use it in either language.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add cachetta
9
+ ```
10
+
11
+ ## Features
12
+
13
+ - **Local File Storage**: Supports local files with automatic directory creation
14
+ - **JSON Serialization**: JSON-based caching (native JavaScript support)
15
+ - **Async Support**: Works with both synchronous and asynchronous functions
16
+ - **Automatic Expiration**: Cache expiration based on file modification time
17
+ - **In-Memory LRU**: Optional in-memory LRU layer for fast repeated access
18
+ - **Stale-While-Revalidate**: Serve stale data while refreshing in the background
19
+ - **Conditional Caching**: Cache only when a condition is met
20
+ - **Cache Inspection**: Check existence, age, and expiry state of cache entries
21
+ - **Auto Cache Keys**: Automatic unique paths based on function arguments
22
+ - **Flexible Paths**: Dynamic cache paths using functions
23
+ - **Error Handling**: Graceful handling of corrupt cache files
24
+ - **Logging**: Built-in logging for debugging
25
+
26
+ ## Usage
27
+
28
+ ### Basic Usage
29
+
30
+ Create a cache object:
31
+
32
+ ```javascript
33
+ import { Cachetta } from 'cachetta';
34
+
35
+ const cache = new Cachetta({
36
+ read: true, // allow reading from local caches
37
+ write: true, // allow writing to local caches
38
+ path: './cache.json', // specify path to cache file
39
+ duration: 24 * 60 * 60 * 1000, // specify length of cache in milliseconds (1 day)
40
+ });
41
+ ```
42
+
43
+ Read and write to a cache object:
44
+
45
+ ```javascript
46
+ import { readCache, writeCache } from 'cachetta';
47
+
48
+ async function getData() {
49
+ const cachedData = await readCache(cache);
50
+ if (cachedData) {
51
+ return cachedData;
52
+ } else {
53
+ const data = await fetchData(); // some long running process
54
+ await writeCache(cache, data);
55
+ return data;
56
+ }
57
+ }
58
+ ```
59
+
60
+ ### Specifying paths
61
+
62
+ You can specify a base path for your cache folder and then quickly specify cache paths within that folder:
63
+
64
+ ```javascript
65
+ import { readCache, writeCache, Cachetta } from 'cachetta';
66
+ import path from 'path';
67
+
68
+ const cache = new Cachetta({
69
+ path: './cache', // our base cache folder
70
+ });
71
+
72
+ async function getData() {
73
+ // specify your file path like below:
74
+ const cachePath = path.join(cache.path, 'my-data.json');
75
+ const cachedData = await readCache(cache.copy({ path: cachePath }));
76
+ // ...
77
+ }
78
+ ```
79
+
80
+ For modifying other attributes of a base `cache` object, use `copy`:
81
+
82
+ ```javascript
83
+ const cache = new Cachetta({
84
+ path: './cache', // our base cache folder
85
+ });
86
+
87
+ const newCache = cache.copy({
88
+ read: false,
89
+ write: false,
90
+ duration: 2 * 24 * 60 * 60 * 1000, // 2 days
91
+ });
92
+ ```
93
+
94
+ **Note**: The `copy` method is the intended public API for creating variations of cache configurations. It creates a new `Cachetta` instance with the specified overrides while preserving the original configuration.
95
+
96
+ ### Decorators
97
+
98
+ You can use `Cachetta` as a decorator (requires experimental decorators):
99
+
100
+ ```javascript
101
+ import { Cachetta } from 'cachetta';
102
+
103
+ class DataService {
104
+ @Cachetta({ path: '/my-cache.json' })
105
+ async getData() {
106
+ const parts = [];
107
+ for (let i = 0; i < 10; i++) {
108
+ parts.push(i);
109
+ await new Promise(resolve => setTimeout(resolve, 1000));
110
+ }
111
+ return parts;
112
+ }
113
+ }
114
+ ```
115
+
116
+ You can also use a specific cache object as a decorator:
117
+
118
+ ```javascript
119
+ import { Cachetta } from 'cachetta';
120
+
121
+ const cache = new Cachetta({ path: '/my-cache.json' });
122
+
123
+ class DataService {
124
+ @cache
125
+ async getData() {
126
+ const parts = [];
127
+ for (let i = 0; i < 10; i++) {
128
+ parts.push(i);
129
+ await new Promise(resolve => setTimeout(resolve, 1000));
130
+ }
131
+ return parts;
132
+ }
133
+ }
134
+ ```
135
+
136
+ Or with arguments:
137
+
138
+ ```javascript
139
+ import { Cachetta } from 'cachetta';
140
+
141
+ const cache = new Cachetta({ path: '/my-cache.json' });
142
+
143
+ class DataService {
144
+ @cache({ duration: 1000 })
145
+ async getData() {
146
+ const parts = [];
147
+ for (let i = 0; i < 10; i++) {
148
+ parts.push(i);
149
+ await new Promise(resolve => setTimeout(resolve, 1000));
150
+ }
151
+ return parts;
152
+ }
153
+ }
154
+ ```
155
+
156
+ **Important Note**: Decorated functions always return Promises, even if the original function is synchronous. This is because the caching mechanism involves async file operations. Always use `await` when calling decorated functions:
157
+
158
+ ```javascript
159
+ const cache = new Cachetta({ path: './cache.json' });
160
+
161
+ // Even though this is a sync function, the decorated version returns a Promise
162
+ const cachedFunction = cache.call(() => {
163
+ return "Hello World";
164
+ });
165
+
166
+ // Must await the result
167
+ const result = await cachedFunction();
168
+ console.log(result); // "Hello World"
169
+ ```
170
+
171
+ ### Async Function Support
172
+
173
+ Cachetta works seamlessly with async functions:
174
+
175
+ ```javascript
176
+ import { Cachetta } from 'cachetta';
177
+
178
+ @Cachetta({ path: './async-cache.json' })
179
+ async function getAsyncData() {
180
+ // Simulate async API call
181
+ await new Promise(resolve => setTimeout(resolve, 2000));
182
+ return { status: "success", data: [1, 2, 3] };
183
+ }
184
+
185
+ // Usage
186
+ async function main() {
187
+ const result = await getAsyncData();
188
+ console.log(result);
189
+ }
190
+ ```
191
+
192
+ ### Auto Cache Keys
193
+
194
+ When a wrapped function receives arguments, Cachetta automatically generates unique cache paths by hashing the arguments:
195
+
196
+ ```javascript
197
+ const cache = new Cachetta({ path: './cache/users.json' });
198
+
199
+ const getUser = cache((userId) => fetchUser(userId));
200
+
201
+ await getUser(1); // cached at ./cache/users-<hash1>.json
202
+ await getUser(2); // cached at ./cache/users-<hash2>.json
203
+ ```
204
+
205
+ ### In-Memory LRU
206
+
207
+ Add an in-memory LRU layer that is checked before hitting disk:
208
+
209
+ ```javascript
210
+ const cache = new Cachetta({
211
+ path: './cache.json',
212
+ lruSize: 100, // keep up to 100 entries in memory
213
+ });
214
+ ```
215
+
216
+ LRU entries respect the same `duration` as disk entries and use lazy expiration (evicted on access, not via background timers).
217
+
218
+ ### Conditional Caching
219
+
220
+ Cache results only when a condition function returns `true`:
221
+
222
+ ```javascript
223
+ const cache = new Cachetta({
224
+ path: './cache.json',
225
+ condition: (result) => result !== null, // don't cache null
226
+ });
227
+ ```
228
+
229
+ ### Stale-While-Revalidate
230
+
231
+ Return expired (stale) data immediately while refreshing the cache in the background:
232
+
233
+ ```javascript
234
+ const cache = new Cachetta({
235
+ path: './cache.json',
236
+ duration: 60 * 60 * 1000, // 1 hour
237
+ staleDuration: 30 * 60 * 1000, // serve stale data up to 30min past expiry
238
+ });
239
+ ```
240
+
241
+ ### Cache Invalidation
242
+
243
+ Delete cache files on disk:
244
+
245
+ ```javascript
246
+ const cache = new Cachetta({ path: './cache.json' });
247
+
248
+ await cache.invalidate(); // or cache.clear()
249
+
250
+ // With arguments (when using path functions)
251
+ await cache.invalidate('userId');
252
+ ```
253
+
254
+ ### Cache Inspection
255
+
256
+ Query cache state without reading the cached data:
257
+
258
+ ```javascript
259
+ const cache = new Cachetta({ path: './cache.json' });
260
+
261
+ await cache.exists(); // true if the cache file exists
262
+ await cache.age(); // age in milliseconds, or null
263
+ await cache.info(); // { exists: true, age: 1234, expired: false, stale: false, path: "..." }
264
+ ```
265
+
266
+ ### Dynamic Cache Paths
267
+
268
+ You can specify a function for defining the path as well:
269
+
270
+ ```javascript
271
+ function getCachePath(n) {
272
+ return `./cache/${n}.json`;
273
+ }
274
+
275
+ @Cachetta({ path: getCachePath })
276
+ async function foo(n) {
277
+ const parts = [];
278
+ for (let i = 0; i < n; i++) {
279
+ parts.push(i);
280
+ await new Promise(resolve => setTimeout(resolve, 1000));
281
+ }
282
+ return parts;
283
+ }
284
+ ```
285
+
286
+ Or, using a pre-existing cache object:
287
+
288
+ ```javascript
289
+ const cache = new Cachetta({ path: './cache' });
290
+
291
+ function getCachePath(n) {
292
+ return path.join(cache.path, `${n}.json`);
293
+ }
294
+
295
+ @cache.copy({ path: getCachePath })
296
+ async function foo(n) {
297
+ const parts = [];
298
+ for (let i = 0; i < n; i++) {
299
+ parts.push(i);
300
+ await new Promise(resolve => setTimeout(resolve, 1000));
301
+ }
302
+ return parts;
303
+ }
304
+ ```
305
+
306
+ ### Function Wrapper (Alternative to Decorators)
307
+
308
+ If you're not using decorators, you can wrap functions manually:
309
+
310
+ ```javascript
311
+ import { Cachetta } from 'cachetta';
312
+
313
+ const cache = new Cachetta({ path: './my-cache.json' });
314
+
315
+ async function getData() {
316
+ const parts = [];
317
+ for (let i = 0; i < 10; i++) {
318
+ parts.push(i);
319
+ await new Promise(resolve => setTimeout(resolve, 1000));
320
+ }
321
+ return parts;
322
+ }
323
+
324
+ // Wrap the function with caching
325
+ const cachedGetData = cache(getData);
326
+
327
+ // Usage - always await the result, even for sync functions
328
+ const result = await cachedGetData();
329
+ ```
330
+
331
+ You can also pass configuration when wrapping:
332
+
333
+ ```javascript
334
+ const cache = new Cachetta({ path: './cache' });
335
+
336
+ function getData(id) {
337
+ return { id, data: 'some data' };
338
+ }
339
+
340
+ // Wrap with specific configuration
341
+ const cachedGetData = cache(getData, {
342
+ path: (id) => `./cache/data-${id}.json`,
343
+ duration: 5000
344
+ });
345
+
346
+ // Usage
347
+ const result = await cachedGetData(123);
348
+ ```
349
+
350
+ **Note**: Wrapped functions always return Promises, even if the original function is synchronous, due to the async nature of file operations in the caching mechanism.
351
+
352
+ ### Error Handling
353
+
354
+ Cachetta gracefully handles corrupt cache files:
355
+
356
+ ```javascript
357
+ import { readCache, writeCache, Cachetta } from 'cachetta';
358
+
359
+ const cache = new Cachetta({ path: './corrupt-cache.json' });
360
+
361
+ // If the cache file is corrupt, readCache will return null
362
+ async function getData() {
363
+ const data = await readCache(cache);
364
+ if (data === null) {
365
+ // Cache is missing or corrupt, regenerate data
366
+ const freshData = await fetchFreshData();
367
+ await writeCache(cache, freshData);
368
+ return freshData;
369
+ }
370
+ return data;
371
+ }
372
+ ```
373
+
374
+ ### Logging
375
+
376
+ Cachetta provides detailed logging for debugging. You can configure the log level:
377
+
378
+ ```javascript
379
+ import { setLogLevel } from 'cachetta';
380
+
381
+ // Enable debug logging
382
+ setLogLevel('debug');
383
+
384
+ // Available levels: 'error', 'warn', 'info', 'debug'
385
+ // Default is 'warn'
386
+ ```
387
+
388
+ Cachetta uses a simple logger that outputs to `console` by default, but you can also configure it to use your preferred logging library:
389
+
390
+ ```javascript
391
+ import { setLogger } from 'cachetta';
392
+
393
+ // Use a custom logger (e.g., winston, pino)
394
+ setLogger({
395
+ debug: (msg) => console.debug(`[Cachetta] ${msg}`),
396
+ info: (msg) => console.info(`[Cachetta] ${msg}`),
397
+ warn: (msg) => console.warn(`[Cachetta] ${msg}`),
398
+ error: (msg) => console.error(`[Cachetta] ${msg}`),
399
+ });
400
+ ```
401
+
402
+ ### TypeScript Support
403
+
404
+ Cachetta includes full TypeScript support:
405
+
406
+ ```typescript
407
+ import { Cachetta } from 'cachetta';
408
+
409
+ interface UserData {
410
+ id: number;
411
+ name: string;
412
+ email: string;
413
+ }
414
+
415
+ const cache = new Cachetta<UserData>({
416
+ path: './user-cache.json',
417
+ });
418
+
419
+ @cache
420
+ async function fetchUserData(id: number): Promise<UserData> {
421
+ // API call implementation
422
+ return { id, name: 'John Doe', email: 'john@example.com' };
423
+ }
424
+ ```
425
+
426
+ ### Default Configuration
427
+
428
+ - **Default duration**: 7 days (7 * 24 * 60 * 60 * 1000 milliseconds)
429
+ - **Default read**: `true`
430
+ - **Default write**: `true`
431
+ - **Supported format**: JSON only
package/dist/index.js ADDED
@@ -0,0 +1,368 @@
1
+ import { promises as c } from "fs";
2
+ import { normalize as I, resolve as D, dirname as C, join as $ } from "path";
3
+ import { randomBytes as U, createHash as N } from "crypto";
4
+ import { inspect as O } from "util";
5
+ const R = (e) => typeof e == "object" && e !== null, F = (e) => R(e) && ("path" in e || "write" in e || "read" in e || "duration" in e || "lruSize" in e || "condition" in e || "staleDuration" in e), j = (e) => typeof e == "function" && "__cacheBuddy__" in e && e.__cacheBuddy__ === !0, _ = Symbol("LRU_MISS");
6
+ class m extends Error {
7
+ constructor(t) {
8
+ super(t), this.name = "CachettaError";
9
+ }
10
+ }
11
+ class M extends m {
12
+ constructor(t) {
13
+ super(`Invalid cache path (path traversal detected): ${t}`), this.name = "InvalidPathError";
14
+ }
15
+ }
16
+ class E extends m {
17
+ constructor(t) {
18
+ super(`Unsupported cache format: ${t}`), this.name = "UnsupportedFormatError";
19
+ }
20
+ }
21
+ function v(e) {
22
+ var i;
23
+ const n = (i = String(e).split("/").pop()) == null ? void 0 : i.split(".");
24
+ if (!n || n.length === 1 || n[n.length - 1] === "")
25
+ throw new m(`Missing file extension: ${e}`);
26
+ return n[n.length - 1];
27
+ }
28
+ async function d(e) {
29
+ try {
30
+ return (await c.stat(e)).mtime.getTime();
31
+ } catch (t) {
32
+ if (t.code === "ENOENT")
33
+ return null;
34
+ throw t;
35
+ }
36
+ }
37
+ function A(e, t, n) {
38
+ if (e > t)
39
+ throw new Error(
40
+ `Invalid arguments, cache time ${e} cannot be greater than now ${t}`
41
+ );
42
+ return t - e >= n;
43
+ }
44
+ let P = "warn";
45
+ const x = ["error", "warn", "info", "debug"], G = (e) => {
46
+ const t = x.indexOf(P);
47
+ return x.indexOf(e) <= t;
48
+ }, J = (e) => {
49
+ switch (e) {
50
+ case "debug":
51
+ return console.debug;
52
+ case "info":
53
+ return console.info;
54
+ case "warn":
55
+ return console.warn;
56
+ case "error":
57
+ return console.error;
58
+ }
59
+ }, p = (e) => (...t) => {
60
+ if (G(e)) {
61
+ const n = J(e);
62
+ n && n("[Cachetta]", ...t);
63
+ }
64
+ };
65
+ let a = {
66
+ debug: p("debug"),
67
+ info: p("info"),
68
+ warn: p("warn"),
69
+ error: p("error")
70
+ };
71
+ function Z(e) {
72
+ P = e;
73
+ }
74
+ function k(e) {
75
+ a = e;
76
+ }
77
+ async function T({ duration: e, read: t }, n) {
78
+ const i = e ?? 6048e5, r = await d(n);
79
+ if (r === null)
80
+ return a.debug(`Cache time is null for ${n}`), !1;
81
+ if (i <= 0)
82
+ return a.debug(`Cache length is ${i}, considering expired for ${n}`), !1;
83
+ const s = Date.now();
84
+ return r > s ? (a.debug(`Cache time ${r} is ahead of now ${s}, treating as valid cache for ${n}`), t ?? !0) : A(r, s, i) ? (a.debug(
85
+ `Cache is expired (${r}, expected ${r + i}) for ${n}`
86
+ ), !1) : (a.debug(
87
+ `Cache is not expired (${r}, expected ${r + i}) for ${n}`
88
+ ), t ?? !0);
89
+ }
90
+ function h(e) {
91
+ const t = I(e);
92
+ if (t.split(/[/\\]/).includes(".."))
93
+ throw new M(e);
94
+ return D(t);
95
+ }
96
+ const K = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
97
+ async function z(e) {
98
+ const t = v(e);
99
+ if (t !== "json")
100
+ throw new E(t);
101
+ try {
102
+ const n = await c.readFile(e, "utf8");
103
+ return JSON.parse(n, (i, r) => {
104
+ if (!K.has(i))
105
+ return r;
106
+ });
107
+ } catch (n) {
108
+ return n.code === "ENOENT" ? null : n instanceof SyntaxError ? (a.error(`Corrupt JSON: ${n}`), null) : (a.error(`Read error: ${n}`), null);
109
+ }
110
+ }
111
+ async function W(e, ...t) {
112
+ if (!j(e))
113
+ throw new m(`Invalid value provided, you must provide an instance of Cachetta: ${e}`);
114
+ const n = e._getPath(...t);
115
+ h(n);
116
+ const i = e._lruGet(n);
117
+ if (i !== _)
118
+ return a.debug(`LRU cache hit for ${n}`), i;
119
+ if (await T(e, n)) {
120
+ a.debug(`Using cache at ${n}`);
121
+ const r = await z(n);
122
+ return r !== null && (a.debug(`Used cache at ${n}`), e._lruSet(n, r)), r;
123
+ } else
124
+ return a.debug("cache.read is false, skipping cache"), null;
125
+ }
126
+ async function H(e, ...t) {
127
+ if (!e.staleDuration || !e.read) return null;
128
+ const n = e._getPath(...t);
129
+ h(n);
130
+ const i = await d(n);
131
+ if (i === null) return null;
132
+ const r = Date.now() - i, s = r >= e.duration, o = r < e.duration + e.staleDuration;
133
+ return s && o ? (a.debug(`Returning stale cache for ${n} (age: ${r}ms)`), z(n)) : null;
134
+ }
135
+ const b = /* @__PURE__ */ new Set();
136
+ async function y(e, t, ...n) {
137
+ if (!e || !e.write)
138
+ return;
139
+ const i = e._getPath(...n);
140
+ h(i);
141
+ const r = v(i), s = D(C(i));
142
+ if (b.has(s) || (await c.mkdir(s, { recursive: !0 }), b.add(s)), r === "json") {
143
+ const o = JSON.stringify(t), u = $(s, `.cachetta-${U(8).toString("hex")}.tmp`);
144
+ try {
145
+ await c.writeFile(u, o, "utf8"), await c.rename(u, i), e._lruSet(i, t);
146
+ } catch (l) {
147
+ try {
148
+ await c.unlink(u);
149
+ } catch {
150
+ }
151
+ throw l;
152
+ }
153
+ } else
154
+ throw new E(r);
155
+ }
156
+ const w = /* @__PURE__ */ new Map(), S = /* @__PURE__ */ new Set(), g = (e, t) => {
157
+ async function n(...i) {
158
+ const r = await W(e, ...i);
159
+ if (r != null)
160
+ return r;
161
+ const s = e._getPath(...i);
162
+ if (e.staleDuration) {
163
+ const l = await H(e, ...i);
164
+ if (l != null)
165
+ return !S.has(s) && !w.has(s) && (S.add(s), (async () => {
166
+ try {
167
+ const f = await t.apply(this, i);
168
+ (!e.condition || e.condition(f)) && await y(e, f, ...i);
169
+ } catch (f) {
170
+ a.error(`Background revalidation failed for ${s}: ${f}`);
171
+ } finally {
172
+ S.delete(s);
173
+ }
174
+ })()), l;
175
+ }
176
+ const o = w.get(s);
177
+ if (o)
178
+ return o;
179
+ const u = (async () => {
180
+ const l = await t.apply(this, i);
181
+ return (!e.condition || e.condition(l)) && await y(e, l, ...i), l;
182
+ })();
183
+ w.set(s, u);
184
+ try {
185
+ return await u;
186
+ } finally {
187
+ w.delete(s);
188
+ }
189
+ }
190
+ return n;
191
+ }, Y = 10080 * 60 * 1e3;
192
+ class L extends Function {
193
+ constructor(t) {
194
+ super(), this.__cacheBuddy__ = !0, this.path = t.path, this.write = t.write ?? !0, this.read = t.read ?? !0, this.duration = t.duration ?? Y, this.lruSize = t.lruSize, this.condition = t.condition, this.staleDuration = t.staleDuration, this._lru = this.lruSize ? /* @__PURE__ */ new Map() : void 0;
195
+ const n = this.call.bind(this), i = Object.assign(
196
+ n,
197
+ this,
198
+ {
199
+ copy: this.copy.bind(this),
200
+ wrap: this.wrap.bind(this),
201
+ invalidate: this.invalidate.bind(this),
202
+ clear: this.invalidate.bind(this),
203
+ // alias
204
+ exists: this.exists.bind(this),
205
+ age: this.age.bind(this),
206
+ info: this.info.bind(this),
207
+ _getPath: this._getPath.bind(this),
208
+ _lruGet: this._lruGet.bind(this),
209
+ _lruSet: this._lruSet.bind(this),
210
+ __cacheBuddy__: !0,
211
+ _lru: this._lru,
212
+ lruSize: this.lruSize,
213
+ condition: this.condition,
214
+ staleDuration: this.staleDuration
215
+ }
216
+ );
217
+ return i[O.custom] = () => `Cachetta { path: '${this.path}', write: ${this.write}, read: ${this.read}, duration: ${this.duration} }`, i;
218
+ }
219
+ /**
220
+ * Creates a copy of this Cachetta instance with overridden configuration.
221
+ * Useful for creating variations of a base cache configuration.
222
+ *
223
+ * @param kwargs - Partial configuration to override
224
+ * @returns A new Cachetta instance with the specified overrides
225
+ */
226
+ copy(t) {
227
+ return new L({
228
+ path: t.path ?? this.path,
229
+ write: t.write ?? this.write,
230
+ read: t.read ?? this.read,
231
+ duration: t.duration ?? this.duration,
232
+ lruSize: t.lruSize ?? this.lruSize,
233
+ condition: t.condition ?? this.condition,
234
+ staleDuration: t.staleDuration ?? this.staleDuration
235
+ });
236
+ }
237
+ /**
238
+ * Wraps a function with caching behavior. Alias for calling the cache instance directly.
239
+ *
240
+ * @param fn - The function to wrap
241
+ * @returns A cached version of the function
242
+ */
243
+ wrap(t) {
244
+ return g(this, t);
245
+ }
246
+ /**
247
+ * Deletes the cache file on disk and clears LRU entries for this path.
248
+ * No-op if the cache file does not exist.
249
+ *
250
+ * @param args - Arguments to resolve the cache path (when using a path function)
251
+ */
252
+ async invalidate(...t) {
253
+ const n = this._getPath(...t);
254
+ h(n), this._lru && this._lru.delete(n);
255
+ try {
256
+ await c.unlink(n);
257
+ } catch (i) {
258
+ if (i.code !== "ENOENT")
259
+ throw i;
260
+ }
261
+ }
262
+ /**
263
+ * Checks whether the cache file exists on disk.
264
+ *
265
+ * @param args - Arguments to resolve the cache path (when using a path function)
266
+ * @returns true if the cache file exists
267
+ */
268
+ async exists(...t) {
269
+ const n = this._getPath(...t);
270
+ return h(n), await d(n) !== null;
271
+ }
272
+ /**
273
+ * Returns the age of the cache file in milliseconds, or null if it does not exist.
274
+ *
275
+ * @param args - Arguments to resolve the cache path (when using a path function)
276
+ * @returns Age in ms, or null
277
+ */
278
+ async age(...t) {
279
+ const n = this._getPath(...t);
280
+ h(n);
281
+ const i = await d(n);
282
+ return i === null ? null : Date.now() - i;
283
+ }
284
+ /**
285
+ * Returns detailed information about the cache state.
286
+ *
287
+ * @param args - Arguments to resolve the cache path (when using a path function)
288
+ * @returns CacheInfo with exists, age, expired, stale, and path fields
289
+ */
290
+ async info(...t) {
291
+ const n = this._getPath(...t);
292
+ h(n);
293
+ const i = await d(n);
294
+ if (i === null)
295
+ return { exists: !1, age: null, expired: !1, stale: !1, path: n };
296
+ const r = Date.now() - i, s = r >= this.duration, o = s && this.staleDuration != null && r < this.duration + this.staleDuration;
297
+ return { exists: !0, age: r, expired: s, stale: o, path: n };
298
+ }
299
+ /**
300
+ * Internal method to resolve the cache path.
301
+ * When path is a string and arguments are provided, auto-generates a unique
302
+ * cache path by hashing the arguments.
303
+ * @internal
304
+ */
305
+ _getPath(...t) {
306
+ if (typeof this.path == "string") {
307
+ if (t.length === 0)
308
+ return this.path;
309
+ const n = N("sha256").update(JSON.stringify(t)).digest("hex").slice(0, 16), i = C(this.path), r = this.path.split("/").pop(), s = r.lastIndexOf(".");
310
+ if (s === -1)
311
+ return $(i, `${r}-${n}`);
312
+ const o = r.slice(0, s), u = r.slice(s);
313
+ return $(i, `${o}-${n}${u}`);
314
+ }
315
+ return this.path(...t);
316
+ }
317
+ /**
318
+ * Get a value from the in-memory LRU cache.
319
+ * Returns undefined if LRU is disabled, key not found, or entry is expired.
320
+ * @internal
321
+ */
322
+ _lruGet(t) {
323
+ if (!this._lru) return _;
324
+ const n = this._lru.get(t);
325
+ return n ? Date.now() - n.timestamp > this.duration ? (this._lru.delete(t), _) : (this._lru.delete(t), this._lru.set(t, n), n.value) : _;
326
+ }
327
+ /**
328
+ * Set a value in the in-memory LRU cache.
329
+ * No-op if LRU is disabled.
330
+ * @internal
331
+ */
332
+ _lruSet(t, n) {
333
+ if (!(!this._lru || !this.lruSize)) {
334
+ if (this._lru.size >= this.lruSize && !this._lru.has(t)) {
335
+ const i = this._lru.keys().next().value;
336
+ i !== void 0 && this._lru.delete(i);
337
+ }
338
+ this._lru.set(t, { value: n, timestamp: Date.now() });
339
+ }
340
+ }
341
+ // Implementation signature
342
+ call(t, n, i) {
343
+ if (F(t)) {
344
+ const s = t;
345
+ return this.copy(s);
346
+ }
347
+ if (i) {
348
+ const s = i.value;
349
+ return i.value = g(this, s), i;
350
+ }
351
+ const r = t;
352
+ if (n) {
353
+ const s = n, o = this.copy(s);
354
+ return g(o, r);
355
+ }
356
+ return g(this, r);
357
+ }
358
+ }
359
+ export {
360
+ L as Cachetta,
361
+ m as CachettaError,
362
+ M as InvalidPathError,
363
+ E as UnsupportedFormatError,
364
+ W as readCache,
365
+ Z as setLogLevel,
366
+ k as setLogger,
367
+ y as writeCache
368
+ };
@@ -0,0 +1,87 @@
1
+ import { CacheConfig, CacheInfo, CachableFunction, PathFn } from './types.js';
2
+ import { LRU_MISS } from './constants.js';
3
+ interface LruEntry {
4
+ value: unknown;
5
+ timestamp: number;
6
+ }
7
+ export declare class Cachetta<Path extends string | PathFn<any> = string> extends Function {
8
+ protected __cacheBuddy__: boolean;
9
+ path: Path;
10
+ write: boolean;
11
+ read: boolean;
12
+ duration: number;
13
+ lruSize: number | undefined;
14
+ condition: ((result: unknown) => boolean) | undefined;
15
+ staleDuration: number | undefined;
16
+ /** Alias for {@link invalidate}. Deletes the cache file. */
17
+ clear: (...args: unknown[]) => Promise<void>;
18
+ /** @internal */
19
+ _lru: Map<string, LruEntry> | undefined;
20
+ constructor(config: CacheConfig<Path>);
21
+ /**
22
+ * Creates a copy of this Cachetta instance with overridden configuration.
23
+ * Useful for creating variations of a base cache configuration.
24
+ *
25
+ * @param kwargs - Partial configuration to override
26
+ * @returns A new Cachetta instance with the specified overrides
27
+ */
28
+ copy<NewPath extends string | PathFn<any> = string>(kwargs: Partial<CacheConfig<NewPath>>): Cachetta<NewPath>;
29
+ /**
30
+ * Wraps a function with caching behavior. Alias for calling the cache instance directly.
31
+ *
32
+ * @param fn - The function to wrap
33
+ * @returns A cached version of the function
34
+ */
35
+ wrap(fn: CachableFunction): CachableFunction;
36
+ /**
37
+ * Deletes the cache file on disk and clears LRU entries for this path.
38
+ * No-op if the cache file does not exist.
39
+ *
40
+ * @param args - Arguments to resolve the cache path (when using a path function)
41
+ */
42
+ invalidate(...args: unknown[]): Promise<void>;
43
+ /**
44
+ * Checks whether the cache file exists on disk.
45
+ *
46
+ * @param args - Arguments to resolve the cache path (when using a path function)
47
+ * @returns true if the cache file exists
48
+ */
49
+ exists(...args: unknown[]): Promise<boolean>;
50
+ /**
51
+ * Returns the age of the cache file in milliseconds, or null if it does not exist.
52
+ *
53
+ * @param args - Arguments to resolve the cache path (when using a path function)
54
+ * @returns Age in ms, or null
55
+ */
56
+ age(...args: unknown[]): Promise<number | null>;
57
+ /**
58
+ * Returns detailed information about the cache state.
59
+ *
60
+ * @param args - Arguments to resolve the cache path (when using a path function)
61
+ * @returns CacheInfo with exists, age, expired, stale, and path fields
62
+ */
63
+ info(...args: unknown[]): Promise<CacheInfo>;
64
+ /**
65
+ * Internal method to resolve the cache path.
66
+ * When path is a string and arguments are provided, auto-generates a unique
67
+ * cache path by hashing the arguments.
68
+ * @internal
69
+ */
70
+ _getPath(...args: unknown[]): string;
71
+ /**
72
+ * Get a value from the in-memory LRU cache.
73
+ * Returns undefined if LRU is disabled, key not found, or entry is expired.
74
+ * @internal
75
+ */
76
+ _lruGet(key: string): unknown | typeof LRU_MISS;
77
+ /**
78
+ * Set a value in the in-memory LRU cache.
79
+ * No-op if LRU is disabled.
80
+ * @internal
81
+ */
82
+ _lruSet(key: string, value: unknown): void;
83
+ call(target: CachableFunction, propertyKey: string, descriptor: PropertyDescriptor): PropertyDescriptor;
84
+ call(target: CachableFunction): CachableFunction;
85
+ call(config: Partial<CacheConfig>): Cachetta;
86
+ }
87
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ /** Sentinel value returned by _lruGet on cache miss, distinguishable from any cached value including undefined/null. */
2
+ export declare const LRU_MISS: unique symbol;
@@ -0,0 +1,9 @@
1
+ export declare class CachettaError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ export declare class InvalidPathError extends CachettaError {
5
+ constructor(cachePath: string);
6
+ }
7
+ export declare class UnsupportedFormatError extends CachettaError {
8
+ constructor(extension: string);
9
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ export { Cachetta } from './Cachetta.js';
2
+ export { writeCache } from './write-cache.js';
3
+ export { readCache } from './read-cache.js';
4
+ export { setLogLevel, setLogger } from './utils/logger.js';
5
+ export { CachettaError, InvalidPathError, UnsupportedFormatError } from './errors.js';
6
+ export type { CacheConfig, CacheInfo, PathFn, CachableFunction, Logger, LogLevel } from './types.js';
@@ -0,0 +1,8 @@
1
+ import { Cachetta } from './Cachetta.js';
2
+ export declare function readCache<T>(cacheBuddy: Cachetta<any>, ...args: unknown[]): Promise<T | null>;
3
+ /**
4
+ * Reads stale cache data: returns data only if the file exists and is within the
5
+ * staleDuration window (expired but not yet past duration + staleDuration).
6
+ * @internal
7
+ */
8
+ export declare function readStaleCache<T>(cacheBuddy: Cachetta<any>, ...args: unknown[]): Promise<T | null>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ import { Cachetta } from './Cachetta.js';
2
+ import { CacheConfig } from './types.js';
3
+ export declare const isCacheConfig: (value: unknown) => value is CacheConfig;
4
+ export declare const isPartialCacheConfig: (value: unknown) => value is Partial<CacheConfig>;
5
+ export declare const isCachetta: (value: unknown) => value is Cachetta<any>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,27 @@
1
+ export interface CacheConfig<Path extends string | PathFn<any> = string> {
2
+ path: Path;
3
+ write?: boolean;
4
+ read?: boolean;
5
+ duration?: number;
6
+ lruSize?: number;
7
+ /** Function that decides whether to cache a result. Return true to cache, false to skip. */
8
+ condition?: (result: unknown) => boolean;
9
+ /** Duration in ms after `duration` expires during which stale data is returned while a background refresh runs. */
10
+ staleDuration?: number;
11
+ }
12
+ export interface CacheInfo {
13
+ exists: boolean;
14
+ age: number | null;
15
+ expired: boolean;
16
+ stale: boolean;
17
+ path: string;
18
+ }
19
+ export type PathFn<T extends unknown[] = unknown[]> = (...args: T) => string;
20
+ export type CachableFunction = (...args: unknown[]) => unknown;
21
+ export interface Logger {
22
+ debug: (...messages: unknown[]) => void;
23
+ info: (...messages: unknown[]) => void;
24
+ warn: (...messages: unknown[]) => void;
25
+ error: (...messages: unknown[]) => void;
26
+ }
27
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
@@ -0,0 +1,3 @@
1
+ import { Cachetta } from '../Cachetta.js';
2
+ import { CachableFunction } from '../types.js';
3
+ export declare const cacheFn: (cache: Cachetta<any>, originalMethod: CachableFunction) => (this: ThisParameterType<typeof originalMethod>, ...args: Parameters<typeof originalMethod>) => Promise<unknown>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import { PathLike } from 'fs';
2
+ export declare function getExtension(cachePath: string | PathLike): string;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import { PathLike } from 'fs';
2
+ export declare function getLastUpdated(filePath: string | PathLike): Promise<number | null>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export declare function isCacheExpired(cacheTime: number, now: number, cacheLength: number): boolean;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ import { Logger, LogLevel } from '../types.js';
2
+ export declare let logger: Logger;
3
+ export declare function setLogLevel(level: LogLevel): void;
4
+ export declare function setLogger(customLogger: Logger): void;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import { CacheConfig } from '../types.js';
2
+ export declare function shouldUseReadCache({ duration, read }: Pick<CacheConfig, 'duration' | 'read'>, cachePath: string): Promise<boolean>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export declare function validateCachePath(cachePath: string): string;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import { Cachetta } from './Cachetta.js';
2
+ export declare function writeCache<T>(cache: Cachetta<any>, data: T, ...args: unknown[]): Promise<void>;
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "cachetta",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "license": "MIT",
18
+ "wireit": {
19
+ "test:unit": {
20
+ "command": "vitest run -c vitest.config.unit.ts"
21
+ },
22
+ "test:unit:watch": {
23
+ "command": "vitest watch -c vitest.config.unit.ts"
24
+ },
25
+ "test:integration": {
26
+ "command": "vitest run -c vitest.config.integration.ts",
27
+ "dependencies": [
28
+ "build"
29
+ ]
30
+ },
31
+ "test:integration:watch": {
32
+ "command": "vitest watch -c vitest.config.integration.ts",
33
+ "dependencies": [
34
+ "build"
35
+ ]
36
+ },
37
+ "build": {
38
+ "command": "vite build",
39
+ "dependencies": [],
40
+ "files": [
41
+ "./src/**/*.ts",
42
+ "./tsconfig.json",
43
+ "./package.json",
44
+ "./vite.config.ts"
45
+ ],
46
+ "output": [
47
+ "dist"
48
+ ]
49
+ }
50
+ },
51
+ "devDependencies": {
52
+ "@types/node": "^22.10.0",
53
+ "typescript": "^5.7.2",
54
+ "vite": "^6.0.0",
55
+ "vite-plugin-dts": "^4.5.4",
56
+ "vitest": "^2.1.6",
57
+ "wireit": "^0.14.9",
58
+ "cachetta": "0.1.0"
59
+ },
60
+ "dependencies": {},
61
+ "scripts": {
62
+ "test:unit": "wireit",
63
+ "test:unit:watch": "wireit",
64
+ "test:integration": "wireit",
65
+ "test:integration:watch": "wireit",
66
+ "build": "wireit"
67
+ }
68
+ }