@nerdalytics/beacon 1000.3.1 → 1000.3.3
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 -4
- package/dist/src/index.min.js +1 -1
- package/package.json +13 -35
- package/src/index.ts +83 -80
package/README.md
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
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)
|
|
7
|
-
[](https://socket.dev/npm/package/@nerdalytics/beacon/overview/1000.3.3)
|
|
8
8
|
|
|
9
9
|
[](https://nodejs.org/)
|
|
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
|
```
|
|
@@ -39,6 +37,16 @@ count.set(5);
|
|
|
39
37
|
Full documentation, API reference, and examples available at:
|
|
40
38
|
**[nerdalytics.github.io/beacon](https://nerdalytics.github.io/beacon/)**
|
|
41
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)).
|
|
49
|
+
|
|
42
50
|
## License
|
|
43
51
|
|
|
44
52
|
MIT - See [LICENSE](./LICENSE) for details.
|
package/dist/src/index.d.ts
CHANGED
|
@@ -4,10 +4,7 @@ interface WriteableState<T> {
|
|
|
4
4
|
set(value: T): void;
|
|
5
5
|
update(fn: (value: T) => T): void;
|
|
6
6
|
}
|
|
7
|
-
|
|
8
|
-
type State<T> = ReadOnlyState<T> & WriteableState<T> & {
|
|
9
|
-
[STATE_ID]?: symbol;
|
|
10
|
-
};
|
|
7
|
+
type State<T> = ReadOnlyState<T> & WriteableState<T>;
|
|
11
8
|
declare const createState: <T>(initialValue: T, equalityFn?: (a: T, b: T) => boolean) => State<T>;
|
|
12
9
|
declare const createEffect: (fn: () => void) => Unsubscribe;
|
|
13
10
|
declare const executeBatch: <T>(fn: () => T) => T;
|
package/dist/src/index.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
let
|
|
1
|
+
let o=null,r=new Set,a=new Set,s=a,u=!1,c=0,n=[],f=new Set,i=new WeakMap,v=new WeakMap,d=new WeakMap,l=new WeakMap,w=new Set(["__proto__","constructor","prototype"]),p=(e,t,r)=>{let a=e.get(t);return a||(a=r(),e.set(t,a)),a},y=()=>{if(!u){u=!0;try{for(;0<s.size;){var e,t=s;s=t===a?r:a;for(e of t)e();t.clear()}}finally{u=!1}}},S=e=>{s.delete(e);var t=v.get(e);if(t){for(var r of t)r.delete(e);t.clear(),v.delete(e)}},g=e=>{S(e),f.delete(e),i.delete(e);var t=d.get(e),t=(t&&(t=l.get(t))&&t.delete(e),d.delete(e),l.get(e));if(t){for(var r of t)g(r);t.clear(),l.delete(e)}},h=(e,a=Object.is)=>{let n=e,f=new Set,l=Symbol(),t=()=>{var e=o;return e&&(f.add(e),p(v,e,()=>new Set).add(f),p(i,e,()=>new Set).add(l)),n};return t.set=e=>{if(!a(n,e)){var t=o;if(t)if(i.get(t)?.has(l)&&!d.get(t))throw new Error("Infinite loop detected: effect() cannot update a state() it depends on!");if(n=e,0!==f.size){for(var r of f)s.add(r);0!==c||u||y()}}},t.update=e=>{t.set(e(n))},t},b=r=>{let a=()=>{if(!f.has(a)){f.add(a);var e=o;try{S(a),o=a;var t=i.get(a);t?t.clear():i.set(a,new Set),e&&(d.set(a,e),p(l,e,()=>new Set).add(a)),r()}finally{o=e,f.delete(a)}}};var e;return 0===c?a():(o&&(e=o,d.set(a,e),p(l,e,()=>new Set).add(a)),n.push(a)),()=>{g(a)}};var e=e=>{c++;try{return e()}catch(e){throw 1===c&&(s.clear(),n.length=0),e}finally{if(0===--c){if(0<n.length){var t,e=n;n=[];for(t of e)t()}0<s.size&&!u&&y()}}},t=r=>{let a=void 0,n=!1,f=new Set;return b(function(){var e=r();if(!n||!Object.is(a,e)){a=e;for(var t of f)s.add(t);0!==c||u||y()}n=!0}),function(){var e=o;return e&&(f.add(e),p(v,e,()=>new Set).add(f)),n||(a=r(),n=!0),a}},j=(r,a,n=Object.is)=>{let f=!1,l,i,d=new Set;return b(function(){var e=r();if(!f||!Object.is(i,e)){i=e;e=a(e);if(!f||void 0===l||!n(l,e)){l=e,f=!0;for(var t of d)s.add(t);0!==c||u||y()}}}),function(){var e=o;return e&&(d.add(e),p(v,e,()=>new Set).add(d)),f||(i=r(),l=a(i),f=!0),l}};let N=(e,t)=>null!=e?e:void 0===t||Number.isNaN(Number(t))?{}:[],O=(t,e,r,a)=>{if(r>=e.length)return a;if(null==t)return O({},e,r,a);var n=e[r];if(void 0===n)return t;var f=Array.isArray(t),n=f?Number(n):n;if(r===e.length-1){if(f)return(l=[...t])[n]=a,l;let e={...t};return e[n]=a,e}var l=r+1,r=e[l],i=t[n],i=N(i,r);if(f)return(r=[...t])[n]=O(i,e,l,a),r;let d={...t};return d[n]=O(i,e,l,a),d};var k=(e,t)=>{let r=!1;let a=(()=>{let r=[],a=!1,n=new Proxy({},{get:(e,t)=>(a||"string"!=typeof t||(w.has(String(t))?a=!0:r.push(t)),n)});try{t(n)}catch{}return a?[]:r})(),n=h(t(e())),f=n.set;return b(function(){if(!r){r=!0;try{n.set(t(e()))}finally{r=!1}}}),n.set=function(t){if(!r&&0!==a.length){r=!0;try{f(t),e.update(e=>O(e,a,0,t))}finally{r=!1}}},n.update=function(e){n.set(e(n()))},n};let m=e=>()=>e();var M=(e,t=Object.is)=>{let r=h(e,t);return[m(r),{set:e=>r.set(e),update:e=>r.update(e)}]};export{t as derive,b as effect,k as lens,M as protectedState,m as readonlyState,j as select,h as state,e as batch};
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
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
|
-
"@biomejs/biome": "2.
|
|
6
|
-
"@types/node": "25.
|
|
7
|
-
"npm-check-updates": "
|
|
8
|
-
"typescript": "
|
|
5
|
+
"@biomejs/biome": "2.4.11",
|
|
6
|
+
"@types/node": "25.5.2",
|
|
7
|
+
"npm-check-updates": "20.0.0",
|
|
8
|
+
"typescript": "6.0.2",
|
|
9
9
|
"uglify-js": "3.19.3"
|
|
10
10
|
},
|
|
11
11
|
"engines": {
|
|
@@ -32,32 +32,25 @@
|
|
|
32
32
|
],
|
|
33
33
|
"keywords": [
|
|
34
34
|
"backend",
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"effects",
|
|
39
|
-
"fine-grained",
|
|
40
|
-
"lightweight",
|
|
41
|
-
"memory-management",
|
|
35
|
+
"batch",
|
|
36
|
+
"dependency-graph",
|
|
37
|
+
"effect",
|
|
42
38
|
"nodejs",
|
|
43
|
-
"performance",
|
|
44
39
|
"reactive",
|
|
45
|
-
"server-side",
|
|
46
40
|
"signals",
|
|
47
|
-
"state
|
|
48
|
-
"typescript"
|
|
41
|
+
"state"
|
|
49
42
|
],
|
|
50
43
|
"license": "MIT",
|
|
51
44
|
"main": "dist/src/index.min.js",
|
|
52
45
|
"name": "@nerdalytics/beacon",
|
|
53
|
-
"packageManager": "npm@11.
|
|
46
|
+
"packageManager": "npm@11.12.1",
|
|
54
47
|
"repository": {
|
|
55
48
|
"type": "git",
|
|
56
49
|
"url": "git+https://github.com/nerdalytics/beacon.git"
|
|
57
50
|
},
|
|
58
51
|
"scripts": {
|
|
59
52
|
"benchmark": "node --expose-gc scripts/benchmark.ts -R 3",
|
|
60
|
-
|
|
53
|
+
|
|
61
54
|
"build": "npm run build:lts",
|
|
62
55
|
"build:lts": "tsc -p tsconfig.lts.json",
|
|
63
56
|
"check": "npx @biomejs/biome check",
|
|
@@ -71,28 +64,13 @@
|
|
|
71
64
|
"prepublishOnly": "npm run build:lts",
|
|
72
65
|
"pretest:lts": "node scripts/run-lts-tests.js",
|
|
73
66
|
"test": "node --test --test-skip-pattern=\"COMPONENT NAME\" tests/**/*.ts",
|
|
74
|
-
"test:behavior": "node --test tests/infinite-loop.test.ts tests/cyclic-dependency.test.ts tests/cleanup.test.ts",
|
|
75
|
-
"test:core": "node --test tests/*-core.test.ts",
|
|
76
67
|
"test:coverage": "node --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=lcov.info tests/**/*.test.ts",
|
|
77
|
-
"test:integration": "node --test tests/state-*.test.ts tests/batch-integration.test.ts",
|
|
78
68
|
"test:lts:20": "node --test dist/tests/**.js",
|
|
79
69
|
"test:lts:22": "node --test --test-skip-pattern=\"COMPONENT NAME\" dist/tests/**/*.js",
|
|
80
|
-
"
|
|
81
|
-
"test:unit:cleanup": "node --test tests/cleanup.test.ts",
|
|
82
|
-
"test:unit:custom-equality": "node --test tests/custom-equality.test.ts",
|
|
83
|
-
"test:unit:cyclic-dependency": "node --test tests/cyclic-dependency.test.ts",
|
|
84
|
-
"test:unit:deep-chain": "node --test tests/deep-chain.test.ts",
|
|
85
|
-
"test:unit:derive": "node --test tests/derive.test.ts",
|
|
86
|
-
"test:unit:effect": "node --test tests/effect.test.ts",
|
|
87
|
-
"test:unit:infinite-loop": "node --test tests/infinite-loop.test.ts",
|
|
88
|
-
"test:unit:lens": "node --test tests/lens.test.ts",
|
|
89
|
-
"test:unit:select": "node --test tests/select.test.ts",
|
|
90
|
-
"test:unit:state": "node --test tests/state.test.ts",
|
|
91
|
-
"update-dependencies": "npx npm-check-updates --interactive --upgrade --removeRange",
|
|
92
|
-
"update-performance-docs": "node --experimental-config-file=node.config.json scripts/update-performance-docs.ts"
|
|
70
|
+
"update-dependencies": "npx npm-check-updates --interactive --upgrade --removeRange"
|
|
93
71
|
},
|
|
94
72
|
"sideEffects": false,
|
|
95
73
|
"type": "module",
|
|
96
74
|
"types": "dist/src/index.d.ts",
|
|
97
|
-
"version": "1000.3.
|
|
75
|
+
"version": "1000.3.3"
|
|
98
76
|
}
|
package/src/index.ts
CHANGED
|
@@ -7,17 +7,13 @@ interface WriteableState<T> {
|
|
|
7
7
|
update(fn: (value: T) => T): void
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
const STATE_ID: unique symbol = Symbol('STATE_ID')
|
|
12
|
-
|
|
13
|
-
type State<T> = ReadOnlyState<T> &
|
|
14
|
-
WriteableState<T> & {
|
|
15
|
-
[STATE_ID]?: symbol
|
|
16
|
-
}
|
|
10
|
+
type State<T> = ReadOnlyState<T> & WriteableState<T>
|
|
17
11
|
|
|
18
12
|
// Module-level reactive state
|
|
19
13
|
let currentSubscriber: Subscriber | null = null
|
|
20
|
-
|
|
14
|
+
const flushing: Set<Subscriber> = new Set<Subscriber>()
|
|
15
|
+
const queued: Set<Subscriber> = new Set<Subscriber>()
|
|
16
|
+
let pendingSubscribers: Set<Subscriber> = queued
|
|
21
17
|
let isNotifying = false
|
|
22
18
|
let batchDepth = 0
|
|
23
19
|
let deferredEffectCreations: Subscriber[] = []
|
|
@@ -29,6 +25,11 @@ const subscriberDependencies: WeakMap<Subscriber, Set<Set<Subscriber>>> = new We
|
|
|
29
25
|
>()
|
|
30
26
|
const parentSubscriber: WeakMap<Subscriber, Subscriber> = new WeakMap<Subscriber, Subscriber>()
|
|
31
27
|
const childSubscribers: WeakMap<Subscriber, Set<Subscriber>> = new WeakMap<Subscriber, Set<Subscriber>>()
|
|
28
|
+
const DANGEROUS_KEYS: ReadonlySet<string> = new Set([
|
|
29
|
+
'__proto__',
|
|
30
|
+
'constructor',
|
|
31
|
+
'prototype',
|
|
32
|
+
])
|
|
32
33
|
|
|
33
34
|
const getOrCreate = <K extends object, V>(map: WeakMap<K, V>, key: K, factory: () => V): V => {
|
|
34
35
|
let value = map.get(key)
|
|
@@ -49,11 +50,12 @@ const notifySubscribers = (): void => {
|
|
|
49
50
|
try {
|
|
50
51
|
while (pendingSubscribers.size > 0) {
|
|
51
52
|
const subscribers = pendingSubscribers
|
|
52
|
-
pendingSubscribers =
|
|
53
|
+
pendingSubscribers = subscribers === queued ? flushing : queued
|
|
53
54
|
|
|
54
55
|
for (const effect of subscribers) {
|
|
55
56
|
effect()
|
|
56
57
|
}
|
|
58
|
+
subscribers.clear()
|
|
57
59
|
}
|
|
58
60
|
} finally {
|
|
59
61
|
isNotifying = false
|
|
@@ -146,7 +148,6 @@ const createState = <T>(initialValue: T, equalityFn: (a: T, b: T) => boolean = O
|
|
|
146
148
|
get.set(fn(value))
|
|
147
149
|
}
|
|
148
150
|
|
|
149
|
-
get[STATE_ID] = stateId
|
|
150
151
|
return get as State<T>
|
|
151
152
|
}
|
|
152
153
|
|
|
@@ -231,26 +232,37 @@ const executeBatch = <T>(fn: () => T): T => {
|
|
|
231
232
|
const createDerive = <T>(computeFn: () => T): ReadOnlyState<T> => {
|
|
232
233
|
let cachedValue: T = undefined as unknown as T
|
|
233
234
|
let initialized = false
|
|
234
|
-
const
|
|
235
|
+
const subscribers = new Set<Subscriber>()
|
|
235
236
|
|
|
236
237
|
createEffect(function deriveEffect(): void {
|
|
237
238
|
const newValue = computeFn()
|
|
238
239
|
|
|
239
240
|
if (!(initialized && Object.is(cachedValue, newValue))) {
|
|
240
241
|
cachedValue = newValue
|
|
241
|
-
|
|
242
|
+
|
|
243
|
+
for (const sub of subscribers) {
|
|
244
|
+
pendingSubscribers.add(sub)
|
|
245
|
+
}
|
|
246
|
+
if (batchDepth === 0 && !isNotifying) {
|
|
247
|
+
notifySubscribers()
|
|
248
|
+
}
|
|
242
249
|
}
|
|
243
250
|
|
|
244
251
|
initialized = true
|
|
245
252
|
})
|
|
246
253
|
|
|
247
254
|
return function deriveGetter(): T {
|
|
255
|
+
const currentEffect = currentSubscriber
|
|
256
|
+
if (currentEffect) {
|
|
257
|
+
subscribers.add(currentEffect)
|
|
258
|
+
getOrCreate(subscriberDependencies, currentEffect, () => new Set()).add(subscribers)
|
|
259
|
+
}
|
|
260
|
+
|
|
248
261
|
if (!initialized) {
|
|
249
262
|
cachedValue = computeFn()
|
|
250
263
|
initialized = true
|
|
251
|
-
valueState.set(cachedValue)
|
|
252
264
|
}
|
|
253
|
-
return
|
|
265
|
+
return cachedValue
|
|
254
266
|
}
|
|
255
267
|
}
|
|
256
268
|
|
|
@@ -262,7 +274,7 @@ const createSelect = <T, R>(
|
|
|
262
274
|
let initialized = false
|
|
263
275
|
let lastSelectedValue: R | undefined
|
|
264
276
|
let lastSourceValue: T | undefined
|
|
265
|
-
const
|
|
277
|
+
const subscribers = new Set<Subscriber>()
|
|
266
278
|
|
|
267
279
|
createEffect(function selectEffect(): void {
|
|
268
280
|
const sourceValue = source()
|
|
@@ -279,55 +291,45 @@ const createSelect = <T, R>(
|
|
|
279
291
|
}
|
|
280
292
|
|
|
281
293
|
lastSelectedValue = newSelectedValue
|
|
282
|
-
valueState.set(newSelectedValue)
|
|
283
294
|
initialized = true
|
|
295
|
+
|
|
296
|
+
for (const sub of subscribers) {
|
|
297
|
+
pendingSubscribers.add(sub)
|
|
298
|
+
}
|
|
299
|
+
if (batchDepth === 0 && !isNotifying) {
|
|
300
|
+
notifySubscribers()
|
|
301
|
+
}
|
|
284
302
|
})
|
|
285
303
|
|
|
286
304
|
return function selectGetter(): R {
|
|
305
|
+
const currentEffect = currentSubscriber
|
|
306
|
+
if (currentEffect) {
|
|
307
|
+
subscribers.add(currentEffect)
|
|
308
|
+
getOrCreate(subscriberDependencies, currentEffect, () => new Set()).add(subscribers)
|
|
309
|
+
}
|
|
310
|
+
|
|
287
311
|
if (!initialized) {
|
|
288
312
|
lastSourceValue = source()
|
|
289
313
|
lastSelectedValue = selectorFn(lastSourceValue)
|
|
290
|
-
valueState.set(lastSelectedValue)
|
|
291
314
|
initialized = true
|
|
292
315
|
}
|
|
293
|
-
return
|
|
316
|
+
return lastSelectedValue as R
|
|
294
317
|
}
|
|
295
318
|
}
|
|
296
319
|
|
|
297
|
-
//
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
copy[index] = value
|
|
303
|
-
return copy
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// Helper for single-level updates (optimization)
|
|
307
|
-
const updateShallowProperty = <V>(
|
|
308
|
-
obj: Record<string | number, unknown>,
|
|
309
|
-
key: string | number,
|
|
310
|
-
value: V
|
|
311
|
-
): Record<string | number, unknown> => {
|
|
312
|
-
const result = {
|
|
313
|
-
...obj,
|
|
314
|
-
}
|
|
315
|
-
result[key] = value
|
|
316
|
-
return result
|
|
320
|
+
// Returns the value if non-nullish, otherwise creates the appropriate container type
|
|
321
|
+
const ensureContainer = (value: unknown, nextKey: string | undefined): unknown => {
|
|
322
|
+
if (value != null) return value
|
|
323
|
+
if (nextKey !== undefined && !Number.isNaN(Number(nextKey))) return []
|
|
324
|
+
return {}
|
|
317
325
|
}
|
|
318
326
|
|
|
319
|
-
|
|
320
|
-
const createContainer = (key: string | number): Record<string | number, unknown> | unknown[] => {
|
|
321
|
-
const isArrayKey = typeof key === 'number' || !Number.isNaN(Number(key))
|
|
322
|
-
return isArrayKey ? [] : {}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
const setValueAtPath = <V, O>(obj: O, pathSegments: (string | number)[], depth: number, value: V): O => {
|
|
327
|
+
const setValueAtPath = <V, O>(obj: O, pathSegments: string[], depth: number, value: V): O => {
|
|
326
328
|
if (depth >= pathSegments.length) {
|
|
327
329
|
return value as unknown as O
|
|
328
330
|
}
|
|
329
331
|
|
|
330
|
-
if (obj
|
|
332
|
+
if (obj == null) {
|
|
331
333
|
return setValueAtPath({} as O, pathSegments, depth, value)
|
|
332
334
|
}
|
|
333
335
|
|
|
@@ -336,60 +338,61 @@ const setValueAtPath = <V, O>(obj: O, pathSegments: (string | number)[], depth:
|
|
|
336
338
|
return obj
|
|
337
339
|
}
|
|
338
340
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
+
const isArray = Array.isArray(obj)
|
|
342
|
+
const key = isArray ? Number(currentKey) : currentKey
|
|
341
343
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
+
if (depth === pathSegments.length - 1) {
|
|
345
|
+
if (isArray) {
|
|
346
|
+
const copy = [
|
|
347
|
+
...(obj as unknown[]),
|
|
348
|
+
]
|
|
349
|
+
copy[key as number] = value
|
|
350
|
+
return copy as unknown as O
|
|
344
351
|
}
|
|
345
|
-
|
|
346
|
-
|
|
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)
|
|
352
|
+
const result = {
|
|
353
|
+
...(obj as Record<string, unknown>),
|
|
355
354
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
return copy as unknown as O
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
const record = obj as Record<string | number, unknown>
|
|
362
|
-
|
|
363
|
-
if (depth === pathSegments.length - 1) {
|
|
364
|
-
return updateShallowProperty(record, currentKey, value) as unknown as O
|
|
355
|
+
result[key] = value
|
|
356
|
+
return result as unknown as O
|
|
365
357
|
}
|
|
366
358
|
|
|
367
359
|
const nextDepth = depth + 1
|
|
368
360
|
const nextKey = pathSegments[nextDepth]
|
|
361
|
+
const source = isArray ? (obj as unknown[])[key as number] : (obj as Record<string | number, unknown>)[key]
|
|
362
|
+
|
|
363
|
+
const nextValue = ensureContainer(source, nextKey)
|
|
369
364
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
365
|
+
if (isArray) {
|
|
366
|
+
const copy = [
|
|
367
|
+
...(obj as unknown[]),
|
|
368
|
+
]
|
|
369
|
+
copy[key as number] = setValueAtPath(nextValue, pathSegments, nextDepth, value)
|
|
370
|
+
return copy as unknown as O
|
|
373
371
|
}
|
|
374
372
|
|
|
375
373
|
const result = {
|
|
376
|
-
...
|
|
374
|
+
...(obj as Record<string | number, unknown>),
|
|
377
375
|
}
|
|
378
|
-
result[
|
|
376
|
+
result[key] = setValueAtPath(nextValue, pathSegments, nextDepth, value)
|
|
379
377
|
return result as unknown as O
|
|
380
378
|
}
|
|
381
379
|
|
|
382
380
|
const createLens = <T, K>(source: State<T>, accessor: (state: T) => K): State<K> => {
|
|
383
381
|
let isUpdating = false
|
|
384
382
|
|
|
385
|
-
const extractPath = ():
|
|
386
|
-
const pathCollector:
|
|
383
|
+
const extractPath = (): string[] => {
|
|
384
|
+
const pathCollector: string[] = []
|
|
385
|
+
let tainted = false
|
|
387
386
|
const proxy = new Proxy(
|
|
388
387
|
{},
|
|
389
388
|
{
|
|
390
389
|
get: (_: object, prop: string | symbol): unknown => {
|
|
391
|
-
if (
|
|
392
|
-
|
|
390
|
+
if (!tainted && typeof prop === 'string') {
|
|
391
|
+
if (DANGEROUS_KEYS.has(String(prop))) {
|
|
392
|
+
tainted = true
|
|
393
|
+
} else {
|
|
394
|
+
pathCollector.push(prop)
|
|
395
|
+
}
|
|
393
396
|
}
|
|
394
397
|
return proxy
|
|
395
398
|
},
|
|
@@ -402,7 +405,7 @@ const createLens = <T, K>(source: State<T>, accessor: (state: T) => K): State<K>
|
|
|
402
405
|
// Ignore errors, we're just collecting the path
|
|
403
406
|
}
|
|
404
407
|
|
|
405
|
-
return pathCollector
|
|
408
|
+
return tainted ? [] : pathCollector
|
|
406
409
|
}
|
|
407
410
|
|
|
408
411
|
const path = extractPath()
|
|
@@ -423,7 +426,7 @@ const createLens = <T, K>(source: State<T>, accessor: (state: T) => K): State<K>
|
|
|
423
426
|
})
|
|
424
427
|
|
|
425
428
|
lensState.set = function lensSet(value: K): void {
|
|
426
|
-
if (isUpdating) {
|
|
429
|
+
if (isUpdating || path.length === 0) {
|
|
427
430
|
return
|
|
428
431
|
}
|
|
429
432
|
|