@nerdalytics/beacon 1000.2.1 → 1000.2.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.
@@ -1,4 +1,4 @@
1
- type Unsubscribe = () => void;
1
+ export type Unsubscribe = () => void;
2
2
  export type ReadOnlyState<T> = () => T;
3
3
  export interface WriteableState<T> {
4
4
  set(value: T): void;
package/dist/src/index.js CHANGED
@@ -1,4 +1,4 @@
1
- const STATE_ID = Symbol();
1
+ const STATE_ID = Symbol('STATE_ID');
2
2
  export const state = (initialValue, equalityFn = Object.is) => StateImpl.createState(initialValue, equalityFn);
3
3
  export const effect = (fn) => StateImpl.createEffect(fn);
4
4
  export const batch = (fn) => StateImpl.executeBatch(fn);
@@ -169,7 +169,9 @@ class StateImpl {
169
169
  StateImpl.batchDepth--;
170
170
  if (StateImpl.batchDepth === 0) {
171
171
  if (StateImpl.deferredEffectCreations.length > 0) {
172
- const effectsToRun = [...StateImpl.deferredEffectCreations];
172
+ const effectsToRun = [
173
+ ...StateImpl.deferredEffectCreations,
174
+ ];
173
175
  StateImpl.deferredEffectCreations.length = 0;
174
176
  for (const effect of effectsToRun) {
175
177
  effect();
@@ -182,106 +184,123 @@ class StateImpl {
182
184
  }
183
185
  };
184
186
  static createDerive = (computeFn) => {
185
- const valueState = StateImpl.createState(undefined);
186
- let initialized = false;
187
- let cachedValue;
188
- StateImpl.createEffect(() => {
189
- const newValue = computeFn();
190
- if (!(initialized && Object.is(cachedValue, newValue))) {
191
- cachedValue = newValue;
192
- valueState.set(newValue);
187
+ const container = {
188
+ cachedValue: undefined,
189
+ computeFn,
190
+ initialized: false,
191
+ valueState: StateImpl.createState(undefined),
192
+ };
193
+ StateImpl.createEffect(function deriveEffect() {
194
+ const newValue = container.computeFn();
195
+ if (!(container.initialized && Object.is(container.cachedValue, newValue))) {
196
+ container.cachedValue = newValue;
197
+ container.valueState.set(newValue);
193
198
  }
194
- initialized = true;
199
+ container.initialized = true;
195
200
  });
196
- return () => {
197
- if (!initialized) {
198
- cachedValue = computeFn();
199
- initialized = true;
200
- valueState.set(cachedValue);
201
+ return function deriveGetter() {
202
+ if (!container.initialized) {
203
+ container.cachedValue = container.computeFn();
204
+ container.initialized = true;
205
+ container.valueState.set(container.cachedValue);
201
206
  }
202
- return valueState();
207
+ return container.valueState();
203
208
  };
204
209
  };
205
210
  static createSelect = (source, selectorFn, equalityFn = Object.is) => {
206
- let lastSourceValue;
207
- let lastSelectedValue;
208
- let initialized = false;
209
- const valueState = StateImpl.createState(undefined);
210
- StateImpl.createEffect(() => {
211
- const sourceValue = source();
212
- if (initialized && Object.is(lastSourceValue, sourceValue)) {
211
+ const container = {
212
+ equalityFn,
213
+ initialized: false,
214
+ lastSelectedValue: undefined,
215
+ lastSourceValue: undefined,
216
+ selectorFn,
217
+ source,
218
+ valueState: StateImpl.createState(undefined),
219
+ };
220
+ StateImpl.createEffect(function selectEffect() {
221
+ const sourceValue = container.source();
222
+ if (container.initialized && Object.is(container.lastSourceValue, sourceValue)) {
213
223
  return;
214
224
  }
215
- lastSourceValue = sourceValue;
216
- const newSelectedValue = selectorFn(sourceValue);
217
- if (initialized && lastSelectedValue !== undefined && equalityFn(lastSelectedValue, newSelectedValue)) {
225
+ container.lastSourceValue = sourceValue;
226
+ const newSelectedValue = container.selectorFn(sourceValue);
227
+ if (container.initialized &&
228
+ container.lastSelectedValue !== undefined &&
229
+ container.equalityFn(container.lastSelectedValue, newSelectedValue)) {
218
230
  return;
219
231
  }
220
- lastSelectedValue = newSelectedValue;
221
- valueState.set(newSelectedValue);
222
- initialized = true;
232
+ container.lastSelectedValue = newSelectedValue;
233
+ container.valueState.set(newSelectedValue);
234
+ container.initialized = true;
223
235
  });
224
- return () => {
225
- if (!initialized) {
226
- lastSourceValue = source();
227
- lastSelectedValue = selectorFn(lastSourceValue);
228
- valueState.set(lastSelectedValue);
229
- initialized = true;
236
+ return function selectGetter() {
237
+ if (!container.initialized) {
238
+ container.lastSourceValue = container.source();
239
+ container.lastSelectedValue = container.selectorFn(container.lastSourceValue);
240
+ container.valueState.set(container.lastSelectedValue);
241
+ container.initialized = true;
230
242
  }
231
- return valueState();
243
+ return container.valueState();
232
244
  };
233
245
  };
234
246
  static createLens = (source, accessor) => {
247
+ const container = {
248
+ accessor,
249
+ isUpdating: false,
250
+ lensState: null,
251
+ originalSet: null,
252
+ path: [],
253
+ source,
254
+ };
235
255
  const extractPath = () => {
236
- const path = [];
256
+ const pathCollector = [];
237
257
  const proxy = new Proxy({}, {
238
258
  get: (_, prop) => {
239
259
  if (typeof prop === 'string' || typeof prop === 'number') {
240
- path.push(prop);
260
+ pathCollector.push(prop);
241
261
  }
242
262
  return proxy;
243
263
  },
244
264
  });
245
265
  try {
246
- accessor(proxy);
266
+ container.accessor(proxy);
247
267
  }
248
268
  catch {
249
269
  }
250
- return path;
270
+ return pathCollector;
251
271
  };
252
- const path = extractPath();
253
- const lensState = StateImpl.createState(accessor(source()));
254
- let isUpdating = false;
255
- StateImpl.createEffect(() => {
256
- if (isUpdating) {
272
+ container.path = extractPath();
273
+ container.lensState = StateImpl.createState(container.accessor(container.source()));
274
+ container.originalSet = container.lensState.set;
275
+ StateImpl.createEffect(function lensEffect() {
276
+ if (container.isUpdating) {
257
277
  return;
258
278
  }
259
- isUpdating = true;
279
+ container.isUpdating = true;
260
280
  try {
261
- lensState.set(accessor(source()));
281
+ container.lensState.set(container.accessor(container.source()));
262
282
  }
263
283
  finally {
264
- isUpdating = false;
284
+ container.isUpdating = false;
265
285
  }
266
286
  });
267
- const originalSet = lensState.set;
268
- lensState.set = (value) => {
269
- if (isUpdating) {
287
+ container.lensState.set = function lensSet(value) {
288
+ if (container.isUpdating) {
270
289
  return;
271
290
  }
272
- isUpdating = true;
291
+ container.isUpdating = true;
273
292
  try {
274
- originalSet(value);
275
- source.update((current) => setValueAtPath(current, path, value));
293
+ container.originalSet(value);
294
+ container.source.update((current) => setValueAtPath(current, container.path, value));
276
295
  }
277
296
  finally {
278
- isUpdating = false;
297
+ container.isUpdating = false;
279
298
  }
280
299
  };
281
- lensState.update = (fn) => {
282
- lensState.set(fn(lensState()));
300
+ container.lensState.update = function lensUpdate(fn) {
301
+ container.lensState.set(fn(container.lensState()));
283
302
  };
284
- return lensState;
303
+ return container.lensState;
285
304
  };
286
305
  static notifySubscribers = () => {
287
306
  if (StateImpl.isNotifying) {
@@ -314,12 +333,16 @@ class StateImpl {
314
333
  };
315
334
  }
316
335
  const updateArrayItem = (arr, index, value) => {
317
- const copy = [...arr];
336
+ const copy = [
337
+ ...arr,
338
+ ];
318
339
  copy[index] = value;
319
340
  return copy;
320
341
  };
321
342
  const updateShallowProperty = (obj, key, value) => {
322
- const result = { ...obj };
343
+ const result = {
344
+ ...obj,
345
+ };
323
346
  result[key] = value;
324
347
  return result;
325
348
  };
@@ -332,7 +355,9 @@ const updateArrayPath = (array, pathSegments, value) => {
332
355
  if (pathSegments.length === 1) {
333
356
  return updateArrayItem(array, index, value);
334
357
  }
335
- const copy = [...array];
358
+ const copy = [
359
+ ...array,
360
+ ];
336
361
  const nextPathSegments = pathSegments.slice(1);
337
362
  const nextKey = nextPathSegments[0];
338
363
  let nextValue = array[index];
@@ -356,7 +381,9 @@ const updateObjectPath = (obj, pathSegments, value) => {
356
381
  if (currentValue === undefined || currentValue === null) {
357
382
  currentValue = nextKey !== undefined ? createContainer(nextKey) : {};
358
383
  }
359
- const result = { ...obj };
384
+ const result = {
385
+ ...obj,
386
+ };
360
387
  result[currentKey] = setValueAtPath(currentValue, nextPathSegments, value);
361
388
  return result;
362
389
  };
@@ -1 +1 @@
1
- let s=Symbol(),i=(e,t=Object.is)=>u.createState(e,t);var e=e=>u.createEffect(e),t=e=>u.executeBatch(e),r=e=>u.createDerive(e),c=(e,t,r=Object.is)=>u.createSelect(e,t,r);let a=e=>()=>e();var n=(e,t=Object.is)=>{let r=i(e,t);return[()=>a(r)(),{set:e=>r.set(e),update:e=>r.update(e)}]},b=(e,t)=>u.createLens(e,t);class u{static currentSubscriber=null;static pendingSubscribers=new Set;static isNotifying=!1;static batchDepth=0;static deferredEffectCreations=[];static activeSubscribers=new Set;static stateTracking=new WeakMap;static subscriberDependencies=new WeakMap;static parentSubscriber=new WeakMap;static childSubscribers=new WeakMap;value;subscribers=new Set;stateId=Symbol();equalityFn;constructor(e,t=Object.is){this.value=e,this.equalityFn=t}static createState=(e,t=Object.is)=>{let r=new u(e,t);e=()=>r.get();return e.set=e=>r.set(e),e.update=e=>r.update(e),e[s]=r.stateId,e};get=()=>{var r=u.currentSubscriber;if(r){this.subscribers.add(r);let e=u.subscriberDependencies.get(r),t=(e||(e=new Set,u.subscriberDependencies.set(r,e)),e.add(this.subscribers),u.stateTracking.get(r));t||(t=new Set,u.stateTracking.set(r,t)),t.add(this.stateId)}return this.value};set=e=>{if(!this.equalityFn(this.value,e)){var t=u.currentSubscriber;if(t)if(u.stateTracking.get(t)?.has(this.stateId)&&!u.parentSubscriber.get(t))throw new Error("Infinite loop detected: effect() cannot update a state() it depends on!");if(this.value=e,0!==this.subscribers.size){for(var r of this.subscribers)u.pendingSubscribers.add(r);0!==u.batchDepth||u.isNotifying||u.notifySubscribers()}}};update=e=>{this.set(e(this.value))};static createEffect=e=>{let r=()=>{if(!u.activeSubscribers.has(r)){u.activeSubscribers.add(r);var t=u.currentSubscriber;try{if(u.cleanupEffect(r),u.currentSubscriber=r,u.stateTracking.set(r,new Set),t){u.parentSubscriber.set(r,t);let e=u.childSubscribers.get(t);e||(e=new Set,u.childSubscribers.set(t,e)),e.add(r)}e()}finally{u.currentSubscriber=t,u.activeSubscribers.delete(r)}}};if(0===u.batchDepth)r();else{if(u.currentSubscriber){var t=u.currentSubscriber;u.parentSubscriber.set(r,t);let e=u.childSubscribers.get(t);e||(e=new Set,u.childSubscribers.set(t,e)),e.add(r)}u.deferredEffectCreations.push(r)}return()=>{u.cleanupEffect(r),u.pendingSubscribers.delete(r),u.activeSubscribers.delete(r),u.stateTracking.delete(r);var e=u.parentSubscriber.get(r),e=(e&&(e=u.childSubscribers.get(e))&&e.delete(r),u.parentSubscriber.delete(r),u.childSubscribers.get(r));if(e){for(var t of e)u.cleanupEffect(t);e.clear(),u.childSubscribers.delete(r)}}};static executeBatch=e=>{u.batchDepth++;try{return e()}catch(e){throw 1===u.batchDepth&&(u.pendingSubscribers.clear(),u.deferredEffectCreations.length=0),e}finally{if(u.batchDepth--,0===u.batchDepth){if(0<u.deferredEffectCreations.length){var t,e=[...u.deferredEffectCreations];u.deferredEffectCreations.length=0;for(t of e)t()}0<u.pendingSubscribers.size&&!u.isNotifying&&u.notifySubscribers()}}};static createDerive=t=>{let r=u.createState(void 0),s=!1,i;return u.createEffect(()=>{var e=t();s&&Object.is(i,e)||(i=e,r.set(e)),s=!0}),()=>(s||(i=t(),s=!0,r.set(i)),r())};static createSelect=(t,r,s=Object.is)=>{let i,c,a=!1,n=u.createState(void 0);return u.createEffect(()=>{var e=t();a&&Object.is(i,e)||(i=e,e=r(e),a&&void 0!==c&&s(c,e))||(c=e,n.set(e),a=!0)}),()=>(a||(i=t(),c=r(i),n.set(c),a=!0),n())};static createLens=(e,t)=>{let r=(()=>{let r=[],s=new Proxy({},{get:(e,t)=>("string"!=typeof t&&"number"!=typeof t||r.push(t),s)});try{t(s)}catch{}return r})(),s=u.createState(t(e())),i=!1,c=(u.createEffect(()=>{if(!i){i=!0;try{s.set(t(e()))}finally{i=!1}}}),s.set);return s.set=t=>{if(!i){i=!0;try{c(t),e.update(e=>p(e,r,t))}finally{i=!1}}},s.update=e=>{s.set(e(s()))},s};static notifySubscribers=()=>{if(!u.isNotifying){u.isNotifying=!0;try{for(;0<u.pendingSubscribers.size;){var e,t=Array.from(u.pendingSubscribers);u.pendingSubscribers.clear();for(e of t)e()}}finally{u.isNotifying=!1}}};static cleanupEffect=e=>{u.pendingSubscribers.delete(e);var t=u.subscriberDependencies.get(e);if(t){for(var r of t)r.delete(e);t.clear(),u.subscriberDependencies.delete(e)}}}let f=(e,t,r)=>{e=[...e];return e[t]=r,e},l=(e,t,r)=>{e={...e};return e[t]=r,e},d=e=>"number"==typeof e||!Number.isNaN(Number(e))?[]:{},S=(e,t,r)=>{var s=Number(t[0]);if(1===t.length)return f(e,s,r);var i=[...e],t=t.slice(1),c=t[0];let a=e[s];return null==a&&(a=void 0!==c?d(c):{}),i[s]=p(a,t,r),i},h=(e,t,r)=>{var s=t[0];if(void 0===s)return e;if(1===t.length)return l(e,s,r);var t=t.slice(1),i=t[0];let c=e[s];null==c&&(c=void 0!==i?d(i):{});i={...e};return i[s]=p(c,t,r),i},p=(e,t,r)=>0===t.length?r:null==e?p({},t,r):void 0===t[0]?e:(Array.isArray(e)?S:h)(e,t,r);export{i as state,e as effect,t as batch,r as derive,c as select,a as readonlyState,n as protectedState,b as lens};
1
+ let i=Symbol("STATE_ID"),a=(e,t=Object.is)=>l.createState(e,t);var e=e=>l.createEffect(e),t=e=>l.executeBatch(e),r=e=>l.createDerive(e),s=(e,t,r=Object.is)=>l.createSelect(e,t,r);let c=e=>()=>e();var n=(e,t=Object.is)=>{let r=a(e,t);return[()=>c(r)(),{set:e=>r.set(e),update:e=>r.update(e)}]},u=(e,t)=>l.createLens(e,t);class l{static currentSubscriber=null;static pendingSubscribers=new Set;static isNotifying=!1;static batchDepth=0;static deferredEffectCreations=[];static activeSubscribers=new Set;static stateTracking=new WeakMap;static subscriberDependencies=new WeakMap;static parentSubscriber=new WeakMap;static childSubscribers=new WeakMap;value;subscribers=new Set;stateId=Symbol();equalityFn;constructor(e,t=Object.is){this.value=e,this.equalityFn=t}static createState=(e,t=Object.is)=>{let r=new l(e,t);e=()=>r.get();return e.set=e=>r.set(e),e.update=e=>r.update(e),e[i]=r.stateId,e};get=()=>{var r=l.currentSubscriber;if(r){this.subscribers.add(r);let e=l.subscriberDependencies.get(r),t=(e||(e=new Set,l.subscriberDependencies.set(r,e)),e.add(this.subscribers),l.stateTracking.get(r));t||(t=new Set,l.stateTracking.set(r,t)),t.add(this.stateId)}return this.value};set=e=>{if(!this.equalityFn(this.value,e)){var t=l.currentSubscriber;if(t)if(l.stateTracking.get(t)?.has(this.stateId)&&!l.parentSubscriber.get(t))throw new Error("Infinite loop detected: effect() cannot update a state() it depends on!");if(this.value=e,0!==this.subscribers.size){for(var r of this.subscribers)l.pendingSubscribers.add(r);0!==l.batchDepth||l.isNotifying||l.notifySubscribers()}}};update=e=>{this.set(e(this.value))};static createEffect=e=>{let r=()=>{if(!l.activeSubscribers.has(r)){l.activeSubscribers.add(r);var t=l.currentSubscriber;try{if(l.cleanupEffect(r),l.currentSubscriber=r,l.stateTracking.set(r,new Set),t){l.parentSubscriber.set(r,t);let e=l.childSubscribers.get(t);e||(e=new Set,l.childSubscribers.set(t,e)),e.add(r)}e()}finally{l.currentSubscriber=t,l.activeSubscribers.delete(r)}}};if(0===l.batchDepth)r();else{if(l.currentSubscriber){var t=l.currentSubscriber;l.parentSubscriber.set(r,t);let e=l.childSubscribers.get(t);e||(e=new Set,l.childSubscribers.set(t,e)),e.add(r)}l.deferredEffectCreations.push(r)}return()=>{l.cleanupEffect(r),l.pendingSubscribers.delete(r),l.activeSubscribers.delete(r),l.stateTracking.delete(r);var e=l.parentSubscriber.get(r),e=(e&&(e=l.childSubscribers.get(e))&&e.delete(r),l.parentSubscriber.delete(r),l.childSubscribers.get(r));if(e){for(var t of e)l.cleanupEffect(t);e.clear(),l.childSubscribers.delete(r)}}};static executeBatch=e=>{l.batchDepth++;try{return e()}catch(e){throw 1===l.batchDepth&&(l.pendingSubscribers.clear(),l.deferredEffectCreations.length=0),e}finally{if(l.batchDepth--,0===l.batchDepth){if(0<l.deferredEffectCreations.length){var t,e=[...l.deferredEffectCreations];l.deferredEffectCreations.length=0;for(t of e)t()}0<l.pendingSubscribers.size&&!l.isNotifying&&l.notifySubscribers()}}};static createDerive=e=>{let t={cachedValue:void 0,computeFn:e,initialized:!1,valueState:l.createState(void 0)};return l.createEffect(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()}};static createSelect=(e,t,r=Object.is)=>{let i={equalityFn:r,initialized:!1,lastSelectedValue:void 0,lastSourceValue:void 0,selectorFn:t,source:e,valueState:l.createState(void 0)};return l.createEffect(function(){var e=i.source();i.initialized&&Object.is(i.lastSourceValue,e)||(i.lastSourceValue=e,e=i.selectorFn(e),i.initialized&&void 0!==i.lastSelectedValue&&i.equalityFn(i.lastSelectedValue,e))||(i.lastSelectedValue=e,i.valueState.set(e),i.initialized=!0)}),function(){return i.initialized||(i.lastSourceValue=i.source(),i.lastSelectedValue=i.selectorFn(i.lastSourceValue),i.valueState.set(i.lastSelectedValue),i.initialized=!0),i.valueState()}};static createLens=(e,t)=>{let a={accessor:t,isUpdating:!1,lensState:null,originalSet:null,path:[],source:e};return a.path=(()=>{let r=[],i=new Proxy({},{get:(e,t)=>("string"!=typeof t&&"number"!=typeof t||r.push(t),i)});try{a.accessor(i)}catch{}return r})(),a.lensState=l.createState(a.accessor(a.source())),a.originalSet=a.lensState.set,l.createEffect(function(){if(!a.isUpdating){a.isUpdating=!0;try{a.lensState.set(a.accessor(a.source()))}finally{a.isUpdating=!1}}}),a.lensState.set=function(t){if(!a.isUpdating){a.isUpdating=!0;try{a.originalSet(t),a.source.update(e=>p(e,a.path,t))}finally{a.isUpdating=!1}}},a.lensState.update=function(e){a.lensState.set(e(a.lensState()))},a.lensState};static notifySubscribers=()=>{if(!l.isNotifying){l.isNotifying=!0;try{for(;0<l.pendingSubscribers.size;){var e,t=Array.from(l.pendingSubscribers);l.pendingSubscribers.clear();for(e of t)e()}}finally{l.isNotifying=!1}}};static cleanupEffect=e=>{l.pendingSubscribers.delete(e);var t=l.subscriberDependencies.get(e);if(t){for(var r of t)r.delete(e);t.clear(),l.subscriberDependencies.delete(e)}}}let b=(e,t,r)=>{e=[...e];return e[t]=r,e},d=(e,t,r)=>{e={...e};return e[t]=r,e},f=e=>"number"==typeof e||!Number.isNaN(Number(e))?[]:{},S=(e,t,r)=>{var i=Number(t[0]);if(1===t.length)return b(e,i,r);var a=[...e],t=t.slice(1),s=t[0];let c=e[i];return null==c&&(c=void 0!==s?f(s):{}),a[i]=p(c,t,r),a},o=(e,t,r)=>{var i=t[0];if(void 0===i)return e;if(1===t.length)return d(e,i,r);var t=t.slice(1),a=t[0];let s=e[i];null==s&&(s=void 0!==a?f(a):{});a={...e};return a[i]=p(s,t,r),a},p=(e,t,r)=>0===t.length?r:null==e?p({},t,r):void 0===t[0]?e:(Array.isArray(e)?S:o)(e,t,r);export{a as state,e as effect,t as batch,r as derive,s as select,c as readonlyState,n as protectedState,u as lens};
package/package.json CHANGED
@@ -1,80 +1,95 @@
1
1
  {
2
- "name": "@nerdalytics/beacon",
3
- "version": "1000.2.1",
2
+ "author": "Denny Trebbin (nerdalytics)",
4
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.",
5
- "type": "module",
6
- "main": "dist/src/index.min.js",
7
- "types": "dist/src/index.d.ts",
8
- "files": ["dist/src/index.js", "dist/src/index.d.ts", "src/index.ts", "LICENSE"],
4
+ "devDependencies": {
5
+ "@biomejs/biome": "2.2.6",
6
+ "@types/node": "24.9.1",
7
+ "typescript": "5.9.3",
8
+ "uglify-js": "3.19.3"
9
+ },
10
+ "engines": {
11
+ "node": ">=20.0.0"
12
+ },
9
13
  "exports": {
10
14
  ".": {
11
- "typescript": "./src/index.ts",
12
- "default": "./dist/src/index.js"
15
+ "import": {
16
+ "default": "./dist/src/index.min.js",
17
+ "types": "./dist/src/index.d.ts"
18
+ },
19
+ "require": {
20
+ "default": "./dist/src/index.min.js"
21
+ },
22
+ "types": "./dist/src/index.d.ts",
23
+ "typescript": "./dist/src/index.ts"
13
24
  }
14
25
  },
26
+ "files": [
27
+ "dist/src/index.js",
28
+ "dist/src/index.d.ts",
29
+ "dist/src/index.ts",
30
+ "LICENSE"
31
+ ],
32
+ "keywords": [
33
+ "backend",
34
+ "batching",
35
+ "computed-values",
36
+ "dependency-tracking",
37
+ "effects",
38
+ "fine-grained",
39
+ "lightweight",
40
+ "memory-management",
41
+ "nodejs",
42
+ "performance",
43
+ "reactive",
44
+ "server-side",
45
+ "signals",
46
+ "state-management",
47
+ "typescript"
48
+ ],
49
+ "license": "MIT",
50
+ "main": "dist/src/index.min.js",
51
+ "name": "@nerdalytics/beacon",
52
+ "packageManager": "npm@11.6.0",
15
53
  "repository": {
16
- "url": "git+https://github.com/nerdalytics/beacon.git",
17
- "type": "git"
54
+ "type": "git",
55
+ "url": "git+https://github.com/nerdalytics/beacon.git"
18
56
  },
19
57
  "scripts": {
20
- "lint": "npx @biomejs/biome lint --config-path=./biome.json",
21
- "lint:fix": "npx @biomejs/biome lint --fix --config-path=./biome.json",
22
- "lint:fix:unsafe": "npx @biomejs/biome lint --fix --unsafe --config-path=./biome.json",
23
- "format": "npx @biomejs/biome format --write --config-path=./biome.json",
24
- "check": "npx @biomejs/biome check --config-path=./biome.json",
25
- "check:fix": "npx @biomejs/biome format --fix --config-path=./biome.json",
26
- "test": "node --test --test-skip-pattern=\"COMPONENT NAME\" tests/**/*.ts",
27
- "test:coverage": "node --test --experimental-config-file=node.config.json --test-skip-pattern=\"[COMPONENT NAME]\" --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=lcov.info tests/**/*.ts",
28
- "test:unit:state": "node --test tests/state.test.ts",
29
- "test:unit:effect": "node --test tests/effect.test.ts",
30
- "test:unit:batch": "node --test tests/batch.test.ts",
31
- "test:unit:derive": "node --test tests/derive.test.ts",
32
- "test:unit:select": "node --test tests/select.test.ts",
33
- "test:unit:lens": "node --test tests/lens.test.ts",
34
- "test:unit:cleanup": "node --test tests/cleanup.test.ts",
35
- "test:unit:cyclic-dependency": "node --test tests/cyclic-dependency.test.ts",
36
- "test:unit:deep-chain": "node --test tests/deep-chain.test.ts",
37
- "test:unit:infinite-loop": "node --test tests/infinite-loop.test.ts",
38
- "test:unit:custom-equality": "node --test tests/custom-equality.test.ts",
39
58
  "benchmark": "node scripts/benchmark.ts",
59
+ "benchmark:naiv": "node scripts/naiv-benchmark.ts",
40
60
  "build": "npm run build:lts",
41
- "prebuild:lts": "rm -rf dist/",
42
61
  "build:lts": "tsc -p tsconfig.lts.json",
62
+ "check": "npx @biomejs/biome check",
63
+ "check:fix": "npx @biomejs/biome format --fix",
64
+ "format": "npx @biomejs/biome format --write",
65
+ "lint": "npx @biomejs/biome lint",
66
+ "lint:fix": "npx @biomejs/biome lint --fix",
67
+ "lint:fix:unsafe": "npx @biomejs/biome lint --fix --unsafe",
43
68
  "postbuild:lts": "npx uglify-js --compress --mangle --module --toplevel --v8 --warn --source-map \"content='dist/src/index.js.map'\" --output dist/src/index.min.js dist/src/index.js",
69
+ "prebuild:lts": "rm -rf dist/",
44
70
  "prepublishOnly": "npm run build:lts",
45
71
  "pretest:lts": "node scripts/run-lts-tests.js",
72
+ "test": "node --test --test-skip-pattern=\"COMPONENT NAME\" tests/**/*.ts",
73
+ "test:behavior": "node --test tests/infinite-loop.test.ts tests/cyclic-dependency.test.ts tests/cleanup.test.ts",
74
+ "test:core": "node --test tests/*-core.test.ts",
75
+ "test:coverage": "node --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=lcov.info tests/**/*.test.ts",
76
+ "test:integration": "node --test tests/state-*.test.ts tests/batch-integration.test.ts",
46
77
  "test:lts:20": "node --test dist/tests/**.js",
47
78
  "test:lts:22": "node --test --test-skip-pattern=\"COMPONENT NAME\" dist/tests/**/*.js",
79
+ "test:unit:batch": "node --test tests/batch.test.ts",
80
+ "test:unit:cleanup": "node --test tests/cleanup.test.ts",
81
+ "test:unit:custom-equality": "node --test tests/custom-equality.test.ts",
82
+ "test:unit:cyclic-dependency": "node --test tests/cyclic-dependency.test.ts",
83
+ "test:unit:deep-chain": "node --test tests/deep-chain.test.ts",
84
+ "test:unit:derive": "node --test tests/derive.test.ts",
85
+ "test:unit:effect": "node --test tests/effect.test.ts",
86
+ "test:unit:infinite-loop": "node --test tests/infinite-loop.test.ts",
87
+ "test:unit:lens": "node --test tests/lens.test.ts",
88
+ "test:unit:select": "node --test tests/select.test.ts",
89
+ "test:unit:state": "node --test tests/state.test.ts",
48
90
  "update-performance-docs": "node --experimental-config-file=node.config.json scripts/update-performance-docs.ts"
49
91
  },
50
- "keywords": [
51
- "state-management",
52
- "effects",
53
- "fine-grained",
54
- "computed-values",
55
- "batching",
56
- "signals",
57
- "reactive",
58
- "lightweight",
59
- "performance",
60
- "dependency-tracking",
61
- "memoization",
62
- "memory-management",
63
- "nodejs",
64
- "server-side",
65
- "backend",
66
- "typescript"
67
- ],
68
- "author": "Denny Trebbin (nerdalytics)",
69
- "license": "MIT",
70
- "devDependencies": {
71
- "@biomejs/biome": "1.9.4",
72
- "@types/node": "22.14.1",
73
- "typescript": "5.8.3",
74
- "uglify-js": "3.19.3"
75
- },
76
- "engines": {
77
- "node": ">=20.0.0"
78
- },
79
- "packageManager": "npm@11.3.0"
92
+ "type": "module",
93
+ "types": "dist/src/index.d.ts",
94
+ "version": "1000.2.2"
80
95
  }
package/src/index.ts DELETED
@@ -1,639 +0,0 @@
1
- // Core types for reactive primitives
2
- type Subscriber = () => void
3
- type Unsubscribe = () => void
4
- export type ReadOnlyState<T> = () => T
5
- export interface WriteableState<T> {
6
- set(value: T): void
7
- update(fn: (value: T) => T): void
8
- }
9
-
10
- // Special symbol used for internal tracking
11
- const STATE_ID = Symbol()
12
-
13
- export type State<T> = ReadOnlyState<T> &
14
- WriteableState<T> & {
15
- [STATE_ID]?: symbol
16
- }
17
-
18
- /**
19
- * Creates a reactive state container with the provided initial value.
20
- */
21
- export const state = <T>(initialValue: T, equalityFn: (a: T, b: T) => boolean = Object.is): State<T> =>
22
- StateImpl.createState(initialValue, equalityFn)
23
-
24
- /**
25
- * Registers a function to run whenever its reactive dependencies change.
26
- */
27
- export const effect = (fn: () => void): Unsubscribe => StateImpl.createEffect(fn)
28
-
29
- /**
30
- * Groups multiple state updates to trigger effects only once at the end.
31
- */
32
- export const batch = <T>(fn: () => T): T => StateImpl.executeBatch(fn)
33
-
34
- /**
35
- * Creates a read-only computed value that updates when its dependencies change.
36
- */
37
- export const derive = <T>(computeFn: () => T): ReadOnlyState<T> => StateImpl.createDerive(computeFn)
38
-
39
- /**
40
- * Creates an efficient subscription to a subset of a state value.
41
- */
42
- export const select = <T, R>(
43
- source: ReadOnlyState<T>,
44
- selectorFn: (state: T) => R,
45
- equalityFn: (a: R, b: R) => boolean = Object.is
46
- ): ReadOnlyState<R> => StateImpl.createSelect(source, selectorFn, equalityFn)
47
-
48
- /**
49
- * Creates a read-only view of a state, hiding mutation methods.
50
- */
51
- export const readonlyState =
52
- <T>(state: State<T>): ReadOnlyState<T> =>
53
- (): T =>
54
- state()
55
-
56
- /**
57
- * Creates a state with access control, returning a tuple of reader and writer.
58
- */
59
- export const protectedState = <T>(
60
- initialValue: T,
61
- equalityFn: (a: T, b: T) => boolean = Object.is
62
- ): [ReadOnlyState<T>, WriteableState<T>] => {
63
- const fullState = state(initialValue, equalityFn)
64
- return [
65
- (): T => readonlyState(fullState)(),
66
- {
67
- set: (value: T): void => fullState.set(value),
68
- update: (fn: (value: T) => T): void => fullState.update(fn),
69
- },
70
- ]
71
- }
72
-
73
- /**
74
- * Creates a lens for direct updates to nested properties of a state.
75
- */
76
- export const lens = <T, K>(source: State<T>, accessor: (state: T) => K): State<K> =>
77
- StateImpl.createLens(source, accessor)
78
-
79
- class StateImpl<T> {
80
- // Static fields track global reactivity state - this centralized approach allows
81
- // for coordinated updates while maintaining individual state isolation
82
- private static currentSubscriber: Subscriber | null = null
83
- private static pendingSubscribers = new Set<Subscriber>()
84
- private static isNotifying = false
85
- private static batchDepth = 0
86
- private static deferredEffectCreations: Subscriber[] = []
87
- private static activeSubscribers = new Set<Subscriber>()
88
-
89
- // WeakMaps enable automatic garbage collection when subscribers are no
90
- // longer referenced, preventing memory leaks in long-running applications
91
- private static stateTracking = new WeakMap<Subscriber, Set<symbol>>()
92
- private static subscriberDependencies = new WeakMap<Subscriber, Set<Set<Subscriber>>>()
93
- private static parentSubscriber = new WeakMap<Subscriber, Subscriber>()
94
- private static childSubscribers = new WeakMap<Subscriber, Set<Subscriber>>()
95
-
96
- // Instance state - each state has unique subscribers and ID
97
- private value: T
98
- private subscribers = new Set<Subscriber>()
99
- private stateId = Symbol()
100
- private equalityFn: (a: T, b: T) => boolean
101
-
102
- constructor(initialValue: T, equalityFn: (a: T, b: T) => boolean = Object.is) {
103
- this.value = initialValue
104
- this.equalityFn = equalityFn
105
- }
106
-
107
- /**
108
- * Creates a reactive state container with the provided initial value.
109
- * Implementation of the public 'state' function.
110
- */
111
- static createState = <T>(initialValue: T, equalityFn: (a: T, b: T) => boolean = Object.is): State<T> => {
112
- const instance = new StateImpl<T>(initialValue, equalityFn)
113
- const get = (): T => instance.get()
114
- get.set = (value: T): void => instance.set(value)
115
- get.update = (fn: (currentValue: T) => T): void => instance.update(fn)
116
- get[STATE_ID] = instance.stateId
117
- return get as State<T>
118
- }
119
-
120
- // Auto-tracks dependencies when called within effects, creating a fine-grained
121
- // reactivity graph that only updates affected components
122
- get = (): T => {
123
- const currentEffect = StateImpl.currentSubscriber
124
- if (currentEffect) {
125
- // Add this effect to subscribers for future notification
126
- this.subscribers.add(currentEffect)
127
-
128
- // Maintain bidirectional dependency tracking to enable precise cleanup
129
- // when effects are unsubscribed, preventing memory leaks
130
- let dependencies = StateImpl.subscriberDependencies.get(currentEffect)
131
- if (!dependencies) {
132
- dependencies = new Set()
133
- StateImpl.subscriberDependencies.set(currentEffect, dependencies)
134
- }
135
- dependencies.add(this.subscribers)
136
-
137
- // Track read states to detect direct cyclical dependencies that
138
- // could cause infinite loops
139
- let readStates = StateImpl.stateTracking.get(currentEffect)
140
- if (!readStates) {
141
- readStates = new Set()
142
- StateImpl.stateTracking.set(currentEffect, readStates)
143
- }
144
- readStates.add(this.stateId)
145
- }
146
- return this.value
147
- }
148
-
149
- // Handles value updates with built-in optimizations and safeguards
150
- set = (newValue: T): void => {
151
- // Skip updates for unchanged values to prevent redundant effect executions
152
- if (this.equalityFn(this.value, newValue)) {
153
- return
154
- }
155
-
156
- // Infinite loop detection prevents direct self-mutation within effects,
157
- // while allowing nested effect patterns that would otherwise appear cyclical
158
- const effect = StateImpl.currentSubscriber
159
- if (effect) {
160
- const states = StateImpl.stateTracking.get(effect)
161
- if (states?.has(this.stateId) && !StateImpl.parentSubscriber.get(effect)) {
162
- throw new Error('Infinite loop detected: effect() cannot update a state() it depends on!')
163
- }
164
- }
165
-
166
- this.value = newValue
167
-
168
- // Skip updates when there are no subscribers, avoiding unnecessary processing
169
- if (this.subscribers.size === 0) {
170
- return
171
- }
172
-
173
- // Queue notifications instead of executing immediately to support batch operations
174
- // and prevent redundant effect runs
175
- for (const sub of this.subscribers) {
176
- StateImpl.pendingSubscribers.add(sub)
177
- }
178
-
179
- // Immediate execution outside of batches, deferred execution inside batches
180
- if (StateImpl.batchDepth === 0 && !StateImpl.isNotifying) {
181
- StateImpl.notifySubscribers()
182
- }
183
- }
184
-
185
- update = (fn: (currentValue: T) => T): void => {
186
- this.set(fn(this.value))
187
- }
188
-
189
- /**
190
- * Registers a function to run whenever its reactive dependencies change.
191
- * Implementation of the public 'effect' function.
192
- */
193
- static createEffect = (fn: () => void): Unsubscribe => {
194
- const runEffect = (): void => {
195
- // Prevent re-entrance to avoid cascade updates during effect execution
196
- if (StateImpl.activeSubscribers.has(runEffect)) {
197
- return
198
- }
199
-
200
- StateImpl.activeSubscribers.add(runEffect)
201
- const parentEffect = StateImpl.currentSubscriber
202
-
203
- try {
204
- // Clean existing subscriptions before running to ensure only
205
- // currently accessed states are tracked as dependencies
206
- StateImpl.cleanupEffect(runEffect)
207
-
208
- // Set current context for automatic dependency tracking
209
- StateImpl.currentSubscriber = runEffect
210
- StateImpl.stateTracking.set(runEffect, new Set())
211
-
212
- // Track parent-child relationships to handle nested effects correctly
213
- // and enable hierarchical cleanup later
214
- if (parentEffect) {
215
- StateImpl.parentSubscriber.set(runEffect, parentEffect)
216
- let children = StateImpl.childSubscribers.get(parentEffect)
217
- if (!children) {
218
- children = new Set()
219
- StateImpl.childSubscribers.set(parentEffect, children)
220
- }
221
- children.add(runEffect)
222
- }
223
-
224
- // Execute the effect function, which will auto-track dependencies
225
- fn()
226
- } finally {
227
- // Restore previous context when done
228
- StateImpl.currentSubscriber = parentEffect
229
- StateImpl.activeSubscribers.delete(runEffect)
230
- }
231
- }
232
-
233
- // Run immediately unless we're in a batch operation
234
- if (StateImpl.batchDepth === 0) {
235
- runEffect()
236
- } else {
237
- // Still track parent-child relationship even when deferred,
238
- // ensuring proper hierarchical cleanup later
239
- if (StateImpl.currentSubscriber) {
240
- const parent = StateImpl.currentSubscriber
241
- StateImpl.parentSubscriber.set(runEffect, parent)
242
- let children = StateImpl.childSubscribers.get(parent)
243
- if (!children) {
244
- children = new Set()
245
- StateImpl.childSubscribers.set(parent, children)
246
- }
247
- children.add(runEffect)
248
- }
249
-
250
- // Queue for execution when batch completes
251
- StateImpl.deferredEffectCreations.push(runEffect)
252
- }
253
-
254
- // Return cleanup function to properly disconnect from reactivity graph
255
- return (): void => {
256
- // Remove from dependency tracking to stop future notifications
257
- StateImpl.cleanupEffect(runEffect)
258
- StateImpl.pendingSubscribers.delete(runEffect)
259
- StateImpl.activeSubscribers.delete(runEffect)
260
- StateImpl.stateTracking.delete(runEffect)
261
-
262
- // Clean up parent-child relationship bidirectionally
263
- const parent = StateImpl.parentSubscriber.get(runEffect)
264
- if (parent) {
265
- const siblings = StateImpl.childSubscribers.get(parent)
266
- if (siblings) {
267
- siblings.delete(runEffect)
268
- }
269
- }
270
- StateImpl.parentSubscriber.delete(runEffect)
271
-
272
- // Recursively clean up child effects to prevent memory leaks in
273
- // nested effect scenarios
274
- const children = StateImpl.childSubscribers.get(runEffect)
275
- if (children) {
276
- for (const child of children) {
277
- StateImpl.cleanupEffect(child)
278
- }
279
- children.clear()
280
- StateImpl.childSubscribers.delete(runEffect)
281
- }
282
- }
283
- }
284
-
285
- /**
286
- * Groups multiple state updates to trigger effects only once at the end.
287
- * Implementation of the public 'batch' function.
288
- */
289
- static executeBatch = <T>(fn: () => T): T => {
290
- // Increment depth counter to handle nested batches correctly
291
- StateImpl.batchDepth++
292
- try {
293
- return fn()
294
- } catch (error: unknown) {
295
- // Clean up on error to prevent stale subscribers from executing
296
- // and potentially causing cascading errors
297
- if (StateImpl.batchDepth === 1) {
298
- StateImpl.pendingSubscribers.clear()
299
- StateImpl.deferredEffectCreations.length = 0
300
- }
301
- throw error
302
- } finally {
303
- StateImpl.batchDepth--
304
-
305
- // Only process effects when exiting the outermost batch,
306
- // maintaining proper execution order while avoiding redundant runs
307
- if (StateImpl.batchDepth === 0) {
308
- // Process effects created during the batch
309
- if (StateImpl.deferredEffectCreations.length > 0) {
310
- const effectsToRun = [...StateImpl.deferredEffectCreations]
311
- StateImpl.deferredEffectCreations.length = 0
312
- for (const effect of effectsToRun) {
313
- effect()
314
- }
315
- }
316
-
317
- // Process state updates that occurred during the batch
318
- if (StateImpl.pendingSubscribers.size > 0 && !StateImpl.isNotifying) {
319
- StateImpl.notifySubscribers()
320
- }
321
- }
322
- }
323
- }
324
-
325
- /**
326
- * Creates a read-only computed value that updates when its dependencies change.
327
- * Implementation of the public 'derive' function.
328
- */
329
- static createDerive = <T>(computeFn: () => T): ReadOnlyState<T> => {
330
- const valueState = StateImpl.createState<T | undefined>(undefined)
331
- let initialized = false
332
- let cachedValue: T
333
-
334
- // Internal effect automatically tracks dependencies and updates the derived value
335
- StateImpl.createEffect((): void => {
336
- const newValue = computeFn()
337
-
338
- // Only update if the value actually changed to preserve referential equality
339
- // and prevent unnecessary downstream updates
340
- if (!(initialized && Object.is(cachedValue, newValue))) {
341
- cachedValue = newValue
342
- valueState.set(newValue)
343
- }
344
-
345
- initialized = true
346
- })
347
-
348
- // Return function with lazy initialization - ensures value is available
349
- // even when accessed before its dependencies have had a chance to update
350
- return (): T => {
351
- if (!initialized) {
352
- cachedValue = computeFn()
353
- initialized = true
354
- valueState.set(cachedValue)
355
- }
356
- return valueState() as T
357
- }
358
- }
359
-
360
- /**
361
- * Creates an efficient subscription to a subset of a state value.
362
- * Implementation of the public 'select' function.
363
- */
364
- static createSelect = <T, R>(
365
- source: ReadOnlyState<T>,
366
- selectorFn: (state: T) => R,
367
- equalityFn: (a: R, b: R) => boolean = Object.is
368
- ): ReadOnlyState<R> => {
369
- let lastSourceValue: T | undefined
370
- let lastSelectedValue: R | undefined
371
- let initialized = false
372
- const valueState = StateImpl.createState<R | undefined>(undefined)
373
-
374
- // Internal effect to track the source and update only when needed
375
- StateImpl.createEffect((): void => {
376
- const sourceValue = source()
377
-
378
- // Skip computation if source reference hasn't changed
379
- if (initialized && Object.is(lastSourceValue, sourceValue)) {
380
- return
381
- }
382
-
383
- lastSourceValue = sourceValue
384
- const newSelectedValue = selectorFn(sourceValue)
385
-
386
- // Use custom equality function to determine if value semantically changed,
387
- // allowing for deep equality comparisons with complex objects
388
- if (initialized && lastSelectedValue !== undefined && equalityFn(lastSelectedValue, newSelectedValue)) {
389
- return
390
- }
391
-
392
- // Update cache and notify subscribers due the value has changed
393
- lastSelectedValue = newSelectedValue
394
- valueState.set(newSelectedValue)
395
- initialized = true
396
- })
397
-
398
- // Return function with eager initialization capability
399
- return (): R => {
400
- if (!initialized) {
401
- lastSourceValue = source()
402
- lastSelectedValue = selectorFn(lastSourceValue)
403
- valueState.set(lastSelectedValue)
404
- initialized = true
405
- }
406
- return valueState() as R
407
- }
408
- }
409
-
410
- /**
411
- * Creates a lens for direct updates to nested properties of a state.
412
- * Implementation of the public 'lens' function.
413
- */
414
- static createLens = <T, K>(source: State<T>, accessor: (state: T) => K): State<K> => {
415
- // Extract the property path once during lens creation
416
- const extractPath = (): (string | number)[] => {
417
- const path: (string | number)[] = []
418
- const proxy = new Proxy(
419
- {},
420
- {
421
- get: (_: object, prop: string | symbol): unknown => {
422
- if (typeof prop === 'string' || typeof prop === 'number') {
423
- path.push(prop)
424
- }
425
- return proxy
426
- },
427
- }
428
- )
429
-
430
- try {
431
- accessor(proxy as unknown as T)
432
- } catch {
433
- // Ignore errors, we're just collecting the path
434
- }
435
-
436
- return path
437
- }
438
-
439
- // Capture the path once
440
- const path = extractPath()
441
-
442
- // Create a state with the initial value from the source
443
- const lensState = StateImpl.createState<K>(accessor(source()))
444
-
445
- // Prevent circular updates
446
- let isUpdating = false
447
-
448
- // Set up an effect to sync from source to lens
449
- StateImpl.createEffect((): void => {
450
- if (isUpdating) {
451
- return
452
- }
453
-
454
- isUpdating = true
455
- try {
456
- lensState.set(accessor(source()))
457
- } finally {
458
- isUpdating = false
459
- }
460
- })
461
-
462
- // Override the lens state's set method to update the source
463
- const originalSet = lensState.set
464
- lensState.set = (value: K): void => {
465
- if (isUpdating) {
466
- return
467
- }
468
-
469
- isUpdating = true
470
- try {
471
- // Update lens state
472
- originalSet(value)
473
-
474
- // Update source by modifying the value at path
475
- source.update((current: T): T => setValueAtPath(current, path, value))
476
- } finally {
477
- isUpdating = false
478
- }
479
- }
480
-
481
- // Add update method for completeness
482
- lensState.update = (fn: (value: K) => K): void => {
483
- lensState.set(fn(lensState()))
484
- }
485
-
486
- return lensState
487
- }
488
-
489
- // Processes queued subscriber notifications in a controlled, non-reentrant way
490
- private static notifySubscribers = (): void => {
491
- // Prevent reentrance to avoid cascading notification loops when
492
- // effects trigger further state changes
493
- if (StateImpl.isNotifying) {
494
- return
495
- }
496
-
497
- StateImpl.isNotifying = true
498
-
499
- try {
500
- // Process all pending effects in batches for better perf,
501
- // ensuring topological execution order is maintained
502
- while (StateImpl.pendingSubscribers.size > 0) {
503
- // Process in snapshot batches to prevent infinite loops
504
- // when effects trigger further state changes
505
- const subscribers = Array.from(StateImpl.pendingSubscribers)
506
- StateImpl.pendingSubscribers.clear()
507
-
508
- for (const effect of subscribers) {
509
- effect()
510
- }
511
- }
512
- } finally {
513
- StateImpl.isNotifying = false
514
- }
515
- }
516
-
517
- // Removes effect from dependency tracking to prevent memory leaks
518
- private static cleanupEffect = (effect: Subscriber): void => {
519
- // Remove from execution queue to prevent stale updates
520
- StateImpl.pendingSubscribers.delete(effect)
521
-
522
- // Remove bidirectional dependency references to prevent memory leaks
523
- const deps = StateImpl.subscriberDependencies.get(effect)
524
- if (deps) {
525
- for (const subscribers of deps) {
526
- subscribers.delete(effect)
527
- }
528
- deps.clear()
529
- StateImpl.subscriberDependencies.delete(effect)
530
- }
531
- }
532
- }
533
- // Helper for array updates
534
- const updateArrayItem = <V>(arr: unknown[], index: number, value: V): unknown[] => {
535
- const copy = [...arr]
536
- copy[index] = value
537
- return copy
538
- }
539
-
540
- // Helper for single-level updates (optimization)
541
- const updateShallowProperty = <V>(
542
- obj: Record<string | number, unknown>,
543
- key: string | number,
544
- value: V
545
- ): Record<string | number, unknown> => {
546
- const result = { ...obj }
547
- result[key] = value
548
- return result
549
- }
550
-
551
- // Helper to create the appropriate container type
552
- const createContainer = (key: string | number): Record<string | number, unknown> | unknown[] => {
553
- const isArrayKey = typeof key === 'number' || !Number.isNaN(Number(key))
554
- return isArrayKey ? [] : {}
555
- }
556
-
557
- // Helper for handling array path updates
558
- const updateArrayPath = <V>(array: unknown[], pathSegments: (string | number)[], value: V): unknown[] => {
559
- const index = Number(pathSegments[0])
560
-
561
- if (pathSegments.length === 1) {
562
- // Simple array item update
563
- return updateArrayItem(array, index, value)
564
- }
565
-
566
- // Nested path in array
567
- const copy = [...array]
568
- const nextPathSegments = pathSegments.slice(1)
569
- const nextKey = nextPathSegments[0]
570
-
571
- // For null/undefined values in arrays, create appropriate containers
572
- let nextValue = array[index]
573
- if (nextValue === undefined || nextValue === null) {
574
- // Use empty object as default if nextKey is undefined
575
- nextValue = nextKey !== undefined ? createContainer(nextKey) : {}
576
- }
577
-
578
- copy[index] = setValueAtPath(nextValue, nextPathSegments, value)
579
- return copy
580
- }
581
-
582
- // Helper for handling object path updates
583
- const updateObjectPath = <V>(
584
- obj: Record<string | number, unknown>,
585
- pathSegments: (string | number)[],
586
- value: V
587
- ): Record<string | number, unknown> => {
588
- // Ensure we have a valid key
589
- const currentKey = pathSegments[0]
590
- if (currentKey === undefined) {
591
- // This shouldn't happen given our checks in the main function
592
- return obj
593
- }
594
-
595
- if (pathSegments.length === 1) {
596
- // Simple object property update
597
- return updateShallowProperty(obj, currentKey, value)
598
- }
599
-
600
- // Nested path in object
601
- const nextPathSegments = pathSegments.slice(1)
602
- const nextKey = nextPathSegments[0]
603
-
604
- // For null/undefined values, create appropriate containers
605
- let currentValue = obj[currentKey]
606
- if (currentValue === undefined || currentValue === null) {
607
- // Use empty object as default if nextKey is undefined
608
- currentValue = nextKey !== undefined ? createContainer(nextKey) : {}
609
- }
610
-
611
- // Create new object with updated property
612
- const result = { ...obj }
613
- result[currentKey] = setValueAtPath(currentValue, nextPathSegments, value)
614
- return result
615
- }
616
-
617
- // Simplified function to update a nested value at a path
618
- const setValueAtPath = <V, O>(obj: O, pathSegments: (string | number)[], value: V): O => {
619
- // Handle base cases
620
- if (pathSegments.length === 0) {
621
- return value as unknown as O
622
- }
623
-
624
- if (obj === undefined || obj === null) {
625
- return setValueAtPath({} as O, pathSegments, value)
626
- }
627
-
628
- const currentKey = pathSegments[0]
629
- if (currentKey === undefined) {
630
- return obj
631
- }
632
-
633
- // Delegate to specialized handlers based on data type
634
- if (Array.isArray(obj)) {
635
- return updateArrayPath(obj, pathSegments, value) as unknown as O
636
- }
637
-
638
- return updateObjectPath(obj as Record<string | number, unknown>, pathSegments, value) as unknown as O
639
- }