@nerdalytics/beacon
Advanced tools
+14
-13
@@ -1,4 +0,4 @@ | ||
| export type Unsubscribe = () => void; | ||
| export type ReadOnlyState<T> = () => T; | ||
| export interface WriteableState<T> { | ||
| type Unsubscribe = () => void; | ||
| type ReadOnlyState<T> = () => T; | ||
| interface WriteableState<T> { | ||
| set(value: T): void; | ||
@@ -8,13 +8,14 @@ update(fn: (value: T) => T): void; | ||
| declare const STATE_ID: unique symbol; | ||
| export type State<T> = ReadOnlyState<T> & WriteableState<T> & { | ||
| type State<T> = ReadOnlyState<T> & WriteableState<T> & { | ||
| [STATE_ID]?: symbol; | ||
| }; | ||
| export declare const state: <T>(initialValue: T, equalityFn?: (a: T, b: T) => boolean) => State<T>; | ||
| export declare const effect: (fn: () => void) => Unsubscribe; | ||
| export declare const batch: <T>(fn: () => T) => T; | ||
| export declare const derive: <T>(computeFn: () => T) => ReadOnlyState<T>; | ||
| export declare const select: <T, R>(source: ReadOnlyState<T>, selectorFn: (state: T) => R, equalityFn?: (a: R, b: R) => boolean) => ReadOnlyState<R>; | ||
| export declare const readonlyState: <T>(state: State<T>) => ReadOnlyState<T>; | ||
| export declare const protectedState: <T>(initialValue: T, equalityFn?: (a: T, b: T) => boolean) => [ReadOnlyState<T>, WriteableState<T>]; | ||
| export declare const lens: <T, K>(source: State<T>, accessor: (state: T) => K) => State<K>; | ||
| export {}; | ||
| declare const createState: <T>(initialValue: T, equalityFn?: (a: T, b: T) => boolean) => State<T>; | ||
| declare const createEffect: (fn: () => void) => Unsubscribe; | ||
| declare const executeBatch: <T>(fn: () => T) => T; | ||
| declare const createDerive: <T>(computeFn: () => T) => ReadOnlyState<T>; | ||
| declare const createSelect: <T, R>(source: ReadOnlyState<T>, selectorFn: (state: T) => R, equalityFn?: (a: R, b: R) => boolean) => ReadOnlyState<R>; | ||
| declare const createLens: <T, K>(source: State<T>, accessor: (state: T) => K) => State<K>; | ||
| declare const createReadonlyState: <T>(source: State<T>) => ReadOnlyState<T>; | ||
| declare const createProtectedState: <T>(initialValue: T, equalityFn?: (a: T, b: T) => boolean) => [ReadOnlyState<T>, WriteableState<T>]; | ||
| export { createDerive as derive, createEffect as effect, createLens as lens, createProtectedState as protectedState, createReadonlyState as readonlyState, createSelect as select, createState as state, executeBatch as batch, }; | ||
| export type { ReadOnlyState, State, Unsubscribe, WriteableState }; |
@@ -1,1 +0,1 @@ | ||
| 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=[];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=l.pendingSubscribers;l.pendingSubscribers=new Set;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}; | ||
| let a=Symbol("STATE_ID"),s=null,u=new Set,d=!1,c=0,l=[],i=new Set,o=new WeakMap,f=new WeakMap,S=new WeakMap,r=new WeakMap,v=()=>{if(!d){d=!0;try{for(;0<u.size;){var e,t=u;u=new Set;for(e of t)e()}}finally{d=!1}}},n=e=>{u.delete(e);var t=f.get(e);if(t){for(var a of t)a.delete(e);t.clear(),f.delete(e)}},p=(e,l=Object.is)=>{let i=e,r=new Set,n=Symbol(),t=()=>{var a=s;if(a){r.add(a);let e=f.get(a),t=(e||(e=new Set,f.set(a,e)),e.add(r),o.get(a));t||(t=new Set,o.set(a,t)),t.add(n)}return i};return t.set=e=>{if(!l(i,e)){var t=s;if(t)if(o.get(t)?.has(n)&&!S.get(t))throw new Error("Infinite loop detected: effect() cannot update a state() it depends on!");if(i=e,0!==r.size){for(var a of r)u.add(a);0!==c||d||v()}}},t.update=e=>{t.set(e(i))},t[a]=n,t},g=e=>{let a=()=>{if(!i.has(a)){i.add(a);var t=s;try{if(n(a),s=a,o.set(a,new Set),t){S.set(a,t);let e=r.get(t);e||(e=new Set,r.set(t,e)),e.add(a)}e()}finally{s=t,i.delete(a)}}};if(0===c)a();else{if(s){var t=s;S.set(a,t);let e=r.get(t);e||(e=new Set,r.set(t,e)),e.add(a)}l.push(a)}return()=>{n(a),u.delete(a),i.delete(a),o.delete(a);var e=S.get(a),e=(e&&(e=r.get(e))&&e.delete(a),S.delete(a),r.get(a));if(e){for(var t of e)n(t);e.clear(),r.delete(a)}}};var e=e=>{c++;try{return e()}catch(e){throw 1===c&&(u.clear(),l.length=0),e}finally{if(0===--c){if(0<l.length){var t,e=l;l=[];for(t of e)t()}0<u.size&&!d&&v()}}},t=e=>{let t={cachedValue:void 0,computeFn:e,initialized:!1,valueState:p(void 0)};return g(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()}},h=(e,t,a=Object.is)=>{let l={equalityFn:a,initialized:!1,lastSelectedValue:void 0,lastSourceValue:void 0,selectorFn:t,source:e,valueState:p(void 0)};return g(function(){var e=l.source();l.initialized&&Object.is(l.lastSourceValue,e)||(l.lastSourceValue=e,e=l.selectorFn(e),l.initialized&&void 0!==l.lastSelectedValue&&l.equalityFn(l.lastSelectedValue,e))||(l.lastSelectedValue=e,l.valueState.set(e),l.initialized=!0)}),function(){return l.initialized||(l.lastSourceValue=l.source(),l.lastSelectedValue=l.selectorFn(l.lastSourceValue),l.valueState.set(l.lastSelectedValue),l.initialized=!0),l.valueState()}};let y=(e,t,a)=>{e=[...e];return e[t]=a,e},w=(e,t,a)=>{e={...e};return e[t]=a,e},V=e=>"number"==typeof e||!Number.isNaN(Number(e))?[]:{},z=(e,t,a)=>{var l=Number(t[0]);if(1===t.length)return y(e,l,a);var i=[...e],t=t.slice(1),r=t[0];let n=e[l];return null==n&&(n=void 0!==r?V(r):{}),i[l]=m(n,t,a),i},b=(e,t,a)=>{var l=t[0];if(void 0===l)return e;if(1===t.length)return w(e,l,a);var t=t.slice(1),i=t[0];let r=e[l];null==r&&(r=void 0!==i?V(i):{});i={...e};return i[l]=m(r,t,a),i},m=(e,t,a)=>0===t.length?a:null==e?m({},t,a):void 0===t[0]?e:(Array.isArray(e)?z:b)(e,t,a);var F=(e,t)=>{let i={accessor:t,isUpdating:!1,lensState:null,originalSet:null,path:[],source:e};return i.path=(()=>{let a=[],l=new Proxy({},{get:(e,t)=>("string"!=typeof t&&"number"!=typeof t||a.push(t),l)});try{i.accessor(l)}catch{}return a})(),i.lensState=p(i.accessor(i.source())),i.originalSet=i.lensState.set,g(function(){if(!i.isUpdating){i.isUpdating=!0;try{i.lensState.set(i.accessor(i.source()))}finally{i.isUpdating=!1}}}),i.lensState.set=function(t){if(!i.isUpdating){i.isUpdating=!0;try{i.originalSet(t),i.source.update(e=>m(e,i.path,t))}finally{i.isUpdating=!1}}},i.lensState.update=function(e){i.lensState.set(e(i.lensState()))},i.lensState};let U=e=>()=>e();var j=(e,t=Object.is)=>{let a=p(e,t);return[()=>U(a)(),{set:e=>a.set(e),update:e=>a.update(e)}]};export{t as derive,g as effect,F as lens,j as protectedState,U as readonlyState,h as select,p as state,e as batch}; |
+6
-5
@@ -5,4 +5,4 @@ { | ||
| "devDependencies": { | ||
| "@biomejs/biome": "2.3.13", | ||
| "@types/node": "25.0.10", | ||
| "@biomejs/biome": "2.3.14", | ||
| "@types/node": "25.2.2", | ||
| "npm-check-updates": "19.3.2", | ||
@@ -54,3 +54,3 @@ "typescript": "5.9.3", | ||
| "name": "@nerdalytics/beacon", | ||
| "packageManager": "npm@11.8.0", | ||
| "packageManager": "npm@11.9.0", | ||
| "repository": { | ||
@@ -61,3 +61,3 @@ "type": "git", | ||
| "scripts": { | ||
| "benchmark": "node scripts/benchmark.ts", | ||
| "benchmark": "node --expose-gc scripts/benchmark.ts", | ||
| "benchmark:naiv": "node scripts/naiv-benchmark.ts", | ||
@@ -97,5 +97,6 @@ "build": "npm run build:lts", | ||
| }, | ||
| "sideEffects": false, | ||
| "type": "module", | ||
| "types": "dist/src/index.d.ts", | ||
| "version": "1000.2.5" | ||
| "version": "1000.3.0" | ||
| } |
+2
-2
@@ -1,2 +0,2 @@ | ||
| # Beacon <img align="right" src="https://raw.githubusercontent.com/nerdalytics/beacon/refs/heads/trunk/assets/beacon-logo.svg" width="128px" alt="A stylized lighthouse beacon with golden light against a dark blue background, representing the reactive state library"/> | ||
| # Beacon <img align="right" src="https://raw.githubusercontent.com/nerdalytics/beacon/refs/heads/trunk/assets/beacon-logo-v2.svg" width="128px" alt="A stylized lighthouse beacon with golden light against a dark blue background, representing the reactive state library"/> | ||
@@ -7,3 +7,3 @@ > Lightweight reactive state management for Node.js backends | ||
| [](https://www.npmjs.com/package/@nerdalytics/beacon) | ||
| [](https://socket.dev/npm/package/@nerdalytics/beacon/overview/1000.2.4) | ||
| [](https://socket.dev/npm/package/@nerdalytics/beacon/overview/1000.3.0) | ||
@@ -10,0 +10,0 @@ [](https://nodejs.org/) |
+336
-463
| // Core types for reactive primitives | ||
| type Subscriber = () => void | ||
| export type Unsubscribe = () => void | ||
| export type ReadOnlyState<T> = () => T | ||
| export interface WriteableState<T> { | ||
| type Unsubscribe = () => void | ||
| type ReadOnlyState<T> = () => T | ||
| interface WriteableState<T> { | ||
| set(value: T): void | ||
@@ -13,3 +13,3 @@ update(fn: (value: T) => T): void | ||
| export type State<T> = ReadOnlyState<T> & | ||
| type State<T> = ReadOnlyState<T> & | ||
| WriteableState<T> & { | ||
@@ -19,149 +19,90 @@ [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) | ||
| // Module-level reactive state | ||
| let currentSubscriber: Subscriber | null = null | ||
| let pendingSubscribers: Set<Subscriber> = new Set<Subscriber>() | ||
| let isNotifying = false | ||
| let batchDepth = 0 | ||
| let deferredEffectCreations: Subscriber[] = [] | ||
| const activeSubscribers: Set<Subscriber> = new Set<Subscriber>() | ||
| const stateTracking: WeakMap<Subscriber, Set<symbol>> = new WeakMap<Subscriber, Set<symbol>>() | ||
| const subscriberDependencies: WeakMap<Subscriber, Set<Set<Subscriber>>> = new WeakMap< | ||
| Subscriber, | ||
| Set<Set<Subscriber>> | ||
| >() | ||
| const parentSubscriber: WeakMap<Subscriber, Subscriber> = new WeakMap<Subscriber, Subscriber>() | ||
| const childSubscribers: WeakMap<Subscriber, Set<Subscriber>> = new WeakMap<Subscriber, Set<Subscriber>>() | ||
| /** | ||
| * Registers a function to run whenever its reactive dependencies change. | ||
| */ | ||
| export const effect = (fn: () => void): Unsubscribe => StateImpl.createEffect(fn) | ||
| const notifySubscribers = (): void => { | ||
| if (isNotifying) { | ||
| return | ||
| } | ||
| /** | ||
| * Groups multiple state updates to trigger effects only once at the end. | ||
| */ | ||
| export const batch = <T>(fn: () => T): T => StateImpl.executeBatch(fn) | ||
| isNotifying = true | ||
| /** | ||
| * Creates a read-only computed value that updates when its dependencies change. | ||
| */ | ||
| export const derive = <T>(computeFn: () => T): ReadOnlyState<T> => StateImpl.createDerive(computeFn) | ||
| try { | ||
| while (pendingSubscribers.size > 0) { | ||
| const subscribers = pendingSubscribers | ||
| pendingSubscribers = new Set() | ||
| /** | ||
| * 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) | ||
| for (const effect of subscribers) { | ||
| effect() | ||
| } | ||
| } | ||
| } finally { | ||
| isNotifying = false | ||
| } | ||
| } | ||
| /** | ||
| * Creates a read-only view of a state, hiding mutation methods. | ||
| */ | ||
| export const readonlyState = | ||
| <T>(state: State<T>): ReadOnlyState<T> => | ||
| (): T => | ||
| state() | ||
| const cleanupEffect = (effect: Subscriber): void => { | ||
| pendingSubscribers.delete(effect) | ||
| /** | ||
| * 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), | ||
| }, | ||
| ] | ||
| const deps = subscriberDependencies.get(effect) | ||
| if (deps) { | ||
| for (const subscribers of deps) { | ||
| subscribers.delete(effect) | ||
| } | ||
| deps.clear() | ||
| subscriberDependencies.delete(effect) | ||
| } | ||
| } | ||
| /** | ||
| * Creates a lens for direct updates to nested properties of a state. | ||
| * Creates a reactive state container with the provided initial value. | ||
| */ | ||
| export const lens = <T, K>(source: State<T>, accessor: (state: T) => K): State<K> => | ||
| StateImpl.createLens(source, accessor) | ||
| const createState = <T>(initialValue: T, equalityFn: (a: T, b: T) => boolean = Object.is): State<T> => { | ||
| let value = initialValue | ||
| const subscribers = new Set<Subscriber>() | ||
| const stateId = Symbol() | ||
| 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 | ||
| const get = (): T => { | ||
| const currentEffect = currentSubscriber | ||
| if (currentEffect) { | ||
| // Add this effect to subscribers for future notification | ||
| this.subscribers.add(currentEffect) | ||
| subscribers.add(currentEffect) | ||
| // Maintain bidirectional dependency tracking to enable precise cleanup | ||
| // when effects are unsubscribed, preventing memory leaks | ||
| let dependencies = StateImpl.subscriberDependencies.get(currentEffect) | ||
| let dependencies = subscriberDependencies.get(currentEffect) | ||
| if (!dependencies) { | ||
| dependencies = new Set() | ||
| StateImpl.subscriberDependencies.set(currentEffect, dependencies) | ||
| subscriberDependencies.set(currentEffect, dependencies) | ||
| } | ||
| dependencies.add(this.subscribers) | ||
| dependencies.add(subscribers) | ||
| // Track read states to detect direct cyclical dependencies that | ||
| // could cause infinite loops | ||
| let readStates = StateImpl.stateTracking.get(currentEffect) | ||
| let readStates = stateTracking.get(currentEffect) | ||
| if (!readStates) { | ||
| readStates = new Set() | ||
| StateImpl.stateTracking.set(currentEffect, readStates) | ||
| stateTracking.set(currentEffect, readStates) | ||
| } | ||
| readStates.add(this.stateId) | ||
| readStates.add(stateId) | ||
| } | ||
| return this.value | ||
| return 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)) { | ||
| get.set = (newValue: T): void => { | ||
| if (equalityFn(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 | ||
| const effect = currentSubscriber | ||
| if (effect) { | ||
| const states = StateImpl.stateTracking.get(effect) | ||
| if (states?.has(this.stateId) && !StateImpl.parentSubscriber.get(effect)) { | ||
| const states = stateTracking.get(effect) | ||
| if (states?.has(stateId) && !parentSubscriber.get(effect)) { | ||
| throw new Error('Infinite loop detected: effect() cannot update a state() it depends on!') | ||
@@ -171,82 +112,49 @@ } | ||
| this.value = newValue | ||
| value = newValue | ||
| // Skip updates when there are no subscribers, avoiding unnecessary processing | ||
| if (this.subscribers.size === 0) { | ||
| if (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) | ||
| for (const sub of subscribers) { | ||
| pendingSubscribers.add(sub) | ||
| } | ||
| // Immediate execution outside of batches, deferred execution inside batches | ||
| if (StateImpl.batchDepth === 0 && !StateImpl.isNotifying) { | ||
| StateImpl.notifySubscribers() | ||
| if (batchDepth === 0 && !isNotifying) { | ||
| notifySubscribers() | ||
| } | ||
| } | ||
| update = (fn: (currentValue: T) => T): void => { | ||
| this.set(fn(this.value)) | ||
| get.update = (fn: (currentValue: T) => T): void => { | ||
| get.set(fn(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 | ||
| } | ||
| get[STATE_ID] = stateId | ||
| return get as State<T> | ||
| } | ||
| StateImpl.activeSubscribers.add(runEffect) | ||
| const parentEffect = StateImpl.currentSubscriber | ||
| /** | ||
| * Registers a function to run whenever its reactive dependencies change. | ||
| */ | ||
| const createEffect = (fn: () => void): Unsubscribe => { | ||
| const runEffect = (): void => { | ||
| if (activeSubscribers.has(runEffect)) { | ||
| return | ||
| } | ||
| try { | ||
| // Clean existing subscriptions before running to ensure only | ||
| // currently accessed states are tracked as dependencies | ||
| StateImpl.cleanupEffect(runEffect) | ||
| activeSubscribers.add(runEffect) | ||
| const parentEffect = currentSubscriber | ||
| // Set current context for automatic dependency tracking | ||
| StateImpl.currentSubscriber = runEffect | ||
| StateImpl.stateTracking.set(runEffect, new Set()) | ||
| try { | ||
| cleanupEffect(runEffect) | ||
| // 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) | ||
| } | ||
| currentSubscriber = runEffect | ||
| stateTracking.set(runEffect, new Set()) | ||
| // 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 (parentEffect) { | ||
| parentSubscriber.set(runEffect, parentEffect) | ||
| let children = childSubscribers.get(parentEffect) | ||
| if (!children) { | ||
| children = new Set() | ||
| StateImpl.childSubscribers.set(parent, children) | ||
| childSubscribers.set(parentEffect, children) | ||
| } | ||
@@ -256,306 +164,168 @@ children.add(runEffect) | ||
| // Queue for execution when batch completes | ||
| StateImpl.deferredEffectCreations.push(runEffect) | ||
| fn() | ||
| } finally { | ||
| currentSubscriber = parentEffect | ||
| activeSubscribers.delete(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) | ||
| } | ||
| if (batchDepth === 0) { | ||
| runEffect() | ||
| } else { | ||
| if (currentSubscriber) { | ||
| const parent = currentSubscriber | ||
| parentSubscriber.set(runEffect, parent) | ||
| let children = childSubscribers.get(parent) | ||
| if (!children) { | ||
| children = new Set() | ||
| childSubscribers.set(parent, children) | ||
| } | ||
| StateImpl.parentSubscriber.delete(runEffect) | ||
| children.add(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) | ||
| } | ||
| } | ||
| deferredEffectCreations.push(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-- | ||
| return (): void => { | ||
| cleanupEffect(runEffect) | ||
| pendingSubscribers.delete(runEffect) | ||
| activeSubscribers.delete(runEffect) | ||
| stateTracking.delete(runEffect) | ||
| // 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) { | ||
| // Swap reference instead of spread copy to avoid array allocation | ||
| const effectsToRun = StateImpl.deferredEffectCreations | ||
| StateImpl.deferredEffectCreations = [] | ||
| for (const effect of effectsToRun) { | ||
| effect() | ||
| } | ||
| } | ||
| // Process state updates that occurred during the batch | ||
| if (StateImpl.pendingSubscribers.size > 0 && !StateImpl.isNotifying) { | ||
| StateImpl.notifySubscribers() | ||
| } | ||
| const parent = parentSubscriber.get(runEffect) | ||
| if (parent) { | ||
| const siblings = childSubscribers.get(parent) | ||
| if (siblings) { | ||
| siblings.delete(runEffect) | ||
| } | ||
| } | ||
| } | ||
| parentSubscriber.delete(runEffect) | ||
| /** | ||
| * 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> => { | ||
| // Create a container to hold state and minimize closure captures | ||
| const container = { | ||
| cachedValue: undefined as unknown as T, | ||
| computeFn, | ||
| initialized: false, | ||
| valueState: StateImpl.createState<T | undefined>(undefined), | ||
| } | ||
| // Internal effect automatically tracks dependencies and updates the derived value | ||
| StateImpl.createEffect(function deriveEffect(): void { | ||
| const newValue = container.computeFn() | ||
| // Only update if the value actually changed to preserve referential equality | ||
| // and prevent unnecessary downstream updates | ||
| if (!(container.initialized && Object.is(container.cachedValue, newValue))) { | ||
| container.cachedValue = newValue | ||
| container.valueState.set(newValue) | ||
| const children = childSubscribers.get(runEffect) | ||
| if (children) { | ||
| for (const child of children) { | ||
| cleanupEffect(child) | ||
| } | ||
| container.initialized = true | ||
| }) | ||
| // Return function with lazy initialization - ensures value is available | ||
| // even when accessed before its dependencies have had a chance to update | ||
| return function deriveGetter(): T { | ||
| if (!container.initialized) { | ||
| container.cachedValue = container.computeFn() | ||
| container.initialized = true | ||
| container.valueState.set(container.cachedValue) | ||
| } | ||
| return container.valueState() as T | ||
| children.clear() | ||
| childSubscribers.delete(runEffect) | ||
| } | ||
| } | ||
| } | ||
| /** | ||
| * 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> => { | ||
| // Create a container to hold state and minimize closure captures | ||
| const container = { | ||
| equalityFn, | ||
| initialized: false, | ||
| lastSelectedValue: undefined as R | undefined, | ||
| lastSourceValue: undefined as T | undefined, | ||
| selectorFn, | ||
| source, | ||
| valueState: StateImpl.createState<R | undefined>(undefined), | ||
| /** | ||
| * Groups multiple state updates to trigger effects only once at the end. | ||
| */ | ||
| const executeBatch = <T>(fn: () => T): T => { | ||
| batchDepth++ | ||
| try { | ||
| return fn() | ||
| } catch (error: unknown) { | ||
| if (batchDepth === 1) { | ||
| pendingSubscribers.clear() | ||
| deferredEffectCreations.length = 0 | ||
| } | ||
| throw error | ||
| } finally { | ||
| batchDepth-- | ||
| // Internal effect to track the source and update only when needed | ||
| StateImpl.createEffect(function selectEffect(): void { | ||
| const sourceValue = container.source() | ||
| // Skip computation if source reference hasn't changed | ||
| if (container.initialized && Object.is(container.lastSourceValue, sourceValue)) { | ||
| return | ||
| if (batchDepth === 0) { | ||
| if (deferredEffectCreations.length > 0) { | ||
| const effectsToRun = deferredEffectCreations | ||
| deferredEffectCreations = [] | ||
| for (const effect of effectsToRun) { | ||
| effect() | ||
| } | ||
| } | ||
| container.lastSourceValue = sourceValue | ||
| const newSelectedValue = container.selectorFn(sourceValue) | ||
| // Use custom equality function to determine if value semantically changed, | ||
| // allowing for deep equality comparisons with complex objects | ||
| if ( | ||
| container.initialized && | ||
| container.lastSelectedValue !== undefined && | ||
| container.equalityFn(container.lastSelectedValue, newSelectedValue) | ||
| ) { | ||
| return | ||
| if (pendingSubscribers.size > 0 && !isNotifying) { | ||
| notifySubscribers() | ||
| } | ||
| // Update cache and notify subscribers due the value has changed | ||
| container.lastSelectedValue = newSelectedValue | ||
| container.valueState.set(newSelectedValue) | ||
| container.initialized = true | ||
| }) | ||
| // Return function with eager initialization capability | ||
| return function selectGetter(): R { | ||
| if (!container.initialized) { | ||
| container.lastSourceValue = container.source() | ||
| container.lastSelectedValue = container.selectorFn(container.lastSourceValue) | ||
| container.valueState.set(container.lastSelectedValue) | ||
| container.initialized = true | ||
| } | ||
| return container.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> => { | ||
| // Create a container to hold lens state and minimize closure captures | ||
| const container = { | ||
| accessor, | ||
| isUpdating: false, | ||
| lensState: null as unknown as State<K>, | ||
| originalSet: null as unknown as (value: K) => void, | ||
| path: [] as (string | number)[], | ||
| source, | ||
| } | ||
| /** | ||
| * Creates a read-only computed value that updates when its dependencies change. | ||
| */ | ||
| const createDerive = <T>(computeFn: () => T): ReadOnlyState<T> => { | ||
| const container = { | ||
| cachedValue: undefined as unknown as T, | ||
| computeFn, | ||
| initialized: false, | ||
| valueState: createState<T | undefined>(undefined), | ||
| } | ||
| // Extract the property path once during lens creation | ||
| const extractPath = (): (string | number)[] => { | ||
| const pathCollector: (string | number)[] = [] | ||
| const proxy = new Proxy( | ||
| {}, | ||
| { | ||
| get: (_: object, prop: string | symbol): unknown => { | ||
| if (typeof prop === 'string' || typeof prop === 'number') { | ||
| pathCollector.push(prop) | ||
| } | ||
| return proxy | ||
| }, | ||
| } | ||
| ) | ||
| createEffect(function deriveEffect(): void { | ||
| const newValue = container.computeFn() | ||
| try { | ||
| container.accessor(proxy as unknown as T) | ||
| } catch { | ||
| // Ignore errors, we're just collecting the path | ||
| } | ||
| return pathCollector | ||
| if (!(container.initialized && Object.is(container.cachedValue, newValue))) { | ||
| container.cachedValue = newValue | ||
| container.valueState.set(newValue) | ||
| } | ||
| // Capture the path once | ||
| container.path = extractPath() | ||
| container.initialized = true | ||
| }) | ||
| // Create a state with the initial value from the source | ||
| container.lensState = StateImpl.createState<K>(container.accessor(container.source())) | ||
| container.originalSet = container.lensState.set | ||
| // Set up an effect to sync from source to lens | ||
| StateImpl.createEffect(function lensEffect(): void { | ||
| if (container.isUpdating) { | ||
| return | ||
| } | ||
| container.isUpdating = true | ||
| try { | ||
| container.lensState.set(container.accessor(container.source())) | ||
| } finally { | ||
| container.isUpdating = false | ||
| } | ||
| }) | ||
| // Override the lens state's set method to update the source | ||
| container.lensState.set = function lensSet(value: K): void { | ||
| if (container.isUpdating) { | ||
| return | ||
| } | ||
| container.isUpdating = true | ||
| try { | ||
| // Update lens state | ||
| container.originalSet(value) | ||
| // Update source by modifying the value at path | ||
| container.source.update((current: T): T => setValueAtPath(current, container.path, value)) | ||
| } finally { | ||
| container.isUpdating = false | ||
| } | ||
| return function deriveGetter(): T { | ||
| if (!container.initialized) { | ||
| container.cachedValue = container.computeFn() | ||
| container.initialized = true | ||
| container.valueState.set(container.cachedValue) | ||
| } | ||
| return container.valueState() as T | ||
| } | ||
| } | ||
| // Add update method for completeness | ||
| container.lensState.update = function lensUpdate(fn: (value: K) => K): void { | ||
| container.lensState.set(fn(container.lensState())) | ||
| } | ||
| return container.lensState | ||
| /** | ||
| * Creates an efficient subscription to a subset of a state value. | ||
| */ | ||
| const createSelect = <T, R>( | ||
| source: ReadOnlyState<T>, | ||
| selectorFn: (state: T) => R, | ||
| equalityFn: (a: R, b: R) => boolean = Object.is | ||
| ): ReadOnlyState<R> => { | ||
| const container = { | ||
| equalityFn, | ||
| initialized: false, | ||
| lastSelectedValue: undefined as R | undefined, | ||
| lastSourceValue: undefined as T | undefined, | ||
| selectorFn, | ||
| source, | ||
| valueState: createState<R | undefined>(undefined), | ||
| } | ||
| // 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) { | ||
| createEffect(function selectEffect(): void { | ||
| const sourceValue = container.source() | ||
| if (container.initialized && Object.is(container.lastSourceValue, sourceValue)) { | ||
| return | ||
| } | ||
| StateImpl.isNotifying = true | ||
| container.lastSourceValue = sourceValue | ||
| const newSelectedValue = container.selectorFn(sourceValue) | ||
| try { | ||
| // Process all pending effects in batches for better perf, | ||
| // ensuring topological execution order is maintained | ||
| while (StateImpl.pendingSubscribers.size > 0) { | ||
| // Swap with empty Set instead of Array.from() to avoid array allocation | ||
| const subscribers = StateImpl.pendingSubscribers | ||
| StateImpl.pendingSubscribers = new Set() | ||
| for (const effect of subscribers) { | ||
| effect() | ||
| } | ||
| } | ||
| } finally { | ||
| StateImpl.isNotifying = false | ||
| if ( | ||
| container.initialized && | ||
| container.lastSelectedValue !== undefined && | ||
| container.equalityFn(container.lastSelectedValue, newSelectedValue) | ||
| ) { | ||
| return | ||
| } | ||
| } | ||
| // 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) | ||
| container.lastSelectedValue = newSelectedValue | ||
| container.valueState.set(newSelectedValue) | ||
| container.initialized = true | ||
| }) | ||
| // 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) | ||
| return function selectGetter(): R { | ||
| if (!container.initialized) { | ||
| container.lastSourceValue = container.source() | ||
| container.lastSelectedValue = container.selectorFn(container.lastSourceValue) | ||
| container.valueState.set(container.lastSelectedValue) | ||
| container.initialized = true | ||
| } | ||
| return container.valueState() as R | ||
| } | ||
| } | ||
| // Helper for array updates | ||
@@ -594,7 +364,5 @@ const updateArrayItem = <V>(arr: unknown[], index: number, value: V): unknown[] => { | ||
| if (pathSegments.length === 1) { | ||
| // Simple array item update | ||
| return updateArrayItem(array, index, value) | ||
| } | ||
| // Nested path in array | ||
| const copy = [ | ||
@@ -606,6 +374,4 @@ ...array, | ||
| // 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) : {} | ||
@@ -624,6 +390,4 @@ } | ||
| ): 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 | ||
@@ -633,18 +397,13 @@ } | ||
| 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 = { | ||
@@ -657,5 +416,3 @@ ...obj, | ||
| // 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) { | ||
@@ -674,3 +431,2 @@ return value as unknown as O | ||
| // Delegate to specialized handlers based on data type | ||
| if (Array.isArray(obj)) { | ||
@@ -682,1 +438,118 @@ return updateArrayPath(obj, pathSegments, value) as unknown as O | ||
| } | ||
| /** | ||
| * Creates a lens for direct updates to nested properties of a state. | ||
| */ | ||
| const createLens = <T, K>(source: State<T>, accessor: (state: T) => K): State<K> => { | ||
| const container = { | ||
| accessor, | ||
| isUpdating: false, | ||
| lensState: null as unknown as State<K>, | ||
| originalSet: null as unknown as (value: K) => void, | ||
| path: [] as (string | number)[], | ||
| source, | ||
| } | ||
| const extractPath = (): (string | number)[] => { | ||
| const pathCollector: (string | number)[] = [] | ||
| const proxy = new Proxy( | ||
| {}, | ||
| { | ||
| get: (_: object, prop: string | symbol): unknown => { | ||
| if (typeof prop === 'string' || typeof prop === 'number') { | ||
| pathCollector.push(prop) | ||
| } | ||
| return proxy | ||
| }, | ||
| } | ||
| ) | ||
| try { | ||
| container.accessor(proxy as unknown as T) | ||
| } catch { | ||
| // Ignore errors, we're just collecting the path | ||
| } | ||
| return pathCollector | ||
| } | ||
| container.path = extractPath() | ||
| container.lensState = createState<K>(container.accessor(container.source())) | ||
| container.originalSet = container.lensState.set | ||
| createEffect(function lensEffect(): void { | ||
| if (container.isUpdating) { | ||
| return | ||
| } | ||
| container.isUpdating = true | ||
| try { | ||
| container.lensState.set(container.accessor(container.source())) | ||
| } finally { | ||
| container.isUpdating = false | ||
| } | ||
| }) | ||
| container.lensState.set = function lensSet(value: K): void { | ||
| if (container.isUpdating) { | ||
| return | ||
| } | ||
| container.isUpdating = true | ||
| try { | ||
| container.originalSet(value) | ||
| container.source.update((current: T): T => setValueAtPath(current, container.path, value)) | ||
| } finally { | ||
| container.isUpdating = false | ||
| } | ||
| } | ||
| container.lensState.update = function lensUpdate(fn: (value: K) => K): void { | ||
| container.lensState.set(fn(container.lensState())) | ||
| } | ||
| return container.lensState | ||
| } | ||
| /** | ||
| * Creates a read-only view of a state, hiding mutation methods. | ||
| */ | ||
| const createReadonlyState = | ||
| <T>(source: State<T>): ReadOnlyState<T> => | ||
| (): T => | ||
| source() | ||
| /** | ||
| * Creates a state with access control, returning a tuple of reader and writer. | ||
| */ | ||
| const createProtectedState = <T>( | ||
| initialValue: T, | ||
| equalityFn: (a: T, b: T) => boolean = Object.is | ||
| ): [ | ||
| ReadOnlyState<T>, | ||
| WriteableState<T>, | ||
| ] => { | ||
| const fullState = createState(initialValue, equalityFn) | ||
| return [ | ||
| (): T => createReadonlyState(fullState)(), | ||
| { | ||
| set: (value: T): void => fullState.set(value), | ||
| update: (fn: (value: T) => T): void => fullState.update(fn), | ||
| }, | ||
| ] | ||
| } | ||
| export { | ||
| createDerive as derive, | ||
| createEffect as effect, | ||
| createLens as lens, | ||
| createProtectedState as protectedState, | ||
| createReadonlyState as readonlyState, | ||
| createSelect as select, | ||
| createState as state, | ||
| executeBatch as batch, | ||
| } | ||
| export type { ReadOnlyState, State, Unsubscribe, WriteableState } |
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
25021
-28.01%482
-20.07%1
Infinity%