@asaidimu/utils-cache 2.1.2 → 2.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/index.js +1 -1
  2. package/index.mjs +1 -1
  3. package/package.json +2 -2
package/index.js CHANGED
@@ -1 +1 @@
1
- "use strict";var e=require("./cache"),t=require("./types");Object.keys(e).forEach((function(t){"default"===t||Object.prototype.hasOwnProperty.call(exports,t)||Object.defineProperty(exports,t,{enumerable:!0,get:function(){return e[t]}})})),Object.keys(t).forEach((function(e){"default"===e||Object.prototype.hasOwnProperty.call(exports,e)||Object.defineProperty(exports,e,{enumerable:!0,get:function(){return t[e]}})}));
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
- export*from"./cache";export*from"./types";
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@asaidimu/utils-cache",
3
- "version": "2.1.2",
3
+ "version": "2.1.3",
4
4
  "description": "Resource and cache management utilities for @asaidimu applications.",
5
5
  "main": "index.js",
6
6
  "module": "index.mjs",
@@ -30,7 +30,7 @@
30
30
  "access": "public"
31
31
  },
32
32
  "dependencies": {
33
- "@asaidimu/utils-persistence": "2.2.0",
33
+ "@asaidimu/utils-persistence": "2.2.1",
34
34
  "uuid": "^11.1.0",
35
35
  "@asaidimu/events": "^1.1.1"
36
36
  },