@asaidimu/utils-cache 2.0.5 → 2.1.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.
package/README.md CHANGED
@@ -64,7 +64,7 @@ Unlike simpler caches, `Cache` manages data freshness intelligently, allowing yo
64
64
  * **Configurable Eviction Policies**:
65
65
  * **Time-Based (TTL)**: Automatically evicts entries that haven't been accessed for a specified `cacheTime`, managing memory efficiently.
66
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.
67
+ * **Comprehensive Event System**: Subscribe to granular, scoped cache events (e.g., `'cache:read:hit'`, `'cache:fetch:start'`, `'cache:data:set'`) for real-time logging, debugging, analytics, and advanced reactivity. Wildcard subscriptions (e.g., `'cache:read:*'`) are supported for capturing related events.
68
68
  * **Performance Metrics**: Built-in tracking for `hits`, `misses`, `fetches`, `errors`, `evictions`, and `staleHits`, providing insights into cache efficiency with calculated hit rates.
69
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
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.
@@ -81,14 +81,14 @@ Unlike simpler caches, `Cache` manages data freshness intelligently, allowing yo
81
81
 
82
82
  ### Installation Steps
83
83
 
84
- Install `@asaidimu/utils-cache` using your preferred package manager:
84
+ Install `@asaidimu/utils-cache` and its peer dependency `@asaidimu/events`:
85
85
 
86
86
  ```bash
87
- bun add @asaidimu/utils-cache
87
+ bun add @asaidimu/utils-cache @asaidimu/events
88
88
  # or
89
- npm install @asaidimu/utils-cache
89
+ npm install @asaidimu/utils-cache @asaidimu/events
90
90
  # or
91
- yarn add @asaidimu/utils-cache
91
+ yarn add @asaidimu/utils-cache @asaidimu/events
92
92
  ```
93
93
 
94
94
  ### Configuration
@@ -368,11 +368,11 @@ Removes a specific entry from the cache. Returns `true` if an entry was found an
368
368
  cache.remove('user/session');
369
369
  ```
370
370
 
371
- #### `cache.on<EType extends CacheEventType>(event: EType, listener: (ev: Extract<CacheEvent, { type: EType }>) => void): void`
371
+ #### `cache.on<EType extends CacheEventType>(event: EType, listener: (ev: Extract<CacheEvent, { type: EType }>) => void): () => void`
372
372
 
373
- Subscribes a listener function to specific cache events.
373
+ Subscribes a listener function to specific cache events. Returns an `unsubscribe` function.
374
374
 
375
- - `event`: The type of event to listen for (e.g., `'hit'`, `'miss'`, `'error'`, `'persistence'`). See `CacheEventType` in `types.ts` for all available types.
375
+ - `event`: The type of event to listen for (e.g., `'cache:read:hit'`, `'cache:fetch:error'`). Wildcards like `'cache:read:*'` are supported. See `CacheEventType` in `types.ts` for all available types.
376
376
  - `listener`: A callback function that receives the specific event payload for the subscribed event type.
377
377
 
378
378
  ```typescript
@@ -380,26 +380,24 @@ import { Cache, CacheEvent, CacheEventType } from '@asaidimu/utils-cache';
380
380
 
381
381
  const myCache = new Cache();
382
382
 
383
- myCache.on('hit', (e) => {
383
+ const unsubscribeHit = myCache.on('cache:read:hit', (e) => {
384
384
  console.log(`[CacheEvent] HIT for ${e.key} (isStale: ${e.isStale})`);
385
385
  });
386
386
 
387
- myCache.on('miss', (e) => {
387
+ myCache.on('cache:read:miss', (e) => {
388
388
  console.log(`[CacheEvent] MISS for ${e.key}`);
389
389
  });
390
390
 
391
- myCache.on('error', (e) => {
391
+ myCache.on('cache:fetch:error', (e) => {
392
392
  console.error(`[CacheEvent] ERROR for ${e.key} (attempt ${e.attempt}):`, e.error.message);
393
393
  });
394
394
 
395
- myCache.on('persistence', (e) => {
396
- if (e.event === 'save_success') {
397
- console.log(`[CacheEvent] Persistence: Cache state saved successfully for ID: ${e.key}`);
398
- } else if (e.event === 'load_fail') {
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}`);
402
- }
395
+ myCache.on('cache:persistence:save:success', (e) => {
396
+ console.log(`[CacheEvent] Persistence: Cache state saved successfully for ID: ${e.key}`);
397
+ });
398
+
399
+ myCache.on('cache:persistence:load:error', (e) => {
400
+ console.error(`[CacheEvent] Persistence: Failed to load cache state for ID: ${e.key}`, e.error);
403
401
  });
404
402
 
405
403
  // For demonstration, register a query and trigger events
@@ -412,18 +410,9 @@ myCache.registerQuery('demo-item', async () => {
412
410
  myCache.get('demo-item'); // Triggers miss, fetch, set_data
413
411
  setTimeout(() => myCache.get('demo-item'), 50); // Triggers hit
414
412
  setTimeout(() => myCache.get('demo-item'), 150); // Triggers stale hit, background fetch
415
- ```
416
-
417
- #### `cache.off<EType extends CacheEventType>(event: EType, listener: (ev: Extract<CacheEvent, { type: EType }>) => void): void`
418
-
419
- Unsubscribes a previously registered listener from a cache event. The `listener` reference must be the exact same function that was passed to `on()`.
420
-
421
- ```typescript
422
- const myHitLogger = (e: any) => console.log(`[Log] Cache Hit: ${e.key}`);
423
- myCache.on('hit', myHitLogger);
424
413
 
425
- // Later, when you no longer need the listener:
426
- myCache.off('hit', myHitLogger);
414
+ // To unsubscribe from a specific event later:
415
+ unsubscribeHit();
427
416
  ```
428
417
 
429
418
  #### `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 }> }`
@@ -637,28 +626,28 @@ const monitorCache = new Cache({ enableMetrics: true });
637
626
 
638
627
  monitorCache.registerQuery('stock/AAPL', async () => {
639
628
  const price = Math.random() * 100 + 150;
640
- console.log(`--- Fetching AAPL price: $${price.toFixed(2)} ---`);
629
+ console.log(`--- Fetching AAPL price: ${price.toFixed(2)} ---`);
641
630
  return { symbol: 'AAPL', price: parseFloat(price.toFixed(2)), timestamp: Date.now() };
642
631
  }, { staleTime: 1000 }); // Very short staleTime for frequent fetches
643
632
 
644
633
  // Subscribe to various cache events
645
- monitorCache.on('fetch', (e) => {
634
+ monitorCache.on('cache:fetch:start', (e) => {
646
635
  console.log(`[EVENT] Fetching ${e.key} (attempt ${e.attempt})`);
647
636
  });
648
- monitorCache.on('hit', (e) => {
637
+ monitorCache.on('cache:read:hit', (e) => {
649
638
  console.log(`[EVENT] Cache hit for ${e.key}. Stale: ${e.isStale}`);
650
639
  });
651
- monitorCache.on('miss', (e) => {
640
+ monitorCache.on('cache:read:miss', (e) => {
652
641
  console.log(`[EVENT] Cache miss for ${e.key}`);
653
642
  });
654
- monitorCache.on('eviction', (e) => {
643
+ monitorCache.on('cache:data:evict', (e) => {
655
644
  console.log(`[EVENT] Evicted ${e.key} due to ${e.reason}`);
656
645
  });
657
- monitorCache.on('set_data', (e) => {
646
+ monitorCache.on('cache:data:set', (e) => {
658
647
  console.log(`[EVENT] Data for ${e.key} manually set. Old price: ${e.oldData?.price}, New price: ${e.newData.price}`);
659
648
  });
660
- monitorCache.on('persistence', (e) => {
661
- if (e.event === 'save_success') console.log(`[EVENT] Persistence: ${e.message || 'Save successful'}`);
649
+ monitorCache.on('cache:persistence:save:success', (e) => {
650
+ console.log(`[EVENT] Persistence: ${e.message || 'Save successful'}`);
662
651
  });
663
652
 
664
653
 
@@ -714,13 +703,13 @@ package.json # Package metadata and dependencies for this specific mo
714
703
  * **`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
704
  * **`CacheMetrics` (`types.ts`)**: Defines the structure for tracking cache performance statistics, including hits, misses, fetches, errors, and evictions.
716
705
  * **`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.
706
+ * **`CacheEvent` / `CacheEventType` (`types.ts`)**: A union type defining all possible events emitted by the cache (e.g., `'cache:read:hit'`, `'cache:fetch:start'`, `'cache:data:evict'`). This enables a fine-grained, scoped observability model for the cache's lifecycle.
718
707
 
719
708
  ### Data Flow
720
709
 
721
710
  1. **Initialization**:
722
711
  * 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()`.
712
+ * If a `persistence` layer is configured, it attempts to load a previously saved state using `persistence.get()`, emitting `'cache:persistence:load:success'` or `'cache:persistence:load:error'`.
724
713
  * 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
714
 
726
715
  2. **`registerQuery`**:
@@ -728,11 +717,11 @@ package.json # Package metadata and dependencies for this specific mo
728
717
 
729
718
  3. **`get` Request**:
730
719
  * 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`.
720
+ * **Cache Hit**: If an entry exists, `lastAccessed` and `accessCount` are updated, a `'cache:read:hit'` event is emitted, and metrics are incremented. The entry's staleness is evaluated based on `staleTime`.
732
721
  * If `waitForFresh` is `true` OR if the entry is stale/loading, it proceeds to `fetchAndWait`.
733
722
  * 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
723
  * 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.
724
+ * **Cache Miss**: If no entry exists, a `'cache:read:miss'` event is emitted. A placeholder `CacheEntry` (marked `isLoading`) is created, and a `fetch` is immediately triggered to retrieve the data.
736
725
 
737
726
  4. **`fetch` / `fetchAndWait`**:
738
727
  * These methods ensure that only one `fetchFunction` runs concurrently for a given `key` by tracking ongoing fetches in `this.fetching`.
@@ -740,21 +729,21 @@ package.json # Package metadata and dependencies for this specific mo
740
729
 
741
730
  5. **`performFetchWithRetry`**:
742
731
  * 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.
732
+ * Before each attempt, a `'cache:fetch:start'` event is emitted, and `fetches` metrics are updated.
744
733
  * **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.
734
+ * **On Failure**: If the `fetchFunction` fails, a `'cache:fetch: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
735
 
747
736
  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.
737
+ * 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 (`'cache:persistence:save:success'`/`'cache:persistence:save:error'`) are emitted.
749
738
 
750
739
  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`).
740
+ * 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 `'cache:persistence:sync'` event.
752
741
 
753
742
  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.
743
+ * 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 `'cache:data:evict'` events.
755
744
 
756
745
  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.
746
+ * 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 `'cache:data:evict'` events.
758
747
 
759
748
  ### Extension Points
760
749
 
@@ -762,7 +751,7 @@ The design of `@asaidimu/utils-cache` provides several powerful extension points
762
751
 
763
752
  * **`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
753
  * **`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:
754
+ * **Event Listeners (`on`)**: The comprehensive event system, powered by `@asaidimu/events`, allows you to subscribe to a wide range of cache lifecycle events. This enables powerful integrations for:
766
755
  * **Logging**: Detailed logging of cache activity (hits, misses, errors, evictions).
767
756
  * **Analytics**: Feeding cache performance metrics into an analytics platform.
768
757
  * **UI Reactivity**: Updating UI components in response to cache changes (e.g., showing a "stale data" indicator or a "refreshing" spinner).
@@ -779,16 +768,16 @@ We welcome contributions to `@asaidimu/utils-cache`! Whether it's a bug fix, a n
779
768
 
780
769
  To set up the development environment for `@asaidimu/utils-cache`:
781
770
 
782
- 1. **Clone the monorepo:**
771
+ 1. **Clone the monorepo**:
783
772
  ```bash
784
773
  git clone https://github.com/asaidimu/erp-utils.git
785
774
  cd erp-utils
786
775
  ```
787
- 2. **Navigate to the cache package:**
776
+ 2. **Navigate to the cache package**:
788
777
  ```bash
789
778
  cd src/cache
790
779
  ```
791
- 3. **Install dependencies:**
780
+ 3. **Install dependencies**:
792
781
  ```bash
793
782
  npm install
794
783
  # or
@@ -796,7 +785,7 @@ To set up the development environment for `@asaidimu/utils-cache`:
796
785
  # or
797
786
  bun install
798
787
  ```
799
- 4. **Build the project:**
788
+ 4. **Build the project**:
800
789
  ```bash
801
790
  npm run build
802
791
  # or
@@ -871,7 +860,7 @@ When reporting a bug, please include:
871
860
  2. **`persistenceId`**: Ensure you've provided a unique `persistenceId` if multiple cache instances share the same persistence layer.
872
861
  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
862
  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', ...)`).
863
+ 5. **Event Errors**: Check for persistence event errors in your browser's or Node.js console (e.g., `cache.on('cache:persistence:save:error', ...)`).
875
864
  * **Cache not evicting data**:
876
865
  * **Cause**: Eviction policies might be disabled or configured with very long durations.
877
866
  * **Solution**:
@@ -881,9 +870,9 @@ When reporting a bug, please include:
881
870
  * **Event listeners not firing**:
882
871
  * **Cause**: The listener might be removed, or the expected event is not actually occurring.
883
872
  * **Solution**:
884
- 1. **Correct Event Type**: Ensure you are subscribing to the exact `CacheEventType` you expect (e.g., `'hit'`, `'error'`).
873
+ 1. **Correct Event Type**: Ensure you are subscribing to the exact `CacheEventType` you expect (e.g., `'cache:read:hit'`, `'cache:fetch:error'`).
885
874
  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()`.
875
+ 3. **Unsubscribe Function**: Ensure you are not accidentally calling the `unsubscribe` function returned by `on()` prematurely.
887
876
 
888
877
  ### FAQ
889
878
 
@@ -911,3 +900,4 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
911
900
 
912
901
  * Inspired by modern data fetching and caching libraries like [React Query](https://react-query.tanstack.com/) and [SWR](https://swr.vercel.app/).
913
902
  * Uses the `uuid` library for generating unique cache instance IDs.
903
+ * Event system powered by `@asaidimu/events`.
package/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { S as SimplePersistence } from '../types-DUZGkNEB.js';
1
+ import { S as SimplePersistence } from '../types-ZDD-bdCz.js';
2
2
 
3
3
  interface CacheOptions {
4
4
  staleTime?: number;
@@ -13,6 +13,18 @@ interface CacheOptions {
13
13
  deserializeValue?: (value: any) => any;
14
14
  persistenceDebounceTime?: number;
15
15
  }
16
+ interface CacheEntry<T = any> {
17
+ data: T;
18
+ lastUpdated: number;
19
+ lastAccessed: number;
20
+ accessCount: number;
21
+ error?: Error;
22
+ isLoading?: boolean;
23
+ }
24
+ interface QueryConfig {
25
+ fetchFunction: () => Promise<any>;
26
+ options: Required<CacheOptions>;
27
+ }
16
28
  interface CacheMetrics {
17
29
  hits: number;
18
30
  misses: number;
@@ -38,47 +50,59 @@ type CacheEventBase<Type extends string, Payload = {}> = {
38
50
  key: string;
39
51
  timestamp: number;
40
52
  } & Payload;
41
- type CacheHitEvent<T = any> = CacheEventBase<'hit', {
53
+ type CacheReadHitEvent<T = any> = CacheEventBase<'cache:read:hit', {
42
54
  data: T;
43
55
  isStale: boolean;
44
56
  }>;
45
- type CacheMissEvent = CacheEventBase<'miss'>;
46
- type CacheFetchEvent = CacheEventBase<'fetch', {
57
+ type CacheReadMissEvent = CacheEventBase<'cache:read:miss'>;
58
+ type CacheFetchStartEvent = CacheEventBase<'cache:fetch:start', {
47
59
  attempt: number;
48
60
  }>;
49
- type CacheErrorEvent = CacheEventBase<'error', {
61
+ type CacheFetchSuccessEvent<T = any> = CacheEventBase<'cache:fetch:success', {
62
+ data: T;
63
+ }>;
64
+ type CacheFetchErrorEvent = CacheEventBase<'cache:fetch:error', {
50
65
  error: Error;
51
66
  attempt: number;
52
67
  }>;
53
- type CacheEvictionEvent = CacheEventBase<'eviction', {
68
+ type CacheDataEvictEvent = CacheEventBase<'cache:data:evict', {
54
69
  reason?: string;
55
70
  }>;
56
- type CacheInvalidationEvent = CacheEventBase<'invalidation'>;
57
- type CacheSetDataEvent<T = any> = CacheEventBase<'set_data', {
71
+ type CacheDataInvalidateEvent = CacheEventBase<'cache:data:invalidate'>;
72
+ type CacheDataSetEvent<T = any> = CacheEventBase<'cache:data:set', {
58
73
  newData: T;
59
74
  oldData?: T;
60
75
  }>;
61
- type CachePersistenceEventPayload = {
62
- event: 'load_success' | 'remote_update';
76
+ type CachePersistenceLoadSuccessEvent = CacheEventBase<'cache:persistence:load:success', {
63
77
  message?: string;
64
- } | {
65
- event: 'load_fail' | 'save_fail' | 'clear_fail';
78
+ }>;
79
+ type CachePersistenceLoadErrorEvent = CacheEventBase<'cache:persistence:load:error', {
66
80
  message?: string;
67
81
  error?: any;
68
- } | {
69
- event: 'save_success' | 'clear_success';
70
- };
71
- type CachePersistenceEvent = CacheEventBase<'persistence', CachePersistenceEventPayload>;
72
- type CacheEvent = CacheHitEvent | CacheMissEvent | CacheFetchEvent | CacheErrorEvent | CacheEvictionEvent | CacheInvalidationEvent | CacheSetDataEvent | CachePersistenceEvent;
82
+ }>;
83
+ type CachePersistenceSaveSuccessEvent = CacheEventBase<'cache:persistence:save:success'>;
84
+ type CachePersistenceSaveErrorEvent = CacheEventBase<'cache:persistence:save:error', {
85
+ message?: string;
86
+ error?: any;
87
+ }>;
88
+ type CachePersistenceClearSuccessEvent = CacheEventBase<'cache:persistence:clear:success'>;
89
+ type CachePersistenceClearErrorEvent = CacheEventBase<'cache:persistence:clear:error', {
90
+ message?: string;
91
+ error?: any;
92
+ }>;
93
+ type CachePersistenceSyncEvent = CacheEventBase<'cache:persistence:sync', {
94
+ message?: string;
95
+ }>;
96
+ type CacheEvent = CacheReadHitEvent | CacheReadMissEvent | CacheFetchStartEvent | CacheFetchSuccessEvent | CacheFetchErrorEvent | CacheDataEvictEvent | CacheDataInvalidateEvent | CacheDataSetEvent | CachePersistenceLoadSuccessEvent | CachePersistenceLoadErrorEvent | CachePersistenceSaveSuccessEvent | CachePersistenceSaveErrorEvent | CachePersistenceClearSuccessEvent | CachePersistenceClearErrorEvent | CachePersistenceSyncEvent;
73
97
  type CacheEventType = CacheEvent['type'];
74
98
 
75
- declare class Cache {
99
+ declare class QueryCache {
76
100
  private cache;
77
101
  private queries;
78
102
  private fetching;
79
103
  private readonly defaultOptions;
80
104
  private metrics;
81
- private eventListeners;
105
+ private eventBus;
82
106
  private gcTimer?;
83
107
  private readonly persistenceId;
84
108
  private persistenceUnsubscribe?;
@@ -127,10 +151,7 @@ declare class Cache {
127
151
  };
128
152
  on<EType extends CacheEventType>(event: EType, listener: (ev: Extract<CacheEvent, {
129
153
  type: EType;
130
- }>) => void): void;
131
- off<EType extends CacheEventType>(event: EType, listener: (ev: Extract<CacheEvent, {
132
- type: EType;
133
- }>) => void): void;
154
+ }>) => void): () => void;
134
155
  private emitEvent;
135
156
  private updateMetrics;
136
157
  private delay;
@@ -138,4 +159,4 @@ declare class Cache {
138
159
  destroy(): void;
139
160
  }
140
161
 
141
- export { Cache };
162
+ export { type CacheDataEvictEvent, type CacheDataInvalidateEvent, type CacheDataSetEvent, type CacheEntry, type CacheEvent, type CacheEventBase, type CacheEventType, type CacheFetchErrorEvent, type CacheFetchStartEvent, type CacheFetchSuccessEvent, type CacheMetrics, type CacheOptions, type CachePersistenceClearErrorEvent, type CachePersistenceClearSuccessEvent, type CachePersistenceLoadErrorEvent, type CachePersistenceLoadSuccessEvent, type CachePersistenceSaveErrorEvent, type CachePersistenceSaveSuccessEvent, type CachePersistenceSyncEvent, type CacheReadHitEvent, type CacheReadMissEvent, QueryCache, type QueryConfig, type SerializableCacheEntry, type SerializableCacheState };
package/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { S as SimplePersistence } from '../types-DUZGkNEB.js';
1
+ import { S as SimplePersistence } from '../types-ZDD-bdCz.js';
2
2
 
3
3
  interface CacheOptions {
4
4
  staleTime?: number;
@@ -13,6 +13,18 @@ interface CacheOptions {
13
13
  deserializeValue?: (value: any) => any;
14
14
  persistenceDebounceTime?: number;
15
15
  }
16
+ interface CacheEntry<T = any> {
17
+ data: T;
18
+ lastUpdated: number;
19
+ lastAccessed: number;
20
+ accessCount: number;
21
+ error?: Error;
22
+ isLoading?: boolean;
23
+ }
24
+ interface QueryConfig {
25
+ fetchFunction: () => Promise<any>;
26
+ options: Required<CacheOptions>;
27
+ }
16
28
  interface CacheMetrics {
17
29
  hits: number;
18
30
  misses: number;
@@ -38,47 +50,59 @@ type CacheEventBase<Type extends string, Payload = {}> = {
38
50
  key: string;
39
51
  timestamp: number;
40
52
  } & Payload;
41
- type CacheHitEvent<T = any> = CacheEventBase<'hit', {
53
+ type CacheReadHitEvent<T = any> = CacheEventBase<'cache:read:hit', {
42
54
  data: T;
43
55
  isStale: boolean;
44
56
  }>;
45
- type CacheMissEvent = CacheEventBase<'miss'>;
46
- type CacheFetchEvent = CacheEventBase<'fetch', {
57
+ type CacheReadMissEvent = CacheEventBase<'cache:read:miss'>;
58
+ type CacheFetchStartEvent = CacheEventBase<'cache:fetch:start', {
47
59
  attempt: number;
48
60
  }>;
49
- type CacheErrorEvent = CacheEventBase<'error', {
61
+ type CacheFetchSuccessEvent<T = any> = CacheEventBase<'cache:fetch:success', {
62
+ data: T;
63
+ }>;
64
+ type CacheFetchErrorEvent = CacheEventBase<'cache:fetch:error', {
50
65
  error: Error;
51
66
  attempt: number;
52
67
  }>;
53
- type CacheEvictionEvent = CacheEventBase<'eviction', {
68
+ type CacheDataEvictEvent = CacheEventBase<'cache:data:evict', {
54
69
  reason?: string;
55
70
  }>;
56
- type CacheInvalidationEvent = CacheEventBase<'invalidation'>;
57
- type CacheSetDataEvent<T = any> = CacheEventBase<'set_data', {
71
+ type CacheDataInvalidateEvent = CacheEventBase<'cache:data:invalidate'>;
72
+ type CacheDataSetEvent<T = any> = CacheEventBase<'cache:data:set', {
58
73
  newData: T;
59
74
  oldData?: T;
60
75
  }>;
61
- type CachePersistenceEventPayload = {
62
- event: 'load_success' | 'remote_update';
76
+ type CachePersistenceLoadSuccessEvent = CacheEventBase<'cache:persistence:load:success', {
63
77
  message?: string;
64
- } | {
65
- event: 'load_fail' | 'save_fail' | 'clear_fail';
78
+ }>;
79
+ type CachePersistenceLoadErrorEvent = CacheEventBase<'cache:persistence:load:error', {
66
80
  message?: string;
67
81
  error?: any;
68
- } | {
69
- event: 'save_success' | 'clear_success';
70
- };
71
- type CachePersistenceEvent = CacheEventBase<'persistence', CachePersistenceEventPayload>;
72
- type CacheEvent = CacheHitEvent | CacheMissEvent | CacheFetchEvent | CacheErrorEvent | CacheEvictionEvent | CacheInvalidationEvent | CacheSetDataEvent | CachePersistenceEvent;
82
+ }>;
83
+ type CachePersistenceSaveSuccessEvent = CacheEventBase<'cache:persistence:save:success'>;
84
+ type CachePersistenceSaveErrorEvent = CacheEventBase<'cache:persistence:save:error', {
85
+ message?: string;
86
+ error?: any;
87
+ }>;
88
+ type CachePersistenceClearSuccessEvent = CacheEventBase<'cache:persistence:clear:success'>;
89
+ type CachePersistenceClearErrorEvent = CacheEventBase<'cache:persistence:clear:error', {
90
+ message?: string;
91
+ error?: any;
92
+ }>;
93
+ type CachePersistenceSyncEvent = CacheEventBase<'cache:persistence:sync', {
94
+ message?: string;
95
+ }>;
96
+ type CacheEvent = CacheReadHitEvent | CacheReadMissEvent | CacheFetchStartEvent | CacheFetchSuccessEvent | CacheFetchErrorEvent | CacheDataEvictEvent | CacheDataInvalidateEvent | CacheDataSetEvent | CachePersistenceLoadSuccessEvent | CachePersistenceLoadErrorEvent | CachePersistenceSaveSuccessEvent | CachePersistenceSaveErrorEvent | CachePersistenceClearSuccessEvent | CachePersistenceClearErrorEvent | CachePersistenceSyncEvent;
73
97
  type CacheEventType = CacheEvent['type'];
74
98
 
75
- declare class Cache {
99
+ declare class QueryCache {
76
100
  private cache;
77
101
  private queries;
78
102
  private fetching;
79
103
  private readonly defaultOptions;
80
104
  private metrics;
81
- private eventListeners;
105
+ private eventBus;
82
106
  private gcTimer?;
83
107
  private readonly persistenceId;
84
108
  private persistenceUnsubscribe?;
@@ -127,10 +151,7 @@ declare class Cache {
127
151
  };
128
152
  on<EType extends CacheEventType>(event: EType, listener: (ev: Extract<CacheEvent, {
129
153
  type: EType;
130
- }>) => void): void;
131
- off<EType extends CacheEventType>(event: EType, listener: (ev: Extract<CacheEvent, {
132
- type: EType;
133
- }>) => void): void;
154
+ }>) => void): () => void;
134
155
  private emitEvent;
135
156
  private updateMetrics;
136
157
  private delay;
@@ -138,4 +159,4 @@ declare class Cache {
138
159
  destroy(): void;
139
160
  }
140
161
 
141
- export { Cache };
162
+ export { type CacheDataEvictEvent, type CacheDataInvalidateEvent, type CacheDataSetEvent, type CacheEntry, type CacheEvent, type CacheEventBase, type CacheEventType, type CacheFetchErrorEvent, type CacheFetchStartEvent, type CacheFetchSuccessEvent, type CacheMetrics, type CacheOptions, type CachePersistenceClearErrorEvent, type CachePersistenceClearSuccessEvent, type CachePersistenceLoadErrorEvent, type CachePersistenceLoadSuccessEvent, type CachePersistenceSaveErrorEvent, type CachePersistenceSaveSuccessEvent, type CachePersistenceSyncEvent, type CacheReadHitEvent, type CacheReadMissEvent, QueryCache, type QueryConfig, type SerializableCacheEntry, type SerializableCacheState };
package/index.js CHANGED
@@ -1 +1 @@
1
- "use strict";var e=require("uuid");exports.Cache=class{cache=new Map;queries=new Map;fetching=new Map;defaultOptions;metrics;eventListeners=new Map;gcTimer;persistenceId;persistenceUnsubscribe;persistenceDebounceTimer;isHandlingRemoteUpdate=!1;constructor(t={}){void 0!==t.staleTime&&t.staleTime<0&&(console.warn("CacheOptions: staleTime should be non-negative. Using 0."),t.staleTime=0),void 0!==t.cacheTime&&t.cacheTime<0&&(console.warn("CacheOptions: cacheTime should be non-negative. Using 0."),t.cacheTime=0),void 0!==t.retryAttempts&&t.retryAttempts<0&&(console.warn("CacheOptions: retryAttempts should be non-negative. Using 0."),t.retryAttempts=0),void 0!==t.retryDelay&&t.retryDelay<0&&(console.warn("CacheOptions: retryDelay should be non-negative. Using 0."),t.retryDelay=0),void 0!==t.maxSize&&t.maxSize<0&&(console.warn("CacheOptions: maxSize should be non-negative. Using 0."),t.maxSize=0),this.defaultOptions={staleTime:3e5,cacheTime:18e5,retryAttempts:3,retryDelay:1e3,maxSize:1e3,enableMetrics:!0,persistence:void 0,persistenceId:void 0,serializeValue:e=>e,deserializeValue:e=>e,persistenceDebounceTime:500,...t},this.metrics={hits:0,misses:0,fetches:0,errors:0,evictions:0,staleHits:0},this.persistenceId=this.defaultOptions.persistenceId||e.v4(),this.startGarbageCollection(),this.initializePersistence()}async initializePersistence(){const{persistence:e}=this.defaultOptions;if(e){try{const t=await e.get();t&&(this.deserializeAndLoadCache(t),this.emitEvent({type:"persistence",key:this.persistenceId,timestamp:Date.now(),event:"load_success",message:`Cache loaded for ID: ${this.persistenceId}`}))}catch(e){console.error(`Cache (${this.persistenceId}): Failed to load state from persistence:`,e),this.emitEvent({type:"persistence",key:this.persistenceId,timestamp:Date.now(),event:"load_fail",error:e,message:`Failed to load cache for ID: ${this.persistenceId}`})}if("function"==typeof e.subscribe)try{this.persistenceUnsubscribe=e.subscribe(this.persistenceId,(e=>{this.handleRemoteStateChange(e)}))}catch(e){console.error(`Cache (${this.persistenceId}): Failed to subscribe to persistence:`,e)}}}serializeCache(){const e=[],{serializeValue:t}=this.defaultOptions;for(const[s,i]of this.cache)i.isLoading&&void 0===i.data&&0===i.lastUpdated||e.push([s,{data:t(i.data),lastUpdated:i.lastUpdated,lastAccessed:i.lastAccessed,accessCount:i.accessCount,error:i.error?{name:i.error.name,message:i.error.message,stack:i.error.stack}:void 0}]);return e}deserializeAndLoadCache(e){this.isHandlingRemoteUpdate=!0;const t=new Map,{deserializeValue:s}=this.defaultOptions;for(const[i,a]of e){let e;a.error&&(e=new Error(a.error.message),e.name=a.error.name,e.stack=a.error.stack),t.set(i,{data:s(a.data),lastUpdated:a.lastUpdated,lastAccessed:a.lastAccessed,accessCount:a.accessCount,error:e,isLoading:!1})}this.cache=t,this.enforceSizeLimit(!1),this.isHandlingRemoteUpdate=!1}schedulePersistState(){this.defaultOptions.persistence&&!this.isHandlingRemoteUpdate&&(this.persistenceDebounceTimer&&clearTimeout(this.persistenceDebounceTimer),this.persistenceDebounceTimer=setTimeout((async()=>{try{const e=this.serializeCache();await this.defaultOptions.persistence.set(this.persistenceId,e),this.emitEvent({type:"persistence",key:this.persistenceId,timestamp:Date.now(),event:"save_success"})}catch(e){console.error(`Cache (${this.persistenceId}): Failed to persist state:`,e),this.emitEvent({type:"persistence",key:this.persistenceId,timestamp:Date.now(),event:"save_fail",error:e})}}),this.defaultOptions.persistenceDebounceTime))}handleRemoteStateChange(e){if(this.isHandlingRemoteUpdate||!e)return;this.isHandlingRemoteUpdate=!0;const{deserializeValue:t}=this.defaultOptions,s=new Map;let i=!1;for(const[a,r]of e){let e;r.error&&(e=new Error(r.error.message),e.name=r.error.name,e.stack=r.error.stack);const c={data:t(r.data),lastUpdated:r.lastUpdated,lastAccessed:r.lastAccessed,accessCount:r.accessCount,error:e,isLoading:!1};s.set(a,c);const n=this.cache.get(a);(!n||n.lastUpdated<c.lastUpdated||JSON.stringify(n.data)!==JSON.stringify(c.data))&&(i=!0)}this.cache.size!==s.size&&(i=!0),i&&(this.cache=s,this.enforceSizeLimit(!1),this.emitEvent({type:"persistence",key:this.persistenceId,timestamp:Date.now(),event:"remote_update",message:"Cache updated from remote state."})),this.isHandlingRemoteUpdate=!1}registerQuery(e,t,s={}){void 0!==s.staleTime&&s.staleTime<0&&(s.staleTime=0),void 0!==s.cacheTime&&s.cacheTime<0&&(s.cacheTime=0),this.queries.set(e,{fetchFunction:t,options:{...this.defaultOptions,...s}})}async get(e,t){const s=this.queries.get(e);if(!s)throw new Error(`No query registered for key: ${e}`);let i=this.cache.get(e);const a=this.isStale(i,s.options);let r=!1;if(i)i.lastAccessed=Date.now(),i.accessCount++,this.updateMetrics("hits"),a&&this.updateMetrics("staleHits"),this.emitEvent({type:"hit",key:e,timestamp:Date.now(),data:i.data,isStale:a});else if(this.updateMetrics("misses"),this.emitEvent({type:"miss",key:e,timestamp:Date.now()}),!t?.waitForFresh||a){const t={data:void 0,lastUpdated:0,lastAccessed:Date.now(),accessCount:1,isLoading:!0,error:void 0};this.cache.set(e,t),i=t,r=!0}if(t?.waitForFresh&&(!i||a||i.isLoading))try{const t=await this.fetchAndWait(e,s);return r&&this.cache.get(e)===i&&this.schedulePersistState(),t}catch(s){if(t.throwOnError)throw s;return this.cache.get(e)?.data}if(!i||a||i&&!i.isLoading&&0===i.lastUpdated&&!i.error){if(i&&!i.isLoading)i.isLoading=!0;else if(!i){const t={data:void 0,lastUpdated:0,lastAccessed:Date.now(),accessCount:0,isLoading:!0,error:void 0};this.cache.set(e,t),i=t,r=!0}this.fetch(e,s).catch((()=>{}))}if(r&&this.schedulePersistState(),i?.error&&t?.throwOnError)throw i.error;return i?.data}peek(e){const t=this.cache.get(e);return t&&(t.lastAccessed=Date.now(),t.accessCount++),t?.data}has(e){const t=this.cache.get(e),s=this.queries.get(e);return!(!t||!s)&&(!this.isStale(t,s.options)&&!t.isLoading)}async fetch(e,t){if(this.fetching.has(e))return this.fetching.get(e);let s=this.cache.get(e);s?s.isLoading||(s.isLoading=!0,s.error=void 0):(s={data:void 0,lastUpdated:0,lastAccessed:Date.now(),accessCount:0,isLoading:!0,error:void 0},this.cache.set(e,s),this.schedulePersistState());const i=this.performFetchWithRetry(e,t,s);this.fetching.set(e,i);try{return await i}finally{this.fetching.delete(e)}}async fetchAndWait(e,t){const s=this.fetching.get(e);if(s)return s;const i=await this.fetch(e,t);if(void 0===i){const t=this.cache.get(e);if(t?.error)throw t.error;throw new Error(`Failed to fetch data for key: ${e} after retries.`)}return i}async performFetchWithRetry(e,t,s){const{retryAttempts:i,retryDelay:a}=t.options;let r;s.isLoading=!0;for(let c=0;c<=i;c++)try{this.emitEvent({type:"fetch",key:e,timestamp:Date.now(),attempt:c}),this.updateMetrics("fetches");const i=await t.fetchFunction();return s.data=i,s.lastUpdated=Date.now(),s.isLoading=!1,s.error=void 0,this.cache.set(e,s),this.schedulePersistState(),this.enforceSizeLimit(),i}catch(t){r=t,this.emitEvent({type:"error",key:e,timestamp:Date.now(),error:r,attempt:c}),c<i&&await this.delay(a*Math.pow(2,c))}this.updateMetrics("errors"),s.error=r,s.isLoading=!1,this.cache.set(e,s),this.schedulePersistState()}isStale(e,t){if(!e||e.error)return!0;if(e.isLoading&&!e.data)return!0;const{staleTime:s}=t;return 0!==s&&s!==1/0&&Date.now()-e.lastUpdated>s}async invalidate(e,t=!0){const s=this.cache.get(e),i=this.queries.get(e);let a=!1;s&&(a=0!==s.lastUpdated||void 0!==s.error,s.lastUpdated=0,s.error=void 0,this.emitEvent({type:"invalidation",key:e,timestamp:Date.now()}),a&&this.schedulePersistState(),t&&i&&this.fetch(e,i).catch((()=>{})))}async invalidatePattern(e,t=!0){const s=[];let i=!1;for(const t of this.cache.keys())e.test(t)&&s.push(t);s.forEach((e=>{const t=this.cache.get(e);t&&(0===t.lastUpdated&&void 0===t.error||(i=!0),t.lastUpdated=0,t.error=void 0,this.emitEvent({type:"invalidation",key:e,timestamp:Date.now()}))})),i&&this.schedulePersistState(),t&&s.length>0&&await Promise.all(s.map((e=>{const t=this.queries.get(e);return t?this.fetch(e,t).catch((()=>{})):Promise.resolve()})))}async prefetch(e){const t=this.queries.get(e);if(!t)return void console.warn(`Cannot prefetch: No query registered for key: ${e}`);const s=this.cache.get(e);s&&!this.isStale(s,t.options)||this.fetch(e,t).catch((()=>{}))}async refresh(e){const t=this.queries.get(e);if(!t)return void console.warn(`Cannot refresh: No query registered for key: ${e}`);this.fetching.delete(e);let s=this.cache.get(e);s?(s.isLoading=!0,s.error=void 0):(s={data:void 0,lastUpdated:0,lastAccessed:Date.now(),accessCount:0,isLoading:!0,error:void 0},this.cache.set(e,s),this.schedulePersistState());const i=this.performFetchWithRetry(e,t,s);this.fetching.set(e,i);try{return await i}finally{this.fetching.delete(e)}}setData(e,t){const s=this.cache.get(e),i=s?.data,a={data:t,lastUpdated:Date.now(),lastAccessed:Date.now(),accessCount:(s?.accessCount||0)+1,isLoading:!1,error:void 0};this.cache.set(e,a),this.schedulePersistState(),this.enforceSizeLimit(),this.emitEvent({type:"set_data",key:e,timestamp:Date.now(),newData:t,oldData:i})}remove(e){this.fetching.delete(e);const t=this.cache.has(e),s=this.cache.delete(e);return s&&t&&this.schedulePersistState(),s}enforceSizeLimit(e=!0){const{maxSize:t}=this.defaultOptions;if(t===1/0||this.cache.size<=t)return;let s=0;if(0===t){s=this.cache.size;for(const e of this.cache.keys())this.cache.delete(e),this.emitEvent({type:"eviction",key:e,timestamp:Date.now(),reason:"size_limit_zero"}),this.updateMetrics("evictions")}else{const e=Array.from(this.cache.entries()).sort((([,e],[,t])=>e.lastAccessed-t.lastAccessed)),i=this.cache.size-t;if(i>0){e.slice(0,i).forEach((([e])=>{this.cache.delete(e)&&(s++,this.emitEvent({type:"eviction",key:e,timestamp:Date.now(),reason:"size_limit_lru"}),this.updateMetrics("evictions"))}))}}s>0&&e&&this.schedulePersistState()}startGarbageCollection(){const{cacheTime:e}=this.defaultOptions;if(e===1/0||e<=0)return;const t=Math.max(1e3,Math.min(e/4,3e5));this.gcTimer=setInterval((()=>this.garbageCollect()),t)}garbageCollect(){const e=Date.now();let t=0;const s=[];for(const[t,i]of this.cache){if(i.isLoading)continue;const a=this.queries.get(t),r=a?.options.cacheTime??this.defaultOptions.cacheTime;r===1/0||r<=0||e-i.lastAccessed>r&&s.push(t)}return s.length>0&&(s.forEach((e=>{this.cache.delete(e)&&(this.fetching.delete(e),this.emitEvent({type:"eviction",key:e,timestamp:Date.now(),reason:"garbage_collected_idle"}),this.updateMetrics("evictions"),t++)})),this.schedulePersistState()),t}getStats(){const e=this.metrics.hits+this.metrics.misses,t=e>0?this.metrics.hits/e:0,s=this.metrics.hits>0?this.metrics.staleHits/this.metrics.hits:0,i=Array.from(this.cache.entries()).map((([e,t])=>{const s=this.queries.get(e),i=!s||this.isStale(t,s.options);return{key:e,lastAccessed:t.lastAccessed,lastUpdated:t.lastUpdated,accessCount:t.accessCount,isStale:i,isLoading:t.isLoading,error:!!t.error}}));return{size:this.cache.size,metrics:{...this.metrics},hitRate:t,staleHitRate:s,entries:i}}on(e,t){this.eventListeners.has(e)||this.eventListeners.set(e,new Set),this.eventListeners.get(e).add(t)}off(e,t){this.eventListeners.get(e)?.delete(t)}emitEvent(e){const t=this.eventListeners.get(e.type);t&&t.forEach((t=>{try{t(e)}catch(t){const s="persistence"===e.type?`for ID ${e.key}`:`for key ${e.key}`;console.error(`Cache event listener error during ${e.type} ${s}:`,t)}}))}updateMetrics(e,t=1){this.defaultOptions.enableMetrics&&(this.metrics[e]=(this.metrics[e]||0)+t)}delay(e){return new Promise((t=>setTimeout(t,e)))}async clear(){const e=this.cache.size>0;if(this.cache.clear(),this.fetching.clear(),this.defaultOptions.enableMetrics&&(this.metrics={hits:0,misses:0,fetches:0,errors:0,evictions:0,staleHits:0}),this.defaultOptions.persistence)try{await this.defaultOptions.persistence.clear(),this.emitEvent({type:"persistence",key:this.persistenceId,timestamp:Date.now(),event:"clear_success"})}catch(e){console.error(`Cache (${this.persistenceId}): Failed to clear persisted state:`,e),this.emitEvent({type:"persistence",key:this.persistenceId,timestamp:Date.now(),event:"clear_fail",error:e})}else e&&this.schedulePersistState()}destroy(){if(this.gcTimer&&(clearInterval(this.gcTimer),this.gcTimer=void 0),this.persistenceDebounceTimer&&clearTimeout(this.persistenceDebounceTimer),this.persistenceUnsubscribe)try{this.persistenceUnsubscribe()}catch(e){console.error(`Cache (${this.persistenceId}): Error unsubscribing persistence:`,e)}this.cache.clear(),this.fetching.clear(),this.queries.clear(),this.eventListeners.clear(),this.metrics={hits:0,misses:0,fetches:0,errors:0,evictions:0,staleHits:0}}};
1
+ "use strict";var e,t,s=require("uuid"),i=Object.create,a=Object.defineProperty,r=Object.getOwnPropertyDescriptor,c=Object.getOwnPropertyNames,n=Object.getPrototypeOf,o=Object.prototype.hasOwnProperty,h=(e={"node_modules/@asaidimu/events/index.js"(e,t){var s,i=Object.defineProperty,a=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,c=Object.prototype.hasOwnProperty,n={};((e,t)=>{for(var s in t)i(e,s,{get:t[s],enumerable:!0})})(n,{createEventBus:()=>o}),t.exports=(s=n,((e,t,s,n)=>{if(t&&"object"==typeof t||"function"==typeof t)for(let o of r(t))c.call(e,o)||o===s||i(e,o,{get:()=>t[o],enumerable:!(n=a(t,o))||n.enumerable});return e})(i({},"__esModule",{value:!0}),s));var o=(e={async:!1,batchSize:1e3,batchDelay:16,errorHandler:e=>console.error("EventBus Error:",e),crossTab:!1,channelName:"event-bus-channel"})=>{const t=new Map;let s=[],i=0,a=0;const r=new Map,c=new Map;let n=null;e.crossTab&&"undefined"!=typeof BroadcastChannel?n=new BroadcastChannel(e.channelName):e.crossTab&&console.warn("BroadcastChannel is not supported in this browser. Cross-tab notifications are disabled.");const o=(e,t)=>{i++,a+=t,r.set(e,(r.get(e)||0)+1)},h=()=>{const t=s;s=[],t.forEach((({name:t,payload:s})=>{const i=performance.now();try{(c.get(t)||[]).forEach((e=>e(s)))}catch(i){e.errorHandler({...i,eventName:t,payload:s})}o(t,performance.now()-i)}))},d=(()=>{let t;return()=>{clearTimeout(t),t=setTimeout(h,e.batchDelay)}})(),l=e=>{const s=t.get(e);s?c.set(e,Array.from(s)):c.delete(e)};return n&&(n.onmessage=e=>{const{name:t,payload:s}=e.data;(c.get(t)||[]).forEach((e=>e(s)))}),{subscribe:(e,s)=>{t.has(e)||t.set(e,new Set);const i=t.get(e);return i.add(s),l(e),()=>{i.delete(s),0===i.size?(t.delete(e),c.delete(e)):l(e)}},emit:({name:t,payload:i})=>{if(e.async)return s.push({name:t,payload:i}),s.length>=e.batchSize?h():d(),void(n&&n.postMessage({name:t,payload:i}));const a=performance.now();try{(c.get(t)||[]).forEach((e=>e(i))),n&&n.postMessage({name:t,payload:i})}catch(s){e.errorHandler({...s,eventName:t,payload:i})}o(t,performance.now()-a)},getMetrics:()=>({totalEvents:i,activeSubscriptions:Array.from(t.values()).reduce(((e,t)=>e+t.size),0),eventCounts:r,averageEmitDuration:i>0?a/i:0}),clear:()=>{t.clear(),c.clear(),s=[],i=0,a=0,r.clear(),n&&(n.close(),n=null)}}}}},function(){return t||(0,e[c(e)[0]])((t={exports:{}}).exports,t),t.exports}),d=((e,t,s)=>(s=null!=e?i(n(e)):{},((e,t,s,i)=>{if(t&&"object"==typeof t||"function"==typeof t)for(let n of c(t))o.call(e,n)||n===s||a(e,n,{get:()=>t[n],enumerable:!(i=r(t,n))||i.enumerable});return e})(e&&e.__esModule?s:a(s,"default",{value:e,enumerable:!0}),e)))(h());exports.QueryCache=class{cache=new Map;queries=new Map;fetching=new Map;defaultOptions;metrics;eventBus;gcTimer;persistenceId;persistenceUnsubscribe;persistenceDebounceTimer;isHandlingRemoteUpdate=!1;constructor(e={}){void 0!==e.staleTime&&e.staleTime<0&&(console.warn("CacheOptions: staleTime should be non-negative. Using 0."),e.staleTime=0),void 0!==e.cacheTime&&e.cacheTime<0&&(console.warn("CacheOptions: cacheTime should be non-negative. Using 0."),e.cacheTime=0),void 0!==e.retryAttempts&&e.retryAttempts<0&&(console.warn("CacheOptions: retryAttempts should be non-negative. Using 0."),e.retryAttempts=0),void 0!==e.retryDelay&&e.retryDelay<0&&(console.warn("CacheOptions: retryDelay should be non-negative. Using 0."),e.retryDelay=0),void 0!==e.maxSize&&e.maxSize<0&&(console.warn("CacheOptions: maxSize should be non-negative. Using 0."),e.maxSize=0),this.defaultOptions={staleTime:0,cacheTime:18e5,retryAttempts:3,retryDelay:1e3,maxSize:1e3,enableMetrics:!0,persistence:void 0,persistenceId:void 0,serializeValue:e=>e,deserializeValue:e=>e,persistenceDebounceTime:500,...e},this.metrics={hits:0,misses:0,fetches:0,errors:0,evictions:0,staleHits:0},this.persistenceId=this.defaultOptions.persistenceId||s.v4(),this.eventBus=(0,d.createEventBus)(),this.startGarbageCollection(),this.initializePersistence()}async initializePersistence(){const{persistence:e}=this.defaultOptions;if(e){try{const t=await e.get();t&&(this.deserializeAndLoadCache(t),this.emitEvent({type:"cache:persistence:load:success",key:this.persistenceId,timestamp:Date.now(),message:`Cache loaded for ID: ${this.persistenceId}`}))}catch(e){console.error(`Cache (${this.persistenceId}): Failed to load state from persistence:`,e),this.emitEvent({type:"cache:persistence:load:error",key:this.persistenceId,timestamp:Date.now(),error:e,message:`Failed to load cache for ID: ${this.persistenceId}`})}if("function"==typeof e.subscribe)try{this.persistenceUnsubscribe=e.subscribe(this.persistenceId,(e=>{this.handleRemoteStateChange(e)}))}catch(e){console.error(`Cache (${this.persistenceId}): Failed to subscribe to persistence:`,e)}}}serializeCache(){const e=[],{serializeValue:t}=this.defaultOptions;for(const[s,i]of this.cache)i.isLoading&&void 0===i.data&&0===i.lastUpdated||e.push([s,{data:t(i.data),lastUpdated:i.lastUpdated,lastAccessed:i.lastAccessed,accessCount:i.accessCount,error:i.error?{name:i.error.name,message:i.error.message,stack:i.error.stack}:void 0}]);return e}deserializeAndLoadCache(e){this.isHandlingRemoteUpdate=!0;const t=new Map,{deserializeValue:s}=this.defaultOptions;for(const[i,a]of e){let e;a.error&&(e=new Error(a.error.message),e.name=a.error.name,e.stack=a.error.stack),t.set(i,{data:s(a.data),lastUpdated:a.lastUpdated,lastAccessed:a.lastAccessed,accessCount:a.accessCount,error:e,isLoading:!1})}this.cache=t,this.enforceSizeLimit(!1),this.isHandlingRemoteUpdate=!1}schedulePersistState(){this.defaultOptions.persistence&&!this.isHandlingRemoteUpdate&&(this.persistenceDebounceTimer&&clearTimeout(this.persistenceDebounceTimer),this.persistenceDebounceTimer=setTimeout((async()=>{try{const e=this.serializeCache();await this.defaultOptions.persistence.set(this.persistenceId,e),this.emitEvent({type:"cache:persistence:save:success",key:this.persistenceId,timestamp:Date.now()})}catch(e){console.error(`Cache (${this.persistenceId}): Failed to persist state:`,e),this.emitEvent({type:"cache:persistence:save:error",key:this.persistenceId,timestamp:Date.now(),error:e})}}),this.defaultOptions.persistenceDebounceTime))}handleRemoteStateChange(e){if(this.isHandlingRemoteUpdate||!e)return;this.isHandlingRemoteUpdate=!0;const{deserializeValue:t}=this.defaultOptions,s=new Map;let i=!1;for(const[a,r]of e){let e;r.error&&(e=new Error(r.error.message),e.name=r.error.name,e.stack=r.error.stack);const c={data:t(r.data),lastUpdated:r.lastUpdated,lastAccessed:r.lastAccessed,accessCount:r.accessCount,error:e,isLoading:!1};s.set(a,c);const n=this.cache.get(a);(!n||n.lastUpdated<c.lastUpdated||JSON.stringify(n.data)!==JSON.stringify(c.data))&&(i=!0)}this.cache.size!==s.size&&(i=!0),i&&(this.cache=s,this.enforceSizeLimit(!1),this.emitEvent({type:"cache:persistence:sync",key:this.persistenceId,timestamp:Date.now(),message:"Cache updated from remote state."})),this.isHandlingRemoteUpdate=!1}registerQuery(e,t,s={}){void 0!==s.staleTime&&s.staleTime<0&&(s.staleTime=0),void 0!==s.cacheTime&&s.cacheTime<0&&(s.cacheTime=0),this.queries.set(e,{fetchFunction:t,options:{...this.defaultOptions,...s}})}async get(e,t){const s=this.queries.get(e);if(!s)throw new Error(`No query registered for key: ${e}`);let i=this.cache.get(e);const a=this.isStale(i,s.options);let r=!1;if(i)i.lastAccessed=Date.now(),i.accessCount++,this.updateMetrics("hits"),a&&this.updateMetrics("staleHits"),this.emitEvent({type:"cache:read:hit",key:e,timestamp:Date.now(),data:i.data,isStale:a});else if(this.updateMetrics("misses"),this.emitEvent({type:"cache:read:miss",key:e,timestamp:Date.now()}),!t?.waitForFresh||a){const t={data:void 0,lastUpdated:0,lastAccessed:Date.now(),accessCount:1,isLoading:!0,error:void 0};this.cache.set(e,t),i=t,r=!0}if(t?.waitForFresh&&(!i||a||i.isLoading))try{const t=await this.fetchAndWait(e,s);return r&&this.cache.get(e)===i&&this.schedulePersistState(),t}catch(s){if(t.throwOnError)throw s;return this.cache.get(e)?.data}if(!i||a||i&&!i.isLoading&&0===i.lastUpdated&&!i.error){if(i&&!i.isLoading)i.isLoading=!0;else if(!i){const t={data:void 0,lastUpdated:0,lastAccessed:Date.now(),accessCount:0,isLoading:!0,error:void 0};this.cache.set(e,t),i=t,r=!0}this.fetch(e,s).catch((()=>{}))}if(r&&this.schedulePersistState(),i?.error&&t?.throwOnError)throw i.error;return i?.data}peek(e){const t=this.cache.get(e);return t&&(t.lastAccessed=Date.now(),t.accessCount++),t?.data}has(e){const t=this.cache.get(e),s=this.queries.get(e);return!(!t||!s)&&(!this.isStale(t,s.options)&&!t.isLoading)}async fetch(e,t){if(this.fetching.has(e))return this.fetching.get(e);let s=this.cache.get(e);s?s.isLoading||(s.isLoading=!0,s.error=void 0):(s={data:void 0,lastUpdated:0,lastAccessed:Date.now(),accessCount:0,isLoading:!0,error:void 0},this.cache.set(e,s),this.schedulePersistState());const i=this.performFetchWithRetry(e,t,s);this.fetching.set(e,i);try{return await i}finally{this.fetching.delete(e)}}async fetchAndWait(e,t){const s=this.fetching.get(e);if(s)return s;const i=await this.fetch(e,t);if(void 0===i){const t=this.cache.get(e);if(t?.error)throw t.error;throw new Error(`Failed to fetch data for key: ${e} after retries.`)}return i}async performFetchWithRetry(e,t,s){const{retryAttempts:i,retryDelay:a}=t.options;let r;s.isLoading=!0;for(let c=0;c<=i;c++)try{this.emitEvent({type:"cache:fetch:start",key:e,timestamp:Date.now(),attempt:c}),this.updateMetrics("fetches");const i=await t.fetchFunction();return s.data=i,s.lastUpdated=Date.now(),s.isLoading=!1,s.error=void 0,this.cache.set(e,s),this.schedulePersistState(),this.enforceSizeLimit(),this.emitEvent({type:"cache:fetch:success",key:e,timestamp:Date.now(),data:i}),i}catch(t){r=t,this.updateMetrics("errors"),this.emitEvent({type:"cache:fetch:error",key:e,timestamp:Date.now(),error:r,attempt:c}),c<i&&await this.delay(a*Math.pow(2,c))}s.error=r,s.isLoading=!1,this.cache.set(e,s),this.schedulePersistState()}isStale(e,t){if(!e||e.error)return!0;if(e.isLoading&&!e.data)return!0;const{staleTime:s}=t;return 0!==s&&s!==1/0&&Date.now()-e.lastUpdated>s}async invalidate(e,t=!0){const s=this.cache.get(e),i=this.queries.get(e);let a=!1;s&&(a=0!==s.lastUpdated||void 0!==s.error,s.lastUpdated=0,s.error=void 0,this.emitEvent({type:"cache:data:invalidate",key:e,timestamp:Date.now()}),a&&this.schedulePersistState(),t&&i&&this.fetch(e,i).catch((()=>{})))}async invalidatePattern(e,t=!0){const s=[];let i=!1;for(const t of this.cache.keys())e.test(t)&&s.push(t);s.forEach((e=>{const t=this.cache.get(e);t&&(0===t.lastUpdated&&void 0===t.error||(i=!0),t.lastUpdated=0,t.error=void 0,this.emitEvent({type:"cache:data:invalidate",key:e,timestamp:Date.now()}))})),i&&this.schedulePersistState(),t&&s.length>0&&await Promise.all(s.map((e=>{const t=this.queries.get(e);return t?this.fetch(e,t).catch((()=>{})):Promise.resolve()})))}async prefetch(e){const t=this.queries.get(e);if(!t)return void console.warn(`Cannot prefetch: No query registered for key: ${e}`);const s=this.cache.get(e);s&&!this.isStale(s,t.options)||this.fetch(e,t).catch((()=>{}))}async refresh(e){const t=this.queries.get(e);if(!t)return void console.warn(`Cannot refresh: No query registered for key: ${e}`);this.fetching.delete(e);let s=this.cache.get(e);s?(s.isLoading=!0,s.error=void 0):(s={data:void 0,lastUpdated:0,lastAccessed:Date.now(),accessCount:0,isLoading:!0,error:void 0},this.cache.set(e,s),this.schedulePersistState());const i=this.performFetchWithRetry(e,t,s);this.fetching.set(e,i);try{return await i}finally{this.fetching.delete(e)}}setData(e,t){const s=this.cache.get(e),i=s?.data,a={data:t,lastUpdated:Date.now(),lastAccessed:Date.now(),accessCount:(s?.accessCount||0)+1,isLoading:!1,error:void 0};this.cache.set(e,a),this.schedulePersistState(),this.enforceSizeLimit(),this.emitEvent({type:"cache:data:set",key:e,timestamp:Date.now(),newData:t,oldData:i})}remove(e){this.fetching.delete(e);const t=this.cache.has(e),s=this.cache.delete(e);return s&&t&&this.schedulePersistState(),s}enforceSizeLimit(e=!0){const{maxSize:t}=this.defaultOptions;if(t===1/0||this.cache.size<=t)return;let s=0;if(0===t){s=this.cache.size;for(const e of this.cache.keys())this.cache.delete(e),this.emitEvent({type:"cache:data:evict",key:e,timestamp:Date.now(),reason:"size_limit_zero"}),this.updateMetrics("evictions")}else{const e=Array.from(this.cache.entries()).sort((([,e],[,t])=>e.lastAccessed-t.lastAccessed)),i=this.cache.size-t;if(i>0){e.slice(0,i).forEach((([e])=>{this.cache.delete(e)&&(s++,this.emitEvent({type:"cache:data:evict",key:e,timestamp:Date.now(),reason:"size_limit_lru"}),this.updateMetrics("evictions"))}))}}s>0&&e&&this.schedulePersistState()}startGarbageCollection(){const{cacheTime:e}=this.defaultOptions;if(e===1/0||e<=0)return;const t=Math.max(1e3,Math.min(e/4,3e5));this.gcTimer=setInterval((()=>this.garbageCollect()),t)}garbageCollect(){const e=Date.now();let t=0;const s=[];for(const[t,i]of this.cache){if(i.isLoading)continue;const a=this.queries.get(t),r=a?.options.cacheTime??this.defaultOptions.cacheTime;r===1/0||r<=0||e-i.lastAccessed>r&&s.push(t)}return s.length>0&&(s.forEach((e=>{this.cache.delete(e)&&(this.fetching.delete(e),this.emitEvent({type:"cache:data:evict",key:e,timestamp:Date.now(),reason:"garbage_collected_idle"}),this.updateMetrics("evictions"),t++)})),this.schedulePersistState()),t}getStats(){const e=this.metrics.hits+this.metrics.misses,t=e>0?this.metrics.hits/e:0,s=this.metrics.hits>0?this.metrics.staleHits/this.metrics.hits:0,i=Array.from(this.cache.entries()).map((([e,t])=>{const s=this.queries.get(e),i=!s||this.isStale(t,s.options);return{key:e,lastAccessed:t.lastAccessed,lastUpdated:t.lastUpdated,accessCount:t.accessCount,isStale:i,isLoading:t.isLoading,error:!!t.error}}));return{size:this.cache.size,metrics:{...this.metrics},hitRate:t,staleHitRate:s,entries:i}}on(e,t){return this.eventBus.subscribe(e,t)}emitEvent(e){this.eventBus.emit({name:e.type,payload:e})}updateMetrics(e,t=1){this.defaultOptions.enableMetrics&&(this.metrics[e]=(this.metrics[e]||0)+t)}delay(e){return new Promise((t=>setTimeout(t,e)))}async clear(){const e=this.cache.size>0;if(this.cache.clear(),this.fetching.clear(),this.defaultOptions.enableMetrics&&(this.metrics={hits:0,misses:0,fetches:0,errors:0,evictions:0,staleHits:0}),this.defaultOptions.persistence)try{await this.defaultOptions.persistence.clear(),this.emitEvent({type:"cache:persistence:clear:success",key:this.persistenceId,timestamp:Date.now()})}catch(e){console.error(`Cache (${this.persistenceId}): Failed to clear persisted state:`,e),this.emitEvent({type:"cache:persistence:clear:error",key:this.persistenceId,timestamp:Date.now(),error:e})}else e&&this.schedulePersistState()}destroy(){if(this.gcTimer&&(clearInterval(this.gcTimer),this.gcTimer=void 0),this.persistenceDebounceTimer&&clearTimeout(this.persistenceDebounceTimer),this.persistenceUnsubscribe)try{this.persistenceUnsubscribe()}catch(e){console.error(`Cache (${this.persistenceId}): Error unsubscribing persistence:`,e)}this.cache.clear(),this.fetching.clear(),this.queries.clear(),this.eventBus.clear(),this.metrics={hits:0,misses:0,fetches:0,errors:0,evictions:0,staleHits:0}}};
package/index.mjs CHANGED
@@ -1 +1 @@
1
- import{v4 as e}from"uuid";var t=class{cache=new Map;queries=new Map;fetching=new Map;defaultOptions;metrics;eventListeners=new Map;gcTimer;persistenceId;persistenceUnsubscribe;persistenceDebounceTimer;isHandlingRemoteUpdate=!1;constructor(t={}){void 0!==t.staleTime&&t.staleTime<0&&(console.warn("CacheOptions: staleTime should be non-negative. Using 0."),t.staleTime=0),void 0!==t.cacheTime&&t.cacheTime<0&&(console.warn("CacheOptions: cacheTime should be non-negative. Using 0."),t.cacheTime=0),void 0!==t.retryAttempts&&t.retryAttempts<0&&(console.warn("CacheOptions: retryAttempts should be non-negative. Using 0."),t.retryAttempts=0),void 0!==t.retryDelay&&t.retryDelay<0&&(console.warn("CacheOptions: retryDelay should be non-negative. Using 0."),t.retryDelay=0),void 0!==t.maxSize&&t.maxSize<0&&(console.warn("CacheOptions: maxSize should be non-negative. Using 0."),t.maxSize=0),this.defaultOptions={staleTime:3e5,cacheTime:18e5,retryAttempts:3,retryDelay:1e3,maxSize:1e3,enableMetrics:!0,persistence:void 0,persistenceId:void 0,serializeValue:e=>e,deserializeValue:e=>e,persistenceDebounceTime:500,...t},this.metrics={hits:0,misses:0,fetches:0,errors:0,evictions:0,staleHits:0},this.persistenceId=this.defaultOptions.persistenceId||e(),this.startGarbageCollection(),this.initializePersistence()}async initializePersistence(){const{persistence:e}=this.defaultOptions;if(e){try{const t=await e.get();t&&(this.deserializeAndLoadCache(t),this.emitEvent({type:"persistence",key:this.persistenceId,timestamp:Date.now(),event:"load_success",message:`Cache loaded for ID: ${this.persistenceId}`}))}catch(e){console.error(`Cache (${this.persistenceId}): Failed to load state from persistence:`,e),this.emitEvent({type:"persistence",key:this.persistenceId,timestamp:Date.now(),event:"load_fail",error:e,message:`Failed to load cache for ID: ${this.persistenceId}`})}if("function"==typeof e.subscribe)try{this.persistenceUnsubscribe=e.subscribe(this.persistenceId,(e=>{this.handleRemoteStateChange(e)}))}catch(e){console.error(`Cache (${this.persistenceId}): Failed to subscribe to persistence:`,e)}}}serializeCache(){const e=[],{serializeValue:t}=this.defaultOptions;for(const[s,i]of this.cache)i.isLoading&&void 0===i.data&&0===i.lastUpdated||e.push([s,{data:t(i.data),lastUpdated:i.lastUpdated,lastAccessed:i.lastAccessed,accessCount:i.accessCount,error:i.error?{name:i.error.name,message:i.error.message,stack:i.error.stack}:void 0}]);return e}deserializeAndLoadCache(e){this.isHandlingRemoteUpdate=!0;const t=new Map,{deserializeValue:s}=this.defaultOptions;for(const[i,a]of e){let e;a.error&&(e=new Error(a.error.message),e.name=a.error.name,e.stack=a.error.stack),t.set(i,{data:s(a.data),lastUpdated:a.lastUpdated,lastAccessed:a.lastAccessed,accessCount:a.accessCount,error:e,isLoading:!1})}this.cache=t,this.enforceSizeLimit(!1),this.isHandlingRemoteUpdate=!1}schedulePersistState(){this.defaultOptions.persistence&&!this.isHandlingRemoteUpdate&&(this.persistenceDebounceTimer&&clearTimeout(this.persistenceDebounceTimer),this.persistenceDebounceTimer=setTimeout((async()=>{try{const e=this.serializeCache();await this.defaultOptions.persistence.set(this.persistenceId,e),this.emitEvent({type:"persistence",key:this.persistenceId,timestamp:Date.now(),event:"save_success"})}catch(e){console.error(`Cache (${this.persistenceId}): Failed to persist state:`,e),this.emitEvent({type:"persistence",key:this.persistenceId,timestamp:Date.now(),event:"save_fail",error:e})}}),this.defaultOptions.persistenceDebounceTime))}handleRemoteStateChange(e){if(this.isHandlingRemoteUpdate||!e)return;this.isHandlingRemoteUpdate=!0;const{deserializeValue:t}=this.defaultOptions,s=new Map;let i=!1;for(const[a,r]of e){let e;r.error&&(e=new Error(r.error.message),e.name=r.error.name,e.stack=r.error.stack);const c={data:t(r.data),lastUpdated:r.lastUpdated,lastAccessed:r.lastAccessed,accessCount:r.accessCount,error:e,isLoading:!1};s.set(a,c);const n=this.cache.get(a);(!n||n.lastUpdated<c.lastUpdated||JSON.stringify(n.data)!==JSON.stringify(c.data))&&(i=!0)}this.cache.size!==s.size&&(i=!0),i&&(this.cache=s,this.enforceSizeLimit(!1),this.emitEvent({type:"persistence",key:this.persistenceId,timestamp:Date.now(),event:"remote_update",message:"Cache updated from remote state."})),this.isHandlingRemoteUpdate=!1}registerQuery(e,t,s={}){void 0!==s.staleTime&&s.staleTime<0&&(s.staleTime=0),void 0!==s.cacheTime&&s.cacheTime<0&&(s.cacheTime=0),this.queries.set(e,{fetchFunction:t,options:{...this.defaultOptions,...s}})}async get(e,t){const s=this.queries.get(e);if(!s)throw new Error(`No query registered for key: ${e}`);let i=this.cache.get(e);const a=this.isStale(i,s.options);let r=!1;if(i)i.lastAccessed=Date.now(),i.accessCount++,this.updateMetrics("hits"),a&&this.updateMetrics("staleHits"),this.emitEvent({type:"hit",key:e,timestamp:Date.now(),data:i.data,isStale:a});else if(this.updateMetrics("misses"),this.emitEvent({type:"miss",key:e,timestamp:Date.now()}),!t?.waitForFresh||a){const t={data:void 0,lastUpdated:0,lastAccessed:Date.now(),accessCount:1,isLoading:!0,error:void 0};this.cache.set(e,t),i=t,r=!0}if(t?.waitForFresh&&(!i||a||i.isLoading))try{const t=await this.fetchAndWait(e,s);return r&&this.cache.get(e)===i&&this.schedulePersistState(),t}catch(s){if(t.throwOnError)throw s;return this.cache.get(e)?.data}if(!i||a||i&&!i.isLoading&&0===i.lastUpdated&&!i.error){if(i&&!i.isLoading)i.isLoading=!0;else if(!i){const t={data:void 0,lastUpdated:0,lastAccessed:Date.now(),accessCount:0,isLoading:!0,error:void 0};this.cache.set(e,t),i=t,r=!0}this.fetch(e,s).catch((()=>{}))}if(r&&this.schedulePersistState(),i?.error&&t?.throwOnError)throw i.error;return i?.data}peek(e){const t=this.cache.get(e);return t&&(t.lastAccessed=Date.now(),t.accessCount++),t?.data}has(e){const t=this.cache.get(e),s=this.queries.get(e);return!(!t||!s)&&(!this.isStale(t,s.options)&&!t.isLoading)}async fetch(e,t){if(this.fetching.has(e))return this.fetching.get(e);let s=this.cache.get(e);s?s.isLoading||(s.isLoading=!0,s.error=void 0):(s={data:void 0,lastUpdated:0,lastAccessed:Date.now(),accessCount:0,isLoading:!0,error:void 0},this.cache.set(e,s),this.schedulePersistState());const i=this.performFetchWithRetry(e,t,s);this.fetching.set(e,i);try{return await i}finally{this.fetching.delete(e)}}async fetchAndWait(e,t){const s=this.fetching.get(e);if(s)return s;const i=await this.fetch(e,t);if(void 0===i){const t=this.cache.get(e);if(t?.error)throw t.error;throw new Error(`Failed to fetch data for key: ${e} after retries.`)}return i}async performFetchWithRetry(e,t,s){const{retryAttempts:i,retryDelay:a}=t.options;let r;s.isLoading=!0;for(let c=0;c<=i;c++)try{this.emitEvent({type:"fetch",key:e,timestamp:Date.now(),attempt:c}),this.updateMetrics("fetches");const i=await t.fetchFunction();return s.data=i,s.lastUpdated=Date.now(),s.isLoading=!1,s.error=void 0,this.cache.set(e,s),this.schedulePersistState(),this.enforceSizeLimit(),i}catch(t){r=t,this.emitEvent({type:"error",key:e,timestamp:Date.now(),error:r,attempt:c}),c<i&&await this.delay(a*Math.pow(2,c))}this.updateMetrics("errors"),s.error=r,s.isLoading=!1,this.cache.set(e,s),this.schedulePersistState()}isStale(e,t){if(!e||e.error)return!0;if(e.isLoading&&!e.data)return!0;const{staleTime:s}=t;return 0!==s&&s!==1/0&&Date.now()-e.lastUpdated>s}async invalidate(e,t=!0){const s=this.cache.get(e),i=this.queries.get(e);let a=!1;s&&(a=0!==s.lastUpdated||void 0!==s.error,s.lastUpdated=0,s.error=void 0,this.emitEvent({type:"invalidation",key:e,timestamp:Date.now()}),a&&this.schedulePersistState(),t&&i&&this.fetch(e,i).catch((()=>{})))}async invalidatePattern(e,t=!0){const s=[];let i=!1;for(const t of this.cache.keys())e.test(t)&&s.push(t);s.forEach((e=>{const t=this.cache.get(e);t&&(0===t.lastUpdated&&void 0===t.error||(i=!0),t.lastUpdated=0,t.error=void 0,this.emitEvent({type:"invalidation",key:e,timestamp:Date.now()}))})),i&&this.schedulePersistState(),t&&s.length>0&&await Promise.all(s.map((e=>{const t=this.queries.get(e);return t?this.fetch(e,t).catch((()=>{})):Promise.resolve()})))}async prefetch(e){const t=this.queries.get(e);if(!t)return void console.warn(`Cannot prefetch: No query registered for key: ${e}`);const s=this.cache.get(e);s&&!this.isStale(s,t.options)||this.fetch(e,t).catch((()=>{}))}async refresh(e){const t=this.queries.get(e);if(!t)return void console.warn(`Cannot refresh: No query registered for key: ${e}`);this.fetching.delete(e);let s=this.cache.get(e);s?(s.isLoading=!0,s.error=void 0):(s={data:void 0,lastUpdated:0,lastAccessed:Date.now(),accessCount:0,isLoading:!0,error:void 0},this.cache.set(e,s),this.schedulePersistState());const i=this.performFetchWithRetry(e,t,s);this.fetching.set(e,i);try{return await i}finally{this.fetching.delete(e)}}setData(e,t){const s=this.cache.get(e),i=s?.data,a={data:t,lastUpdated:Date.now(),lastAccessed:Date.now(),accessCount:(s?.accessCount||0)+1,isLoading:!1,error:void 0};this.cache.set(e,a),this.schedulePersistState(),this.enforceSizeLimit(),this.emitEvent({type:"set_data",key:e,timestamp:Date.now(),newData:t,oldData:i})}remove(e){this.fetching.delete(e);const t=this.cache.has(e),s=this.cache.delete(e);return s&&t&&this.schedulePersistState(),s}enforceSizeLimit(e=!0){const{maxSize:t}=this.defaultOptions;if(t===1/0||this.cache.size<=t)return;let s=0;if(0===t){s=this.cache.size;for(const e of this.cache.keys())this.cache.delete(e),this.emitEvent({type:"eviction",key:e,timestamp:Date.now(),reason:"size_limit_zero"}),this.updateMetrics("evictions")}else{const e=Array.from(this.cache.entries()).sort((([,e],[,t])=>e.lastAccessed-t.lastAccessed)),i=this.cache.size-t;if(i>0){e.slice(0,i).forEach((([e])=>{this.cache.delete(e)&&(s++,this.emitEvent({type:"eviction",key:e,timestamp:Date.now(),reason:"size_limit_lru"}),this.updateMetrics("evictions"))}))}}s>0&&e&&this.schedulePersistState()}startGarbageCollection(){const{cacheTime:e}=this.defaultOptions;if(e===1/0||e<=0)return;const t=Math.max(1e3,Math.min(e/4,3e5));this.gcTimer=setInterval((()=>this.garbageCollect()),t)}garbageCollect(){const e=Date.now();let t=0;const s=[];for(const[t,i]of this.cache){if(i.isLoading)continue;const a=this.queries.get(t),r=a?.options.cacheTime??this.defaultOptions.cacheTime;r===1/0||r<=0||e-i.lastAccessed>r&&s.push(t)}return s.length>0&&(s.forEach((e=>{this.cache.delete(e)&&(this.fetching.delete(e),this.emitEvent({type:"eviction",key:e,timestamp:Date.now(),reason:"garbage_collected_idle"}),this.updateMetrics("evictions"),t++)})),this.schedulePersistState()),t}getStats(){const e=this.metrics.hits+this.metrics.misses,t=e>0?this.metrics.hits/e:0,s=this.metrics.hits>0?this.metrics.staleHits/this.metrics.hits:0,i=Array.from(this.cache.entries()).map((([e,t])=>{const s=this.queries.get(e),i=!s||this.isStale(t,s.options);return{key:e,lastAccessed:t.lastAccessed,lastUpdated:t.lastUpdated,accessCount:t.accessCount,isStale:i,isLoading:t.isLoading,error:!!t.error}}));return{size:this.cache.size,metrics:{...this.metrics},hitRate:t,staleHitRate:s,entries:i}}on(e,t){this.eventListeners.has(e)||this.eventListeners.set(e,new Set),this.eventListeners.get(e).add(t)}off(e,t){this.eventListeners.get(e)?.delete(t)}emitEvent(e){const t=this.eventListeners.get(e.type);t&&t.forEach((t=>{try{t(e)}catch(t){const s="persistence"===e.type?`for ID ${e.key}`:`for key ${e.key}`;console.error(`Cache event listener error during ${e.type} ${s}:`,t)}}))}updateMetrics(e,t=1){this.defaultOptions.enableMetrics&&(this.metrics[e]=(this.metrics[e]||0)+t)}delay(e){return new Promise((t=>setTimeout(t,e)))}async clear(){const e=this.cache.size>0;if(this.cache.clear(),this.fetching.clear(),this.defaultOptions.enableMetrics&&(this.metrics={hits:0,misses:0,fetches:0,errors:0,evictions:0,staleHits:0}),this.defaultOptions.persistence)try{await this.defaultOptions.persistence.clear(),this.emitEvent({type:"persistence",key:this.persistenceId,timestamp:Date.now(),event:"clear_success"})}catch(e){console.error(`Cache (${this.persistenceId}): Failed to clear persisted state:`,e),this.emitEvent({type:"persistence",key:this.persistenceId,timestamp:Date.now(),event:"clear_fail",error:e})}else e&&this.schedulePersistState()}destroy(){if(this.gcTimer&&(clearInterval(this.gcTimer),this.gcTimer=void 0),this.persistenceDebounceTimer&&clearTimeout(this.persistenceDebounceTimer),this.persistenceUnsubscribe)try{this.persistenceUnsubscribe()}catch(e){console.error(`Cache (${this.persistenceId}): Error unsubscribing persistence:`,e)}this.cache.clear(),this.fetching.clear(),this.queries.clear(),this.eventListeners.clear(),this.metrics={hits:0,misses:0,fetches:0,errors:0,evictions:0,staleHits:0}}};export{t as Cache};
1
+ import{v4 as e}from"uuid";var t,s,i=Object.create,a=Object.defineProperty,r=Object.getOwnPropertyDescriptor,c=Object.getOwnPropertyNames,n=Object.getPrototypeOf,o=Object.prototype.hasOwnProperty,h=(t={"node_modules/@asaidimu/events/index.js"(e,t){var s,i=Object.defineProperty,a=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,c=Object.prototype.hasOwnProperty,n={};((e,t)=>{for(var s in t)i(e,s,{get:t[s],enumerable:!0})})(n,{createEventBus:()=>o}),t.exports=(s=n,((e,t,s,n)=>{if(t&&"object"==typeof t||"function"==typeof t)for(let o of r(t))c.call(e,o)||o===s||i(e,o,{get:()=>t[o],enumerable:!(n=a(t,o))||n.enumerable});return e})(i({},"__esModule",{value:!0}),s));var o=(e={async:!1,batchSize:1e3,batchDelay:16,errorHandler:e=>console.error("EventBus Error:",e),crossTab:!1,channelName:"event-bus-channel"})=>{const t=new Map;let s=[],i=0,a=0;const r=new Map,c=new Map;let n=null;e.crossTab&&"undefined"!=typeof BroadcastChannel?n=new BroadcastChannel(e.channelName):e.crossTab&&console.warn("BroadcastChannel is not supported in this browser. Cross-tab notifications are disabled.");const o=(e,t)=>{i++,a+=t,r.set(e,(r.get(e)||0)+1)},h=()=>{const t=s;s=[],t.forEach((({name:t,payload:s})=>{const i=performance.now();try{(c.get(t)||[]).forEach((e=>e(s)))}catch(i){e.errorHandler({...i,eventName:t,payload:s})}o(t,performance.now()-i)}))},d=(()=>{let t;return()=>{clearTimeout(t),t=setTimeout(h,e.batchDelay)}})(),l=e=>{const s=t.get(e);s?c.set(e,Array.from(s)):c.delete(e)};return n&&(n.onmessage=e=>{const{name:t,payload:s}=e.data;(c.get(t)||[]).forEach((e=>e(s)))}),{subscribe:(e,s)=>{t.has(e)||t.set(e,new Set);const i=t.get(e);return i.add(s),l(e),()=>{i.delete(s),0===i.size?(t.delete(e),c.delete(e)):l(e)}},emit:({name:t,payload:i})=>{if(e.async)return s.push({name:t,payload:i}),s.length>=e.batchSize?h():d(),void(n&&n.postMessage({name:t,payload:i}));const a=performance.now();try{(c.get(t)||[]).forEach((e=>e(i))),n&&n.postMessage({name:t,payload:i})}catch(s){e.errorHandler({...s,eventName:t,payload:i})}o(t,performance.now()-a)},getMetrics:()=>({totalEvents:i,activeSubscriptions:Array.from(t.values()).reduce(((e,t)=>e+t.size),0),eventCounts:r,averageEmitDuration:i>0?a/i:0}),clear:()=>{t.clear(),c.clear(),s=[],i=0,a=0,r.clear(),n&&(n.close(),n=null)}}}}},function(){return s||(0,t[c(t)[0]])((s={exports:{}}).exports,s),s.exports}),d=((e,t,s)=>(s=null!=e?i(n(e)):{},((e,t,s,i)=>{if(t&&"object"==typeof t||"function"==typeof t)for(let n of c(t))o.call(e,n)||n===s||a(e,n,{get:()=>t[n],enumerable:!(i=r(t,n))||i.enumerable});return e})(e&&e.__esModule?s:a(s,"default",{value:e,enumerable:!0}),e)))(h()),l=class{cache=new Map;queries=new Map;fetching=new Map;defaultOptions;metrics;eventBus;gcTimer;persistenceId;persistenceUnsubscribe;persistenceDebounceTimer;isHandlingRemoteUpdate=!1;constructor(t={}){void 0!==t.staleTime&&t.staleTime<0&&(console.warn("CacheOptions: staleTime should be non-negative. Using 0."),t.staleTime=0),void 0!==t.cacheTime&&t.cacheTime<0&&(console.warn("CacheOptions: cacheTime should be non-negative. Using 0."),t.cacheTime=0),void 0!==t.retryAttempts&&t.retryAttempts<0&&(console.warn("CacheOptions: retryAttempts should be non-negative. Using 0."),t.retryAttempts=0),void 0!==t.retryDelay&&t.retryDelay<0&&(console.warn("CacheOptions: retryDelay should be non-negative. Using 0."),t.retryDelay=0),void 0!==t.maxSize&&t.maxSize<0&&(console.warn("CacheOptions: maxSize should be non-negative. Using 0."),t.maxSize=0),this.defaultOptions={staleTime:0,cacheTime:18e5,retryAttempts:3,retryDelay:1e3,maxSize:1e3,enableMetrics:!0,persistence:void 0,persistenceId:void 0,serializeValue:e=>e,deserializeValue:e=>e,persistenceDebounceTime:500,...t},this.metrics={hits:0,misses:0,fetches:0,errors:0,evictions:0,staleHits:0},this.persistenceId=this.defaultOptions.persistenceId||e(),this.eventBus=(0,d.createEventBus)(),this.startGarbageCollection(),this.initializePersistence()}async initializePersistence(){const{persistence:e}=this.defaultOptions;if(e){try{const t=await e.get();t&&(this.deserializeAndLoadCache(t),this.emitEvent({type:"cache:persistence:load:success",key:this.persistenceId,timestamp:Date.now(),message:`Cache loaded for ID: ${this.persistenceId}`}))}catch(e){console.error(`Cache (${this.persistenceId}): Failed to load state from persistence:`,e),this.emitEvent({type:"cache:persistence:load:error",key:this.persistenceId,timestamp:Date.now(),error:e,message:`Failed to load cache for ID: ${this.persistenceId}`})}if("function"==typeof e.subscribe)try{this.persistenceUnsubscribe=e.subscribe(this.persistenceId,(e=>{this.handleRemoteStateChange(e)}))}catch(e){console.error(`Cache (${this.persistenceId}): Failed to subscribe to persistence:`,e)}}}serializeCache(){const e=[],{serializeValue:t}=this.defaultOptions;for(const[s,i]of this.cache)i.isLoading&&void 0===i.data&&0===i.lastUpdated||e.push([s,{data:t(i.data),lastUpdated:i.lastUpdated,lastAccessed:i.lastAccessed,accessCount:i.accessCount,error:i.error?{name:i.error.name,message:i.error.message,stack:i.error.stack}:void 0}]);return e}deserializeAndLoadCache(e){this.isHandlingRemoteUpdate=!0;const t=new Map,{deserializeValue:s}=this.defaultOptions;for(const[i,a]of e){let e;a.error&&(e=new Error(a.error.message),e.name=a.error.name,e.stack=a.error.stack),t.set(i,{data:s(a.data),lastUpdated:a.lastUpdated,lastAccessed:a.lastAccessed,accessCount:a.accessCount,error:e,isLoading:!1})}this.cache=t,this.enforceSizeLimit(!1),this.isHandlingRemoteUpdate=!1}schedulePersistState(){this.defaultOptions.persistence&&!this.isHandlingRemoteUpdate&&(this.persistenceDebounceTimer&&clearTimeout(this.persistenceDebounceTimer),this.persistenceDebounceTimer=setTimeout((async()=>{try{const e=this.serializeCache();await this.defaultOptions.persistence.set(this.persistenceId,e),this.emitEvent({type:"cache:persistence:save:success",key:this.persistenceId,timestamp:Date.now()})}catch(e){console.error(`Cache (${this.persistenceId}): Failed to persist state:`,e),this.emitEvent({type:"cache:persistence:save:error",key:this.persistenceId,timestamp:Date.now(),error:e})}}),this.defaultOptions.persistenceDebounceTime))}handleRemoteStateChange(e){if(this.isHandlingRemoteUpdate||!e)return;this.isHandlingRemoteUpdate=!0;const{deserializeValue:t}=this.defaultOptions,s=new Map;let i=!1;for(const[a,r]of e){let e;r.error&&(e=new Error(r.error.message),e.name=r.error.name,e.stack=r.error.stack);const c={data:t(r.data),lastUpdated:r.lastUpdated,lastAccessed:r.lastAccessed,accessCount:r.accessCount,error:e,isLoading:!1};s.set(a,c);const n=this.cache.get(a);(!n||n.lastUpdated<c.lastUpdated||JSON.stringify(n.data)!==JSON.stringify(c.data))&&(i=!0)}this.cache.size!==s.size&&(i=!0),i&&(this.cache=s,this.enforceSizeLimit(!1),this.emitEvent({type:"cache:persistence:sync",key:this.persistenceId,timestamp:Date.now(),message:"Cache updated from remote state."})),this.isHandlingRemoteUpdate=!1}registerQuery(e,t,s={}){void 0!==s.staleTime&&s.staleTime<0&&(s.staleTime=0),void 0!==s.cacheTime&&s.cacheTime<0&&(s.cacheTime=0),this.queries.set(e,{fetchFunction:t,options:{...this.defaultOptions,...s}})}async get(e,t){const s=this.queries.get(e);if(!s)throw new Error(`No query registered for key: ${e}`);let i=this.cache.get(e);const a=this.isStale(i,s.options);let r=!1;if(i)i.lastAccessed=Date.now(),i.accessCount++,this.updateMetrics("hits"),a&&this.updateMetrics("staleHits"),this.emitEvent({type:"cache:read:hit",key:e,timestamp:Date.now(),data:i.data,isStale:a});else if(this.updateMetrics("misses"),this.emitEvent({type:"cache:read:miss",key:e,timestamp:Date.now()}),!t?.waitForFresh||a){const t={data:void 0,lastUpdated:0,lastAccessed:Date.now(),accessCount:1,isLoading:!0,error:void 0};this.cache.set(e,t),i=t,r=!0}if(t?.waitForFresh&&(!i||a||i.isLoading))try{const t=await this.fetchAndWait(e,s);return r&&this.cache.get(e)===i&&this.schedulePersistState(),t}catch(s){if(t.throwOnError)throw s;return this.cache.get(e)?.data}if(!i||a||i&&!i.isLoading&&0===i.lastUpdated&&!i.error){if(i&&!i.isLoading)i.isLoading=!0;else if(!i){const t={data:void 0,lastUpdated:0,lastAccessed:Date.now(),accessCount:0,isLoading:!0,error:void 0};this.cache.set(e,t),i=t,r=!0}this.fetch(e,s).catch((()=>{}))}if(r&&this.schedulePersistState(),i?.error&&t?.throwOnError)throw i.error;return i?.data}peek(e){const t=this.cache.get(e);return t&&(t.lastAccessed=Date.now(),t.accessCount++),t?.data}has(e){const t=this.cache.get(e),s=this.queries.get(e);return!(!t||!s)&&(!this.isStale(t,s.options)&&!t.isLoading)}async fetch(e,t){if(this.fetching.has(e))return this.fetching.get(e);let s=this.cache.get(e);s?s.isLoading||(s.isLoading=!0,s.error=void 0):(s={data:void 0,lastUpdated:0,lastAccessed:Date.now(),accessCount:0,isLoading:!0,error:void 0},this.cache.set(e,s),this.schedulePersistState());const i=this.performFetchWithRetry(e,t,s);this.fetching.set(e,i);try{return await i}finally{this.fetching.delete(e)}}async fetchAndWait(e,t){const s=this.fetching.get(e);if(s)return s;const i=await this.fetch(e,t);if(void 0===i){const t=this.cache.get(e);if(t?.error)throw t.error;throw new Error(`Failed to fetch data for key: ${e} after retries.`)}return i}async performFetchWithRetry(e,t,s){const{retryAttempts:i,retryDelay:a}=t.options;let r;s.isLoading=!0;for(let c=0;c<=i;c++)try{this.emitEvent({type:"cache:fetch:start",key:e,timestamp:Date.now(),attempt:c}),this.updateMetrics("fetches");const i=await t.fetchFunction();return s.data=i,s.lastUpdated=Date.now(),s.isLoading=!1,s.error=void 0,this.cache.set(e,s),this.schedulePersistState(),this.enforceSizeLimit(),this.emitEvent({type:"cache:fetch:success",key:e,timestamp:Date.now(),data:i}),i}catch(t){r=t,this.updateMetrics("errors"),this.emitEvent({type:"cache:fetch:error",key:e,timestamp:Date.now(),error:r,attempt:c}),c<i&&await this.delay(a*Math.pow(2,c))}s.error=r,s.isLoading=!1,this.cache.set(e,s),this.schedulePersistState()}isStale(e,t){if(!e||e.error)return!0;if(e.isLoading&&!e.data)return!0;const{staleTime:s}=t;return 0!==s&&s!==1/0&&Date.now()-e.lastUpdated>s}async invalidate(e,t=!0){const s=this.cache.get(e),i=this.queries.get(e);let a=!1;s&&(a=0!==s.lastUpdated||void 0!==s.error,s.lastUpdated=0,s.error=void 0,this.emitEvent({type:"cache:data:invalidate",key:e,timestamp:Date.now()}),a&&this.schedulePersistState(),t&&i&&this.fetch(e,i).catch((()=>{})))}async invalidatePattern(e,t=!0){const s=[];let i=!1;for(const t of this.cache.keys())e.test(t)&&s.push(t);s.forEach((e=>{const t=this.cache.get(e);t&&(0===t.lastUpdated&&void 0===t.error||(i=!0),t.lastUpdated=0,t.error=void 0,this.emitEvent({type:"cache:data:invalidate",key:e,timestamp:Date.now()}))})),i&&this.schedulePersistState(),t&&s.length>0&&await Promise.all(s.map((e=>{const t=this.queries.get(e);return t?this.fetch(e,t).catch((()=>{})):Promise.resolve()})))}async prefetch(e){const t=this.queries.get(e);if(!t)return void console.warn(`Cannot prefetch: No query registered for key: ${e}`);const s=this.cache.get(e);s&&!this.isStale(s,t.options)||this.fetch(e,t).catch((()=>{}))}async refresh(e){const t=this.queries.get(e);if(!t)return void console.warn(`Cannot refresh: No query registered for key: ${e}`);this.fetching.delete(e);let s=this.cache.get(e);s?(s.isLoading=!0,s.error=void 0):(s={data:void 0,lastUpdated:0,lastAccessed:Date.now(),accessCount:0,isLoading:!0,error:void 0},this.cache.set(e,s),this.schedulePersistState());const i=this.performFetchWithRetry(e,t,s);this.fetching.set(e,i);try{return await i}finally{this.fetching.delete(e)}}setData(e,t){const s=this.cache.get(e),i=s?.data,a={data:t,lastUpdated:Date.now(),lastAccessed:Date.now(),accessCount:(s?.accessCount||0)+1,isLoading:!1,error:void 0};this.cache.set(e,a),this.schedulePersistState(),this.enforceSizeLimit(),this.emitEvent({type:"cache:data:set",key:e,timestamp:Date.now(),newData:t,oldData:i})}remove(e){this.fetching.delete(e);const t=this.cache.has(e),s=this.cache.delete(e);return s&&t&&this.schedulePersistState(),s}enforceSizeLimit(e=!0){const{maxSize:t}=this.defaultOptions;if(t===1/0||this.cache.size<=t)return;let s=0;if(0===t){s=this.cache.size;for(const e of this.cache.keys())this.cache.delete(e),this.emitEvent({type:"cache:data:evict",key:e,timestamp:Date.now(),reason:"size_limit_zero"}),this.updateMetrics("evictions")}else{const e=Array.from(this.cache.entries()).sort((([,e],[,t])=>e.lastAccessed-t.lastAccessed)),i=this.cache.size-t;if(i>0){e.slice(0,i).forEach((([e])=>{this.cache.delete(e)&&(s++,this.emitEvent({type:"cache:data:evict",key:e,timestamp:Date.now(),reason:"size_limit_lru"}),this.updateMetrics("evictions"))}))}}s>0&&e&&this.schedulePersistState()}startGarbageCollection(){const{cacheTime:e}=this.defaultOptions;if(e===1/0||e<=0)return;const t=Math.max(1e3,Math.min(e/4,3e5));this.gcTimer=setInterval((()=>this.garbageCollect()),t)}garbageCollect(){const e=Date.now();let t=0;const s=[];for(const[t,i]of this.cache){if(i.isLoading)continue;const a=this.queries.get(t),r=a?.options.cacheTime??this.defaultOptions.cacheTime;r===1/0||r<=0||e-i.lastAccessed>r&&s.push(t)}return s.length>0&&(s.forEach((e=>{this.cache.delete(e)&&(this.fetching.delete(e),this.emitEvent({type:"cache:data:evict",key:e,timestamp:Date.now(),reason:"garbage_collected_idle"}),this.updateMetrics("evictions"),t++)})),this.schedulePersistState()),t}getStats(){const e=this.metrics.hits+this.metrics.misses,t=e>0?this.metrics.hits/e:0,s=this.metrics.hits>0?this.metrics.staleHits/this.metrics.hits:0,i=Array.from(this.cache.entries()).map((([e,t])=>{const s=this.queries.get(e),i=!s||this.isStale(t,s.options);return{key:e,lastAccessed:t.lastAccessed,lastUpdated:t.lastUpdated,accessCount:t.accessCount,isStale:i,isLoading:t.isLoading,error:!!t.error}}));return{size:this.cache.size,metrics:{...this.metrics},hitRate:t,staleHitRate:s,entries:i}}on(e,t){return this.eventBus.subscribe(e,t)}emitEvent(e){this.eventBus.emit({name:e.type,payload:e})}updateMetrics(e,t=1){this.defaultOptions.enableMetrics&&(this.metrics[e]=(this.metrics[e]||0)+t)}delay(e){return new Promise((t=>setTimeout(t,e)))}async clear(){const e=this.cache.size>0;if(this.cache.clear(),this.fetching.clear(),this.defaultOptions.enableMetrics&&(this.metrics={hits:0,misses:0,fetches:0,errors:0,evictions:0,staleHits:0}),this.defaultOptions.persistence)try{await this.defaultOptions.persistence.clear(),this.emitEvent({type:"cache:persistence:clear:success",key:this.persistenceId,timestamp:Date.now()})}catch(e){console.error(`Cache (${this.persistenceId}): Failed to clear persisted state:`,e),this.emitEvent({type:"cache:persistence:clear:error",key:this.persistenceId,timestamp:Date.now(),error:e})}else e&&this.schedulePersistState()}destroy(){if(this.gcTimer&&(clearInterval(this.gcTimer),this.gcTimer=void 0),this.persistenceDebounceTimer&&clearTimeout(this.persistenceDebounceTimer),this.persistenceUnsubscribe)try{this.persistenceUnsubscribe()}catch(e){console.error(`Cache (${this.persistenceId}): Error unsubscribing persistence:`,e)}this.cache.clear(),this.fetching.clear(),this.queries.clear(),this.eventBus.clear(),this.metrics={hits:0,misses:0,fetches:0,errors:0,evictions:0,staleHits:0}}};export{l as QueryCache};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@asaidimu/utils-cache",
3
- "version": "2.0.5",
4
- "description": "Caching utilities for @asaidimu applications.",
3
+ "version": "2.1.1",
4
+ "description": "Resource and cache management utilities for @asaidimu applications.",
5
5
  "main": "index.js",
6
6
  "module": "index.mjs",
7
7
  "types": "index.d.ts",
@@ -10,6 +10,7 @@
10
10
  ],
11
11
  "keywords": [
12
12
  "typescript",
13
+ "query",
13
14
  "cache",
14
15
  "utility"
15
16
  ],
@@ -29,8 +30,9 @@
29
30
  "access": "public"
30
31
  },
31
32
  "dependencies": {
32
- "@asaidimu/utils-persistence": "2.0.4",
33
- "uuid": "^11.1.0"
33
+ "@asaidimu/utils-persistence": "2.1.0",
34
+ "uuid": "^11.1.0",
35
+ "@asaidimu/events": "^1.1.1"
34
36
  },
35
37
  "exports": {
36
38
  ".": {