@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 +3 -3
- package/dist/src/index.d.ts +14 -13
- package/dist/src/index.min.js +1 -1
- package/package.json +11 -10
- package/src/index.ts +340 -532
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
|
[](https://github.com/nerdalytics/beacon/blob/trunk/LICENSE)
|
|
6
6
|
[](https://www.npmjs.com/package/@nerdalytics/beacon)
|
|
7
|
-
[](https://socket.dev/npm/package/@nerdalytics/beacon/overview/1000.3.0)
|
|
8
8
|
|
|
9
9
|
[](https://nodejs.org/)
|
|
10
10
|
[](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.
|
|
40
|
+
**[nerdalytics.github.io/beacon](https://nerdalytics.github.io/beacon/)**
|
|
41
41
|
|
|
42
42
|
## License
|
|
43
43
|
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
8
|
+
type State<T> = ReadOnlyState<T> & WriteableState<T> & {
|
|
9
9
|
[STATE_ID]?: symbol;
|
|
10
10
|
};
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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, };
|
package/dist/src/index.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
let
|
|
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.
|
|
6
|
-
"@types/node": "25.
|
|
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
|
-
"
|
|
18
|
-
"
|
|
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.
|
|
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.
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
13
|
+
type State<T> = ReadOnlyState<T> &
|
|
14
14
|
WriteableState<T> & {
|
|
15
15
|
[STATE_ID]?: symbol
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
114
|
+
return value
|
|
150
115
|
}
|
|
151
116
|
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
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 =
|
|
164
|
-
if (states?.has(
|
|
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
|
-
|
|
130
|
+
value = newValue
|
|
170
131
|
|
|
171
|
-
|
|
172
|
-
if (this.subscribers.size === 0) {
|
|
132
|
+
if (subscribers.size === 0) {
|
|
173
133
|
return
|
|
174
134
|
}
|
|
175
135
|
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
StateImpl.notifySubscribers()
|
|
140
|
+
if (batchDepth === 0 && !isNotifying) {
|
|
141
|
+
notifySubscribers()
|
|
185
142
|
}
|
|
186
143
|
}
|
|
187
144
|
|
|
188
|
-
update = (fn: (currentValue: T) => T): void => {
|
|
189
|
-
|
|
145
|
+
get.update = (fn: (currentValue: T) => T): void => {
|
|
146
|
+
get.set(fn(value))
|
|
190
147
|
}
|
|
191
148
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
254
|
-
|
|
153
|
+
const createEffect = (fn: () => void): Unsubscribe => {
|
|
154
|
+
const runEffect = (): void => {
|
|
155
|
+
if (activeSubscribers.has(runEffect)) {
|
|
156
|
+
return
|
|
255
157
|
}
|
|
256
158
|
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
if (
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
343
|
-
|
|
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
|
-
|
|
357
|
-
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
|
|
411
|
-
|
|
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
|
-
|
|
430
|
-
|
|
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
|
-
|
|
459
|
-
|
|
460
|
-
} catch {
|
|
461
|
-
// Ignore errors, we're just collecting the path
|
|
462
|
-
}
|
|
236
|
+
createEffect(function deriveEffect(): void {
|
|
237
|
+
const newValue = computeFn()
|
|
463
238
|
|
|
464
|
-
|
|
239
|
+
if (!(initialized && Object.is(cachedValue, newValue))) {
|
|
240
|
+
cachedValue = newValue
|
|
241
|
+
valueState.set(newValue)
|
|
465
242
|
}
|
|
466
243
|
|
|
467
|
-
|
|
468
|
-
|
|
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
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
-
|
|
512
|
-
|
|
267
|
+
createEffect(function selectEffect(): void {
|
|
268
|
+
const sourceValue = source()
|
|
513
269
|
|
|
514
|
-
|
|
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
|
-
|
|
274
|
+
lastSourceValue = sourceValue
|
|
275
|
+
const newSelectedValue = selectorFn(sourceValue)
|
|
523
276
|
|
|
524
|
-
|
|
525
|
-
|
|
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
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
-
|
|
586
|
-
|
|
587
|
-
|
|
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
|
-
|
|
595
|
-
|
|
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
|
-
|
|
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 (
|
|
626
|
-
|
|
627
|
-
|
|
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
|
-
|
|
631
|
-
const nextPathSegments = pathSegments.slice(1)
|
|
632
|
-
const nextKey = nextPathSegments[0]
|
|
361
|
+
const record = obj as Record<string | number, unknown>
|
|
633
362
|
|
|
634
|
-
|
|
635
|
-
|
|
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
|
-
|
|
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
|
-
...
|
|
376
|
+
...record,
|
|
644
377
|
}
|
|
645
|
-
result[currentKey] = setValueAtPath(currentValue,
|
|
646
|
-
return result
|
|
378
|
+
result[currentKey] = setValueAtPath(currentValue, pathSegments, nextDepth, value)
|
|
379
|
+
return result as unknown as O
|
|
647
380
|
}
|
|
648
381
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
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
|
-
|
|
657
|
-
|
|
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
|
|
661
|
-
|
|
662
|
-
|
|
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
|
-
|
|
666
|
-
|
|
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
|
|
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
|
}
|