@nerdalytics/beacon
Advanced tools
@@ -1,2 +0,2 @@ | ||
| type Unsubscribe = () => void; | ||
| export type Unsubscribe = () => void; | ||
| export type ReadOnlyState<T> = () => T; | ||
@@ -3,0 +3,0 @@ export interface WriteableState<T> { |
+90
-63
@@ -1,2 +0,2 @@ | ||
| const STATE_ID = Symbol(); | ||
| const STATE_ID = Symbol('STATE_ID'); | ||
| export const state = (initialValue, equalityFn = Object.is) => StateImpl.createState(initialValue, equalityFn); | ||
@@ -172,3 +172,5 @@ export const effect = (fn) => StateImpl.createEffect(fn); | ||
| if (StateImpl.deferredEffectCreations.length > 0) { | ||
| const effectsToRun = [...StateImpl.deferredEffectCreations]; | ||
| const effectsToRun = [ | ||
| ...StateImpl.deferredEffectCreations, | ||
| ]; | ||
| StateImpl.deferredEffectCreations.length = 0; | ||
@@ -186,58 +188,76 @@ for (const effect of effectsToRun) { | ||
| static createDerive = (computeFn) => { | ||
| const valueState = StateImpl.createState(undefined); | ||
| let initialized = false; | ||
| let cachedValue; | ||
| StateImpl.createEffect(() => { | ||
| const newValue = computeFn(); | ||
| if (!(initialized && Object.is(cachedValue, newValue))) { | ||
| cachedValue = newValue; | ||
| valueState.set(newValue); | ||
| const container = { | ||
| cachedValue: undefined, | ||
| computeFn, | ||
| initialized: false, | ||
| valueState: StateImpl.createState(undefined), | ||
| }; | ||
| StateImpl.createEffect(function deriveEffect() { | ||
| const newValue = container.computeFn(); | ||
| if (!(container.initialized && Object.is(container.cachedValue, newValue))) { | ||
| container.cachedValue = newValue; | ||
| container.valueState.set(newValue); | ||
| } | ||
| initialized = true; | ||
| container.initialized = true; | ||
| }); | ||
| return () => { | ||
| if (!initialized) { | ||
| cachedValue = computeFn(); | ||
| initialized = true; | ||
| valueState.set(cachedValue); | ||
| return function deriveGetter() { | ||
| if (!container.initialized) { | ||
| container.cachedValue = container.computeFn(); | ||
| container.initialized = true; | ||
| container.valueState.set(container.cachedValue); | ||
| } | ||
| return valueState(); | ||
| return container.valueState(); | ||
| }; | ||
| }; | ||
| static createSelect = (source, selectorFn, equalityFn = Object.is) => { | ||
| let lastSourceValue; | ||
| let lastSelectedValue; | ||
| let initialized = false; | ||
| const valueState = StateImpl.createState(undefined); | ||
| StateImpl.createEffect(() => { | ||
| const sourceValue = source(); | ||
| if (initialized && Object.is(lastSourceValue, sourceValue)) { | ||
| const container = { | ||
| equalityFn, | ||
| initialized: false, | ||
| lastSelectedValue: undefined, | ||
| lastSourceValue: undefined, | ||
| selectorFn, | ||
| source, | ||
| valueState: StateImpl.createState(undefined), | ||
| }; | ||
| StateImpl.createEffect(function selectEffect() { | ||
| const sourceValue = container.source(); | ||
| if (container.initialized && Object.is(container.lastSourceValue, sourceValue)) { | ||
| return; | ||
| } | ||
| lastSourceValue = sourceValue; | ||
| const newSelectedValue = selectorFn(sourceValue); | ||
| if (initialized && lastSelectedValue !== undefined && equalityFn(lastSelectedValue, newSelectedValue)) { | ||
| container.lastSourceValue = sourceValue; | ||
| const newSelectedValue = container.selectorFn(sourceValue); | ||
| if (container.initialized && | ||
| container.lastSelectedValue !== undefined && | ||
| container.equalityFn(container.lastSelectedValue, newSelectedValue)) { | ||
| return; | ||
| } | ||
| lastSelectedValue = newSelectedValue; | ||
| valueState.set(newSelectedValue); | ||
| initialized = true; | ||
| container.lastSelectedValue = newSelectedValue; | ||
| container.valueState.set(newSelectedValue); | ||
| container.initialized = true; | ||
| }); | ||
| return () => { | ||
| if (!initialized) { | ||
| lastSourceValue = source(); | ||
| lastSelectedValue = selectorFn(lastSourceValue); | ||
| valueState.set(lastSelectedValue); | ||
| initialized = true; | ||
| return function selectGetter() { | ||
| if (!container.initialized) { | ||
| container.lastSourceValue = container.source(); | ||
| container.lastSelectedValue = container.selectorFn(container.lastSourceValue); | ||
| container.valueState.set(container.lastSelectedValue); | ||
| container.initialized = true; | ||
| } | ||
| return valueState(); | ||
| return container.valueState(); | ||
| }; | ||
| }; | ||
| static createLens = (source, accessor) => { | ||
| const container = { | ||
| accessor, | ||
| isUpdating: false, | ||
| lensState: null, | ||
| originalSet: null, | ||
| path: [], | ||
| source, | ||
| }; | ||
| const extractPath = () => { | ||
| const path = []; | ||
| const pathCollector = []; | ||
| const proxy = new Proxy({}, { | ||
| get: (_, prop) => { | ||
| if (typeof prop === 'string' || typeof prop === 'number') { | ||
| path.push(prop); | ||
| pathCollector.push(prop); | ||
| } | ||
@@ -248,41 +268,40 @@ return proxy; | ||
| try { | ||
| accessor(proxy); | ||
| container.accessor(proxy); | ||
| } | ||
| catch { | ||
| } | ||
| return path; | ||
| return pathCollector; | ||
| }; | ||
| const path = extractPath(); | ||
| const lensState = StateImpl.createState(accessor(source())); | ||
| let isUpdating = false; | ||
| StateImpl.createEffect(() => { | ||
| if (isUpdating) { | ||
| container.path = extractPath(); | ||
| container.lensState = StateImpl.createState(container.accessor(container.source())); | ||
| container.originalSet = container.lensState.set; | ||
| StateImpl.createEffect(function lensEffect() { | ||
| if (container.isUpdating) { | ||
| return; | ||
| } | ||
| isUpdating = true; | ||
| container.isUpdating = true; | ||
| try { | ||
| lensState.set(accessor(source())); | ||
| container.lensState.set(container.accessor(container.source())); | ||
| } | ||
| finally { | ||
| isUpdating = false; | ||
| container.isUpdating = false; | ||
| } | ||
| }); | ||
| const originalSet = lensState.set; | ||
| lensState.set = (value) => { | ||
| if (isUpdating) { | ||
| container.lensState.set = function lensSet(value) { | ||
| if (container.isUpdating) { | ||
| return; | ||
| } | ||
| isUpdating = true; | ||
| container.isUpdating = true; | ||
| try { | ||
| originalSet(value); | ||
| source.update((current) => setValueAtPath(current, path, value)); | ||
| container.originalSet(value); | ||
| container.source.update((current) => setValueAtPath(current, container.path, value)); | ||
| } | ||
| finally { | ||
| isUpdating = false; | ||
| container.isUpdating = false; | ||
| } | ||
| }; | ||
| lensState.update = (fn) => { | ||
| lensState.set(fn(lensState())); | ||
| container.lensState.update = function lensUpdate(fn) { | ||
| container.lensState.set(fn(container.lensState())); | ||
| }; | ||
| return lensState; | ||
| return container.lensState; | ||
| }; | ||
@@ -320,3 +339,5 @@ static notifySubscribers = () => { | ||
| const updateArrayItem = (arr, index, value) => { | ||
| const copy = [...arr]; | ||
| const copy = [ | ||
| ...arr, | ||
| ]; | ||
| copy[index] = value; | ||
@@ -326,3 +347,5 @@ return copy; | ||
| const updateShallowProperty = (obj, key, value) => { | ||
| const result = { ...obj }; | ||
| const result = { | ||
| ...obj, | ||
| }; | ||
| result[key] = value; | ||
@@ -340,3 +363,5 @@ return result; | ||
| } | ||
| const copy = [...array]; | ||
| const copy = [ | ||
| ...array, | ||
| ]; | ||
| const nextPathSegments = pathSegments.slice(1); | ||
@@ -365,3 +390,5 @@ const nextKey = nextPathSegments[0]; | ||
| } | ||
| const result = { ...obj }; | ||
| const result = { | ||
| ...obj, | ||
| }; | ||
| result[currentKey] = setValueAtPath(currentValue, nextPathSegments, value); | ||
@@ -368,0 +395,0 @@ return result; |
@@ -1,1 +0,1 @@ | ||
| let s=Symbol(),i=(e,t=Object.is)=>u.createState(e,t);var e=e=>u.createEffect(e),t=e=>u.executeBatch(e),r=e=>u.createDerive(e),c=(e,t,r=Object.is)=>u.createSelect(e,t,r);let a=e=>()=>e();var n=(e,t=Object.is)=>{let r=i(e,t);return[()=>a(r)(),{set:e=>r.set(e),update:e=>r.update(e)}]},b=(e,t)=>u.createLens(e,t);class u{static currentSubscriber=null;static pendingSubscribers=new Set;static isNotifying=!1;static batchDepth=0;static deferredEffectCreations=[];static activeSubscribers=new Set;static stateTracking=new WeakMap;static subscriberDependencies=new WeakMap;static parentSubscriber=new WeakMap;static childSubscribers=new WeakMap;value;subscribers=new Set;stateId=Symbol();equalityFn;constructor(e,t=Object.is){this.value=e,this.equalityFn=t}static createState=(e,t=Object.is)=>{let r=new u(e,t);e=()=>r.get();return e.set=e=>r.set(e),e.update=e=>r.update(e),e[s]=r.stateId,e};get=()=>{var r=u.currentSubscriber;if(r){this.subscribers.add(r);let e=u.subscriberDependencies.get(r),t=(e||(e=new Set,u.subscriberDependencies.set(r,e)),e.add(this.subscribers),u.stateTracking.get(r));t||(t=new Set,u.stateTracking.set(r,t)),t.add(this.stateId)}return this.value};set=e=>{if(!this.equalityFn(this.value,e)){var t=u.currentSubscriber;if(t)if(u.stateTracking.get(t)?.has(this.stateId)&&!u.parentSubscriber.get(t))throw new Error("Infinite loop detected: effect() cannot update a state() it depends on!");if(this.value=e,0!==this.subscribers.size){for(var r of this.subscribers)u.pendingSubscribers.add(r);0!==u.batchDepth||u.isNotifying||u.notifySubscribers()}}};update=e=>{this.set(e(this.value))};static createEffect=e=>{let r=()=>{if(!u.activeSubscribers.has(r)){u.activeSubscribers.add(r);var t=u.currentSubscriber;try{if(u.cleanupEffect(r),u.currentSubscriber=r,u.stateTracking.set(r,new Set),t){u.parentSubscriber.set(r,t);let e=u.childSubscribers.get(t);e||(e=new Set,u.childSubscribers.set(t,e)),e.add(r)}e()}finally{u.currentSubscriber=t,u.activeSubscribers.delete(r)}}};if(0===u.batchDepth)r();else{if(u.currentSubscriber){var t=u.currentSubscriber;u.parentSubscriber.set(r,t);let e=u.childSubscribers.get(t);e||(e=new Set,u.childSubscribers.set(t,e)),e.add(r)}u.deferredEffectCreations.push(r)}return()=>{u.cleanupEffect(r),u.pendingSubscribers.delete(r),u.activeSubscribers.delete(r),u.stateTracking.delete(r);var e=u.parentSubscriber.get(r),e=(e&&(e=u.childSubscribers.get(e))&&e.delete(r),u.parentSubscriber.delete(r),u.childSubscribers.get(r));if(e){for(var t of e)u.cleanupEffect(t);e.clear(),u.childSubscribers.delete(r)}}};static executeBatch=e=>{u.batchDepth++;try{return e()}catch(e){throw 1===u.batchDepth&&(u.pendingSubscribers.clear(),u.deferredEffectCreations.length=0),e}finally{if(u.batchDepth--,0===u.batchDepth){if(0<u.deferredEffectCreations.length){var t,e=[...u.deferredEffectCreations];u.deferredEffectCreations.length=0;for(t of e)t()}0<u.pendingSubscribers.size&&!u.isNotifying&&u.notifySubscribers()}}};static createDerive=t=>{let r=u.createState(void 0),s=!1,i;return u.createEffect(()=>{var e=t();s&&Object.is(i,e)||(i=e,r.set(e)),s=!0}),()=>(s||(i=t(),s=!0,r.set(i)),r())};static createSelect=(t,r,s=Object.is)=>{let i,c,a=!1,n=u.createState(void 0);return u.createEffect(()=>{var e=t();a&&Object.is(i,e)||(i=e,e=r(e),a&&void 0!==c&&s(c,e))||(c=e,n.set(e),a=!0)}),()=>(a||(i=t(),c=r(i),n.set(c),a=!0),n())};static createLens=(e,t)=>{let r=(()=>{let r=[],s=new Proxy({},{get:(e,t)=>("string"!=typeof t&&"number"!=typeof t||r.push(t),s)});try{t(s)}catch{}return r})(),s=u.createState(t(e())),i=!1,c=(u.createEffect(()=>{if(!i){i=!0;try{s.set(t(e()))}finally{i=!1}}}),s.set);return s.set=t=>{if(!i){i=!0;try{c(t),e.update(e=>p(e,r,t))}finally{i=!1}}},s.update=e=>{s.set(e(s()))},s};static notifySubscribers=()=>{if(!u.isNotifying){u.isNotifying=!0;try{for(;0<u.pendingSubscribers.size;){var e,t=Array.from(u.pendingSubscribers);u.pendingSubscribers.clear();for(e of t)e()}}finally{u.isNotifying=!1}}};static cleanupEffect=e=>{u.pendingSubscribers.delete(e);var t=u.subscriberDependencies.get(e);if(t){for(var r of t)r.delete(e);t.clear(),u.subscriberDependencies.delete(e)}}}let f=(e,t,r)=>{e=[...e];return e[t]=r,e},l=(e,t,r)=>{e={...e};return e[t]=r,e},d=e=>"number"==typeof e||!Number.isNaN(Number(e))?[]:{},S=(e,t,r)=>{var s=Number(t[0]);if(1===t.length)return f(e,s,r);var i=[...e],t=t.slice(1),c=t[0];let a=e[s];return null==a&&(a=void 0!==c?d(c):{}),i[s]=p(a,t,r),i},h=(e,t,r)=>{var s=t[0];if(void 0===s)return e;if(1===t.length)return l(e,s,r);var t=t.slice(1),i=t[0];let c=e[s];null==c&&(c=void 0!==i?d(i):{});i={...e};return i[s]=p(c,t,r),i},p=(e,t,r)=>0===t.length?r:null==e?p({},t,r):void 0===t[0]?e:(Array.isArray(e)?S:h)(e,t,r);export{i as state,e as effect,t as batch,r as derive,c as select,a as readonlyState,n as protectedState,b as lens}; | ||
| let i=Symbol("STATE_ID"),a=(e,t=Object.is)=>l.createState(e,t);var e=e=>l.createEffect(e),t=e=>l.executeBatch(e),r=e=>l.createDerive(e),s=(e,t,r=Object.is)=>l.createSelect(e,t,r);let c=e=>()=>e();var n=(e,t=Object.is)=>{let r=a(e,t);return[()=>c(r)(),{set:e=>r.set(e),update:e=>r.update(e)}]},u=(e,t)=>l.createLens(e,t);class l{static currentSubscriber=null;static pendingSubscribers=new Set;static isNotifying=!1;static batchDepth=0;static deferredEffectCreations=[];static activeSubscribers=new Set;static stateTracking=new WeakMap;static subscriberDependencies=new WeakMap;static parentSubscriber=new WeakMap;static childSubscribers=new WeakMap;value;subscribers=new Set;stateId=Symbol();equalityFn;constructor(e,t=Object.is){this.value=e,this.equalityFn=t}static createState=(e,t=Object.is)=>{let r=new l(e,t);e=()=>r.get();return e.set=e=>r.set(e),e.update=e=>r.update(e),e[i]=r.stateId,e};get=()=>{var r=l.currentSubscriber;if(r){this.subscribers.add(r);let e=l.subscriberDependencies.get(r),t=(e||(e=new Set,l.subscriberDependencies.set(r,e)),e.add(this.subscribers),l.stateTracking.get(r));t||(t=new Set,l.stateTracking.set(r,t)),t.add(this.stateId)}return this.value};set=e=>{if(!this.equalityFn(this.value,e)){var t=l.currentSubscriber;if(t)if(l.stateTracking.get(t)?.has(this.stateId)&&!l.parentSubscriber.get(t))throw new Error("Infinite loop detected: effect() cannot update a state() it depends on!");if(this.value=e,0!==this.subscribers.size){for(var r of this.subscribers)l.pendingSubscribers.add(r);0!==l.batchDepth||l.isNotifying||l.notifySubscribers()}}};update=e=>{this.set(e(this.value))};static createEffect=e=>{let r=()=>{if(!l.activeSubscribers.has(r)){l.activeSubscribers.add(r);var t=l.currentSubscriber;try{if(l.cleanupEffect(r),l.currentSubscriber=r,l.stateTracking.set(r,new Set),t){l.parentSubscriber.set(r,t);let e=l.childSubscribers.get(t);e||(e=new Set,l.childSubscribers.set(t,e)),e.add(r)}e()}finally{l.currentSubscriber=t,l.activeSubscribers.delete(r)}}};if(0===l.batchDepth)r();else{if(l.currentSubscriber){var t=l.currentSubscriber;l.parentSubscriber.set(r,t);let e=l.childSubscribers.get(t);e||(e=new Set,l.childSubscribers.set(t,e)),e.add(r)}l.deferredEffectCreations.push(r)}return()=>{l.cleanupEffect(r),l.pendingSubscribers.delete(r),l.activeSubscribers.delete(r),l.stateTracking.delete(r);var e=l.parentSubscriber.get(r),e=(e&&(e=l.childSubscribers.get(e))&&e.delete(r),l.parentSubscriber.delete(r),l.childSubscribers.get(r));if(e){for(var t of e)l.cleanupEffect(t);e.clear(),l.childSubscribers.delete(r)}}};static executeBatch=e=>{l.batchDepth++;try{return e()}catch(e){throw 1===l.batchDepth&&(l.pendingSubscribers.clear(),l.deferredEffectCreations.length=0),e}finally{if(l.batchDepth--,0===l.batchDepth){if(0<l.deferredEffectCreations.length){var t,e=[...l.deferredEffectCreations];l.deferredEffectCreations.length=0;for(t of e)t()}0<l.pendingSubscribers.size&&!l.isNotifying&&l.notifySubscribers()}}};static createDerive=e=>{let t={cachedValue:void 0,computeFn:e,initialized:!1,valueState:l.createState(void 0)};return l.createEffect(function(){var e=t.computeFn();t.initialized&&Object.is(t.cachedValue,e)||(t.cachedValue=e,t.valueState.set(e)),t.initialized=!0}),function(){return t.initialized||(t.cachedValue=t.computeFn(),t.initialized=!0,t.valueState.set(t.cachedValue)),t.valueState()}};static createSelect=(e,t,r=Object.is)=>{let i={equalityFn:r,initialized:!1,lastSelectedValue:void 0,lastSourceValue:void 0,selectorFn:t,source:e,valueState:l.createState(void 0)};return l.createEffect(function(){var e=i.source();i.initialized&&Object.is(i.lastSourceValue,e)||(i.lastSourceValue=e,e=i.selectorFn(e),i.initialized&&void 0!==i.lastSelectedValue&&i.equalityFn(i.lastSelectedValue,e))||(i.lastSelectedValue=e,i.valueState.set(e),i.initialized=!0)}),function(){return i.initialized||(i.lastSourceValue=i.source(),i.lastSelectedValue=i.selectorFn(i.lastSourceValue),i.valueState.set(i.lastSelectedValue),i.initialized=!0),i.valueState()}};static createLens=(e,t)=>{let a={accessor:t,isUpdating:!1,lensState:null,originalSet:null,path:[],source:e};return a.path=(()=>{let r=[],i=new Proxy({},{get:(e,t)=>("string"!=typeof t&&"number"!=typeof t||r.push(t),i)});try{a.accessor(i)}catch{}return r})(),a.lensState=l.createState(a.accessor(a.source())),a.originalSet=a.lensState.set,l.createEffect(function(){if(!a.isUpdating){a.isUpdating=!0;try{a.lensState.set(a.accessor(a.source()))}finally{a.isUpdating=!1}}}),a.lensState.set=function(t){if(!a.isUpdating){a.isUpdating=!0;try{a.originalSet(t),a.source.update(e=>p(e,a.path,t))}finally{a.isUpdating=!1}}},a.lensState.update=function(e){a.lensState.set(e(a.lensState()))},a.lensState};static notifySubscribers=()=>{if(!l.isNotifying){l.isNotifying=!0;try{for(;0<l.pendingSubscribers.size;){var e,t=Array.from(l.pendingSubscribers);l.pendingSubscribers.clear();for(e of t)e()}}finally{l.isNotifying=!1}}};static cleanupEffect=e=>{l.pendingSubscribers.delete(e);var t=l.subscriberDependencies.get(e);if(t){for(var r of t)r.delete(e);t.clear(),l.subscriberDependencies.delete(e)}}}let b=(e,t,r)=>{e=[...e];return e[t]=r,e},d=(e,t,r)=>{e={...e};return e[t]=r,e},f=e=>"number"==typeof e||!Number.isNaN(Number(e))?[]:{},S=(e,t,r)=>{var i=Number(t[0]);if(1===t.length)return b(e,i,r);var a=[...e],t=t.slice(1),s=t[0];let c=e[i];return null==c&&(c=void 0!==s?f(s):{}),a[i]=p(c,t,r),a},o=(e,t,r)=>{var i=t[0];if(void 0===i)return e;if(1===t.length)return d(e,i,r);var t=t.slice(1),a=t[0];let s=e[i];null==s&&(s=void 0!==a?f(a):{});a={...e};return a[i]=p(s,t,r),a},p=(e,t,r)=>0===t.length?r:null==e?p({},t,r):void 0===t[0]?e:(Array.isArray(e)?S:o)(e,t,r);export{a as state,e as effect,t as batch,r as derive,s as select,c as readonlyState,n as protectedState,u as lens}; |
+75
-60
| { | ||
| "name": "@nerdalytics/beacon", | ||
| "version": "1000.2.1", | ||
| "author": "Denny Trebbin (nerdalytics)", | ||
| "description": "A lightweight reactive state library for Node.js backends. Enables reactive state management with automatic dependency tracking and efficient updates for server-side applications.", | ||
| "type": "module", | ||
| "main": "dist/src/index.min.js", | ||
| "types": "dist/src/index.d.ts", | ||
| "files": ["dist/src/index.js", "dist/src/index.d.ts", "src/index.ts", "LICENSE"], | ||
| "devDependencies": { | ||
| "@biomejs/biome": "2.2.6", | ||
| "@types/node": "24.9.1", | ||
| "typescript": "5.9.3", | ||
| "uglify-js": "3.19.3" | ||
| }, | ||
| "engines": { | ||
| "node": ">=20.0.0" | ||
| }, | ||
| "exports": { | ||
| ".": { | ||
| "typescript": "./src/index.ts", | ||
| "default": "./dist/src/index.js" | ||
| "import": { | ||
| "default": "./dist/src/index.min.js", | ||
| "types": "./dist/src/index.d.ts" | ||
| }, | ||
| "require": { | ||
| "default": "./dist/src/index.min.js" | ||
| }, | ||
| "types": "./dist/src/index.d.ts", | ||
| "typescript": "./dist/src/index.ts" | ||
| } | ||
| }, | ||
| "files": [ | ||
| "dist/src/index.js", | ||
| "dist/src/index.d.ts", | ||
| "dist/src/index.ts", | ||
| "LICENSE" | ||
| ], | ||
| "keywords": [ | ||
| "backend", | ||
| "batching", | ||
| "computed-values", | ||
| "dependency-tracking", | ||
| "effects", | ||
| "fine-grained", | ||
| "lightweight", | ||
| "memory-management", | ||
| "nodejs", | ||
| "performance", | ||
| "reactive", | ||
| "server-side", | ||
| "signals", | ||
| "state-management", | ||
| "typescript" | ||
| ], | ||
| "license": "MIT", | ||
| "main": "dist/src/index.min.js", | ||
| "name": "@nerdalytics/beacon", | ||
| "packageManager": "npm@11.6.0", | ||
| "repository": { | ||
| "url": "git+https://github.com/nerdalytics/beacon.git", | ||
| "type": "git" | ||
| "type": "git", | ||
| "url": "git+https://github.com/nerdalytics/beacon.git" | ||
| }, | ||
| "scripts": { | ||
| "lint": "npx @biomejs/biome lint --config-path=./biome.json", | ||
| "lint:fix": "npx @biomejs/biome lint --fix --config-path=./biome.json", | ||
| "lint:fix:unsafe": "npx @biomejs/biome lint --fix --unsafe --config-path=./biome.json", | ||
| "format": "npx @biomejs/biome format --write --config-path=./biome.json", | ||
| "check": "npx @biomejs/biome check --config-path=./biome.json", | ||
| "check:fix": "npx @biomejs/biome format --fix --config-path=./biome.json", | ||
| "test": "node --test --test-skip-pattern=\"COMPONENT NAME\" tests/**/*.ts", | ||
| "test:coverage": "node --test --experimental-config-file=node.config.json --test-skip-pattern=\"[COMPONENT NAME]\" --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=lcov.info tests/**/*.ts", | ||
| "test:unit:state": "node --test tests/state.test.ts", | ||
| "test:unit:effect": "node --test tests/effect.test.ts", | ||
| "test:unit:batch": "node --test tests/batch.test.ts", | ||
| "test:unit:derive": "node --test tests/derive.test.ts", | ||
| "test:unit:select": "node --test tests/select.test.ts", | ||
| "test:unit:lens": "node --test tests/lens.test.ts", | ||
| "test:unit:cleanup": "node --test tests/cleanup.test.ts", | ||
| "test:unit:cyclic-dependency": "node --test tests/cyclic-dependency.test.ts", | ||
| "test:unit:deep-chain": "node --test tests/deep-chain.test.ts", | ||
| "test:unit:infinite-loop": "node --test tests/infinite-loop.test.ts", | ||
| "test:unit:custom-equality": "node --test tests/custom-equality.test.ts", | ||
| "benchmark": "node scripts/benchmark.ts", | ||
| "benchmark:naiv": "node scripts/naiv-benchmark.ts", | ||
| "build": "npm run build:lts", | ||
| "prebuild:lts": "rm -rf dist/", | ||
| "build:lts": "tsc -p tsconfig.lts.json", | ||
| "check": "npx @biomejs/biome check", | ||
| "check:fix": "npx @biomejs/biome format --fix", | ||
| "format": "npx @biomejs/biome format --write", | ||
| "lint": "npx @biomejs/biome lint", | ||
| "lint:fix": "npx @biomejs/biome lint --fix", | ||
| "lint:fix:unsafe": "npx @biomejs/biome lint --fix --unsafe", | ||
| "postbuild:lts": "npx uglify-js --compress --mangle --module --toplevel --v8 --warn --source-map \"content='dist/src/index.js.map'\" --output dist/src/index.min.js dist/src/index.js", | ||
| "prebuild:lts": "rm -rf dist/", | ||
| "prepublishOnly": "npm run build:lts", | ||
| "pretest:lts": "node scripts/run-lts-tests.js", | ||
| "test": "node --test --test-skip-pattern=\"COMPONENT NAME\" tests/**/*.ts", | ||
| "test:behavior": "node --test tests/infinite-loop.test.ts tests/cyclic-dependency.test.ts tests/cleanup.test.ts", | ||
| "test:core": "node --test tests/*-core.test.ts", | ||
| "test:coverage": "node --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=lcov.info tests/**/*.test.ts", | ||
| "test:integration": "node --test tests/state-*.test.ts tests/batch-integration.test.ts", | ||
| "test:lts:20": "node --test dist/tests/**.js", | ||
| "test:lts:22": "node --test --test-skip-pattern=\"COMPONENT NAME\" dist/tests/**/*.js", | ||
| "test:unit:batch": "node --test tests/batch.test.ts", | ||
| "test:unit:cleanup": "node --test tests/cleanup.test.ts", | ||
| "test:unit:custom-equality": "node --test tests/custom-equality.test.ts", | ||
| "test:unit:cyclic-dependency": "node --test tests/cyclic-dependency.test.ts", | ||
| "test:unit:deep-chain": "node --test tests/deep-chain.test.ts", | ||
| "test:unit:derive": "node --test tests/derive.test.ts", | ||
| "test:unit:effect": "node --test tests/effect.test.ts", | ||
| "test:unit:infinite-loop": "node --test tests/infinite-loop.test.ts", | ||
| "test:unit:lens": "node --test tests/lens.test.ts", | ||
| "test:unit:select": "node --test tests/select.test.ts", | ||
| "test:unit:state": "node --test tests/state.test.ts", | ||
| "update-performance-docs": "node --experimental-config-file=node.config.json scripts/update-performance-docs.ts" | ||
| }, | ||
| "keywords": [ | ||
| "state-management", | ||
| "effects", | ||
| "fine-grained", | ||
| "computed-values", | ||
| "batching", | ||
| "signals", | ||
| "reactive", | ||
| "lightweight", | ||
| "performance", | ||
| "dependency-tracking", | ||
| "memoization", | ||
| "memory-management", | ||
| "nodejs", | ||
| "server-side", | ||
| "backend", | ||
| "typescript" | ||
| ], | ||
| "author": "Denny Trebbin (nerdalytics)", | ||
| "license": "MIT", | ||
| "devDependencies": { | ||
| "@biomejs/biome": "1.9.4", | ||
| "@types/node": "22.14.1", | ||
| "typescript": "5.8.3", | ||
| "uglify-js": "3.19.3" | ||
| }, | ||
| "engines": { | ||
| "node": ">=20.0.0" | ||
| }, | ||
| "packageManager": "npm@11.3.0" | ||
| "type": "module", | ||
| "types": "dist/src/index.d.ts", | ||
| "version": "1000.2.2" | ||
| } |
-639
| // Core types for reactive primitives | ||
| type Subscriber = () => void | ||
| type Unsubscribe = () => void | ||
| export type ReadOnlyState<T> = () => T | ||
| export interface WriteableState<T> { | ||
| set(value: T): void | ||
| update(fn: (value: T) => T): void | ||
| } | ||
| // Special symbol used for internal tracking | ||
| const STATE_ID = Symbol() | ||
| export type State<T> = ReadOnlyState<T> & | ||
| WriteableState<T> & { | ||
| [STATE_ID]?: symbol | ||
| } | ||
| /** | ||
| * Creates a reactive state container with the provided initial value. | ||
| */ | ||
| export const state = <T>(initialValue: T, equalityFn: (a: T, b: T) => boolean = Object.is): State<T> => | ||
| StateImpl.createState(initialValue, equalityFn) | ||
| /** | ||
| * Registers a function to run whenever its reactive dependencies change. | ||
| */ | ||
| export const effect = (fn: () => void): Unsubscribe => StateImpl.createEffect(fn) | ||
| /** | ||
| * Groups multiple state updates to trigger effects only once at the end. | ||
| */ | ||
| export const batch = <T>(fn: () => T): T => StateImpl.executeBatch(fn) | ||
| /** | ||
| * Creates a read-only computed value that updates when its dependencies change. | ||
| */ | ||
| export const derive = <T>(computeFn: () => T): ReadOnlyState<T> => StateImpl.createDerive(computeFn) | ||
| /** | ||
| * Creates an efficient subscription to a subset of a state value. | ||
| */ | ||
| export const select = <T, R>( | ||
| source: ReadOnlyState<T>, | ||
| selectorFn: (state: T) => R, | ||
| equalityFn: (a: R, b: R) => boolean = Object.is | ||
| ): ReadOnlyState<R> => StateImpl.createSelect(source, selectorFn, equalityFn) | ||
| /** | ||
| * Creates a read-only view of a state, hiding mutation methods. | ||
| */ | ||
| export const readonlyState = | ||
| <T>(state: State<T>): ReadOnlyState<T> => | ||
| (): T => | ||
| state() | ||
| /** | ||
| * Creates a state with access control, returning a tuple of reader and writer. | ||
| */ | ||
| export const protectedState = <T>( | ||
| initialValue: T, | ||
| equalityFn: (a: T, b: T) => boolean = Object.is | ||
| ): [ReadOnlyState<T>, WriteableState<T>] => { | ||
| const fullState = state(initialValue, equalityFn) | ||
| return [ | ||
| (): T => readonlyState(fullState)(), | ||
| { | ||
| set: (value: T): void => fullState.set(value), | ||
| update: (fn: (value: T) => T): void => fullState.update(fn), | ||
| }, | ||
| ] | ||
| } | ||
| /** | ||
| * Creates a lens for direct updates to nested properties of a state. | ||
| */ | ||
| export const lens = <T, K>(source: State<T>, accessor: (state: T) => K): State<K> => | ||
| StateImpl.createLens(source, accessor) | ||
| class StateImpl<T> { | ||
| // Static fields track global reactivity state - this centralized approach allows | ||
| // for coordinated updates while maintaining individual state isolation | ||
| private static currentSubscriber: Subscriber | null = null | ||
| private static pendingSubscribers = new Set<Subscriber>() | ||
| private static isNotifying = false | ||
| private static batchDepth = 0 | ||
| private static deferredEffectCreations: Subscriber[] = [] | ||
| private static activeSubscribers = new Set<Subscriber>() | ||
| // WeakMaps enable automatic garbage collection when subscribers are no | ||
| // longer referenced, preventing memory leaks in long-running applications | ||
| private static stateTracking = new WeakMap<Subscriber, Set<symbol>>() | ||
| private static subscriberDependencies = new WeakMap<Subscriber, Set<Set<Subscriber>>>() | ||
| private static parentSubscriber = new WeakMap<Subscriber, Subscriber>() | ||
| private static childSubscribers = new WeakMap<Subscriber, Set<Subscriber>>() | ||
| // Instance state - each state has unique subscribers and ID | ||
| private value: T | ||
| private subscribers = new Set<Subscriber>() | ||
| private stateId = Symbol() | ||
| private equalityFn: (a: T, b: T) => boolean | ||
| constructor(initialValue: T, equalityFn: (a: T, b: T) => boolean = Object.is) { | ||
| this.value = initialValue | ||
| this.equalityFn = equalityFn | ||
| } | ||
| /** | ||
| * Creates a reactive state container with the provided initial value. | ||
| * Implementation of the public 'state' function. | ||
| */ | ||
| static createState = <T>(initialValue: T, equalityFn: (a: T, b: T) => boolean = Object.is): State<T> => { | ||
| const instance = new StateImpl<T>(initialValue, equalityFn) | ||
| const get = (): T => instance.get() | ||
| get.set = (value: T): void => instance.set(value) | ||
| get.update = (fn: (currentValue: T) => T): void => instance.update(fn) | ||
| get[STATE_ID] = instance.stateId | ||
| return get as State<T> | ||
| } | ||
| // Auto-tracks dependencies when called within effects, creating a fine-grained | ||
| // reactivity graph that only updates affected components | ||
| get = (): T => { | ||
| const currentEffect = StateImpl.currentSubscriber | ||
| if (currentEffect) { | ||
| // Add this effect to subscribers for future notification | ||
| this.subscribers.add(currentEffect) | ||
| // Maintain bidirectional dependency tracking to enable precise cleanup | ||
| // when effects are unsubscribed, preventing memory leaks | ||
| let dependencies = StateImpl.subscriberDependencies.get(currentEffect) | ||
| if (!dependencies) { | ||
| dependencies = new Set() | ||
| StateImpl.subscriberDependencies.set(currentEffect, dependencies) | ||
| } | ||
| dependencies.add(this.subscribers) | ||
| // Track read states to detect direct cyclical dependencies that | ||
| // could cause infinite loops | ||
| let readStates = StateImpl.stateTracking.get(currentEffect) | ||
| if (!readStates) { | ||
| readStates = new Set() | ||
| StateImpl.stateTracking.set(currentEffect, readStates) | ||
| } | ||
| readStates.add(this.stateId) | ||
| } | ||
| return this.value | ||
| } | ||
| // Handles value updates with built-in optimizations and safeguards | ||
| set = (newValue: T): void => { | ||
| // Skip updates for unchanged values to prevent redundant effect executions | ||
| if (this.equalityFn(this.value, newValue)) { | ||
| return | ||
| } | ||
| // Infinite loop detection prevents direct self-mutation within effects, | ||
| // while allowing nested effect patterns that would otherwise appear cyclical | ||
| const effect = StateImpl.currentSubscriber | ||
| if (effect) { | ||
| const states = StateImpl.stateTracking.get(effect) | ||
| if (states?.has(this.stateId) && !StateImpl.parentSubscriber.get(effect)) { | ||
| throw new Error('Infinite loop detected: effect() cannot update a state() it depends on!') | ||
| } | ||
| } | ||
| this.value = newValue | ||
| // Skip updates when there are no subscribers, avoiding unnecessary processing | ||
| if (this.subscribers.size === 0) { | ||
| return | ||
| } | ||
| // Queue notifications instead of executing immediately to support batch operations | ||
| // and prevent redundant effect runs | ||
| for (const sub of this.subscribers) { | ||
| StateImpl.pendingSubscribers.add(sub) | ||
| } | ||
| // Immediate execution outside of batches, deferred execution inside batches | ||
| if (StateImpl.batchDepth === 0 && !StateImpl.isNotifying) { | ||
| StateImpl.notifySubscribers() | ||
| } | ||
| } | ||
| update = (fn: (currentValue: T) => T): void => { | ||
| this.set(fn(this.value)) | ||
| } | ||
| /** | ||
| * Registers a function to run whenever its reactive dependencies change. | ||
| * Implementation of the public 'effect' function. | ||
| */ | ||
| static createEffect = (fn: () => void): Unsubscribe => { | ||
| const runEffect = (): void => { | ||
| // Prevent re-entrance to avoid cascade updates during effect execution | ||
| if (StateImpl.activeSubscribers.has(runEffect)) { | ||
| return | ||
| } | ||
| StateImpl.activeSubscribers.add(runEffect) | ||
| const parentEffect = StateImpl.currentSubscriber | ||
| try { | ||
| // Clean existing subscriptions before running to ensure only | ||
| // currently accessed states are tracked as dependencies | ||
| StateImpl.cleanupEffect(runEffect) | ||
| // Set current context for automatic dependency tracking | ||
| StateImpl.currentSubscriber = runEffect | ||
| StateImpl.stateTracking.set(runEffect, new Set()) | ||
| // Track parent-child relationships to handle nested effects correctly | ||
| // and enable hierarchical cleanup later | ||
| if (parentEffect) { | ||
| StateImpl.parentSubscriber.set(runEffect, parentEffect) | ||
| let children = StateImpl.childSubscribers.get(parentEffect) | ||
| if (!children) { | ||
| children = new Set() | ||
| StateImpl.childSubscribers.set(parentEffect, children) | ||
| } | ||
| children.add(runEffect) | ||
| } | ||
| // Execute the effect function, which will auto-track dependencies | ||
| fn() | ||
| } finally { | ||
| // Restore previous context when done | ||
| StateImpl.currentSubscriber = parentEffect | ||
| StateImpl.activeSubscribers.delete(runEffect) | ||
| } | ||
| } | ||
| // Run immediately unless we're in a batch operation | ||
| if (StateImpl.batchDepth === 0) { | ||
| runEffect() | ||
| } else { | ||
| // Still track parent-child relationship even when deferred, | ||
| // ensuring proper hierarchical cleanup later | ||
| if (StateImpl.currentSubscriber) { | ||
| const parent = StateImpl.currentSubscriber | ||
| StateImpl.parentSubscriber.set(runEffect, parent) | ||
| let children = StateImpl.childSubscribers.get(parent) | ||
| if (!children) { | ||
| children = new Set() | ||
| StateImpl.childSubscribers.set(parent, children) | ||
| } | ||
| children.add(runEffect) | ||
| } | ||
| // Queue for execution when batch completes | ||
| StateImpl.deferredEffectCreations.push(runEffect) | ||
| } | ||
| // Return cleanup function to properly disconnect from reactivity graph | ||
| return (): void => { | ||
| // Remove from dependency tracking to stop future notifications | ||
| StateImpl.cleanupEffect(runEffect) | ||
| StateImpl.pendingSubscribers.delete(runEffect) | ||
| StateImpl.activeSubscribers.delete(runEffect) | ||
| StateImpl.stateTracking.delete(runEffect) | ||
| // Clean up parent-child relationship bidirectionally | ||
| const parent = StateImpl.parentSubscriber.get(runEffect) | ||
| if (parent) { | ||
| const siblings = StateImpl.childSubscribers.get(parent) | ||
| if (siblings) { | ||
| siblings.delete(runEffect) | ||
| } | ||
| } | ||
| StateImpl.parentSubscriber.delete(runEffect) | ||
| // Recursively clean up child effects to prevent memory leaks in | ||
| // nested effect scenarios | ||
| const children = StateImpl.childSubscribers.get(runEffect) | ||
| if (children) { | ||
| for (const child of children) { | ||
| StateImpl.cleanupEffect(child) | ||
| } | ||
| children.clear() | ||
| StateImpl.childSubscribers.delete(runEffect) | ||
| } | ||
| } | ||
| } | ||
| /** | ||
| * Groups multiple state updates to trigger effects only once at the end. | ||
| * Implementation of the public 'batch' function. | ||
| */ | ||
| static executeBatch = <T>(fn: () => T): T => { | ||
| // Increment depth counter to handle nested batches correctly | ||
| StateImpl.batchDepth++ | ||
| try { | ||
| return fn() | ||
| } catch (error: unknown) { | ||
| // Clean up on error to prevent stale subscribers from executing | ||
| // and potentially causing cascading errors | ||
| if (StateImpl.batchDepth === 1) { | ||
| StateImpl.pendingSubscribers.clear() | ||
| StateImpl.deferredEffectCreations.length = 0 | ||
| } | ||
| throw error | ||
| } finally { | ||
| StateImpl.batchDepth-- | ||
| // Only process effects when exiting the outermost batch, | ||
| // maintaining proper execution order while avoiding redundant runs | ||
| if (StateImpl.batchDepth === 0) { | ||
| // Process effects created during the batch | ||
| if (StateImpl.deferredEffectCreations.length > 0) { | ||
| const effectsToRun = [...StateImpl.deferredEffectCreations] | ||
| StateImpl.deferredEffectCreations.length = 0 | ||
| for (const effect of effectsToRun) { | ||
| effect() | ||
| } | ||
| } | ||
| // Process state updates that occurred during the batch | ||
| if (StateImpl.pendingSubscribers.size > 0 && !StateImpl.isNotifying) { | ||
| StateImpl.notifySubscribers() | ||
| } | ||
| } | ||
| } | ||
| } | ||
| /** | ||
| * Creates a read-only computed value that updates when its dependencies change. | ||
| * Implementation of the public 'derive' function. | ||
| */ | ||
| static createDerive = <T>(computeFn: () => T): ReadOnlyState<T> => { | ||
| const valueState = StateImpl.createState<T | undefined>(undefined) | ||
| let initialized = false | ||
| let cachedValue: T | ||
| // Internal effect automatically tracks dependencies and updates the derived value | ||
| StateImpl.createEffect((): void => { | ||
| const newValue = computeFn() | ||
| // Only update if the value actually changed to preserve referential equality | ||
| // and prevent unnecessary downstream updates | ||
| if (!(initialized && Object.is(cachedValue, newValue))) { | ||
| cachedValue = newValue | ||
| valueState.set(newValue) | ||
| } | ||
| initialized = true | ||
| }) | ||
| // Return function with lazy initialization - ensures value is available | ||
| // even when accessed before its dependencies have had a chance to update | ||
| return (): T => { | ||
| if (!initialized) { | ||
| cachedValue = computeFn() | ||
| initialized = true | ||
| valueState.set(cachedValue) | ||
| } | ||
| return valueState() as T | ||
| } | ||
| } | ||
| /** | ||
| * Creates an efficient subscription to a subset of a state value. | ||
| * Implementation of the public 'select' function. | ||
| */ | ||
| static createSelect = <T, R>( | ||
| source: ReadOnlyState<T>, | ||
| selectorFn: (state: T) => R, | ||
| equalityFn: (a: R, b: R) => boolean = Object.is | ||
| ): ReadOnlyState<R> => { | ||
| let lastSourceValue: T | undefined | ||
| let lastSelectedValue: R | undefined | ||
| let initialized = false | ||
| const valueState = StateImpl.createState<R | undefined>(undefined) | ||
| // Internal effect to track the source and update only when needed | ||
| StateImpl.createEffect((): void => { | ||
| const sourceValue = source() | ||
| // Skip computation if source reference hasn't changed | ||
| if (initialized && Object.is(lastSourceValue, sourceValue)) { | ||
| return | ||
| } | ||
| lastSourceValue = sourceValue | ||
| const newSelectedValue = selectorFn(sourceValue) | ||
| // Use custom equality function to determine if value semantically changed, | ||
| // allowing for deep equality comparisons with complex objects | ||
| if (initialized && lastSelectedValue !== undefined && equalityFn(lastSelectedValue, newSelectedValue)) { | ||
| return | ||
| } | ||
| // Update cache and notify subscribers due the value has changed | ||
| lastSelectedValue = newSelectedValue | ||
| valueState.set(newSelectedValue) | ||
| initialized = true | ||
| }) | ||
| // Return function with eager initialization capability | ||
| return (): R => { | ||
| if (!initialized) { | ||
| lastSourceValue = source() | ||
| lastSelectedValue = selectorFn(lastSourceValue) | ||
| valueState.set(lastSelectedValue) | ||
| initialized = true | ||
| } | ||
| return valueState() as R | ||
| } | ||
| } | ||
| /** | ||
| * Creates a lens for direct updates to nested properties of a state. | ||
| * Implementation of the public 'lens' function. | ||
| */ | ||
| static createLens = <T, K>(source: State<T>, accessor: (state: T) => K): State<K> => { | ||
| // Extract the property path once during lens creation | ||
| const extractPath = (): (string | number)[] => { | ||
| const path: (string | number)[] = [] | ||
| const proxy = new Proxy( | ||
| {}, | ||
| { | ||
| get: (_: object, prop: string | symbol): unknown => { | ||
| if (typeof prop === 'string' || typeof prop === 'number') { | ||
| path.push(prop) | ||
| } | ||
| return proxy | ||
| }, | ||
| } | ||
| ) | ||
| try { | ||
| accessor(proxy as unknown as T) | ||
| } catch { | ||
| // Ignore errors, we're just collecting the path | ||
| } | ||
| return path | ||
| } | ||
| // Capture the path once | ||
| const path = extractPath() | ||
| // Create a state with the initial value from the source | ||
| const lensState = StateImpl.createState<K>(accessor(source())) | ||
| // Prevent circular updates | ||
| let isUpdating = false | ||
| // Set up an effect to sync from source to lens | ||
| StateImpl.createEffect((): void => { | ||
| if (isUpdating) { | ||
| return | ||
| } | ||
| isUpdating = true | ||
| try { | ||
| lensState.set(accessor(source())) | ||
| } finally { | ||
| isUpdating = false | ||
| } | ||
| }) | ||
| // Override the lens state's set method to update the source | ||
| const originalSet = lensState.set | ||
| lensState.set = (value: K): void => { | ||
| if (isUpdating) { | ||
| return | ||
| } | ||
| isUpdating = true | ||
| try { | ||
| // Update lens state | ||
| originalSet(value) | ||
| // Update source by modifying the value at path | ||
| source.update((current: T): T => setValueAtPath(current, path, value)) | ||
| } finally { | ||
| isUpdating = false | ||
| } | ||
| } | ||
| // Add update method for completeness | ||
| lensState.update = (fn: (value: K) => K): void => { | ||
| lensState.set(fn(lensState())) | ||
| } | ||
| return lensState | ||
| } | ||
| // Processes queued subscriber notifications in a controlled, non-reentrant way | ||
| private static notifySubscribers = (): void => { | ||
| // Prevent reentrance to avoid cascading notification loops when | ||
| // effects trigger further state changes | ||
| if (StateImpl.isNotifying) { | ||
| return | ||
| } | ||
| StateImpl.isNotifying = true | ||
| try { | ||
| // Process all pending effects in batches for better perf, | ||
| // ensuring topological execution order is maintained | ||
| while (StateImpl.pendingSubscribers.size > 0) { | ||
| // Process in snapshot batches to prevent infinite loops | ||
| // when effects trigger further state changes | ||
| const subscribers = Array.from(StateImpl.pendingSubscribers) | ||
| StateImpl.pendingSubscribers.clear() | ||
| for (const effect of subscribers) { | ||
| effect() | ||
| } | ||
| } | ||
| } finally { | ||
| StateImpl.isNotifying = false | ||
| } | ||
| } | ||
| // Removes effect from dependency tracking to prevent memory leaks | ||
| private static cleanupEffect = (effect: Subscriber): void => { | ||
| // Remove from execution queue to prevent stale updates | ||
| StateImpl.pendingSubscribers.delete(effect) | ||
| // Remove bidirectional dependency references to prevent memory leaks | ||
| const deps = StateImpl.subscriberDependencies.get(effect) | ||
| if (deps) { | ||
| for (const subscribers of deps) { | ||
| subscribers.delete(effect) | ||
| } | ||
| deps.clear() | ||
| StateImpl.subscriberDependencies.delete(effect) | ||
| } | ||
| } | ||
| } | ||
| // Helper for array updates | ||
| const updateArrayItem = <V>(arr: unknown[], index: number, value: V): unknown[] => { | ||
| const copy = [...arr] | ||
| copy[index] = value | ||
| return copy | ||
| } | ||
| // Helper for single-level updates (optimization) | ||
| const updateShallowProperty = <V>( | ||
| obj: Record<string | number, unknown>, | ||
| key: string | number, | ||
| value: V | ||
| ): Record<string | number, unknown> => { | ||
| const result = { ...obj } | ||
| result[key] = value | ||
| return result | ||
| } | ||
| // Helper to create the appropriate container type | ||
| const createContainer = (key: string | number): Record<string | number, unknown> | unknown[] => { | ||
| const isArrayKey = typeof key === 'number' || !Number.isNaN(Number(key)) | ||
| return isArrayKey ? [] : {} | ||
| } | ||
| // Helper for handling array path updates | ||
| const updateArrayPath = <V>(array: unknown[], pathSegments: (string | number)[], value: V): unknown[] => { | ||
| const index = Number(pathSegments[0]) | ||
| if (pathSegments.length === 1) { | ||
| // Simple array item update | ||
| return updateArrayItem(array, index, value) | ||
| } | ||
| // Nested path in array | ||
| const copy = [...array] | ||
| const nextPathSegments = pathSegments.slice(1) | ||
| const nextKey = nextPathSegments[0] | ||
| // For null/undefined values in arrays, create appropriate containers | ||
| let nextValue = array[index] | ||
| if (nextValue === undefined || nextValue === null) { | ||
| // Use empty object as default if nextKey is undefined | ||
| nextValue = nextKey !== undefined ? createContainer(nextKey) : {} | ||
| } | ||
| copy[index] = setValueAtPath(nextValue, nextPathSegments, value) | ||
| return copy | ||
| } | ||
| // Helper for handling object path updates | ||
| const updateObjectPath = <V>( | ||
| obj: Record<string | number, unknown>, | ||
| pathSegments: (string | number)[], | ||
| value: V | ||
| ): Record<string | number, unknown> => { | ||
| // Ensure we have a valid key | ||
| const currentKey = pathSegments[0] | ||
| if (currentKey === undefined) { | ||
| // This shouldn't happen given our checks in the main function | ||
| return obj | ||
| } | ||
| if (pathSegments.length === 1) { | ||
| // Simple object property update | ||
| return updateShallowProperty(obj, currentKey, value) | ||
| } | ||
| // Nested path in object | ||
| const nextPathSegments = pathSegments.slice(1) | ||
| const nextKey = nextPathSegments[0] | ||
| // For null/undefined values, create appropriate containers | ||
| let currentValue = obj[currentKey] | ||
| if (currentValue === undefined || currentValue === null) { | ||
| // Use empty object as default if nextKey is undefined | ||
| currentValue = nextKey !== undefined ? createContainer(nextKey) : {} | ||
| } | ||
| // Create new object with updated property | ||
| const result = { ...obj } | ||
| result[currentKey] = setValueAtPath(currentValue, nextPathSegments, value) | ||
| return result | ||
| } | ||
| // Simplified function to update a nested value at a path | ||
| const setValueAtPath = <V, O>(obj: O, pathSegments: (string | number)[], value: V): O => { | ||
| // Handle base cases | ||
| if (pathSegments.length === 0) { | ||
| return value as unknown as O | ||
| } | ||
| if (obj === undefined || obj === null) { | ||
| return setValueAtPath({} as O, pathSegments, value) | ||
| } | ||
| const currentKey = pathSegments[0] | ||
| if (currentKey === undefined) { | ||
| return obj | ||
| } | ||
| // Delegate to specialized handlers based on data type | ||
| if (Array.isArray(obj)) { | ||
| return updateArrayPath(obj, pathSegments, value) as unknown as O | ||
| } | ||
| return updateObjectPath(obj as Record<string | number, unknown>, pathSegments, value) as unknown as O | ||
| } |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
45282
-27.91%6
-14.29%424
-55.32%1
Infinity%