@nerdalytics/beacon 1000.2.5 → 1000.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
- # 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"/>
1
+ # 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"/>
2
2
 
3
3
  > Lightweight reactive state management for Node.js backends
4
4
 
5
5
  [![license:mit](https://flat.badgen.net/static/license/MIT/blue)](https://github.com/nerdalytics/beacon/blob/trunk/LICENSE)
6
6
  [![registry:npm:version](https://img.shields.io/npm/v/@nerdalytics/beacon.svg)](https://www.npmjs.com/package/@nerdalytics/beacon)
7
- [![Socket Badge](https://badge.socket.dev/npm/package/@nerdalytics/beacon/1000.2.4)](https://socket.dev/npm/package/@nerdalytics/beacon/overview/1000.2.4)
7
+ [![Socket Badge](https://badge.socket.dev/npm/package/@nerdalytics/beacon/1000.3.0)](https://socket.dev/npm/package/@nerdalytics/beacon/overview/1000.3.0)
8
8
 
9
9
  [![tech:nodejs](https://img.shields.io/badge/Node%20js-339933?style=for-the-badge&logo=nodedotjs&logoColor=white)](https://nodejs.org/)
10
10
  [![language:typescript](https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white)](https://typescriptlang.org/)
@@ -1,19 +1,20 @@
1
- export type Unsubscribe = () => void;
2
- export type ReadOnlyState<T> = () => T;
3
- export interface WriteableState<T> {
1
+ type Unsubscribe = () => void;
2
+ type ReadOnlyState<T> = () => T;
3
+ interface WriteableState<T> {
4
4
  set(value: T): void;
5
5
  update(fn: (value: T) => T): void;
6
6
  }
7
7
  declare const STATE_ID: unique symbol;
8
- export type State<T> = ReadOnlyState<T> & WriteableState<T> & {
8
+ type State<T> = ReadOnlyState<T> & WriteableState<T> & {
9
9
  [STATE_ID]?: symbol;
10
10
  };
11
- export declare const state: <T>(initialValue: T, equalityFn?: (a: T, b: T) => boolean) => State<T>;
12
- export declare const effect: (fn: () => void) => Unsubscribe;
13
- export declare const batch: <T>(fn: () => T) => T;
14
- export declare const derive: <T>(computeFn: () => T) => ReadOnlyState<T>;
15
- export declare const select: <T, R>(source: ReadOnlyState<T>, selectorFn: (state: T) => R, equalityFn?: (a: R, b: R) => boolean) => ReadOnlyState<R>;
16
- export declare const readonlyState: <T>(state: State<T>) => ReadOnlyState<T>;
17
- export declare const protectedState: <T>(initialValue: T, equalityFn?: (a: T, b: T) => boolean) => [ReadOnlyState<T>, WriteableState<T>];
18
- export declare const lens: <T, K>(source: State<T>, accessor: (state: T) => K) => State<K>;
19
- export {};
11
+ declare const createState: <T>(initialValue: T, equalityFn?: (a: T, b: T) => boolean) => State<T>;
12
+ declare const createEffect: (fn: () => void) => Unsubscribe;
13
+ declare const executeBatch: <T>(fn: () => T) => T;
14
+ declare const createDerive: <T>(computeFn: () => T) => ReadOnlyState<T>;
15
+ declare const createSelect: <T, R>(source: ReadOnlyState<T>, selectorFn: (state: T) => R, equalityFn?: (a: R, b: R) => boolean) => ReadOnlyState<R>;
16
+ declare const createLens: <T, K>(source: State<T>, accessor: (state: T) => K) => State<K>;
17
+ declare const createReadonlyState: <T>(source: State<T>) => ReadOnlyState<T>;
18
+ declare const createProtectedState: <T>(initialValue: T, equalityFn?: (a: T, b: T) => boolean) => [ReadOnlyState<T>, WriteableState<T>];
19
+ 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, };
20
+ export type { ReadOnlyState, State, Unsubscribe, WriteableState };
@@ -1 +1 @@
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};
1
+ 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};
package/package.json CHANGED
@@ -2,8 +2,8 @@
2
2
  "author": "Denny Trebbin (nerdalytics)",
3
3
  "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.",
4
4
  "devDependencies": {
5
- "@biomejs/biome": "2.3.13",
6
- "@types/node": "25.0.10",
5
+ "@biomejs/biome": "2.3.14",
6
+ "@types/node": "25.2.2",
7
7
  "npm-check-updates": "19.3.2",
8
8
  "typescript": "5.9.3",
9
9
  "uglify-js": "3.19.3"
@@ -50,13 +50,13 @@
50
50
  "license": "MIT",
51
51
  "main": "dist/src/index.min.js",
52
52
  "name": "@nerdalytics/beacon",
53
- "packageManager": "npm@11.8.0",
53
+ "packageManager": "npm@11.9.0",
54
54
  "repository": {
55
55
  "type": "git",
56
56
  "url": "git+https://github.com/nerdalytics/beacon.git"
57
57
  },
58
58
  "scripts": {
59
- "benchmark": "node scripts/benchmark.ts",
59
+ "benchmark": "node --expose-gc scripts/benchmark.ts",
60
60
  "benchmark:naiv": "node scripts/naiv-benchmark.ts",
61
61
  "build": "npm run build:lts",
62
62
  "build:lts": "tsc -p tsconfig.lts.json",
@@ -91,7 +91,8 @@
91
91
  "update-dependencies": "npx npm-check-updates --interactive --upgrade --removeRange",
92
92
  "update-performance-docs": "node --experimental-config-file=node.config.json scripts/update-performance-docs.ts"
93
93
  },
94
+ "sideEffects": false,
94
95
  "type": "module",
95
96
  "types": "dist/src/index.d.ts",
96
- "version": "1000.2.5"
97
+ "version": "1000.3.0"
97
98
  }
package/src/index.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  // Core types for reactive primitives
2
2
  type Subscriber = () => void
3
- export type Unsubscribe = () => void
4
- export type ReadOnlyState<T> = () => T
5
- export interface WriteableState<T> {
3
+ type Unsubscribe = () => void
4
+ type ReadOnlyState<T> = () => T
5
+ interface WriteableState<T> {
6
6
  set(value: T): void
7
7
  update(fn: (value: T) => T): void
8
8
  }
@@ -10,550 +10,320 @@ export interface WriteableState<T> {
10
10
  // Special symbol used for internal tracking
11
11
  const STATE_ID: unique symbol = Symbol('STATE_ID')
12
12
 
13
- export type State<T> = ReadOnlyState<T> &
13
+ type State<T> = ReadOnlyState<T> &
14
14
  WriteableState<T> & {
15
15
  [STATE_ID]?: symbol
16
16
  }
17
17
 
18
- /**
19
- * Creates a reactive state container with the provided initial value.
20
- */
21
- export const state = <T>(initialValue: T, equalityFn: (a: T, b: T) => boolean = Object.is): State<T> =>
22
- StateImpl.createState(initialValue, equalityFn)
18
+ // Module-level reactive state
19
+ let currentSubscriber: Subscriber | null = null
20
+ let pendingSubscribers: Set<Subscriber> = new Set<Subscriber>()
21
+ let isNotifying = false
22
+ let batchDepth = 0
23
+ let deferredEffectCreations: Subscriber[] = []
24
+ const activeSubscribers: Set<Subscriber> = new Set<Subscriber>()
25
+ const stateTracking: WeakMap<Subscriber, Set<symbol>> = new WeakMap<Subscriber, Set<symbol>>()
26
+ const subscriberDependencies: WeakMap<Subscriber, Set<Set<Subscriber>>> = new WeakMap<
27
+ Subscriber,
28
+ Set<Set<Subscriber>>
29
+ >()
30
+ const parentSubscriber: WeakMap<Subscriber, Subscriber> = new WeakMap<Subscriber, Subscriber>()
31
+ const childSubscribers: WeakMap<Subscriber, Set<Subscriber>> = new WeakMap<Subscriber, Set<Subscriber>>()
32
+
33
+ const notifySubscribers = (): void => {
34
+ if (isNotifying) {
35
+ return
36
+ }
23
37
 
24
- /**
25
- * Registers a function to run whenever its reactive dependencies change.
26
- */
27
- export const effect = (fn: () => void): Unsubscribe => StateImpl.createEffect(fn)
38
+ isNotifying = true
28
39
 
29
- /**
30
- * Groups multiple state updates to trigger effects only once at the end.
31
- */
32
- export const batch = <T>(fn: () => T): T => StateImpl.executeBatch(fn)
40
+ try {
41
+ while (pendingSubscribers.size > 0) {
42
+ const subscribers = pendingSubscribers
43
+ pendingSubscribers = new Set()
33
44
 
34
- /**
35
- * Creates a read-only computed value that updates when its dependencies change.
36
- */
37
- export const derive = <T>(computeFn: () => T): ReadOnlyState<T> => StateImpl.createDerive(computeFn)
45
+ for (const effect of subscribers) {
46
+ effect()
47
+ }
48
+ }
49
+ } finally {
50
+ isNotifying = false
51
+ }
52
+ }
38
53
 
39
- /**
40
- * Creates an efficient subscription to a subset of a state value.
41
- */
42
- export const select = <T, R>(
43
- source: ReadOnlyState<T>,
44
- selectorFn: (state: T) => R,
45
- equalityFn: (a: R, b: R) => boolean = Object.is
46
- ): ReadOnlyState<R> => StateImpl.createSelect(source, selectorFn, equalityFn)
54
+ const cleanupEffect = (effect: Subscriber): void => {
55
+ pendingSubscribers.delete(effect)
47
56
 
48
- /**
49
- * Creates a read-only view of a state, hiding mutation methods.
50
- */
51
- export const readonlyState =
52
- <T>(state: State<T>): ReadOnlyState<T> =>
53
- (): T =>
54
- state()
55
-
56
- /**
57
- * Creates a state with access control, returning a tuple of reader and writer.
58
- */
59
- export const protectedState = <T>(
60
- initialValue: T,
61
- equalityFn: (a: T, b: T) => boolean = Object.is
62
- ): [
63
- ReadOnlyState<T>,
64
- WriteableState<T>,
65
- ] => {
66
- const fullState = state(initialValue, equalityFn)
67
- return [
68
- (): T => readonlyState(fullState)(),
69
- {
70
- set: (value: T): void => fullState.set(value),
71
- update: (fn: (value: T) => T): void => fullState.update(fn),
72
- },
73
- ]
57
+ const deps = subscriberDependencies.get(effect)
58
+ if (deps) {
59
+ for (const subscribers of deps) {
60
+ subscribers.delete(effect)
61
+ }
62
+ deps.clear()
63
+ subscriberDependencies.delete(effect)
64
+ }
74
65
  }
75
66
 
76
67
  /**
77
- * Creates a lens for direct updates to nested properties of a state.
68
+ * Creates a reactive state container with the provided initial value.
78
69
  */
79
- export const lens = <T, K>(source: State<T>, accessor: (state: T) => K): State<K> =>
80
- StateImpl.createLens(source, accessor)
81
-
82
- class StateImpl<T> {
83
- // Static fields track global reactivity state - this centralized approach allows
84
- // for coordinated updates while maintaining individual state isolation
85
- private static currentSubscriber: Subscriber | null = null
86
- private static pendingSubscribers = new Set<Subscriber>()
87
- private static isNotifying = false
88
- private static batchDepth = 0
89
- private static deferredEffectCreations: Subscriber[] = []
90
- private static activeSubscribers = new Set<Subscriber>()
91
-
92
- // WeakMaps enable automatic garbage collection when subscribers are no
93
- // longer referenced, preventing memory leaks in long-running applications
94
- private static stateTracking = new WeakMap<Subscriber, Set<symbol>>()
95
- private static subscriberDependencies = new WeakMap<Subscriber, Set<Set<Subscriber>>>()
96
- private static parentSubscriber = new WeakMap<Subscriber, Subscriber>()
97
- private static childSubscribers = new WeakMap<Subscriber, Set<Subscriber>>()
98
-
99
- // Instance state - each state has unique subscribers and ID
100
- private value: T
101
- private subscribers = new Set<Subscriber>()
102
- private stateId = Symbol()
103
- private equalityFn: (a: T, b: T) => boolean
104
-
105
- constructor(initialValue: T, equalityFn: (a: T, b: T) => boolean = Object.is) {
106
- this.value = initialValue
107
- this.equalityFn = equalityFn
108
- }
109
-
110
- /**
111
- * Creates a reactive state container with the provided initial value.
112
- * Implementation of the public 'state' function.
113
- */
114
- static createState = <T>(initialValue: T, equalityFn: (a: T, b: T) => boolean = Object.is): State<T> => {
115
- const instance = new StateImpl<T>(initialValue, equalityFn)
116
- const get = (): T => instance.get()
117
- get.set = (value: T): void => instance.set(value)
118
- get.update = (fn: (currentValue: T) => T): void => instance.update(fn)
119
- get[STATE_ID] = instance.stateId
120
- return get as State<T>
121
- }
70
+ const createState = <T>(initialValue: T, equalityFn: (a: T, b: T) => boolean = Object.is): State<T> => {
71
+ let value = initialValue
72
+ const subscribers = new Set<Subscriber>()
73
+ const stateId = Symbol()
122
74
 
123
- // Auto-tracks dependencies when called within effects, creating a fine-grained
124
- // reactivity graph that only updates affected components
125
- get = (): T => {
126
- const currentEffect = StateImpl.currentSubscriber
75
+ const get = (): T => {
76
+ const currentEffect = currentSubscriber
127
77
  if (currentEffect) {
128
- // Add this effect to subscribers for future notification
129
- this.subscribers.add(currentEffect)
78
+ subscribers.add(currentEffect)
130
79
 
131
- // Maintain bidirectional dependency tracking to enable precise cleanup
132
- // when effects are unsubscribed, preventing memory leaks
133
- let dependencies = StateImpl.subscriberDependencies.get(currentEffect)
80
+ let dependencies = subscriberDependencies.get(currentEffect)
134
81
  if (!dependencies) {
135
82
  dependencies = new Set()
136
- StateImpl.subscriberDependencies.set(currentEffect, dependencies)
83
+ subscriberDependencies.set(currentEffect, dependencies)
137
84
  }
138
- dependencies.add(this.subscribers)
85
+ dependencies.add(subscribers)
139
86
 
140
- // Track read states to detect direct cyclical dependencies that
141
- // could cause infinite loops
142
- let readStates = StateImpl.stateTracking.get(currentEffect)
87
+ let readStates = stateTracking.get(currentEffect)
143
88
  if (!readStates) {
144
89
  readStates = new Set()
145
- StateImpl.stateTracking.set(currentEffect, readStates)
90
+ stateTracking.set(currentEffect, readStates)
146
91
  }
147
- readStates.add(this.stateId)
92
+ readStates.add(stateId)
148
93
  }
149
- return this.value
94
+ return value
150
95
  }
151
96
 
152
- // Handles value updates with built-in optimizations and safeguards
153
- set = (newValue: T): void => {
154
- // Skip updates for unchanged values to prevent redundant effect executions
155
- if (this.equalityFn(this.value, newValue)) {
97
+ get.set = (newValue: T): void => {
98
+ if (equalityFn(value, newValue)) {
156
99
  return
157
100
  }
158
101
 
159
- // Infinite loop detection prevents direct self-mutation within effects,
160
- // while allowing nested effect patterns that would otherwise appear cyclical
161
- const effect = StateImpl.currentSubscriber
102
+ const effect = currentSubscriber
162
103
  if (effect) {
163
- const states = StateImpl.stateTracking.get(effect)
164
- if (states?.has(this.stateId) && !StateImpl.parentSubscriber.get(effect)) {
104
+ const states = stateTracking.get(effect)
105
+ if (states?.has(stateId) && !parentSubscriber.get(effect)) {
165
106
  throw new Error('Infinite loop detected: effect() cannot update a state() it depends on!')
166
107
  }
167
108
  }
168
109
 
169
- this.value = newValue
110
+ value = newValue
170
111
 
171
- // Skip updates when there are no subscribers, avoiding unnecessary processing
172
- if (this.subscribers.size === 0) {
112
+ if (subscribers.size === 0) {
173
113
  return
174
114
  }
175
115
 
176
- // Queue notifications instead of executing immediately to support batch operations
177
- // and prevent redundant effect runs
178
- for (const sub of this.subscribers) {
179
- StateImpl.pendingSubscribers.add(sub)
116
+ for (const sub of subscribers) {
117
+ pendingSubscribers.add(sub)
180
118
  }
181
119
 
182
- // Immediate execution outside of batches, deferred execution inside batches
183
- if (StateImpl.batchDepth === 0 && !StateImpl.isNotifying) {
184
- StateImpl.notifySubscribers()
120
+ if (batchDepth === 0 && !isNotifying) {
121
+ notifySubscribers()
185
122
  }
186
123
  }
187
124
 
188
- update = (fn: (currentValue: T) => T): void => {
189
- this.set(fn(this.value))
125
+ get.update = (fn: (currentValue: T) => T): void => {
126
+ get.set(fn(value))
190
127
  }
191
128
 
192
- /**
193
- * Registers a function to run whenever its reactive dependencies change.
194
- * Implementation of the public 'effect' function.
195
- */
196
- static createEffect = (fn: () => void): Unsubscribe => {
197
- const runEffect = (): void => {
198
- // Prevent re-entrance to avoid cascade updates during effect execution
199
- if (StateImpl.activeSubscribers.has(runEffect)) {
200
- return
201
- }
202
-
203
- StateImpl.activeSubscribers.add(runEffect)
204
- const parentEffect = StateImpl.currentSubscriber
205
-
206
- try {
207
- // Clean existing subscriptions before running to ensure only
208
- // currently accessed states are tracked as dependencies
209
- StateImpl.cleanupEffect(runEffect)
210
-
211
- // Set current context for automatic dependency tracking
212
- StateImpl.currentSubscriber = runEffect
213
- StateImpl.stateTracking.set(runEffect, new Set())
214
-
215
- // Track parent-child relationships to handle nested effects correctly
216
- // and enable hierarchical cleanup later
217
- if (parentEffect) {
218
- StateImpl.parentSubscriber.set(runEffect, parentEffect)
219
- let children = StateImpl.childSubscribers.get(parentEffect)
220
- if (!children) {
221
- children = new Set()
222
- StateImpl.childSubscribers.set(parentEffect, children)
223
- }
224
- children.add(runEffect)
225
- }
129
+ get[STATE_ID] = stateId
130
+ return get as State<T>
131
+ }
226
132
 
227
- // Execute the effect function, which will auto-track dependencies
228
- fn()
229
- } finally {
230
- // Restore previous context when done
231
- StateImpl.currentSubscriber = parentEffect
232
- StateImpl.activeSubscribers.delete(runEffect)
233
- }
133
+ /**
134
+ * Registers a function to run whenever its reactive dependencies change.
135
+ */
136
+ const createEffect = (fn: () => void): Unsubscribe => {
137
+ const runEffect = (): void => {
138
+ if (activeSubscribers.has(runEffect)) {
139
+ return
234
140
  }
235
141
 
236
- // Run immediately unless we're in a batch operation
237
- if (StateImpl.batchDepth === 0) {
238
- runEffect()
239
- } else {
240
- // Still track parent-child relationship even when deferred,
241
- // ensuring proper hierarchical cleanup later
242
- if (StateImpl.currentSubscriber) {
243
- const parent = StateImpl.currentSubscriber
244
- StateImpl.parentSubscriber.set(runEffect, parent)
245
- let children = StateImpl.childSubscribers.get(parent)
142
+ activeSubscribers.add(runEffect)
143
+ const parentEffect = currentSubscriber
144
+
145
+ try {
146
+ cleanupEffect(runEffect)
147
+
148
+ currentSubscriber = runEffect
149
+ stateTracking.set(runEffect, new Set())
150
+
151
+ if (parentEffect) {
152
+ parentSubscriber.set(runEffect, parentEffect)
153
+ let children = childSubscribers.get(parentEffect)
246
154
  if (!children) {
247
155
  children = new Set()
248
- StateImpl.childSubscribers.set(parent, children)
156
+ childSubscribers.set(parentEffect, children)
249
157
  }
250
158
  children.add(runEffect)
251
159
  }
252
160
 
253
- // Queue for execution when batch completes
254
- StateImpl.deferredEffectCreations.push(runEffect)
255
- }
256
-
257
- // Return cleanup function to properly disconnect from reactivity graph
258
- return (): void => {
259
- // Remove from dependency tracking to stop future notifications
260
- StateImpl.cleanupEffect(runEffect)
261
- StateImpl.pendingSubscribers.delete(runEffect)
262
- StateImpl.activeSubscribers.delete(runEffect)
263
- StateImpl.stateTracking.delete(runEffect)
264
-
265
- // Clean up parent-child relationship bidirectionally
266
- const parent = StateImpl.parentSubscriber.get(runEffect)
267
- if (parent) {
268
- const siblings = StateImpl.childSubscribers.get(parent)
269
- if (siblings) {
270
- siblings.delete(runEffect)
271
- }
272
- }
273
- StateImpl.parentSubscriber.delete(runEffect)
274
-
275
- // Recursively clean up child effects to prevent memory leaks in
276
- // nested effect scenarios
277
- const children = StateImpl.childSubscribers.get(runEffect)
278
- if (children) {
279
- for (const child of children) {
280
- StateImpl.cleanupEffect(child)
281
- }
282
- children.clear()
283
- StateImpl.childSubscribers.delete(runEffect)
284
- }
285
- }
286
- }
287
-
288
- /**
289
- * Groups multiple state updates to trigger effects only once at the end.
290
- * Implementation of the public 'batch' function.
291
- */
292
- static executeBatch = <T>(fn: () => T): T => {
293
- // Increment depth counter to handle nested batches correctly
294
- StateImpl.batchDepth++
295
- try {
296
- return fn()
297
- } catch (error: unknown) {
298
- // Clean up on error to prevent stale subscribers from executing
299
- // and potentially causing cascading errors
300
- if (StateImpl.batchDepth === 1) {
301
- StateImpl.pendingSubscribers.clear()
302
- StateImpl.deferredEffectCreations.length = 0
303
- }
304
- throw error
161
+ fn()
305
162
  } finally {
306
- StateImpl.batchDepth--
307
-
308
- // Only process effects when exiting the outermost batch,
309
- // maintaining proper execution order while avoiding redundant runs
310
- if (StateImpl.batchDepth === 0) {
311
- // Process effects created during the batch
312
- if (StateImpl.deferredEffectCreations.length > 0) {
313
- // Swap reference instead of spread copy to avoid array allocation
314
- const effectsToRun = StateImpl.deferredEffectCreations
315
- StateImpl.deferredEffectCreations = []
316
- for (const effect of effectsToRun) {
317
- effect()
318
- }
319
- }
320
-
321
- // Process state updates that occurred during the batch
322
- if (StateImpl.pendingSubscribers.size > 0 && !StateImpl.isNotifying) {
323
- StateImpl.notifySubscribers()
324
- }
325
- }
163
+ currentSubscriber = parentEffect
164
+ activeSubscribers.delete(runEffect)
326
165
  }
327
166
  }
328
167
 
329
- /**
330
- * Creates a read-only computed value that updates when its dependencies change.
331
- * Implementation of the public 'derive' function.
332
- */
333
- static createDerive = <T>(computeFn: () => T): ReadOnlyState<T> => {
334
- // Create a container to hold state and minimize closure captures
335
- const container = {
336
- cachedValue: undefined as unknown as T,
337
- computeFn,
338
- initialized: false,
339
- valueState: StateImpl.createState<T | undefined>(undefined),
168
+ if (batchDepth === 0) {
169
+ runEffect()
170
+ } else {
171
+ if (currentSubscriber) {
172
+ const parent = currentSubscriber
173
+ parentSubscriber.set(runEffect, parent)
174
+ let children = childSubscribers.get(parent)
175
+ if (!children) {
176
+ children = new Set()
177
+ childSubscribers.set(parent, children)
178
+ }
179
+ children.add(runEffect)
340
180
  }
341
181
 
342
- // Internal effect automatically tracks dependencies and updates the derived value
343
- StateImpl.createEffect(function deriveEffect(): void {
344
- const newValue = container.computeFn()
182
+ deferredEffectCreations.push(runEffect)
183
+ }
345
184
 
346
- // Only update if the value actually changed to preserve referential equality
347
- // and prevent unnecessary downstream updates
348
- if (!(container.initialized && Object.is(container.cachedValue, newValue))) {
349
- container.cachedValue = newValue
350
- container.valueState.set(newValue)
185
+ return (): void => {
186
+ cleanupEffect(runEffect)
187
+ pendingSubscribers.delete(runEffect)
188
+ activeSubscribers.delete(runEffect)
189
+ stateTracking.delete(runEffect)
190
+
191
+ const parent = parentSubscriber.get(runEffect)
192
+ if (parent) {
193
+ const siblings = childSubscribers.get(parent)
194
+ if (siblings) {
195
+ siblings.delete(runEffect)
351
196
  }
197
+ }
198
+ parentSubscriber.delete(runEffect)
352
199
 
353
- container.initialized = true
354
- })
355
-
356
- // Return function with lazy initialization - ensures value is available
357
- // even when accessed before its dependencies have had a chance to update
358
- return function deriveGetter(): T {
359
- if (!container.initialized) {
360
- container.cachedValue = container.computeFn()
361
- container.initialized = true
362
- container.valueState.set(container.cachedValue)
200
+ const children = childSubscribers.get(runEffect)
201
+ if (children) {
202
+ for (const child of children) {
203
+ cleanupEffect(child)
363
204
  }
364
- return container.valueState() as T
205
+ children.clear()
206
+ childSubscribers.delete(runEffect)
365
207
  }
366
208
  }
209
+ }
367
210
 
368
- /**
369
- * Creates an efficient subscription to a subset of a state value.
370
- * Implementation of the public 'select' function.
371
- */
372
- static createSelect = <T, R>(
373
- source: ReadOnlyState<T>,
374
- selectorFn: (state: T) => R,
375
- equalityFn: (a: R, b: R) => boolean = Object.is
376
- ): ReadOnlyState<R> => {
377
- // Create a container to hold state and minimize closure captures
378
- const container = {
379
- equalityFn,
380
- initialized: false,
381
- lastSelectedValue: undefined as R | undefined,
382
- lastSourceValue: undefined as T | undefined,
383
- selectorFn,
384
- source,
385
- valueState: StateImpl.createState<R | undefined>(undefined),
211
+ /**
212
+ * Groups multiple state updates to trigger effects only once at the end.
213
+ */
214
+ const executeBatch = <T>(fn: () => T): T => {
215
+ batchDepth++
216
+ try {
217
+ return fn()
218
+ } catch (error: unknown) {
219
+ if (batchDepth === 1) {
220
+ pendingSubscribers.clear()
221
+ deferredEffectCreations.length = 0
386
222
  }
387
-
388
- // Internal effect to track the source and update only when needed
389
- StateImpl.createEffect(function selectEffect(): void {
390
- const sourceValue = container.source()
391
-
392
- // Skip computation if source reference hasn't changed
393
- if (container.initialized && Object.is(container.lastSourceValue, sourceValue)) {
394
- return
395
- }
396
-
397
- container.lastSourceValue = sourceValue
398
- const newSelectedValue = container.selectorFn(sourceValue)
399
-
400
- // Use custom equality function to determine if value semantically changed,
401
- // allowing for deep equality comparisons with complex objects
402
- if (
403
- container.initialized &&
404
- container.lastSelectedValue !== undefined &&
405
- container.equalityFn(container.lastSelectedValue, newSelectedValue)
406
- ) {
407
- return
223
+ throw error
224
+ } finally {
225
+ batchDepth--
226
+
227
+ if (batchDepth === 0) {
228
+ if (deferredEffectCreations.length > 0) {
229
+ const effectsToRun = deferredEffectCreations
230
+ deferredEffectCreations = []
231
+ for (const effect of effectsToRun) {
232
+ effect()
233
+ }
408
234
  }
409
235
 
410
- // Update cache and notify subscribers due the value has changed
411
- container.lastSelectedValue = newSelectedValue
412
- container.valueState.set(newSelectedValue)
413
- container.initialized = true
414
- })
415
-
416
- // Return function with eager initialization capability
417
- return function selectGetter(): R {
418
- if (!container.initialized) {
419
- container.lastSourceValue = container.source()
420
- container.lastSelectedValue = container.selectorFn(container.lastSourceValue)
421
- container.valueState.set(container.lastSelectedValue)
422
- container.initialized = true
236
+ if (pendingSubscribers.size > 0 && !isNotifying) {
237
+ notifySubscribers()
423
238
  }
424
- return container.valueState() as R
425
239
  }
426
240
  }
241
+ }
427
242
 
428
- /**
429
- * Creates a lens for direct updates to nested properties of a state.
430
- * Implementation of the public 'lens' function.
431
- */
432
- static createLens = <T, K>(source: State<T>, accessor: (state: T) => K): State<K> => {
433
- // Create a container to hold lens state and minimize closure captures
434
- const container = {
435
- accessor,
436
- isUpdating: false,
437
- lensState: null as unknown as State<K>,
438
- originalSet: null as unknown as (value: K) => void,
439
- path: [] as (string | number)[],
440
- source,
441
- }
442
-
443
- // Extract the property path once during lens creation
444
- const extractPath = (): (string | number)[] => {
445
- const pathCollector: (string | number)[] = []
446
- const proxy = new Proxy(
447
- {},
448
- {
449
- get: (_: object, prop: string | symbol): unknown => {
450
- if (typeof prop === 'string' || typeof prop === 'number') {
451
- pathCollector.push(prop)
452
- }
453
- return proxy
454
- },
455
- }
456
- )
243
+ /**
244
+ * Creates a read-only computed value that updates when its dependencies change.
245
+ */
246
+ const createDerive = <T>(computeFn: () => T): ReadOnlyState<T> => {
247
+ const container = {
248
+ cachedValue: undefined as unknown as T,
249
+ computeFn,
250
+ initialized: false,
251
+ valueState: createState<T | undefined>(undefined),
252
+ }
457
253
 
458
- try {
459
- container.accessor(proxy as unknown as T)
460
- } catch {
461
- // Ignore errors, we're just collecting the path
462
- }
254
+ createEffect(function deriveEffect(): void {
255
+ const newValue = container.computeFn()
463
256
 
464
- return pathCollector
257
+ if (!(container.initialized && Object.is(container.cachedValue, newValue))) {
258
+ container.cachedValue = newValue
259
+ container.valueState.set(newValue)
465
260
  }
466
261
 
467
- // Capture the path once
468
- container.path = extractPath()
469
-
470
- // Create a state with the initial value from the source
471
- container.lensState = StateImpl.createState<K>(container.accessor(container.source()))
472
- container.originalSet = container.lensState.set
473
-
474
- // Set up an effect to sync from source to lens
475
- StateImpl.createEffect(function lensEffect(): void {
476
- if (container.isUpdating) {
477
- return
478
- }
479
-
480
- container.isUpdating = true
481
- try {
482
- container.lensState.set(container.accessor(container.source()))
483
- } finally {
484
- container.isUpdating = false
485
- }
486
- })
487
-
488
- // Override the lens state's set method to update the source
489
- container.lensState.set = function lensSet(value: K): void {
490
- if (container.isUpdating) {
491
- return
492
- }
262
+ container.initialized = true
263
+ })
493
264
 
494
- container.isUpdating = true
495
- try {
496
- // Update lens state
497
- container.originalSet(value)
498
-
499
- // Update source by modifying the value at path
500
- container.source.update((current: T): T => setValueAtPath(current, container.path, value))
501
- } finally {
502
- container.isUpdating = false
503
- }
504
- }
505
-
506
- // Add update method for completeness
507
- container.lensState.update = function lensUpdate(fn: (value: K) => K): void {
508
- container.lensState.set(fn(container.lensState()))
265
+ return function deriveGetter(): T {
266
+ if (!container.initialized) {
267
+ container.cachedValue = container.computeFn()
268
+ container.initialized = true
269
+ container.valueState.set(container.cachedValue)
509
270
  }
271
+ return container.valueState() as T
272
+ }
273
+ }
510
274
 
511
- return container.lensState
275
+ /**
276
+ * Creates an efficient subscription to a subset of a state value.
277
+ */
278
+ const createSelect = <T, R>(
279
+ source: ReadOnlyState<T>,
280
+ selectorFn: (state: T) => R,
281
+ equalityFn: (a: R, b: R) => boolean = Object.is
282
+ ): ReadOnlyState<R> => {
283
+ const container = {
284
+ equalityFn,
285
+ initialized: false,
286
+ lastSelectedValue: undefined as R | undefined,
287
+ lastSourceValue: undefined as T | undefined,
288
+ selectorFn,
289
+ source,
290
+ valueState: createState<R | undefined>(undefined),
512
291
  }
513
292
 
514
- // Processes queued subscriber notifications in a controlled, non-reentrant way
515
- private static notifySubscribers = (): void => {
516
- // Prevent reentrance to avoid cascading notification loops when
517
- // effects trigger further state changes
518
- if (StateImpl.isNotifying) {
293
+ createEffect(function selectEffect(): void {
294
+ const sourceValue = container.source()
295
+
296
+ if (container.initialized && Object.is(container.lastSourceValue, sourceValue)) {
519
297
  return
520
298
  }
521
299
 
522
- StateImpl.isNotifying = true
300
+ container.lastSourceValue = sourceValue
301
+ const newSelectedValue = container.selectorFn(sourceValue)
523
302
 
524
- try {
525
- // Process all pending effects in batches for better perf,
526
- // ensuring topological execution order is maintained
527
- while (StateImpl.pendingSubscribers.size > 0) {
528
- // Swap with empty Set instead of Array.from() to avoid array allocation
529
- const subscribers = StateImpl.pendingSubscribers
530
- StateImpl.pendingSubscribers = new Set()
531
-
532
- for (const effect of subscribers) {
533
- effect()
534
- }
535
- }
536
- } finally {
537
- StateImpl.isNotifying = false
303
+ if (
304
+ container.initialized &&
305
+ container.lastSelectedValue !== undefined &&
306
+ container.equalityFn(container.lastSelectedValue, newSelectedValue)
307
+ ) {
308
+ return
538
309
  }
539
- }
540
310
 
541
- // Removes effect from dependency tracking to prevent memory leaks
542
- private static cleanupEffect = (effect: Subscriber): void => {
543
- // Remove from execution queue to prevent stale updates
544
- StateImpl.pendingSubscribers.delete(effect)
311
+ container.lastSelectedValue = newSelectedValue
312
+ container.valueState.set(newSelectedValue)
313
+ container.initialized = true
314
+ })
545
315
 
546
- // Remove bidirectional dependency references to prevent memory leaks
547
- const deps = StateImpl.subscriberDependencies.get(effect)
548
- if (deps) {
549
- for (const subscribers of deps) {
550
- subscribers.delete(effect)
551
- }
552
- deps.clear()
553
- StateImpl.subscriberDependencies.delete(effect)
316
+ return function selectGetter(): R {
317
+ if (!container.initialized) {
318
+ container.lastSourceValue = container.source()
319
+ container.lastSelectedValue = container.selectorFn(container.lastSourceValue)
320
+ container.valueState.set(container.lastSelectedValue)
321
+ container.initialized = true
554
322
  }
323
+ return container.valueState() as R
555
324
  }
556
325
  }
326
+
557
327
  // Helper for array updates
558
328
  const updateArrayItem = <V>(arr: unknown[], index: number, value: V): unknown[] => {
559
329
  const copy = [
@@ -587,21 +357,17 @@ const updateArrayPath = <V>(array: unknown[], pathSegments: (string | number)[],
587
357
  const index = Number(pathSegments[0])
588
358
 
589
359
  if (pathSegments.length === 1) {
590
- // Simple array item update
591
360
  return updateArrayItem(array, index, value)
592
361
  }
593
362
 
594
- // Nested path in array
595
363
  const copy = [
596
364
  ...array,
597
365
  ]
598
366
  const nextPathSegments = pathSegments.slice(1)
599
367
  const nextKey = nextPathSegments[0]
600
368
 
601
- // For null/undefined values in arrays, create appropriate containers
602
369
  let nextValue = array[index]
603
370
  if (nextValue === undefined || nextValue === null) {
604
- // Use empty object as default if nextKey is undefined
605
371
  nextValue = nextKey !== undefined ? createContainer(nextKey) : {}
606
372
  }
607
373
 
@@ -615,30 +381,23 @@ const updateObjectPath = <V>(
615
381
  pathSegments: (string | number)[],
616
382
  value: V
617
383
  ): Record<string | number, unknown> => {
618
- // Ensure we have a valid key
619
384
  const currentKey = pathSegments[0]
620
385
  if (currentKey === undefined) {
621
- // This shouldn't happen given our checks in the main function
622
386
  return obj
623
387
  }
624
388
 
625
389
  if (pathSegments.length === 1) {
626
- // Simple object property update
627
390
  return updateShallowProperty(obj, currentKey, value)
628
391
  }
629
392
 
630
- // Nested path in object
631
393
  const nextPathSegments = pathSegments.slice(1)
632
394
  const nextKey = nextPathSegments[0]
633
395
 
634
- // For null/undefined values, create appropriate containers
635
396
  let currentValue = obj[currentKey]
636
397
  if (currentValue === undefined || currentValue === null) {
637
- // Use empty object as default if nextKey is undefined
638
398
  currentValue = nextKey !== undefined ? createContainer(nextKey) : {}
639
399
  }
640
400
 
641
- // Create new object with updated property
642
401
  const result = {
643
402
  ...obj,
644
403
  }
@@ -646,9 +405,7 @@ const updateObjectPath = <V>(
646
405
  return result
647
406
  }
648
407
 
649
- // Simplified function to update a nested value at a path
650
408
  const setValueAtPath = <V, O>(obj: O, pathSegments: (string | number)[], value: V): O => {
651
- // Handle base cases
652
409
  if (pathSegments.length === 0) {
653
410
  return value as unknown as O
654
411
  }
@@ -662,10 +419,126 @@ const setValueAtPath = <V, O>(obj: O, pathSegments: (string | number)[], value:
662
419
  return obj
663
420
  }
664
421
 
665
- // Delegate to specialized handlers based on data type
666
422
  if (Array.isArray(obj)) {
667
423
  return updateArrayPath(obj, pathSegments, value) as unknown as O
668
424
  }
669
425
 
670
426
  return updateObjectPath(obj as Record<string | number, unknown>, pathSegments, value) as unknown as O
671
427
  }
428
+
429
+ /**
430
+ * Creates a lens for direct updates to nested properties of a state.
431
+ */
432
+ const createLens = <T, K>(source: State<T>, accessor: (state: T) => K): State<K> => {
433
+ const container = {
434
+ accessor,
435
+ isUpdating: false,
436
+ lensState: null as unknown as State<K>,
437
+ originalSet: null as unknown as (value: K) => void,
438
+ path: [] as (string | number)[],
439
+ source,
440
+ }
441
+
442
+ const extractPath = (): (string | number)[] => {
443
+ const pathCollector: (string | number)[] = []
444
+ const proxy = new Proxy(
445
+ {},
446
+ {
447
+ get: (_: object, prop: string | symbol): unknown => {
448
+ if (typeof prop === 'string' || typeof prop === 'number') {
449
+ pathCollector.push(prop)
450
+ }
451
+ return proxy
452
+ },
453
+ }
454
+ )
455
+
456
+ try {
457
+ container.accessor(proxy as unknown as T)
458
+ } catch {
459
+ // Ignore errors, we're just collecting the path
460
+ }
461
+
462
+ return pathCollector
463
+ }
464
+
465
+ container.path = extractPath()
466
+
467
+ container.lensState = createState<K>(container.accessor(container.source()))
468
+ container.originalSet = container.lensState.set
469
+
470
+ createEffect(function lensEffect(): void {
471
+ if (container.isUpdating) {
472
+ return
473
+ }
474
+
475
+ container.isUpdating = true
476
+ try {
477
+ container.lensState.set(container.accessor(container.source()))
478
+ } finally {
479
+ container.isUpdating = false
480
+ }
481
+ })
482
+
483
+ container.lensState.set = function lensSet(value: K): void {
484
+ if (container.isUpdating) {
485
+ return
486
+ }
487
+
488
+ container.isUpdating = true
489
+ try {
490
+ container.originalSet(value)
491
+
492
+ container.source.update((current: T): T => setValueAtPath(current, container.path, value))
493
+ } finally {
494
+ container.isUpdating = false
495
+ }
496
+ }
497
+
498
+ container.lensState.update = function lensUpdate(fn: (value: K) => K): void {
499
+ container.lensState.set(fn(container.lensState()))
500
+ }
501
+
502
+ return container.lensState
503
+ }
504
+
505
+ /**
506
+ * Creates a read-only view of a state, hiding mutation methods.
507
+ */
508
+ const createReadonlyState =
509
+ <T>(source: State<T>): ReadOnlyState<T> =>
510
+ (): T =>
511
+ source()
512
+
513
+ /**
514
+ * Creates a state with access control, returning a tuple of reader and writer.
515
+ */
516
+ const createProtectedState = <T>(
517
+ initialValue: T,
518
+ equalityFn: (a: T, b: T) => boolean = Object.is
519
+ ): [
520
+ ReadOnlyState<T>,
521
+ WriteableState<T>,
522
+ ] => {
523
+ const fullState = createState(initialValue, equalityFn)
524
+ return [
525
+ (): T => createReadonlyState(fullState)(),
526
+ {
527
+ set: (value: T): void => fullState.set(value),
528
+ update: (fn: (value: T) => T): void => fullState.update(fn),
529
+ },
530
+ ]
531
+ }
532
+
533
+ export {
534
+ createDerive as derive,
535
+ createEffect as effect,
536
+ createLens as lens,
537
+ createProtectedState as protectedState,
538
+ createReadonlyState as readonlyState,
539
+ createSelect as select,
540
+ createState as state,
541
+ executeBatch as batch,
542
+ }
543
+
544
+ export type { ReadOnlyState, State, Unsubscribe, WriteableState }