@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 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
- > Lightweight reactive state management for Node.js backends
3
+ > Reactive dependency graph runtime for Node.js backends. Tracks dependencies between signals and propagates updates automatically.
4
4
 
5
5
  [![license:mit](https://flat.badgen.net/static/license/MIT/blue)](https://github.com/nerdalytics/beacon/blob/trunk/LICENSE)
6
6
  [![registry:npm:version](https://img.shields.io/npm/v/@nerdalytics/beacon.svg)](https://www.npmjs.com/package/@nerdalytics/beacon)
7
- [![Socket Badge](https://badge.socket.dev/npm/package/@nerdalytics/beacon/1000.3.0)](https://socket.dev/npm/package/@nerdalytics/beacon/overview/1000.3.0)
7
+ [![Socket Badge](https://badge.socket.dev/npm/package/@nerdalytics/beacon/1000.3.3)](https://socket.dev/npm/package/@nerdalytics/beacon/overview/1000.3.3)
8
8
 
9
9
  [![tech:nodejs](https://img.shields.io/badge/Node%20js-339933?style=for-the-badge&logo=nodedotjs&logoColor=white)](https://nodejs.org/)
10
10
  [![language:typescript](https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white)](https://typescriptlang.org/)
11
11
  [![linter:biome](https://img.shields.io/badge/biome-60a5fa?style=for-the-badge&logo=biome&logoColor=white)](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.
@@ -4,10 +4,7 @@ interface WriteableState<T> {
4
4
  set(value: T): void;
5
5
  update(fn: (value: T) => T): void;
6
6
  }
7
- declare const STATE_ID: unique symbol;
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;
@@ -1 +1 @@
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};
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": "A lightweight reactive state library for Node.js backends. Enables reactive state management with automatic dependency tracking and efficient updates for server-side applications.",
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.3.14",
6
- "@types/node": "25.2.2",
7
- "npm-check-updates": "19.3.2",
8
- "typescript": "5.9.3",
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
- "batching",
36
- "computed-values",
37
- "dependency-tracking",
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-management",
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.9.0",
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
- "benchmark:naiv": "node scripts/naiv-benchmark.ts",
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
- "test:unit:batch": "node --test tests/batch.test.ts",
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.1"
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
- // Special symbol used for internal tracking
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
- let pendingSubscribers: Set<Subscriber> = new Set<Subscriber>()
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 = new Set()
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 valueState = createState<T | undefined>(undefined)
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
- valueState.set(newValue)
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 valueState() as T
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 valueState = createState<R | undefined>(undefined)
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 valueState() as R
316
+ return lastSelectedValue as R
294
317
  }
295
318
  }
296
319
 
297
- // Helper for array updates
298
- const updateArrayItem = <V>(arr: unknown[], index: number, value: V): unknown[] => {
299
- const copy = [
300
- ...arr,
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
- // Helper to create the appropriate container type
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 === undefined || obj === null) {
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
- if (Array.isArray(obj)) {
340
- const index = Number(currentKey)
341
+ const isArray = Array.isArray(obj)
342
+ const key = isArray ? Number(currentKey) : currentKey
341
343
 
342
- if (depth === pathSegments.length - 1) {
343
- return updateArrayItem(obj, index, value) as unknown as O
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
- 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)
352
+ const result = {
353
+ ...(obj as Record<string, unknown>),
355
354
  }
356
-
357
- copy[index] = setValueAtPath(nextValue, pathSegments, nextDepth, value)
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
- let currentValue = record[currentKey]
371
- if (currentValue === undefined || currentValue === null) {
372
- currentValue = nextKey === undefined ? {} : createContainer(nextKey)
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
- ...record,
374
+ ...(obj as Record<string | number, unknown>),
377
375
  }
378
- result[currentKey] = setValueAtPath(currentValue, pathSegments, nextDepth, value)
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 = (): (string | number)[] => {
386
- const pathCollector: (string | number)[] = []
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 (typeof prop === 'string' || typeof prop === 'number') {
392
- pathCollector.push(prop)
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