@neezco/cache 0.2.1 → 0.4.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/CHANGELOG.md CHANGED
@@ -2,6 +2,26 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## 0.4.0 (2026-02-14)
6
+
7
+ * chore: implement startSweep function to manage cache sweep process ([78ded7a](https://github.com/neezco/cache/commit/78ded7a))
8
+ * chore: install semantic-release plugins and add unified release script ([5b7a8b3](https://github.com/neezco/cache/commit/5b7a8b3))
9
+ * test: add comprehensive tests for LocalTtlCache purge strategies and basic operations ([a410f2d](https://github.com/neezco/cache/commit/a410f2d))
10
+ * test: ensure tag invalidation does not affect entries created after the tag ([9282397](https://github.com/neezco/cache/commit/9282397))
11
+ * fix: enforce stale window upper bound when applying tag-based stale invalidation ([593c1d4](https://github.com/neezco/cache/commit/593c1d4)), closes [#19](https://github.com/neezco/cache/issues/19)
12
+ * feat: enhance cache purging strategy with configurable thresholds ([ee762c1](https://github.com/neezco/cache/commit/ee762c1))
13
+ * feat: refactor purge configuration logic and enhance validation for thresholds ([350b9de](https://github.com/neezco/cache/commit/350b9de)), closes [#18](https://github.com/neezco/cache/issues/18)
14
+ * style: add conventional-changelog-conventionalcommits package for improved changelog generation ([5435048](https://github.com/neezco/cache/commit/5435048))
15
+ * style: simplify commit-analyzer plugin configuration ([ecff6a0](https://github.com/neezco/cache/commit/ecff6a0))
16
+ * style: update release notes generator configuration for improved changelog formatting ([1ac4776](https://github.com/neezco/cache/commit/1ac4776))
17
+
18
+ # [0.3.0](https://github.com/neezco/cache/compare/v0.2.1...v0.3.0) (2026-02-11)
19
+
20
+
21
+ ### Features
22
+
23
+ * enhance cache retrieval with metadata support in `get()` method ([eb198d6](https://github.com/neezco/cache/commit/eb198d66c9e1abda86c448fb81a35f14e376a79c)), closes [#17](https://github.com/neezco/cache/issues/17)
24
+
5
25
  ## [0.2.1](https://github.com/neezco/cache/compare/v0.2.0...v0.2.1) (2026-02-11)
6
26
 
7
27
 
@@ -11,10 +11,10 @@ declare const enum DELETE_REASON {
11
11
  */
12
12
  interface CacheConfigBase {
13
13
  /**
14
- * Callback invoked when a key expires naturally.
14
+ * Callback invoked when an entry expires naturally (not manually deleted).
15
15
  * @param key - The expired key.
16
16
  * @param value - The value associated with the expired key.
17
- * @param reason - The reason for deletion ('expired', or 'stale').
17
+ * @param reason - The reason for expiration: 'expired' (fully expired) or 'stale' (stale window expired).
18
18
  */
19
19
  onExpire?: (key: string, value: unknown, reason: Exclude<DELETE_REASON, DELETE_REASON.MANUAL>) => void;
20
20
  /**
@@ -30,16 +30,13 @@ interface CacheConfigBase {
30
30
  */
31
31
  defaultTtl: number;
32
32
  /**
33
- * Default stale window in milliseconds for entries that do not
34
- * specify their own `staleWindowMs`.
33
+ * Default stale window in milliseconds for entries without explicit `staleWindow`.
35
34
  *
36
- * This window determines how long an entry may continue to be
37
- * served as stale after it reaches its expiration time.
35
+ * Defines how long an entry can be served as stale after expiration.
36
+ * The window is relative to each entry's expiration moment, whether from
37
+ * explicit `ttl` or the cache's `defaultTtl`.
38
38
  *
39
- * The window is always relative to the entry’s own expiration
40
- * moment, regardless of whether that expiration comes from an
41
- * explicit `ttl` or from the cache’s default TTL.
42
- * @default null (No stale window)
39
+ * @default 0 (no stale window)
43
40
  */
44
41
  defaultStaleWindow: number;
45
42
  /**
@@ -55,246 +52,287 @@ interface CacheConfigBase {
55
52
  */
56
53
  maxMemorySize: number;
57
54
  /**
58
- * Controls how stale entries are handled when read from the cache.
59
- *
60
- * - true → stale entries are purged immediately after being returned.
61
- * - false → stale entries are retained after being returned.
62
- *
63
- * @default false
55
+ * Controls stale entry purging behavior on `get()` operations.
56
+ *
57
+ * Possible values:
58
+ * - `true`purge stale entries immediately after read.
59
+ * - `false` → retain stale entries after read.
60
+ * - `number (0-1)` → purge when `resourceUsage ≥ threshold` (uses `purgeResourceMetric`).
61
+ *
62
+ * Numeric threshold validation:
63
+ * - Must be 0 < value ≤ 1 (boolean fallback if invalid range)
64
+ * - Requires purgeResourceMetric to support thresholds (not 'fixed')
65
+ * - Requires matching configuration limits for the metric:
66
+ * * 'size' metric requires maxSize
67
+ * * 'memory' metric requires maxMemorySize
68
+ * * 'higher' metric requires both maxSize and maxMemorySize
69
+ * - Invalid numeric values fallback to default: 0.80 (with limits) or false (without)
70
+ *
71
+ * Environment notes:
72
+ * - Backend: `"memory"` and `"higher"` metrics available; frontend: only `"size"`.
73
+ * - Can be overridden per-read via `get(key, { purgeStale })`.
74
+ *
75
+ * Defaults:
76
+ * - With matching limits → `0.80` (80% resource usage).
77
+ * - Without matching limits → `false`.
64
78
  */
65
- purgeStaleOnGet: boolean;
79
+ purgeStaleOnGet: PurgeMode;
66
80
  /**
67
- * Controls how stale entries are handled during sweep operations.
81
+ * Controls stale entry purging behavior during sweep operations.
82
+ *
83
+ * Possible values:
84
+ * - `true` → purge stale entries during sweeps.
85
+ * - `false` → retain stale entries during sweeps.
86
+ * - `number (0-1)` → purge when `resourceUsage ≥ threshold` (uses `purgeResourceMetric`).
87
+ *
88
+ * Numeric threshold validation:
89
+ * - Must be 0 < value ≤ 1 (boolean fallback if invalid range)
90
+ * - Requires purgeResourceMetric to support thresholds (not 'fixed')
91
+ * - Requires matching configuration limits for the metric:
92
+ * * 'size' metric requires maxSize
93
+ * * 'memory' metric requires maxMemorySize
94
+ * * 'higher' metric requires both maxSize and maxMemorySize
95
+ * - Invalid numeric values fallback to default: 0.5 (with limits) or true (without)
96
+ *
97
+ * Prevents stale entry accumulation when enabled. Without limits, defaults to `true`
98
+ * to prevent unbounded growth.
99
+ *
100
+ * Environment notes:
101
+ * - Backend: `"memory"` and `"higher"` metrics available; frontend: only `"size"`.
102
+ *
103
+ * Defaults:
104
+ * - With matching limits → `0.5` (50% resource usage).
105
+ * - Without matching limits → `true` (prevent unbounded accumulation).
106
+ */
107
+ purgeStaleOnSweep: PurgeMode;
108
+ /**
109
+ * Metric used to evaluate resource usage for threshold-based stale purging.
68
110
  *
69
- * - true → stale entries are purged during sweeps.
70
- * - false → stale entries are retained during sweeps.
111
+ * Applies when `purgeStaleOnGet` or `purgeStaleOnSweep` are numeric (0-1).
71
112
  *
72
- * @default false
113
+ * Metric options:
114
+ * - `"size"` → normalized entry count (`current / maxSize`).
115
+ * - `"memory"` → normalized RAM (`currentMB / maxMemorySize`).
116
+ * - `"higher"` → max of both metrics (recommended for dual limits).
117
+ * - `"fixed"` → disable threshold purging; only bool values apply.
118
+ *
119
+ * Environment support:
120
+ * - Backend: all metrics available.
121
+ * - Frontend: only `"size"`; numeric thresholds fallback to `"fixed"`.
122
+ *
123
+ * Auto-selection (if not specified):
124
+ * - Backend: `"higher"` (both limits) → `"memory"` → `"size"` → `"fixed"`.
125
+ * - Frontend: `"size"` (if valid) → `"fixed"`.
126
+ *
127
+ * @default Depends on environment and valid limits.
73
128
  */
74
- purgeStaleOnSweep: boolean;
129
+ purgeResourceMetric?: "memory" | "size" | "higher" | "fixed";
75
130
  /**
76
- * Whether to automatically start the sweep process when the cache is created.
77
- *
78
- * - true → sweep starts automatically.
79
- * - false → sweep does not start automatically, allowing manual control.
131
+ * Auto-start sweep process on cache initialization.
80
132
  *
81
133
  * @internal
82
134
  * @default true
83
135
  */
84
136
  _autoStartSweep: boolean;
85
137
  /**
86
- * Allowed expired ratio for the cache instance.
138
+ * @internal Maximum allowed ratio of expired entries before aggressive sweep.
87
139
  */
88
140
  _maxAllowExpiredRatio: number;
89
141
  }
90
142
  /**
91
- * Public configuration options for the TTL cache.
143
+ * Purge mode: boolean for immediate/skip, or 0-1 for threshold-based purging.
144
+ */
145
+ type PurgeMode = boolean | number;
146
+ /**
147
+ * Public cache configuration (all fields optional).
92
148
  */
93
149
  type CacheOptions = Partial<CacheConfigBase>;
94
150
  /**
95
- * Options for `invalidateTag` operation. Kept intentionally extensible so
96
- * future flags can be added without breaking callers.
151
+ * Options for tag invalidation. Extensible for forward-compatibility.
97
152
  */
98
153
  interface InvalidateTagOptions {
99
- /** If true, mark affected entries as stale instead of fully expired. */
154
+ /**
155
+ * If true, mark entries as stale instead of fully expired.
156
+ * They remain accessible via stale window if configured.
157
+ */
100
158
  asStale?: boolean;
101
159
  [key: string]: unknown;
102
160
  }
103
- //#endregion
104
- //#region src/index.d.ts
105
161
  /**
106
- * A TTL (Time-To-Live) cache implementation with support for expiration,
107
- * stale windows, tag-based invalidation, and automatic sweeping.
108
- *
109
- * Provides O(1) constant-time operations for all core methods.
110
- *
111
- * @example
112
- * ```typescript
113
- * const cache = new LocalTtlCache();
114
- * cache.set("user:123", { name: "Alice" }, { ttl: 5 * 60 * 1000 });
115
- * const user = cache.get("user:123"); // { name: "Alice" }
116
- * ```
162
+ * Entry status: fresh, stale, or expired.
117
163
  */
118
- declare class LocalTtlCache {
119
- private state;
164
+ declare enum ENTRY_STATUS {
165
+ /** Valid and within TTL. */
166
+ FRESH = "fresh",
167
+ /** Expired but within stale window; still served. */
168
+ STALE = "stale",
169
+ /** Beyond stale window; not served. */
170
+ EXPIRED = "expired",
171
+ }
172
+ /**
173
+ * Metadata returned from `get()` with `includeMetadata: true`.
174
+ * Provides complete entry state including timing, status, and tags.
175
+ */
176
+ interface EntryMetadata<T = unknown> {
177
+ /** The cached value. */
178
+ data: T;
179
+ /** Absolute timestamp (ms) when entry expires. */
180
+ expirationTime: number;
181
+ /** Absolute timestamp (ms) when stale window ends. */
182
+ staleWindowExpiration: number;
183
+ /** Current entry status. */
184
+ status: ENTRY_STATUS;
185
+ /** Associated tags for group invalidation, or null. */
186
+ tags: string[] | null;
187
+ }
188
+ /**
189
+ * Options for `get()` without metadata (default).
190
+ * Returns only the cached value.
191
+ */
192
+ interface GetOptionsWithoutMetadata {
120
193
  /**
121
- * Creates a new cache instance.
122
- *
123
- * @param options - Configuration options for the cache (defaultTtl, defaultStaleWindow, maxSize, etc.)
124
- *
125
- * @example
126
- * ```typescript
127
- * const cache = new LocalTtlCache({
128
- * defaultTtl: 30 * 60 * 1000, // 30 minutes
129
- * defaultStaleWindow: 5 * 60 * 1000, // 5 minutes
130
- * maxSize: 500_000, // Maximum 500_000 entries
131
- * onExpire: (key, value) => console.log(`Expired: ${key}`),
132
- * onDelete: (key, value, reason) => console.log(`Deleted: ${key}, reason: ${reason}`),
133
- * });
134
- * ```
194
+ * If false (or omitted), returns value only without metadata.
195
+ * @default false
135
196
  */
136
- constructor(options?: CacheOptions);
197
+ includeMetadata?: false;
137
198
  /**
138
- * Gets the current number of entries tracked by the cache.
139
- *
140
- * This value may include entries that are already expired but have not yet been
141
- * removed by the lazy cleanup system. Expired keys are cleaned only when it is
142
- * efficient to do so, so the count can temporarily be higher than the number of
143
- * actually valid (non‑expired) entries.
199
+ * Controls stale entry purging on this read.
144
200
  *
145
- * @returns The number of entries currently stored (including entries pending cleanup)
201
+ * - `true` purge immediately after return.
202
+ * - `false` → keep stale entries.
203
+ * - `number (0-1)` → purge at resource usage threshold.
146
204
  *
147
- * @example
148
- * ```typescript
149
- * console.log(cache.size); // e.g., 42
150
- * ```
205
+ * Overrides global `purgeStaleOnGet` setting.
151
206
  */
152
- get size(): number;
207
+ purgeStale?: PurgeMode;
208
+ }
209
+ /**
210
+ * Options for `get()` with metadata.
211
+ * Returns value and complete entry state.
212
+ */
213
+ interface GetOptionsWithMetadata {
153
214
  /**
154
- * Retrieves a value from the cache.
155
- *
156
- * Returns the value if it exists and is not fully expired. If an entry is in the
157
- * stale window (expired but still within staleWindow), the stale value is returned.
158
- *
159
-
160
- * @param key - The key to retrieve
161
- * @returns The cached value if valid, undefined otherwise
162
- *
163
- * @example
164
- * ```typescript
165
- * const user = cache.get<{ name: string }>("user:123");
166
- * ```
167
- *
168
- * @edge-cases
169
- * - Returns `undefined` if the key doesn't exist
170
- * - Returns `undefined` if the key has expired beyond the stale window
171
- * - Returns the stale value if within the stale window
172
- * - If `purgeStaleOnGet` is enabled, stale entries are deleted after being returned
215
+ * If true, returns `EntryMetadata<T>` object with value, timing, and tags.
173
216
  */
174
- get<T = unknown>(key: string): T | undefined;
217
+ includeMetadata: true;
175
218
  /**
176
- * Sets or updates a value in the cache.
219
+ * Controls stale entry purging on this read.
177
220
  *
178
- * If the key already exists, it will be completely replaced.
221
+ * - `true` purge immediately after return.
222
+ * - `false` → keep stale entries.
223
+ * - `number (0-1)` → purge at resource usage threshold.
179
224
  *
180
- * @param key - The key under which to store the value
181
- * @param value - The value to cache (any type)
182
- * @param options - Optional configuration for this specific entry
183
- * @param options.ttl - Time-To-Live in milliseconds. Defaults to `defaultTtl`
184
- * @param options.staleWindow - How long to serve stale data after expiration (milliseconds)
185
- * @param options.tags - One or more tags for group invalidation
186
- * @returns True if the entry was set or updated, false if rejected due to limits or invalid input
225
+ * Overrides global `purgeStaleOnGet` setting.
226
+ */
227
+ purgeStale?: PurgeMode;
228
+ }
229
+ /**
230
+ * Options for `set()` method.
231
+ * Controls TTL, stale window, and tagging per entry.
232
+ */
233
+ interface SetOptions {
234
+ /**
235
+ * Time-To-Live in milliseconds.
236
+ * Determines fresh period before expiration.
187
237
  *
188
- * @example
189
- * ```typescript
190
- * const success = cache.set("user:123", { name: "Alice" }, {
191
- * ttl: 5 * 60 * 1000,
192
- * staleWindow: 1 * 60 * 1000,
193
- * tags: "user:123",
194
- * });
238
+ * Special values:
239
+ * - `0` | `Infinity` → entry never expires
195
240
  *
196
- * if (!success) {
197
- * console.log("Entry was rejected due to size or memory limits");
198
- * }
199
- * ```
241
+ * Falls back to cache's `defaultTtl` if omitted.
242
+ */
243
+ ttl?: number;
244
+ /**
245
+ * Stale window duration in milliseconds.
200
246
  *
201
- * @edge-cases
202
- * - Overwriting an existing key replaces it completely
203
- * - If `ttl` is 0 or Infinite, the entry never expires
204
- * - If `staleWindow` is larger than `ttl`, the entry can be served as stale longer than it was fresh
205
- * - Tags are optional; only necessary for group invalidation via `invalidateTag()`
206
- * - Returns `false` if value is `undefined` (existing value remains untouched)
207
- * - Returns `false` if new key would exceed [`maxSize`](./docs/configuration.md#maxsize-number) limit
208
- * - Returns `false` if new key would exceed [`maxMemorySize`](./docs/configuration.md#maxmemorysize-number) limit
209
- * - Updating existing keys always succeeds, even at limit
247
+ * Determines how long entry serves stale after expiration.
248
+ * Falls back to cache's `defaultStaleWindow` if omitted.
210
249
  */
211
- set(key: string, value: unknown, options?: {
212
- ttl?: number;
213
- staleWindow?: number;
214
- tags?: string | string[];
215
- }): boolean;
250
+ staleWindow?: number;
216
251
  /**
217
- * Deletes a specific key from the cache.
252
+ * One or more tags for group-based invalidation.
218
253
  *
219
- * @param key - The key to delete
220
- * @returns True if the key was deleted, false if it didn't exist
254
+ * Tags enable batch invalidation via `invalidateTag()`.
255
+ * Invalidating ANY tag on an entry invalidates the whole entry.
221
256
  *
222
- * @example
223
- * ```typescript
224
- * const wasDeleted = cache.delete("user:123");
225
- * ```
257
+ * Falls back to cache's default if omitted.
258
+ */
259
+ tags?: string | string[];
260
+ }
261
+ /**
262
+ * TTL cache public interface.
263
+ * Implemented by `LocalTtlCache` class.
264
+ */
265
+ interface LocalTtlCacheInterface {
266
+ /**
267
+ * Current number of entries (may include expired entries pending cleanup).
268
+ */
269
+ readonly size: number;
270
+ /**
271
+ * Retrieves value from cache.
272
+ * Returns fresh, stale, or undefined (expired or not found).
226
273
  *
227
- * @edge-cases
228
- * - Triggers the `onDelete` callback with reason `'manual'`
229
- * - Does not trigger the `onExpire` callback
230
- * - Returns `false` if the key was already expired
231
- * - Deleting a non-existent key returns `false` without error
274
+ * @overload `get<T>(key)` → `T | undefined` (no metadata)
275
+ * @overload `get<T>(key, { includeMetadata: true })``EntryMetadata<T> | undefined` (with metadata)
276
+ */
277
+ get<T = unknown>(key: string): T | undefined;
278
+ get<T = unknown>(key: string, options: GetOptionsWithMetadata): EntryMetadata<T> | undefined;
279
+ get<T = unknown>(key: string, options: GetOptionsWithoutMetadata): T | undefined;
280
+ /**
281
+ * Sets or replaces a cache entry.
282
+ * @returns true if set/updated, false if rejected (limits/invalid).
283
+ */
284
+ set(key: string, value: unknown, options?: SetOptions): boolean;
285
+ /**
286
+ * Deletes a specific key from cache.
287
+ * @returns true if deleted, false if not found.
232
288
  */
233
289
  delete(key: string): boolean;
234
290
  /**
235
- * Checks if a key exists in the cache and is not fully expired.
236
- *
237
- * Returns true if the key exists and is either fresh or within the stale window.
238
- * Use this when you only need to check existence without retrieving the value.
239
- *
240
- * @param key - The key to check
241
- * @returns True if the key exists and is valid, false otherwise
242
- *
243
- * @example
244
- * ```typescript
245
- * if (cache.has("user:123")) {
246
- * // Key exists (either fresh or stale)
247
- * }
248
- * ```
249
- *
250
- * @edge-cases
251
- * - Returns `false` if the key doesn't exist
252
- * - Returns `false` if the key has expired beyond the stale window
253
- * - Returns `true` if the key is in the stale window (still being served)
254
- * - Both `has()` and `get()` have O(1) complexity; prefer `get()` if you need the value
291
+ * Checks if key exists (fresh or stale).
292
+ * @returns true if valid, false if not found or fully expired.
255
293
  */
256
294
  has(key: string): boolean;
257
295
  /**
258
- * Removes all entries from the cache at once.
259
- *
260
- * This is useful for resetting the cache or freeing memory when needed.
261
- * The `onDelete` callback is NOT invoked during clear (intentional optimization).
262
- *
263
- * @example
264
- * ```typescript
265
- * cache.clear(); // cache.size is now 0
266
- * ```
267
- *
268
- * @edge-cases
269
- * - The `onDelete` callback is NOT triggered during clear
270
- * - Clears both expired and fresh entries
271
- * - Resets `cache.size` to 0
296
+ * Removes all entries from cache.
297
+ * Does NOT trigger `onDelete` callbacks (optimization).
272
298
  */
273
299
  clear(): void;
274
300
  /**
275
- * Marks all entries with one or more tags as expired (or stale, if requested).
276
- *
277
- * If an entry has multiple tags, invalidating ANY of those tags will invalidate the entry.
278
- *
279
- * @param tags - A single tag (string) or array of tags to invalidate
280
- * @param asStale - If true, marks entries as stale instead of fully expired (still served from stale window)
281
- *
282
- * @example
283
- * ```typescript
284
- * // Invalidate a single tag
285
- * cache.invalidateTag("user:123");
301
+ * Marks entries with given tags as expired (or stale).
302
+ * Invalidating ANY tag on an entry invalidates it.
303
+ */
304
+ invalidateTag(tags: string | string[], options?: InvalidateTagOptions): void;
305
+ }
306
+ //#endregion
307
+ //#region src/index.d.ts
308
+ /**
309
+ * A TTL (Time-To-Live) cache implementation with support for expiration,
310
+ * stale windows, tag-based invalidation, and smart automatic sweeping.
311
+ *
312
+ * Provides O(1) constant-time operations for all core methods with support for:
313
+ * - Expiration and stale windows
314
+ * - Tag-based invalidation
315
+ * - Automatic sweeping
316
+ */
317
+ declare class LocalTtlCache implements LocalTtlCacheInterface {
318
+ private state;
319
+ /**
320
+ * Creates a new cache instance.
286
321
  *
287
- * // Invalidate multiple tags
288
- * cache.invalidateTag(["user:123", "posts:456"]);
289
- * ```
322
+ * @param options - Configuration options for the cache (defaultTtl, defaultStaleWindow, maxSize, etc.)
290
323
  *
291
- * @edge-cases
292
- * - Does not throw errors if a tag has no associated entries
293
- * - Invalidating a tag doesn't prevent new entries from being tagged with it later
294
- * - The `onDelete` callback is triggered with reason `'expired'` (even if `asStale` is true)
295
324
  */
325
+ constructor(options?: CacheOptions);
326
+ get size(): number;
327
+ get<T = unknown>(key: string): T | undefined;
328
+ get<T = unknown>(key: string, options: GetOptionsWithMetadata): EntryMetadata<T>;
329
+ get<T = unknown>(key: string, options: GetOptionsWithoutMetadata): T | undefined;
330
+ set(key: string, value: unknown, options?: SetOptions): boolean;
331
+ delete(key: string): boolean;
332
+ has(key: string): boolean;
333
+ clear(): void;
296
334
  invalidateTag(tags: string | string[], options?: InvalidateTagOptions): void;
297
335
  }
298
336
  //#endregion
299
- export { type CacheOptions, type InvalidateTagOptions, LocalTtlCache };
337
+ export { type CacheOptions, ENTRY_STATUS, type EntryMetadata, type GetOptionsWithMetadata, type GetOptionsWithoutMetadata, type InvalidateTagOptions, LocalTtlCache, type LocalTtlCacheInterface, type PurgeMode, type SetOptions };
300
338
  //# sourceMappingURL=index.d.ts.map