@nerdalytics/beacon 1.0.0 โ†’ 1000.1.0

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
- # Beacon
1
+ # Beacon <img align="right" src="https://raw.githubusercontent.com/nerdalytics/beacon/refs/heads/trunk/assets/beacon-logo.svg" width="128px" alt="A stylized lighthouse beacon with golden light against a dark blue background, representing the reactive state library"/>
2
2
 
3
- A lightweight reactive signal library for Node.js backends. Enables reactive state management with automatic dependency tracking and efficient updates for server-side applications.
3
+ A lightweight reactive state library for Node.js backends. Enables reactive state management with automatic dependency tracking and efficient updates for server-side applications.
4
4
 
5
5
  ## Table of Contents
6
6
 
@@ -8,10 +8,14 @@ A lightweight reactive signal library for Node.js backends. Enables reactive sta
8
8
  - [Installation](#installation)
9
9
  - [Usage](#usage)
10
10
  - [API](#api)
11
- - [state](#statetinitialvalue-t-signalt)
12
- - [derived](#derivedfn--t-signalt)
13
- - [effect](#effectfn--void--void)
14
- - [batch](#batchfn--t-t)
11
+ - [state](#statetinitialvalue-t-statet)
12
+ - [derive](#derivetfn---t-readonlystatet)
13
+ - [effect](#effectfn---void---void)
14
+ - [batch](#batchtfn---t-t)
15
+ - [select](#selectt-rsource-readonlystatet-selectorfn-state-t--r-equalityfn-a-r-b-r--boolean-readonlystater)
16
+ - [lens](#lenst-ksource-statet-accessor-state-t--k-statek)
17
+ - [readonlyState](#readonlystatetstate-statet-readonlystatet)
18
+ - [protectedState](#protectedstatetinitialvalue-t-readonlystatet-writeablestatet)
15
19
  - [Development](#development)
16
20
  - [Node.js LTS Compatibility](#nodejs-lts-compatibility)
17
21
  - [Key Differences vs TC39 Proposal](#key-differences-between-my-library-and-the-tc39-proposal)
@@ -21,31 +25,33 @@ A lightweight reactive signal library for Node.js backends. Enables reactive sta
21
25
 
22
26
  ## Features
23
27
 
24
- - ๐Ÿ”„ **Reactive signals** - Create reactive values that automatically track dependencies
25
- - ๐Ÿงฎ **Computed values** - Derive values from other signals with automatic updates
26
- - ๐Ÿ” **Fine-grained reactivity** - Dependencies are tracked precisely at the signal level
28
+ - ๐Ÿ“ถ **Reactive state** - Create reactive values that automatically track dependencies
29
+ - ๐Ÿงฎ **Computed values** - Derive values from other states with automatic updates
30
+ - ๐Ÿ” **Fine-grained reactivity** - Dependencies are tracked precisely at the state level
27
31
  - ๐ŸŽ๏ธ **Efficient updates** - Only recompute values when dependencies change
28
32
  - ๐Ÿ“ฆ **Batched updates** - Group multiple updates for performance
33
+ - ๐ŸŽฏ **Targeted subscriptions** - Select and subscribe to specific parts of state objects
29
34
  - ๐Ÿงน **Automatic cleanup** - Effects and computations automatically clean up dependencies
30
- - ๐Ÿ” **Cycle handling** - Safely manages cyclic dependencies without crashing
35
+ - โ™ป๏ธ **Cycle handling** - Safely manages cyclic dependencies without crashing
36
+ - ๐Ÿšจ **Infinite loop detection** - Automatically detects and prevents infinite update loops
31
37
  - ๐Ÿ› ๏ธ **TypeScript-first** - Full TypeScript support with generics
32
38
  - ๐Ÿชถ **Lightweight** - Zero dependencies, < 200 LOC
33
39
  - โœ… **Node.js compatibility** - Works with Node.js LTS v20+ and v22+
34
40
 
35
41
  ## Installation
36
42
 
37
- ```bash
43
+ ```sh
38
44
  npm install @nerdalytics/beacon
39
45
  ```
40
46
 
41
47
  ## Usage
42
48
 
43
49
  ```typescript
44
- import { state, derived, effect, batch } from "@nerdalytics/beacon";
50
+ import { state, derive, effect, batch, select, readonlyState, protectedState } from "@nerdalytics/beacon";
45
51
 
46
52
  // Create reactive state
47
53
  const count = state(0);
48
- const doubled = derived(() => count() * 2);
54
+ const doubled = derive(() => count() * 2);
49
55
 
50
56
  // Read values
51
57
  console.log(count()); // => 0
@@ -73,20 +79,86 @@ batch(() => {
73
79
  });
74
80
  // => "Count is 20, doubled is 40" (only once)
75
81
 
82
+ // Using select to subscribe to specific parts of state
83
+ const user = state({ name: "Alice", age: 30, email: "alice@example.com" });
84
+ const nameSelector = select(user, u => u.name);
85
+
86
+ effect(() => {
87
+ console.log(`Name changed: ${nameSelector()}`);
88
+ });
89
+ // => "Name changed: Alice"
90
+
91
+ // Updates to the selected property will trigger the effect
92
+ user.update(u => ({ ...u, name: "Bob" }));
93
+ // => "Name changed: Bob"
94
+
95
+ // Updates to other properties won't trigger the effect
96
+ user.update(u => ({ ...u, age: 31 })); // No effect triggered
97
+
98
+ // Using lens for two-way binding with nested properties
99
+ const nested = state({
100
+ user: {
101
+ profile: {
102
+ settings: {
103
+ theme: "dark",
104
+ notifications: true
105
+ }
106
+ }
107
+ }
108
+ });
109
+
110
+ // Create a lens focused on a deeply nested property
111
+ const themeLens = lens(nested, n => n.user.profile.settings.theme);
112
+
113
+ // Read the focused value
114
+ console.log(themeLens()); // => "dark"
115
+
116
+ // Update the focused value directly (maintains referential integrity)
117
+ themeLens.set("light");
118
+ console.log(themeLens()); // => "light"
119
+ console.log(nested().user.profile.settings.theme); // => "light"
120
+
76
121
  // Unsubscribe the effect to stop it from running on future updates
77
122
  // and clean up all its internal subscriptions
78
123
  unsubscribe();
124
+
125
+ // Using readonlyState to create a read-only view
126
+ const counter = state(0);
127
+ const readonlyCounter = readonlyState(counter);
128
+ // readonlyCounter() works, but readonlyCounter.set() is not available
129
+
130
+ // Using protectedState to separate read and write capabilities
131
+ const [getUser, setUser] = protectedState({ name: 'Alice' });
132
+ // getUser() works to read the state
133
+ // setUser.set() and setUser.update() work to modify the state
134
+ // but getUser has no mutation methods
135
+
136
+ // Infinite loop detection example (would throw an error)
137
+ try {
138
+ effect(() => {
139
+ const value = counter();
140
+ // The following would throw an error because it attempts to
141
+ // update a state that the effect depends on:
142
+ // "Infinite loop detected: effect() cannot update a state() it depends on!"
143
+ // counter.set(value + 1);
144
+
145
+ // Instead, use a safe pattern with proper dependencies:
146
+ console.log(`Current counter value: ${value}`);
147
+ });
148
+ } catch (error) {
149
+ console.error('Prevented infinite loop:', error.message);
150
+ }
79
151
  ```
80
152
 
81
153
  ## API
82
154
 
83
- ### `state<T>(initialValue: T): Signal<T>`
155
+ ### `state<T>(initialValue: T): State<T>`
84
156
 
85
- Creates a new reactive signal with the given initial value.
157
+ Creates a new reactive state container with the provided initial value.
86
158
 
87
- ### `derived<T>(fn: () => T): Signal<T>`
159
+ ### `derive<T>(fn: () => T): ReadOnlyState<T>`
88
160
 
89
- Creates a derived signal that updates when its dependencies change.
161
+ Creates a read-only computed value that updates when its dependencies change.
90
162
 
91
163
  ### `effect(fn: () => void): () => void`
92
164
 
@@ -96,9 +168,25 @@ Creates an effect that runs the given function immediately and whenever its depe
96
168
 
97
169
  Batches multiple updates to only trigger effects once at the end.
98
170
 
171
+ ### `select<T, R>(source: ReadOnlyState<T>, selectorFn: (state: T) => R, equalityFn?: (a: R, b: R) => boolean): ReadOnlyState<R>`
172
+
173
+ Creates an efficient subscription to a subset of a state value. The selector will only notify its subscribers when the selected value actually changes according to the provided equality function (defaults to `Object.is`).
174
+
175
+ ### `lens<T, K>(source: State<T>, accessor: (state: T) => K): State<K>`
176
+
177
+ Creates a lens for direct updates to nested properties of a state. A lens combines the functionality of `select` (for reading) with the ability to update the nested property while maintaining referential integrity throughout the object tree.
178
+
179
+ ### `readonlyState<T>(state: State<T>): ReadOnlyState<T>`
180
+
181
+ Creates a read-only view of a state, hiding mutation methods. Useful when you want to expose a state to other parts of your application without allowing direct mutations.
182
+
183
+ ### `protectedState<T>(initialValue: T): [ReadOnlyState<T>, WriteableState<T>]`
184
+
185
+ Creates a state with access control, returning a tuple of reader and writer. This pattern separates read and write capabilities, allowing you to expose only the reading capability to consuming code while keeping the writing capability private.
186
+
99
187
  ## Development
100
188
 
101
- ```bash
189
+ ```sh
102
190
  # Install dependencies
103
191
  npm install
104
192
 
@@ -111,19 +199,27 @@ npm run test:coverage
111
199
  # Run specific test suites
112
200
  # Core functionality
113
201
  npm run test:unit:state
114
- npm run test:unit:derived
115
202
  npm run test:unit:effect
203
+ npm run test:unit:derive
116
204
  npm run test:unit:batch
205
+ npm run test:unit:select
206
+ npm run test:unit:lens
207
+ npm run test:unit:readonly
208
+ npm run test:unit:protected
117
209
 
118
210
  # Advanced patterns
119
- npm run test:unit:cleanup # Tests for effect cleanup behavior
120
- npm run test:unit:cyclic # Tests for cyclic dependency handling
211
+ npm run test:unit:cleanup # Tests for effect cleanup behavior
212
+ npm run test:unit:cyclic-dependency # Tests for cyclic dependency handling
213
+ npm run test:unit:deep-chain # Tests for deep chain handling
214
+ npm run test:unit:infinite-loop # Tests for infinite loop detection
215
+
216
+ # Benchmarking
217
+ npm run benchmark # Tests for infinite loop detection
121
218
 
122
219
  # Format code
123
220
  npm run format
124
-
125
- # Build for Node.js LTS compatibility (v20+)
126
- npm run build:lts
221
+ npm run lint
222
+ npm run check # Runs Bioms lint + format
127
223
  ```
128
224
 
129
225
  ### Node.js LTS Compatibility
@@ -134,7 +230,7 @@ Beacon supports the two most recent Node.js LTS versions (currently v20 and v22)
134
230
 
135
231
  | Aspect | @nerdalytics/beacon | TC39 Proposal |
136
232
  |--------|---------------------|---------------|
137
- | **API Style** | Functional approach (`state()`, `derived()`) | Class-based design (`Signal.State`, `Signal.Computed`) |
233
+ | **API Style** | Functional approach (`state()`, `derive()`) | Class-based design (`Signal.State`, `Signal.Computed`) |
138
234
  | **Reading/Writing Pattern** | Function call for reading (`count()`), methods for writing (`count.set(5)`) | Method-based access (`get()`/`set()`) |
139
235
  | **Framework Support** | High-level abstractions like `effect()` and `batch()` | Lower-level primitives (`Signal.subtle.Watcher`) that frameworks build upon |
140
236
  | **Advanced Features** | Focused on core reactivity | Includes introspection capabilities, watched/unwatched callbacks, and Signal.subtle namespace |
@@ -146,42 +242,51 @@ Beacon is designed with a focus on simplicity, performance, and robust handling
146
242
 
147
243
  ### Key Implementation Concepts
148
244
 
149
- - **Fine-grained reactivity**: Dependencies are tracked automatically at the signal level
245
+ - **Fine-grained reactivity**: Dependencies are tracked automatically at the state level
150
246
  - **Efficient updates**: Changes only propagate to affected parts of the dependency graph
151
247
  - **Cyclical dependency handling**: Robust handling of circular references without crashing
248
+ - **Infinite loop detection**: Safeguards against direct self-mutation within effects
152
249
  - **Memory management**: Automatic cleanup of subscriptions when effects are disposed
153
-
154
- For an in-depth explanation of Beacon's internal architecture, advanced features, and best practices for handling complex scenarios like cyclical dependencies, see the [TECHNICAL_DETAILS.md][2] document.
250
+ - **Optimized batching**: Smart scheduling of updates to minimize redundant computations
155
251
 
156
252
  ## FAQ
157
253
 
158
254
  <details>
159
255
 
160
256
  <summary>Why "Beacon" Instead of "Signal"?</summary>
161
- I chose "Beacon" because it clearly represents how the library broadcasts notifications when state changesโ€”just like a lighthouse guides ships. While my library draws inspiration from Preact Signals, Angular Signals, and aspects of Svelte, I wanted to create something lighter and specifically designed for Node.js backends. Using "Beacon" instead of "Signal" helps avoid confusion with the TC39 proposal and similar libraries while still accurately describing the core functionality.
257
+ I chose "Beacon" because it clearly represents how the library broadcasts notifications when state changesโ€”just like a lighthouse guides ships. While my library draws inspiration from Preact Signals, Angular Signals, and aspects of Svelte, I wanted to create something lighter and specifically designed for Node.js backends. Using "Beacon" instead of the term "Signal" helps avoid confusion with the TC39 proposal and similar libraries while still accurately describing the core functionality.
162
258
 
163
259
  </details>
164
260
 
165
261
  <details>
166
262
 
167
263
  <summary>How does Beacon handle infinite update cycles?</summary>
168
- Beacon uses a queue-based update system that won't crash even with cyclical dependencies. If signals form a cycle where values constantly change (A updates B updates A...), the system will continue processing these updates without stack overflows. However, this could potentially affect performance if updates never stabilize. See the [TECHNICAL_DETAILS.md][4] document for best practices on handling cyclical dependencies.
264
+ Beacon employs two complementary strategies for handling cyclical updates:
265
+
266
+ 1. **Infinite Loop Detection**: Beacon actively detects direct infinite loops in effects by tracking which states an effect reads and writes to. If an effect attempts to update a state it depends on (directly modifying its own dependency), Beacon throws an error with a clear message: "Infinite loop detected: effect() cannot update a state() it depends on!"
267
+
268
+ 2. **Safe Cyclic Dependencies**: For indirect cycles and safe update patterns, Beacon uses a queue-based update system that won't crash even with cyclical dependencies. When states form a cycle where values eventually stabilize, the system handles these updates efficiently without stack overflows.
269
+
270
+ This dual approach prevents accidental infinite loops while still supporting legitimate cyclic update patterns that eventually stabilize.
169
271
 
170
272
  </details>
171
273
 
172
274
  <details>
173
275
 
174
276
  <summary>How performant is Beacon?</summary>
175
- Beacon is designed with performance in mind for server-side Node.js environments. It achieves millions of operations per second for core operations like reading and writing signals.
277
+ Beacon is designed with performance in mind for server-side Node.js environments. It achieves millions of operations per second for core operations like reading and writing states.
176
278
 
177
279
  </details>
178
280
 
179
281
  ## License
180
282
 
181
- This project is licensed under the MIT License. See the [LICENSE][3] file for details.
283
+ This project is licensed under the MIT License. See the [LICENSE][2] file for details.
284
+
285
+ <div align="center">
286
+ <img src="https://raw.githubusercontent.com/nerdalytics/nerdalytics/refs/heads/main/nerdalytics-logo-gray-transparent.svg" width="128px">
287
+ </div>
182
288
 
183
289
  <!-- Links collection -->
184
290
 
185
291
  [1]: https://github.com/tc39/proposal-signals
186
- [2]: ./TECHNICAL_DETAILS.md
187
- [3]: ./LICENSE
292
+ [2]: ./LICENSE
package/package.json CHANGED
@@ -1,58 +1,76 @@
1
1
  {
2
2
  "name": "@nerdalytics/beacon",
3
- "version": "1.0.0",
4
- "description": "A lightweight reactive signal library for Node.js backends. Enables reactive state management with automatic dependency tracking and efficient updates for server-side applications.",
3
+ "version": "1000.1.0",
4
+ "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
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
8
  "files": [
9
- "dist/",
10
- "src/",
9
+ "dist/index.js",
10
+ "dist/index.d.ts",
11
+ "src/index.ts",
11
12
  "LICENSE"
12
13
  ],
14
+ "repository": {
15
+ "url": "git+https://github.com/nerdalytics/beacon.git",
16
+ "type": "git"
17
+ },
13
18
  "scripts": {
14
- "test": "node --test --experimental-config-file=node.config.json \"test/**/*.test.ts\"",
15
- "test:coverage": "node --test --experimental-config-file=node.config.json --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=lcov.info \"test/**/*.test.ts\"",
16
- "test:unit": "node --test --experimental-config-file=node.config.json --test-name-pattern=\"/^(State|Derived|Effect|Batch|Cleanup|Cyclic Dependencies)$/\"",
17
- "test:unit:state": "node --test --experimental-config-file=node.config.json --test-name-pattern=\"/^State$/\"",
18
- "test:unit:derived": "node --test --experimental-config-file=node.config.json --test-name-pattern=\"/^Derived$/\"",
19
- "test:unit:effect": "node --test --experimental-config-file=node.config.json --test-name-pattern=\"/^Effect$/\"",
20
- "test:unit:batch": "node --test --experimental-config-file=node.config.json --test-name-pattern=\"/^Batch$/\"",
21
- "test:unit:cleanup": "node --test --experimental-config-file=node.config.json --test-name-pattern=\"/^Cleanup$/\"",
22
- "test:unit:cyclic": "node --test --experimental-config-file=node.config.json --test-name-pattern=\"/^Cyclic Dependencies$/\"",
23
- "test:integration": "node --test --experimental-config-file=node.config.json --test-name-pattern=\"/^Deep Dependency Chains$/\"",
24
- "test:perf": "node --test --experimental-config-file=node.config.json --test-name-pattern=\"/^Performance$/\"",
25
- "test:perf:update-docs": "node scripts/update-performance-docs.ts",
26
- "format": "biome format --write .",
27
- "prebuild": "rm -rf dist/",
19
+ "lint": "npx @biomejs/biome lint --config-path=./biome.json",
20
+ "lint:fix": "npx @biomejs/biome lint --fix --config-path=./biome.json",
21
+ "lint:fix:unsafe": "npx @biomejs/biome lint --fix --unsafe --config-path=./biome.json",
22
+ "format": "npx @biomejs/biome format --write --config-path=./biome.json",
23
+ "check": "npx @biomejs/biome check --config-path=./biome.json",
24
+ "check:fix": "npx @biomejs/biome format --fix --config-path=./biome.json",
25
+ "test": "node --test --test-skip-pattern=\"COMPONENT NAME\" tests/**/*.ts",
26
+ "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",
27
+ "test:unit:state": "node --test tests/state.test.ts",
28
+ "test:unit:effect": "node --test tests/effect.test.ts",
29
+ "test:unit:batch": "node --test tests/batch.test.ts",
30
+ "test:unit:derive": "node --test tests/derive.test.ts",
31
+ "test:unit:select": "node --test tests/select.test.ts",
32
+ "test:unit:lens": "node --test tests/lens.test.ts",
33
+ "test:unit:cleanup": "node --test tests/cleanup.test.ts",
34
+ "test:unit:cyclic-dependency": "node --test tests/cyclic-dependency.test.ts",
35
+ "test:unit:deep-chain": "node --test tests/deep-chain.test.ts",
36
+ "test:unit:infinite-loop": "node --test tests/infinite-loop.test.ts",
37
+ "benchmark": "node scripts/benchmark.ts",
28
38
  "build": "npm run build:lts",
29
- "build:lts": "tsc -p tsconfig.build.json",
30
- "prepublishOnly": "npm run build"
39
+ "prebuild:lts": "rm -rf dist/",
40
+ "build:lts": "tsc -p tsconfig.lts.json",
41
+ "prepublishOnly": "npm run build:lts",
42
+ "pretest:lts": "node scripts/run-lts-tests.js",
43
+ "test:lts:20": "node --test dist/tests/**.js",
44
+ "test:lts:22": "node --test --test-skip-pattern=\"COMPONENT NAME\" dist/tests/**/*.js",
45
+ "update-performance-docs": "node --experimental-config-file=node.config.json scripts/update-performance-docs.ts"
31
46
  },
32
47
  "keywords": [
33
- "reactive",
34
- "signals",
35
48
  "state-management",
36
- "nodejs",
37
- "typescript",
38
- "reactive-programming",
49
+ "effects",
50
+ "fine-grained",
51
+ "computed-values",
52
+ "batching",
53
+ "signals",
54
+ "reactive",
55
+ "lightweight",
56
+ "performance",
39
57
  "dependency-tracking",
40
- "esm",
41
- "observable",
42
- "backend",
58
+ "memoization",
59
+ "memory-management",
60
+ "nodejs",
43
61
  "server-side",
44
- "computed-values",
45
- "effects",
46
- "batching"
62
+ "backend",
63
+ "typescript"
47
64
  ],
48
- "author": "Denny Trebbin",
65
+ "author": "Denny Trebbin (nerdalytics)",
49
66
  "license": "MIT",
50
67
  "devDependencies": {
51
68
  "@biomejs/biome": "1.9.4",
52
- "@types/node": "22.13.16",
53
- "typescript": "5.8.2"
69
+ "@types/node": "22.14.0",
70
+ "typescript": "5.8.3"
54
71
  },
55
72
  "engines": {
56
73
  "node": ">=20.0.0"
57
- }
74
+ },
75
+ "packageManager": "npm@11.2.0"
58
76
  }
package/src/index.ts CHANGED
@@ -1,175 +1,633 @@
1
- // Types
1
+ // Core types for reactive primitives
2
2
  type Subscriber = () => void
3
-
4
3
  type Unsubscribe = () => void
5
-
6
- export interface Signal<T> {
7
- (): T // get value
8
- set(value: T): void // set value directly
9
- update(fn: (currentValue: T) => T): void // update value with a function
4
+ export type ReadOnlyState<T> = () => T
5
+ export interface WriteableState<T> {
6
+ set(value: T): void
7
+ update(fn: (value: T) => T): void
10
8
  }
11
9
 
12
- // Global state for tracking
13
- let currentEffect: Subscriber | null = null
10
+ // Special symbol used for internal tracking
11
+ const STATE_ID = Symbol()
14
12
 
15
- let batchDepth = 0
13
+ export type State<T> = ReadOnlyState<T> &
14
+ WriteableState<T> & {
15
+ [STATE_ID]?: symbol
16
+ }
16
17
 
17
- const pendingEffects = new Set<Subscriber>()
18
+ /**
19
+ * Creates a reactive state container with the provided initial value.
20
+ */
21
+ export const state = <T>(initialValue: T): State<T> => StateImpl.createState(initialValue)
18
22
 
19
- const subscriberDependencies = new WeakMap<Subscriber, Set<Set<Subscriber>>>()
23
+ /**
24
+ * Registers a function to run whenever its reactive dependencies change.
25
+ */
26
+ export const effect = (fn: () => void): Unsubscribe => StateImpl.createEffect(fn)
20
27
 
21
- // Use a flag to prevent multiple updates from running the effects
22
- let updateInProgress = false
28
+ /**
29
+ * Groups multiple state updates to trigger effects only once at the end.
30
+ */
31
+ export const batch = <T>(fn: () => T): T => StateImpl.executeBatch(fn)
23
32
 
24
33
  /**
25
- * Creates a new reactive state with the provided initial value
34
+ * Creates a read-only computed value that updates when its dependencies change.
26
35
  */
27
- export const state = <T>(initialValue: T): Signal<T> => {
28
- let value = initialValue
36
+ export const derive = <T>(computeFn: () => T): ReadOnlyState<T> => StateImpl.createDerive(computeFn)
29
37
 
30
- const subscribers = new Set<Subscriber>()
38
+ /**
39
+ * Creates an efficient subscription to a subset of a state value.
40
+ */
41
+ export const select = <T, R>(
42
+ source: ReadOnlyState<T>,
43
+ selectorFn: (state: T) => R,
44
+ equalityFn: (a: R, b: R) => boolean = Object.is
45
+ ): ReadOnlyState<R> => StateImpl.createSelect(source, selectorFn, equalityFn)
31
46
 
32
- const read = (): T => {
33
- if (currentEffect) {
34
- subscribers.add(currentEffect)
47
+ /**
48
+ * Creates a read-only view of a state, hiding mutation methods.
49
+ */
50
+ export const readonlyState =
51
+ <T>(state: State<T>): ReadOnlyState<T> =>
52
+ (): T =>
53
+ state()
54
+
55
+ /**
56
+ * Creates a state with access control, returning a tuple of reader and writer.
57
+ */
58
+ export const protectedState = <T>(initialValue: T): [ReadOnlyState<T>, WriteableState<T>] => {
59
+ const fullState = state(initialValue)
60
+ return [
61
+ (): T => readonlyState(fullState)(),
62
+ {
63
+ set: (value: T): void => fullState.set(value),
64
+ update: (fn: (value: T) => T): void => fullState.update(fn),
65
+ },
66
+ ]
67
+ }
68
+
69
+ /**
70
+ * Creates a lens for direct updates to nested properties of a state.
71
+ */
72
+ export const lens = <T, K>(source: State<T>, accessor: (state: T) => K): State<K> =>
73
+ StateImpl.createLens(source, accessor)
74
+
75
+ class StateImpl<T> {
76
+ // Static fields track global reactivity state - this centralized approach allows
77
+ // for coordinated updates while maintaining individual state isolation
78
+ private static currentSubscriber: Subscriber | null = null
79
+ private static pendingSubscribers = new Set<Subscriber>()
80
+ private static isNotifying = false
81
+ private static batchDepth = 0
82
+ private static deferredEffectCreations: Subscriber[] = []
83
+ private static activeSubscribers = new Set<Subscriber>()
84
+
85
+ // WeakMaps enable automatic garbage collection when subscribers are no
86
+ // longer referenced, preventing memory leaks in long-running applications
87
+ private static stateTracking = new WeakMap<Subscriber, Set<symbol>>()
88
+ private static subscriberDependencies = new WeakMap<Subscriber, Set<Set<Subscriber>>>()
89
+ private static parentSubscriber = new WeakMap<Subscriber, Subscriber>()
90
+ private static childSubscribers = new WeakMap<Subscriber, Set<Subscriber>>()
91
+
92
+ // Instance state - each state has unique subscribers and ID
93
+ private value: T
94
+ private subscribers = new Set<Subscriber>()
95
+ private stateId = Symbol()
96
+
97
+ constructor(initialValue: T) {
98
+ this.value = initialValue
99
+ }
35
100
 
36
- let dependencies = subscriberDependencies.get(currentEffect)
101
+ /**
102
+ * Creates a reactive state container with the provided initial value.
103
+ * Implementation of the public 'state' function.
104
+ */
105
+ static createState = <T>(initialValue: T): State<T> => {
106
+ const instance = new StateImpl<T>(initialValue)
107
+ const get = (): T => instance.get()
108
+ get.set = (value: T): void => instance.set(value)
109
+ get.update = (fn: (currentValue: T) => T): void => instance.update(fn)
110
+ get[STATE_ID] = instance.stateId
111
+ return get as State<T>
112
+ }
37
113
 
114
+ // Auto-tracks dependencies when called within effects, creating a fine-grained
115
+ // reactivity graph that only updates affected components
116
+ get = (): T => {
117
+ const currentEffect = StateImpl.currentSubscriber
118
+ if (currentEffect) {
119
+ // Add this effect to subscribers for future notification
120
+ this.subscribers.add(currentEffect)
121
+
122
+ // Maintain bidirectional dependency tracking to enable precise cleanup
123
+ // when effects are unsubscribed, preventing memory leaks
124
+ let dependencies = StateImpl.subscriberDependencies.get(currentEffect)
38
125
  if (!dependencies) {
39
126
  dependencies = new Set()
127
+ StateImpl.subscriberDependencies.set(currentEffect, dependencies)
128
+ }
129
+ dependencies.add(this.subscribers)
130
+
131
+ // Track read states to detect direct cyclical dependencies that
132
+ // could cause infinite loops
133
+ let readStates = StateImpl.stateTracking.get(currentEffect)
134
+ if (!readStates) {
135
+ readStates = new Set()
136
+ StateImpl.stateTracking.set(currentEffect, readStates)
137
+ }
138
+ readStates.add(this.stateId)
139
+ }
140
+ return this.value
141
+ }
142
+
143
+ // Handles value updates with built-in optimizations and safeguards
144
+ set = (newValue: T): void => {
145
+ // Skip updates for unchanged values to prevent redundant effect executions
146
+ if (Object.is(this.value, newValue)) {
147
+ return
148
+ }
40
149
 
41
- subscriberDependencies.set(currentEffect, dependencies)
150
+ // Infinite loop detection prevents direct self-mutation within effects,
151
+ // while allowing nested effect patterns that would otherwise appear cyclical
152
+ const effect = StateImpl.currentSubscriber
153
+ if (effect) {
154
+ const states = StateImpl.stateTracking.get(effect)
155
+ if (states?.has(this.stateId) && !StateImpl.parentSubscriber.get(effect)) {
156
+ throw new Error('Infinite loop detected: effect() cannot update a state() it depends on!')
42
157
  }
158
+ }
159
+
160
+ this.value = newValue
43
161
 
44
- dependencies.add(subscribers)
162
+ // Skip updates when there are no subscribers, avoiding unnecessary processing
163
+ if (this.subscribers.size === 0) {
164
+ return
45
165
  }
46
166
 
47
- return value
48
- }
167
+ // Queue notifications instead of executing immediately to support batch operations
168
+ // and prevent redundant effect runs
169
+ for (const sub of this.subscribers) {
170
+ StateImpl.pendingSubscribers.add(sub)
171
+ }
49
172
 
50
- const write = (newValue: T): void => {
51
- if (Object.is(value, newValue)) {
52
- return // No change
173
+ // Immediate execution outside of batches, deferred execution inside batches
174
+ if (StateImpl.batchDepth === 0 && !StateImpl.isNotifying) {
175
+ StateImpl.notifySubscribers()
53
176
  }
54
- value = newValue
177
+ }
55
178
 
56
- if (subscribers.size === 0) {
57
- return
179
+ update = (fn: (currentValue: T) => T): void => {
180
+ this.set(fn(this.value))
181
+ }
182
+
183
+ /**
184
+ * Registers a function to run whenever its reactive dependencies change.
185
+ * Implementation of the public 'effect' function.
186
+ */
187
+ static createEffect = (fn: () => void): Unsubscribe => {
188
+ const runEffect = (): void => {
189
+ // Prevent re-entrance to avoid cascade updates during effect execution
190
+ if (StateImpl.activeSubscribers.has(runEffect)) {
191
+ return
192
+ }
193
+
194
+ StateImpl.activeSubscribers.add(runEffect)
195
+ const parentEffect = StateImpl.currentSubscriber
196
+
197
+ try {
198
+ // Clean existing subscriptions before running to ensure only
199
+ // currently accessed states are tracked as dependencies
200
+ StateImpl.cleanupEffect(runEffect)
201
+
202
+ // Set current context for automatic dependency tracking
203
+ StateImpl.currentSubscriber = runEffect
204
+ StateImpl.stateTracking.set(runEffect, new Set())
205
+
206
+ // Track parent-child relationships to handle nested effects correctly
207
+ // and enable hierarchical cleanup later
208
+ if (parentEffect) {
209
+ StateImpl.parentSubscriber.set(runEffect, parentEffect)
210
+ let children = StateImpl.childSubscribers.get(parentEffect)
211
+ if (!children) {
212
+ children = new Set()
213
+ StateImpl.childSubscribers.set(parentEffect, children)
214
+ }
215
+ children.add(runEffect)
216
+ }
217
+
218
+ // Execute the effect function, which will auto-track dependencies
219
+ fn()
220
+ } finally {
221
+ // Restore previous context when done
222
+ StateImpl.currentSubscriber = parentEffect
223
+ StateImpl.activeSubscribers.delete(runEffect)
224
+ }
58
225
  }
59
226
 
60
- // Add subscribers to pendingEffects - always use loop for better performance
61
- for (const sub of subscribers) {
62
- pendingEffects.add(sub)
227
+ // Run immediately unless we're in a batch operation
228
+ if (StateImpl.batchDepth === 0) {
229
+ runEffect()
230
+ } else {
231
+ // Still track parent-child relationship even when deferred,
232
+ // ensuring proper hierarchical cleanup later
233
+ if (StateImpl.currentSubscriber) {
234
+ const parent = StateImpl.currentSubscriber
235
+ StateImpl.parentSubscriber.set(runEffect, parent)
236
+ let children = StateImpl.childSubscribers.get(parent)
237
+ if (!children) {
238
+ children = new Set()
239
+ StateImpl.childSubscribers.set(parent, children)
240
+ }
241
+ children.add(runEffect)
242
+ }
243
+
244
+ // Queue for execution when batch completes
245
+ StateImpl.deferredEffectCreations.push(runEffect)
63
246
  }
64
247
 
65
- if (batchDepth === 0 && !updateInProgress) {
66
- processEffects()
248
+ // Return cleanup function to properly disconnect from reactivity graph
249
+ return (): void => {
250
+ // Remove from dependency tracking to stop future notifications
251
+ StateImpl.cleanupEffect(runEffect)
252
+ StateImpl.pendingSubscribers.delete(runEffect)
253
+ StateImpl.activeSubscribers.delete(runEffect)
254
+ StateImpl.stateTracking.delete(runEffect)
255
+
256
+ // Clean up parent-child relationship bidirectionally
257
+ const parent = StateImpl.parentSubscriber.get(runEffect)
258
+ if (parent) {
259
+ const siblings = StateImpl.childSubscribers.get(parent)
260
+ if (siblings) {
261
+ siblings.delete(runEffect)
262
+ }
263
+ }
264
+ StateImpl.parentSubscriber.delete(runEffect)
265
+
266
+ // Recursively clean up child effects to prevent memory leaks in
267
+ // nested effect scenarios
268
+ const children = StateImpl.childSubscribers.get(runEffect)
269
+ if (children) {
270
+ for (const child of children) {
271
+ StateImpl.cleanupEffect(child)
272
+ }
273
+ children.clear()
274
+ StateImpl.childSubscribers.delete(runEffect)
275
+ }
67
276
  }
68
277
  }
69
278
 
70
- const update = (fn: (currentValue: T) => T): void => {
71
- write(fn(value))
279
+ /**
280
+ * Groups multiple state updates to trigger effects only once at the end.
281
+ * Implementation of the public 'batch' function.
282
+ */
283
+ static executeBatch = <T>(fn: () => T): T => {
284
+ // Increment depth counter to handle nested batches correctly
285
+ StateImpl.batchDepth++
286
+ try {
287
+ return fn()
288
+ } catch (error: unknown) {
289
+ // Clean up on error to prevent stale subscribers from executing
290
+ // and potentially causing cascading errors
291
+ if (StateImpl.batchDepth === 1) {
292
+ StateImpl.pendingSubscribers.clear()
293
+ StateImpl.deferredEffectCreations.length = 0
294
+ }
295
+ throw error
296
+ } finally {
297
+ StateImpl.batchDepth--
298
+
299
+ // Only process effects when exiting the outermost batch,
300
+ // maintaining proper execution order while avoiding redundant runs
301
+ if (StateImpl.batchDepth === 0) {
302
+ // Process effects created during the batch
303
+ if (StateImpl.deferredEffectCreations.length > 0) {
304
+ const effectsToRun = [...StateImpl.deferredEffectCreations]
305
+ StateImpl.deferredEffectCreations.length = 0
306
+ for (const effect of effectsToRun) {
307
+ effect()
308
+ }
309
+ }
310
+
311
+ // Process state updates that occurred during the batch
312
+ if (StateImpl.pendingSubscribers.size > 0 && !StateImpl.isNotifying) {
313
+ StateImpl.notifySubscribers()
314
+ }
315
+ }
316
+ }
72
317
  }
73
318
 
74
- return Object.assign(read, { set: write, update })
75
- }
319
+ /**
320
+ * Creates a read-only computed value that updates when its dependencies change.
321
+ * Implementation of the public 'derive' function.
322
+ */
323
+ static createDerive = <T>(computeFn: () => T): ReadOnlyState<T> => {
324
+ const valueState = StateImpl.createState<T | undefined>(undefined)
325
+ let initialized = false
326
+ let cachedValue: T
327
+
328
+ // Internal effect automatically tracks dependencies and updates the derived value
329
+ StateImpl.createEffect((): void => {
330
+ const newValue = computeFn()
331
+
332
+ // Only update if the value actually changed to preserve referential equality
333
+ // and prevent unnecessary downstream updates
334
+ if (!(initialized && Object.is(cachedValue, newValue))) {
335
+ cachedValue = newValue
336
+ valueState.set(newValue)
337
+ }
76
338
 
77
- /**
78
- * Process all pending effects, ensuring full propagation through the dependency chain
79
- */
80
- const processEffects = (): void => {
81
- if (pendingEffects.size === 0 || updateInProgress) {
82
- return
339
+ initialized = true
340
+ })
341
+
342
+ // Return function with lazy initialization - ensures value is available
343
+ // even when accessed before its dependencies have had a chance to update
344
+ return (): T => {
345
+ if (!initialized) {
346
+ cachedValue = computeFn()
347
+ initialized = true
348
+ valueState.set(cachedValue)
349
+ }
350
+ return valueState() as T
351
+ }
83
352
  }
84
353
 
85
- updateInProgress = true
354
+ /**
355
+ * Creates an efficient subscription to a subset of a state value.
356
+ * Implementation of the public 'select' function.
357
+ */
358
+ static createSelect = <T, R>(
359
+ source: ReadOnlyState<T>,
360
+ selectorFn: (state: T) => R,
361
+ equalityFn: (a: R, b: R) => boolean = Object.is
362
+ ): ReadOnlyState<R> => {
363
+ let lastSourceValue: T | undefined
364
+ let lastSelectedValue: R | undefined
365
+ let initialized = false
366
+ const valueState = StateImpl.createState<R | undefined>(undefined)
367
+
368
+ // Internal effect to track the source and update only when needed
369
+ StateImpl.createEffect((): void => {
370
+ const sourceValue = source()
371
+
372
+ // Skip computation if source reference hasn't changed
373
+ if (initialized && Object.is(lastSourceValue, sourceValue)) {
374
+ return
375
+ }
376
+
377
+ lastSourceValue = sourceValue
378
+ const newSelectedValue = selectorFn(sourceValue)
86
379
 
87
- while (pendingEffects.size > 0) {
88
- const currentEffects = [...pendingEffects]
89
- pendingEffects.clear()
380
+ // Use custom equality function to determine if value semantically changed,
381
+ // allowing for deep equality comparisons with complex objects
382
+ if (initialized && lastSelectedValue !== undefined && equalityFn(lastSelectedValue, newSelectedValue)) {
383
+ return
384
+ }
90
385
 
91
- for (const effect of currentEffects) {
92
- effect()
386
+ // Update cache and notify subscribers due the value has changed
387
+ lastSelectedValue = newSelectedValue
388
+ valueState.set(newSelectedValue)
389
+ initialized = true
390
+ })
391
+
392
+ // Return function with eager initialization capability
393
+ return (): R => {
394
+ if (!initialized) {
395
+ lastSourceValue = source()
396
+ lastSelectedValue = selectorFn(lastSourceValue)
397
+ valueState.set(lastSelectedValue)
398
+ initialized = true
399
+ }
400
+ return valueState() as R
93
401
  }
94
402
  }
95
403
 
96
- updateInProgress = false
97
- }
404
+ /**
405
+ * Creates a lens for direct updates to nested properties of a state.
406
+ * Implementation of the public 'lens' function.
407
+ */
408
+ static createLens = <T, K>(source: State<T>, accessor: (state: T) => K): State<K> => {
409
+ // Extract the property path once during lens creation
410
+ const extractPath = (): (string | number)[] => {
411
+ const path: (string | number)[] = []
412
+ const proxy = new Proxy(
413
+ {},
414
+ {
415
+ get: (_: object, prop: string | symbol): unknown => {
416
+ if (typeof prop === 'string' || typeof prop === 'number') {
417
+ path.push(prop)
418
+ }
419
+ return proxy
420
+ },
421
+ }
422
+ )
423
+
424
+ try {
425
+ accessor(proxy as unknown as T)
426
+ } catch {
427
+ // Ignore errors, we're just collecting the path
428
+ }
98
429
 
99
- /**
100
- * Helper to clean up effect subscriptions
101
- */
102
- const cleanupEffect = (effect: Subscriber): void => {
103
- const deps = subscriberDependencies.get(effect)
430
+ return path
431
+ }
432
+
433
+ // Capture the path once
434
+ const path = extractPath()
435
+
436
+ // Create a state with the initial value from the source
437
+ const lensState = StateImpl.createState<K>(accessor(source()))
438
+
439
+ // Prevent circular updates
440
+ let isUpdating = false
441
+
442
+ // Set up an effect to sync from source to lens
443
+ StateImpl.createEffect((): void => {
444
+ if (isUpdating) {
445
+ return
446
+ }
447
+
448
+ isUpdating = true
449
+ try {
450
+ lensState.set(accessor(source()))
451
+ } finally {
452
+ isUpdating = false
453
+ }
454
+ })
455
+
456
+ // Override the lens state's set method to update the source
457
+ const originalSet = lensState.set
458
+ lensState.set = (value: K): void => {
459
+ if (isUpdating) {
460
+ return
461
+ }
462
+
463
+ isUpdating = true
464
+ try {
465
+ // Update lens state
466
+ originalSet(value)
104
467
 
105
- if (deps) {
106
- for (const subscribers of deps) {
107
- subscribers.delete(effect)
468
+ // Update source by modifying the value at path
469
+ source.update((current: T): T => setValueAtPath(current, path, value))
470
+ } finally {
471
+ isUpdating = false
472
+ }
108
473
  }
109
474
 
110
- deps.clear()
111
- }
112
- }
475
+ // Add update method for completeness
476
+ lensState.update = (fn: (value: K) => K): void => {
477
+ lensState.set(fn(lensState()))
478
+ }
113
479
 
114
- /**
115
- * Creates an effect that runs when its dependencies change
116
- */
117
- export const effect = (fn: () => void): Unsubscribe => {
118
- const runEffect = (): void => {
119
- cleanupEffect(runEffect)
480
+ return lensState
481
+ }
120
482
 
121
- const prevEffect = currentEffect
483
+ // Processes queued subscriber notifications in a controlled, non-reentrant way
484
+ private static notifySubscribers = (): void => {
485
+ // Prevent reentrance to avoid cascading notification loops when
486
+ // effects trigger further state changes
487
+ if (StateImpl.isNotifying) {
488
+ return
489
+ }
122
490
 
123
- currentEffect = runEffect
491
+ StateImpl.isNotifying = true
124
492
 
125
493
  try {
126
- fn()
494
+ // Process all pending effects in batches for better perf,
495
+ // ensuring topological execution order is maintained
496
+ while (StateImpl.pendingSubscribers.size > 0) {
497
+ // Process in snapshot batches to prevent infinite loops
498
+ // when effects trigger further state changes
499
+ const subscribers = Array.from(StateImpl.pendingSubscribers)
500
+ StateImpl.pendingSubscribers.clear()
501
+
502
+ for (const effect of subscribers) {
503
+ effect()
504
+ }
505
+ }
127
506
  } finally {
128
- currentEffect = prevEffect
507
+ StateImpl.isNotifying = false
129
508
  }
130
509
  }
131
510
 
132
- runEffect()
511
+ // Removes effect from dependency tracking to prevent memory leaks
512
+ private static cleanupEffect = (effect: Subscriber): void => {
513
+ // Remove from execution queue to prevent stale updates
514
+ StateImpl.pendingSubscribers.delete(effect)
133
515
 
134
- return (): void => {
135
- cleanupEffect(runEffect)
516
+ // Remove bidirectional dependency references to prevent memory leaks
517
+ const deps = StateImpl.subscriberDependencies.get(effect)
518
+ if (deps) {
519
+ for (const subscribers of deps) {
520
+ subscribers.delete(effect)
521
+ }
522
+ deps.clear()
523
+ StateImpl.subscriberDependencies.delete(effect)
524
+ }
136
525
  }
137
526
  }
527
+ // Helper for array updates
528
+ const updateArrayItem = <V>(arr: unknown[], index: number, value: V): unknown[] => {
529
+ const copy = [...arr]
530
+ copy[index] = value
531
+ return copy
532
+ }
138
533
 
139
- /**
140
- * Creates a derived signal that computes its value from other signals
141
- */
142
- export const derived = <T>(fn: () => T): Signal<T> => {
143
- // Initialize signal with the computed value
144
- const signal = state<T>(fn())
534
+ // Helper for single-level updates (optimization)
535
+ const updateShallowProperty = <V>(
536
+ obj: Record<string | number, unknown>,
537
+ key: string | number,
538
+ value: V
539
+ ): Record<string | number, unknown> => {
540
+ const result = { ...obj }
541
+ result[key] = value
542
+ return result
543
+ }
544
+
545
+ // Helper to create the appropriate container type
546
+ const createContainer = (key: string | number): Record<string | number, unknown> | unknown[] => {
547
+ const isArrayKey = typeof key === 'number' || !Number.isNaN(Number(key))
548
+ return isArrayKey ? [] : {}
549
+ }
145
550
 
146
- // Only run fn() again when dependencies change
147
- effect((): void => {
148
- signal.set(fn())
149
- })
551
+ // Helper for handling array path updates
552
+ const updateArrayPath = <V>(array: unknown[], pathSegments: (string | number)[], value: V): unknown[] => {
553
+ const index = Number(pathSegments[0])
150
554
 
151
- return signal
555
+ if (pathSegments.length === 1) {
556
+ // Simple array item update
557
+ return updateArrayItem(array, index, value)
558
+ }
559
+
560
+ // Nested path in array
561
+ const copy = [...array]
562
+ const nextPathSegments = pathSegments.slice(1)
563
+ const nextKey = nextPathSegments[0]
564
+
565
+ // For null/undefined values in arrays, create appropriate containers
566
+ let nextValue = array[index]
567
+ if (nextValue === undefined || nextValue === null) {
568
+ // Use empty object as default if nextKey is undefined
569
+ nextValue = nextKey !== undefined ? createContainer(nextKey) : {}
570
+ }
571
+
572
+ copy[index] = setValueAtPath(nextValue, nextPathSegments, value)
573
+ return copy
152
574
  }
153
575
 
154
- /**
155
- * Batches multiple updates to run effects only once at the end
156
- */
157
- export const batch = <T>(fn: () => T): T => {
158
- batchDepth++
576
+ // Helper for handling object path updates
577
+ const updateObjectPath = <V>(
578
+ obj: Record<string | number, unknown>,
579
+ pathSegments: (string | number)[],
580
+ value: V
581
+ ): Record<string | number, unknown> => {
582
+ // Ensure we have a valid key
583
+ const currentKey = pathSegments[0]
584
+ if (currentKey === undefined) {
585
+ // This shouldn't happen given our checks in the main function
586
+ return obj
587
+ }
159
588
 
160
- try {
161
- return fn()
162
- } catch (error) {
163
- if (batchDepth === 1) {
164
- pendingEffects.clear()
165
- }
589
+ if (pathSegments.length === 1) {
590
+ // Simple object property update
591
+ return updateShallowProperty(obj, currentKey, value)
592
+ }
166
593
 
167
- throw error
168
- } finally {
169
- batchDepth--
594
+ // Nested path in object
595
+ const nextPathSegments = pathSegments.slice(1)
596
+ const nextKey = nextPathSegments[0]
170
597
 
171
- if (batchDepth === 0 && pendingEffects.size > 0) {
172
- processEffects()
173
- }
598
+ // For null/undefined values, create appropriate containers
599
+ let currentValue = obj[currentKey]
600
+ if (currentValue === undefined || currentValue === null) {
601
+ // Use empty object as default if nextKey is undefined
602
+ currentValue = nextKey !== undefined ? createContainer(nextKey) : {}
603
+ }
604
+
605
+ // Create new object with updated property
606
+ const result = { ...obj }
607
+ result[currentKey] = setValueAtPath(currentValue, nextPathSegments, value)
608
+ return result
609
+ }
610
+
611
+ // Simplified function to update a nested value at a path
612
+ const setValueAtPath = <V, O>(obj: O, pathSegments: (string | number)[], value: V): O => {
613
+ // Handle base cases
614
+ if (pathSegments.length === 0) {
615
+ return value as unknown as O
174
616
  }
617
+
618
+ if (obj === undefined || obj === null) {
619
+ return setValueAtPath({} as O, pathSegments, value)
620
+ }
621
+
622
+ const currentKey = pathSegments[0]
623
+ if (currentKey === undefined) {
624
+ return obj
625
+ }
626
+
627
+ // Delegate to specialized handlers based on data type
628
+ if (Array.isArray(obj)) {
629
+ return updateArrayPath(obj, pathSegments, value) as unknown as O
630
+ }
631
+
632
+ return updateObjectPath(obj as Record<string | number, unknown>, pathSegments, value) as unknown as O
175
633
  }
package/dist/index.d.ts DELETED
@@ -1,23 +0,0 @@
1
- type Unsubscribe = () => void;
2
- export interface Signal<T> {
3
- (): T;
4
- set(value: T): void;
5
- update(fn: (currentValue: T) => T): void;
6
- }
7
- /**
8
- * Creates a new reactive state with the provided initial value
9
- */
10
- export declare const state: <T>(initialValue: T) => Signal<T>;
11
- /**
12
- * Creates an effect that runs when its dependencies change
13
- */
14
- export declare const effect: (fn: () => void) => Unsubscribe;
15
- /**
16
- * Creates a derived signal that computes its value from other signals
17
- */
18
- export declare const derived: <T>(fn: () => T) => Signal<T>;
19
- /**
20
- * Batches multiple updates to run effects only once at the end
21
- */
22
- export declare const batch: <T>(fn: () => T) => T;
23
- export {};
package/dist/index.js DELETED
@@ -1,129 +0,0 @@
1
- // Global state for tracking
2
- let currentEffect = null;
3
- let batchDepth = 0;
4
- const pendingEffects = new Set();
5
- const subscriberDependencies = new WeakMap();
6
- // Use a flag to prevent multiple updates from running the effects
7
- let updateInProgress = false;
8
- /**
9
- * Creates a new reactive state with the provided initial value
10
- */
11
- export const state = (initialValue) => {
12
- let value = initialValue;
13
- const subscribers = new Set();
14
- const read = () => {
15
- if (currentEffect) {
16
- subscribers.add(currentEffect);
17
- let dependencies = subscriberDependencies.get(currentEffect);
18
- if (!dependencies) {
19
- dependencies = new Set();
20
- subscriberDependencies.set(currentEffect, dependencies);
21
- }
22
- dependencies.add(subscribers);
23
- }
24
- return value;
25
- };
26
- const write = (newValue) => {
27
- if (Object.is(value, newValue)) {
28
- return; // No change
29
- }
30
- value = newValue;
31
- if (subscribers.size === 0) {
32
- return;
33
- }
34
- // Add subscribers to pendingEffects - always use loop for better performance
35
- for (const sub of subscribers) {
36
- pendingEffects.add(sub);
37
- }
38
- if (batchDepth === 0 && !updateInProgress) {
39
- processEffects();
40
- }
41
- };
42
- const update = (fn) => {
43
- write(fn(value));
44
- };
45
- return Object.assign(read, { set: write, update });
46
- };
47
- /**
48
- * Process all pending effects, ensuring full propagation through the dependency chain
49
- */
50
- const processEffects = () => {
51
- if (pendingEffects.size === 0 || updateInProgress) {
52
- return;
53
- }
54
- updateInProgress = true;
55
- while (pendingEffects.size > 0) {
56
- const currentEffects = [...pendingEffects];
57
- pendingEffects.clear();
58
- for (const effect of currentEffects) {
59
- effect();
60
- }
61
- }
62
- updateInProgress = false;
63
- };
64
- /**
65
- * Helper to clean up effect subscriptions
66
- */
67
- const cleanupEffect = (effect) => {
68
- const deps = subscriberDependencies.get(effect);
69
- if (deps) {
70
- for (const subscribers of deps) {
71
- subscribers.delete(effect);
72
- }
73
- deps.clear();
74
- }
75
- };
76
- /**
77
- * Creates an effect that runs when its dependencies change
78
- */
79
- export const effect = (fn) => {
80
- const runEffect = () => {
81
- cleanupEffect(runEffect);
82
- const prevEffect = currentEffect;
83
- currentEffect = runEffect;
84
- try {
85
- fn();
86
- }
87
- finally {
88
- currentEffect = prevEffect;
89
- }
90
- };
91
- runEffect();
92
- return () => {
93
- cleanupEffect(runEffect);
94
- };
95
- };
96
- /**
97
- * Creates a derived signal that computes its value from other signals
98
- */
99
- export const derived = (fn) => {
100
- // Initialize signal with the computed value
101
- const signal = state(fn());
102
- // Only run fn() again when dependencies change
103
- effect(() => {
104
- signal.set(fn());
105
- });
106
- return signal;
107
- };
108
- /**
109
- * Batches multiple updates to run effects only once at the end
110
- */
111
- export const batch = (fn) => {
112
- batchDepth++;
113
- try {
114
- return fn();
115
- }
116
- catch (error) {
117
- if (batchDepth === 1) {
118
- pendingEffects.clear();
119
- }
120
- throw error;
121
- }
122
- finally {
123
- batchDepth--;
124
- if (batchDepth === 0 && pendingEffects.size > 0) {
125
- processEffects();
126
- }
127
- }
128
- };
129
- //# sourceMappingURL=index.js.map
package/dist/index.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}