@nerdalytics/beacon 1000.2.5 → 1000.3.1

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/)
@@ -37,7 +37,7 @@ count.set(5);
37
37
  ## Documentation
38
38
 
39
39
  Full documentation, API reference, and examples available at:
40
- **[github.com/nerdalytics/beacon](https://github.com/nerdalytics/beacon)**
40
+ **[nerdalytics.github.io/beacon](https://nerdalytics.github.io/beacon/)**
41
41
 
42
42
  ## License
43
43
 
@@ -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 type { ReadOnlyState, State, Unsubscribe, WriteableState };
20
+ 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, };
@@ -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 r=Symbol("STATE_ID"),f=null,u=new Set,s=!1,d=0,a=[],l=new Set,o=new WeakMap,v=new WeakMap,c=new WeakMap,i=new WeakMap,y=(e,t,r)=>{let n=e.get(t);return n||(n=r(),e.set(t,n)),n},p=()=>{if(!s){s=!0;try{for(;0<u.size;){var e,t=u;u=new Set;for(e of t)e()}}finally{s=!1}}},w=e=>{u.delete(e);var t=v.get(e);if(t){for(var r of t)r.delete(e);t.clear(),v.delete(e)}},g=e=>{w(e),l.delete(e),o.delete(e);var t=c.get(e),t=(t&&(t=i.get(t))&&t.delete(e),c.delete(e),i.get(e));if(t){for(var r of t)g(r);t.clear(),i.delete(e)}},h=(e,n=Object.is)=>{let a=e,l=new Set,i=Symbol(),t=()=>{var e=f;return e&&(l.add(e),y(v,e,()=>new Set).add(l),y(o,e,()=>new Set).add(i)),a};return t.set=e=>{if(!n(a,e)){var t=f;if(t)if(o.get(t)?.has(i)&&!c.get(t))throw new Error("Infinite loop detected: effect() cannot update a state() it depends on!");if(a=e,0!==l.size){for(var r of l)u.add(r);0!==d||s||p()}}},t.update=e=>{t.set(e(a))},t[r]=i,t},S=r=>{let n=()=>{if(!l.has(n)){l.add(n);var e=f;try{w(n),f=n;var t=o.get(n);t?t.clear():o.set(n,new Set),e&&(c.set(n,e),y(i,e,()=>new Set).add(n)),r()}finally{f=e,l.delete(n)}}};var e;return 0===d?n():(f&&(e=f,c.set(n,e),y(i,e,()=>new Set).add(n)),a.push(n)),()=>{g(n)}};var e=e=>{d++;try{return e()}catch(e){throw 1===d&&(u.clear(),a.length=0),e}finally{if(0===--d){if(0<a.length){var t,e=a;a=[];for(t of e)t()}0<u.size&&!s&&p()}}},t=t=>{let r=void 0,n=!1,a=h(void 0);return S(function(){var e=t();n&&Object.is(r,e)||(r=e,a.set(e)),n=!0}),function(){return n||(r=t(),n=!0,a.set(r)),a()}},n=(t,r,n=Object.is)=>{let a=!1,l,i,f=h(void 0);return S(function(){var e=t();a&&Object.is(i,e)||(i=e,e=r(e),a&&void 0!==l&&n(l,e))||(l=e,f.set(e),a=!0)}),function(){return a||(i=t(),l=r(i),f.set(l),a=!0),f()}};let b=(e,t,r)=>{e=[...e];return e[t]=r,e},m=(e,t,r)=>{e={...e};return e[t]=r,e},j=e=>"number"==typeof e||!Number.isNaN(Number(e))?[]:{},N=(n,a,l,i)=>{if(l>=a.length)return i;if(null==n)return N({},a,l,i);var f=a[l];if(void 0===f)return n;if(Array.isArray(n)){var u=Number(f);if(l===a.length-1)return b(n,u,i);var s=[...n];let e=l+1,t=a[e],r=n[u];return null==r&&(r=void 0===t?{}:j(t)),s[u]=N(r,a,e,i),s}u=n;if(l===a.length-1)return m(u,f,i);let e=l+1,t=a[e],r=u[f];null==r&&(r=void 0===t?{}:j(t));s={...u};return s[f]=N(r,a,e,i),s};var O=(e,t)=>{let r=!1;let n=(()=>{let r=[],n=new Proxy({},{get:(e,t)=>("string"!=typeof t&&"number"!=typeof t||r.push(t),n)});try{t(n)}catch{}return r})(),a=h(t(e())),l=a.set;return S(function(){if(!r){r=!0;try{a.set(t(e()))}finally{r=!1}}}),a.set=function(t){if(!r){r=!0;try{l(t),e.update(e=>N(e,n,0,t))}finally{r=!1}}},a.update=function(e){a.set(e(a()))},a};let k=e=>()=>e();var M=(e,t=Object.is)=>{let r=h(e,t);return[k(r),{set:e=>r.set(e),update:e=>r.update(e)}]};export{t as derive,S as effect,O as lens,M as protectedState,k as readonlyState,n as select,h 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"
@@ -13,15 +13,15 @@
13
13
  },
14
14
  "exports": {
15
15
  ".": {
16
+ "types": "./dist/src/index.d.ts",
17
+ "typescript": "./src/index.ts",
16
18
  "import": {
17
- "default": "./dist/src/index.min.js",
18
- "types": "./dist/src/index.d.ts"
19
+ "types": "./dist/src/index.d.ts",
20
+ "default": "./dist/src/index.min.js"
19
21
  },
20
22
  "require": {
21
23
  "default": "./dist/src/index.min.js"
22
- },
23
- "types": "./dist/src/index.d.ts",
24
- "typescript": "./src/index.ts"
24
+ }
25
25
  }
26
26
  },
27
27
  "files": [
@@ -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 -R 3",
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.1"
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,290 @@ 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)
23
-
24
- /**
25
- * Registers a function to run whenever its reactive dependencies change.
26
- */
27
- export const effect = (fn: () => void): Unsubscribe => StateImpl.createEffect(fn)
28
-
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)
33
-
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)
38
-
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)
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 getOrCreate = <K extends object, V>(map: WeakMap<K, V>, key: K, factory: () => V): V => {
34
+ let value = map.get(key)
35
+ if (!value) {
36
+ value = factory()
37
+ map.set(key, value)
38
+ }
39
+ return value
40
+ }
47
41
 
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()
42
+ const notifySubscribers = (): void => {
43
+ if (isNotifying) {
44
+ return
45
+ }
55
46
 
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
- ]
47
+ isNotifying = true
48
+
49
+ try {
50
+ while (pendingSubscribers.size > 0) {
51
+ const subscribers = pendingSubscribers
52
+ pendingSubscribers = new Set()
53
+
54
+ for (const effect of subscribers) {
55
+ effect()
56
+ }
57
+ }
58
+ } finally {
59
+ isNotifying = false
60
+ }
74
61
  }
75
62
 
76
- /**
77
- * Creates a lens for direct updates to nested properties of a state.
78
- */
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
63
+ const cleanupEffect = (effect: Subscriber): void => {
64
+ pendingSubscribers.delete(effect)
65
+
66
+ const deps = subscriberDependencies.get(effect)
67
+ if (deps) {
68
+ for (const subscribers of deps) {
69
+ subscribers.delete(effect)
70
+ }
71
+ deps.clear()
72
+ subscriberDependencies.delete(effect)
108
73
  }
74
+ }
75
+
76
+ const disposeEffect = (effect: Subscriber): void => {
77
+ cleanupEffect(effect)
78
+ activeSubscribers.delete(effect)
79
+ stateTracking.delete(effect)
109
80
 
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>
81
+ const parent = parentSubscriber.get(effect)
82
+ if (parent) {
83
+ const siblings = childSubscribers.get(parent)
84
+ if (siblings) {
85
+ siblings.delete(effect)
86
+ }
121
87
  }
88
+ parentSubscriber.delete(effect)
89
+
90
+ const children = childSubscribers.get(effect)
91
+ if (children) {
92
+ for (const child of children) {
93
+ disposeEffect(child)
94
+ }
95
+ children.clear()
96
+ childSubscribers.delete(effect)
97
+ }
98
+ }
122
99
 
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
100
+ const createState = <T>(initialValue: T, equalityFn: (a: T, b: T) => boolean = Object.is): State<T> => {
101
+ let value = initialValue
102
+ const subscribers = new Set<Subscriber>()
103
+ const stateId = Symbol()
104
+
105
+ const get = (): T => {
106
+ const currentEffect = currentSubscriber
127
107
  if (currentEffect) {
128
- // Add this effect to subscribers for future notification
129
- this.subscribers.add(currentEffect)
130
-
131
- // Maintain bidirectional dependency tracking to enable precise cleanup
132
- // when effects are unsubscribed, preventing memory leaks
133
- let dependencies = StateImpl.subscriberDependencies.get(currentEffect)
134
- if (!dependencies) {
135
- dependencies = new Set()
136
- StateImpl.subscriberDependencies.set(currentEffect, dependencies)
137
- }
138
- dependencies.add(this.subscribers)
139
-
140
- // Track read states to detect direct cyclical dependencies that
141
- // could cause infinite loops
142
- let readStates = StateImpl.stateTracking.get(currentEffect)
143
- if (!readStates) {
144
- readStates = new Set()
145
- StateImpl.stateTracking.set(currentEffect, readStates)
146
- }
147
- readStates.add(this.stateId)
108
+ subscribers.add(currentEffect)
109
+
110
+ getOrCreate(subscriberDependencies, currentEffect, () => new Set()).add(subscribers)
111
+
112
+ getOrCreate(stateTracking, currentEffect, () => new Set()).add(stateId)
148
113
  }
149
- return this.value
114
+ return value
150
115
  }
151
116
 
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)) {
117
+ get.set = (newValue: T): void => {
118
+ if (equalityFn(value, newValue)) {
156
119
  return
157
120
  }
158
121
 
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
122
+ const effect = currentSubscriber
162
123
  if (effect) {
163
- const states = StateImpl.stateTracking.get(effect)
164
- if (states?.has(this.stateId) && !StateImpl.parentSubscriber.get(effect)) {
124
+ const states = stateTracking.get(effect)
125
+ if (states?.has(stateId) && !parentSubscriber.get(effect)) {
165
126
  throw new Error('Infinite loop detected: effect() cannot update a state() it depends on!')
166
127
  }
167
128
  }
168
129
 
169
- this.value = newValue
130
+ value = newValue
170
131
 
171
- // Skip updates when there are no subscribers, avoiding unnecessary processing
172
- if (this.subscribers.size === 0) {
132
+ if (subscribers.size === 0) {
173
133
  return
174
134
  }
175
135
 
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)
136
+ for (const sub of subscribers) {
137
+ pendingSubscribers.add(sub)
180
138
  }
181
139
 
182
- // Immediate execution outside of batches, deferred execution inside batches
183
- if (StateImpl.batchDepth === 0 && !StateImpl.isNotifying) {
184
- StateImpl.notifySubscribers()
140
+ if (batchDepth === 0 && !isNotifying) {
141
+ notifySubscribers()
185
142
  }
186
143
  }
187
144
 
188
- update = (fn: (currentValue: T) => T): void => {
189
- this.set(fn(this.value))
145
+ get.update = (fn: (currentValue: T) => T): void => {
146
+ get.set(fn(value))
190
147
  }
191
148
 
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
- }
226
-
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
- }
234
- }
235
-
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)
246
- if (!children) {
247
- children = new Set()
248
- StateImpl.childSubscribers.set(parent, children)
249
- }
250
- children.add(runEffect)
251
- }
149
+ get[STATE_ID] = stateId
150
+ return get as State<T>
151
+ }
252
152
 
253
- // Queue for execution when batch completes
254
- StateImpl.deferredEffectCreations.push(runEffect)
153
+ const createEffect = (fn: () => void): Unsubscribe => {
154
+ const runEffect = (): void => {
155
+ if (activeSubscribers.has(runEffect)) {
156
+ return
255
157
  }
256
158
 
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
- }
159
+ activeSubscribers.add(runEffect)
160
+ const parentEffect = currentSubscriber
287
161
 
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
162
  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
163
+ cleanupEffect(runEffect)
164
+
165
+ currentSubscriber = runEffect
166
+ const existingStates = stateTracking.get(runEffect)
167
+ if (existingStates) {
168
+ existingStates.clear()
169
+ } else {
170
+ stateTracking.set(runEffect, new Set())
303
171
  }
304
- throw error
305
- } 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
172
 
321
- // Process state updates that occurred during the batch
322
- if (StateImpl.pendingSubscribers.size > 0 && !StateImpl.isNotifying) {
323
- StateImpl.notifySubscribers()
324
- }
173
+ if (parentEffect) {
174
+ parentSubscriber.set(runEffect, parentEffect)
175
+ getOrCreate(childSubscribers, parentEffect, () => new Set()).add(runEffect)
325
176
  }
177
+
178
+ fn()
179
+ } finally {
180
+ currentSubscriber = parentEffect
181
+ activeSubscribers.delete(runEffect)
326
182
  }
327
183
  }
328
184
 
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),
185
+ if (batchDepth === 0) {
186
+ runEffect()
187
+ } else {
188
+ if (currentSubscriber) {
189
+ const parent = currentSubscriber
190
+ parentSubscriber.set(runEffect, parent)
191
+ getOrCreate(childSubscribers, parent, () => new Set()).add(runEffect)
340
192
  }
341
193
 
342
- // Internal effect automatically tracks dependencies and updates the derived value
343
- StateImpl.createEffect(function deriveEffect(): void {
344
- const newValue = container.computeFn()
345
-
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)
351
- }
352
-
353
- container.initialized = true
354
- })
194
+ deferredEffectCreations.push(runEffect)
195
+ }
355
196
 
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)
363
- }
364
- return container.valueState() as T
365
- }
197
+ return (): void => {
198
+ disposeEffect(runEffect)
366
199
  }
200
+ }
367
201
 
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),
202
+ const executeBatch = <T>(fn: () => T): T => {
203
+ batchDepth++
204
+ try {
205
+ return fn()
206
+ } catch (error: unknown) {
207
+ if (batchDepth === 1) {
208
+ pendingSubscribers.clear()
209
+ deferredEffectCreations.length = 0
386
210
  }
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
211
+ throw error
212
+ } finally {
213
+ batchDepth--
214
+
215
+ if (batchDepth === 0) {
216
+ if (deferredEffectCreations.length > 0) {
217
+ const effectsToRun = deferredEffectCreations
218
+ deferredEffectCreations = []
219
+ for (const effect of effectsToRun) {
220
+ effect()
221
+ }
408
222
  }
409
223
 
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
224
+ if (pendingSubscribers.size > 0 && !isNotifying) {
225
+ notifySubscribers()
423
226
  }
424
- return container.valueState() as R
425
227
  }
426
228
  }
229
+ }
427
230
 
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
- )
231
+ const createDerive = <T>(computeFn: () => T): ReadOnlyState<T> => {
232
+ let cachedValue: T = undefined as unknown as T
233
+ let initialized = false
234
+ const valueState = createState<T | undefined>(undefined)
457
235
 
458
- try {
459
- container.accessor(proxy as unknown as T)
460
- } catch {
461
- // Ignore errors, we're just collecting the path
462
- }
236
+ createEffect(function deriveEffect(): void {
237
+ const newValue = computeFn()
463
238
 
464
- return pathCollector
239
+ if (!(initialized && Object.is(cachedValue, newValue))) {
240
+ cachedValue = newValue
241
+ valueState.set(newValue)
465
242
  }
466
243
 
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
- }
493
-
494
- container.isUpdating = true
495
- try {
496
- // Update lens state
497
- container.originalSet(value)
244
+ initialized = true
245
+ })
498
246
 
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
- }
247
+ return function deriveGetter(): T {
248
+ if (!initialized) {
249
+ cachedValue = computeFn()
250
+ initialized = true
251
+ valueState.set(cachedValue)
504
252
  }
253
+ return valueState() as T
254
+ }
255
+ }
505
256
 
506
- // Add update method for completeness
507
- container.lensState.update = function lensUpdate(fn: (value: K) => K): void {
508
- container.lensState.set(fn(container.lensState()))
509
- }
257
+ const createSelect = <T, R>(
258
+ source: ReadOnlyState<T>,
259
+ selectorFn: (state: T) => R,
260
+ equalityFn: (a: R, b: R) => boolean = Object.is
261
+ ): ReadOnlyState<R> => {
262
+ let initialized = false
263
+ let lastSelectedValue: R | undefined
264
+ let lastSourceValue: T | undefined
265
+ const valueState = createState<R | undefined>(undefined)
510
266
 
511
- return container.lensState
512
- }
267
+ createEffect(function selectEffect(): void {
268
+ const sourceValue = source()
513
269
 
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) {
270
+ if (initialized && Object.is(lastSourceValue, sourceValue)) {
519
271
  return
520
272
  }
521
273
 
522
- StateImpl.isNotifying = true
274
+ lastSourceValue = sourceValue
275
+ const newSelectedValue = selectorFn(sourceValue)
523
276
 
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
277
+ if (initialized && lastSelectedValue !== undefined && equalityFn(lastSelectedValue, newSelectedValue)) {
278
+ return
538
279
  }
539
- }
540
280
 
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)
545
-
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)
281
+ lastSelectedValue = newSelectedValue
282
+ valueState.set(newSelectedValue)
283
+ initialized = true
284
+ })
285
+
286
+ return function selectGetter(): R {
287
+ if (!initialized) {
288
+ lastSourceValue = source()
289
+ lastSelectedValue = selectorFn(lastSourceValue)
290
+ valueState.set(lastSelectedValue)
291
+ initialized = true
554
292
  }
293
+ return valueState() as R
555
294
  }
556
295
  }
296
+
557
297
  // Helper for array updates
558
298
  const updateArrayItem = <V>(arr: unknown[], index: number, value: V): unknown[] => {
559
299
  const copy = [
@@ -582,90 +322,158 @@ const createContainer = (key: string | number): Record<string | number, unknown>
582
322
  return isArrayKey ? [] : {}
583
323
  }
584
324
 
585
- // Helper for handling array path updates
586
- const updateArrayPath = <V>(array: unknown[], pathSegments: (string | number)[], value: V): unknown[] => {
587
- const index = Number(pathSegments[0])
588
-
589
- if (pathSegments.length === 1) {
590
- // Simple array item update
591
- return updateArrayItem(array, index, value)
325
+ const setValueAtPath = <V, O>(obj: O, pathSegments: (string | number)[], depth: number, value: V): O => {
326
+ if (depth >= pathSegments.length) {
327
+ return value as unknown as O
592
328
  }
593
329
 
594
- // Nested path in array
595
- const copy = [
596
- ...array,
597
- ]
598
- const nextPathSegments = pathSegments.slice(1)
599
- const nextKey = nextPathSegments[0]
600
-
601
- // For null/undefined values in arrays, create appropriate containers
602
- let nextValue = array[index]
603
- if (nextValue === undefined || nextValue === null) {
604
- // Use empty object as default if nextKey is undefined
605
- nextValue = nextKey !== undefined ? createContainer(nextKey) : {}
330
+ if (obj === undefined || obj === null) {
331
+ return setValueAtPath({} as O, pathSegments, depth, value)
606
332
  }
607
333
 
608
- copy[index] = setValueAtPath(nextValue, nextPathSegments, value)
609
- return copy
610
- }
611
-
612
- // Helper for handling object path updates
613
- const updateObjectPath = <V>(
614
- obj: Record<string | number, unknown>,
615
- pathSegments: (string | number)[],
616
- value: V
617
- ): Record<string | number, unknown> => {
618
- // Ensure we have a valid key
619
- const currentKey = pathSegments[0]
334
+ const currentKey = pathSegments[depth]
620
335
  if (currentKey === undefined) {
621
- // This shouldn't happen given our checks in the main function
622
336
  return obj
623
337
  }
624
338
 
625
- if (pathSegments.length === 1) {
626
- // Simple object property update
627
- return updateShallowProperty(obj, currentKey, value)
339
+ if (Array.isArray(obj)) {
340
+ const index = Number(currentKey)
341
+
342
+ if (depth === pathSegments.length - 1) {
343
+ return updateArrayItem(obj, index, value) as unknown as O
344
+ }
345
+
346
+ const copy = [
347
+ ...obj,
348
+ ]
349
+ const nextDepth = depth + 1
350
+ const nextKey = pathSegments[nextDepth]
351
+
352
+ let nextValue = obj[index]
353
+ if (nextValue === undefined || nextValue === null) {
354
+ nextValue = nextKey === undefined ? {} : createContainer(nextKey)
355
+ }
356
+
357
+ copy[index] = setValueAtPath(nextValue, pathSegments, nextDepth, value)
358
+ return copy as unknown as O
628
359
  }
629
360
 
630
- // Nested path in object
631
- const nextPathSegments = pathSegments.slice(1)
632
- const nextKey = nextPathSegments[0]
361
+ const record = obj as Record<string | number, unknown>
633
362
 
634
- // For null/undefined values, create appropriate containers
635
- let currentValue = obj[currentKey]
363
+ if (depth === pathSegments.length - 1) {
364
+ return updateShallowProperty(record, currentKey, value) as unknown as O
365
+ }
366
+
367
+ const nextDepth = depth + 1
368
+ const nextKey = pathSegments[nextDepth]
369
+
370
+ let currentValue = record[currentKey]
636
371
  if (currentValue === undefined || currentValue === null) {
637
- // Use empty object as default if nextKey is undefined
638
- currentValue = nextKey !== undefined ? createContainer(nextKey) : {}
372
+ currentValue = nextKey === undefined ? {} : createContainer(nextKey)
639
373
  }
640
374
 
641
- // Create new object with updated property
642
375
  const result = {
643
- ...obj,
376
+ ...record,
644
377
  }
645
- result[currentKey] = setValueAtPath(currentValue, nextPathSegments, value)
646
- return result
378
+ result[currentKey] = setValueAtPath(currentValue, pathSegments, nextDepth, value)
379
+ return result as unknown as O
647
380
  }
648
381
 
649
- // Simplified function to update a nested value at a path
650
- const setValueAtPath = <V, O>(obj: O, pathSegments: (string | number)[], value: V): O => {
651
- // Handle base cases
652
- if (pathSegments.length === 0) {
653
- return value as unknown as O
654
- }
382
+ const createLens = <T, K>(source: State<T>, accessor: (state: T) => K): State<K> => {
383
+ let isUpdating = false
384
+
385
+ const extractPath = (): (string | number)[] => {
386
+ const pathCollector: (string | number)[] = []
387
+ const proxy = new Proxy(
388
+ {},
389
+ {
390
+ get: (_: object, prop: string | symbol): unknown => {
391
+ if (typeof prop === 'string' || typeof prop === 'number') {
392
+ pathCollector.push(prop)
393
+ }
394
+ return proxy
395
+ },
396
+ }
397
+ )
655
398
 
656
- if (obj === undefined || obj === null) {
657
- return setValueAtPath({} as O, pathSegments, value)
399
+ try {
400
+ accessor(proxy as unknown as T)
401
+ } catch {
402
+ // Ignore errors, we're just collecting the path
403
+ }
404
+
405
+ return pathCollector
658
406
  }
659
407
 
660
- const currentKey = pathSegments[0]
661
- if (currentKey === undefined) {
662
- return obj
408
+ const path = extractPath()
409
+ const lensState = createState<K>(accessor(source()))
410
+ const originalSet = lensState.set
411
+
412
+ createEffect(function lensEffect(): void {
413
+ if (isUpdating) {
414
+ return
415
+ }
416
+
417
+ isUpdating = true
418
+ try {
419
+ lensState.set(accessor(source()))
420
+ } finally {
421
+ isUpdating = false
422
+ }
423
+ })
424
+
425
+ lensState.set = function lensSet(value: K): void {
426
+ if (isUpdating) {
427
+ return
428
+ }
429
+
430
+ isUpdating = true
431
+ try {
432
+ originalSet(value)
433
+ source.update((current: T): T => setValueAtPath(current, path, 0, value))
434
+ } finally {
435
+ isUpdating = false
436
+ }
663
437
  }
664
438
 
665
- // Delegate to specialized handlers based on data type
666
- if (Array.isArray(obj)) {
667
- return updateArrayPath(obj, pathSegments, value) as unknown as O
439
+ lensState.update = function lensUpdate(fn: (value: K) => K): void {
440
+ lensState.set(fn(lensState()))
668
441
  }
669
442
 
670
- return updateObjectPath(obj as Record<string | number, unknown>, pathSegments, value) as unknown as O
443
+ return lensState
444
+ }
445
+
446
+ const createReadonlyState =
447
+ <T>(source: State<T>): ReadOnlyState<T> =>
448
+ (): T =>
449
+ source()
450
+
451
+ const createProtectedState = <T>(
452
+ initialValue: T,
453
+ equalityFn: (a: T, b: T) => boolean = Object.is
454
+ ): [
455
+ ReadOnlyState<T>,
456
+ WriteableState<T>,
457
+ ] => {
458
+ const fullState = createState(initialValue, equalityFn)
459
+ const reader = createReadonlyState(fullState)
460
+ return [
461
+ reader,
462
+ {
463
+ set: (value: T): void => fullState.set(value),
464
+ update: (fn: (value: T) => T): void => fullState.update(fn),
465
+ },
466
+ ]
467
+ }
468
+
469
+ export type { ReadOnlyState, State, Unsubscribe, WriteableState }
470
+ export {
471
+ createDerive as derive,
472
+ createEffect as effect,
473
+ createLens as lens,
474
+ createProtectedState as protectedState,
475
+ createReadonlyState as readonlyState,
476
+ createSelect as select,
477
+ createState as state,
478
+ executeBatch as batch,
671
479
  }