@nerdalytics/beacon 1000.2.2 → 1000.2.4
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 +2 -0
- package/dist/src/index.min.js +1 -1
- package/package.json +9 -7
- package/src/index.ts +671 -0
- package/dist/src/index.js +0 -406
package/README.md
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
> Lightweight reactive state management for Node.js backends
|
|
4
4
|
|
|
5
|
+
|
|
5
6
|
[](https://github.com/nerdalytics/beacon/blob/trunk/LICENSE)
|
|
6
7
|
[](https://www.npmjs.com/package/@nerdalytics/beacon)
|
|
8
|
+
[](https://socket.dev/npm/package/@nerdalytics/beacon/overview/1000.2.3)
|
|
7
9
|
|
|
8
10
|
[](https://nodejs.org/)
|
|
9
11
|
[](https://typescriptlang.org/)
|
package/dist/src/index.min.js
CHANGED
|
@@ -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=
|
|
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};
|
package/package.json
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
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.
|
|
6
|
-
"@types/node": "
|
|
5
|
+
"@biomejs/biome": "2.3.13",
|
|
6
|
+
"@types/node": "25.0.10",
|
|
7
|
+
"npm-check-updates": "19.3.2",
|
|
7
8
|
"typescript": "5.9.3",
|
|
8
9
|
"uglify-js": "3.19.3"
|
|
9
10
|
},
|
|
@@ -20,13 +21,13 @@
|
|
|
20
21
|
"default": "./dist/src/index.min.js"
|
|
21
22
|
},
|
|
22
23
|
"types": "./dist/src/index.d.ts",
|
|
23
|
-
"typescript": "./
|
|
24
|
+
"typescript": "./src/index.ts"
|
|
24
25
|
}
|
|
25
26
|
},
|
|
26
27
|
"files": [
|
|
27
|
-
"dist/src/index.js",
|
|
28
|
+
"dist/src/index.min.js",
|
|
28
29
|
"dist/src/index.d.ts",
|
|
29
|
-
"
|
|
30
|
+
"src/index.ts",
|
|
30
31
|
"LICENSE"
|
|
31
32
|
],
|
|
32
33
|
"keywords": [
|
|
@@ -49,7 +50,7 @@
|
|
|
49
50
|
"license": "MIT",
|
|
50
51
|
"main": "dist/src/index.min.js",
|
|
51
52
|
"name": "@nerdalytics/beacon",
|
|
52
|
-
"packageManager": "npm@11.
|
|
53
|
+
"packageManager": "npm@11.8.0",
|
|
53
54
|
"repository": {
|
|
54
55
|
"type": "git",
|
|
55
56
|
"url": "git+https://github.com/nerdalytics/beacon.git"
|
|
@@ -87,9 +88,10 @@
|
|
|
87
88
|
"test:unit:lens": "node --test tests/lens.test.ts",
|
|
88
89
|
"test:unit:select": "node --test tests/select.test.ts",
|
|
89
90
|
"test:unit:state": "node --test tests/state.test.ts",
|
|
91
|
+
"update-dependencies": "npx npm-check-updates --interactive --upgrade --removeRange",
|
|
90
92
|
"update-performance-docs": "node --experimental-config-file=node.config.json scripts/update-performance-docs.ts"
|
|
91
93
|
},
|
|
92
94
|
"type": "module",
|
|
93
95
|
"types": "dist/src/index.d.ts",
|
|
94
|
-
"version": "1000.2.
|
|
96
|
+
"version": "1000.2.4"
|
|
95
97
|
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
// Core types for reactive primitives
|
|
2
|
+
type Subscriber = () => void
|
|
3
|
+
export type Unsubscribe = () => void
|
|
4
|
+
export type ReadOnlyState<T> = () => T
|
|
5
|
+
export interface WriteableState<T> {
|
|
6
|
+
set(value: T): void
|
|
7
|
+
update(fn: (value: T) => T): void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Special symbol used for internal tracking
|
|
11
|
+
const STATE_ID: unique symbol = Symbol('STATE_ID')
|
|
12
|
+
|
|
13
|
+
export type State<T> = ReadOnlyState<T> &
|
|
14
|
+
WriteableState<T> & {
|
|
15
|
+
[STATE_ID]?: symbol
|
|
16
|
+
}
|
|
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)
|
|
47
|
+
|
|
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
|
+
]
|
|
74
|
+
}
|
|
75
|
+
|
|
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
|
|
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
|
+
}
|
|
122
|
+
|
|
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
|
|
127
|
+
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)
|
|
148
|
+
}
|
|
149
|
+
return this.value
|
|
150
|
+
}
|
|
151
|
+
|
|
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)) {
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
|
|
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
|
|
162
|
+
if (effect) {
|
|
163
|
+
const states = StateImpl.stateTracking.get(effect)
|
|
164
|
+
if (states?.has(this.stateId) && !StateImpl.parentSubscriber.get(effect)) {
|
|
165
|
+
throw new Error('Infinite loop detected: effect() cannot update a state() it depends on!')
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
this.value = newValue
|
|
170
|
+
|
|
171
|
+
// Skip updates when there are no subscribers, avoiding unnecessary processing
|
|
172
|
+
if (this.subscribers.size === 0) {
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
|
|
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)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Immediate execution outside of batches, deferred execution inside batches
|
|
183
|
+
if (StateImpl.batchDepth === 0 && !StateImpl.isNotifying) {
|
|
184
|
+
StateImpl.notifySubscribers()
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
update = (fn: (currentValue: T) => T): void => {
|
|
189
|
+
this.set(fn(this.value))
|
|
190
|
+
}
|
|
191
|
+
|
|
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
|
+
}
|
|
252
|
+
|
|
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
|
|
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
|
+
|
|
321
|
+
// Process state updates that occurred during the batch
|
|
322
|
+
if (StateImpl.pendingSubscribers.size > 0 && !StateImpl.isNotifying) {
|
|
323
|
+
StateImpl.notifySubscribers()
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
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),
|
|
340
|
+
}
|
|
341
|
+
|
|
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
|
+
})
|
|
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)
|
|
363
|
+
}
|
|
364
|
+
return container.valueState() as T
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
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),
|
|
386
|
+
}
|
|
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
|
|
408
|
+
}
|
|
409
|
+
|
|
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
|
|
423
|
+
}
|
|
424
|
+
return container.valueState() as R
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
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
|
+
)
|
|
457
|
+
|
|
458
|
+
try {
|
|
459
|
+
container.accessor(proxy as unknown as T)
|
|
460
|
+
} catch {
|
|
461
|
+
// Ignore errors, we're just collecting the path
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return pathCollector
|
|
465
|
+
}
|
|
466
|
+
|
|
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)
|
|
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()))
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return container.lensState
|
|
512
|
+
}
|
|
513
|
+
|
|
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) {
|
|
519
|
+
return
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
StateImpl.isNotifying = true
|
|
523
|
+
|
|
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
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
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)
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
// Helper for array updates
|
|
558
|
+
const updateArrayItem = <V>(arr: unknown[], index: number, value: V): unknown[] => {
|
|
559
|
+
const copy = [
|
|
560
|
+
...arr,
|
|
561
|
+
]
|
|
562
|
+
copy[index] = value
|
|
563
|
+
return copy
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Helper for single-level updates (optimization)
|
|
567
|
+
const updateShallowProperty = <V>(
|
|
568
|
+
obj: Record<string | number, unknown>,
|
|
569
|
+
key: string | number,
|
|
570
|
+
value: V
|
|
571
|
+
): Record<string | number, unknown> => {
|
|
572
|
+
const result = {
|
|
573
|
+
...obj,
|
|
574
|
+
}
|
|
575
|
+
result[key] = value
|
|
576
|
+
return result
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Helper to create the appropriate container type
|
|
580
|
+
const createContainer = (key: string | number): Record<string | number, unknown> | unknown[] => {
|
|
581
|
+
const isArrayKey = typeof key === 'number' || !Number.isNaN(Number(key))
|
|
582
|
+
return isArrayKey ? [] : {}
|
|
583
|
+
}
|
|
584
|
+
|
|
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)
|
|
592
|
+
}
|
|
593
|
+
|
|
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) : {}
|
|
606
|
+
}
|
|
607
|
+
|
|
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]
|
|
620
|
+
if (currentKey === undefined) {
|
|
621
|
+
// This shouldn't happen given our checks in the main function
|
|
622
|
+
return obj
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (pathSegments.length === 1) {
|
|
626
|
+
// Simple object property update
|
|
627
|
+
return updateShallowProperty(obj, currentKey, value)
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Nested path in object
|
|
631
|
+
const nextPathSegments = pathSegments.slice(1)
|
|
632
|
+
const nextKey = nextPathSegments[0]
|
|
633
|
+
|
|
634
|
+
// For null/undefined values, create appropriate containers
|
|
635
|
+
let currentValue = obj[currentKey]
|
|
636
|
+
if (currentValue === undefined || currentValue === null) {
|
|
637
|
+
// Use empty object as default if nextKey is undefined
|
|
638
|
+
currentValue = nextKey !== undefined ? createContainer(nextKey) : {}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Create new object with updated property
|
|
642
|
+
const result = {
|
|
643
|
+
...obj,
|
|
644
|
+
}
|
|
645
|
+
result[currentKey] = setValueAtPath(currentValue, nextPathSegments, value)
|
|
646
|
+
return result
|
|
647
|
+
}
|
|
648
|
+
|
|
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
|
+
}
|
|
655
|
+
|
|
656
|
+
if (obj === undefined || obj === null) {
|
|
657
|
+
return setValueAtPath({} as O, pathSegments, value)
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const currentKey = pathSegments[0]
|
|
661
|
+
if (currentKey === undefined) {
|
|
662
|
+
return obj
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Delegate to specialized handlers based on data type
|
|
666
|
+
if (Array.isArray(obj)) {
|
|
667
|
+
return updateArrayPath(obj, pathSegments, value) as unknown as O
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
return updateObjectPath(obj as Record<string | number, unknown>, pathSegments, value) as unknown as O
|
|
671
|
+
}
|
package/dist/src/index.js
DELETED
|
@@ -1,406 +0,0 @@
|
|
|
1
|
-
const STATE_ID = Symbol('STATE_ID');
|
|
2
|
-
export const state = (initialValue, equalityFn = Object.is) => StateImpl.createState(initialValue, equalityFn);
|
|
3
|
-
export const effect = (fn) => StateImpl.createEffect(fn);
|
|
4
|
-
export const batch = (fn) => StateImpl.executeBatch(fn);
|
|
5
|
-
export const derive = (computeFn) => StateImpl.createDerive(computeFn);
|
|
6
|
-
export const select = (source, selectorFn, equalityFn = Object.is) => StateImpl.createSelect(source, selectorFn, equalityFn);
|
|
7
|
-
export const readonlyState = (state) => () => state();
|
|
8
|
-
export const protectedState = (initialValue, equalityFn = Object.is) => {
|
|
9
|
-
const fullState = state(initialValue, equalityFn);
|
|
10
|
-
return [
|
|
11
|
-
() => readonlyState(fullState)(),
|
|
12
|
-
{
|
|
13
|
-
set: (value) => fullState.set(value),
|
|
14
|
-
update: (fn) => fullState.update(fn),
|
|
15
|
-
},
|
|
16
|
-
];
|
|
17
|
-
};
|
|
18
|
-
export const lens = (source, accessor) => StateImpl.createLens(source, accessor);
|
|
19
|
-
class StateImpl {
|
|
20
|
-
static currentSubscriber = null;
|
|
21
|
-
static pendingSubscribers = new Set();
|
|
22
|
-
static isNotifying = false;
|
|
23
|
-
static batchDepth = 0;
|
|
24
|
-
static deferredEffectCreations = [];
|
|
25
|
-
static activeSubscribers = new Set();
|
|
26
|
-
static stateTracking = new WeakMap();
|
|
27
|
-
static subscriberDependencies = new WeakMap();
|
|
28
|
-
static parentSubscriber = new WeakMap();
|
|
29
|
-
static childSubscribers = new WeakMap();
|
|
30
|
-
value;
|
|
31
|
-
subscribers = new Set();
|
|
32
|
-
stateId = Symbol();
|
|
33
|
-
equalityFn;
|
|
34
|
-
constructor(initialValue, equalityFn = Object.is) {
|
|
35
|
-
this.value = initialValue;
|
|
36
|
-
this.equalityFn = equalityFn;
|
|
37
|
-
}
|
|
38
|
-
static createState = (initialValue, equalityFn = Object.is) => {
|
|
39
|
-
const instance = new StateImpl(initialValue, equalityFn);
|
|
40
|
-
const get = () => instance.get();
|
|
41
|
-
get.set = (value) => instance.set(value);
|
|
42
|
-
get.update = (fn) => instance.update(fn);
|
|
43
|
-
get[STATE_ID] = instance.stateId;
|
|
44
|
-
return get;
|
|
45
|
-
};
|
|
46
|
-
get = () => {
|
|
47
|
-
const currentEffect = StateImpl.currentSubscriber;
|
|
48
|
-
if (currentEffect) {
|
|
49
|
-
this.subscribers.add(currentEffect);
|
|
50
|
-
let dependencies = StateImpl.subscriberDependencies.get(currentEffect);
|
|
51
|
-
if (!dependencies) {
|
|
52
|
-
dependencies = new Set();
|
|
53
|
-
StateImpl.subscriberDependencies.set(currentEffect, dependencies);
|
|
54
|
-
}
|
|
55
|
-
dependencies.add(this.subscribers);
|
|
56
|
-
let readStates = StateImpl.stateTracking.get(currentEffect);
|
|
57
|
-
if (!readStates) {
|
|
58
|
-
readStates = new Set();
|
|
59
|
-
StateImpl.stateTracking.set(currentEffect, readStates);
|
|
60
|
-
}
|
|
61
|
-
readStates.add(this.stateId);
|
|
62
|
-
}
|
|
63
|
-
return this.value;
|
|
64
|
-
};
|
|
65
|
-
set = (newValue) => {
|
|
66
|
-
if (this.equalityFn(this.value, newValue)) {
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
const effect = StateImpl.currentSubscriber;
|
|
70
|
-
if (effect) {
|
|
71
|
-
const states = StateImpl.stateTracking.get(effect);
|
|
72
|
-
if (states?.has(this.stateId) && !StateImpl.parentSubscriber.get(effect)) {
|
|
73
|
-
throw new Error('Infinite loop detected: effect() cannot update a state() it depends on!');
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
this.value = newValue;
|
|
77
|
-
if (this.subscribers.size === 0) {
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
for (const sub of this.subscribers) {
|
|
81
|
-
StateImpl.pendingSubscribers.add(sub);
|
|
82
|
-
}
|
|
83
|
-
if (StateImpl.batchDepth === 0 && !StateImpl.isNotifying) {
|
|
84
|
-
StateImpl.notifySubscribers();
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
update = (fn) => {
|
|
88
|
-
this.set(fn(this.value));
|
|
89
|
-
};
|
|
90
|
-
static createEffect = (fn) => {
|
|
91
|
-
const runEffect = () => {
|
|
92
|
-
if (StateImpl.activeSubscribers.has(runEffect)) {
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
StateImpl.activeSubscribers.add(runEffect);
|
|
96
|
-
const parentEffect = StateImpl.currentSubscriber;
|
|
97
|
-
try {
|
|
98
|
-
StateImpl.cleanupEffect(runEffect);
|
|
99
|
-
StateImpl.currentSubscriber = runEffect;
|
|
100
|
-
StateImpl.stateTracking.set(runEffect, new Set());
|
|
101
|
-
if (parentEffect) {
|
|
102
|
-
StateImpl.parentSubscriber.set(runEffect, parentEffect);
|
|
103
|
-
let children = StateImpl.childSubscribers.get(parentEffect);
|
|
104
|
-
if (!children) {
|
|
105
|
-
children = new Set();
|
|
106
|
-
StateImpl.childSubscribers.set(parentEffect, children);
|
|
107
|
-
}
|
|
108
|
-
children.add(runEffect);
|
|
109
|
-
}
|
|
110
|
-
fn();
|
|
111
|
-
}
|
|
112
|
-
finally {
|
|
113
|
-
StateImpl.currentSubscriber = parentEffect;
|
|
114
|
-
StateImpl.activeSubscribers.delete(runEffect);
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
|
-
if (StateImpl.batchDepth === 0) {
|
|
118
|
-
runEffect();
|
|
119
|
-
}
|
|
120
|
-
else {
|
|
121
|
-
if (StateImpl.currentSubscriber) {
|
|
122
|
-
const parent = StateImpl.currentSubscriber;
|
|
123
|
-
StateImpl.parentSubscriber.set(runEffect, parent);
|
|
124
|
-
let children = StateImpl.childSubscribers.get(parent);
|
|
125
|
-
if (!children) {
|
|
126
|
-
children = new Set();
|
|
127
|
-
StateImpl.childSubscribers.set(parent, children);
|
|
128
|
-
}
|
|
129
|
-
children.add(runEffect);
|
|
130
|
-
}
|
|
131
|
-
StateImpl.deferredEffectCreations.push(runEffect);
|
|
132
|
-
}
|
|
133
|
-
return () => {
|
|
134
|
-
StateImpl.cleanupEffect(runEffect);
|
|
135
|
-
StateImpl.pendingSubscribers.delete(runEffect);
|
|
136
|
-
StateImpl.activeSubscribers.delete(runEffect);
|
|
137
|
-
StateImpl.stateTracking.delete(runEffect);
|
|
138
|
-
const parent = StateImpl.parentSubscriber.get(runEffect);
|
|
139
|
-
if (parent) {
|
|
140
|
-
const siblings = StateImpl.childSubscribers.get(parent);
|
|
141
|
-
if (siblings) {
|
|
142
|
-
siblings.delete(runEffect);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
StateImpl.parentSubscriber.delete(runEffect);
|
|
146
|
-
const children = StateImpl.childSubscribers.get(runEffect);
|
|
147
|
-
if (children) {
|
|
148
|
-
for (const child of children) {
|
|
149
|
-
StateImpl.cleanupEffect(child);
|
|
150
|
-
}
|
|
151
|
-
children.clear();
|
|
152
|
-
StateImpl.childSubscribers.delete(runEffect);
|
|
153
|
-
}
|
|
154
|
-
};
|
|
155
|
-
};
|
|
156
|
-
static executeBatch = (fn) => {
|
|
157
|
-
StateImpl.batchDepth++;
|
|
158
|
-
try {
|
|
159
|
-
return fn();
|
|
160
|
-
}
|
|
161
|
-
catch (error) {
|
|
162
|
-
if (StateImpl.batchDepth === 1) {
|
|
163
|
-
StateImpl.pendingSubscribers.clear();
|
|
164
|
-
StateImpl.deferredEffectCreations.length = 0;
|
|
165
|
-
}
|
|
166
|
-
throw error;
|
|
167
|
-
}
|
|
168
|
-
finally {
|
|
169
|
-
StateImpl.batchDepth--;
|
|
170
|
-
if (StateImpl.batchDepth === 0) {
|
|
171
|
-
if (StateImpl.deferredEffectCreations.length > 0) {
|
|
172
|
-
const effectsToRun = [
|
|
173
|
-
...StateImpl.deferredEffectCreations,
|
|
174
|
-
];
|
|
175
|
-
StateImpl.deferredEffectCreations.length = 0;
|
|
176
|
-
for (const effect of effectsToRun) {
|
|
177
|
-
effect();
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
if (StateImpl.pendingSubscribers.size > 0 && !StateImpl.isNotifying) {
|
|
181
|
-
StateImpl.notifySubscribers();
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
};
|
|
186
|
-
static createDerive = (computeFn) => {
|
|
187
|
-
const container = {
|
|
188
|
-
cachedValue: undefined,
|
|
189
|
-
computeFn,
|
|
190
|
-
initialized: false,
|
|
191
|
-
valueState: StateImpl.createState(undefined),
|
|
192
|
-
};
|
|
193
|
-
StateImpl.createEffect(function deriveEffect() {
|
|
194
|
-
const newValue = container.computeFn();
|
|
195
|
-
if (!(container.initialized && Object.is(container.cachedValue, newValue))) {
|
|
196
|
-
container.cachedValue = newValue;
|
|
197
|
-
container.valueState.set(newValue);
|
|
198
|
-
}
|
|
199
|
-
container.initialized = true;
|
|
200
|
-
});
|
|
201
|
-
return function deriveGetter() {
|
|
202
|
-
if (!container.initialized) {
|
|
203
|
-
container.cachedValue = container.computeFn();
|
|
204
|
-
container.initialized = true;
|
|
205
|
-
container.valueState.set(container.cachedValue);
|
|
206
|
-
}
|
|
207
|
-
return container.valueState();
|
|
208
|
-
};
|
|
209
|
-
};
|
|
210
|
-
static createSelect = (source, selectorFn, equalityFn = Object.is) => {
|
|
211
|
-
const container = {
|
|
212
|
-
equalityFn,
|
|
213
|
-
initialized: false,
|
|
214
|
-
lastSelectedValue: undefined,
|
|
215
|
-
lastSourceValue: undefined,
|
|
216
|
-
selectorFn,
|
|
217
|
-
source,
|
|
218
|
-
valueState: StateImpl.createState(undefined),
|
|
219
|
-
};
|
|
220
|
-
StateImpl.createEffect(function selectEffect() {
|
|
221
|
-
const sourceValue = container.source();
|
|
222
|
-
if (container.initialized && Object.is(container.lastSourceValue, sourceValue)) {
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
container.lastSourceValue = sourceValue;
|
|
226
|
-
const newSelectedValue = container.selectorFn(sourceValue);
|
|
227
|
-
if (container.initialized &&
|
|
228
|
-
container.lastSelectedValue !== undefined &&
|
|
229
|
-
container.equalityFn(container.lastSelectedValue, newSelectedValue)) {
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
container.lastSelectedValue = newSelectedValue;
|
|
233
|
-
container.valueState.set(newSelectedValue);
|
|
234
|
-
container.initialized = true;
|
|
235
|
-
});
|
|
236
|
-
return function selectGetter() {
|
|
237
|
-
if (!container.initialized) {
|
|
238
|
-
container.lastSourceValue = container.source();
|
|
239
|
-
container.lastSelectedValue = container.selectorFn(container.lastSourceValue);
|
|
240
|
-
container.valueState.set(container.lastSelectedValue);
|
|
241
|
-
container.initialized = true;
|
|
242
|
-
}
|
|
243
|
-
return container.valueState();
|
|
244
|
-
};
|
|
245
|
-
};
|
|
246
|
-
static createLens = (source, accessor) => {
|
|
247
|
-
const container = {
|
|
248
|
-
accessor,
|
|
249
|
-
isUpdating: false,
|
|
250
|
-
lensState: null,
|
|
251
|
-
originalSet: null,
|
|
252
|
-
path: [],
|
|
253
|
-
source,
|
|
254
|
-
};
|
|
255
|
-
const extractPath = () => {
|
|
256
|
-
const pathCollector = [];
|
|
257
|
-
const proxy = new Proxy({}, {
|
|
258
|
-
get: (_, prop) => {
|
|
259
|
-
if (typeof prop === 'string' || typeof prop === 'number') {
|
|
260
|
-
pathCollector.push(prop);
|
|
261
|
-
}
|
|
262
|
-
return proxy;
|
|
263
|
-
},
|
|
264
|
-
});
|
|
265
|
-
try {
|
|
266
|
-
container.accessor(proxy);
|
|
267
|
-
}
|
|
268
|
-
catch {
|
|
269
|
-
}
|
|
270
|
-
return pathCollector;
|
|
271
|
-
};
|
|
272
|
-
container.path = extractPath();
|
|
273
|
-
container.lensState = StateImpl.createState(container.accessor(container.source()));
|
|
274
|
-
container.originalSet = container.lensState.set;
|
|
275
|
-
StateImpl.createEffect(function lensEffect() {
|
|
276
|
-
if (container.isUpdating) {
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
container.isUpdating = true;
|
|
280
|
-
try {
|
|
281
|
-
container.lensState.set(container.accessor(container.source()));
|
|
282
|
-
}
|
|
283
|
-
finally {
|
|
284
|
-
container.isUpdating = false;
|
|
285
|
-
}
|
|
286
|
-
});
|
|
287
|
-
container.lensState.set = function lensSet(value) {
|
|
288
|
-
if (container.isUpdating) {
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
container.isUpdating = true;
|
|
292
|
-
try {
|
|
293
|
-
container.originalSet(value);
|
|
294
|
-
container.source.update((current) => setValueAtPath(current, container.path, value));
|
|
295
|
-
}
|
|
296
|
-
finally {
|
|
297
|
-
container.isUpdating = false;
|
|
298
|
-
}
|
|
299
|
-
};
|
|
300
|
-
container.lensState.update = function lensUpdate(fn) {
|
|
301
|
-
container.lensState.set(fn(container.lensState()));
|
|
302
|
-
};
|
|
303
|
-
return container.lensState;
|
|
304
|
-
};
|
|
305
|
-
static notifySubscribers = () => {
|
|
306
|
-
if (StateImpl.isNotifying) {
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
StateImpl.isNotifying = true;
|
|
310
|
-
try {
|
|
311
|
-
while (StateImpl.pendingSubscribers.size > 0) {
|
|
312
|
-
const subscribers = Array.from(StateImpl.pendingSubscribers);
|
|
313
|
-
StateImpl.pendingSubscribers.clear();
|
|
314
|
-
for (const effect of subscribers) {
|
|
315
|
-
effect();
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
finally {
|
|
320
|
-
StateImpl.isNotifying = false;
|
|
321
|
-
}
|
|
322
|
-
};
|
|
323
|
-
static cleanupEffect = (effect) => {
|
|
324
|
-
StateImpl.pendingSubscribers.delete(effect);
|
|
325
|
-
const deps = StateImpl.subscriberDependencies.get(effect);
|
|
326
|
-
if (deps) {
|
|
327
|
-
for (const subscribers of deps) {
|
|
328
|
-
subscribers.delete(effect);
|
|
329
|
-
}
|
|
330
|
-
deps.clear();
|
|
331
|
-
StateImpl.subscriberDependencies.delete(effect);
|
|
332
|
-
}
|
|
333
|
-
};
|
|
334
|
-
}
|
|
335
|
-
const updateArrayItem = (arr, index, value) => {
|
|
336
|
-
const copy = [
|
|
337
|
-
...arr,
|
|
338
|
-
];
|
|
339
|
-
copy[index] = value;
|
|
340
|
-
return copy;
|
|
341
|
-
};
|
|
342
|
-
const updateShallowProperty = (obj, key, value) => {
|
|
343
|
-
const result = {
|
|
344
|
-
...obj,
|
|
345
|
-
};
|
|
346
|
-
result[key] = value;
|
|
347
|
-
return result;
|
|
348
|
-
};
|
|
349
|
-
const createContainer = (key) => {
|
|
350
|
-
const isArrayKey = typeof key === 'number' || !Number.isNaN(Number(key));
|
|
351
|
-
return isArrayKey ? [] : {};
|
|
352
|
-
};
|
|
353
|
-
const updateArrayPath = (array, pathSegments, value) => {
|
|
354
|
-
const index = Number(pathSegments[0]);
|
|
355
|
-
if (pathSegments.length === 1) {
|
|
356
|
-
return updateArrayItem(array, index, value);
|
|
357
|
-
}
|
|
358
|
-
const copy = [
|
|
359
|
-
...array,
|
|
360
|
-
];
|
|
361
|
-
const nextPathSegments = pathSegments.slice(1);
|
|
362
|
-
const nextKey = nextPathSegments[0];
|
|
363
|
-
let nextValue = array[index];
|
|
364
|
-
if (nextValue === undefined || nextValue === null) {
|
|
365
|
-
nextValue = nextKey !== undefined ? createContainer(nextKey) : {};
|
|
366
|
-
}
|
|
367
|
-
copy[index] = setValueAtPath(nextValue, nextPathSegments, value);
|
|
368
|
-
return copy;
|
|
369
|
-
};
|
|
370
|
-
const updateObjectPath = (obj, pathSegments, value) => {
|
|
371
|
-
const currentKey = pathSegments[0];
|
|
372
|
-
if (currentKey === undefined) {
|
|
373
|
-
return obj;
|
|
374
|
-
}
|
|
375
|
-
if (pathSegments.length === 1) {
|
|
376
|
-
return updateShallowProperty(obj, currentKey, value);
|
|
377
|
-
}
|
|
378
|
-
const nextPathSegments = pathSegments.slice(1);
|
|
379
|
-
const nextKey = nextPathSegments[0];
|
|
380
|
-
let currentValue = obj[currentKey];
|
|
381
|
-
if (currentValue === undefined || currentValue === null) {
|
|
382
|
-
currentValue = nextKey !== undefined ? createContainer(nextKey) : {};
|
|
383
|
-
}
|
|
384
|
-
const result = {
|
|
385
|
-
...obj,
|
|
386
|
-
};
|
|
387
|
-
result[currentKey] = setValueAtPath(currentValue, nextPathSegments, value);
|
|
388
|
-
return result;
|
|
389
|
-
};
|
|
390
|
-
const setValueAtPath = (obj, pathSegments, value) => {
|
|
391
|
-
if (pathSegments.length === 0) {
|
|
392
|
-
return value;
|
|
393
|
-
}
|
|
394
|
-
if (obj === undefined || obj === null) {
|
|
395
|
-
return setValueAtPath({}, pathSegments, value);
|
|
396
|
-
}
|
|
397
|
-
const currentKey = pathSegments[0];
|
|
398
|
-
if (currentKey === undefined) {
|
|
399
|
-
return obj;
|
|
400
|
-
}
|
|
401
|
-
if (Array.isArray(obj)) {
|
|
402
|
-
return updateArrayPath(obj, pathSegments, value);
|
|
403
|
-
}
|
|
404
|
-
return updateObjectPath(obj, pathSegments, value);
|
|
405
|
-
};
|
|
406
|
-
//# sourceMappingURL=index.js.map
|