@asaidimu/utils-store 10.2.15 → 10.2.17

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/index.mjs CHANGED
@@ -1 +1,2402 @@
1
- import{createEventBus as e}from"@asaidimu/utils-events";import{Debouncer as t,Latch as n,Once as r,Serializer as i,SharedResource as a}from"@asaidimu/utils-sync";import{v4 as o}from"uuid";import{Logger as s}from"@asaidimu/utils-logger";const c=Symbol.for(`delete`),l=e=>Array.isArray(e)?[...e]:{...e};function u(e){let t=e?.deleteMarker||c;function n(e){if(e==null)return e;if(Array.isArray(e))return e.filter(e=>e!==t).map(e=>typeof e==`object`&&e&&!Array.isArray(e)?n(e):e);if(typeof e==`object`){let r={};for(let[i,a]of Object.entries(e))if(a!==t)if(typeof a==`object`&&a){let e=n(a);e!==void 0&&(r[i]=e)}else r[i]=a;return r}return e===t?void 0:e}function r(e,r){if(typeof e!=`object`||!e)return typeof r==`object`&&r?n(r):r===t?{}:r;if(typeof r!=`object`||!r)return e;let i=l(e),a=[{target:i,source:r}];for(;a.length>0;){let{target:e,source:n}=a.pop();for(let r of Object.keys(n)){let i=n[r];if(i===t){delete e[r];continue}if(Array.isArray(i)){e[r]=i;continue}typeof i==`object`&&i?(e[r]=l(r in e&&typeof e[r]==`object`&&e[r]!==null?e[r]:{}),a.push({target:e[r],source:i})):e[r]=i}}return i}return r}const d=u(),f=[`map`,`filter`,`reduce`,`forEach`,`find`,`findIndex`,`some`,`every`,`includes`,`flatMap`,`flat`,`slice`,`splice`];var p=class{reactiveSelectors=new Map;pathBasedCache=new Map;dependencyMap=new Map;getState;eventBus;unsubscribeFromStore;constructor(e,t){this.getState=e,this.eventBus=t,this.unsubscribeFromStore=this.eventBus.subscribe(`update:complete`,this.handleStoreUpdate)}handleStoreUpdate=e=>{let t=new Set;for(let n of e.deltas){let e=n.path;for(let[n,r]of this.dependencyMap)if(n===e||n.startsWith(e+`.`)||e.startsWith(n+`.`))for(let e of r)t.add(e)}for(let e of t){let t=this.reactiveSelectors.get(e);t&&this.evaluateEntry(t)}};evaluateEntry(e){let t;try{t=e.selector(this.getState())}catch{t=void 0}if(t!==e.lastResult){e.lastResult=t;for(let n of e.subscribers)n(t);this.eventBus.emit({name:`selector:changed`,payload:{selectorId:e.id,newResult:t,timestamp:Date.now()}})}}createReactiveSelector(e){let t=m(e),n=[...t].sort().join(`|`),r=this.pathBasedCache.get(n);if(r)return r.cleanupTimer!==void 0&&(clearTimeout(r.cleanupTimer),r.cleanupTimer=void 0),r.reactiveSelectorInstance;let i=`sel-${Math.random().toString(36).slice(2,9)}`,a={id:i,selector:e,lastResult:e(this.getState()),accessedPaths:t,subscribers:new Set,count:0,cleanupTimer:void 0,pathCacheKey:n,reactiveSelectorInstance:null};for(let e of t)this.dependencyMap.has(e)||this.dependencyMap.set(e,new Set),this.dependencyMap.get(e).add(i);let o={id:i,get:()=>{try{return a.selector(this.getState())}catch{return}},subscribe:e=>(a.cleanupTimer!==void 0&&(clearTimeout(a.cleanupTimer),a.cleanupTimer=void 0),a.subscribers.add(e),a.count++,()=>{a.subscribers.delete(e),a.count--,a.count===0&&(a.cleanupTimer=setTimeout(()=>{a.count===0&&this.evictEntry(a)},0))})};return a.reactiveSelectorInstance=o,this.reactiveSelectors.set(i,a),this.pathBasedCache.set(n,a),this.eventBus.emit({name:`selector:accessed`,payload:{selectorId:i,accessedPaths:t,duration:0,timestamp:Date.now()}}),o}evictEntry(e){for(let t of e.accessedPaths){let n=this.dependencyMap.get(t);n&&(n.delete(e.id),n.size===0&&this.dependencyMap.delete(t))}this.reactiveSelectors.delete(e.id),this.pathBasedCache.delete(e.pathCacheKey)}dispose(){this.unsubscribeFromStore(),this.reactiveSelectors.clear(),this.dependencyMap.clear(),this.pathBasedCache.clear()}};function m(e,t=`.`){let n=new Set,r=new Map,i=(e=``)=>{if(r.has(e))return r.get(e);let a=new Proxy(()=>{},{get:(r,a)=>{if(typeof a==`symbol`||a===`then`)return;if(a===`valueOf`||a===`toString`)throw Error(`Cannot perform logic, arithmetic, or string operations inside a selector.`);if(f.includes(a))throw Error(`Array method .${a}() is not allowed in selectors.`);let o=e?`${e}${t}${a}`:a;return e&&n.delete(e),n.add(o),i(o)},has:()=>{throw Error(`The 'in' operator is not allowed in selectors.`)},apply:()=>{throw Error(`Selectors cannot call functions or methods.`)}});return r.set(e,a),a};try{e(i())}catch(e){throw Error(`Selector failed during path analysis. Selectors must be simple property accessors only. Error: ${e instanceof Error?e.message:String(e)}`)}return Array.from(n)}function h(e,t){if(e===t)return!0;if(e&&t&&typeof e==`object`&&typeof t==`object`){if(e.constructor!==t.constructor)return!1;let n,r;if(Array.isArray(e)){if(n=e.length,n!=t.length)return!1;for(r=n;r-->0;)if(!h(e[r],t[r]))return!1;return!0}let[i,a]=[Object.keys(e),Object.keys(t)];if(n=i.length,n!==a.length)return!1;for(r=n;r-->0;){let n=i[r];if(!Object.prototype.hasOwnProperty.call(t,n)||!h(e[n],t[n]))return!1}return!0}return e!==e&&t!==t}function g(e){let t=e?.deleteMarker||c;function n(e,n){let r=[],i=[{pathStr:``,orig:e||{},part:n||{}}];for(;i.length>0;){let{pathStr:e,orig:n,part:a}=i.pop();if(a!=null&&!h(n,a))if(typeof a==`object`&&!Array.isArray(a))for(let o of Object.keys(a)){let s=e?e+`.`+o:o,c=a[o],l=n&&typeof n==`object`?n[o]:void 0;if(c===t){l!==void 0&&r.push({path:s,oldValue:l,newValue:void 0});continue}typeof c==`object`&&c?i.push({pathStr:s,orig:l,part:c}):h(l,c)||r.push({path:s,oldValue:l,newValue:c})}else e&&r.push({path:e,oldValue:n,newValue:a})}return r}return n}function _(e){let t=e?.deleteMarker||c;function n(e){let n=new Set,r=[{obj:e,currentPath:``}];for(;r.length>0;){let{obj:e,currentPath:i}=r.pop();if(!(typeof e!=`object`||!e||Array.isArray(e)))for(let a of Object.keys(e)){let o=i?`${i}.${a}`:a;n.add(o);let s=e[a];typeof s==`object`&&s&&!Array.isArray(s)&&s!==t&&r.push({obj:s,currentPath:o})}}return Array.from(n)}return n}const v=g(),y=_();var b=class{updateBus;diff;cache;constructor(e,t,n){this.updateBus=t,this.diff=n,this.cache=structuredClone(e)}get(e){return e?structuredClone(this.cache):this.cache}applyChanges(e,t=!1,n=!1,r=[]){if(t)return this.cache=n?structuredClone(e):e,this.notifyListeners([]),[];r.length===0&&(r=[e]);let i=this.get(!1),a=new Map;for(let e=0;e<r.length;e++){let t=r[e],n=this.diff(i,t);for(let e=0;e<n.length;e++){let t=n[e];a.set(t.path,t)}}let o=a.size?[...a.values()]:[];if(o.length>0){this.cache=n?structuredClone(e):e;let t=new Set;for(let e=0;e<o.length;e++){let n=o[e].path;for(;n&&!t.has(n);){t.add(n);let e=n.lastIndexOf(`.`);if(e<0)break;n=n.slice(0,e)}}this.notifyListeners(t)}return o}notifyListeners(e){for(let t of e)this.updateBus.emit({name:`update`,payload:t})}},x=class{eventBus;executionState;merge;logger;middleware=[];blockingMiddleware=[];constructor(e,t,n,r){this.eventBus=e,this.executionState=t,this.merge=n,this.logger=r}async executeBlocking(e,t){for(let{fn:n,name:r,id:i}of this.blockingMiddleware){let a={id:i,name:r,startTime:Date.now()};this.executionState.runningMiddleware={id:i,name:r,startTime:Date.now()},this.emitMiddlewareLifecycle(`start`,{id:i,name:r,type:`blocking`});try{let o=await Promise.resolve(n(e,t));if(a.endTime=Date.now(),a.duration=a.endTime-a.startTime,o===!1)return a.blocked=!0,this.emitMiddlewareLifecycle(`blocked`,{id:i,name:r,duration:a.duration}),this.emit(this.eventBus,{name:`middleware:executed`,payload:a}),{blocked:!0};this.emitMiddlewareLifecycle(`complete`,{id:i,name:r,type:`blocking`,duration:a.duration}),this.emit(this.eventBus,{name:`middleware:executed`,payload:{...a,blocked:!1}})}catch(e){return a.endTime=Date.now(),a.duration=a.endTime-a.startTime,a.error=e instanceof Error?e:Error(String(e)),a.blocked=!0,this.emitMiddlewareError(i,r,a.error,a.duration),this.emit(this.eventBus,{name:`middleware:executed`,payload:a}),{blocked:!0,error:a.error}}finally{this.executionState.runningMiddleware=null}}return{blocked:!1}}async executeTransform(e,t){let n=e,r=t;for(let{fn:e,name:i,id:a}of this.middleware){let o={id:a,name:i,startTime:Date.now()};this.executionState.runningMiddleware={id:a,name:i,startTime:Date.now()},this.emitMiddlewareLifecycle(`start`,{id:a,name:i,type:`transform`});try{let s=await Promise.resolve(e(n,t));o.endTime=Date.now(),o.duration=o.endTime-o.startTime,o.blocked=!1,s&&typeof s==`object`&&(n=this.merge(n,s),r=this.merge(r,s)),this.emit(this.eventBus,{name:`middleware:executed`,payload:o}),this.emitMiddlewareLifecycle(`complete`,{id:a,name:i,type:`transform`,duration:o.duration})}catch(e){o.endTime=Date.now(),o.duration=o.endTime-o.startTime,o.error=e instanceof Error?e:Error(String(e)),o.blocked=!1,this.emit(this.eventBus,{name:`middleware:executed`,payload:o}),this.emitMiddlewareError(a,i,o.error,o.duration),this.logger.error(`Middleware error`,{name:i,error:e})}finally{this.executionState.runningMiddleware=null}}return r}addMiddleware(e,t=`unnamed-middleware`){let n=this.generateId();return this.middleware.push({fn:e,name:t,id:n}),this.updateExecutionState(),n}addBlockingMiddleware(e,t=`unnamed-blocking-middleware`){let n=this.generateId();return this.blockingMiddleware.push({fn:e,name:t,id:n}),this.updateExecutionState(),n}removeMiddleware(e){let t=this.middleware.length+this.blockingMiddleware.length;return this.middleware=this.middleware.filter(t=>t.id!==e),this.blockingMiddleware=this.blockingMiddleware.filter(t=>t.id!==e),this.updateExecutionState(),this.middleware.length+this.blockingMiddleware.length<t}updateExecutionState(){this.executionState.middlewares=[...this.middleware.map(e=>e.name),...this.blockingMiddleware.map(e=>e.name)]}emitMiddlewareLifecycle(e,t){this.emit(this.eventBus,{name:`middleware:${e}`,payload:{...t,timestamp:Date.now()}})}emitMiddlewareError(e,t,n,r){this.emit(this.eventBus,{name:`middleware:error`,payload:{id:e,name:t,error:n,duration:r,timestamp:Date.now()}})}generateId(){return crypto.randomUUID?crypto.randomUUID():`${Date.now()}-${Math.random().toString(36).substring(2,15)}`}emit(e,t){queueMicrotask(()=>{e.emit(t)})}};let S;function C(e){return e||(S||=new s([]),S)}var w=class{eventBus;coreState;persistence;instanceID;persistenceReady=!1;backgroundQueue=[];isProcessingQueue=!1;maxRetries=3;retryDelay=1e3;queueProcessor;pendingRetries=new Set;logger;constructor(e,t,n,r){this.eventBus=e,this.coreState=t,this.instanceID=n,this.maxRetries=r?.maxRetries??3,this.retryDelay=r?.retryDelay??1e3,this.logger=r?.logger??C()}async initialize(e){e?await this.setPersistence(e):this.setPersistenceReady()}isReady(){return this.persistenceReady}handleStateChange(e,t){if(!this.persistence||e.length===0)return;let n={id:`${Date.now()}-${Math.random().toString(36).slice(2,11)}`,state:structuredClone(t),changedPaths:[...e],timestamp:Date.now(),retries:0};this.backgroundQueue.push(n),this.scheduleQueueProcessing(),this.emit(this.eventBus,{name:`persistence:queued`,payload:{taskId:n.id,changedPaths:e,queueSize:this.backgroundQueue.length,timestamp:n.timestamp}})}getQueueStatus(){return{queueSize:this.backgroundQueue.length,isProcessing:this.isProcessingQueue,pendingRetries:this.pendingRetries.size,oldestTask:this.backgroundQueue[0]?.timestamp}}async flush(){this.isProcessingQueue&&await new Promise(e=>{let t=()=>{this.isProcessingQueue?setTimeout(t,10):e()};t()}),await this.processQueue()}discardQueue(){let e=this.backgroundQueue.length+this.pendingRetries.size;this.backgroundQueue=[],this.pendingRetries.clear(),this.queueProcessor&&=(clearTimeout(this.queueProcessor),void 0),this.emit(this.eventBus,{name:`persistence:queue_cleared`,payload:{clearedTasks:e,timestamp:Date.now()}})}scheduleQueueProcessing(){this.queueProcessor||this.isProcessingQueue||(this.queueProcessor=setTimeout(()=>{this.processQueue().catch(e=>{this.logger.error(`Queue processing failed`,{error:e})})},10))}async processQueue(){if(!(this.isProcessingQueue||this.backgroundQueue.length===0)){this.isProcessingQueue=!0,this.queueProcessor=void 0;try{for(;this.backgroundQueue.length>0;){let e=this.backgroundQueue.shift();await this.processTask(e)}}finally{this.isProcessingQueue=!1}}}async processTask(e){try{await this.persistence.set(this.instanceID,e.state)?this.emit(this.eventBus,{name:`persistence:success`,payload:{taskId:e.id,changedPaths:e.changedPaths,duration:Date.now()-e.timestamp,timestamp:Date.now()}}):await this.handleTaskFailure(e,Error(`Persistence returned false`))}catch(t){await this.handleTaskFailure(e,t)}}async handleTaskFailure(e,t){if(e.retries++,e.retries<=this.maxRetries){let n=this.retryDelay*2**(e.retries-1);this.emit(this.eventBus,{name:`persistence:retry`,payload:{taskId:e.id,attempt:e.retries,maxRetries:this.maxRetries,nextRetryIn:n,error:t,timestamp:Date.now()}}),this.pendingRetries.add(e.id),setTimeout(()=>{this.pendingRetries.has(e.id)&&(this.pendingRetries.delete(e.id),this.backgroundQueue.unshift(e),this.scheduleQueueProcessing())},n)}else this.emit(this.eventBus,{name:`persistence:failed`,payload:{taskId:e.id,changedPaths:e.changedPaths,attempts:e.retries,error:t,timestamp:Date.now()}})}setPersistenceReady(){this.persistenceReady=!0,this.emit(this.eventBus,{name:`persistence:ready`,payload:{timestamp:Date.now()}})}async setPersistence(e){this.persistence=e;try{let e=await this.persistence.get();e&&this.coreState.applyChanges(e)}catch(e){this.logger.error(`Failed to initialize persistence`,{error:e}),this.emit(this.eventBus,{name:`persistence:init_error`,payload:{error:e,timestamp:Date.now()}})}finally{this.setPersistenceReady()}this.persistence.subscribe(this.instanceID,async e=>{let t=this.coreState.applyChanges(e);t.length>0&&this.emit(this.eventBus,{name:`update:complete`,payload:{changedPaths:t,source:`external`,timestamp:Date.now()}})})}dispose(){this.discardQueue(),this.isProcessingQueue=!1,this.persistenceReady=!1}emit(e,t){queueMicrotask(()=>{e.emit(t)})}},T=class{eventBus;coreState;executionState;constructor(e,t,n){this.eventBus=e,this.coreState=t,this.executionState=n}async execute(e){let t=this.coreState.get(!0);this.executionState.transactionActive=!0,this.emit(this.eventBus,{name:`transaction:start`,payload:{timestamp:Date.now()}});try{let t=await Promise.resolve(e());return this.emit(this.eventBus,{name:`transaction:complete`,payload:{timestamp:Date.now()}}),this.executionState.transactionActive=!1,t}catch(e){throw this.coreState.applyChanges(t,!0,!1),this.emit(this.eventBus,{name:`transaction:error`,payload:{error:e instanceof Error?e:Error(String(e)),timestamp:Date.now()}}),this.executionState.transactionActive=!1,e}}emit(e,t){queueMicrotask(()=>{e.emit(t)})}},E=class{updateCount=0;listenerExecutions=0;averageUpdateTime=0;largestUpdateSize=0;mostActiveListenerPaths=[];totalUpdates=0;blockedUpdates=0;averageUpdateDuration=0;middlewareExecutions=0;transactionCount=0;totalEventsFired=0;totalActionsDispatched=0;totalActionsSucceeded=0;totalActionsFailed=0;averageActionDuration=0;updateTimes=[];actionTimes=[];pathExecutionCounts=new Map;constructor(e){this.setupEventListeners(e)}getMetrics(){return{updateCount:this.updateCount,listenerExecutions:this.listenerExecutions,averageUpdateTime:this.averageUpdateTime,largestUpdateSize:this.largestUpdateSize,mostActiveListenerPaths:[...this.mostActiveListenerPaths],totalUpdates:this.totalUpdates,blockedUpdates:this.blockedUpdates,averageUpdateDuration:this.averageUpdateDuration,middlewareExecutions:this.middlewareExecutions,transactionCount:this.transactionCount,totalEventsFired:this.totalEventsFired,totalActionsDispatched:this.totalActionsDispatched,totalActionsSucceeded:this.totalActionsSucceeded,totalActionsFailed:this.totalActionsFailed,averageActionDuration:this.averageActionDuration}}setupEventListeners(e){let t=e.emit;e.emit=n=>(this.totalEventsFired++,t.call(e,n)),e.subscribe(`update:complete`,e=>{if(this.totalUpdates++,e.blocked){this.blockedUpdates++;return}if(e.duration){this.updateTimes.push(e.duration),this.updateTimes.length>100&&this.updateTimes.shift();let t=this.updateTimes.reduce((e,t)=>e+t,0)/this.updateTimes.length;this.averageUpdateTime=t,this.averageUpdateDuration=t}e.deltas?.length&&(this.updateCount++,this.largestUpdateSize=Math.max(this.largestUpdateSize,e.deltas.length),e.deltas.forEach(e=>{let t=this.pathExecutionCounts.get(e.path)||0;this.pathExecutionCounts.set(e.path,t+1)}),this.mostActiveListenerPaths=Array.from(this.pathExecutionCounts.entries()).sort(([,e],[,t])=>t-e).slice(0,5).map(([e])=>e))}),e.subscribe(`middleware:start`,()=>{this.middlewareExecutions++}),e.subscribe(`transaction:start`,()=>{this.transactionCount++}),e.subscribe(`action:start`,()=>{this.totalActionsDispatched++}),e.subscribe(`action:complete`,e=>{this.totalActionsSucceeded++,e.duration&&(this.actionTimes.push(e.duration),this.actionTimes.length>100&&this.actionTimes.shift(),this.averageActionDuration=this.actionTimes.reduce((e,t)=>e+t,0)/this.actionTimes.length)}),e.subscribe(`action:error`,()=>{this.totalActionsFailed++})}reset(){this.updateCount=0,this.listenerExecutions=0,this.averageUpdateTime=0,this.largestUpdateSize=0,this.mostActiveListenerPaths=[],this.totalUpdates=0,this.blockedUpdates=0,this.averageUpdateDuration=0,this.middlewareExecutions=0,this.transactionCount=0,this.totalEventsFired=0,this.totalActionsDispatched=0,this.totalActionsSucceeded=0,this.totalActionsFailed=0,this.averageActionDuration=0,this.updateTimes=[],this.actionTimes=[],this.pathExecutionCounts.clear()}getDetailedMetrics(){return{pathExecutionCounts:new Map(this.pathExecutionCounts),recentUpdateTimes:[...this.updateTimes],successRate:this.totalUpdates>0?(this.totalUpdates-this.blockedUpdates)/this.totalUpdates:1,averagePathsPerUpdate:this.updateCount>0?Array.from(this.pathExecutionCounts.values()).reduce((e,t)=>e+t,0)/this.updateCount:0}}dispose(){this.reset()}},D=class extends Error{constructor(){super(`Action Cancelled by Debounce`),this.name=`ActionCancelledError`}},O=class extends Error{constructor({action:e}){super(`Unknown action: "${e}"`),this.name=`UnknownActionError`}};const k=()=>{},A={name:`UNDEFINED ACTION`,status:()=>!1,subscribe:e=>()=>{}};var j=class{eventBus;set;registrations=new Map;constructor(e,t){this.eventBus=e,this.set=t}register(e){let n={action:{name:e.name,id:o(),action:e.fn,debounce:e.debounce?{...e.debounce,condition:e.debounce.condition??(()=>!0)}:void 0},debouncer:e.debounce&&e.debounce.delay>0?new t({delay:e.debounce.delay}):void 0,previousArgs:void 0,running:!1,subscription:{listeners:new Set,watcher:null,watchers:new a(()=>[this.eventBus.subscribe(`action:start`,t=>t.name===e.name&&this.notifyStatusListeners(e.name)),this.eventBus.subscribe(`action:complete`,t=>t.name===e.name&&this.notifyStatusListeners(e.name)),this.eventBus.subscribe(`action:error`,t=>t.name===e.name&&this.notifyStatusListeners(e.name))],e=>e?.forEach(e=>e()),{gracePeriod:`microtask`})}};return this.registrations.set(e.name,n),()=>{let t=this.registrations.get(e.name);t&&(t.debouncer?.cancel(),this.registrations.delete(e.name))}}async dispatch(e,...t){let n=this.registrations.get(e);if(!n)throw new O({action:e});let{action:r,debouncer:i}=n,{debounce:a}=r;if(!i||!a)return this.executeAction(n,t);let o=a.condition(n.previousArgs,t);if(n.previousArgs=t,!o)return this.executeAction(n,t);let s=await i.do(()=>this.executeAction(n,t));if(s.status===`cancelled`)throw new D;if(s.status===`error`&&s.error)throw s.error;return s.value}async executeAction(e,t){let n=Date.now();e.running=!0,this.emit(this.eventBus,{name:`action:start`,payload:{actionId:e.action.id,name:e.action.name,params:t||[],timestamp:n}});try{let r=await this.set(n=>e.action.action(n,...t),{actionId:e.action.id}),i=Date.now();return e.running=!1,this.emit(this.eventBus,{name:`action:complete`,payload:{actionId:e.action.id,name:e.action.name,params:t,startTime:n,endTime:i,duration:i-n,result:r}}),r}catch(r){let i=Date.now();throw e.running=!1,this.emit(this.eventBus,{name:`action:error`,payload:{actionId:e.action.id,name:e.action.name,params:t,startTime:n,endTime:i,duration:i-n,error:r}}),r}}running(e){let t=this.registrations.get(e);return t?t.running:!1}subscribe(e,t){let n=this.registrations.get(e);return n?(n.subscription.listeners.add(t),n.subscription.watchers.acquire(),()=>{n.subscription.listeners.delete(t),n.subscription.watchers?.release()}):k}watch(e){let t=this.registrations.get(e);return t?(t.subscription.watcher||(t.subscription.watcher={name:e,status:()=>this.running(e),subscribe:t=>this.subscribe(e,t)}),t.subscription.watcher):A}notifyStatusListeners(e){let t=this.registrations.get(e).subscription.listeners;t&&t.forEach(e=>e())}emit(e,t){queueMicrotask(()=>{e.emit(t)})}dispose(){for(let e of this.registrations.values())e.debouncer?.cancel(),e.subscription.watchers.forceCleanup,e.subscription.listeners.clear();this.registrations.clear()}},M=class{coreState;middlewareEngine;persistenceHandler;transactionManager;metricsCollector;selectorManager;actions;updateSerializer=new i({yieldMode:`macrotask`,capacity:1e3});readyLatch=new n;disposeOnce=new r;updateBus;eventBus;executionState;instanceID=o();merge;diff;logger;constructor(t,n,r=c,i){this.logger=C(i?.logger),this.eventBus=e(i?.broadcastChannel?{batch:{size:0,delay:0},broadcast:{channel:i.broadcastChannel}}:void 0),this.updateBus=e(i?.broadcastChannel?{batch:{size:0,delay:0},broadcast:{channel:i.broadcastChannel}}:void 0),this.executionState={executing:!1,changes:null,pendingChanges:[],middlewares:[],runningMiddleware:null,transactionActive:!1},this.merge=u({deleteMarker:r}),this.diff=g({deleteMarker:r}),this.coreState=new b(t,this.updateBus,this.diff),this.middlewareEngine=new x(this.eventBus,this.executionState,this.merge,this.logger),this.persistenceHandler=new w(this.eventBus,this.coreState,this.instanceID,{maxRetries:i?.persistenceMaxRetries,retryDelay:i?.persistenceRetryDelay,logger:this.logger}),this.transactionManager=new T(this.eventBus,this.coreState,this.executionState),this.metricsCollector=new E(this.eventBus),this.actions=new j(this.eventBus,this.set.bind(this)),this.persistenceHandler.initialize(n),this.setupPersistenceListener(),this.setupReadyLatch(),this.selectorManager=new p(this.get.bind(this),this.eventBus)}isReady(){return this.readyLatch.isOpen()}async ready(e){return this.readyLatch.wait(e)}state(){return this.executionState.executing=this.updateSerializer.running(),this.executionState}get(e){return this.coreState.get(e??!1)}subset(e,t=`.`){let n={},r=this.get();for(let i of e)n[i]=i.split(t).reduce((e,t)=>e&&e[t]!==void 0?e[t]:void 0,r);return n}select(e){return this.checkDisposed(),this.selectorManager.createReactiveSelector(e)}register(e){return this.checkDisposed(),this.actions.register(e)}async dispatch(e,...t){return this.checkDisposed(),this.actions.dispatch(e,...t)}async set(e,t={}){this.checkDisposed();let n=await this.updateSerializer.do(()=>this._performUpdate(e,t));if(n.error)throw n.error;return n.value}async _performUpdate(e,t){let n=Date.now();this.emit(this.eventBus,{name:`update:start`,payload:{timestamp:n,actionId:t.actionId}});try{if(t.force){let r=this.get(!1),i=typeof e==`function`?e(r):e;this.coreState.applyChanges(i,!0);let a=Date.now();return this.emit(this.eventBus,{name:`update:complete`,payload:{deltas:[],duration:a-n,timestamp:Date.now(),actionId:t.actionId,newState:i}}),i}let r,i=this.get(!1);if(typeof e==`function`){let t=e(i);r=t instanceof Promise?await t:t}else r=e;let a=await this.middlewareEngine.executeBlocking(i,r);if(a.blocked)throw a.error||Error(`Update blocked by middleware`);let o=this.merge(i,r),s=await this.middlewareEngine.executeTransform(o,r),c=this.merge(o,s),l=this.coreState.applyChanges(c,!1,!1,[r,s]),u=Date.now(),d=this.get(!1);return this.emit(this.eventBus,{name:`update:complete`,payload:{deltas:l,duration:u-n,timestamp:Date.now(),actionId:t.actionId,newState:d}}),d}catch(e){throw this.emit(this.eventBus,{name:`update:complete`,payload:{blocked:!0,error:e,timestamp:Date.now(),actionId:t.actionId,newState:this.get(!1)}}),e}finally{this.executionState.executing=!1,this.executionState.changes=null,this.executionState.runningMiddleware=null,this.executionState.pendingChanges=[]}}setupReadyLatch(){if(this.persistenceHandler.isReady())this.readyLatch.open();else{let e=this.eventBus.subscribe(`persistence:ready`,()=>{this.readyLatch.isOpen()||this.readyLatch.open(),e()})}}setupPersistenceListener(){this.updateBus.subscribe(`update`,e=>{e&&this.persistenceHandler.isReady()&&this.persistenceHandler.handleStateChange([e],this.get(!1))})}watch(e,t,n){let r=Array.isArray(e)?e:[e],i=e===``||r.length===0;return this.updateBus.subscribe(`update`,e=>{(i||r.includes(e))&&(t(this.get(!1)),this.metricsCollector.listenerExecutions++)},n)}watchAction(e){return this.checkDisposed(),this.actions.watch(e)}debouncedSetter(e){let n=new t({delay:e.delay,leading:e.leading});return(e,t={})=>{n.fire(()=>this.set(e,t))}}id(){return this.instanceID}async transaction(e,t){this.checkDisposed();let n=await this.transactionManager.execute(e);return t?.flush&&await this.flush(),n}use(e){this.checkDisposed();let t=(e.block?this.middlewareEngine.addBlockingMiddleware:this.middlewareEngine.addMiddleware).bind(this.middlewareEngine)(e.action,e.name);return()=>this.middlewareEngine.removeMiddleware(t)}metrics(){return this.metricsCollector.getMetrics()}on(e,t){return this.checkDisposed(),this.eventBus.subscribe(e,t)}getPersistenceStatus(){return this.persistenceHandler.getQueueStatus()}async flush(){return this.persistenceHandler.flush()}discardPersistenceQueue(){this.persistenceHandler.discardQueue()}dispose(){return this.disposeOnce.do(async()=>{await this.flush(),this.updateSerializer.close(),this.eventBus?.clear({permanent:!0}),this.updateBus?.clear({permanent:!0}),this.actions.dispose(),this.persistenceHandler.dispose(),this.metricsCollector.dispose(),this.selectorManager.dispose(),this.coreState=null,this.middlewareEngine=null,this.transactionManager=null,this.actions=null})}checkDisposed(){if(this.disposed())throw Error(`StoreExecutionDone: Cannot perform operations on a disposed store.`)}disposed(){return this.disposeOnce.done()}emit(e,t){queueMicrotask(()=>{e.emit(t)})}},N=class{stores=new Map;storeToOnce=new WeakMap;finalizer;onEvict;logger;constructor(e){this.onEvict=e?.onEvict,this.logger=C(e?.logger),this.finalizer=new FinalizationRegistry(({storeId:e,ref:t})=>{try{this.stores.get(e)===t&&(this.stores.delete(e),this.onEvict?.(e))}catch(t){this.logger.error(`StoreRegistry Finalizer error`,{storeId:e,error:t})}})}async get(e,t={}){let n=this.stores.get(e)?.deref();if(!n){n=new r({throws:!1});let t=new WeakRef(n);this.stores.set(e,t),this.finalizer.register(n,{storeId:e,ref:t},n)}let i=await n.do(async()=>{let e=new M(t.state||{},t.persistence,t.deleteMarker,t.options);return await e.ready(),e},t.timeout);if(i.error)throw this.stores.get(e)?.deref()===n&&(this.stores.delete(e),this.finalizer.unregister(n)),i.error;let a=i.value;return this.storeToOnce.set(a,n),a}getSync(e,t={}){let n=this.stores.get(e)?.deref();if(!n){n=new r({throws:!1});let t=new WeakRef(n);this.stores.set(e,t),this.finalizer.register(n,{storeId:e,ref:t},n)}let i=n.doSync(()=>new M(t.state||{},t.persistence,t.deleteMarker,t.options));if(i.error)throw this.stores.get(e)?.deref()===n&&(this.stores.delete(e),this.finalizer.unregister(n)),i.error;let a=i.value;return this.storeToOnce.set(a,n),a}async release(e){let t=this.stores.get(e);if(t){let n=t.deref();if(n&&(this.finalizer.unregister(n),n.done())){let t=n.get();if(t){try{await t.dispose()}catch(t){this.logger.error(`StoreRegistry Error disposing store during release`,{storeId:e,error:t})}this.storeToOnce.delete(t)}}return this.stores.delete(e)}return!1}async clear(){for(let e of this.stores.values()){let t=e.deref();if(t&&(this.finalizer.unregister(t),t.done())){let e=t.get();if(e){try{await e.dispose()}catch{}this.storeToOnce.delete(e)}}}this.stores.clear()}has(e){return this.stores.has(e)}get size(){return this.stores.size}},P=class{store;eventHistory=[];stateHistory=[];unsubscribers=[];isTimeTraveling=!1;devTools=null;middlewareExecutions=[];activeTransactionCount=0;activeBatches=new Set;maxEvents;maxStateHistory;enableConsoleLogging;isSilent;logEvents;performanceThresholds;logger;constructor(e,t={}){this.store=e,this.maxEvents=t.maxEvents??500,this.maxStateHistory=t.maxStateHistory??20,this.enableConsoleLogging=t.enableConsoleLogging??!1,this.isSilent=t.silent??!1,this.logger=C(t.logger),this.logEvents={updates:t.logEvents?.updates??!0,middleware:t.logEvents?.middleware??!0,transactions:t.logEvents?.transactions??!0,actions:t.logEvents?.actions??!0,selectors:t.logEvents?.selectors??!0},this.performanceThresholds={updateTime:t.performanceThresholds?.updateTime??50,middlewareTime:t.performanceThresholds?.middlewareTime??20},this.recordStateSnapshot([]),this.setupEventListeners()}_consoleLog(e,...t){if(this.isSilent)return;if(e===`group`||e===`groupEnd`||e===`table`){typeof console[e]==`function`&&console[e](...t);return}let n=typeof t[0]==`string`?t[0]:String(t[0]??``),r=t.length>1?{detail:t.slice(1)}:void 0;switch(e){case`log`:this.logger.log(n,r);break;case`warn`:this.logger.warn(n,r);break;case`error`:this.logger.error(n,r);break;case`debug`:this.logger.debug(n,r);break}}setupEventListeners(){for(let e of[`update:start`,`update:complete`,`middleware:start`,`middleware:complete`,`middleware:error`,`middleware:blocked`,`transaction:start`,`transaction:complete`,`transaction:error`,`middleware:executed`,`action:start`,`action:complete`,`action:error`,`selector:accessed`]){let t=e.startsWith(`update`)&&this.logEvents.updates||e.startsWith(`middleware`)&&this.logEvents.middleware||e.startsWith(`transaction`)&&this.logEvents.transactions||e.startsWith(`action`)&&this.logEvents.actions||e.startsWith(`selector`)&&this.logEvents.selectors;this.unsubscribers.push(this.store.on(e,n=>{this.isTimeTraveling||(e===`update:complete`&&!n.blocked&&this.recordStateSnapshot(n.deltas),e===`middleware:executed`?this.middlewareExecutions.push(n):e===`transaction:start`?this.activeTransactionCount++:(e===`transaction:complete`||e===`transaction:error`)&&(this.activeTransactionCount=Math.max(0,this.activeTransactionCount-1)),n.batchId&&(e.endsWith(`start`)?this.activeBatches.add(n.batchId):(e.endsWith(`complete`)||e.endsWith(`error`))&&this.activeBatches.delete(n.batchId)),this.recordEvent(e,n),this.enableConsoleLogging&&t&&this._log(e,n),this._checkPerformance(e,n))}))}}recordStateSnapshot(e){let t={state:this.store.get(!0),timestamp:Date.now(),deltas:e};this.stateHistory.unshift(t),this.stateHistory.length>this.maxStateHistory&&this.stateHistory.pop()}recordEvent(e,t){let n={type:e,timestamp:Date.now(),data:structuredClone(t)};this.eventHistory.unshift(n),this.eventHistory.length>this.maxEvents&&this.eventHistory.pop()}getEventHistory(){return structuredClone(this.eventHistory)}getStateHistory(){return structuredClone(this.stateHistory)}getMiddlewareExecutions(){return this.middlewareExecutions}getTransactionStatus(){return{activeTransactions:this.activeTransactionCount,activeBatches:Array.from(this.activeBatches)}}createLoggingMiddleware(e={}){let{logLevel:t=`debug`,logUpdates:n=!0}=e;return(e,r)=>(n&&this.logger[t](`State Update`,{update:r}),r)}createValidationMiddleware(e){return(t,n)=>{let r=e(t,n);return typeof r==`boolean`?r:(!r.valid&&r.reason&&this._consoleLog(`warn`,`Validation failed:`,r.reason),r.valid)}}getRecentChanges(e=5){let t=[],n=Math.min(e,this.stateHistory.length);for(let e=0;e<n;e++){let n=this.stateHistory[e];if(!n.deltas||n.deltas.length===0)continue;let r={},i={},a=(e,t,n)=>{t.reduce((e,r,i)=>(i===t.length-1?e[r]=n:e[r]=e[r]??{},e[r]),e)};for(let e of n.deltas){let t=e.path.split(`.`);a(r,t,e.oldValue),a(i,t,e.newValue)}t.push({timestamp:n.timestamp,changedPaths:n.deltas.map(e=>e.path),from:r,to:i})}return t}clearHistory(){this.eventHistory=[],this.stateHistory.length>0&&(this.stateHistory=[this.stateHistory[0]])}getHistoryForAction(e){return this.eventHistory.filter(t=>t.data?.actionId===e)}async replay(e){let t=this.eventHistory.filter(e=>e.type===`update:start`)[e];t?.data.update?(this._consoleLog(`log`,`Replaying event at index ${e}:`,t),await this.store.set(t.data.update,{force:!0})):this._consoleLog(`warn`,`No replayable event found at index ${e}.`)}createTimeTravel(){let e=0,t=[],n=this.store.on(`update:complete`,n=>{!this.isTimeTraveling&&!n.blocked&&(t=[],e=0)});this.unsubscribers.push(n);let r=()=>this.stateHistory.length,i=()=>e<r()-1,a=()=>t.length>0;return{canUndo:i,canRedo:a,undo:async()=>{if(!i())return;t.unshift(this.stateHistory[e]),e++;let n=this.stateHistory[e].state;this.isTimeTraveling=!0,await this.store.set({...n},{force:!0}),this.isTimeTraveling=!1},redo:async()=>{if(!a())return;let n=t.shift();e--,this.isTimeTraveling=!0,await this.store.set({...n.state},{force:!0}),this.isTimeTraveling=!1},length:r,clear:()=>{t=[],e=0}}}async saveSession(e){let t=this.store.id(),n={eventHistory:this.eventHistory,stateHistory:this.stateHistory};return Promise.resolve(e.set(t,n))}async loadSession(e){let t=await Promise.resolve(e.get());return t?(this.eventHistory=t.eventHistory||[],this.stateHistory=t.stateHistory||[],this.stateHistory.length>0&&await this.store.set(this.stateHistory[0].state,{force:!0}),!0):!1}exportSession(){let e={eventHistory:this.eventHistory,stateHistory:this.stateHistory},t=new Blob([JSON.stringify(e,null,2)],{type:`application/json`}),n=URL.createObjectURL(t),r=document.createElement(`a`);r.href=n,r.download=`store-observer-session-${new Date().toISOString()}.json`,r.click(),URL.revokeObjectURL(n)}importSession(e){return new Promise((t,n)=>{let r=new FileReader;r.onload=async e=>{try{let n=JSON.parse(e.target?.result);this.eventHistory=n.eventHistory||[],this.stateHistory=n.stateHistory||[],this.stateHistory.length>0&&await this.store.set(this.stateHistory[0].state,{force:!0}),t()}catch(e){n(e)}},r.onerror=e=>n(e),r.readAsText(e)})}disconnect(){this.unsubscribers.forEach(e=>e()),this.unsubscribers=[],this.devTools?.disconnect(),this.clearHistory()}_log(e,t){let n=new Date(t.timestamp||Date.now()).toISOString().split(`T`)[1].replace(`Z`,``);if(e===`update:start`)this._consoleLog(`group`,`%c⚡ Store Update Started [${n}]`,`color: #4a6da7`);else if(e===`update:complete`){if(t.blocked)this._consoleLog(`warn`,`%c✋ Update Blocked [${n}]`,`color: #bf8c0a`,t.error);else{let e=t.deltas||[];e.length>0&&(this._consoleLog(`log`,`%c✅ Update Complete [${n}] - ${e.length} paths changed in ${t.duration?.toFixed(2)}ms`,`color: #2a9d8f`),this._consoleLog(`table`,e.map(e=>({path:e.path,oldValue:e.oldValue,newValue:e.newValue}))))}this._consoleLog(`groupEnd`)}else e===`middleware:start`?this._consoleLog(`debug`,`%c◀ Middleware \"${t.name}\" started [${n}] (${t.type})`,`color: #8c8c8c`):e===`middleware:complete`?this._consoleLog(`debug`,`%c▶ Middleware \"${t.name}\" completed [${n}] in ${t.duration?.toFixed(2)}ms`,`color: #7c9c7c`):e===`middleware:error`?this._consoleLog(`error`,`%c❌ Middleware \"${t.name}\" error [${n}]:`,`color: #e63946`,t.error):e===`middleware:blocked`?this._consoleLog(`warn`,`%c🛑 Middleware \"${t.name}\" blocked update [${n}]`,`color: #e76f51`):e===`transaction:start`?this._consoleLog(`group`,`%c📦 Transaction Started [${n}]`,`color: #6d597a`):e===`transaction:complete`?(this._consoleLog(`log`,`%c📦 Transaction Complete [${n}]`,`color: #355070`),this._consoleLog(`groupEnd`)):e===`transaction:error`?(this._consoleLog(`error`,`%c📦 Transaction Error [${n}]:`,`color: #e56b6f`,t.error),this._consoleLog(`groupEnd`)):e===`action:start`?this._consoleLog(`group`,`%c🚀 Action \"${t.name}\" Started [${n}]`,`color: #9b59b6`,{params:t.params}):e===`action:complete`?(this._consoleLog(`log`,`%c✔️ Action \"${t.name}\" Complete [${n}] in ${t.duration?.toFixed(2)}ms`,`color: #2ecc71`),this._consoleLog(`groupEnd`)):e===`action:error`?(this._consoleLog(`error`,`%c🔥 Action \"${t.name}\" Error [${n}]:`,`color: #e74c3c`,t.error),this._consoleLog(`groupEnd`)):e===`selector:accessed`&&this._consoleLog(`debug`,`%c👀 Selector Accessed [${n}] in ${t.duration?.toFixed(2)}ms`,`color: #f1c40f`,{accessedPaths:t.accessedPaths,selectorId:t.selectorId})}_checkPerformance(e,t){this.enableConsoleLogging&&(e===`update:complete`&&!t.blocked&&t.duration>this.performanceThresholds.updateTime&&this._consoleLog(`warn`,`%c⚠️ Slow update detected [${t.duration.toFixed(2)}ms]`,`color: #ff9f1c`,{deltas:t.deltas,threshold:this.performanceThresholds.updateTime}),e===`middleware:complete`&&t.duration>this.performanceThresholds.middlewareTime&&this._consoleLog(`warn`,`%c⚠️ Slow middleware \"${t.name}\" [${t.duration.toFixed(2)}ms]`,`color: #ff9f1c`,{threshold:this.performanceThresholds.middlewareTime}))}};export{D as ActionCancelledError,j as ActionManager,c as DELETE_SYMBOL,M as ReactiveDataStore,p as SelectorManager,P as StoreObserver,N as StoreRegistry,O as UnknownActionError,m as buildPaths,_ as createDerivePaths,g as createDiff,u as createMerge,C as createStoreLogger,y as derivePaths,v as diff,d as merge,l as shallowClone};
1
+ import { createEventBus } from "@asaidimu/utils-events";
2
+ import { Debouncer, Latch, Once, Serializer, SharedResource } from "@asaidimu/utils-sync";
3
+ import { v4 } from "uuid";
4
+ import { Logger } from "@asaidimu/utils-logger";
5
+
6
+ //#region src/store/types.ts
7
+ /**
8
+ * A unique symbol used within DeepPartial objects to explicitly signal that a property
9
+ * should be deleted from the state object.
10
+ */
11
+ const DELETE_SYMBOL = Symbol.for("delete");
12
+
13
+ //#endregion
14
+ //#region src/store/merge.ts
15
+ /**
16
+ * Creates a shallow clone of an object or array.
17
+ */
18
+ const shallowClone = (obj) => {
19
+ if (Array.isArray(obj)) return [...obj];
20
+ return { ...obj };
21
+ };
22
+ /**
23
+ * Creates a deep merge function with configurable options.
24
+ *
25
+ * @param {object} options - Configuration options for the merge function.
26
+ * @param {symbol} [options.deleteMarker=Symbol.for("delete")] - A custom symbol to mark properties for deletion during merge.
27
+ */
28
+ function createMerge(options) {
29
+ const deleteSymbol = options?.deleteMarker || DELETE_SYMBOL;
30
+ /**
31
+ * Recursively strips deleteSymbol from an object or array.
32
+ *
33
+ * Used only when `original` is not an object and `changes` is being
34
+ * used as the base result directly. In normal operation (T extends object),
35
+ * `original` is always an object and this path is not reached.
36
+ *
37
+ * Note: arrays in this context are filtered for deleteSymbol elements
38
+ * and their object elements are cleaned recursively, consistent with the
39
+ * contract that deleteSymbol has no meaning *inside* arrays during a
40
+ * normal merge — but when an array is the entire result of this edge-case
41
+ * path, cleaning is the safe choice.
42
+ */
43
+ function cleanObject(obj) {
44
+ if (obj === null || obj === void 0) return obj;
45
+ if (Array.isArray(obj)) return obj.filter((item) => item !== deleteSymbol).map((item) => typeof item === "object" && item !== null && !Array.isArray(item) ? cleanObject(item) : item);
46
+ if (typeof obj === "object") {
47
+ const result = {};
48
+ for (const [key, value] of Object.entries(obj)) {
49
+ if (value === deleteSymbol) continue;
50
+ if (typeof value === "object" && value !== null) {
51
+ const cleaned = cleanObject(value);
52
+ if (cleaned !== void 0) result[key] = cleaned;
53
+ } else result[key] = value;
54
+ }
55
+ return result;
56
+ }
57
+ return obj === deleteSymbol ? void 0 : obj;
58
+ }
59
+ /**
60
+ * Deep merges changes into an original object immutably.
61
+ *
62
+ * - The original object and changes object are not modified.
63
+ * - Arrays are replaced entirely, not element-merged. deleteSymbol has
64
+ * no meaning inside arrays — if you need to remove an array element,
65
+ * pass the desired array directly.
66
+ * - Object properties set to deleteSymbol are removed from the result.
67
+ * - Uses an iterative stack to avoid call-stack overflows on deep objects.
68
+ *
69
+ * @template T The type of the original object.
70
+ * @param {T} original The base object to merge into.
71
+ * @param {DeepPartial<T>} changes The patch to apply.
72
+ * @returns {T} A new object with changes applied.
73
+ *
74
+ * @example
75
+ * const merge = createMerge();
76
+ * merge({ a: 1, b: { c: 2 } }, { b: { c: 3, d: 4 } });
77
+ * // → { a: 1, b: { c: 3, d: 4 } }
78
+ *
79
+ * merge({ a: 1, b: 2 }, { b: Symbol.for('delete') });
80
+ * // → { a: 1 }
81
+ *
82
+ * merge({ a: 1, tags: ['x', 'y'] }, { tags: ['z'] });
83
+ * // → { a: 1, tags: ['z'] } (array replaced wholesale)
84
+ */
85
+ function mergeFn(original, changes) {
86
+ if (typeof original !== "object" || original === null) {
87
+ if (typeof changes === "object" && changes !== null) return cleanObject(changes);
88
+ return changes === deleteSymbol ? {} : changes;
89
+ }
90
+ if (typeof changes !== "object" || changes === null) return original;
91
+ const result = shallowClone(original);
92
+ const stack = [{
93
+ target: result,
94
+ source: changes
95
+ }];
96
+ while (stack.length > 0) {
97
+ const { target, source } = stack.pop();
98
+ for (const key of Object.keys(source)) {
99
+ const sourceValue = source[key];
100
+ if (sourceValue === deleteSymbol) {
101
+ delete target[key];
102
+ continue;
103
+ }
104
+ if (Array.isArray(sourceValue)) {
105
+ target[key] = sourceValue;
106
+ continue;
107
+ }
108
+ if (typeof sourceValue === "object" && sourceValue !== null) {
109
+ target[key] = shallowClone(key in target && typeof target[key] === "object" && target[key] !== null ? target[key] : {});
110
+ stack.push({
111
+ target: target[key],
112
+ source: sourceValue
113
+ });
114
+ } else target[key] = sourceValue;
115
+ }
116
+ }
117
+ return result;
118
+ }
119
+ return mergeFn;
120
+ }
121
+ /**
122
+ * @deprecated Use `createMerge()` instead.
123
+ * Provided for backward compatibility with `Symbol.for("delete")` as the default delete marker.
124
+ */
125
+ const merge = createMerge();
126
+
127
+ //#endregion
128
+ //#region src/store/selector.ts
129
+ const ARRAY_METHODS = [
130
+ "map",
131
+ "filter",
132
+ "reduce",
133
+ "forEach",
134
+ "find",
135
+ "findIndex",
136
+ "some",
137
+ "every",
138
+ "includes",
139
+ "flatMap",
140
+ "flat",
141
+ "slice",
142
+ "splice"
143
+ ];
144
+ /**
145
+ * Manages reactive selectors with static path tracking.
146
+ *
147
+ * Selectors MUST be simple property accesses only — no transformations,
148
+ * no iterations, no conditionals. Use store effects for derivations instead.
149
+ *
150
+ * Cache strategy: paths-only.
151
+ * Because selectors are guaranteed to be pure property accessors, two selectors
152
+ * that access the same set of paths are semantically identical and share a single
153
+ * SelectorEntry. There is no WeakMap ref-cache (refs are unstable for inline
154
+ * arrows) and no function-body component in the key (redundant given the
155
+ * invariant). The sorted, pipe-joined path set is the complete identity of a
156
+ * selector.
157
+ *
158
+ * Lifetime: reference-counted with deferred cleanup.
159
+ * When the subscriber count reaches zero we schedule cleanup on a setTimeout(0)
160
+ * rather than destroying the entry immediately. This absorbs React Strict Mode's
161
+ * subscribe → unsubscribe → re-subscribe cycle, which otherwise causes a
162
+ * re-initialisation storm (new id, lost lastResult, spurious re-renders).
163
+ * If count is still zero when the timer fires, the entry is genuinely abandoned
164
+ * and is evicted cleanly.
165
+ *
166
+ * @template T - The type of the root state object.
167
+ */
168
+ var SelectorManager = class {
169
+ reactiveSelectors = /* @__PURE__ */ new Map();
170
+ /**
171
+ * Single source of truth for caching. Key: sorted paths joined by "|".
172
+ * Maps to the live SelectorEntry (not just the ReactiveSelector facade),
173
+ * so ref-counting and deferred cleanup operate on the same object.
174
+ */
175
+ pathBasedCache = /* @__PURE__ */ new Map();
176
+ /** Reverse lookup: Path -> Set of Selector IDs for O(1) update dispatch. */
177
+ dependencyMap = /* @__PURE__ */ new Map();
178
+ getState;
179
+ eventBus;
180
+ unsubscribeFromStore;
181
+ constructor(getState, eventBus) {
182
+ this.getState = getState;
183
+ this.eventBus = eventBus;
184
+ this.unsubscribeFromStore = this.eventBus.subscribe("update:complete", this.handleStoreUpdate);
185
+ }
186
+ handleStoreUpdate = (payload) => {
187
+ const selectorsToEvaluate = /* @__PURE__ */ new Set();
188
+ const divider = ".";
189
+ for (const delta of payload.deltas) {
190
+ const changedPath = delta.path;
191
+ for (const [registeredPath, dependents] of this.dependencyMap) if (registeredPath === changedPath || registeredPath.startsWith(changedPath + divider) || changedPath.startsWith(registeredPath + divider)) for (const id of dependents) selectorsToEvaluate.add(id);
192
+ }
193
+ for (const id of selectorsToEvaluate) {
194
+ const entry = this.reactiveSelectors.get(id);
195
+ if (entry) this.evaluateEntry(entry);
196
+ }
197
+ };
198
+ evaluateEntry(entry) {
199
+ let newResult;
200
+ try {
201
+ newResult = entry.selector(this.getState());
202
+ } catch {
203
+ newResult = void 0;
204
+ }
205
+ if (newResult !== entry.lastResult) {
206
+ entry.lastResult = newResult;
207
+ for (const cb of entry.subscribers) cb(newResult);
208
+ this.eventBus.emit({
209
+ name: "selector:changed",
210
+ payload: {
211
+ selectorId: entry.id,
212
+ newResult,
213
+ timestamp: Date.now()
214
+ }
215
+ });
216
+ }
217
+ }
218
+ createReactiveSelector(selector) {
219
+ const accessedPaths = buildPaths(selector);
220
+ const pathCacheKey = [...accessedPaths].sort().join("|");
221
+ const cachedEntry = this.pathBasedCache.get(pathCacheKey);
222
+ if (cachedEntry) {
223
+ if (cachedEntry.cleanupTimer !== void 0) {
224
+ clearTimeout(cachedEntry.cleanupTimer);
225
+ cachedEntry.cleanupTimer = void 0;
226
+ }
227
+ return cachedEntry.reactiveSelectorInstance;
228
+ }
229
+ const id = `sel-${Math.random().toString(36).slice(2, 9)}`;
230
+ const entry = {
231
+ id,
232
+ selector,
233
+ lastResult: selector(this.getState()),
234
+ accessedPaths,
235
+ subscribers: /* @__PURE__ */ new Set(),
236
+ count: 0,
237
+ cleanupTimer: void 0,
238
+ pathCacheKey,
239
+ reactiveSelectorInstance: null
240
+ };
241
+ for (const path of accessedPaths) {
242
+ if (!this.dependencyMap.has(path)) this.dependencyMap.set(path, /* @__PURE__ */ new Set());
243
+ this.dependencyMap.get(path).add(id);
244
+ }
245
+ const reactiveSelector = {
246
+ id,
247
+ /**
248
+ * Always recomputes from current state.
249
+ *
250
+ * useSyncExternalStore requires getSnapshot() to be consistent with
251
+ * the external store at the moment it is called — returning a cached
252
+ * lastResult risks a stale snapshot if state changed between the last
253
+ * evaluateEntry call and this render. Direct recomputation is safe
254
+ * because selectors are pure, cheap property accessors.
255
+ *
256
+ * If the selector throws (e.g. accessing a path that has been deleted
257
+ * from state), we return undefined rather than crashing the caller.
258
+ * This mirrors evaluateEntry's behaviour so get() and subscribers
259
+ * stay consistent.
260
+ */
261
+ get: () => {
262
+ try {
263
+ return entry.selector(this.getState());
264
+ } catch {
265
+ return;
266
+ }
267
+ },
268
+ /**
269
+ * subscribe is the hot path for useSyncExternalStore.
270
+ *
271
+ * Cancels any pending deferred cleanup so React Strict Mode's
272
+ * unsubscribe → re-subscribe cycle never destroys a live entry.
273
+ * Returns a stable unsubscribe function whose only job is
274
+ * ref-counting and scheduling deferred cleanup.
275
+ */
276
+ subscribe: (callback) => {
277
+ if (entry.cleanupTimer !== void 0) {
278
+ clearTimeout(entry.cleanupTimer);
279
+ entry.cleanupTimer = void 0;
280
+ }
281
+ entry.subscribers.add(callback);
282
+ entry.count++;
283
+ return () => {
284
+ entry.subscribers.delete(callback);
285
+ entry.count--;
286
+ if (entry.count === 0) entry.cleanupTimer = setTimeout(() => {
287
+ if (entry.count === 0) this.evictEntry(entry);
288
+ }, 0);
289
+ };
290
+ }
291
+ };
292
+ entry.reactiveSelectorInstance = reactiveSelector;
293
+ this.reactiveSelectors.set(id, entry);
294
+ this.pathBasedCache.set(pathCacheKey, entry);
295
+ this.eventBus.emit({
296
+ name: "selector:accessed",
297
+ payload: {
298
+ selectorId: id,
299
+ accessedPaths,
300
+ duration: 0,
301
+ timestamp: Date.now()
302
+ }
303
+ });
304
+ return reactiveSelector;
305
+ }
306
+ /**
307
+ * Fully removes a selector entry from all internal maps.
308
+ * Only called from the deferred cleanup timer, after confirming count === 0.
309
+ */
310
+ evictEntry(entry) {
311
+ for (const path of entry.accessedPaths) {
312
+ const dependents = this.dependencyMap.get(path);
313
+ if (dependents) {
314
+ dependents.delete(entry.id);
315
+ if (dependents.size === 0) this.dependencyMap.delete(path);
316
+ }
317
+ }
318
+ this.reactiveSelectors.delete(entry.id);
319
+ this.pathBasedCache.delete(entry.pathCacheKey);
320
+ }
321
+ dispose() {
322
+ this.unsubscribeFromStore();
323
+ this.reactiveSelectors.clear();
324
+ this.dependencyMap.clear();
325
+ this.pathBasedCache.clear();
326
+ }
327
+ };
328
+ /**
329
+ * Builds an array of leaf paths accessed by a selector.
330
+ * Uses static proxy-based analysis — the selector is run with a proxy tree,
331
+ * never against real state.
332
+ *
333
+ * Works reliably ONLY for simple property access patterns:
334
+ * state.user.name
335
+ * state.items[0].value
336
+ * state.config.theme.colors.primary
337
+ *
338
+ * Does NOT work for (and will throw):
339
+ * state.items.map(...) — array iteration
340
+ * state.user.isAdmin ? ... : .. — conditionals (valueOf coercion)
341
+ * state.a + state.b — arithmetic (valueOf coercion)
342
+ * 'key' in state — in operator
343
+ *
344
+ * @param selector - The selector function to analyse
345
+ * @param divider - The path separator (default: ".")
346
+ * @returns Sorted array of leaf paths accessed by the selector
347
+ *
348
+ * @throws {Error} If the selector attempts any operation beyond property access
349
+ */
350
+ function buildPaths(selector, divider = ".") {
351
+ const leafPaths = /* @__PURE__ */ new Set();
352
+ const proxyCache = /* @__PURE__ */ new Map();
353
+ const createProxy = (currentPathString = "") => {
354
+ if (proxyCache.has(currentPathString)) return proxyCache.get(currentPathString);
355
+ const proxy = new Proxy(() => {}, {
356
+ get: (_, prop) => {
357
+ if (typeof prop === "symbol" || prop === "then") return;
358
+ if (prop === "valueOf" || prop === "toString") throw new Error("Cannot perform logic, arithmetic, or string operations inside a selector.");
359
+ if (ARRAY_METHODS.includes(prop)) throw new Error(`Array method .${prop}() is not allowed in selectors.`);
360
+ const newPathString = currentPathString ? `${currentPathString}${divider}${prop}` : prop;
361
+ if (currentPathString) leafPaths.delete(currentPathString);
362
+ leafPaths.add(newPathString);
363
+ return createProxy(newPathString);
364
+ },
365
+ has: () => {
366
+ throw new Error("The 'in' operator is not allowed in selectors.");
367
+ },
368
+ apply: () => {
369
+ throw new Error("Selectors cannot call functions or methods.");
370
+ }
371
+ });
372
+ proxyCache.set(currentPathString, proxy);
373
+ return proxy;
374
+ };
375
+ try {
376
+ selector(createProxy());
377
+ } catch (error) {
378
+ throw new Error(`Selector failed during path analysis. Selectors must be simple property accessors only. Error: ${error instanceof Error ? error.message : String(error)}`);
379
+ }
380
+ return Array.from(leafPaths);
381
+ }
382
+
383
+ //#endregion
384
+ //#region src/store/diff.ts
385
+ /**
386
+ * A performant deep equality check for any value.
387
+ * Faster than JSON.stringify for comparisons.
388
+ */
389
+ function isEqual(a, b) {
390
+ if (a === b) return true;
391
+ if (a && b && typeof a === "object" && typeof b === "object") {
392
+ if (a.constructor !== b.constructor) return false;
393
+ let length, i;
394
+ if (Array.isArray(a)) {
395
+ length = a.length;
396
+ if (length != b.length) return false;
397
+ for (i = length; i-- > 0;) if (!isEqual(a[i], b[i])) return false;
398
+ return true;
399
+ }
400
+ const [aKeys, bKeys] = [Object.keys(a), Object.keys(b)];
401
+ length = aKeys.length;
402
+ if (length !== bKeys.length) return false;
403
+ for (i = length; i-- > 0;) {
404
+ const key = aKeys[i];
405
+ if (!Object.prototype.hasOwnProperty.call(b, key) || !isEqual(a[key], b[key])) return false;
406
+ }
407
+ return true;
408
+ }
409
+ return a !== a && b !== b;
410
+ }
411
+ /**
412
+ * Creates a diff function with configurable options.
413
+ *
414
+ * @param {object} options - Configuration options for the diff function.
415
+ * @param {symbol} [options.deleteMarker] - A custom symbol to mark properties for deletion.
416
+ */
417
+ function createDiff(options) {
418
+ const deleteSymbol = options?.deleteMarker || DELETE_SYMBOL;
419
+ function diffFn(original, partial) {
420
+ const deltas = [];
421
+ const stack = [{
422
+ pathStr: "",
423
+ orig: original || {},
424
+ part: partial || {}
425
+ }];
426
+ while (stack.length > 0) {
427
+ const { pathStr, orig, part } = stack.pop();
428
+ if (part === null || part === void 0) continue;
429
+ if (!isEqual(orig, part)) {
430
+ if (typeof part === "object" && !Array.isArray(part)) for (const key of Object.keys(part)) {
431
+ const newPathStr = pathStr ? pathStr + "." + key : key;
432
+ const partValue = part[key];
433
+ const origValue = orig && typeof orig === "object" ? orig[key] : void 0;
434
+ if (partValue === deleteSymbol) {
435
+ if (origValue !== void 0) deltas.push({
436
+ path: newPathStr,
437
+ oldValue: origValue,
438
+ newValue: void 0
439
+ });
440
+ continue;
441
+ }
442
+ if (typeof partValue === "object" && partValue !== null) stack.push({
443
+ pathStr: newPathStr,
444
+ orig: origValue,
445
+ part: partValue
446
+ });
447
+ else if (!isEqual(origValue, partValue)) deltas.push({
448
+ path: newPathStr,
449
+ oldValue: origValue,
450
+ newValue: partValue
451
+ });
452
+ }
453
+ else if (pathStr) deltas.push({
454
+ path: pathStr,
455
+ oldValue: orig,
456
+ newValue: part
457
+ });
458
+ }
459
+ }
460
+ return deltas;
461
+ }
462
+ return diffFn;
463
+ }
464
+ /**
465
+ * Creates a derivePaths function with configurable options.
466
+ *
467
+ * @param {object} options - Configuration options for the derivePaths function.
468
+ * @param {symbol} [options.deleteMarker=Symbol.for("delete")] - A custom symbol to mark properties for deletion.
469
+ */
470
+ function createDerivePaths(options) {
471
+ const deleteSymbol = options?.deleteMarker || DELETE_SYMBOL;
472
+ /**
473
+ * Computes all touched paths from a DeepPartial changes object.
474
+ * Each key at every level of nesting is a path, including intermediate
475
+ * parent paths. The stack naturally visits parents before children, so
476
+ * parent paths are added as each object level is processed — no
477
+ * secondary backfill loop needed.
478
+ */
479
+ function derivePathsFn(changes) {
480
+ const paths = /* @__PURE__ */ new Set();
481
+ const stack = [{
482
+ obj: changes,
483
+ currentPath: ""
484
+ }];
485
+ while (stack.length > 0) {
486
+ const { obj, currentPath } = stack.pop();
487
+ if (obj === null || obj === void 0 || typeof obj !== "object" || Array.isArray(obj)) continue;
488
+ for (const key of Object.keys(obj)) {
489
+ const newPath = currentPath ? `${currentPath}.${key}` : key;
490
+ paths.add(newPath);
491
+ const value = obj[key];
492
+ if (typeof value === "object" && value !== null && !Array.isArray(value) && value !== deleteSymbol) stack.push({
493
+ obj: value,
494
+ currentPath: newPath
495
+ });
496
+ }
497
+ }
498
+ return Array.from(paths);
499
+ }
500
+ return derivePathsFn;
501
+ }
502
+ /**
503
+ * @deprecated Use `createDiff()` instead.
504
+ * Provided for backward compatibility with `Symbol.for("delete")` as the default delete marker.
505
+ */
506
+ const diff = createDiff();
507
+ /**
508
+ * @deprecated Use `createDerivePaths()` instead.
509
+ * Provided for backward compatibility with `Symbol.for("delete")` as the default delete marker.
510
+ */
511
+ const derivePaths = createDerivePaths();
512
+
513
+ //#endregion
514
+ //#region src/store/state.ts
515
+ /**
516
+ * Core state management component - handles pure state operations
517
+ */
518
+ var StateManager = class {
519
+ updateBus;
520
+ diff;
521
+ cache;
522
+ /**
523
+ * Creates an instance of CoreStateManager.
524
+ * @param initialData The initial state of the store.
525
+ * @param updateBus The event bus to notify listeners of updates.
526
+ * @param diff The function to calculate the difference between two states.
527
+ */
528
+ constructor(initialData, updateBus, diff) {
529
+ this.updateBus = updateBus;
530
+ this.diff = diff;
531
+ this.cache = structuredClone(initialData);
532
+ }
533
+ /**
534
+ * Gets the current state of the store.
535
+ * @param clone If true, returns a deep clone of the state. Defaults to false.
536
+ * @returns The current state.
537
+ */
538
+ get(clone) {
539
+ if (clone) return structuredClone(this.cache);
540
+ return this.cache;
541
+ }
542
+ /**
543
+ * Applies changes to the state and notifies listeners.
544
+ * @param newState The new state to apply.
545
+ * @returns An array of paths that were changed.
546
+ */
547
+ applyChanges(newState, force = false, clone = false, changes = []) {
548
+ if (force) {
549
+ this.cache = clone ? structuredClone(newState) : newState;
550
+ this.notifyListeners([]);
551
+ return [];
552
+ }
553
+ if (changes.length === 0) changes = [newState];
554
+ const state = this.get(false);
555
+ const deltaMap = /* @__PURE__ */ new Map();
556
+ for (let i = 0; i < changes.length; i++) {
557
+ const partial = changes[i];
558
+ const diffs = this.diff(state, partial);
559
+ for (let j = 0; j < diffs.length; j++) {
560
+ const d = diffs[j];
561
+ deltaMap.set(d.path, d);
562
+ }
563
+ }
564
+ const deltas = deltaMap.size ? [...deltaMap.values()] : [];
565
+ if (deltas.length > 0) {
566
+ this.cache = clone ? structuredClone(newState) : newState;
567
+ const changed = /* @__PURE__ */ new Set();
568
+ for (let i = 0; i < deltas.length; i++) {
569
+ let p = deltas[i].path;
570
+ while (p) {
571
+ if (!changed.has(p)) changed.add(p);
572
+ else break;
573
+ const dot = p.lastIndexOf(".");
574
+ if (dot < 0) break;
575
+ p = p.slice(0, dot);
576
+ }
577
+ }
578
+ this.notifyListeners(changed);
579
+ }
580
+ return deltas;
581
+ }
582
+ /**
583
+ * Notifies listeners of changes to the state.
584
+ * @param changedPaths An array of paths that were changed.
585
+ */
586
+ notifyListeners(changedPaths) {
587
+ for (const changed of changedPaths) this.updateBus.emit({
588
+ name: "update",
589
+ payload: changed
590
+ });
591
+ }
592
+ };
593
+
594
+ //#endregion
595
+ //#region src/store/middleware.ts
596
+ /**
597
+ * Middleware execution engine - handles all middleware processing
598
+ */
599
+ var MiddlewareEngine = class {
600
+ eventBus;
601
+ executionState;
602
+ merge;
603
+ logger;
604
+ middleware = [];
605
+ blockingMiddleware = [];
606
+ constructor(eventBus, executionState, merge, logger) {
607
+ this.eventBus = eventBus;
608
+ this.executionState = executionState;
609
+ this.merge = merge;
610
+ this.logger = logger;
611
+ }
612
+ async executeBlocking(currentState, partialUpdate) {
613
+ for (const { fn, name, id } of this.blockingMiddleware) {
614
+ const execInfo = {
615
+ id,
616
+ name,
617
+ startTime: Date.now()
618
+ };
619
+ this.executionState.runningMiddleware = {
620
+ id,
621
+ name,
622
+ startTime: Date.now()
623
+ };
624
+ this.emitMiddlewareLifecycle("start", {
625
+ id,
626
+ name,
627
+ type: "blocking"
628
+ });
629
+ try {
630
+ const result = await Promise.resolve(fn(currentState, partialUpdate));
631
+ execInfo.endTime = Date.now();
632
+ execInfo.duration = execInfo.endTime - execInfo.startTime;
633
+ if (result === false) {
634
+ execInfo.blocked = true;
635
+ this.emitMiddlewareLifecycle("blocked", {
636
+ id,
637
+ name,
638
+ duration: execInfo.duration
639
+ });
640
+ this.emit(this.eventBus, {
641
+ name: "middleware:executed",
642
+ payload: execInfo
643
+ });
644
+ return { blocked: true };
645
+ }
646
+ this.emitMiddlewareLifecycle("complete", {
647
+ id,
648
+ name,
649
+ type: "blocking",
650
+ duration: execInfo.duration
651
+ });
652
+ this.emit(this.eventBus, {
653
+ name: "middleware:executed",
654
+ payload: {
655
+ ...execInfo,
656
+ blocked: false
657
+ }
658
+ });
659
+ } catch (error) {
660
+ execInfo.endTime = Date.now();
661
+ execInfo.duration = execInfo.endTime - execInfo.startTime;
662
+ execInfo.error = error instanceof Error ? error : new Error(String(error));
663
+ execInfo.blocked = true;
664
+ this.emitMiddlewareError(id, name, execInfo.error, execInfo.duration);
665
+ this.emit(this.eventBus, {
666
+ name: "middleware:executed",
667
+ payload: execInfo
668
+ });
669
+ return {
670
+ blocked: true,
671
+ error: execInfo.error
672
+ };
673
+ } finally {
674
+ this.executionState.runningMiddleware = null;
675
+ }
676
+ }
677
+ return { blocked: false };
678
+ }
679
+ async executeTransform(initialState, partialUpdate) {
680
+ let currentState = initialState;
681
+ let changes = partialUpdate;
682
+ for (const { fn, name, id } of this.middleware) {
683
+ const execInfo = {
684
+ id,
685
+ name,
686
+ startTime: Date.now()
687
+ };
688
+ this.executionState.runningMiddleware = {
689
+ id,
690
+ name,
691
+ startTime: Date.now()
692
+ };
693
+ this.emitMiddlewareLifecycle("start", {
694
+ id,
695
+ name,
696
+ type: "transform"
697
+ });
698
+ try {
699
+ const middlewareResult = await Promise.resolve(fn(currentState, partialUpdate));
700
+ execInfo.endTime = Date.now();
701
+ execInfo.duration = execInfo.endTime - execInfo.startTime;
702
+ execInfo.blocked = false;
703
+ if (middlewareResult && typeof middlewareResult === "object") {
704
+ currentState = this.merge(currentState, middlewareResult);
705
+ changes = this.merge(changes, middlewareResult);
706
+ }
707
+ this.emit(this.eventBus, {
708
+ name: "middleware:executed",
709
+ payload: execInfo
710
+ });
711
+ this.emitMiddlewareLifecycle("complete", {
712
+ id,
713
+ name,
714
+ type: "transform",
715
+ duration: execInfo.duration
716
+ });
717
+ } catch (e) {
718
+ execInfo.endTime = Date.now();
719
+ execInfo.duration = execInfo.endTime - execInfo.startTime;
720
+ execInfo.error = e instanceof Error ? e : new Error(String(e));
721
+ execInfo.blocked = false;
722
+ this.emit(this.eventBus, {
723
+ name: "middleware:executed",
724
+ payload: execInfo
725
+ });
726
+ this.emitMiddlewareError(id, name, execInfo.error, execInfo.duration);
727
+ this.logger.error("Middleware error", {
728
+ name,
729
+ error: e
730
+ });
731
+ } finally {
732
+ this.executionState.runningMiddleware = null;
733
+ }
734
+ }
735
+ return changes;
736
+ }
737
+ addMiddleware(middleware, name = "unnamed-middleware") {
738
+ const id = this.generateId();
739
+ this.middleware.push({
740
+ fn: middleware,
741
+ name,
742
+ id
743
+ });
744
+ this.updateExecutionState();
745
+ return id;
746
+ }
747
+ addBlockingMiddleware(middleware, name = "unnamed-blocking-middleware") {
748
+ const id = this.generateId();
749
+ this.blockingMiddleware.push({
750
+ fn: middleware,
751
+ name,
752
+ id
753
+ });
754
+ this.updateExecutionState();
755
+ return id;
756
+ }
757
+ removeMiddleware(id) {
758
+ const initialLength = this.middleware.length + this.blockingMiddleware.length;
759
+ this.middleware = this.middleware.filter((m) => m.id !== id);
760
+ this.blockingMiddleware = this.blockingMiddleware.filter((m) => m.id !== id);
761
+ this.updateExecutionState();
762
+ return this.middleware.length + this.blockingMiddleware.length < initialLength;
763
+ }
764
+ updateExecutionState() {
765
+ this.executionState.middlewares = [...this.middleware.map((m) => m.name), ...this.blockingMiddleware.map((m) => m.name)];
766
+ }
767
+ emitMiddlewareLifecycle(phase, data) {
768
+ this.emit(this.eventBus, {
769
+ name: `middleware:${phase}`,
770
+ payload: {
771
+ ...data,
772
+ timestamp: Date.now()
773
+ }
774
+ });
775
+ }
776
+ emitMiddlewareError(id, name, error, duration) {
777
+ this.emit(this.eventBus, {
778
+ name: "middleware:error",
779
+ payload: {
780
+ id,
781
+ name,
782
+ error,
783
+ duration,
784
+ timestamp: Date.now()
785
+ }
786
+ });
787
+ }
788
+ generateId() {
789
+ return crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
790
+ }
791
+ emit(bus, event) {
792
+ queueMicrotask(() => {
793
+ bus.emit(event);
794
+ });
795
+ }
796
+ };
797
+
798
+ //#endregion
799
+ //#region src/store/logger.ts
800
+ let noopLogger;
801
+ function createStoreLogger(logger) {
802
+ if (logger) return logger;
803
+ if (!noopLogger) noopLogger = new Logger([]);
804
+ return noopLogger;
805
+ }
806
+
807
+ //#endregion
808
+ //#region src/store/persistence.ts
809
+ /**
810
+ * Persistence management component - handles all persistence operations
811
+ * Supports background persistence for better UI performance.
812
+ */
813
+ var PersistenceHandler = class {
814
+ eventBus;
815
+ coreState;
816
+ persistence;
817
+ instanceID;
818
+ persistenceReady = false;
819
+ backgroundQueue = [];
820
+ isProcessingQueue = false;
821
+ maxRetries = 3;
822
+ retryDelay = 1e3;
823
+ queueProcessor;
824
+ /**
825
+ * Tracks tasks that are pending retry. These are held outside the main
826
+ * queue until their backoff delay has elapsed, at which point they are
827
+ * re-inserted. This prevents the processQueue while-loop from immediately
828
+ * picking up a retried task before its delay has passed.
829
+ */
830
+ pendingRetries = /* @__PURE__ */ new Set();
831
+ logger;
832
+ constructor(eventBus, coreState, instanceID, options) {
833
+ this.eventBus = eventBus;
834
+ this.coreState = coreState;
835
+ this.instanceID = instanceID;
836
+ this.maxRetries = options?.maxRetries ?? 3;
837
+ this.retryDelay = options?.retryDelay ?? 1e3;
838
+ this.logger = options?.logger ?? createStoreLogger();
839
+ }
840
+ async initialize(persistence) {
841
+ if (persistence) await this.setPersistence(persistence);
842
+ else this.setPersistenceReady();
843
+ }
844
+ isReady() {
845
+ return this.persistenceReady;
846
+ }
847
+ /**
848
+ * Handle state changes with background persistence.
849
+ * Returns immediately for optimistic updates.
850
+ */
851
+ handleStateChange(changedPaths, state) {
852
+ if (!this.persistence || changedPaths.length === 0) return;
853
+ const task = {
854
+ id: `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
855
+ state: structuredClone(state),
856
+ changedPaths: [...changedPaths],
857
+ timestamp: Date.now(),
858
+ retries: 0
859
+ };
860
+ this.backgroundQueue.push(task);
861
+ this.scheduleQueueProcessing();
862
+ this.emit(this.eventBus, {
863
+ name: "persistence:queued",
864
+ payload: {
865
+ taskId: task.id,
866
+ changedPaths,
867
+ queueSize: this.backgroundQueue.length,
868
+ timestamp: task.timestamp
869
+ }
870
+ });
871
+ }
872
+ /**
873
+ * Get current persistence queue status.
874
+ */
875
+ getQueueStatus() {
876
+ return {
877
+ queueSize: this.backgroundQueue.length,
878
+ isProcessing: this.isProcessingQueue,
879
+ pendingRetries: this.pendingRetries.size,
880
+ oldestTask: this.backgroundQueue[0]?.timestamp
881
+ };
882
+ }
883
+ /**
884
+ * Force immediate processing of the persistence queue.
885
+ * Waits for any in-progress processing to complete first.
886
+ */
887
+ async flush() {
888
+ if (this.isProcessingQueue) await new Promise((resolve) => {
889
+ const check = () => {
890
+ if (!this.isProcessingQueue) resolve();
891
+ else setTimeout(check, 10);
892
+ };
893
+ check();
894
+ });
895
+ await this.processQueue();
896
+ }
897
+ /**
898
+ * Discards all pending (unprocessed) tasks in the queue.
899
+ * Any tasks currently being processed or awaiting retry are also dropped.
900
+ * WARNING: This will result in data loss for any queued writes that have
901
+ * not yet been persisted. Prefer flush() for safe teardown.
902
+ */
903
+ discardQueue() {
904
+ const discardedTasks = this.backgroundQueue.length + this.pendingRetries.size;
905
+ this.backgroundQueue = [];
906
+ this.pendingRetries.clear();
907
+ if (this.queueProcessor) {
908
+ clearTimeout(this.queueProcessor);
909
+ this.queueProcessor = void 0;
910
+ }
911
+ this.emit(this.eventBus, {
912
+ name: "persistence:queue_cleared",
913
+ payload: {
914
+ clearedTasks: discardedTasks,
915
+ timestamp: Date.now()
916
+ }
917
+ });
918
+ }
919
+ scheduleQueueProcessing() {
920
+ if (this.queueProcessor || this.isProcessingQueue) return;
921
+ this.queueProcessor = setTimeout(() => {
922
+ this.processQueue().catch((error) => {
923
+ this.logger.error("Queue processing failed", { error });
924
+ });
925
+ }, 10);
926
+ }
927
+ async processQueue() {
928
+ if (this.isProcessingQueue || this.backgroundQueue.length === 0) return;
929
+ this.isProcessingQueue = true;
930
+ this.queueProcessor = void 0;
931
+ try {
932
+ while (this.backgroundQueue.length > 0) {
933
+ const task = this.backgroundQueue.shift();
934
+ await this.processTask(task);
935
+ }
936
+ } finally {
937
+ this.isProcessingQueue = false;
938
+ }
939
+ }
940
+ async processTask(task) {
941
+ try {
942
+ if (await this.persistence.set(this.instanceID, task.state)) this.emit(this.eventBus, {
943
+ name: "persistence:success",
944
+ payload: {
945
+ taskId: task.id,
946
+ changedPaths: task.changedPaths,
947
+ duration: Date.now() - task.timestamp,
948
+ timestamp: Date.now()
949
+ }
950
+ });
951
+ else await this.handleTaskFailure(task, /* @__PURE__ */ new Error("Persistence returned false"));
952
+ } catch (error) {
953
+ await this.handleTaskFailure(task, error);
954
+ }
955
+ }
956
+ async handleTaskFailure(task, error) {
957
+ task.retries++;
958
+ if (task.retries <= this.maxRetries) {
959
+ const delay = this.retryDelay * Math.pow(2, task.retries - 1);
960
+ this.emit(this.eventBus, {
961
+ name: "persistence:retry",
962
+ payload: {
963
+ taskId: task.id,
964
+ attempt: task.retries,
965
+ maxRetries: this.maxRetries,
966
+ nextRetryIn: delay,
967
+ error,
968
+ timestamp: Date.now()
969
+ }
970
+ });
971
+ this.pendingRetries.add(task.id);
972
+ setTimeout(() => {
973
+ if (this.pendingRetries.has(task.id)) {
974
+ this.pendingRetries.delete(task.id);
975
+ this.backgroundQueue.unshift(task);
976
+ this.scheduleQueueProcessing();
977
+ }
978
+ }, delay);
979
+ } else this.emit(this.eventBus, {
980
+ name: "persistence:failed",
981
+ payload: {
982
+ taskId: task.id,
983
+ changedPaths: task.changedPaths,
984
+ attempts: task.retries,
985
+ error,
986
+ timestamp: Date.now()
987
+ }
988
+ });
989
+ }
990
+ setPersistenceReady() {
991
+ this.persistenceReady = true;
992
+ this.emit(this.eventBus, {
993
+ name: "persistence:ready",
994
+ payload: { timestamp: Date.now() }
995
+ });
996
+ }
997
+ async setPersistence(persistence) {
998
+ this.persistence = persistence;
999
+ try {
1000
+ const persistedState = await this.persistence.get();
1001
+ if (persistedState) this.coreState.applyChanges(persistedState);
1002
+ } catch (error) {
1003
+ this.logger.error("Failed to initialize persistence", { error });
1004
+ this.emit(this.eventBus, {
1005
+ name: "persistence:init_error",
1006
+ payload: {
1007
+ error,
1008
+ timestamp: Date.now()
1009
+ }
1010
+ });
1011
+ } finally {
1012
+ this.setPersistenceReady();
1013
+ }
1014
+ this.persistence.subscribe(this.instanceID, async (state) => {
1015
+ const changedPaths = this.coreState.applyChanges(state);
1016
+ if (changedPaths.length > 0) this.emit(this.eventBus, {
1017
+ name: "update:complete",
1018
+ payload: {
1019
+ changedPaths,
1020
+ source: "external",
1021
+ timestamp: Date.now()
1022
+ }
1023
+ });
1024
+ });
1025
+ }
1026
+ /**
1027
+ * Cleanup method for proper resource disposal.
1028
+ * Discards any unprocessed tasks — call flush() first if writes must not be lost.
1029
+ */
1030
+ async dispose() {
1031
+ this.discardQueue();
1032
+ this.isProcessingQueue = false;
1033
+ this.persistenceReady = false;
1034
+ await (this.persistence?.close && this.persistence.close());
1035
+ }
1036
+ emit(bus, event) {
1037
+ queueMicrotask(() => {
1038
+ bus.emit(event);
1039
+ });
1040
+ }
1041
+ };
1042
+
1043
+ //#endregion
1044
+ //#region src/store/transactions.ts
1045
+ /**
1046
+ * Transaction management component - handles transactional operations
1047
+ */
1048
+ var TransactionManager = class {
1049
+ eventBus;
1050
+ coreState;
1051
+ executionState;
1052
+ constructor(eventBus, coreState, executionState) {
1053
+ this.eventBus = eventBus;
1054
+ this.coreState = coreState;
1055
+ this.executionState = executionState;
1056
+ }
1057
+ async execute(operation) {
1058
+ const snapshot = this.coreState.get(true);
1059
+ this.executionState.transactionActive = true;
1060
+ this.emit(this.eventBus, {
1061
+ name: "transaction:start",
1062
+ payload: { timestamp: Date.now() }
1063
+ });
1064
+ try {
1065
+ const result = await Promise.resolve(operation());
1066
+ this.emit(this.eventBus, {
1067
+ name: "transaction:complete",
1068
+ payload: { timestamp: Date.now() }
1069
+ });
1070
+ this.executionState.transactionActive = false;
1071
+ return result;
1072
+ } catch (error) {
1073
+ this.coreState.applyChanges(snapshot, true, false);
1074
+ this.emit(this.eventBus, {
1075
+ name: "transaction:error",
1076
+ payload: {
1077
+ error: error instanceof Error ? error : new Error(String(error)),
1078
+ timestamp: Date.now()
1079
+ }
1080
+ });
1081
+ this.executionState.transactionActive = false;
1082
+ throw error;
1083
+ }
1084
+ }
1085
+ emit(bus, event) {
1086
+ queueMicrotask(() => {
1087
+ bus.emit(event);
1088
+ });
1089
+ }
1090
+ };
1091
+
1092
+ //#endregion
1093
+ //#region src/store/metrics.ts
1094
+ /**
1095
+ * Event-driven metrics collector - derives metrics from event stream
1096
+ * Now properly implements the full StoreMetrics interface
1097
+ */
1098
+ var MetricsCollector = class {
1099
+ updateCount = 0;
1100
+ listenerExecutions = 0;
1101
+ averageUpdateTime = 0;
1102
+ largestUpdateSize = 0;
1103
+ mostActiveListenerPaths = [];
1104
+ totalUpdates = 0;
1105
+ blockedUpdates = 0;
1106
+ averageUpdateDuration = 0;
1107
+ middlewareExecutions = 0;
1108
+ transactionCount = 0;
1109
+ totalEventsFired = 0;
1110
+ totalActionsDispatched = 0;
1111
+ totalActionsSucceeded = 0;
1112
+ totalActionsFailed = 0;
1113
+ averageActionDuration = 0;
1114
+ updateTimes = [];
1115
+ actionTimes = [];
1116
+ pathExecutionCounts = /* @__PURE__ */ new Map();
1117
+ constructor(eventBus) {
1118
+ this.setupEventListeners(eventBus);
1119
+ }
1120
+ getMetrics() {
1121
+ return {
1122
+ updateCount: this.updateCount,
1123
+ listenerExecutions: this.listenerExecutions,
1124
+ averageUpdateTime: this.averageUpdateTime,
1125
+ largestUpdateSize: this.largestUpdateSize,
1126
+ mostActiveListenerPaths: [...this.mostActiveListenerPaths],
1127
+ totalUpdates: this.totalUpdates,
1128
+ blockedUpdates: this.blockedUpdates,
1129
+ averageUpdateDuration: this.averageUpdateDuration,
1130
+ middlewareExecutions: this.middlewareExecutions,
1131
+ transactionCount: this.transactionCount,
1132
+ totalEventsFired: this.totalEventsFired,
1133
+ totalActionsDispatched: this.totalActionsDispatched,
1134
+ totalActionsSucceeded: this.totalActionsSucceeded,
1135
+ totalActionsFailed: this.totalActionsFailed,
1136
+ averageActionDuration: this.averageActionDuration
1137
+ };
1138
+ }
1139
+ setupEventListeners(eventBus) {
1140
+ const originalEmit = eventBus.emit;
1141
+ eventBus.emit = (event) => {
1142
+ this.totalEventsFired++;
1143
+ return originalEmit.call(eventBus, event);
1144
+ };
1145
+ eventBus.subscribe("update:complete", (data) => {
1146
+ this.totalUpdates++;
1147
+ if (data.blocked) {
1148
+ this.blockedUpdates++;
1149
+ return;
1150
+ }
1151
+ if (data.duration) {
1152
+ this.updateTimes.push(data.duration);
1153
+ if (this.updateTimes.length > 100) this.updateTimes.shift();
1154
+ const average = this.updateTimes.reduce((a, b) => a + b, 0) / this.updateTimes.length;
1155
+ this.averageUpdateTime = average;
1156
+ this.averageUpdateDuration = average;
1157
+ }
1158
+ if (data.deltas?.length) {
1159
+ this.updateCount++;
1160
+ this.largestUpdateSize = Math.max(this.largestUpdateSize, data.deltas.length);
1161
+ data.deltas.forEach((delta) => {
1162
+ const current = this.pathExecutionCounts.get(delta.path) || 0;
1163
+ this.pathExecutionCounts.set(delta.path, current + 1);
1164
+ });
1165
+ this.mostActiveListenerPaths = Array.from(this.pathExecutionCounts.entries()).sort(([, a], [, b]) => b - a).slice(0, 5).map(([path]) => path);
1166
+ }
1167
+ });
1168
+ eventBus.subscribe("middleware:start", () => {
1169
+ this.middlewareExecutions++;
1170
+ });
1171
+ eventBus.subscribe("transaction:start", () => {
1172
+ this.transactionCount++;
1173
+ });
1174
+ eventBus.subscribe("action:start", () => {
1175
+ this.totalActionsDispatched++;
1176
+ });
1177
+ eventBus.subscribe("action:complete", (data) => {
1178
+ this.totalActionsSucceeded++;
1179
+ if (data.duration) {
1180
+ this.actionTimes.push(data.duration);
1181
+ if (this.actionTimes.length > 100) this.actionTimes.shift();
1182
+ this.averageActionDuration = this.actionTimes.reduce((a, b) => a + b, 0) / this.actionTimes.length;
1183
+ }
1184
+ });
1185
+ eventBus.subscribe("action:error", () => {
1186
+ this.totalActionsFailed++;
1187
+ });
1188
+ }
1189
+ /**
1190
+ * Reset all metrics to initial state
1191
+ */
1192
+ reset() {
1193
+ this.updateCount = 0;
1194
+ this.listenerExecutions = 0;
1195
+ this.averageUpdateTime = 0;
1196
+ this.largestUpdateSize = 0;
1197
+ this.mostActiveListenerPaths = [];
1198
+ this.totalUpdates = 0;
1199
+ this.blockedUpdates = 0;
1200
+ this.averageUpdateDuration = 0;
1201
+ this.middlewareExecutions = 0;
1202
+ this.transactionCount = 0;
1203
+ this.totalEventsFired = 0;
1204
+ this.totalActionsDispatched = 0;
1205
+ this.totalActionsSucceeded = 0;
1206
+ this.totalActionsFailed = 0;
1207
+ this.averageActionDuration = 0;
1208
+ this.updateTimes = [];
1209
+ this.actionTimes = [];
1210
+ this.pathExecutionCounts.clear();
1211
+ }
1212
+ /**
1213
+ * Get detailed metrics breakdown
1214
+ */
1215
+ getDetailedMetrics() {
1216
+ return {
1217
+ pathExecutionCounts: new Map(this.pathExecutionCounts),
1218
+ recentUpdateTimes: [...this.updateTimes],
1219
+ successRate: this.totalUpdates > 0 ? (this.totalUpdates - this.blockedUpdates) / this.totalUpdates : 1,
1220
+ averagePathsPerUpdate: this.updateCount > 0 ? Array.from(this.pathExecutionCounts.values()).reduce((a, b) => a + b, 0) / this.updateCount : 0
1221
+ };
1222
+ }
1223
+ /**
1224
+ * Cleanup method for proper resource disposal
1225
+ */
1226
+ dispose() {
1227
+ this.reset();
1228
+ }
1229
+ };
1230
+
1231
+ //#endregion
1232
+ //#region src/store/actions.ts
1233
+ var ActionCancelledError = class extends Error {
1234
+ constructor() {
1235
+ super("Action Cancelled by Debounce");
1236
+ this.name = "ActionCancelledError";
1237
+ }
1238
+ };
1239
+ var UnknownActionError = class extends Error {
1240
+ constructor({ action }) {
1241
+ super(`Unknown action: "${action}"`);
1242
+ this.name = "UnknownActionError";
1243
+ }
1244
+ };
1245
+ const sentinel = () => {};
1246
+ const sentinelWatcher = {
1247
+ name: "UNDEFINED ACTION",
1248
+ status: () => false,
1249
+ subscribe: (_) => () => {}
1250
+ };
1251
+ var ActionManager = class {
1252
+ eventBus;
1253
+ set;
1254
+ registrations = /* @__PURE__ */ new Map();
1255
+ constructor(eventBus, set) {
1256
+ this.eventBus = eventBus;
1257
+ this.set = set;
1258
+ }
1259
+ register(action) {
1260
+ const registration = {
1261
+ action: {
1262
+ name: action.name,
1263
+ id: v4(),
1264
+ action: action.fn,
1265
+ debounce: action.debounce ? {
1266
+ ...action.debounce,
1267
+ condition: action.debounce.condition ?? (() => true)
1268
+ } : void 0
1269
+ },
1270
+ debouncer: action.debounce && action.debounce.delay > 0 ? new Debouncer({ delay: action.debounce.delay }) : void 0,
1271
+ previousArgs: void 0,
1272
+ running: false,
1273
+ subscription: {
1274
+ listeners: /* @__PURE__ */ new Set(),
1275
+ watcher: null,
1276
+ watchers: new SharedResource(() => [
1277
+ this.eventBus.subscribe("action:start", (p) => p.name === action.name && this.notifyStatusListeners(action.name)),
1278
+ this.eventBus.subscribe("action:complete", (p) => p.name === action.name && this.notifyStatusListeners(action.name)),
1279
+ this.eventBus.subscribe("action:error", (p) => p.name === action.name && this.notifyStatusListeners(action.name))
1280
+ ], (unsubs) => unsubs?.forEach((unsub) => unsub()), { gracePeriod: "microtask" })
1281
+ }
1282
+ };
1283
+ this.registrations.set(action.name, registration);
1284
+ return () => {
1285
+ const reg = this.registrations.get(action.name);
1286
+ if (!reg) return;
1287
+ reg.debouncer?.cancel();
1288
+ this.registrations.delete(action.name);
1289
+ };
1290
+ }
1291
+ async dispatch(name, ...params) {
1292
+ const reg = this.registrations.get(name);
1293
+ if (!reg) throw new UnknownActionError({ action: name });
1294
+ const { action, debouncer } = reg;
1295
+ const { debounce } = action;
1296
+ if (!debouncer || !debounce) return this.executeAction(reg, params);
1297
+ const shouldDebounce = debounce.condition(reg.previousArgs, params);
1298
+ reg.previousArgs = params;
1299
+ if (!shouldDebounce) return this.executeAction(reg, params);
1300
+ const debouncerResult = await debouncer.do(() => this.executeAction(reg, params));
1301
+ if (debouncerResult.status === "cancelled") throw new ActionCancelledError();
1302
+ if (debouncerResult.status === "error" && debouncerResult.error) throw debouncerResult.error;
1303
+ return debouncerResult.value;
1304
+ }
1305
+ async executeAction(reg, params) {
1306
+ const startTime = Date.now();
1307
+ reg.running = true;
1308
+ this.emit(this.eventBus, {
1309
+ name: "action:start",
1310
+ payload: {
1311
+ actionId: reg.action.id,
1312
+ name: reg.action.name,
1313
+ params: params || [],
1314
+ timestamp: startTime
1315
+ }
1316
+ });
1317
+ try {
1318
+ const result = await this.set((s) => reg.action.action(s, ...params), { actionId: reg.action.id });
1319
+ const endTime = Date.now();
1320
+ reg.running = false;
1321
+ this.emit(this.eventBus, {
1322
+ name: "action:complete",
1323
+ payload: {
1324
+ actionId: reg.action.id,
1325
+ name: reg.action.name,
1326
+ params,
1327
+ startTime,
1328
+ endTime,
1329
+ duration: endTime - startTime,
1330
+ result
1331
+ }
1332
+ });
1333
+ return result;
1334
+ } catch (error) {
1335
+ const endTime = Date.now();
1336
+ reg.running = false;
1337
+ this.emit(this.eventBus, {
1338
+ name: "action:error",
1339
+ payload: {
1340
+ actionId: reg.action.id,
1341
+ name: reg.action.name,
1342
+ params,
1343
+ startTime,
1344
+ endTime,
1345
+ duration: endTime - startTime,
1346
+ error
1347
+ }
1348
+ });
1349
+ throw error;
1350
+ }
1351
+ }
1352
+ running(name) {
1353
+ const reg = this.registrations.get(name);
1354
+ return reg ? reg.running : false;
1355
+ }
1356
+ subscribe(name, callback) {
1357
+ const reg = this.registrations.get(name);
1358
+ if (!reg) return sentinel;
1359
+ reg.subscription.listeners.add(callback);
1360
+ reg.subscription.watchers.acquire();
1361
+ return () => {
1362
+ reg.subscription.listeners.delete(callback);
1363
+ reg.subscription.watchers?.release();
1364
+ };
1365
+ }
1366
+ watch(name) {
1367
+ const reg = this.registrations.get(name);
1368
+ if (!reg) return sentinelWatcher;
1369
+ if (!reg.subscription.watcher) reg.subscription.watcher = {
1370
+ name,
1371
+ status: () => this.running(name),
1372
+ subscribe: (callback) => this.subscribe(name, callback)
1373
+ };
1374
+ return reg.subscription.watcher;
1375
+ }
1376
+ notifyStatusListeners(name) {
1377
+ const listeners = this.registrations.get(name).subscription.listeners;
1378
+ if (listeners) listeners.forEach((fn) => fn());
1379
+ }
1380
+ emit(bus, event) {
1381
+ queueMicrotask(() => {
1382
+ bus.emit(event);
1383
+ });
1384
+ }
1385
+ dispose() {
1386
+ for (const reg of this.registrations.values()) {
1387
+ reg.debouncer?.cancel();
1388
+ reg.subscription.watchers.forceCleanup;
1389
+ reg.subscription.listeners.clear();
1390
+ }
1391
+ this.registrations.clear();
1392
+ }
1393
+ };
1394
+
1395
+ //#endregion
1396
+ //#region src/store/store.ts
1397
+ /**
1398
+ * Reactive Data Store Implementation - Updated for Concurrency and Performance
1399
+ * The public API is preserved for test compatibility.
1400
+ */
1401
+ /**
1402
+ * Main ReactiveDataStore - a robust, type-safe state management solution.
1403
+ * It features optimistic updates, a Serializer-based update queue for safe serial
1404
+ * execution with backpressure, a Latch for async readiness gating, and decoupled
1405
+ * persistence.
1406
+ *
1407
+ * @template T The type of the data object managed by the store.
1408
+ */
1409
+ var ReactiveDataStore = class {
1410
+ /** Manages the core application state. */
1411
+ coreState;
1412
+ /** Orchestrates and executes middleware for state changes. */
1413
+ middlewareEngine;
1414
+ /** Manages saving state changes to a persistence layer. */
1415
+ persistenceHandler;
1416
+ /** Manages atomic state updates via transactions. */
1417
+ transactionManager;
1418
+ /** Collects and provides performance metrics for the store. */
1419
+ metricsCollector;
1420
+ selectorManager;
1421
+ actions;
1422
+ /**
1423
+ * Serializer enforces FIFO serial execution of all state updates.
1424
+ *
1425
+ * Replaces the bare `Promise<T | void>` chain. Benefits over the old pattern:
1426
+ * - Backpressure: rejects when the queue exceeds capacity rather than growing
1427
+ * unboundedly under write pressure.
1428
+ * - Clean shutdown: `close()` in dispose() immediately rejects any subsequent
1429
+ * `set()` calls with a clear error instead of silently enqueuing them.
1430
+ * - Observability: `updateSerializer.running()` and `updateSerializer.pending()`
1431
+ * replace the manually-managed `executionState.executing` flag.
1432
+ * - Error isolation: errors in one update do not corrupt the queue — the
1433
+ * Serializer's mutex is always released in its own `finally` block.
1434
+ */
1435
+ updateSerializer = new Serializer({
1436
+ yieldMode: "macrotask",
1437
+ capacity: 1e3
1438
+ });
1439
+ /**
1440
+ * Latch that opens once the store is fully initialised (persistence loaded).
1441
+ *
1442
+ * Replaces the poll-only `isReady(): boolean` pattern. Callers can now
1443
+ * `await store.ready()` with an optional timeout rather than polling or
1444
+ * subscribing to an internal event. `isReady()` is preserved as `latch.isOpen()`
1445
+ * for synchronous checks and backwards compatibility.
1446
+ */
1447
+ readyLatch = new Latch();
1448
+ disposeOnce = new Once();
1449
+ updateBus;
1450
+ eventBus;
1451
+ executionState;
1452
+ instanceID = v4();
1453
+ merge;
1454
+ diff;
1455
+ logger;
1456
+ /**
1457
+ * Creates a new ReactiveDataStore instance.
1458
+ */
1459
+ constructor(initialData, persistence, deleteMarker = DELETE_SYMBOL, options) {
1460
+ this.logger = createStoreLogger(options?.logger);
1461
+ this.eventBus = createEventBus(options?.broadcastChannel ? {
1462
+ batch: {
1463
+ size: 0,
1464
+ delay: 0
1465
+ },
1466
+ broadcast: { channel: options.broadcastChannel }
1467
+ } : void 0);
1468
+ this.updateBus = createEventBus(options?.broadcastChannel ? {
1469
+ batch: {
1470
+ size: 0,
1471
+ delay: 0
1472
+ },
1473
+ broadcast: { channel: options.broadcastChannel }
1474
+ } : void 0);
1475
+ this.executionState = {
1476
+ executing: false,
1477
+ changes: null,
1478
+ pendingChanges: [],
1479
+ middlewares: [],
1480
+ runningMiddleware: null,
1481
+ transactionActive: false
1482
+ };
1483
+ this.merge = createMerge({ deleteMarker });
1484
+ this.diff = createDiff({ deleteMarker });
1485
+ this.coreState = new StateManager(initialData, this.updateBus, this.diff);
1486
+ this.middlewareEngine = new MiddlewareEngine(this.eventBus, this.executionState, this.merge, this.logger);
1487
+ this.persistenceHandler = new PersistenceHandler(this.eventBus, this.coreState, this.instanceID, {
1488
+ maxRetries: options?.persistenceMaxRetries,
1489
+ retryDelay: options?.persistenceRetryDelay,
1490
+ logger: this.logger
1491
+ });
1492
+ this.transactionManager = new TransactionManager(this.eventBus, this.coreState, this.executionState);
1493
+ this.metricsCollector = new MetricsCollector(this.eventBus);
1494
+ this.actions = new ActionManager(this.eventBus, this.set.bind(this));
1495
+ this.persistenceHandler.initialize(persistence);
1496
+ this.setupPersistenceListener();
1497
+ this.setupReadyLatch();
1498
+ this.selectorManager = new SelectorManager(this.get.bind(this), this.eventBus);
1499
+ }
1500
+ /**
1501
+ * Returns true if the store has finished initialising (persistence loaded).
1502
+ * For async flows, prefer `await store.ready()` over polling this.
1503
+ */
1504
+ isReady() {
1505
+ return this.readyLatch.isOpen();
1506
+ }
1507
+ /**
1508
+ * Returns a promise that resolves once the store is fully initialised.
1509
+ * Safe to call multiple times — all callers share the same latch.
1510
+ *
1511
+ * @param timeout - Optional maximum wait time in milliseconds.
1512
+ * @throws {TimeoutError} If the store does not become ready within the timeout.
1513
+ *
1514
+ * @example
1515
+ * await store.ready();
1516
+ * const state = store.get();
1517
+ */
1518
+ async ready(timeout) {
1519
+ return this.readyLatch.wait(timeout);
1520
+ }
1521
+ state() {
1522
+ this.executionState.executing = this.updateSerializer.running();
1523
+ return this.executionState;
1524
+ }
1525
+ /**
1526
+ * Gets the current state. Optimized default to return reference (false) for performance.
1527
+ * @param clone If `true`, returns a deep clone (use sparingly).
1528
+ * @returns The current state object.
1529
+ */
1530
+ get(clone) {
1531
+ return this.coreState.get(clone ?? false);
1532
+ }
1533
+ subset(paths, separator = ".") {
1534
+ const result = {};
1535
+ const state = this.get();
1536
+ for (const path of paths) result[path] = path.split(separator).reduce((acc, key) => {
1537
+ return acc && acc[key] !== void 0 ? acc[key] : void 0;
1538
+ }, state);
1539
+ return result;
1540
+ }
1541
+ select(selector) {
1542
+ this.checkDisposed();
1543
+ return this.selectorManager.createReactiveSelector(selector);
1544
+ }
1545
+ register(action) {
1546
+ this.checkDisposed();
1547
+ return this.actions.register(action);
1548
+ }
1549
+ async dispatch(name, ...params) {
1550
+ this.checkDisposed();
1551
+ return this.actions.dispatch(name, ...params);
1552
+ }
1553
+ /**
1554
+ * Enqueues a state update for serial execution.
1555
+ *
1556
+ * Uses a `Serializer` internally, which guarantees:
1557
+ * - FIFO ordering of all updates.
1558
+ * - No concurrent execution — each update waits for the previous to complete.
1559
+ * - Backpressure: throws if the queue exceeds capacity.
1560
+ * - Clean rejection after `dispose()`.
1561
+ *
1562
+ * @returns A Promise that resolves with the new state T.
1563
+ */
1564
+ async set(update, options = {}) {
1565
+ this.checkDisposed();
1566
+ const result = await this.updateSerializer.do(() => this._performUpdate(update, options));
1567
+ if (result.error) throw result.error;
1568
+ return result.value;
1569
+ }
1570
+ /**
1571
+ * Internal logic for performing a single, sequential state update.
1572
+ * Always called inside the Serializer — never call directly.
1573
+ */
1574
+ async _performUpdate(update, options) {
1575
+ const startTime = Date.now();
1576
+ this.emit(this.eventBus, {
1577
+ name: "update:start",
1578
+ payload: {
1579
+ timestamp: startTime,
1580
+ actionId: options.actionId
1581
+ }
1582
+ });
1583
+ try {
1584
+ if (options.force) {
1585
+ const currentState = this.get(false);
1586
+ const finalState = typeof update === "function" ? update(currentState) : update;
1587
+ this.coreState.applyChanges(finalState, true);
1588
+ const endTime = Date.now();
1589
+ this.emit(this.eventBus, {
1590
+ name: "update:complete",
1591
+ payload: {
1592
+ deltas: [],
1593
+ duration: endTime - startTime,
1594
+ timestamp: Date.now(),
1595
+ actionId: options.actionId,
1596
+ newState: finalState
1597
+ }
1598
+ });
1599
+ return finalState;
1600
+ }
1601
+ let partialState;
1602
+ const currentState = this.get(false);
1603
+ if (typeof update === "function") {
1604
+ const res = update(currentState);
1605
+ partialState = res instanceof Promise ? await res : res;
1606
+ } else partialState = update;
1607
+ const blockingResult = await this.middlewareEngine.executeBlocking(currentState, partialState);
1608
+ if (blockingResult.blocked) throw blockingResult.error || /* @__PURE__ */ new Error("Update blocked by middleware");
1609
+ const merged = this.merge(currentState, partialState);
1610
+ const processed = await this.middlewareEngine.executeTransform(merged, partialState);
1611
+ const finalState = this.merge(merged, processed);
1612
+ const deltas = this.coreState.applyChanges(finalState, false, false, [partialState, processed]);
1613
+ const endTime = Date.now();
1614
+ const newState = this.get(false);
1615
+ this.emit(this.eventBus, {
1616
+ name: "update:complete",
1617
+ payload: {
1618
+ deltas,
1619
+ duration: endTime - startTime,
1620
+ timestamp: Date.now(),
1621
+ actionId: options.actionId,
1622
+ newState
1623
+ }
1624
+ });
1625
+ return newState;
1626
+ } catch (error) {
1627
+ this.emit(this.eventBus, {
1628
+ name: "update:complete",
1629
+ payload: {
1630
+ blocked: true,
1631
+ error,
1632
+ timestamp: Date.now(),
1633
+ actionId: options.actionId,
1634
+ newState: this.get(false)
1635
+ }
1636
+ });
1637
+ throw error;
1638
+ } finally {
1639
+ this.executionState.executing = false;
1640
+ this.executionState.changes = null;
1641
+ this.executionState.runningMiddleware = null;
1642
+ this.executionState.pendingChanges = [];
1643
+ }
1644
+ }
1645
+ /**
1646
+ * Opens the readyLatch once the persistence layer signals it is ready.
1647
+ * The latch can only open once — subsequent `persistence:ready` events are no-ops.
1648
+ */
1649
+ setupReadyLatch() {
1650
+ if (!this.persistenceHandler.isReady()) {
1651
+ const unsub = this.eventBus.subscribe("persistence:ready", () => {
1652
+ if (!this.readyLatch.isOpen()) this.readyLatch.open();
1653
+ unsub();
1654
+ });
1655
+ } else this.readyLatch.open();
1656
+ }
1657
+ setupPersistenceListener() {
1658
+ this.updateBus.subscribe("update", (path) => {
1659
+ if (path && this.persistenceHandler.isReady()) this.persistenceHandler.handleStateChange([path], this.get(false));
1660
+ });
1661
+ }
1662
+ watch(path, callback, options) {
1663
+ const all = Array.isArray(path) ? path : [path];
1664
+ const isRoot = path === "" || all.length === 0;
1665
+ return this.updateBus.subscribe("update", (changedPath) => {
1666
+ if (isRoot || all.includes(changedPath)) {
1667
+ callback(this.get(false));
1668
+ this.metricsCollector.listenerExecutions++;
1669
+ }
1670
+ }, options);
1671
+ }
1672
+ /**
1673
+ * Subscribes to execution‑status changes of a registered action.
1674
+ *
1675
+ * The provided callback is called **every time** the action transitions
1676
+ * between idle and running (i.e. on `action:start`, `action:complete`, or
1677
+ * `action:error` for the given name). The current status can also be read
1678
+ * synchronously with `isActionRunning(name)`.
1679
+ *
1680
+ * **Deferred listener teardown**
1681
+ * To avoid unnecessary churn when subscriptions are rapidly created and
1682
+ * destroyed, the underlying event listeners are not removed immediately
1683
+ * on unsubscribe. Instead, a *pending reset* is queued via `queueMicrotask`.
1684
+ * If a new subscription for the same action arrives before that microtask
1685
+ * executes, the reset is silently cancelled and the already‑established
1686
+ * listeners are reused. This keeps the subscription infrastructure stable
1687
+ * and prevents cascading notifications that could otherwise arise from
1688
+ * repeated subscribe‑unsubscribe‑subscribe cycles.
1689
+ *
1690
+ * @param name - The name of the action to watch.
1691
+ * @returns ActionWatcher
1692
+ */
1693
+ watchAction(name) {
1694
+ this.checkDisposed();
1695
+ return this.actions.watch(name);
1696
+ }
1697
+ /**
1698
+ * Creates a debounced version of `set()` using a `Debouncer`.
1699
+ *
1700
+ * Useful for high-frequency write sources (e.g. text input, scroll position)
1701
+ * where you want to collapse rapid updates into a single state write after a
1702
+ * quiet period.
1703
+ *
1704
+ * @param delay - Quiet period in milliseconds before the update is applied.
1705
+ * @param leading - If true, also applies the first update immediately.
1706
+ * @returns A debounced setter function with the same signature as `set()`.
1707
+ *
1708
+ * @example
1709
+ * const debouncedSet = store.debouncedSetter({ delay: 200 });
1710
+ * inputEl.addEventListener("input", (e) => {
1711
+ * debouncedSet((s) => ({ ...s, query: e.target.value }));
1712
+ * });
1713
+ */
1714
+ debouncedSetter(options) {
1715
+ const debouncer = new Debouncer({
1716
+ delay: options.delay,
1717
+ leading: options.leading
1718
+ });
1719
+ return (update, setOptions = {}) => {
1720
+ debouncer.fire(() => this.set(update, setOptions));
1721
+ };
1722
+ }
1723
+ id() {
1724
+ return this.instanceID;
1725
+ }
1726
+ async transaction(operation, options) {
1727
+ this.checkDisposed();
1728
+ const result = await this.transactionManager.execute(operation);
1729
+ if (options?.flush) await this.flush();
1730
+ return result;
1731
+ }
1732
+ use(props) {
1733
+ this.checkDisposed();
1734
+ const id = (props.block ? this.middlewareEngine.addBlockingMiddleware : this.middlewareEngine.addMiddleware).bind(this.middlewareEngine)(props.action, props.name);
1735
+ return () => this.middlewareEngine.removeMiddleware(id);
1736
+ }
1737
+ metrics() {
1738
+ return this.metricsCollector.getMetrics();
1739
+ }
1740
+ on(event, listener) {
1741
+ this.checkDisposed();
1742
+ return this.eventBus.subscribe(event, listener);
1743
+ }
1744
+ getPersistenceStatus() {
1745
+ return this.persistenceHandler.getQueueStatus();
1746
+ }
1747
+ async flush() {
1748
+ return this.persistenceHandler.flush();
1749
+ }
1750
+ /**
1751
+ * Discards all pending persistence tasks.
1752
+ * WARNING: Any queued writes not yet persisted will be lost.
1753
+ * Call flushPersistence() first if writes must not be lost.
1754
+ */
1755
+ discardPersistenceQueue() {
1756
+ this.persistenceHandler.discardQueue();
1757
+ }
1758
+ /**
1759
+ * Disposes the store and all owned resources.
1760
+ *
1761
+ * Closes the update Serializer — any `set()` calls after dispose() will
1762
+ * reject immediately with `SerializerExecutionDone` rather than silently
1763
+ * enqueuing work against a dead store.
1764
+ */
1765
+ dispose() {
1766
+ return this.disposeOnce.do(async () => {
1767
+ await this.flush();
1768
+ this.updateSerializer.close();
1769
+ this.eventBus?.clear({ permanent: true });
1770
+ this.updateBus?.clear({ permanent: true });
1771
+ this.actions.dispose();
1772
+ await this.persistenceHandler.dispose();
1773
+ this.metricsCollector.dispose();
1774
+ this.selectorManager.dispose();
1775
+ this.coreState = null;
1776
+ this.middlewareEngine = null;
1777
+ this.transactionManager = null;
1778
+ this.actions = null;
1779
+ });
1780
+ }
1781
+ checkDisposed() {
1782
+ if (this.disposed()) throw new Error("StoreExecutionDone: Cannot perform operations on a disposed store.");
1783
+ }
1784
+ disposed() {
1785
+ return this.disposeOnce.done();
1786
+ }
1787
+ emit(bus, event) {
1788
+ queueMicrotask(() => {
1789
+ bus.emit(event);
1790
+ });
1791
+ }
1792
+ };
1793
+
1794
+ //#endregion
1795
+ //#region src/store/registry.ts
1796
+ /**
1797
+ * A memory-safe, single-flight registry for ReactiveDataStores.
1798
+ * Uses WeakRefs and FinalizationRegistry to allow stores to be garbage collected
1799
+ * when no longer in use, automatically cleaning up internal references.
1800
+ */
1801
+ var StoreRegistry = class {
1802
+ stores = /* @__PURE__ */ new Map();
1803
+ storeToOnce = /* @__PURE__ */ new WeakMap();
1804
+ finalizer;
1805
+ onEvict;
1806
+ logger;
1807
+ constructor(options) {
1808
+ this.onEvict = options?.onEvict;
1809
+ this.logger = createStoreLogger(options?.logger);
1810
+ this.finalizer = new FinalizationRegistry(({ storeId, ref }) => {
1811
+ try {
1812
+ if (this.stores.get(storeId) === ref) {
1813
+ this.stores.delete(storeId);
1814
+ this.onEvict?.(storeId);
1815
+ }
1816
+ } catch (error) {
1817
+ this.logger.error("StoreRegistry Finalizer error", {
1818
+ storeId,
1819
+ error
1820
+ });
1821
+ }
1822
+ });
1823
+ }
1824
+ /**
1825
+ * Get an existing store for storeId, or create a new one via an always-cached Once primitive.
1826
+ * Ensures only one instantiation occurs even under concurrent calls.
1827
+ * * @param storeId - Unique identifier for the store.
1828
+ * @param initialState - The initial state to seed the store if it is created.
1829
+ * @param timeout - Optional timeout (ms) for store initialization.
1830
+ */
1831
+ async get(storeId, options = {}) {
1832
+ let once = this.stores.get(storeId)?.deref();
1833
+ if (!once) {
1834
+ once = new Once({ throws: false });
1835
+ const newRef = new WeakRef(once);
1836
+ this.stores.set(storeId, newRef);
1837
+ this.finalizer.register(once, {
1838
+ storeId,
1839
+ ref: newRef
1840
+ }, once);
1841
+ }
1842
+ const result = await once.do(async () => {
1843
+ const store = new ReactiveDataStore(options.state || {}, options.persistence, options.deleteMarker, options.options);
1844
+ await store.ready();
1845
+ return store;
1846
+ }, options.timeout);
1847
+ if (result.error) {
1848
+ if (this.stores.get(storeId)?.deref() === once) {
1849
+ this.stores.delete(storeId);
1850
+ this.finalizer.unregister(once);
1851
+ }
1852
+ throw result.error;
1853
+ }
1854
+ const store = result.value;
1855
+ this.storeToOnce.set(store, once);
1856
+ return store;
1857
+ }
1858
+ getSync(storeId, options = {}) {
1859
+ let once = this.stores.get(storeId)?.deref();
1860
+ if (!once) {
1861
+ once = new Once({ throws: false });
1862
+ const newRef = new WeakRef(once);
1863
+ this.stores.set(storeId, newRef);
1864
+ this.finalizer.register(once, {
1865
+ storeId,
1866
+ ref: newRef
1867
+ }, once);
1868
+ }
1869
+ const result = once.doSync(() => {
1870
+ return new ReactiveDataStore(options.state || {}, options.persistence, options.deleteMarker, options.options);
1871
+ });
1872
+ if (result.error) {
1873
+ if (this.stores.get(storeId)?.deref() === once) {
1874
+ this.stores.delete(storeId);
1875
+ this.finalizer.unregister(once);
1876
+ }
1877
+ throw result.error;
1878
+ }
1879
+ const store = result.value;
1880
+ this.storeToOnce.set(store, once);
1881
+ return store;
1882
+ }
1883
+ /**
1884
+ * Manually remove a store registry entry, unregistering its finalizer and disposing it.
1885
+ * @param storeId - Unique identifier for the store.
1886
+ * @returns True if a store was actively removed, false otherwise.
1887
+ */
1888
+ async release(storeId) {
1889
+ const ref = this.stores.get(storeId);
1890
+ if (ref) {
1891
+ const once = ref.deref();
1892
+ if (once) {
1893
+ this.finalizer.unregister(once);
1894
+ if (once.done()) {
1895
+ const store = once.get();
1896
+ if (store) {
1897
+ try {
1898
+ await store.dispose();
1899
+ } catch (e) {
1900
+ this.logger.error("StoreRegistry Error disposing store during release", {
1901
+ storeId,
1902
+ error: e
1903
+ });
1904
+ }
1905
+ this.storeToOnce.delete(store);
1906
+ }
1907
+ }
1908
+ }
1909
+ return this.stores.delete(storeId);
1910
+ }
1911
+ return false;
1912
+ }
1913
+ /**
1914
+ * Clears all stores from the registry, disposes them, and unregisters all finalizers.
1915
+ */
1916
+ async clear() {
1917
+ for (const ref of this.stores.values()) {
1918
+ const once = ref.deref();
1919
+ if (once) {
1920
+ this.finalizer.unregister(once);
1921
+ if (once.done()) {
1922
+ const store = once.get();
1923
+ if (store) {
1924
+ try {
1925
+ await store.dispose();
1926
+ } catch {}
1927
+ this.storeToOnce.delete(store);
1928
+ }
1929
+ }
1930
+ }
1931
+ }
1932
+ this.stores.clear();
1933
+ }
1934
+ /**
1935
+ * Get the store if it exists and has successfully finished initialization.
1936
+ * Does not trigger initialization if the store is missing or pending.
1937
+ */
1938
+ has(storeId) {
1939
+ return this.stores.has(storeId);
1940
+ }
1941
+ /**
1942
+ * Returns the current number of tracked stores.
1943
+ * Note: This includes entries that may have lost external references but haven't been GC'd yet.
1944
+ */
1945
+ get size() {
1946
+ return this.stores.size;
1947
+ }
1948
+ };
1949
+
1950
+ //#endregion
1951
+ //#region src/store/observer.ts
1952
+ /**
1953
+ * @class StoreObserver
1954
+ * @description Connects to a DataStore to capture state changes, events, and performance data.
1955
+ * @template T The type of the store's state object.
1956
+ */
1957
+ var StoreObserver = class {
1958
+ store;
1959
+ eventHistory = [];
1960
+ stateHistory = [];
1961
+ unsubscribers = [];
1962
+ isTimeTraveling = false;
1963
+ devTools = null;
1964
+ middlewareExecutions = [];
1965
+ activeTransactionCount = 0;
1966
+ activeBatches = /* @__PURE__ */ new Set();
1967
+ maxEvents;
1968
+ maxStateHistory;
1969
+ enableConsoleLogging;
1970
+ isSilent;
1971
+ logEvents;
1972
+ performanceThresholds;
1973
+ logger;
1974
+ /**
1975
+ * @param store The DataStore instance to observe.
1976
+ * @param options Configuration options for the observer.
1977
+ */
1978
+ constructor(store, options = {}) {
1979
+ this.store = store;
1980
+ this.maxEvents = options.maxEvents ?? 500;
1981
+ this.maxStateHistory = options.maxStateHistory ?? 20;
1982
+ this.enableConsoleLogging = options.enableConsoleLogging ?? false;
1983
+ this.isSilent = options.silent ?? false;
1984
+ this.logger = createStoreLogger(options.logger);
1985
+ this.logEvents = {
1986
+ updates: options.logEvents?.updates ?? true,
1987
+ middleware: options.logEvents?.middleware ?? true,
1988
+ transactions: options.logEvents?.transactions ?? true,
1989
+ actions: options.logEvents?.actions ?? true,
1990
+ selectors: options.logEvents?.selectors ?? true
1991
+ };
1992
+ this.performanceThresholds = {
1993
+ updateTime: options.performanceThresholds?.updateTime ?? 50,
1994
+ middlewareTime: options.performanceThresholds?.middlewareTime ?? 20
1995
+ };
1996
+ this.recordStateSnapshot([]);
1997
+ this.setupEventListeners();
1998
+ }
1999
+ /**
2000
+ * @private
2001
+ * Centralized logging method. Routes to the structured logger for level-based calls
2002
+ * (log, warn, error, debug) and to raw console for group/table calls which have no
2003
+ * logger equivalent. Respects the `isSilent` flag.
2004
+ * @param method The console method to call ('log', 'warn', 'error', 'group', 'groupEnd', 'table', 'debug').
2005
+ * @param args Arguments to pass to the console/log method.
2006
+ */
2007
+ _consoleLog(method, ...args) {
2008
+ if (this.isSilent) return;
2009
+ if (method === "group" || method === "groupEnd" || method === "table") {
2010
+ if (typeof console[method] === "function") console[method](...args);
2011
+ return;
2012
+ }
2013
+ const message = typeof args[0] === "string" ? args[0] : String(args[0] ?? "");
2014
+ const data = args.length > 1 ? { detail: args.slice(1) } : void 0;
2015
+ switch (method) {
2016
+ case "log":
2017
+ this.logger.log(message, data);
2018
+ break;
2019
+ case "warn":
2020
+ this.logger.warn(message, data);
2021
+ break;
2022
+ case "error":
2023
+ this.logger.error(message, data);
2024
+ break;
2025
+ case "debug":
2026
+ this.logger.debug(message, data);
2027
+ break;
2028
+ }
2029
+ }
2030
+ /**
2031
+ * @private
2032
+ * Sets up listeners for all relevant store events.
2033
+ */
2034
+ setupEventListeners() {
2035
+ for (const event of [
2036
+ "update:start",
2037
+ "update:complete",
2038
+ "middleware:start",
2039
+ "middleware:complete",
2040
+ "middleware:error",
2041
+ "middleware:blocked",
2042
+ "transaction:start",
2043
+ "transaction:complete",
2044
+ "transaction:error",
2045
+ "middleware:executed",
2046
+ "action:start",
2047
+ "action:complete",
2048
+ "action:error",
2049
+ "selector:accessed"
2050
+ ]) {
2051
+ const shouldLog = event.startsWith("update") && this.logEvents.updates || event.startsWith("middleware") && this.logEvents.middleware || event.startsWith("transaction") && this.logEvents.transactions || event.startsWith("action") && this.logEvents.actions || event.startsWith("selector") && this.logEvents.selectors;
2052
+ this.unsubscribers.push(this.store.on(event, (data) => {
2053
+ if (this.isTimeTraveling) return;
2054
+ if (event === "update:complete" && !data.blocked) this.recordStateSnapshot(data.deltas);
2055
+ if (event === "middleware:executed") this.middlewareExecutions.push(data);
2056
+ else if (event === "transaction:start") this.activeTransactionCount++;
2057
+ else if (event === "transaction:complete" || event === "transaction:error") this.activeTransactionCount = Math.max(0, this.activeTransactionCount - 1);
2058
+ if (data.batchId) {
2059
+ if (event.endsWith("start")) this.activeBatches.add(data.batchId);
2060
+ else if (event.endsWith("complete") || event.endsWith("error")) this.activeBatches.delete(data.batchId);
2061
+ }
2062
+ this.recordEvent(event, data);
2063
+ if (this.enableConsoleLogging && shouldLog) this._log(event, data);
2064
+ this._checkPerformance(event, data);
2065
+ }));
2066
+ }
2067
+ }
2068
+ /**
2069
+ * @private
2070
+ * Records a new state snapshot and manages the state history limit.
2071
+ * @param deltas The state changes that led to this snapshot.
2072
+ */
2073
+ recordStateSnapshot(deltas) {
2074
+ const snapshot = {
2075
+ state: this.store.get(true),
2076
+ timestamp: Date.now(),
2077
+ deltas
2078
+ };
2079
+ this.stateHistory.unshift(snapshot);
2080
+ if (this.stateHistory.length > this.maxStateHistory) this.stateHistory.pop();
2081
+ }
2082
+ /**
2083
+ * @private
2084
+ * Records a debug event and manages the event history limit.
2085
+ * @param type The type of event.
2086
+ * @param data The data payload of the event.
2087
+ */
2088
+ recordEvent(type, data) {
2089
+ const event = {
2090
+ type,
2091
+ timestamp: Date.now(),
2092
+ data: structuredClone(data)
2093
+ };
2094
+ this.eventHistory.unshift(event);
2095
+ if (this.eventHistory.length > this.maxEvents) this.eventHistory.pop();
2096
+ }
2097
+ /**
2098
+ * Returns a cloned copy of the recorded event history.
2099
+ * @returns Array of debug events.
2100
+ */
2101
+ getEventHistory() {
2102
+ return structuredClone(this.eventHistory);
2103
+ }
2104
+ /**
2105
+ * Returns a cloned copy of the recorded state history (snapshots).
2106
+ * @returns Array of state snapshots.
2107
+ */
2108
+ getStateHistory() {
2109
+ return structuredClone(this.stateHistory);
2110
+ }
2111
+ /**
2112
+ * Returns middleware execution history from the state.
2113
+ * @returns Array of middleware executions.
2114
+ */
2115
+ getMiddlewareExecutions() {
2116
+ return this.middlewareExecutions;
2117
+ }
2118
+ /**
2119
+ * Returns current transaction status.
2120
+ * @returns Object with transaction status information.
2121
+ */
2122
+ getTransactionStatus() {
2123
+ return {
2124
+ activeTransactions: this.activeTransactionCount,
2125
+ activeBatches: Array.from(this.activeBatches)
2126
+ };
2127
+ }
2128
+ /**
2129
+ * Creates a standard middleware that logs all updates.
2130
+ * @param options Options for the logging middleware.
2131
+ * @returns A middleware function.
2132
+ */
2133
+ createLoggingMiddleware(options = {}) {
2134
+ const { logLevel = "debug", logUpdates = true } = options;
2135
+ return (_, update) => {
2136
+ if (logUpdates) this.logger[logLevel]("State Update", { update });
2137
+ return update;
2138
+ };
2139
+ }
2140
+ /**
2141
+ * Creates a middleware that validates updates against a schema.
2142
+ * Uses the internal `_consoleLog` for warnings.
2143
+ * @param validator Function that validates updates.
2144
+ * @returns A blocking middleware function.
2145
+ */
2146
+ createValidationMiddleware(validator) {
2147
+ return (state, update) => {
2148
+ const result = validator(state, update);
2149
+ if (typeof result === "boolean") return result;
2150
+ if (!result.valid && result.reason) this._consoleLog("warn", "Validation failed:", result.reason);
2151
+ return result.valid;
2152
+ };
2153
+ }
2154
+ /**
2155
+ * Returns a simplified view of recent state changes.
2156
+ * @param limit Maximum number of state changes to compare. Defaults to 5.
2157
+ * @returns Array of state difference objects.
2158
+ */
2159
+ getRecentChanges(limit = 5) {
2160
+ const changes = [];
2161
+ const historyLimit = Math.min(limit, this.stateHistory.length);
2162
+ for (let i = 0; i < historyLimit; i++) {
2163
+ const snapshot = this.stateHistory[i];
2164
+ if (!snapshot.deltas || snapshot.deltas.length === 0) continue;
2165
+ const from = {};
2166
+ const to = {};
2167
+ const setNestedValue = (obj, parts, value) => {
2168
+ parts.reduce((o, key, index) => {
2169
+ if (index === parts.length - 1) o[key] = value;
2170
+ else o[key] = o[key] ?? {};
2171
+ return o[key];
2172
+ }, obj);
2173
+ };
2174
+ for (const delta of snapshot.deltas) {
2175
+ const pathParts = delta.path.split(".");
2176
+ setNestedValue(from, pathParts, delta.oldValue);
2177
+ setNestedValue(to, pathParts, delta.newValue);
2178
+ }
2179
+ changes.push({
2180
+ timestamp: snapshot.timestamp,
2181
+ changedPaths: snapshot.deltas.map((d) => d.path),
2182
+ from,
2183
+ to
2184
+ });
2185
+ }
2186
+ return changes;
2187
+ }
2188
+ /**
2189
+ * Clears both the event and state history, preserving only the initial state snapshot.
2190
+ */
2191
+ clearHistory() {
2192
+ this.eventHistory = [];
2193
+ if (this.stateHistory.length > 0) this.stateHistory = [this.stateHistory[0]];
2194
+ }
2195
+ /**
2196
+ * Filters and returns events related to a specific action ID.
2197
+ * @param actionId The ID of the action to filter by.
2198
+ * @returns Array of debug events associated with the action.
2199
+ */
2200
+ getHistoryForAction(actionId) {
2201
+ return this.eventHistory.filter((event) => event.data?.actionId === actionId);
2202
+ }
2203
+ /**
2204
+ * Attempts to replay a state update from history using the recorded update payload.
2205
+ * @param eventIndex The index of the replayable event (an "update:start" event) in the history array.
2206
+ */
2207
+ async replay(eventIndex) {
2208
+ const eventToReplay = this.eventHistory.filter((e) => e.type === "update:start")[eventIndex];
2209
+ if (eventToReplay?.data.update) {
2210
+ this._consoleLog("log", `Replaying event at index ${eventIndex}:`, eventToReplay);
2211
+ await this.store.set(eventToReplay.data.update, { force: true });
2212
+ } else this._consoleLog("warn", `No replayable event found at index ${eventIndex}.`);
2213
+ }
2214
+ /**
2215
+ * Creates a time-travel utility object with undo/redo capabilities.
2216
+ * @returns An object with methods for time-travel navigation.
2217
+ */
2218
+ createTimeTravel() {
2219
+ let currentIndex = 0;
2220
+ let future = [];
2221
+ const unsubscribe = this.store.on("update:complete", (data) => {
2222
+ if (!this.isTimeTraveling && !data.blocked) {
2223
+ future = [];
2224
+ currentIndex = 0;
2225
+ }
2226
+ });
2227
+ this.unsubscribers.push(unsubscribe);
2228
+ const getHistoryLength = () => this.stateHistory.length;
2229
+ const canUndo = () => currentIndex < getHistoryLength() - 1;
2230
+ const canRedo = () => future.length > 0;
2231
+ const undo = async () => {
2232
+ if (!canUndo()) return;
2233
+ future.unshift(this.stateHistory[currentIndex]);
2234
+ currentIndex++;
2235
+ const previousState = this.stateHistory[currentIndex].state;
2236
+ this.isTimeTraveling = true;
2237
+ await this.store.set({ ...previousState }, { force: true });
2238
+ this.isTimeTraveling = false;
2239
+ };
2240
+ const redo = async () => {
2241
+ if (!canRedo()) return;
2242
+ const nextSnapshot = future.shift();
2243
+ currentIndex--;
2244
+ this.isTimeTraveling = true;
2245
+ await this.store.set({ ...nextSnapshot.state }, { force: true });
2246
+ this.isTimeTraveling = false;
2247
+ };
2248
+ const clear = () => {
2249
+ future = [];
2250
+ currentIndex = 0;
2251
+ };
2252
+ return {
2253
+ canUndo,
2254
+ canRedo,
2255
+ undo,
2256
+ redo,
2257
+ length: getHistoryLength,
2258
+ clear
2259
+ };
2260
+ }
2261
+ /**
2262
+ * Saves the current observer session (events and state history) using a persistence mechanism.
2263
+ * @param persistence An object implementing the SimplePersistence interface.
2264
+ * @returns A promise that resolves to true if the session was saved successfully.
2265
+ */
2266
+ async saveSession(persistence) {
2267
+ const id = this.store.id();
2268
+ const sessionData = {
2269
+ eventHistory: this.eventHistory,
2270
+ stateHistory: this.stateHistory
2271
+ };
2272
+ return Promise.resolve(persistence.set(id, sessionData));
2273
+ }
2274
+ /**
2275
+ * Loads a previously saved observer session and restores the state to the latest saved snapshot.
2276
+ * @param persistence An object implementing the SimplePersistence interface.
2277
+ * @returns A promise that resolves to true if a session was loaded.
2278
+ */
2279
+ async loadSession(persistence) {
2280
+ const sessionData = await Promise.resolve(persistence.get());
2281
+ if (sessionData) {
2282
+ this.eventHistory = sessionData.eventHistory || [];
2283
+ this.stateHistory = sessionData.stateHistory || [];
2284
+ if (this.stateHistory.length > 0) await this.store.set(this.stateHistory[0].state, { force: true });
2285
+ return true;
2286
+ }
2287
+ return false;
2288
+ }
2289
+ /**
2290
+ * Exports the current session data as a JSON file, initiating a browser download.
2291
+ */
2292
+ exportSession() {
2293
+ const sessionData = {
2294
+ eventHistory: this.eventHistory,
2295
+ stateHistory: this.stateHistory
2296
+ };
2297
+ const blob = new Blob([JSON.stringify(sessionData, null, 2)], { type: "application/json" });
2298
+ const url = URL.createObjectURL(blob);
2299
+ const a = document.createElement("a");
2300
+ a.href = url;
2301
+ a.download = `store-observer-session-${(/* @__PURE__ */ new Date()).toISOString()}.json`;
2302
+ a.click();
2303
+ URL.revokeObjectURL(url);
2304
+ }
2305
+ /**
2306
+ * Imports a session from a JSON file, restoring event history and state.
2307
+ * @param file The file object (e.g., from an <input type="file"> event) to import.
2308
+ * @returns A promise that resolves when the import is complete.
2309
+ */
2310
+ importSession(file) {
2311
+ return new Promise((resolve, reject) => {
2312
+ const reader = new FileReader();
2313
+ reader.onload = async (event) => {
2314
+ try {
2315
+ const sessionData = JSON.parse(event.target?.result);
2316
+ this.eventHistory = sessionData.eventHistory || [];
2317
+ this.stateHistory = sessionData.stateHistory || [];
2318
+ if (this.stateHistory.length > 0) await this.store.set(this.stateHistory[0].state, { force: true });
2319
+ resolve();
2320
+ } catch (error) {
2321
+ reject(error);
2322
+ }
2323
+ };
2324
+ reader.onerror = (error) => reject(error);
2325
+ reader.readAsText(file);
2326
+ });
2327
+ }
2328
+ /**
2329
+ * Cleans up the observer by unsubscribing all event listeners and clearing history.
2330
+ */
2331
+ disconnect() {
2332
+ this.unsubscribers.forEach((unsubscribe) => unsubscribe());
2333
+ this.unsubscribers = [];
2334
+ this.devTools?.disconnect();
2335
+ this.clearHistory();
2336
+ }
2337
+ /**
2338
+ * @private
2339
+ * Logs an event to the console with appropriate formatting.
2340
+ * Uses `this._consoleLog` for all output.
2341
+ * @param type Event type.
2342
+ * @param data Event data.
2343
+ */
2344
+ _log(type, data) {
2345
+ const timestamp = new Date(data.timestamp || Date.now()).toISOString().split("T")[1].replace("Z", "");
2346
+ if (type === "update:start") this._consoleLog("group", `%c⚡ Store Update Started [${timestamp}]`, "color: #4a6da7");
2347
+ else if (type === "update:complete") {
2348
+ if (data.blocked) this._consoleLog("warn", `%c✋ Update Blocked [${timestamp}]`, "color: #bf8c0a", data.error);
2349
+ else {
2350
+ const deltas = data.deltas || [];
2351
+ if (deltas.length > 0) {
2352
+ this._consoleLog("log", `%c✅ Update Complete [${timestamp}] - ${deltas.length} paths changed in ${data.duration?.toFixed(2)}ms`, "color: #2a9d8f");
2353
+ this._consoleLog("table", deltas.map((d) => ({
2354
+ path: d.path,
2355
+ oldValue: d.oldValue,
2356
+ newValue: d.newValue
2357
+ })));
2358
+ }
2359
+ }
2360
+ this._consoleLog("groupEnd");
2361
+ } else if (type === "middleware:start") this._consoleLog("debug", `%c◀ Middleware \"${data.name}\" started [${timestamp}] (${data.type})`, "color: #8c8c8c");
2362
+ else if (type === "middleware:complete") this._consoleLog("debug", `%c▶ Middleware \"${data.name}\" completed [${timestamp}] in ${data.duration?.toFixed(2)}ms`, "color: #7c9c7c");
2363
+ else if (type === "middleware:error") this._consoleLog("error", `%c❌ Middleware \"${data.name}\" error [${timestamp}]:`, "color: #e63946", data.error);
2364
+ else if (type === "middleware:blocked") this._consoleLog("warn", `%c🛑 Middleware \"${data.name}\" blocked update [${timestamp}]`, "color: #e76f51");
2365
+ else if (type === "transaction:start") this._consoleLog("group", `%c📦 Transaction Started [${timestamp}]`, "color: #6d597a");
2366
+ else if (type === "transaction:complete") {
2367
+ this._consoleLog("log", `%c📦 Transaction Complete [${timestamp}]`, "color: #355070");
2368
+ this._consoleLog("groupEnd");
2369
+ } else if (type === "transaction:error") {
2370
+ this._consoleLog("error", `%c📦 Transaction Error [${timestamp}]:`, "color: #e56b6f", data.error);
2371
+ this._consoleLog("groupEnd");
2372
+ } else if (type === "action:start") this._consoleLog("group", `%c🚀 Action \"${data.name}\" Started [${timestamp}]`, "color: #9b59b6", { params: data.params });
2373
+ else if (type === "action:complete") {
2374
+ this._consoleLog("log", `%c✔️ Action \"${data.name}\" Complete [${timestamp}] in ${data.duration?.toFixed(2)}ms`, "color: #2ecc71");
2375
+ this._consoleLog("groupEnd");
2376
+ } else if (type === "action:error") {
2377
+ this._consoleLog("error", `%c🔥 Action \"${data.name}\" Error [${timestamp}]:`, "color: #e74c3c", data.error);
2378
+ this._consoleLog("groupEnd");
2379
+ } else if (type === "selector:accessed") this._consoleLog("debug", `%c👀 Selector Accessed [${timestamp}] in ${data.duration?.toFixed(2)}ms`, "color: #f1c40f", {
2380
+ accessedPaths: data.accessedPaths,
2381
+ selectorId: data.selectorId
2382
+ });
2383
+ }
2384
+ /**
2385
+ * @private
2386
+ * Checks for performance issues in the events.
2387
+ * Uses `this._consoleLog` for all output.
2388
+ * @param type Event type.
2389
+ * @param data Event data.
2390
+ */
2391
+ _checkPerformance(type, data) {
2392
+ if (!this.enableConsoleLogging) return;
2393
+ if (type === "update:complete" && !data.blocked && data.duration > this.performanceThresholds.updateTime) this._consoleLog("warn", `%c⚠️ Slow update detected [${data.duration.toFixed(2)}ms]`, "color: #ff9f1c", {
2394
+ deltas: data.deltas,
2395
+ threshold: this.performanceThresholds.updateTime
2396
+ });
2397
+ if (type === "middleware:complete" && data.duration > this.performanceThresholds.middlewareTime) this._consoleLog("warn", `%c⚠️ Slow middleware \"${data.name}\" [${data.duration.toFixed(2)}ms]`, "color: #ff9f1c", { threshold: this.performanceThresholds.middlewareTime });
2398
+ }
2399
+ };
2400
+
2401
+ //#endregion
2402
+ export { ActionCancelledError, ActionManager, DELETE_SYMBOL, ReactiveDataStore, SelectorManager, StoreObserver, StoreRegistry, UnknownActionError, buildPaths, createDerivePaths, createDiff, createMerge, createStoreLogger, derivePaths, diff, merge, shallowClone };