@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 +139 -34
- package/package.json +53 -35
- package/src/index.ts +565 -107
- package/dist/index.d.ts +0 -23
- package/dist/index.js +0 -129
- package/dist/index.js.map +0 -1
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
|
|
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-
|
|
12
|
-
- [
|
|
13
|
-
- [effect](#effectfn
|
|
14
|
-
- [batch](#
|
|
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
|
-
-
|
|
25
|
-
- ๐งฎ **Computed values** - Derive values from other
|
|
26
|
-
- ๐ **Fine-grained reactivity** - Dependencies are tracked precisely at the
|
|
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
|
-
-
|
|
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
|
-
```
|
|
43
|
+
```sh
|
|
38
44
|
npm install @nerdalytics/beacon
|
|
39
45
|
```
|
|
40
46
|
|
|
41
47
|
## Usage
|
|
42
48
|
|
|
43
49
|
```typescript
|
|
44
|
-
import { state,
|
|
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 =
|
|
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):
|
|
155
|
+
### `state<T>(initialValue: T): State<T>`
|
|
84
156
|
|
|
85
|
-
Creates a new reactive
|
|
157
|
+
Creates a new reactive state container with the provided initial value.
|
|
86
158
|
|
|
87
|
-
### `
|
|
159
|
+
### `derive<T>(fn: () => T): ReadOnlyState<T>`
|
|
88
160
|
|
|
89
|
-
Creates a
|
|
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
|
-
```
|
|
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
|
|
120
|
-
npm run test:unit:cyclic
|
|
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
|
-
|
|
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()`, `
|
|
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
|
|
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
|
|
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
|
|
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][
|
|
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]: ./
|
|
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
|
|
4
|
-
"description": "A lightweight reactive
|
|
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
|
-
"
|
|
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
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"test
|
|
21
|
-
"test:
|
|
22
|
-
"test:unit:
|
|
23
|
-
"test:
|
|
24
|
-
"test:
|
|
25
|
-
"test:
|
|
26
|
-
"
|
|
27
|
-
"
|
|
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
|
-
"
|
|
30
|
-
"
|
|
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
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
49
|
+
"effects",
|
|
50
|
+
"fine-grained",
|
|
51
|
+
"computed-values",
|
|
52
|
+
"batching",
|
|
53
|
+
"signals",
|
|
54
|
+
"reactive",
|
|
55
|
+
"lightweight",
|
|
56
|
+
"performance",
|
|
39
57
|
"dependency-tracking",
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
58
|
+
"memoization",
|
|
59
|
+
"memory-management",
|
|
60
|
+
"nodejs",
|
|
43
61
|
"server-side",
|
|
44
|
-
"
|
|
45
|
-
"
|
|
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.
|
|
53
|
-
"typescript": "5.8.
|
|
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
|
-
//
|
|
1
|
+
// Core types for reactive primitives
|
|
2
2
|
type Subscriber = () => void
|
|
3
|
-
|
|
4
3
|
type Unsubscribe = () => void
|
|
5
|
-
|
|
6
|
-
export interface
|
|
7
|
-
(
|
|
8
|
-
|
|
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
|
-
//
|
|
13
|
-
|
|
10
|
+
// Special symbol used for internal tracking
|
|
11
|
+
const STATE_ID = Symbol()
|
|
14
12
|
|
|
15
|
-
|
|
13
|
+
export type State<T> = ReadOnlyState<T> &
|
|
14
|
+
WriteableState<T> & {
|
|
15
|
+
[STATE_ID]?: symbol
|
|
16
|
+
}
|
|
16
17
|
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
|
34
|
+
* Creates a read-only computed value that updates when its dependencies change.
|
|
26
35
|
*/
|
|
27
|
-
export const
|
|
28
|
-
let value = initialValue
|
|
36
|
+
export const derive = <T>(computeFn: () => T): ReadOnlyState<T> => StateImpl.createDerive(computeFn)
|
|
29
37
|
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
162
|
+
// Skip updates when there are no subscribers, avoiding unnecessary processing
|
|
163
|
+
if (this.subscribers.size === 0) {
|
|
164
|
+
return
|
|
45
165
|
}
|
|
46
166
|
|
|
47
|
-
|
|
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
|
-
|
|
51
|
-
if (
|
|
52
|
-
|
|
173
|
+
// Immediate execution outside of batches, deferred execution inside batches
|
|
174
|
+
if (StateImpl.batchDepth === 0 && !StateImpl.isNotifying) {
|
|
175
|
+
StateImpl.notifySubscribers()
|
|
53
176
|
}
|
|
54
|
-
|
|
177
|
+
}
|
|
55
178
|
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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
|
-
|
|
116
|
-
*/
|
|
117
|
-
export const effect = (fn: () => void): Unsubscribe => {
|
|
118
|
-
const runEffect = (): void => {
|
|
119
|
-
cleanupEffect(runEffect)
|
|
480
|
+
return lensState
|
|
481
|
+
}
|
|
120
482
|
|
|
121
|
-
|
|
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
|
-
|
|
491
|
+
StateImpl.isNotifying = true
|
|
124
492
|
|
|
125
493
|
try {
|
|
126
|
-
|
|
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
|
-
|
|
507
|
+
StateImpl.isNotifying = false
|
|
129
508
|
}
|
|
130
509
|
}
|
|
131
510
|
|
|
132
|
-
|
|
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
|
-
|
|
135
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
pendingEffects.clear()
|
|
165
|
-
}
|
|
589
|
+
if (pathSegments.length === 1) {
|
|
590
|
+
// Simple object property update
|
|
591
|
+
return updateShallowProperty(obj, currentKey, value)
|
|
592
|
+
}
|
|
166
593
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
594
|
+
// Nested path in object
|
|
595
|
+
const nextPathSegments = pathSegments.slice(1)
|
|
596
|
+
const nextKey = nextPathSegments[0]
|
|
170
597
|
|
|
171
|
-
|
|
172
|
-
|
|
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":"AAWA,4BAA4B;AAC5B,IAAI,aAAa,GAAsB,IAAI,CAAA;AAE3C,IAAI,UAAU,GAAG,CAAC,CAAA;AAElB,MAAM,cAAc,GAAG,IAAI,GAAG,EAAc,CAAA;AAE5C,MAAM,sBAAsB,GAAG,IAAI,OAAO,EAAoC,CAAA;AAE9E,kEAAkE;AAClE,IAAI,gBAAgB,GAAG,KAAK,CAAA;AAE5B;;GAEG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,CAAI,YAAe,EAAa,EAAE;IACtD,IAAI,KAAK,GAAG,YAAY,CAAA;IAExB,MAAM,WAAW,GAAG,IAAI,GAAG,EAAc,CAAA;IAEzC,MAAM,IAAI,GAAG,GAAM,EAAE;QACpB,IAAI,aAAa,EAAE,CAAC;YACnB,WAAW,CAAC,GAAG,CAAC,aAAa,CAAC,CAAA;YAE9B,IAAI,YAAY,GAAG,sBAAsB,CAAC,GAAG,CAAC,aAAa,CAAC,CAAA;YAE5D,IAAI,CAAC,YAAY,EAAE,CAAC;gBACnB,YAAY,GAAG,IAAI,GAAG,EAAE,CAAA;gBAExB,sBAAsB,CAAC,GAAG,CAAC,aAAa,EAAE,YAAY,CAAC,CAAA;YACxD,CAAC;YAED,YAAY,CAAC,GAAG,CAAC,WAAW,CAAC,CAAA;QAC9B,CAAC;QAED,OAAO,KAAK,CAAA;IACb,CAAC,CAAA;IAED,MAAM,KAAK,GAAG,CAAC,QAAW,EAAQ,EAAE;QACnC,IAAI,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,CAAC,EAAE,CAAC;YAChC,OAAM,CAAC,YAAY;QACpB,CAAC;QACD,KAAK,GAAG,QAAQ,CAAA;QAEhB,IAAI,WAAW,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;YAC5B,OAAM;QACP,CAAC;QAED,6EAA6E;QAC7E,KAAK,MAAM,GAAG,IAAI,WAAW,EAAE,CAAC;YAC/B,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QACxB,CAAC;QAED,IAAI,UAAU,KAAK,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC;YAC3C,cAAc,EAAE,CAAA;QACjB,CAAC;IACF,CAAC,CAAA;IAED,MAAM,MAAM,GAAG,CAAC,EAA0B,EAAQ,EAAE;QACnD,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAA;IACjB,CAAC,CAAA;IAED,OAAO,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAA;AACnD,CAAC,CAAA;AAED;;GAEG;AACH,MAAM,cAAc,GAAG,GAAS,EAAE;IACjC,IAAI,cAAc,CAAC,IAAI,KAAK,CAAC,IAAI,gBAAgB,EAAE,CAAC;QACnD,OAAM;IACP,CAAC;IAED,gBAAgB,GAAG,IAAI,CAAA;IAEvB,OAAO,cAAc,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;QAChC,MAAM,cAAc,GAAG,CAAC,GAAG,cAAc,CAAC,CAAA;QAC1C,cAAc,CAAC,KAAK,EAAE,CAAA;QAEtB,KAAK,MAAM,MAAM,IAAI,cAAc,EAAE,CAAC;YACrC,MAAM,EAAE,CAAA;QACT,CAAC;IACF,CAAC;IAED,gBAAgB,GAAG,KAAK,CAAA;AACzB,CAAC,CAAA;AAED;;GAEG;AACH,MAAM,aAAa,GAAG,CAAC,MAAkB,EAAQ,EAAE;IAClD,MAAM,IAAI,GAAG,sBAAsB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;IAE/C,IAAI,IAAI,EAAE,CAAC;QACV,KAAK,MAAM,WAAW,IAAI,IAAI,EAAE,CAAC;YAChC,WAAW,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;QAC3B,CAAC;QAED,IAAI,CAAC,KAAK,EAAE,CAAA;IACb,CAAC;AACF,CAAC,CAAA;AAED;;GAEG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,EAAc,EAAe,EAAE;IACrD,MAAM,SAAS,GAAG,GAAS,EAAE;QAC5B,aAAa,CAAC,SAAS,CAAC,CAAA;QAExB,MAAM,UAAU,GAAG,aAAa,CAAA;QAEhC,aAAa,GAAG,SAAS,CAAA;QAEzB,IAAI,CAAC;YACJ,EAAE,EAAE,CAAA;QACL,CAAC;gBAAS,CAAC;YACV,aAAa,GAAG,UAAU,CAAA;QAC3B,CAAC;IACF,CAAC,CAAA;IAED,SAAS,EAAE,CAAA;IAEX,OAAO,GAAS,EAAE;QACjB,aAAa,CAAC,SAAS,CAAC,CAAA;IACzB,CAAC,CAAA;AACF,CAAC,CAAA;AAED;;GAEG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,CAAI,EAAW,EAAa,EAAE;IACpD,4CAA4C;IAC5C,MAAM,MAAM,GAAG,KAAK,CAAI,EAAE,EAAE,CAAC,CAAA;IAE7B,+CAA+C;IAC/C,MAAM,CAAC,GAAS,EAAE;QACjB,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAA;IACjB,CAAC,CAAC,CAAA;IAEF,OAAO,MAAM,CAAA;AACd,CAAC,CAAA;AAED;;GAEG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,CAAI,EAAW,EAAK,EAAE;IAC1C,UAAU,EAAE,CAAA;IAEZ,IAAI,CAAC;QACJ,OAAO,EAAE,EAAE,CAAA;IACZ,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,IAAI,UAAU,KAAK,CAAC,EAAE,CAAC;YACtB,cAAc,CAAC,KAAK,EAAE,CAAA;QACvB,CAAC;QAED,MAAM,KAAK,CAAA;IACZ,CAAC;YAAS,CAAC;QACV,UAAU,EAAE,CAAA;QAEZ,IAAI,UAAU,KAAK,CAAC,IAAI,cAAc,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;YACjD,cAAc,EAAE,CAAA;QACjB,CAAC;IACF,CAAC;AACF,CAAC,CAAA"}
|