@asaidimu/utils-store 10.2.16 → 10.2.18
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.d.cts +212 -58
- package/index.d.ts +6 -7
- package/index.js +2429 -1
- package/index.mjs +2402 -1
- package/package.json +5 -5
package/index.mjs
CHANGED
|
@@ -1 +1,2402 @@
|
|
|
1
|
-
import{createEventBus as e}from"@core/events";import{Debouncer as t,Latch as n,Once as r,Serializer as i,SharedResource as a}from"@core/sync";import{v4 as o}from"uuid";import{Logger as s}from"@core/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 };
|