@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 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
- > 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)
@@ -10,8 +10,6 @@
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
  ```
@@ -37,7 +35,17 @@ count.set(5);
37
35
  ## Documentation
38
36
 
39
37
  Full documentation, API reference, and examples available at:
40
- **[github.com/nerdalytics/beacon](https://github.com/nerdalytics/beacon)**
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
 
@@ -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, };
@@ -1 +1 @@
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};
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": "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
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
- "default": "./dist/src/index.min.js",
18
- "types": "./dist/src/index.d.ts"
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.0"
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
- * Creates a reactive state container with the provided initial value.
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
- let dependencies = subscriberDependencies.get(currentEffect)
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
- let readStates = stateTracking.get(currentEffect)
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.set(runEffect, new Set())
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
- let children = childSubscribers.get(parentEffect)
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
- let children = childSubscribers.get(parent)
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
- 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)
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
- const container = {
248
- cachedValue: undefined as unknown as T,
249
- computeFn,
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 = container.computeFn()
242
+ const newValue = computeFn()
256
243
 
257
- if (!(container.initialized && Object.is(container.cachedValue, newValue))) {
258
- container.cachedValue = newValue
259
- container.valueState.set(newValue)
244
+ if (!(initialized && Object.is(cachedValue, newValue))) {
245
+ cachedValue = newValue
246
+ valueState.set(newValue)
260
247
  }
261
248
 
262
- container.initialized = true
249
+ initialized = true
263
250
  })
264
251
 
265
252
  return function deriveGetter(): T {
266
- if (!container.initialized) {
267
- container.cachedValue = container.computeFn()
268
- container.initialized = true
269
- container.valueState.set(container.cachedValue)
253
+ if (!initialized) {
254
+ cachedValue = computeFn()
255
+ initialized = true
256
+ valueState.set(cachedValue)
270
257
  }
271
- return container.valueState() as T
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
- 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),
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 = container.source()
273
+ const sourceValue = source()
295
274
 
296
- if (container.initialized && Object.is(container.lastSourceValue, sourceValue)) {
275
+ if (initialized && Object.is(lastSourceValue, sourceValue)) {
297
276
  return
298
277
  }
299
278
 
300
- container.lastSourceValue = sourceValue
301
- const newSelectedValue = container.selectorFn(sourceValue)
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
- container.lastSelectedValue = newSelectedValue
312
- container.valueState.set(newSelectedValue)
313
- container.initialized = true
286
+ lastSelectedValue = newSelectedValue
287
+ valueState.set(newSelectedValue)
288
+ initialized = true
314
289
  })
315
290
 
316
291
  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
292
+ if (!initialized) {
293
+ lastSourceValue = source()
294
+ lastSelectedValue = selectorFn(lastSourceValue)
295
+ valueState.set(lastSelectedValue)
296
+ initialized = true
322
297
  }
323
- return container.valueState() as R
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
- // Helper for handling array path updates
356
- const updateArrayPath = <V>(array: unknown[], pathSegments: (string | number)[], value: V): unknown[] => {
357
- const index = Number(pathSegments[0])
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
- const copy = [
364
- ...array,
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
- copy[index] = setValueAtPath(nextValue, nextPathSegments, value)
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 (pathSegments.length === 1) {
390
- return updateShallowProperty(obj, currentKey, value)
391
- }
344
+ if (Array.isArray(obj)) {
345
+ const index = Number(currentKey)
392
346
 
393
- const nextPathSegments = pathSegments.slice(1)
394
- const nextKey = nextPathSegments[0]
347
+ if (depth === pathSegments.length - 1) {
348
+ return updateArrayItem(obj, index, value) as unknown as O
349
+ }
395
350
 
396
- let currentValue = obj[currentKey]
397
- if (currentValue === undefined || currentValue === null) {
398
- currentValue = nextKey !== undefined ? createContainer(nextKey) : {}
399
- }
351
+ const copy = [
352
+ ...obj,
353
+ ]
354
+ const nextDepth = depth + 1
355
+ const nextKey = pathSegments[nextDepth]
400
356
 
401
- const result = {
402
- ...obj,
403
- }
404
- result[currentKey] = setValueAtPath(currentValue, nextPathSegments, value)
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
- const setValueAtPath = <V, O>(obj: O, pathSegments: (string | number)[], value: V): O => {
409
- if (pathSegments.length === 0) {
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
- if (obj === undefined || obj === null) {
414
- return setValueAtPath({} as O, pathSegments, value)
415
- }
366
+ const record = obj as Record<string | number, unknown>
416
367
 
417
- const currentKey = pathSegments[0]
418
- if (currentKey === undefined) {
419
- return obj
368
+ if (depth === pathSegments.length - 1) {
369
+ return updateShallowProperty(record, currentKey, value) as unknown as O
420
370
  }
421
371
 
422
- if (Array.isArray(obj)) {
423
- return updateArrayPath(obj, pathSegments, value) as unknown as O
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
- return updateObjectPath(obj as Record<string | number, unknown>, pathSegments, value) as unknown as O
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
- 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
- }
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
- pathCollector.push(prop)
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
- container.accessor(proxy as unknown as T)
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
- container.path = extractPath()
466
-
467
- container.lensState = createState<K>(container.accessor(container.source()))
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 (container.isUpdating) {
423
+ if (isUpdating) {
472
424
  return
473
425
  }
474
426
 
475
- container.isUpdating = true
427
+ isUpdating = true
476
428
  try {
477
- container.lensState.set(container.accessor(container.source()))
429
+ lensState.set(accessor(source()))
478
430
  } finally {
479
- container.isUpdating = false
431
+ isUpdating = false
480
432
  }
481
433
  })
482
434
 
483
- container.lensState.set = function lensSet(value: K): void {
484
- if (container.isUpdating) {
435
+ lensState.set = function lensSet(value: K): void {
436
+ if (isUpdating || path.length === 0) {
485
437
  return
486
438
  }
487
439
 
488
- container.isUpdating = true
440
+ isUpdating = true
489
441
  try {
490
- container.originalSet(value)
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
- container.isUpdating = false
445
+ isUpdating = false
495
446
  }
496
447
  }
497
448
 
498
- container.lensState.update = function lensUpdate(fn: (value: K) => K): void {
499
- container.lensState.set(fn(container.lensState()))
449
+ lensState.update = function lensUpdate(fn: (value: K) => K): void {
450
+ lensState.set(fn(lensState()))
500
451
  }
501
452
 
502
- return container.lensState
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
- (): T => createReadonlyState(fullState)(),
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 }