@asaidimu/utils-cache 1.0.0 → 2.0.1

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.
Files changed (2) hide show
  1. package/README.md +400 -240
  2. package/package.json +3 -3
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
- # @asaidimu/utils-cache
1
+ # `@asaidimu/utils-cache`
2
2
 
3
3
  An intelligent, configurable in-memory cache library for Node.js and browser environments, designed for optimal performance, data consistency, and developer observability.
4
4
 
5
- [![npm version](https://img.shields.io/npm/v/@ausaidimu/utils.svg)](https://www.npmjs.com/package/@augustine/utils)
5
+ [![npm version](https://img.shields.io/npm/v/@asaidimu/utils-cache.svg)](https://www.npmjs.com/package/@asaidimu/utils-cache)
6
6
  [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
- [![Build Status](https://img.shields.io/github/actions/workflow/status/ausaidimu/utils-ci.yml?branch=main)](https://github.com/augustine/utils-actions)
7
+ [![Build Status](https://img.shields.io/github/actions/workflow/status/asaidimu/erp-utils/utils-ci.yml?branch=main)](https://github.com/asaidimu/erp-utils/actions)
8
8
  [![TypeScript](https://img.shields.io/badge/Built%20with-TypeScript-blue.svg)](https://www.typescriptlang.org/)
9
9
 
10
10
  ---
@@ -48,27 +48,27 @@ An intelligent, configurable in-memory cache library for Node.js and browser env
48
48
 
49
49
  ### Detailed Description
50
50
 
51
- `Cache` provides a robust, in-memory caching solution designed for applications that require efficient data retrieval, resilience against network failures, and state persistence across sessions or processes. It implements common caching patterns like *stale-while-revalidate* and *Least Recently Used (LRU)* eviction, along with advanced features like automatic retries for failed fetches, extensible persistence mechanisms, and a comprehensive event system for real-time monitoring.
51
+ `@asaidimu/utils-cache` provides a robust, in-memory caching solution designed for applications that require efficient data retrieval, resilience against network failures, and state persistence across sessions or processes. It implements common caching patterns like *stale-while-revalidate* and *Least Recently Used (LRU)* eviction, along with advanced features such as automatic retries for failed fetches, an extensible persistence mechanism, and a comprehensive event system for real-time monitoring.
52
52
 
53
- Unlike simpler caches, `Cache` manages data freshness intelligently, allowing you to serve stale data while a fresh copy is being fetched in the background. Its pluggable persistence layer enables you to save and restore the cache state, making it ideal for client-side applications that need to maintain state offline or server-side applications that need rapid startup with pre-populated data. With built-in metrics and events, `SimpleCache` offers deep insights into cache performance and lifecycle.
53
+ Unlike simpler caches, `Cache` manages data freshness intelligently, allowing you to serve stale data immediately while a fresh copy is being fetched in the background. Its pluggable persistence layer enables you to save and restore the cache state, making it ideal for client-side applications that need to maintain state offline or server-side applications that need rapid startup with pre-populated data. With built-in metrics and events, `@asaidimu/utils-cache` offers deep insights into cache performance and lifecycle, ensuring both speed and data integrity.
54
54
 
55
55
  ### Key Features
56
56
 
57
- * **Configurable In-Memory Store**: Fast access to cached data.
58
- * **Stale-While-Revalidate (SWR)**: Serve existing data immediately while fetching new data in the background, minimizing perceived latency.
59
- * **Automatic Retries**: Configurable retry attempts and exponential backoff for `fetchFunction` failures.
60
- * **Pluggable Persistence**: Integrates with any `SimplePersistence` implementation (e.g., LocalStorage, IndexedDB, custom backend) to save and restore cache state.
61
- * **Debounced Persistence Writes**: Optimizes write frequency to the underlying persistence layer.
62
- * **Remote Update Handling**: Synchronizes cache state when the persistence layer is updated externally.
63
- * **Custom Serialization/Deserialization**: Control how your data is prepared for persistence.
57
+ * **Configurable In-Memory Store**: Provides fast access to cached data with an underlying `Map` structure.
58
+ * **Stale-While-Revalidate (SWR)**: Serve existing data immediately while fetching new data in the background, minimizing perceived latency and improving user experience.
59
+ * **Automatic Retries with Exponential Backoff**: Configurable retry attempts and an exponentially increasing delay between retries for `fetchFunction` failures, enhancing resilience to transient network issues.
60
+ * **Pluggable Persistence**: Seamlessly integrates with any `SimplePersistence` implementation (e.g., LocalStorage, IndexedDB via `@asaidimu/utils-persistence`, or custom backend) to save and restore cache state across application restarts or sessions.
61
+ * **Debounced Persistence Writes**: Optimizes write frequency to the underlying persistence layer, reducing I/O operations and improving performance.
62
+ * **Remote Update Handling**: Automatically synchronizes cache state when the persistence layer is updated externally by other instances or processes.
63
+ * **Custom Serialization/Deserialization**: Provides options to serialize and deserialize complex data types (e.g., `Date`, `Map`, custom classes) for proper storage and retrieval.
64
64
  * **Configurable Eviction Policies**:
65
- * **Time-Based (TTL)**: Automatically evicts entries inactive for `cacheTime`.
66
- * **Size-Based (LRU)**: Evicts least recently used items when `maxSize` is exceeded.
67
- * **Comprehensive Event System**: Subscribe to granular cache events (hit, miss, fetch, error, eviction, invalidation, persistence, set_data) for logging, debugging, and advanced reactivity.
68
- * **Performance Metrics**: Built-in tracking for hits, misses, fetches, errors, evictions, and stale hits, with calculated hit rates.
69
- * **Flexible Query Management**: Register `fetchFunction`s for specific keys, allowing `Cache` to manage their lifecycle.
70
- * **Imperative Control**: Methods for manual `invalidate`, `prefetch`, `refresh`, `setData`, and `remove` operations.
71
- * **TypeScript Support**: Fully typed API for enhanced developer experience.
65
+ * **Time-Based (TTL)**: Automatically evicts entries that haven't been accessed for a specified `cacheTime`, managing memory efficiently.
66
+ * **Size-Based (LRU)**: Evicts least recently used items when the `maxSize` limit is exceeded, preventing unbounded memory growth.
67
+ * **Comprehensive Event System**: Subscribe to granular cache events (e.g., `'hit'`, `'miss'`, `'fetch'`, `'error'`, `'eviction'`, `'invalidation'`, `'set_data'`, `'persistence'`) for real-time logging, debugging, analytics, and advanced reactivity.
68
+ * **Performance Metrics**: Built-in tracking for `hits`, `misses`, `fetches`, `errors`, `evictions`, and `staleHits`, providing insights into cache efficiency with calculated hit rates.
69
+ * **Flexible Query Management**: Register asynchronous `fetchFunction`s for specific keys, allowing the `Cache` instance to intelligently manage their data lifecycle, including fetching, caching, and invalidation.
70
+ * **Imperative Control**: Offers direct methods for `invalidate` (making data stale), `prefetch` (loading data proactively), `refresh` (forcing a re-fetch), `setData` (manual data injection), and `remove` operations.
71
+ * **TypeScript Support**: Fully typed API for enhanced developer experience, compile-time safety, and autocompletion.
72
72
 
73
73
  ---
74
74
 
@@ -77,47 +77,60 @@ Unlike simpler caches, `Cache` manages data freshness intelligently, allowing yo
77
77
  ### Prerequisites
78
78
 
79
79
  * Node.js (v14.x or higher)
80
- * npm or yarn
80
+ * npm, yarn, or bun
81
81
 
82
82
  ### Installation Steps
83
83
 
84
- Install `Cache` using your preferred package manager:
84
+ Install `@asaidimu/utils-cache` using your preferred package manager:
85
85
 
86
86
  ```bash
87
- bun add @ausaidimu/utils-cache
87
+ bun add @asaidimu/utils-cache
88
+ # or
89
+ npm install @asaidimu/utils-cache
90
+ # or
91
+ yarn add @asaidimu/utils-cache
88
92
  ```
89
93
 
90
94
  ### Configuration
91
95
 
92
- `Cache` is initialized with a `CacheOptions` object, allowing you to customize its behavior globally. Individual queries can override these options.
96
+ `Cache` is initialized with a `CacheOptions` object, allowing you to customize its behavior globally. Individual queries registered via `registerQuery` can override these options for specific data keys.
93
97
 
94
98
  ```typescript
95
- import { Cache } from '@ausaidimu/utils-cache';
96
- import { IndexedDBPersistence } from '@asaidimu/utils-persistence';
97
-
99
+ import { Cache } from '@asaidimu/utils-cache';
100
+ // Example persistence layer (install separately, e.g., @asaidimu/utils-persistence)
101
+ import { IndexedDBPersistence } from '@asaidimu/utils-persistence'; // Example
98
102
 
99
103
  const myCache = new Cache({
100
- staleTime: 5 * 60 * 1000, // Data considered stale after 5 minutes
101
- cacheTime: 30 * 60 * 1000, // Data evicted if not accessed for 30 minutes
102
- retryAttempts: 2, // Retry fetch up to 2 times on failure
103
- retryDelay: 2000, // 2-second initial delay between retries (doubles each attempt)
104
- maxSize: 500, // Maximum 500 entries (LRU eviction)
105
- enableMetrics: true,
106
- persistence: new IndexedDBPersistence(), // Plug in your persistence layer
104
+ staleTime: 5 * 60 * 1000, // Data considered stale after 5 minutes (5 * 60 * 1000ms)
105
+ cacheTime: 30 * 60 * 1000, // Data evicted if not accessed for 30 minutes
106
+ retryAttempts: 2, // Retry fetch up to 2 times on failure
107
+ retryDelay: 2000, // 2-second initial delay between retries (doubles each attempt)
108
+ maxSize: 500, // Maximum 500 entries in cache (LRU eviction)
109
+ enableMetrics: true, // Enable performance tracking
110
+
111
+ // Persistence options (optional but recommended for stateful caches)
112
+ persistence: new IndexedDBPersistence('my-app-db'), // Plug in your persistence layer
107
113
  persistenceId: 'my-app-cache-v1', // Unique ID for this cache instance in persistence
108
114
  persistenceDebounceTime: 1000, // Debounce persistence writes by 1 second
109
- // Custom serializers/deserializers for non-JSON-serializable data
110
- serializeValue: (value) => {
115
+
116
+ // Custom serializers/deserializers for non-JSON-serializable data (optional)
117
+ serializeValue: (value: any) => {
111
118
  if (value instanceof Map) return { _type: 'Map', data: Array.from(value.entries()) };
119
+ if (value instanceof Date) return { _type: 'Date', data: value.toISOString() };
112
120
  return value;
113
121
  },
114
- deserializeValue: (value) => {
115
- if (typeof value === 'object' && value !== null && value._type === 'Map') {
116
- return new Map(value.data);
122
+ deserializeValue: (value: any) => {
123
+ if (typeof value === 'object' && value !== null) {
124
+ if (value._type === 'Map') return new Map(value.data);
125
+ if (value._type === 'Date') return new Date(value.data);
117
126
  }
118
127
  return value;
119
128
  },
120
129
  });
130
+
131
+ // Negative option values are automatically clamped to 0 with a console warning.
132
+ const invalidCache = new Cache({ staleTime: -100, cacheTime: -1, maxSize: -5 });
133
+ // console.warn output for each negative value will appear
121
134
  ```
122
135
 
123
136
  ### Verification
@@ -125,15 +138,22 @@ const myCache = new Cache({
125
138
  To verify that `Cache` is installed and initialized correctly, you can run a simple test:
126
139
 
127
140
  ```typescript
128
- import { Cache } from '@ausaidimu/utils-cache';
141
+ import { Cache } from '@asaidimu/utils-cache';
129
142
 
130
143
  const cache = new Cache();
131
144
  console.log('Cache initialized successfully!');
132
145
 
133
- // You can add a simple query and try to fetch:
134
- cache.registerQuery('hello', async () => 'world');
146
+ // Register a simple query
147
+ cache.registerQuery('hello', async () => {
148
+ console.log('Fetching "hello" data...');
149
+ return 'world';
150
+ });
151
+
152
+ // Try to fetch data
135
153
  cache.get('hello').then(data => {
136
- console.log(`Fetched 'hello': ${data}`);
154
+ console.log(`Fetched 'hello': ${data}`); // Expected: Fetching "hello" data... \n Fetched 'hello': world
155
+ }).catch(error => {
156
+ console.error('Error fetching:', error);
137
157
  });
138
158
  ```
139
159
 
@@ -143,47 +163,54 @@ cache.get('hello').then(data => {
143
163
 
144
164
  ### Basic Usage
145
165
 
146
- The core of `Cache` involves registering queries and then retrieving data using those queries.
166
+ The core of `Cache` involves registering queries (data fetching functions) and then retrieving data using those queries.
147
167
 
148
168
  ```typescript
149
- import { Cache } from '@ausaidimu/utils-cache';
169
+ import { Cache } from '@asaidimu/utils-cache';
150
170
 
151
171
  const myCache = new Cache({
152
- staleTime: 5000, // 5 seconds
153
- cacheTime: 60000, // 1 minute
172
+ staleTime: 5000, // Data becomes stale after 5 seconds
173
+ cacheTime: 60000, // Data will be garbage collected if not accessed for 1 minute
154
174
  });
155
175
 
156
- // 1. Register a query with a unique key and a function to fetch the data
176
+ // 1. Register a query with a unique string key and an async function to fetch the data.
157
177
  myCache.registerQuery('user/123', async () => {
158
- console.log('Fetching user data...');
178
+ console.log('--- Fetching user data from API... ---');
159
179
  // Simulate network delay
160
180
  await new Promise(resolve => setTimeout(resolve, 1000));
161
181
  return { id: 123, name: 'Alice', email: 'alice@example.com' };
162
- });
182
+ }, { staleTime: 2000 }); // Override staleTime for this specific query to 2 seconds
163
183
 
164
- // 2. Retrieve data from the cache
165
- async function getUserData() {
166
- const userData = await myCache.get('user/123');
167
- console.log('User data:', userData);
184
+ // 2. Retrieve data from the cache using `get()`.
185
+
186
+ async function getUserData(label: string) {
187
+ console.log(`\n${label}: Requesting user/123`);
188
+ const userData = await myCache.get('user/123'); // Default: stale-while-revalidate
189
+ console.log(`${label}: User data received:`, userData);
168
190
  }
169
191
 
170
- // First call: Triggers fetch, data is fetched and cached
171
- getUserData();
192
+ // First call: Data is not in cache (miss). Triggers fetch.
193
+ getUserData('Initial Call');
172
194
 
173
- // Subsequent calls (within staleTime): Data is returned instantly from cache
174
- setTimeout(() => getUserData(), 500);
195
+ // Subsequent calls (within staleTime): Data is returned instantly from cache. No fetch.
196
+ setTimeout(() => getUserData('Cached Call'), 500);
175
197
 
176
- // Call after staleTime: Data is returned instantly, but a background fetch is triggered
177
- setTimeout(() => getUserData(), 6000);
198
+ // Call after query's staleTime: Data is returned instantly, but a background fetch is triggered.
199
+ setTimeout(() => getUserData('Stale & Background Fetch'), 2500);
178
200
 
179
201
  // Example of waiting for fresh data
180
202
  async function getFreshUserData() {
181
- console.log('\nRequesting fresh user data...');
182
- const freshUserData = await myCache.get('user/123', { waitForFresh: true });
183
- console.log('Fresh user data:', freshUserData);
203
+ console.log('\n--- Requesting FRESH user data (waiting for fetch)... ---');
204
+ try {
205
+ const freshUserData = await myCache.get('user/123', { waitForFresh: true });
206
+ console.log('Fresh user data received:', freshUserData);
207
+ } catch (error) {
208
+ console.error('Failed to get fresh user data:', error);
209
+ }
184
210
  }
185
211
 
186
- setTimeout(() => getFreshUserData(), 7000);
212
+ // This will wait for the background fetch triggered by the previous call (if still ongoing) or trigger a new one.
213
+ setTimeout(() => getFreshUserData(), 3000);
187
214
  ```
188
215
 
189
216
  ### API Usage
@@ -193,7 +220,7 @@ setTimeout(() => getFreshUserData(), 7000);
193
220
  Creates a new `Cache` instance with global default options.
194
221
 
195
222
  ```typescript
196
- import { Cache } from '@ausaidimu/utils-cache';
223
+ import { Cache } from '@asaidimu/utils-cache';
197
224
 
198
225
  const cache = new Cache({
199
226
  staleTime: 5 * 60 * 1000, // 5 minutes
@@ -204,20 +231,20 @@ const cache = new Cache({
204
231
 
205
232
  #### `cache.registerQuery<T>(key: string, fetchFunction: () => Promise<T>, options?: CacheOptions): void`
206
233
 
207
- Registers a data fetching function associated with a `key`. This function will be called when data for the `key` is not in cache, is stale, or explicitly invalidated/refreshed.
234
+ Registers a data fetching function associated with a unique `key`. This `fetchFunction` will be called when data for the `key` is not in cache, is stale, or explicitly invalidated/refreshed.
208
235
 
209
236
  - `key`: A unique string identifier for the data.
210
- - `fetchFunction`: An `async` function that returns a `Promise` resolving to the data.
211
- - `options`: Optional `CacheOptions` to override the instance's default options for this specific query.
237
+ - `fetchFunction`: An `async` function that returns a `Promise` resolving to the data of type `T`.
238
+ - `options`: Optional `CacheOptions` to override the instance's default options for this specific query (e.g., a shorter `staleTime` for frequently changing data).
212
239
 
213
240
  ```typescript
214
241
  cache.registerQuery('products/featured', async () => {
215
- const response = await fetch('/api/products/featured');
242
+ const response = await fetch('https://api.example.com/products/featured');
216
243
  if (!response.ok) throw new Error('Failed to fetch featured products');
217
244
  return response.json();
218
245
  }, {
219
246
  staleTime: 60 * 1000, // This query's data is stale after 1 minute
220
- retryAttempts: 5,
247
+ retryAttempts: 5, // It will retry fetching up to 5 times
221
248
  });
222
249
  ```
223
250
 
@@ -226,10 +253,10 @@ cache.registerQuery('products/featured', async () => {
226
253
  Retrieves data for a given `key`.
227
254
 
228
255
  - If data is fresh, returns it immediately.
229
- - If data is stale, returns it immediately and triggers a background refetch (stale-while-revalidate).
230
- - If data is not in cache (miss), triggers a fetch.
231
- - `waitForFresh`: If `true`, the method will await the fetch function to complete and return fresh data. If `false` (default), it will return existing stale data immediately if available, otherwise `undefined` while a fetch is ongoing in the background.
232
- - `throwOnError`: If `true`, and the fetch fails, the promise returned by `get` will reject with the error. If `false` (default), it will return `undefined` on fetch failure, or the last successfully fetched data if available.
256
+ - If data is stale (and `waitForFresh` is `false` or unset), returns it immediately and triggers a background refetch (stale-while-revalidate).
257
+ - If data is not in cache (miss), it triggers a fetch.
258
+ - `waitForFresh`: If `true`, the method will await the `fetchFunction` to complete and return fresh data. If `false` (default), it will return existing stale data immediately if available, otherwise `undefined` while a fetch is ongoing in the background.
259
+ - `throwOnError`: If `true`, and the `fetchFunction` fails after all retries, the promise returned by `get` will reject with the error. If `false` (default), it will return `undefined` on fetch failure, or the last successfully fetched data if available.
233
260
 
234
261
  ```typescript
235
262
  // Basic usage (stale-while-revalidate)
@@ -238,17 +265,18 @@ const post = await cache.get('posts/latest');
238
265
  // Wait for fresh data, throw if fetch fails
239
266
  try {
240
267
  const userProfile = await cache.get('user/profile', { waitForFresh: true, throwOnError: true });
268
+ console.log('Latest user profile:', userProfile);
241
269
  } catch (error) {
242
- console.error('Could not get fresh user profile:', error);
270
+ console.error('Could not get fresh user profile due to an error:', error);
243
271
  }
244
272
  ```
245
273
 
246
274
  #### `cache.peek<T>(key: string): T | undefined`
247
275
 
248
- Retrieves data from the cache without triggering any fetches or updating `lastUpdated` time. Useful for quick synchronous checks.
276
+ Retrieves data from the cache without triggering any fetches, updating `lastAccessed` time, or `accessCount`. Useful for quick synchronous checks.
249
277
 
250
278
  ```typescript
251
- const cachedValue = cache.peek('some-key');
279
+ const cachedValue = cache.peek('some-config-key');
252
280
  if (cachedValue) {
253
281
  console.log('Value is in cache:', cachedValue);
254
282
  } else {
@@ -263,72 +291,77 @@ Checks if a non-stale, non-loading entry exists in the cache for the given `key`
263
291
  ```typescript
264
292
  if (cache.has('config/app')) {
265
293
  console.log('App config is ready and fresh.');
294
+ } else {
295
+ console.log('App config is missing, stale, or currently loading.');
266
296
  }
267
297
  ```
268
298
 
269
- #### `cache.invalidate(key: string, refetch?: boolean): Promise<void>`
299
+ #### `cache.invalidate(key: string, refetch = true): Promise<void>`
270
300
 
271
- Marks an entry as stale, forcing the next `get` call to trigger a refetch. Optionally triggers an immediate background refetch.
301
+ Marks a specific cache entry as stale, forcing the next `get` call for that key to trigger a refetch. Optionally triggers an immediate background refetch.
272
302
 
273
303
  - `key`: The cache key to invalidate.
274
- - `refetch`: If `true` (default), triggers an immediate background fetch for the invalidated key.
304
+ - `refetch`: If `true` (default), triggers an immediate background fetch for the invalidated key using its registered `fetchFunction`.
275
305
 
276
306
  ```typescript
277
- // Invalidate a specific user's posts
278
- await cache.invalidate('user/123/posts');
307
+ // After updating a user, invalidate their profile data to ensure next fetch is fresh
308
+ await cache.invalidate('user/123/profile');
279
309
 
280
- // Invalidate and don't refetch until `get` is called
310
+ // Invalidate and don't refetch until `get` is explicitly called later
281
311
  await cache.invalidate('admin/dashboard/stats', false);
282
312
  ```
283
313
 
284
- #### `cache.invalidatePattern(pattern: RegExp, refetch?: boolean): Promise<void>`
314
+ #### `cache.invalidatePattern(pattern: RegExp, refetch = true): Promise<void>`
285
315
 
286
- Invalidates all cache entries whose keys match the given regular expression.
316
+ Invalidates all cache entries whose keys match the given regular expression. Similar to `invalidate`, it optionally triggers immediate background refetches for all matched keys.
287
317
 
288
- - `pattern`: A `RegExp` to match cache keys.
318
+ - `pattern`: A `RegExp` object to match against cache keys.
289
319
  - `refetch`: If `true` (default), triggers immediate background fetches for all matched keys.
290
320
 
291
321
  ```typescript
292
- // Invalidate all product-related data
293
- await cache.invalidatePattern(/^products\//);
322
+ // Invalidate all product-related data (e.g., after a mass product update)
323
+ await cache.invalidatePattern(/^products\//); // Matches 'products/1', 'products/list', etc.
324
+
325
+ // Invalidate all items containing 'temp' in their key, without immediate refetch
326
+ await cache.invalidatePattern(/temp/, false);
294
327
  ```
295
328
 
296
329
  #### `cache.prefetch(key: string): Promise<void>`
297
330
 
298
- Triggers a background fetch for a `key` if it's not already in cache or is stale. Useful for loading data proactively.
331
+ Triggers a background fetch for a `key` if it's not already in cache or is stale. Useful for loading data proactively before it's explicitly requested.
299
332
 
300
333
  ```typescript
301
- // On page load, prefetch common data
334
+ // On application startup or route change, prefetch common data
302
335
  cache.prefetch('static-content/footer');
303
336
  cache.prefetch('user/notifications/unread');
304
337
  ```
305
338
 
306
339
  #### `cache.refresh<T>(key: string): Promise<T | undefined>`
307
340
 
308
- Forces a re-fetch of data for a given `key`, bypassing staleness checks and existing fetch promises. Returns the fresh data.
341
+ Forces a re-fetch of data for a given `key`, bypassing staleness checks and any existing fetch promises. This ensures you always get the latest data. Returns the fresh data or `undefined` if the fetch ultimately fails.
309
342
 
310
343
  ```typescript
311
- // Force update user data after an API call changes it
344
+ // After an API call modifies a resource, force update its cached version
312
345
  const updatedUser = await cache.refresh('user/current');
313
346
  console.log('User data refreshed:', updatedUser);
314
347
  ```
315
348
 
316
349
  #### `cache.setData<T>(key: string, data: T): void`
317
350
 
318
- Manually sets or updates data in the cache for a given `key`. This immediately updates the cache entry, marks it as fresh, and triggers persistence if configured.
351
+ Manually sets or updates data in the cache for a given `key`. This immediately updates the cache entry, marks it as fresh (by setting `lastUpdated` to `Date.now()`), and triggers persistence if configured. It bypasses any registered `fetchFunction`.
319
352
 
320
353
  ```typescript
321
- // Manually update a shopping cart item count
354
+ // Manually update a shopping cart item count after a local UI interaction
322
355
  cache.setData('cart/item-count', 5);
323
356
 
324
- // Directly inject data fetched from another source
325
- const externalData = { /* ... */ };
326
- cache.setData('external-resource/id', externalData);
357
+ // Directly inject data fetched from another source or computed locally
358
+ const localConfig = { theme: 'dark', fontSize: 'medium' };
359
+ cache.setData('app/settings', localConfig);
327
360
  ```
328
361
 
329
362
  #### `cache.remove(key: string): boolean`
330
363
 
331
- Removes an entry from the cache. Returns `true` if an entry was found and removed, `false` otherwise.
364
+ Removes a specific entry from the cache. Returns `true` if an entry was found and removed, `false` otherwise. Also clears any ongoing fetches for that key and triggers persistence.
332
365
 
333
366
  ```typescript
334
367
  // When a user logs out, remove their specific session data
@@ -337,107 +370,124 @@ cache.remove('user/session');
337
370
 
338
371
  #### `cache.on<EType extends CacheEventType>(event: EType, listener: (ev: Extract<CacheEvent, { type: EType }>) => void): void`
339
372
 
340
- Subscribes to cache events.
373
+ Subscribes a listener function to specific cache events.
341
374
 
342
- - `event`: The type of event to listen for (e.g., `'hit'`, `'miss'`, `'error'`).
343
- - `listener`: A callback function that receives the event payload.
375
+ - `event`: The type of event to listen for (e.g., `'hit'`, `'miss'`, `'error'`, `'persistence'`). See `CacheEventType` in `types.ts` for all available types.
376
+ - `listener`: A callback function that receives the specific event payload for the subscribed event type.
344
377
 
345
378
  ```typescript
346
- import { Cache, CacheEvent, CacheEventType } from '@ausaidimu/utils-cache';
379
+ import { Cache, CacheEvent, CacheEventType } from '@asaidimu/utils-cache';
347
380
 
348
381
  const myCache = new Cache();
349
382
 
350
383
  myCache.on('hit', (e) => {
351
- console.log(`Cache HIT for ${e.key} (stale: ${e.isStale})`);
384
+ console.log(`[CacheEvent] HIT for ${e.key} (isStale: ${e.isStale})`);
352
385
  });
353
386
 
354
387
  myCache.on('miss', (e) => {
355
- console.log(`Cache MISS for ${e.key}`);
388
+ console.log(`[CacheEvent] MISS for ${e.key}`);
356
389
  });
357
390
 
358
391
  myCache.on('error', (e) => {
359
- console.error(`Cache ERROR for ${e.key} (attempt ${e.attempt}):`, e.error.message);
392
+ console.error(`[CacheEvent] ERROR for ${e.key} (attempt ${e.attempt}):`, e.error.message);
360
393
  });
361
394
 
362
395
  myCache.on('persistence', (e) => {
363
396
  if (e.event === 'save_success') {
364
- console.log(`Cache state saved successfully for ID: ${e.key}`);
397
+ console.log(`[CacheEvent] Persistence: Cache state saved successfully for ID: ${e.key}`);
365
398
  } else if (e.event === 'load_fail') {
366
- console.error(`Failed to load cache state for ID: ${e.key}`, e.error);
399
+ console.error(`[CacheEvent] Persistence: Failed to load cache state for ID: ${e.key}`, e.error);
400
+ } else if (e.event === 'remote_update') {
401
+ console.log(`[CacheEvent] Persistence: Cache state updated from remote source for ID: ${e.key}`);
367
402
  }
368
403
  });
404
+
405
+ // For demonstration, register a query and trigger events
406
+ myCache.registerQuery('demo-item', async () => {
407
+ console.log('--- Fetching demo-item ---');
408
+ await new Promise(r => setTimeout(r, 200));
409
+ return 'demo-data';
410
+ }, { staleTime: 100 });
411
+
412
+ myCache.get('demo-item'); // Triggers miss, fetch, set_data
413
+ setTimeout(() => myCache.get('demo-item'), 50); // Triggers hit
414
+ setTimeout(() => myCache.get('demo-item'), 150); // Triggers stale hit, background fetch
369
415
  ```
370
416
 
371
417
  #### `cache.off<EType extends CacheEventType>(event: EType, listener: (ev: Extract<CacheEvent, { type: EType }>) => void): void`
372
418
 
373
- Unsubscribes a listener from a cache event.
419
+ Unsubscribes a previously registered listener from a cache event. The `listener` reference must be the exact same function that was passed to `on()`.
374
420
 
375
421
  ```typescript
376
- const myHitListener = (e: any) => console.log(`Hit: ${e.key}`);
377
- myCache.on('hit', myHitListener);
378
- // Later...
379
- myCache.off('hit', myHitListener);
422
+ const myHitLogger = (e: any) => console.log(`[Log] Cache Hit: ${e.key}`);
423
+ myCache.on('hit', myHitLogger);
424
+
425
+ // Later, when you no longer need the listener:
426
+ myCache.off('hit', myHitLogger);
380
427
  ```
381
428
 
382
- #### `cache.getStats(): { size: number; metrics: CacheMetrics; hitRate: number; staleHitRate: number; entries: Array<CacheEntryInfo> }`
429
+ #### `cache.getStats(): { size: number; metrics: CacheMetrics; hitRate: number; staleHitRate: number; entries: Array<{ key: string; lastAccessed: number; lastUpdated: number; accessCount: number; isStale: boolean; isLoading?: boolean; error?: boolean }> }`
383
430
 
384
- Returns current cache statistics and metrics.
431
+ Returns current cache statistics and detailed metrics.
385
432
 
386
- - `size`: Number of entries in the cache.
387
- - `metrics`: Object containing `hits`, `misses`, `fetches`, `errors`, `evictions`, `staleHits`.
433
+ - `size`: Number of active entries in the cache.
434
+ - `metrics`: An object containing raw counts (`hits`, `misses`, `fetches`, `errors`, `evictions`, `staleHits`).
388
435
  - `hitRate`: Ratio of hits to total requests (hits + misses).
389
436
  - `staleHitRate`: Ratio of stale hits to total hits.
390
- - `entries`: An array of objects providing details for each cached item (key, lastAccessed, lastUpdated, accessCount, isStale, isLoading, error).
437
+ - `entries`: An array of objects providing details for each cached item (key, lastAccessed, lastUpdated, accessCount, isStale, isLoading, error status).
391
438
 
392
439
  ```typescript
393
440
  const stats = myCache.getStats();
394
441
  console.log('Cache Size:', stats.size);
395
442
  console.log('Metrics:', stats.metrics);
396
- console.log('Hit Rate:', (stats.hitRate * 100).toFixed(2) + '%');
443
+ console.log('Overall Hit Rate:', (stats.hitRate * 100).toFixed(2) + '%');
444
+ console.log('Entries details:', stats.entries);
397
445
  ```
398
446
 
399
447
  #### `cache.clear(): Promise<void>`
400
448
 
401
- Clears all data from the in-memory cache and attempts to clear the persisted state.
449
+ Clears all data from the in-memory cache, resets metrics, and attempts to clear the associated persisted state via the `persistence` layer.
402
450
 
403
451
  ```typescript
452
+ console.log('Clearing cache...');
404
453
  await myCache.clear();
405
- console.log('Cache cleared.');
454
+ console.log('Cache cleared. Current size:', myCache.getStats().size);
406
455
  ```
407
456
 
408
457
  #### `cache.destroy(): void`
409
458
 
410
- Shuts down the cache instance, clearing all data, stopping garbage collection, and unsubscribing from persistence. Call this when the cache instance is no longer needed.
459
+ Shuts down the cache instance, clearing all data, stopping the automatic garbage collection timer, unsubscribing from persistence updates, and clearing all internal maps. Call this when the cache instance is no longer needed (e.g., on application shutdown or component unmount) to prevent memory leaks and ensure proper cleanup.
411
460
 
412
461
  ```typescript
413
462
  myCache.destroy();
414
- console.log('Cache instance destroyed.');
463
+ console.log('Cache instance destroyed. All timers stopped and data cleared.');
415
464
  ```
416
465
 
417
466
  ### Configuration Examples
418
467
 
419
- The `CacheOptions` interface provides extensive control:
468
+ The `CacheOptions` interface provides extensive control over the cache's behavior:
420
469
 
421
470
  ```typescript
422
- import { CacheOptions, SimplePersistence, SerializableCacheState } from '@ausaidimu/utils-cache';
471
+ import { CacheOptions, SimplePersistence, SerializableCacheState } from '@asaidimu/utils-cache';
423
472
 
424
- // A mock persistence layer for demonstration
473
+ // A mock persistence layer for demonstration purposes.
474
+ // In a real application, you'd use an actual implementation like IndexedDBPersistence.
425
475
  class MockPersistence implements SimplePersistence<SerializableCacheState> {
426
476
  private store = new Map<string, SerializableCacheState>();
427
477
  private subscribers = new Map<string, Array<(data: SerializableCacheState) => void>>();
428
478
 
429
479
  async get(id: string): Promise<SerializableCacheState | undefined> {
430
- console.log(`[MockPersistence] Getting state for ${id}`);
480
+ console.log(`[MockPersistence] Getting state for ID: ${id}`);
431
481
  return this.store.get(id);
432
482
  }
433
483
  async set(id: string, data: SerializableCacheState): Promise<void> {
434
- console.log(`[MockPersistence] Setting state for ${id}`);
484
+ console.log(`[MockPersistence] Setting state for ID: ${id}`);
435
485
  this.store.set(id, data);
436
- // Simulate remote update to subscribers
486
+ // Simulate remote update notification to all subscribed instances
437
487
  this.subscribers.get(id)?.forEach(cb => cb(data));
438
488
  }
439
489
  async clear(id?: string): Promise<void> {
440
- console.log(`[MockPersistence] Clearing state ${id ? 'for ' + id : 'all'}`);
490
+ console.log(`[MockPersistence] Clearing state ${id ? 'for ID: ' + id : '(all)'}`);
441
491
  if (id) {
442
492
  this.store.delete(id);
443
493
  } else {
@@ -445,44 +495,55 @@ class MockPersistence implements SimplePersistence<SerializableCacheState> {
445
495
  }
446
496
  }
447
497
  subscribe(id: string, callback: (data: SerializableCacheState) => void): () => void {
448
- console.log(`[MockPersistence] Subscribing to ${id}`);
498
+ console.log(`[MockPersistence] Subscribing to ID: ${id}`);
449
499
  if (!this.subscribers.has(id)) {
450
500
  this.subscribers.set(id, []);
451
501
  }
452
502
  this.subscribers.get(id)?.push(callback);
503
+ // Return unsubscribe function
453
504
  return () => {
454
505
  const callbacks = this.subscribers.get(id);
455
506
  if (callbacks) {
456
507
  this.subscribers.set(id, callbacks.filter(cb => cb !== callback));
457
508
  }
509
+ console.log(`[MockPersistence] Unsubscribed from ID: ${id}`);
458
510
  };
459
511
  }
460
512
  }
461
513
 
462
-
463
514
  const fullOptions: CacheOptions = {
464
- staleTime: 1000 * 60 * 5, // 5 minutes
465
- cacheTime: 1000 * 60 * 60, // 1 hour (items idle for this long are garbage collected)
466
- retryAttempts: 3, // Max 3 fetch attempts (initial + 2 retries)
467
- retryDelay: 1000, // 1 second initial delay for retries (doubles each attempt)
468
- maxSize: 2000, // Keep up to 2000 entries (LRU eviction kicks in beyond this)
469
- enableMetrics: true, // Enable performance tracking
470
- persistence: new MockPersistence(), // Provide an instance of your persistence layer
471
- persistenceId: 'my-unique-cache-instance', // Specific ID for this cache in persistence
472
- persistenceDebounceTime: 750, // Wait 750ms before writing to persistence
515
+ staleTime: 1000 * 60 * 5, // 5 minutes: After this time, data is stale; a background fetch is considered.
516
+ cacheTime: 1000 * 60 * 60, // 1 hour: Items idle (not accessed) for this long are eligible for garbage collection.
517
+ retryAttempts: 3, // Max 3 fetch attempts (initial + 2 retries) on network/fetch failures.
518
+ retryDelay: 1000, // 1 second initial delay for retries (doubles each subsequent attempt).
519
+ maxSize: 2000, // Keep up to 2000 entries; LRU eviction kicks in beyond this limit.
520
+ enableMetrics: true, // Enable performance tracking (hits, misses, fetches, etc.).
521
+ persistence: new MockPersistence(), // Provide an instance of your persistence layer implementation.
522
+ persistenceId: 'my-unique-cache-instance', // A unique identifier for this cache instance within the persistence store.
523
+ persistenceDebounceTime: 750, // Wait 750ms after a cache change before writing to persistence to batch writes.
524
+
525
+ // Custom serializers/deserializers for data that isn't natively JSON serializable (e.g., Maps, Dates, custom classes).
473
526
  serializeValue: (value: any) => {
474
527
  // Example: Convert Date objects to ISO strings for JSON serialization
475
528
  if (value instanceof Date) {
476
529
  return { _type: 'Date', data: value.toISOString() };
477
530
  }
478
- return value;
531
+ // Example: Convert Map objects to an array for JSON serialization
532
+ if (value instanceof Map) {
533
+ return { _type: 'Map', data: Array.from(value.entries()) };
534
+ }
535
+ return value; // Return as is for other types
479
536
  },
480
537
  deserializeValue: (value: any) => {
481
538
  // Example: Convert ISO strings back to Date objects
482
539
  if (typeof value === 'object' && value !== null && value._type === 'Date') {
483
540
  return new Date(value.data);
484
541
  }
485
- return value;
542
+ // Example: Convert array back to Map objects
543
+ if (typeof value === 'object' && value !== null && value._type === 'Map') {
544
+ return new Map(value.data);
545
+ }
546
+ return value; // Return as is for other types
486
547
  },
487
548
  };
488
549
 
@@ -491,81 +552,96 @@ const configuredCache = new Cache(fullOptions);
491
552
 
492
553
  ### Common Use Cases
493
554
 
494
- #### Caching API Responses with SWR
555
+ #### Caching API Responses with SWR (Stale-While-Revalidate)
556
+
557
+ This is the default and most common pattern, where you prioritize immediate responsiveness while ensuring data freshness in the background.
495
558
 
496
559
  ```typescript
497
- import { Cache } from '@ausaidimu/utils-cache';
560
+ import { Cache } from '@asaidimu/utils-cache';
498
561
 
499
562
  const apiCache = new Cache({
500
- staleTime: 5 * 60 * 1000, // 5 min before data is considered stale
501
- cacheTime: 30 * 60 * 1000, // 30 min before idle data is garbage collected
502
- retryAttempts: 3,
563
+ staleTime: 5 * 60 * 1000, // Data considered stale after 5 minutes
564
+ cacheTime: 30 * 60 * 1000, // Idle data garbage collected after 30 minutes
565
+ retryAttempts: 3, // Retry fetching on network failures
503
566
  });
504
567
 
568
+ // Register a query for a list of blog posts
505
569
  apiCache.registerQuery('blog/posts', async () => {
506
- console.log('Fetching ALL blog posts from API...');
570
+ console.log('--- Fetching ALL blog posts from API... ---');
507
571
  const response = await fetch('https://api.example.com/blog/posts');
508
572
  if (!response.ok) throw new Error('Failed to fetch blog posts');
509
573
  return response.json();
510
574
  });
511
575
 
512
- // User opens blog page: get posts instantly, refresh if needed
513
- async function displayBlogPosts() {
514
- const posts = await apiCache.get('blog/posts');
576
+ // Function to display blog posts
577
+ async function displayBlogPosts(source: string) {
578
+ console.log(`\nDisplaying blog posts from: ${source}`);
579
+ const posts = await apiCache.get('blog/posts'); // Uses SWR by default
515
580
  if (posts) {
516
- console.log('Displaying posts (from cache or new fetch):', posts.slice(0, 2));
581
+ console.log(`Received ${posts.length} posts (first 2):`, posts.slice(0, 2).map((p: any) => p.title));
517
582
  } else {
518
- console.log('No posts yet, waiting for fetch...');
583
+ console.log('No posts yet, waiting for initial fetch...');
519
584
  }
520
585
  }
521
586
 
522
- displayBlogPosts(); // First load
523
- setTimeout(() => displayBlogPosts(), 2000); // Subsequent fast load from cache
524
- setTimeout(() => displayBlogPosts(), 301000); // After staleTime, returns cached data, triggers background fetch
587
+ displayBlogPosts('Initial Load'); // First `get`: cache miss, triggers fetch.
588
+ setTimeout(() => displayBlogPosts('After 1 sec (cached)'), 1000); // Second `get`: cache hit, returns instantly.
589
+ setTimeout(() => displayBlogPosts('After 6 mins (stale & background fetch)'), 6 * 60 * 1000); // After `staleTime`: returns cached, triggers background fetch.
525
590
  ```
526
591
 
527
592
  #### Using `waitForFresh` for Critical Data
528
593
 
594
+ For scenarios where serving outdated data is unacceptable (e.g., user permissions, critical configuration).
595
+
529
596
  ```typescript
530
- import { Cache } from '@ausaidimu/utils-cache';
597
+ import { Cache } from '@asaidimu/utils-cache';
531
598
 
532
599
  const criticalCache = new Cache({ retryAttempts: 5, retryDelay: 1000 });
533
600
 
534
601
  criticalCache.registerQuery('user/permissions', async () => {
535
- console.log('Fetching user permissions...');
536
- const response = await fetch('/api/user/permissions');
537
- if (!response.ok) throw new Error('Failed to get permissions!');
538
- return response.json();
602
+ console.log('--- Fetching user permissions from API... ---');
603
+ // Simulate potential network flakiness
604
+ if (Math.random() > 0.7) {
605
+ throw new Error('Network error during permission fetch!');
606
+ }
607
+ await new Promise(resolve => setTimeout(resolve, 500));
608
+ return { canEdit: true, canDelete: false, roles: ['user', 'editor'] };
539
609
  });
540
610
 
541
611
  async function checkPermissionsBeforeAction() {
612
+ console.log('\nAttempting to get FRESH user permissions...');
542
613
  try {
543
- // We MUST have the latest permissions before proceeding
614
+ // We MUST have the latest permissions before proceeding with a sensitive action
544
615
  const permissions = await criticalCache.get('user/permissions', { waitForFresh: true, throwOnError: true });
545
- console.log('User permissions:', permissions);
616
+ console.log('User permissions received:', permissions);
546
617
  // Proceed with action based on permissions
547
618
  } catch (error) {
548
- console.error('Failed to load critical permissions:', error);
549
- // Redirect to error page or show alert
619
+ console.error('CRITICAL: Failed to load user permissions:', error);
620
+ // Redirect to error page, show critical alert, or disable functionality
550
621
  }
551
622
  }
552
623
 
553
624
  checkPermissionsBeforeAction();
625
+ // You might call this repeatedly in a test scenario to see retries and eventual success/failure
626
+ setInterval(() => checkPermissionsBeforeAction(), 3000);
554
627
  ```
555
628
 
556
629
  #### Real-time Monitoring with Events
557
630
 
631
+ Utilize the comprehensive event system to log, monitor, or react to cache lifecycle events.
632
+
558
633
  ```typescript
559
- import { Cache } from '@ausaidimu/utils-cache';
634
+ import { Cache } from '@asaidimu/utils-cache';
560
635
 
561
636
  const monitorCache = new Cache({ enableMetrics: true });
562
637
 
563
638
  monitorCache.registerQuery('stock/AAPL', async () => {
564
639
  const price = Math.random() * 100 + 150;
565
- console.log(`Fetching AAPL price: $${price.toFixed(2)}`);
640
+ console.log(`--- Fetching AAPL price: $${price.toFixed(2)} ---`);
566
641
  return { symbol: 'AAPL', price: parseFloat(price.toFixed(2)), timestamp: Date.now() };
567
642
  }, { staleTime: 1000 }); // Very short staleTime for frequent fetches
568
643
 
644
+ // Subscribe to various cache events
569
645
  monitorCache.on('fetch', (e) => {
570
646
  console.log(`[EVENT] Fetching ${e.key} (attempt ${e.attempt})`);
571
647
  });
@@ -579,20 +655,31 @@ monitorCache.on('eviction', (e) => {
579
655
  console.log(`[EVENT] Evicted ${e.key} due to ${e.reason}`);
580
656
  });
581
657
  monitorCache.on('set_data', (e) => {
582
- console.log(`[EVENT] Data for ${e.key} manually set. Old: ${e.oldData?.price}, New: ${e.newData.price}`);
658
+ console.log(`[EVENT] Data for ${e.key} manually set. Old price: ${e.oldData?.price}, New price: ${e.newData.price}`);
659
+ });
660
+ monitorCache.on('persistence', (e) => {
661
+ if (e.event === 'save_success') console.log(`[EVENT] Persistence: ${e.message || 'Save successful'}`);
583
662
  });
584
663
 
664
+
665
+ // Continuously try to get data (will trigger fetches due to short staleTime)
585
666
  setInterval(() => {
586
667
  monitorCache.get('stock/AAPL');
587
- }, 500); // Continuously try to get data
668
+ }, 500);
669
+
670
+ // Manually set data to trigger 'set_data' and 'persistence' events
671
+ setTimeout(() => {
672
+ monitorCache.setData('stock/AAPL', { symbol: 'AAPL', price: 160.00, timestamp: Date.now() });
673
+ }, 3000);
588
674
 
675
+ // Log cache statistics periodically
589
676
  setInterval(() => {
590
677
  const stats = monitorCache.getStats();
591
- console.log(`\n--- STATS ---`);
678
+ console.log(`\n--- CACHE STATS ---`);
592
679
  console.log(`Size: ${stats.size}, Hits: ${stats.metrics.hits}, Misses: ${stats.metrics.misses}, Fetches: ${stats.metrics.fetches}`);
593
680
  console.log(`Hit Rate: ${(stats.hitRate * 100).toFixed(2)}%, Stale Hit Rate: ${(stats.staleHitRate * 100).toFixed(2)}%`);
594
681
  console.log(`Active entries: ${stats.entries.map(e => `${e.key} (stale:${e.isStale})`).join(', ')}`);
595
- console.log(`---\n`);
682
+ console.log(`-------------------\n`);
596
683
  }, 5000); // Log stats every 5 seconds
597
684
  ```
598
685
 
@@ -600,117 +687,173 @@ setInterval(() => {
600
687
 
601
688
  ## 🏗️ Project Architecture
602
689
 
690
+ The `@asaidimu/utils-cache` library is structured to provide a clear separation of concerns, making it modular, testable, and extensible.
691
+
692
+ ### Directory Structure
693
+
694
+ ```
695
+ src/cache/
696
+ ├── cache.ts # Main Cache class implementation
697
+ ├── index.ts # Entry point for the module (re-exports Cache)
698
+ ├── types.ts # TypeScript interfaces and types for options, entries, events, etc.
699
+ └── cache.test.ts # Unit tests for the Cache class
700
+ package.json # Package metadata and dependencies for this specific module
701
+ ```
702
+
603
703
  ### Core Components
604
704
 
605
- * **`Cache` Class (`index.ts`)**: The primary entry point. Manages the in-memory `Map` (`this.cache`), handles fetching, retries, staleness, garbage collection, persistence, and events.
606
- * **`CacheOptions` (`types.ts`)**: Defines the configuration parameters for `Cache` instances and individual queries.
607
- * **`CacheEntry` (`types.ts`)**: Represents a single item stored in the cache, including its data, timestamps, access count, and loading/error status.
608
- * **`QueryConfig` (`types.ts`)**: Stores the `fetchFunction` and resolved options for each registered query.
609
- * **`CacheMetrics` (`types.ts`)**: Defines the structure for tracking cache performance statistics.
610
- * **`SimplePersistence<SerializableCacheState>` (from `@core/persistence/types`)**: An external interface that `Cache` uses for persistent storage. It requires `get()`, `set()`, `clear()`, and optionally `subscribe()` methods to handle data serialization and deserialization for the storage medium.
611
- * **`CacheEvent` / `CacheEventType` (`types.ts`)**: Defines the union of all possible events emitted by the cache, enabling fine-grained observability.
705
+ * **`Cache` Class (`cache.ts`)**: The central component of the library. It orchestrates all caching logic, including:
706
+ * Managing the in-memory `Map` (`this.cache`) that stores `CacheEntry` objects.
707
+ * Handling data fetching, retries, and staleness checks.
708
+ * Implementing time-based (TTL) and size-based (LRU) garbage collection.
709
+ * Integrating with the pluggable persistence layer.
710
+ * Emitting detailed cache events.
711
+ * Tracking performance metrics.
712
+ * **`CacheOptions` (`types.ts`)**: An interface defining the configurable parameters for a `Cache` instance or individual queries. This includes `staleTime`, `cacheTime`, `retryAttempts`, `maxSize`, persistence settings, and custom serialization/deserialization functions.
713
+ * **`CacheEntry` (`types.ts`)**: Represents a single item stored within the cache. It encapsulates the actual `data`, `lastUpdated` and `lastAccessed` timestamps, `accessCount`, and flags like `isLoading` or `error` status.
714
+ * **`QueryConfig` (`types.ts`)**: Stores the `fetchFunction` and the resolved `CacheOptions` (merged with instance defaults) for each registered query, enabling tailored behavior per data key.
715
+ * **`CacheMetrics` (`types.ts`)**: Defines the structure for tracking cache performance statistics, including hits, misses, fetches, errors, and evictions.
716
+ * **`SimplePersistence<SerializableCacheState>` (from `@asaidimu/utils-persistence`)**: An external interface that `Cache` relies on for persistent storage. It requires implementations of `get()`, `set()`, `clear()`, and optionally `subscribe()` methods to handle data serialization and deserialization for the specific storage medium (e.g., IndexedDB, LocalStorage, or a remote backend).
717
+ * **`CacheEvent` / `CacheEventType` (`types.ts`)**: A union type defining all possible events emitted by the cache (e.g., `'hit'`, `'miss'`, `'fetch'`, `'error'`, `'eviction'`, `'invalidation'`, `'set_data'`, `'persistence'`). This enables a fine-grained observability model for the cache's lifecycle.
612
718
 
613
719
  ### Data Flow
614
720
 
615
- 1. **Initialization**: `Cache` constructor sets default options, initializes metrics, starts garbage collection, and attempts to load state from the configured `persistence` layer. It also subscribes to remote updates from persistence if available.
616
- 2. **`registerQuery`**: Stores a `fetchFunction` and its specific `CacheOptions` (merged with defaults) in a `queries` map.
721
+ 1. **Initialization**:
722
+ * The `Cache` constructor sets up global default options, initializes performance metrics, and starts the automatic garbage collection timer.
723
+ * If a `persistence` layer is configured, it attempts to load a previously saved state using `persistence.get()`.
724
+ * It then subscribes to `persistence.subscribe()` (if available) to listen for remote state changes from the underlying storage, ensuring cache consistency across multiple instances or processes.
725
+
726
+ 2. **`registerQuery`**:
727
+ * When `registerQuery(key, fetchFunction, options)` is called, the `fetchFunction` and its specific `options` (merged with the global `defaultOptions`) are stored internally in the `this.queries` map. This prepares the cache to handle requests for that `key`.
728
+
617
729
  3. **`get` Request**:
618
- * Checks `this.cache` for the key.
619
- * If found, updates `lastAccessed`, increments `accessCount`, emits `hit` event.
620
- * Checks `isStale` based on `staleTime`.
621
- * If `waitForFresh` is `true` OR if `isStale` / `cacheEntry` is loading/empty, it calls `fetchAndWait`.
622
- * Otherwise, if `isStale`, it triggers `fetch` in the background.
623
- * If no entry, emits `miss` event and creates a placeholder, then triggers `fetch`.
624
- * Returns cached data (if available and not `waitForFresh`) or `undefined` (if background fetch).
730
+ * When `get(key, options)` is invoked, `Cache` first checks `this.cache` for an existing `CacheEntry` for the `key`.
731
+ * **Cache Hit**: If an entry exists, `lastAccessed` and `accessCount` are updated, a `'hit'` event is emitted, and metrics are incremented. The entry's staleness is evaluated based on `staleTime`.
732
+ * If `waitForFresh` is `true` OR if the entry is stale/loading, it proceeds to `fetchAndWait`.
733
+ * If `waitForFresh` is `false` (default) and the entry is stale, the cached data is returned immediately, and a background `fetch` is triggered to update the data.
734
+ * If `waitForFresh` is `false` and the entry is fresh, the cached data is returned immediately.
735
+ * **Cache Miss**: If no entry exists, a `'miss'` event is emitted. A placeholder `CacheEntry` (marked `isLoading`) is created, and a `fetch` is immediately triggered to retrieve the data.
736
+
625
737
  4. **`fetch` / `fetchAndWait`**:
626
- * Checks `this.fetching` to avoid concurrent fetches for the same key.
627
- * Calls `performFetchWithRetry`.
738
+ * These methods ensure that only one `fetchFunction` runs concurrently for a given `key` by tracking ongoing fetches in `this.fetching`.
739
+ * They delegate the actual data retrieval and retry logic to `performFetchWithRetry`.
740
+
628
741
  5. **`performFetchWithRetry`**:
629
- * Iteratively calls the registered `fetchFunction`.
630
- * Emits `fetch` event before each attempt.
631
- * On success: updates `cacheEntry` with `data`, `lastUpdated`, `isLoading: false`, `error: undefined`. Schedules `schedulePersistState`. Enforces `enforceSizeLimit`. Returns `data`.
632
- * On failure: emits `error` event. Retries after `retryDelay` (with exponential backoff). After all attempts, updates `cacheEntry` with `error` and `isLoading: false`. Schedules `schedulePersistState`. Returns `undefined`.
633
- 6. **`schedulePersistState`**: Debounces persistence writes. When triggered, `serializeCache` transforms the current cache state into a `SerializableCacheState` (applying `serializeValue`), which is then written to the `persistence` layer. Emits `persistence` events.
634
- 7. **`handleRemoteStateChange`**: When the `persistence` layer notifies of a remote update, this method deserializes the state (applying `deserializeValue`) and updates `this.cache`, ensuring local cache consistency with external changes.
635
- 8. **`garbageCollect`**: Periodically (via `gcTimer`) iterates through `this.cache` and removes entries that have not been accessed for `cacheTime`. Emits `eviction` events.
636
- 9. **`enforceSizeLimit`**: Triggered after successful fetches or `setData`. If `maxSize` is exceeded, evicts `LRU` entries. Emits `eviction` events.
742
+ * This is where the registered `fetchFunction` is executed. It attempts to call the `fetchFunction` multiple times (up to `retryAttempts`) with exponential backoff (`retryDelay`).
743
+ * Before each attempt, a `'fetch'` event is emitted, and `fetches` metrics are updated.
744
+ * **On Success**: The `CacheEntry` is updated with the new `data`, `lastUpdated` timestamp, and its `isLoading` status is set to `false`. The cache then calls `schedulePersistState()` to save the updated state and `enforceSizeLimit()` to maintain the `maxSize`.
745
+ * **On Failure**: If the `fetchFunction` fails, an `'error'` event is emitted, and `errors` metrics are updated. If `retryAttempts` are remaining, it waits (`delay`) and retries. After all attempts, the `CacheEntry` is updated with the last `error`, `isLoading` is set to `false`, and `schedulePersistState()` is called.
746
+
747
+ 6. **`schedulePersistState`**:
748
+ * This method debounces write operations to the `persistence` layer. It prevents excessive writes by waiting for a configurable `persistenceDebounceTime` before serializing the current cache state (using `serializeCache` and `serializeValue`) and writing it via `persistence.set()`. Appropriate `'persistence'` events (`save_success`/`save_fail`) are emitted.
749
+
750
+ 7. **`handleRemoteStateChange`**:
751
+ * This callback is invoked by the `persistence` layer's `subscribe` mechanism when an external change to the persisted state is detected. It deserializes the `remoteState` (using `deserializeValue`) and intelligently updates the local `this.cache` to reflect these external changes, emitting a `'persistence'` event (`remote_update`).
752
+
753
+ 8. **`garbageCollect`**:
754
+ * Running on a `setInterval` timer (`gcTimer`), this method periodically scans `this.cache`. It removes any `CacheEntry` that has not been `lastAccessed` for longer than its (or global) `cacheTime`, emitting `'eviction'` events.
755
+
756
+ 9. **`enforceSizeLimit`**:
757
+ * Triggered after successful data updates (`fetch` success or `setData`). If the `cache.size` exceeds `maxSize`, it evicts the Least Recently Used (LRU) entries until the `maxSize` is satisfied, emitting `'eviction'` events.
637
758
 
638
759
  ### Extension Points
639
760
 
640
- * **`SimplePersistence` Interface**: The primary extension point for integrating `Cache` with various storage backends. Implement this interface to use `SimpleCache` with `localStorage`, `IndexedDB`, a database, or any other persistent storage.
641
- * **`serializeValue` / `deserializeValue` Options**: Customize how cache entry data is converted to and from a serializable format (e.g., handling `Date` objects, `Map`s, custom classes) before interacting with the persistence layer.
642
- * **Event Listeners (`on`/`off`)**: Integrate `Cache` events with your application's logging, analytics, UI reactivity, or debugging tools.
761
+ The design of `@asaidimu/utils-cache` provides several powerful extension points for customization and integration:
762
+
763
+ * **`SimplePersistence` Interface**: This is the primary mechanism for integrating `Cache` with various storage backends. By implementing this interface, you can use `Cache` with `localStorage`, `IndexedDB` (e.g., via `@asaidimu/utils-persistence`), a custom database, a server-side cache, or any other persistent storage solution.
764
+ * **`serializeValue` / `deserializeValue` Options**: These functions within `CacheOptions` allow you to define custom logic for how your specific data types are converted to and from a serializable format (e.g., JSON-compatible strings or objects) before being passed to and received from the `persistence` layer. This is crucial for handling `Date` objects, `Map`s, `Set`s, or custom class instances.
765
+ * **Event Listeners (`on`/`off`)**: The comprehensive event system allows you to subscribe to a wide range of cache lifecycle events. This enables powerful integrations for:
766
+ * **Logging**: Detailed logging of cache activity (hits, misses, errors, evictions).
767
+ * **Analytics**: Feeding cache performance metrics into an analytics platform.
768
+ * **UI Reactivity**: Updating UI components in response to cache changes (e.g., showing a "stale data" indicator or a "refreshing" spinner).
769
+ * **Debugging**: Gaining deep insights into cache behavior during development.
770
+ * **External Synchronization**: Triggering side effects or synchronizing with other systems based on cache events.
643
771
 
644
772
  ---
645
773
 
646
774
  ## 🤝 Development & Contributing
647
775
 
776
+ We welcome contributions to `@asaidimu/utils-cache`! Whether it's a bug fix, a new feature, or an improvement to the documentation, your help is appreciated.
777
+
648
778
  ### Development Setup
649
779
 
650
- To set up the development environment:
780
+ To set up the development environment for `@asaidimu/utils-cache`:
651
781
 
652
- 1. **Clone the repository:**
782
+ 1. **Clone the monorepo:**
783
+ ```bash
784
+ git clone https://github.com/asaidimu/erp-utils.git
785
+ cd erp-utils
786
+ ```
787
+ 2. **Navigate to the cache package:**
653
788
  ```bash
654
- git clone https://github.com/ausaidimu/utils.git
655
- cd utils-src/cache
789
+ cd src/cache
656
790
  ```
657
- 2. **Install dependencies:**
791
+ 3. **Install dependencies:**
658
792
  ```bash
659
793
  npm install
660
794
  # or
661
795
  yarn install
796
+ # or
797
+ bun install
662
798
  ```
663
- 3. **Build the project:**
799
+ 4. **Build the project:**
664
800
  ```bash
665
801
  npm run build
666
802
  # or
667
803
  yarn build
804
+ # or
805
+ bun run build
668
806
  ```
669
807
 
670
808
  ### Scripts
671
809
 
672
- The following `npm` scripts are available:
810
+ The following `npm` scripts are typically available in this project's setup:
673
811
 
674
- * `npm run build`: Compiles TypeScript source files to JavaScript.
675
- * `npm run test`: Runs the test suite.
676
- * `npm run lint`: Runs ESLint to check for code style issues.
677
- * `npm run format`: Formats code using Prettier.
812
+ * `npm run build`: Compiles TypeScript source files from `src/` to JavaScript output in `dist/`.
813
+ * `npm run test`: Runs the test suite using `Vitest`.
814
+ * `npm run test:watch`: Runs tests in watch mode for continuous feedback during development.
815
+ * `npm run lint`: Runs ESLint to check for code style and potential errors.
816
+ * `npm run format`: Formats code using Prettier according to the project's style guidelines.
678
817
 
679
818
  ### Testing
680
819
 
681
- Tests are written using `[Your Testing Framework, e.g., Jest]`. To run tests:
820
+ Tests are written using **Vitest**. To run tests:
682
821
 
683
822
  ```bash
684
823
  npm test
685
824
  # or
686
825
  yarn test
826
+ # or
827
+ bun test
687
828
  ```
688
829
 
689
- We aim for high test coverage. Please ensure new features or bug fixes come with appropriate tests.
830
+ We aim for high test coverage. Please ensure that new features or bug fixes come with appropriate unit and/or integration tests to maintain code quality and prevent regressions.
690
831
 
691
832
  ### Contributing Guidelines
692
833
 
693
- We welcome contributions! Please follow these steps:
834
+ Please follow these steps to contribute:
694
835
 
695
- 1. Fork the repository.
696
- 2. Create a new branch for your feature or bug fix: `git checkout -b feature/my-new-feature` or `bugfix/fix-issue-123`.
697
- 3. Make your changes, ensuring they adhere to the existing code style.
698
- 4. Write or update tests to cover your changes.
699
- 5. Ensure all tests pass (`npm test`).
700
- 6. Run lint and format checks (`npm run lint`, `npm run format`).
701
- 7. Write clear, concise commit messages following the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification.
702
- 8. Push your branch and open a Pull Request to the `main` branch.
836
+ 1. **Fork the repository** on GitHub.
837
+ 2. **Create a new branch** for your feature or bug fix: `git checkout -b feature/my-awesome-feature` or `bugfix/resolve-issue-123`.
838
+ 3. **Make your changes**, ensuring they adhere to the existing code style and architecture.
839
+ 4. **Write or update tests** to cover your changes and ensure existing functionality is not broken.
840
+ 5. **Ensure all tests pass** locally by running `npm test`.
841
+ 6. **Run lint and format checks** (`npm run lint` and `npm run format`) and fix any reported issues.
842
+ 7. **Write clear, concise commit messages** following the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification (e.g., `feat: add new caching strategy`, `fix: correct staleTime calculation`).
843
+ 8. **Push your branch** to your fork.
844
+ 9. **Open a Pull Request** to the `main` branch of the original repository. Provide a detailed description of your changes and why they are necessary.
703
845
 
704
846
  ### Issue Reporting
705
847
 
706
- Found a bug or have a feature request? Please open an issue on our [GitHub Issues page](https://github.com/ausaidimu/utils-issues).
848
+ Found a bug, have a feature request, or need clarification? Please open an issue on our [GitHub Issues page](https://github.com/asaidimu/erp-utils/issues).
849
+
707
850
  When reporting a bug, please include:
708
851
 
709
852
  * A clear and concise description of the issue.
710
- * Steps to reproduce the behavior.
711
- * Expected behavior.
712
- * Screenshots or code snippets if applicable.
713
- * Your environment details (Node.js version, OS, browser).
853
+ * Detailed steps to reproduce the behavior.
854
+ * The expected behavior.
855
+ * Any relevant screenshots or code snippets.
856
+ * Your environment details (Node.js version, OS, browser, package version).
714
857
 
715
858
  ---
716
859
 
@@ -718,30 +861,47 @@ When reporting a bug, please include:
718
861
 
719
862
  ### Troubleshooting
720
863
 
721
- * **"No query registered for key: [key]" Error**: This means you tried to `get`, `prefetch`, or `refresh` a key that was not previously registered using `registerQuery`. Ensure you register all keys you intend to use.
864
+ * **"No query registered for key: [key]" Error**:
865
+ * **Cause**: This error occurs if you try to `get()`, `prefetch()`, or `refresh()` a `key` that has not been previously associated with a `fetchFunction` using `cache.registerQuery()`.
866
+ * **Solution**: Ensure you call `cache.registerQuery(key, fetchFunction)` for every `key` you intend to use with the cache before attempting to retrieve data.
722
867
  * **Data not persisting**:
723
- * Double-check that you passed a valid `persistence` instance to the `Cache` constructor.
724
- * Verify your `SimplePersistence` implementation correctly saves and loads data.
725
- * Ensure your data is properly serializable to JSON (or use `serializeValue`/`deserializeValue` options for complex types).
726
- * Check for `persistence` event errors in your console.
868
+ * **Cause**: The cache state is not being correctly saved or loaded from the underlying storage.
869
+ * **Solution**:
870
+ 1. **`persistence` instance**: Double-check that you are passing a valid `SimplePersistence` instance to the `Cache` constructor's `persistence` option.
871
+ 2. **`persistenceId`**: Ensure you've provided a unique `persistenceId` if multiple cache instances share the same persistence layer.
872
+ 3. **Serialization**: Verify that your data types are correctly handled by `serializeValue` and `deserializeValue` options, especially for non-JSON-serializable types like `Map`s, `Date` objects, or custom classes.
873
+ 4. **Persistence Layer**: Confirm your `SimplePersistence` implementation correctly handles `get()`, `set()`, `clear()`, and `subscribe()` operations for the specific storage medium (e.g., local storage quota, IndexedDB permissions).
874
+ 5. **Event Errors**: Check for `persistence` event errors in your browser's or Node.js console (`cache.on('persistence', ...)`).
727
875
  * **Cache not evicting data**:
728
- * Ensure `cacheTime` and `maxSize` are set to finite, non-zero values in `CacheOptions`. `Infinity` or `0` for `cacheTime` disables time-based GC.
729
- * Verify the garbage collection interval is not too long (it's `cacheTime / 4` or `5 minutes` max).
730
- * **Listeners not firing**: Ensure you're subscribing to the correct `CacheEventType` and that the cache operation (e.g., a `hit` or `error`) is actually occurring. Check `enableMetrics` isn't `false` if expecting metric-related events/updates.
876
+ * **Cause**: Eviction policies might be disabled or configured with very long durations.
877
+ * **Solution**:
878
+ 1. **`cacheTime`**: Ensure `cacheTime` in `CacheOptions` is set to a finite, non-zero positive number (in milliseconds). `Infinity` or `0` for `cacheTime` disables time-based garbage collection.
879
+ 2. **`maxSize`**: Ensure `maxSize` is set to a finite, non-zero positive number. `Infinity` disables size-based LRU eviction, and `0` means the cache will always be empty (evicting immediately).
880
+ 3. **Garbage Collection Interval**: The garbage collection runs periodically. While generally sufficient, verify that `cacheTime` isn't so large that you rarely hit the GC interval.
881
+ * **Event listeners not firing**:
882
+ * **Cause**: The listener might be removed, or the expected event is not actually occurring.
883
+ * **Solution**:
884
+ 1. **Correct Event Type**: Ensure you are subscribing to the exact `CacheEventType` you expect (e.g., `'hit'`, `'error'`).
885
+ 2. **`enableMetrics`**: If you expect metric-related events or updates, ensure `enableMetrics` is not set to `false` in your `CacheOptions`.
886
+ 3. **Listener Reference**: When using `off()`, ensure the listener function is the exact same reference passed to `on()`.
731
887
 
732
888
  ### FAQ
733
889
 
734
890
  **Q: How does `staleTime` differ from `cacheTime`?**
735
- A: `staleTime` determines when data is considered "stale" and a background refetch should be triggered. You can still use stale data. `cacheTime` determines how long an item can remain unaccessed before it's eligible for garbage collection and removal from the cache. An item can be stale but still within its `cacheTime`.
891
+ A: `staleTime` determines when a cached data entry is considered "stale." Once `Date.now() - entry.lastUpdated` exceeds `staleTime`, the data is marked stale. If `waitForFresh` is `false` (default), `get()` will return the stale data immediately while triggering a background refetch. `cacheTime`, on the other hand, determines how long an item can remain unaccessed (idle) before it's eligible for garbage collection and removal from the cache. An item can be stale but still within its `cacheTime`.
736
892
 
737
893
  **Q: When should I use `waitForFresh`?**
738
- A: Use `waitForFresh: true` when your application absolutely needs the most up-to-date data before proceeding, and cannot tolerate serving stale or old data. This will block execution until the `fetchFunction` resolves. For most UI display purposes where latency is critical, `waitForFresh: false` (the default SWR behavior) is usually preferred.
894
+ A: Use `waitForFresh: true` when your application absolutely needs the most up-to-date data before proceeding, and cannot tolerate serving stale or old data. This will block execution until the `fetchFunction` successfully resolves. For most UI display purposes where latency is critical and a slightly outdated display is acceptable, `waitForFresh: false` (the default SWR behavior) is usually preferred, as it provides an immediate response.
739
895
 
740
896
  **Q: Can I use `Cache` in a web worker?**
741
- A: Yes, `Cache` is designed to be environment-agnostic. Its persistence mechanism is pluggable, so you can implement a `SimplePersistence` that works within a web worker (e.g., using IndexedDB directly or `postMessage` to the main thread).
897
+ A: Yes, `Cache` is designed to be environment-agnostic. Its persistence mechanism is pluggable, so you can implement a `SimplePersistence` that works within a web worker (e.g., using IndexedDB directly or communicating with the main thread via `postMessage`).
742
898
 
743
899
  **Q: Is `Cache` thread-safe (or safe with concurrent access)?**
744
- A: `Cache` manages internal state with `Map`s and `Promise`s, which are atomic operations in JavaScript's single-threaded execution model. For concurrent `get` requests to the same key, it ensures only one `fetchFunction` runs via `this.fetching` map. It is safe for concurrent access within a single JavaScript runtime.
900
+ A: JavaScript is single-threaded. `Cache` manages its internal state with `Map`s and `Promise`s. For concurrent `get` requests to the *same key*, it ensures only one `fetchFunction` runs via the `this.fetching` map, preventing redundant fetches. Therefore, it is safe for concurrent access within a single JavaScript runtime context. For multiple JavaScript runtimes (e.g., different browser tabs or Node.js processes), the `persistence` layer's `subscribe` mechanism handles synchronization.
901
+
902
+ ### Changelog/Roadmap
903
+
904
+ For a detailed history of changes, new features, and bug fixes, please refer to the [CHANGELOG.md](CHANGELOG.md) file in the repository root. Our future plans are outlined in the [ROADMAP.md](ROADMAP.md) (if available).
745
905
 
746
906
  ### License
747
907
 
@@ -749,5 +909,5 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
749
909
 
750
910
  ### Acknowledgments
751
911
 
752
- * Inspired by modern caching libraries like React Query and SWR.
753
- * Uses `uuid` for generating unique cache IDs.
912
+ * Inspired by modern data fetching and caching libraries like [React Query](https://react-query.tanstack.com/) and [SWR](https://swr.vercel.app/).
913
+ * Uses the `uuid` library for generating unique cache instance IDs.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asaidimu/utils-cache",
3
- "version": "1.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "Caching utilities for @asaidimu applications.",
5
5
  "main": "index.js",
6
6
  "module": "index.mjs",
@@ -29,7 +29,7 @@
29
29
  "access": "public"
30
30
  },
31
31
  "dependencies": {
32
- "@asaidimu/utils-persistence": "^0.0.0",
32
+ "@asaidimu/utils-persistence": "2.0.0",
33
33
  "uuid": "^11.1.0"
34
34
  },
35
35
  "exports": {
@@ -49,7 +49,7 @@
49
49
  [
50
50
  "@semantic-release/npm",
51
51
  {
52
- "pkgRoot": "../../dist/cache"
52
+ "pkgRoot": "./dist"
53
53
  }
54
54
  ],
55
55
  [