@nerdalytics/beacon 1000.3.0 → 1000.3.2
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 +12 -4
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.min.js +1 -1
- package/package.json +8 -8
- package/src/index.ts +145 -200
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
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
|
+
> Reactive dependency graph runtime for Node.js backends. Tracks dependencies between signals and propagates updates automatically.
|
|
4
4
|
|
|
5
5
|
[](https://github.com/nerdalytics/beacon/blob/trunk/LICENSE)
|
|
6
6
|
[](https://www.npmjs.com/package/@nerdalytics/beacon)
|
|
@@ -10,8 +10,6 @@
|
|
|
10
10
|
[](https://typescriptlang.org/)
|
|
11
11
|
[](https://biomejs.dev/)
|
|
12
12
|
|
|
13
|
-
A lightweight reactive state library for Node.js backends. Enables reactive state management with automatic dependency tracking and efficient updates for server-side applications.
|
|
14
|
-
|
|
15
13
|
## Installation
|
|
16
14
|
|
|
17
15
|
```
|
|
@@ -37,7 +35,17 @@ count.set(5);
|
|
|
37
35
|
## Documentation
|
|
38
36
|
|
|
39
37
|
Full documentation, API reference, and examples available at:
|
|
40
|
-
**[github.
|
|
38
|
+
**[nerdalytics.github.io/beacon](https://nerdalytics.github.io/beacon/)**
|
|
39
|
+
|
|
40
|
+
### LLM-friendly docs
|
|
41
|
+
|
|
42
|
+
The handbook has plain-text endpoints for LLMs:
|
|
43
|
+
|
|
44
|
+
- [`llms.txt`](https://nerdalytics.github.io/beacon/llms.txt) lists available versions
|
|
45
|
+
- [`<version>/llms.txt`](https://nerdalytics.github.io/beacon/latest/llms.txt) lists pages for a version
|
|
46
|
+
- [`<version>/llms-full.txt`](https://nerdalytics.github.io/beacon/latest/llms-full.txt) concatenates every page into one file
|
|
47
|
+
|
|
48
|
+
You can also append `.md` to any handbook page URL for its Markdown source (e.g. [`<version>/introduction.md`](https://nerdalytics.github.io/beacon/latest/introduction.md)).
|
|
41
49
|
|
|
42
50
|
## License
|
|
43
51
|
|
package/dist/src/index.d.ts
CHANGED
|
@@ -16,5 +16,5 @@ declare const createSelect: <T, R>(source: ReadOnlyState<T>, selectorFn: (state:
|
|
|
16
16
|
declare const createLens: <T, K>(source: State<T>, accessor: (state: T) => K) => State<K>;
|
|
17
17
|
declare const createReadonlyState: <T>(source: State<T>) => ReadOnlyState<T>;
|
|
18
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
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,o=new Set,s=!1,u=0,a=[],l=new Set,d=new WeakMap,c=new WeakMap,v=new WeakMap,i=new WeakMap,p=new Set(["__proto__","constructor","prototype"]),y=(e,t,r)=>{let n=e.get(t);return n||(n=r(),e.set(t,n)),n},w=()=>{if(!s){s=!0;try{for(;0<o.size;){var e,t=o;o=new Set;for(e of t)e()}}finally{s=!1}}},g=e=>{o.delete(e);var t=c.get(e);if(t){for(var r of t)r.delete(e);t.clear(),c.delete(e)}},h=e=>{g(e),l.delete(e),d.delete(e);var t=v.get(e),t=(t&&(t=i.get(t))&&t.delete(e),v.delete(e),i.get(e));if(t){for(var r of t)h(r);t.clear(),i.delete(e)}},S=(e,n=Object.is)=>{let a=e,l=new Set,i=Symbol(),t=()=>{var e=f;return e&&(l.add(e),y(c,e,()=>new Set).add(l),y(d,e,()=>new Set).add(i)),a};return t.set=e=>{if(!n(a,e)){var t=f;if(t)if(d.get(t)?.has(i)&&!v.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)o.add(r);0!==u||s||w()}}},t.update=e=>{t.set(e(a))},t[r]=i,t},b=r=>{let n=()=>{if(!l.has(n)){l.add(n);var e=f;try{g(n),f=n;var t=d.get(n);t?t.clear():d.set(n,new Set),e&&(v.set(n,e),y(i,e,()=>new Set).add(n)),r()}finally{f=e,l.delete(n)}}};var e;return 0===u?n():(f&&(e=f,v.set(n,e),y(i,e,()=>new Set).add(n)),a.push(n)),()=>{h(n)}};var e=e=>{u++;try{return e()}catch(e){throw 1===u&&(o.clear(),a.length=0),e}finally{if(0===--u){if(0<a.length){var t,e=a;a=[];for(t of e)t()}0<o.size&&!s&&w()}}},t=t=>{let r=void 0,n=!1,a=S(void 0);return b(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=S(void 0);return b(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 m=(e,t,r)=>{e=[...e];return e[t]=r,e},j=(e,t,r)=>{e={...e};return e[t]=r,e},N=e=>"number"==typeof e||!Number.isNaN(Number(e))?[]:{},O=(n,a,l,i)=>{if(l>=a.length)return i;if(null==n)return O({},a,l,i);var f=a[l];if(void 0===f)return n;if(Array.isArray(n)){var o=Number(f);if(l===a.length-1)return m(n,o,i);var s=[...n];let e=l+1,t=a[e],r=n[o];return null==r&&(r=void 0===t?{}:N(t)),s[o]=O(r,a,e,i),s}o=n;if(l===a.length-1)return j(o,f,i);let e=l+1,t=a[e],r=o[f];null==r&&(r=void 0===t?{}:N(t));s={...o};return s[f]=O(r,a,e,i),s};var _=(e,t)=>{let r=!1;let n=(()=>{let r=[],n=!1,a=new Proxy({},{get:(e,t)=>(n||"string"!=typeof t&&"number"!=typeof t||(p.has(String(t))?n=!0:r.push(t)),a)});try{t(a)}catch{}return n?[]:r})(),a=S(t(e())),l=a.set;return b(function(){if(!r){r=!0;try{a.set(t(e()))}finally{r=!1}}}),a.set=function(t){if(!r&&0!==n.length){r=!0;try{l(t),e.update(e=>O(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=S(e,t);return[k(r),{set:e=>r.set(e),update:e=>r.update(e)}]};export{t as derive,b as effect,_ as lens,M as protectedState,k as readonlyState,n as select,S as state,e as batch};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"author": "Denny Trebbin (nerdalytics)",
|
|
3
|
-
"description": "
|
|
3
|
+
"description": "Reactive dependency graph runtime for Node.js backends. Tracks dependencies between signals and propagates updates automatically.",
|
|
4
4
|
"devDependencies": {
|
|
5
5
|
"@biomejs/biome": "2.3.14",
|
|
6
6
|
"@types/node": "25.2.2",
|
|
@@ -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": [
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"url": "git+https://github.com/nerdalytics/beacon.git"
|
|
57
57
|
},
|
|
58
58
|
"scripts": {
|
|
59
|
-
"benchmark": "node --expose-gc 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",
|
|
@@ -94,5 +94,5 @@
|
|
|
94
94
|
"sideEffects": false,
|
|
95
95
|
"type": "module",
|
|
96
96
|
"types": "dist/src/index.d.ts",
|
|
97
|
-
"version": "1000.3.
|
|
97
|
+
"version": "1000.3.2"
|
|
98
98
|
}
|
package/src/index.ts
CHANGED
|
@@ -29,6 +29,20 @@ const subscriberDependencies: WeakMap<Subscriber, Set<Set<Subscriber>>> = new We
|
|
|
29
29
|
>()
|
|
30
30
|
const parentSubscriber: WeakMap<Subscriber, Subscriber> = new WeakMap<Subscriber, Subscriber>()
|
|
31
31
|
const childSubscribers: WeakMap<Subscriber, Set<Subscriber>> = new WeakMap<Subscriber, Set<Subscriber>>()
|
|
32
|
+
const DANGEROUS_KEYS: ReadonlySet<string> = new Set([
|
|
33
|
+
'__proto__',
|
|
34
|
+
'constructor',
|
|
35
|
+
'prototype',
|
|
36
|
+
])
|
|
37
|
+
|
|
38
|
+
const getOrCreate = <K extends object, V>(map: WeakMap<K, V>, key: K, factory: () => V): V => {
|
|
39
|
+
let value = map.get(key)
|
|
40
|
+
if (!value) {
|
|
41
|
+
value = factory()
|
|
42
|
+
map.set(key, value)
|
|
43
|
+
}
|
|
44
|
+
return value
|
|
45
|
+
}
|
|
32
46
|
|
|
33
47
|
const notifySubscribers = (): void => {
|
|
34
48
|
if (isNotifying) {
|
|
@@ -64,9 +78,30 @@ const cleanupEffect = (effect: Subscriber): void => {
|
|
|
64
78
|
}
|
|
65
79
|
}
|
|
66
80
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
81
|
+
const disposeEffect = (effect: Subscriber): void => {
|
|
82
|
+
cleanupEffect(effect)
|
|
83
|
+
activeSubscribers.delete(effect)
|
|
84
|
+
stateTracking.delete(effect)
|
|
85
|
+
|
|
86
|
+
const parent = parentSubscriber.get(effect)
|
|
87
|
+
if (parent) {
|
|
88
|
+
const siblings = childSubscribers.get(parent)
|
|
89
|
+
if (siblings) {
|
|
90
|
+
siblings.delete(effect)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
parentSubscriber.delete(effect)
|
|
94
|
+
|
|
95
|
+
const children = childSubscribers.get(effect)
|
|
96
|
+
if (children) {
|
|
97
|
+
for (const child of children) {
|
|
98
|
+
disposeEffect(child)
|
|
99
|
+
}
|
|
100
|
+
children.clear()
|
|
101
|
+
childSubscribers.delete(effect)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
70
105
|
const createState = <T>(initialValue: T, equalityFn: (a: T, b: T) => boolean = Object.is): State<T> => {
|
|
71
106
|
let value = initialValue
|
|
72
107
|
const subscribers = new Set<Subscriber>()
|
|
@@ -77,19 +112,9 @@ const createState = <T>(initialValue: T, equalityFn: (a: T, b: T) => boolean = O
|
|
|
77
112
|
if (currentEffect) {
|
|
78
113
|
subscribers.add(currentEffect)
|
|
79
114
|
|
|
80
|
-
|
|
81
|
-
if (!dependencies) {
|
|
82
|
-
dependencies = new Set()
|
|
83
|
-
subscriberDependencies.set(currentEffect, dependencies)
|
|
84
|
-
}
|
|
85
|
-
dependencies.add(subscribers)
|
|
115
|
+
getOrCreate(subscriberDependencies, currentEffect, () => new Set()).add(subscribers)
|
|
86
116
|
|
|
87
|
-
|
|
88
|
-
if (!readStates) {
|
|
89
|
-
readStates = new Set()
|
|
90
|
-
stateTracking.set(currentEffect, readStates)
|
|
91
|
-
}
|
|
92
|
-
readStates.add(stateId)
|
|
117
|
+
getOrCreate(stateTracking, currentEffect, () => new Set()).add(stateId)
|
|
93
118
|
}
|
|
94
119
|
return value
|
|
95
120
|
}
|
|
@@ -130,9 +155,6 @@ const createState = <T>(initialValue: T, equalityFn: (a: T, b: T) => boolean = O
|
|
|
130
155
|
return get as State<T>
|
|
131
156
|
}
|
|
132
157
|
|
|
133
|
-
/**
|
|
134
|
-
* Registers a function to run whenever its reactive dependencies change.
|
|
135
|
-
*/
|
|
136
158
|
const createEffect = (fn: () => void): Unsubscribe => {
|
|
137
159
|
const runEffect = (): void => {
|
|
138
160
|
if (activeSubscribers.has(runEffect)) {
|
|
@@ -146,16 +168,16 @@ const createEffect = (fn: () => void): Unsubscribe => {
|
|
|
146
168
|
cleanupEffect(runEffect)
|
|
147
169
|
|
|
148
170
|
currentSubscriber = runEffect
|
|
149
|
-
stateTracking.
|
|
171
|
+
const existingStates = stateTracking.get(runEffect)
|
|
172
|
+
if (existingStates) {
|
|
173
|
+
existingStates.clear()
|
|
174
|
+
} else {
|
|
175
|
+
stateTracking.set(runEffect, new Set())
|
|
176
|
+
}
|
|
150
177
|
|
|
151
178
|
if (parentEffect) {
|
|
152
179
|
parentSubscriber.set(runEffect, parentEffect)
|
|
153
|
-
|
|
154
|
-
if (!children) {
|
|
155
|
-
children = new Set()
|
|
156
|
-
childSubscribers.set(parentEffect, children)
|
|
157
|
-
}
|
|
158
|
-
children.add(runEffect)
|
|
180
|
+
getOrCreate(childSubscribers, parentEffect, () => new Set()).add(runEffect)
|
|
159
181
|
}
|
|
160
182
|
|
|
161
183
|
fn()
|
|
@@ -171,46 +193,17 @@ const createEffect = (fn: () => void): Unsubscribe => {
|
|
|
171
193
|
if (currentSubscriber) {
|
|
172
194
|
const parent = currentSubscriber
|
|
173
195
|
parentSubscriber.set(runEffect, parent)
|
|
174
|
-
|
|
175
|
-
if (!children) {
|
|
176
|
-
children = new Set()
|
|
177
|
-
childSubscribers.set(parent, children)
|
|
178
|
-
}
|
|
179
|
-
children.add(runEffect)
|
|
196
|
+
getOrCreate(childSubscribers, parent, () => new Set()).add(runEffect)
|
|
180
197
|
}
|
|
181
198
|
|
|
182
199
|
deferredEffectCreations.push(runEffect)
|
|
183
200
|
}
|
|
184
201
|
|
|
185
202
|
return (): void => {
|
|
186
|
-
|
|
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)
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
parentSubscriber.delete(runEffect)
|
|
199
|
-
|
|
200
|
-
const children = childSubscribers.get(runEffect)
|
|
201
|
-
if (children) {
|
|
202
|
-
for (const child of children) {
|
|
203
|
-
cleanupEffect(child)
|
|
204
|
-
}
|
|
205
|
-
children.clear()
|
|
206
|
-
childSubscribers.delete(runEffect)
|
|
207
|
-
}
|
|
203
|
+
disposeEffect(runEffect)
|
|
208
204
|
}
|
|
209
205
|
}
|
|
210
206
|
|
|
211
|
-
/**
|
|
212
|
-
* Groups multiple state updates to trigger effects only once at the end.
|
|
213
|
-
*/
|
|
214
207
|
const executeBatch = <T>(fn: () => T): T => {
|
|
215
208
|
batchDepth++
|
|
216
209
|
try {
|
|
@@ -240,87 +233,69 @@ const executeBatch = <T>(fn: () => T): T => {
|
|
|
240
233
|
}
|
|
241
234
|
}
|
|
242
235
|
|
|
243
|
-
/**
|
|
244
|
-
* Creates a read-only computed value that updates when its dependencies change.
|
|
245
|
-
*/
|
|
246
236
|
const createDerive = <T>(computeFn: () => T): ReadOnlyState<T> => {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
initialized: false,
|
|
251
|
-
valueState: createState<T | undefined>(undefined),
|
|
252
|
-
}
|
|
237
|
+
let cachedValue: T = undefined as unknown as T
|
|
238
|
+
let initialized = false
|
|
239
|
+
const valueState = createState<T | undefined>(undefined)
|
|
253
240
|
|
|
254
241
|
createEffect(function deriveEffect(): void {
|
|
255
|
-
const newValue =
|
|
242
|
+
const newValue = computeFn()
|
|
256
243
|
|
|
257
|
-
if (!(
|
|
258
|
-
|
|
259
|
-
|
|
244
|
+
if (!(initialized && Object.is(cachedValue, newValue))) {
|
|
245
|
+
cachedValue = newValue
|
|
246
|
+
valueState.set(newValue)
|
|
260
247
|
}
|
|
261
248
|
|
|
262
|
-
|
|
249
|
+
initialized = true
|
|
263
250
|
})
|
|
264
251
|
|
|
265
252
|
return function deriveGetter(): T {
|
|
266
|
-
if (!
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
253
|
+
if (!initialized) {
|
|
254
|
+
cachedValue = computeFn()
|
|
255
|
+
initialized = true
|
|
256
|
+
valueState.set(cachedValue)
|
|
270
257
|
}
|
|
271
|
-
return
|
|
258
|
+
return valueState() as T
|
|
272
259
|
}
|
|
273
260
|
}
|
|
274
261
|
|
|
275
|
-
/**
|
|
276
|
-
* Creates an efficient subscription to a subset of a state value.
|
|
277
|
-
*/
|
|
278
262
|
const createSelect = <T, R>(
|
|
279
263
|
source: ReadOnlyState<T>,
|
|
280
264
|
selectorFn: (state: T) => R,
|
|
281
265
|
equalityFn: (a: R, b: R) => boolean = Object.is
|
|
282
266
|
): ReadOnlyState<R> => {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
lastSourceValue: undefined as T | undefined,
|
|
288
|
-
selectorFn,
|
|
289
|
-
source,
|
|
290
|
-
valueState: createState<R | undefined>(undefined),
|
|
291
|
-
}
|
|
267
|
+
let initialized = false
|
|
268
|
+
let lastSelectedValue: R | undefined
|
|
269
|
+
let lastSourceValue: T | undefined
|
|
270
|
+
const valueState = createState<R | undefined>(undefined)
|
|
292
271
|
|
|
293
272
|
createEffect(function selectEffect(): void {
|
|
294
|
-
const sourceValue =
|
|
273
|
+
const sourceValue = source()
|
|
295
274
|
|
|
296
|
-
if (
|
|
275
|
+
if (initialized && Object.is(lastSourceValue, sourceValue)) {
|
|
297
276
|
return
|
|
298
277
|
}
|
|
299
278
|
|
|
300
|
-
|
|
301
|
-
const newSelectedValue =
|
|
279
|
+
lastSourceValue = sourceValue
|
|
280
|
+
const newSelectedValue = selectorFn(sourceValue)
|
|
302
281
|
|
|
303
|
-
if (
|
|
304
|
-
container.initialized &&
|
|
305
|
-
container.lastSelectedValue !== undefined &&
|
|
306
|
-
container.equalityFn(container.lastSelectedValue, newSelectedValue)
|
|
307
|
-
) {
|
|
282
|
+
if (initialized && lastSelectedValue !== undefined && equalityFn(lastSelectedValue, newSelectedValue)) {
|
|
308
283
|
return
|
|
309
284
|
}
|
|
310
285
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
286
|
+
lastSelectedValue = newSelectedValue
|
|
287
|
+
valueState.set(newSelectedValue)
|
|
288
|
+
initialized = true
|
|
314
289
|
})
|
|
315
290
|
|
|
316
291
|
return function selectGetter(): R {
|
|
317
|
-
if (!
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
292
|
+
if (!initialized) {
|
|
293
|
+
lastSourceValue = source()
|
|
294
|
+
lastSelectedValue = selectorFn(lastSourceValue)
|
|
295
|
+
valueState.set(lastSelectedValue)
|
|
296
|
+
initialized = true
|
|
322
297
|
}
|
|
323
|
-
return
|
|
298
|
+
return valueState() as R
|
|
324
299
|
}
|
|
325
300
|
}
|
|
326
301
|
|
|
@@ -352,101 +327,79 @@ const createContainer = (key: string | number): Record<string | number, unknown>
|
|
|
352
327
|
return isArrayKey ? [] : {}
|
|
353
328
|
}
|
|
354
329
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
if (pathSegments.length === 1) {
|
|
360
|
-
return updateArrayItem(array, index, value)
|
|
330
|
+
const setValueAtPath = <V, O>(obj: O, pathSegments: (string | number)[], depth: number, value: V): O => {
|
|
331
|
+
if (depth >= pathSegments.length) {
|
|
332
|
+
return value as unknown as O
|
|
361
333
|
}
|
|
362
334
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
]
|
|
366
|
-
const nextPathSegments = pathSegments.slice(1)
|
|
367
|
-
const nextKey = nextPathSegments[0]
|
|
368
|
-
|
|
369
|
-
let nextValue = array[index]
|
|
370
|
-
if (nextValue === undefined || nextValue === null) {
|
|
371
|
-
nextValue = nextKey !== undefined ? createContainer(nextKey) : {}
|
|
335
|
+
if (obj === undefined || obj === null) {
|
|
336
|
+
return setValueAtPath({} as O, pathSegments, depth, value)
|
|
372
337
|
}
|
|
373
338
|
|
|
374
|
-
|
|
375
|
-
return copy
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// Helper for handling object path updates
|
|
379
|
-
const updateObjectPath = <V>(
|
|
380
|
-
obj: Record<string | number, unknown>,
|
|
381
|
-
pathSegments: (string | number)[],
|
|
382
|
-
value: V
|
|
383
|
-
): Record<string | number, unknown> => {
|
|
384
|
-
const currentKey = pathSegments[0]
|
|
339
|
+
const currentKey = pathSegments[depth]
|
|
385
340
|
if (currentKey === undefined) {
|
|
386
341
|
return obj
|
|
387
342
|
}
|
|
388
343
|
|
|
389
|
-
if (
|
|
390
|
-
|
|
391
|
-
}
|
|
344
|
+
if (Array.isArray(obj)) {
|
|
345
|
+
const index = Number(currentKey)
|
|
392
346
|
|
|
393
|
-
|
|
394
|
-
|
|
347
|
+
if (depth === pathSegments.length - 1) {
|
|
348
|
+
return updateArrayItem(obj, index, value) as unknown as O
|
|
349
|
+
}
|
|
395
350
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
351
|
+
const copy = [
|
|
352
|
+
...obj,
|
|
353
|
+
]
|
|
354
|
+
const nextDepth = depth + 1
|
|
355
|
+
const nextKey = pathSegments[nextDepth]
|
|
400
356
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
return result
|
|
406
|
-
}
|
|
357
|
+
let nextValue = obj[index]
|
|
358
|
+
if (nextValue === undefined || nextValue === null) {
|
|
359
|
+
nextValue = nextKey === undefined ? {} : createContainer(nextKey)
|
|
360
|
+
}
|
|
407
361
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
return value as unknown as O
|
|
362
|
+
copy[index] = setValueAtPath(nextValue, pathSegments, nextDepth, value)
|
|
363
|
+
return copy as unknown as O
|
|
411
364
|
}
|
|
412
365
|
|
|
413
|
-
|
|
414
|
-
return setValueAtPath({} as O, pathSegments, value)
|
|
415
|
-
}
|
|
366
|
+
const record = obj as Record<string | number, unknown>
|
|
416
367
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
return obj
|
|
368
|
+
if (depth === pathSegments.length - 1) {
|
|
369
|
+
return updateShallowProperty(record, currentKey, value) as unknown as O
|
|
420
370
|
}
|
|
421
371
|
|
|
422
|
-
|
|
423
|
-
|
|
372
|
+
const nextDepth = depth + 1
|
|
373
|
+
const nextKey = pathSegments[nextDepth]
|
|
374
|
+
|
|
375
|
+
let currentValue = record[currentKey]
|
|
376
|
+
if (currentValue === undefined || currentValue === null) {
|
|
377
|
+
currentValue = nextKey === undefined ? {} : createContainer(nextKey)
|
|
424
378
|
}
|
|
425
379
|
|
|
426
|
-
|
|
380
|
+
const result = {
|
|
381
|
+
...record,
|
|
382
|
+
}
|
|
383
|
+
result[currentKey] = setValueAtPath(currentValue, pathSegments, nextDepth, value)
|
|
384
|
+
return result as unknown as O
|
|
427
385
|
}
|
|
428
386
|
|
|
429
|
-
/**
|
|
430
|
-
* Creates a lens for direct updates to nested properties of a state.
|
|
431
|
-
*/
|
|
432
387
|
const createLens = <T, K>(source: State<T>, accessor: (state: T) => K): State<K> => {
|
|
433
|
-
|
|
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
|
-
}
|
|
388
|
+
let isUpdating = false
|
|
441
389
|
|
|
442
390
|
const extractPath = (): (string | number)[] => {
|
|
443
391
|
const pathCollector: (string | number)[] = []
|
|
392
|
+
let tainted = false
|
|
444
393
|
const proxy = new Proxy(
|
|
445
394
|
{},
|
|
446
395
|
{
|
|
447
396
|
get: (_: object, prop: string | symbol): unknown => {
|
|
448
|
-
if (typeof prop === 'string' || typeof prop === 'number') {
|
|
449
|
-
|
|
397
|
+
if (!tainted && (typeof prop === 'string' || typeof prop === 'number')) {
|
|
398
|
+
if (DANGEROUS_KEYS.has(String(prop))) {
|
|
399
|
+
tainted = true
|
|
400
|
+
} else {
|
|
401
|
+
pathCollector.push(prop)
|
|
402
|
+
}
|
|
450
403
|
}
|
|
451
404
|
return proxy
|
|
452
405
|
},
|
|
@@ -454,65 +407,57 @@ const createLens = <T, K>(source: State<T>, accessor: (state: T) => K): State<K>
|
|
|
454
407
|
)
|
|
455
408
|
|
|
456
409
|
try {
|
|
457
|
-
|
|
410
|
+
accessor(proxy as unknown as T)
|
|
458
411
|
} catch {
|
|
459
412
|
// Ignore errors, we're just collecting the path
|
|
460
413
|
}
|
|
461
414
|
|
|
462
|
-
return pathCollector
|
|
415
|
+
return tainted ? [] : pathCollector
|
|
463
416
|
}
|
|
464
417
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
container.originalSet = container.lensState.set
|
|
418
|
+
const path = extractPath()
|
|
419
|
+
const lensState = createState<K>(accessor(source()))
|
|
420
|
+
const originalSet = lensState.set
|
|
469
421
|
|
|
470
422
|
createEffect(function lensEffect(): void {
|
|
471
|
-
if (
|
|
423
|
+
if (isUpdating) {
|
|
472
424
|
return
|
|
473
425
|
}
|
|
474
426
|
|
|
475
|
-
|
|
427
|
+
isUpdating = true
|
|
476
428
|
try {
|
|
477
|
-
|
|
429
|
+
lensState.set(accessor(source()))
|
|
478
430
|
} finally {
|
|
479
|
-
|
|
431
|
+
isUpdating = false
|
|
480
432
|
}
|
|
481
433
|
})
|
|
482
434
|
|
|
483
|
-
|
|
484
|
-
if (
|
|
435
|
+
lensState.set = function lensSet(value: K): void {
|
|
436
|
+
if (isUpdating || path.length === 0) {
|
|
485
437
|
return
|
|
486
438
|
}
|
|
487
439
|
|
|
488
|
-
|
|
440
|
+
isUpdating = true
|
|
489
441
|
try {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
container.source.update((current: T): T => setValueAtPath(current, container.path, value))
|
|
442
|
+
originalSet(value)
|
|
443
|
+
source.update((current: T): T => setValueAtPath(current, path, 0, value))
|
|
493
444
|
} finally {
|
|
494
|
-
|
|
445
|
+
isUpdating = false
|
|
495
446
|
}
|
|
496
447
|
}
|
|
497
448
|
|
|
498
|
-
|
|
499
|
-
|
|
449
|
+
lensState.update = function lensUpdate(fn: (value: K) => K): void {
|
|
450
|
+
lensState.set(fn(lensState()))
|
|
500
451
|
}
|
|
501
452
|
|
|
502
|
-
return
|
|
453
|
+
return lensState
|
|
503
454
|
}
|
|
504
455
|
|
|
505
|
-
/**
|
|
506
|
-
* Creates a read-only view of a state, hiding mutation methods.
|
|
507
|
-
*/
|
|
508
456
|
const createReadonlyState =
|
|
509
457
|
<T>(source: State<T>): ReadOnlyState<T> =>
|
|
510
458
|
(): T =>
|
|
511
459
|
source()
|
|
512
460
|
|
|
513
|
-
/**
|
|
514
|
-
* Creates a state with access control, returning a tuple of reader and writer.
|
|
515
|
-
*/
|
|
516
461
|
const createProtectedState = <T>(
|
|
517
462
|
initialValue: T,
|
|
518
463
|
equalityFn: (a: T, b: T) => boolean = Object.is
|
|
@@ -521,8 +466,9 @@ const createProtectedState = <T>(
|
|
|
521
466
|
WriteableState<T>,
|
|
522
467
|
] => {
|
|
523
468
|
const fullState = createState(initialValue, equalityFn)
|
|
469
|
+
const reader = createReadonlyState(fullState)
|
|
524
470
|
return [
|
|
525
|
-
|
|
471
|
+
reader,
|
|
526
472
|
{
|
|
527
473
|
set: (value: T): void => fullState.set(value),
|
|
528
474
|
update: (fn: (value: T) => T): void => fullState.update(fn),
|
|
@@ -530,6 +476,7 @@ const createProtectedState = <T>(
|
|
|
530
476
|
]
|
|
531
477
|
}
|
|
532
478
|
|
|
479
|
+
export type { ReadOnlyState, State, Unsubscribe, WriteableState }
|
|
533
480
|
export {
|
|
534
481
|
createDerive as derive,
|
|
535
482
|
createEffect as effect,
|
|
@@ -540,5 +487,3 @@ export {
|
|
|
540
487
|
createState as state,
|
|
541
488
|
executeBatch as batch,
|
|
542
489
|
}
|
|
543
|
-
|
|
544
|
-
export type { ReadOnlyState, State, Unsubscribe, WriteableState }
|