@asaidimu/utils-cache 2.0.4 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -56
- package/index.d.mts +45 -24
- package/index.d.ts +45 -24
- package/index.js +1 -1
- package/index.mjs +1 -1
- package/package.json +6 -4
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'`, `'
|
|
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`
|
|
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'`, `'
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
//
|
|
426
|
-
|
|
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:
|
|
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('
|
|
643
|
+
monitorCache.on('cache:data:evict', (e) => {
|
|
655
644
|
console.log(`[EVENT] Evicted ${e.key} due to ${e.reason}`);
|
|
656
645
|
});
|
|
657
|
-
monitorCache.on('
|
|
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
|
-
|
|
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'`, `'
|
|
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,
|
|
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
|
|
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
|
|
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 `'
|
|
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 `'
|
|
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
|
|
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
|
|
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. **
|
|
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-
|
|
1
|
+
import { S as SimplePersistence } from '../types-DUZGkNEB.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
|
|
53
|
+
type CacheReadHitEvent<T = any> = CacheEventBase<'cache:read:hit', {
|
|
42
54
|
data: T;
|
|
43
55
|
isStale: boolean;
|
|
44
56
|
}>;
|
|
45
|
-
type
|
|
46
|
-
type
|
|
57
|
+
type CacheReadMissEvent = CacheEventBase<'cache:read:miss'>;
|
|
58
|
+
type CacheFetchStartEvent = CacheEventBase<'cache:fetch:start', {
|
|
47
59
|
attempt: number;
|
|
48
60
|
}>;
|
|
49
|
-
type
|
|
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
|
|
68
|
+
type CacheDataEvictEvent = CacheEventBase<'cache:data:evict', {
|
|
54
69
|
reason?: string;
|
|
55
70
|
}>;
|
|
56
|
-
type
|
|
57
|
-
type
|
|
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
|
|
62
|
-
event: 'load_success' | 'remote_update';
|
|
76
|
+
type CachePersistenceLoadSuccessEvent = CacheEventBase<'cache:persistence:load:success', {
|
|
63
77
|
message?: string;
|
|
64
|
-
}
|
|
65
|
-
|
|
78
|
+
}>;
|
|
79
|
+
type CachePersistenceLoadErrorEvent = CacheEventBase<'cache:persistence:load:error', {
|
|
66
80
|
message?: string;
|
|
67
81
|
error?: any;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
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
|
|
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 {
|
|
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-
|
|
1
|
+
import { S as SimplePersistence } from '../types-DUZGkNEB.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
|
|
53
|
+
type CacheReadHitEvent<T = any> = CacheEventBase<'cache:read:hit', {
|
|
42
54
|
data: T;
|
|
43
55
|
isStale: boolean;
|
|
44
56
|
}>;
|
|
45
|
-
type
|
|
46
|
-
type
|
|
57
|
+
type CacheReadMissEvent = CacheEventBase<'cache:read:miss'>;
|
|
58
|
+
type CacheFetchStartEvent = CacheEventBase<'cache:fetch:start', {
|
|
47
59
|
attempt: number;
|
|
48
60
|
}>;
|
|
49
|
-
type
|
|
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
|
|
68
|
+
type CacheDataEvictEvent = CacheEventBase<'cache:data:evict', {
|
|
54
69
|
reason?: string;
|
|
55
70
|
}>;
|
|
56
|
-
type
|
|
57
|
-
type
|
|
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
|
|
62
|
-
event: 'load_success' | 'remote_update';
|
|
76
|
+
type CachePersistenceLoadSuccessEvent = CacheEventBase<'cache:persistence:load:success', {
|
|
63
77
|
message?: string;
|
|
64
|
-
}
|
|
65
|
-
|
|
78
|
+
}>;
|
|
79
|
+
type CachePersistenceLoadErrorEvent = CacheEventBase<'cache:persistence:load:error', {
|
|
66
80
|
message?: string;
|
|
67
81
|
error?: any;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.1.0",
|
|
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": "
|
|
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
|
".": {
|