@nerdalytics/beacon 1000.2.1 → 1000.2.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
@@ -4,6 +4,7 @@
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/latest)](https://badge.socket.dev/npm/package/@nerdalytics/beacon/latest)
7
8
 
8
9
  [![tech:nodejs](https://img.shields.io/badge/Node%20js-339933?style=for-the-badge&logo=nodedotjs&logoColor=white)](https://nodejs.org/)
9
10
  [![language:typescript](https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white)](https://typescriptlang.org/)
@@ -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;
@@ -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,97 @@
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.7",
6
+ "@types/node": "24.9.1",
7
+ "npm-check-updates": "19.1.1",
8
+ "typescript": "5.9.3",
9
+ "uglify-js": "3.19.3"
10
+ },
11
+ "engines": {
12
+ "node": ">=20.0.0"
13
+ },
9
14
  "exports": {
10
15
  ".": {
11
- "typescript": "./src/index.ts",
12
- "default": "./dist/src/index.js"
16
+ "import": {
17
+ "default": "./dist/src/index.min.js",
18
+ "types": "./dist/src/index.d.ts"
19
+ },
20
+ "require": {
21
+ "default": "./dist/src/index.min.js"
22
+ },
23
+ "types": "./dist/src/index.d.ts",
24
+ "typescript": "./src/index.ts"
13
25
  }
14
26
  },
27
+ "files": [
28
+ "dist/src/index.min.js",
29
+ "dist/src/index.d.ts",
30
+ "src/index.ts",
31
+ "LICENSE"
32
+ ],
33
+ "keywords": [
34
+ "backend",
35
+ "batching",
36
+ "computed-values",
37
+ "dependency-tracking",
38
+ "effects",
39
+ "fine-grained",
40
+ "lightweight",
41
+ "memory-management",
42
+ "nodejs",
43
+ "performance",
44
+ "reactive",
45
+ "server-side",
46
+ "signals",
47
+ "state-management",
48
+ "typescript"
49
+ ],
50
+ "license": "MIT",
51
+ "main": "dist/src/index.min.js",
52
+ "name": "@nerdalytics/beacon",
53
+ "packageManager": "npm@11.6.2",
15
54
  "repository": {
16
- "url": "git+https://github.com/nerdalytics/beacon.git",
17
- "type": "git"
55
+ "type": "git",
56
+ "url": "git+https://github.com/nerdalytics/beacon.git"
18
57
  },
19
58
  "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
59
  "benchmark": "node scripts/benchmark.ts",
60
+ "benchmark:naiv": "node scripts/naiv-benchmark.ts",
40
61
  "build": "npm run build:lts",
41
- "prebuild:lts": "rm -rf dist/",
42
62
  "build:lts": "tsc -p tsconfig.lts.json",
63
+ "check": "npx @biomejs/biome check",
64
+ "check:fix": "npx @biomejs/biome format --fix",
65
+ "format": "npx @biomejs/biome format --write",
66
+ "lint": "npx @biomejs/biome lint",
67
+ "lint:fix": "npx @biomejs/biome lint --fix",
68
+ "lint:fix:unsafe": "npx @biomejs/biome lint --fix --unsafe",
43
69
  "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",
70
+ "prebuild:lts": "rm -rf dist/",
44
71
  "prepublishOnly": "npm run build:lts",
45
72
  "pretest:lts": "node scripts/run-lts-tests.js",
73
+ "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
+ "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",
46
78
  "test:lts:20": "node --test dist/tests/**.js",
47
79
  "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",
48
92
  "update-performance-docs": "node --experimental-config-file=node.config.json scripts/update-performance-docs.ts"
49
93
  },
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"
94
+ "type": "module",
95
+ "types": "dist/src/index.d.ts",
96
+ "version": "1000.2.3"
80
97
  }
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // Core types for reactive primitives
2
2
  type Subscriber = () => void
3
- type Unsubscribe = () => void
3
+ export type Unsubscribe = () => void
4
4
  export type ReadOnlyState<T> = () => T
5
5
  export interface WriteableState<T> {
6
6
  set(value: T): void
@@ -8,7 +8,7 @@ export interface WriteableState<T> {
8
8
  }
9
9
 
10
10
  // Special symbol used for internal tracking
11
- const STATE_ID = Symbol()
11
+ const STATE_ID: unique symbol = Symbol('STATE_ID')
12
12
 
13
13
  export type State<T> = ReadOnlyState<T> &
14
14
  WriteableState<T> & {
@@ -59,7 +59,10 @@ export const readonlyState =
59
59
  export const protectedState = <T>(
60
60
  initialValue: T,
61
61
  equalityFn: (a: T, b: T) => boolean = Object.is
62
- ): [ReadOnlyState<T>, WriteableState<T>] => {
62
+ ): [
63
+ ReadOnlyState<T>,
64
+ WriteableState<T>,
65
+ ] => {
63
66
  const fullState = state(initialValue, equalityFn)
64
67
  return [
65
68
  (): T => readonlyState(fullState)(),
@@ -307,7 +310,9 @@ class StateImpl<T> {
307
310
  if (StateImpl.batchDepth === 0) {
308
311
  // Process effects created during the batch
309
312
  if (StateImpl.deferredEffectCreations.length > 0) {
310
- const effectsToRun = [...StateImpl.deferredEffectCreations]
313
+ const effectsToRun = [
314
+ ...StateImpl.deferredEffectCreations,
315
+ ]
311
316
  StateImpl.deferredEffectCreations.length = 0
312
317
  for (const effect of effectsToRun) {
313
318
  effect()
@@ -327,33 +332,37 @@ class StateImpl<T> {
327
332
  * Implementation of the public 'derive' function.
328
333
  */
329
334
  static createDerive = <T>(computeFn: () => T): ReadOnlyState<T> => {
330
- const valueState = StateImpl.createState<T | undefined>(undefined)
331
- let initialized = false
332
- let cachedValue: T
335
+ // Create a container to hold state and minimize closure captures
336
+ const container = {
337
+ cachedValue: undefined as unknown as T,
338
+ computeFn,
339
+ initialized: false,
340
+ valueState: StateImpl.createState<T | undefined>(undefined),
341
+ }
333
342
 
334
343
  // Internal effect automatically tracks dependencies and updates the derived value
335
- StateImpl.createEffect((): void => {
336
- const newValue = computeFn()
344
+ StateImpl.createEffect(function deriveEffect(): void {
345
+ const newValue = container.computeFn()
337
346
 
338
347
  // Only update if the value actually changed to preserve referential equality
339
348
  // and prevent unnecessary downstream updates
340
- if (!(initialized && Object.is(cachedValue, newValue))) {
341
- cachedValue = newValue
342
- valueState.set(newValue)
349
+ if (!(container.initialized && Object.is(container.cachedValue, newValue))) {
350
+ container.cachedValue = newValue
351
+ container.valueState.set(newValue)
343
352
  }
344
353
 
345
- initialized = true
354
+ container.initialized = true
346
355
  })
347
356
 
348
357
  // Return function with lazy initialization - ensures value is available
349
358
  // 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)
359
+ return function deriveGetter(): T {
360
+ if (!container.initialized) {
361
+ container.cachedValue = container.computeFn()
362
+ container.initialized = true
363
+ container.valueState.set(container.cachedValue)
355
364
  }
356
- return valueState() as T
365
+ return container.valueState() as T
357
366
  }
358
367
  }
359
368
 
@@ -366,44 +375,54 @@ class StateImpl<T> {
366
375
  selectorFn: (state: T) => R,
367
376
  equalityFn: (a: R, b: R) => boolean = Object.is
368
377
  ): ReadOnlyState<R> => {
369
- let lastSourceValue: T | undefined
370
- let lastSelectedValue: R | undefined
371
- let initialized = false
372
- const valueState = StateImpl.createState<R | undefined>(undefined)
378
+ // Create a container to hold state and minimize closure captures
379
+ const container = {
380
+ equalityFn,
381
+ initialized: false,
382
+ lastSelectedValue: undefined as R | undefined,
383
+ lastSourceValue: undefined as T | undefined,
384
+ selectorFn,
385
+ source,
386
+ valueState: StateImpl.createState<R | undefined>(undefined),
387
+ }
373
388
 
374
389
  // Internal effect to track the source and update only when needed
375
- StateImpl.createEffect((): void => {
376
- const sourceValue = source()
390
+ StateImpl.createEffect(function selectEffect(): void {
391
+ const sourceValue = container.source()
377
392
 
378
393
  // Skip computation if source reference hasn't changed
379
- if (initialized && Object.is(lastSourceValue, sourceValue)) {
394
+ if (container.initialized && Object.is(container.lastSourceValue, sourceValue)) {
380
395
  return
381
396
  }
382
397
 
383
- lastSourceValue = sourceValue
384
- const newSelectedValue = selectorFn(sourceValue)
398
+ container.lastSourceValue = sourceValue
399
+ const newSelectedValue = container.selectorFn(sourceValue)
385
400
 
386
401
  // Use custom equality function to determine if value semantically changed,
387
402
  // allowing for deep equality comparisons with complex objects
388
- if (initialized && lastSelectedValue !== undefined && equalityFn(lastSelectedValue, newSelectedValue)) {
403
+ if (
404
+ container.initialized &&
405
+ container.lastSelectedValue !== undefined &&
406
+ container.equalityFn(container.lastSelectedValue, newSelectedValue)
407
+ ) {
389
408
  return
390
409
  }
391
410
 
392
411
  // Update cache and notify subscribers due the value has changed
393
- lastSelectedValue = newSelectedValue
394
- valueState.set(newSelectedValue)
395
- initialized = true
412
+ container.lastSelectedValue = newSelectedValue
413
+ container.valueState.set(newSelectedValue)
414
+ container.initialized = true
396
415
  })
397
416
 
398
417
  // 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
418
+ return function selectGetter(): R {
419
+ if (!container.initialized) {
420
+ container.lastSourceValue = container.source()
421
+ container.lastSelectedValue = container.selectorFn(container.lastSourceValue)
422
+ container.valueState.set(container.lastSelectedValue)
423
+ container.initialized = true
405
424
  }
406
- return valueState() as R
425
+ return container.valueState() as R
407
426
  }
408
427
  }
409
428
 
@@ -412,15 +431,25 @@ class StateImpl<T> {
412
431
  * Implementation of the public 'lens' function.
413
432
  */
414
433
  static createLens = <T, K>(source: State<T>, accessor: (state: T) => K): State<K> => {
434
+ // Create a container to hold lens state and minimize closure captures
435
+ const container = {
436
+ accessor,
437
+ isUpdating: false,
438
+ lensState: null as unknown as State<K>,
439
+ originalSet: null as unknown as (value: K) => void,
440
+ path: [] as (string | number)[],
441
+ source,
442
+ }
443
+
415
444
  // Extract the property path once during lens creation
416
445
  const extractPath = (): (string | number)[] => {
417
- const path: (string | number)[] = []
446
+ const pathCollector: (string | number)[] = []
418
447
  const proxy = new Proxy(
419
448
  {},
420
449
  {
421
450
  get: (_: object, prop: string | symbol): unknown => {
422
451
  if (typeof prop === 'string' || typeof prop === 'number') {
423
- path.push(prop)
452
+ pathCollector.push(prop)
424
453
  }
425
454
  return proxy
426
455
  },
@@ -428,62 +457,59 @@ class StateImpl<T> {
428
457
  )
429
458
 
430
459
  try {
431
- accessor(proxy as unknown as T)
460
+ container.accessor(proxy as unknown as T)
432
461
  } catch {
433
462
  // Ignore errors, we're just collecting the path
434
463
  }
435
464
 
436
- return path
465
+ return pathCollector
437
466
  }
438
467
 
439
468
  // Capture the path once
440
- const path = extractPath()
469
+ container.path = extractPath()
441
470
 
442
471
  // 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
472
+ container.lensState = StateImpl.createState<K>(container.accessor(container.source()))
473
+ container.originalSet = container.lensState.set
447
474
 
448
475
  // Set up an effect to sync from source to lens
449
- StateImpl.createEffect((): void => {
450
- if (isUpdating) {
476
+ StateImpl.createEffect(function lensEffect(): void {
477
+ if (container.isUpdating) {
451
478
  return
452
479
  }
453
480
 
454
- isUpdating = true
481
+ container.isUpdating = true
455
482
  try {
456
- lensState.set(accessor(source()))
483
+ container.lensState.set(container.accessor(container.source()))
457
484
  } finally {
458
- isUpdating = false
485
+ container.isUpdating = false
459
486
  }
460
487
  })
461
488
 
462
489
  // 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) {
490
+ container.lensState.set = function lensSet(value: K): void {
491
+ if (container.isUpdating) {
466
492
  return
467
493
  }
468
494
 
469
- isUpdating = true
495
+ container.isUpdating = true
470
496
  try {
471
497
  // Update lens state
472
- originalSet(value)
498
+ container.originalSet(value)
473
499
 
474
500
  // Update source by modifying the value at path
475
- source.update((current: T): T => setValueAtPath(current, path, value))
501
+ container.source.update((current: T): T => setValueAtPath(current, container.path, value))
476
502
  } finally {
477
- isUpdating = false
503
+ container.isUpdating = false
478
504
  }
479
505
  }
480
506
 
481
507
  // Add update method for completeness
482
- lensState.update = (fn: (value: K) => K): void => {
483
- lensState.set(fn(lensState()))
508
+ container.lensState.update = function lensUpdate(fn: (value: K) => K): void {
509
+ container.lensState.set(fn(container.lensState()))
484
510
  }
485
511
 
486
- return lensState
512
+ return container.lensState
487
513
  }
488
514
 
489
515
  // Processes queued subscriber notifications in a controlled, non-reentrant way
@@ -532,7 +558,9 @@ class StateImpl<T> {
532
558
  }
533
559
  // Helper for array updates
534
560
  const updateArrayItem = <V>(arr: unknown[], index: number, value: V): unknown[] => {
535
- const copy = [...arr]
561
+ const copy = [
562
+ ...arr,
563
+ ]
536
564
  copy[index] = value
537
565
  return copy
538
566
  }
@@ -543,7 +571,9 @@ const updateShallowProperty = <V>(
543
571
  key: string | number,
544
572
  value: V
545
573
  ): Record<string | number, unknown> => {
546
- const result = { ...obj }
574
+ const result = {
575
+ ...obj,
576
+ }
547
577
  result[key] = value
548
578
  return result
549
579
  }
@@ -564,7 +594,9 @@ const updateArrayPath = <V>(array: unknown[], pathSegments: (string | number)[],
564
594
  }
565
595
 
566
596
  // Nested path in array
567
- const copy = [...array]
597
+ const copy = [
598
+ ...array,
599
+ ]
568
600
  const nextPathSegments = pathSegments.slice(1)
569
601
  const nextKey = nextPathSegments[0]
570
602
 
@@ -609,7 +641,9 @@ const updateObjectPath = <V>(
609
641
  }
610
642
 
611
643
  // Create new object with updated property
612
- const result = { ...obj }
644
+ const result = {
645
+ ...obj,
646
+ }
613
647
  result[currentKey] = setValueAtPath(currentValue, nextPathSegments, value)
614
648
  return result
615
649
  }
package/dist/src/index.js DELETED
@@ -1,379 +0,0 @@
1
- const STATE_ID = Symbol();
2
- export const state = (initialValue, equalityFn = Object.is) => StateImpl.createState(initialValue, equalityFn);
3
- export const effect = (fn) => StateImpl.createEffect(fn);
4
- export const batch = (fn) => StateImpl.executeBatch(fn);
5
- export const derive = (computeFn) => StateImpl.createDerive(computeFn);
6
- export const select = (source, selectorFn, equalityFn = Object.is) => StateImpl.createSelect(source, selectorFn, equalityFn);
7
- export const readonlyState = (state) => () => state();
8
- export const protectedState = (initialValue, equalityFn = Object.is) => {
9
- const fullState = state(initialValue, equalityFn);
10
- return [
11
- () => readonlyState(fullState)(),
12
- {
13
- set: (value) => fullState.set(value),
14
- update: (fn) => fullState.update(fn),
15
- },
16
- ];
17
- };
18
- export const lens = (source, accessor) => StateImpl.createLens(source, accessor);
19
- class StateImpl {
20
- static currentSubscriber = null;
21
- static pendingSubscribers = new Set();
22
- static isNotifying = false;
23
- static batchDepth = 0;
24
- static deferredEffectCreations = [];
25
- static activeSubscribers = new Set();
26
- static stateTracking = new WeakMap();
27
- static subscriberDependencies = new WeakMap();
28
- static parentSubscriber = new WeakMap();
29
- static childSubscribers = new WeakMap();
30
- value;
31
- subscribers = new Set();
32
- stateId = Symbol();
33
- equalityFn;
34
- constructor(initialValue, equalityFn = Object.is) {
35
- this.value = initialValue;
36
- this.equalityFn = equalityFn;
37
- }
38
- static createState = (initialValue, equalityFn = Object.is) => {
39
- const instance = new StateImpl(initialValue, equalityFn);
40
- const get = () => instance.get();
41
- get.set = (value) => instance.set(value);
42
- get.update = (fn) => instance.update(fn);
43
- get[STATE_ID] = instance.stateId;
44
- return get;
45
- };
46
- get = () => {
47
- const currentEffect = StateImpl.currentSubscriber;
48
- if (currentEffect) {
49
- this.subscribers.add(currentEffect);
50
- let dependencies = StateImpl.subscriberDependencies.get(currentEffect);
51
- if (!dependencies) {
52
- dependencies = new Set();
53
- StateImpl.subscriberDependencies.set(currentEffect, dependencies);
54
- }
55
- dependencies.add(this.subscribers);
56
- let readStates = StateImpl.stateTracking.get(currentEffect);
57
- if (!readStates) {
58
- readStates = new Set();
59
- StateImpl.stateTracking.set(currentEffect, readStates);
60
- }
61
- readStates.add(this.stateId);
62
- }
63
- return this.value;
64
- };
65
- set = (newValue) => {
66
- if (this.equalityFn(this.value, newValue)) {
67
- return;
68
- }
69
- const effect = StateImpl.currentSubscriber;
70
- if (effect) {
71
- const states = StateImpl.stateTracking.get(effect);
72
- if (states?.has(this.stateId) && !StateImpl.parentSubscriber.get(effect)) {
73
- throw new Error('Infinite loop detected: effect() cannot update a state() it depends on!');
74
- }
75
- }
76
- this.value = newValue;
77
- if (this.subscribers.size === 0) {
78
- return;
79
- }
80
- for (const sub of this.subscribers) {
81
- StateImpl.pendingSubscribers.add(sub);
82
- }
83
- if (StateImpl.batchDepth === 0 && !StateImpl.isNotifying) {
84
- StateImpl.notifySubscribers();
85
- }
86
- };
87
- update = (fn) => {
88
- this.set(fn(this.value));
89
- };
90
- static createEffect = (fn) => {
91
- const runEffect = () => {
92
- if (StateImpl.activeSubscribers.has(runEffect)) {
93
- return;
94
- }
95
- StateImpl.activeSubscribers.add(runEffect);
96
- const parentEffect = StateImpl.currentSubscriber;
97
- try {
98
- StateImpl.cleanupEffect(runEffect);
99
- StateImpl.currentSubscriber = runEffect;
100
- StateImpl.stateTracking.set(runEffect, new Set());
101
- if (parentEffect) {
102
- StateImpl.parentSubscriber.set(runEffect, parentEffect);
103
- let children = StateImpl.childSubscribers.get(parentEffect);
104
- if (!children) {
105
- children = new Set();
106
- StateImpl.childSubscribers.set(parentEffect, children);
107
- }
108
- children.add(runEffect);
109
- }
110
- fn();
111
- }
112
- finally {
113
- StateImpl.currentSubscriber = parentEffect;
114
- StateImpl.activeSubscribers.delete(runEffect);
115
- }
116
- };
117
- if (StateImpl.batchDepth === 0) {
118
- runEffect();
119
- }
120
- else {
121
- if (StateImpl.currentSubscriber) {
122
- const parent = StateImpl.currentSubscriber;
123
- StateImpl.parentSubscriber.set(runEffect, parent);
124
- let children = StateImpl.childSubscribers.get(parent);
125
- if (!children) {
126
- children = new Set();
127
- StateImpl.childSubscribers.set(parent, children);
128
- }
129
- children.add(runEffect);
130
- }
131
- StateImpl.deferredEffectCreations.push(runEffect);
132
- }
133
- return () => {
134
- StateImpl.cleanupEffect(runEffect);
135
- StateImpl.pendingSubscribers.delete(runEffect);
136
- StateImpl.activeSubscribers.delete(runEffect);
137
- StateImpl.stateTracking.delete(runEffect);
138
- const parent = StateImpl.parentSubscriber.get(runEffect);
139
- if (parent) {
140
- const siblings = StateImpl.childSubscribers.get(parent);
141
- if (siblings) {
142
- siblings.delete(runEffect);
143
- }
144
- }
145
- StateImpl.parentSubscriber.delete(runEffect);
146
- const children = StateImpl.childSubscribers.get(runEffect);
147
- if (children) {
148
- for (const child of children) {
149
- StateImpl.cleanupEffect(child);
150
- }
151
- children.clear();
152
- StateImpl.childSubscribers.delete(runEffect);
153
- }
154
- };
155
- };
156
- static executeBatch = (fn) => {
157
- StateImpl.batchDepth++;
158
- try {
159
- return fn();
160
- }
161
- catch (error) {
162
- if (StateImpl.batchDepth === 1) {
163
- StateImpl.pendingSubscribers.clear();
164
- StateImpl.deferredEffectCreations.length = 0;
165
- }
166
- throw error;
167
- }
168
- finally {
169
- StateImpl.batchDepth--;
170
- if (StateImpl.batchDepth === 0) {
171
- if (StateImpl.deferredEffectCreations.length > 0) {
172
- const effectsToRun = [...StateImpl.deferredEffectCreations];
173
- StateImpl.deferredEffectCreations.length = 0;
174
- for (const effect of effectsToRun) {
175
- effect();
176
- }
177
- }
178
- if (StateImpl.pendingSubscribers.size > 0 && !StateImpl.isNotifying) {
179
- StateImpl.notifySubscribers();
180
- }
181
- }
182
- }
183
- };
184
- 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);
193
- }
194
- initialized = true;
195
- });
196
- return () => {
197
- if (!initialized) {
198
- cachedValue = computeFn();
199
- initialized = true;
200
- valueState.set(cachedValue);
201
- }
202
- return valueState();
203
- };
204
- };
205
- 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)) {
213
- return;
214
- }
215
- lastSourceValue = sourceValue;
216
- const newSelectedValue = selectorFn(sourceValue);
217
- if (initialized && lastSelectedValue !== undefined && equalityFn(lastSelectedValue, newSelectedValue)) {
218
- return;
219
- }
220
- lastSelectedValue = newSelectedValue;
221
- valueState.set(newSelectedValue);
222
- initialized = true;
223
- });
224
- return () => {
225
- if (!initialized) {
226
- lastSourceValue = source();
227
- lastSelectedValue = selectorFn(lastSourceValue);
228
- valueState.set(lastSelectedValue);
229
- initialized = true;
230
- }
231
- return valueState();
232
- };
233
- };
234
- static createLens = (source, accessor) => {
235
- const extractPath = () => {
236
- const path = [];
237
- const proxy = new Proxy({}, {
238
- get: (_, prop) => {
239
- if (typeof prop === 'string' || typeof prop === 'number') {
240
- path.push(prop);
241
- }
242
- return proxy;
243
- },
244
- });
245
- try {
246
- accessor(proxy);
247
- }
248
- catch {
249
- }
250
- return path;
251
- };
252
- const path = extractPath();
253
- const lensState = StateImpl.createState(accessor(source()));
254
- let isUpdating = false;
255
- StateImpl.createEffect(() => {
256
- if (isUpdating) {
257
- return;
258
- }
259
- isUpdating = true;
260
- try {
261
- lensState.set(accessor(source()));
262
- }
263
- finally {
264
- isUpdating = false;
265
- }
266
- });
267
- const originalSet = lensState.set;
268
- lensState.set = (value) => {
269
- if (isUpdating) {
270
- return;
271
- }
272
- isUpdating = true;
273
- try {
274
- originalSet(value);
275
- source.update((current) => setValueAtPath(current, path, value));
276
- }
277
- finally {
278
- isUpdating = false;
279
- }
280
- };
281
- lensState.update = (fn) => {
282
- lensState.set(fn(lensState()));
283
- };
284
- return lensState;
285
- };
286
- static notifySubscribers = () => {
287
- if (StateImpl.isNotifying) {
288
- return;
289
- }
290
- StateImpl.isNotifying = true;
291
- try {
292
- while (StateImpl.pendingSubscribers.size > 0) {
293
- const subscribers = Array.from(StateImpl.pendingSubscribers);
294
- StateImpl.pendingSubscribers.clear();
295
- for (const effect of subscribers) {
296
- effect();
297
- }
298
- }
299
- }
300
- finally {
301
- StateImpl.isNotifying = false;
302
- }
303
- };
304
- static cleanupEffect = (effect) => {
305
- StateImpl.pendingSubscribers.delete(effect);
306
- const deps = StateImpl.subscriberDependencies.get(effect);
307
- if (deps) {
308
- for (const subscribers of deps) {
309
- subscribers.delete(effect);
310
- }
311
- deps.clear();
312
- StateImpl.subscriberDependencies.delete(effect);
313
- }
314
- };
315
- }
316
- const updateArrayItem = (arr, index, value) => {
317
- const copy = [...arr];
318
- copy[index] = value;
319
- return copy;
320
- };
321
- const updateShallowProperty = (obj, key, value) => {
322
- const result = { ...obj };
323
- result[key] = value;
324
- return result;
325
- };
326
- const createContainer = (key) => {
327
- const isArrayKey = typeof key === 'number' || !Number.isNaN(Number(key));
328
- return isArrayKey ? [] : {};
329
- };
330
- const updateArrayPath = (array, pathSegments, value) => {
331
- const index = Number(pathSegments[0]);
332
- if (pathSegments.length === 1) {
333
- return updateArrayItem(array, index, value);
334
- }
335
- const copy = [...array];
336
- const nextPathSegments = pathSegments.slice(1);
337
- const nextKey = nextPathSegments[0];
338
- let nextValue = array[index];
339
- if (nextValue === undefined || nextValue === null) {
340
- nextValue = nextKey !== undefined ? createContainer(nextKey) : {};
341
- }
342
- copy[index] = setValueAtPath(nextValue, nextPathSegments, value);
343
- return copy;
344
- };
345
- const updateObjectPath = (obj, pathSegments, value) => {
346
- const currentKey = pathSegments[0];
347
- if (currentKey === undefined) {
348
- return obj;
349
- }
350
- if (pathSegments.length === 1) {
351
- return updateShallowProperty(obj, currentKey, value);
352
- }
353
- const nextPathSegments = pathSegments.slice(1);
354
- const nextKey = nextPathSegments[0];
355
- let currentValue = obj[currentKey];
356
- if (currentValue === undefined || currentValue === null) {
357
- currentValue = nextKey !== undefined ? createContainer(nextKey) : {};
358
- }
359
- const result = { ...obj };
360
- result[currentKey] = setValueAtPath(currentValue, nextPathSegments, value);
361
- return result;
362
- };
363
- const setValueAtPath = (obj, pathSegments, value) => {
364
- if (pathSegments.length === 0) {
365
- return value;
366
- }
367
- if (obj === undefined || obj === null) {
368
- return setValueAtPath({}, pathSegments, value);
369
- }
370
- const currentKey = pathSegments[0];
371
- if (currentKey === undefined) {
372
- return obj;
373
- }
374
- if (Array.isArray(obj)) {
375
- return updateArrayPath(obj, pathSegments, value);
376
- }
377
- return updateObjectPath(obj, pathSegments, value);
378
- };
379
- //# sourceMappingURL=index.js.map