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