@zeix/cause-effect 0.17.1 → 0.17.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.ai-context.md +13 -0
- package/.github/copilot-instructions.md +4 -0
- package/.zed/settings.json +3 -0
- package/CLAUDE.md +41 -7
- package/README.md +48 -25
- package/archive/benchmark.ts +0 -5
- package/archive/collection.ts +6 -65
- package/archive/composite.ts +85 -0
- package/archive/computed.ts +18 -20
- package/archive/list.ts +7 -75
- package/archive/memo.ts +15 -15
- package/archive/state.ts +2 -1
- package/archive/store.ts +8 -78
- package/archive/task.ts +20 -25
- package/index.dev.js +508 -526
- package/index.js +1 -1
- package/index.ts +9 -11
- package/package.json +6 -6
- package/src/classes/collection.ts +70 -107
- package/src/classes/computed.ts +165 -149
- package/src/classes/list.ts +145 -107
- package/src/classes/ref.ts +19 -17
- package/src/classes/state.ts +21 -17
- package/src/classes/store.ts +125 -73
- package/src/diff.ts +2 -1
- package/src/effect.ts +17 -10
- package/src/errors.ts +14 -1
- package/src/resolve.ts +1 -1
- package/src/signal.ts +3 -2
- package/src/system.ts +159 -61
- package/src/util.ts +0 -6
- package/test/batch.test.ts +4 -11
- package/test/benchmark.test.ts +4 -2
- package/test/collection.test.ts +106 -107
- package/test/computed.test.ts +351 -112
- package/test/effect.test.ts +2 -2
- package/test/list.test.ts +62 -102
- package/test/ref.test.ts +128 -2
- package/test/state.test.ts +16 -22
- package/test/store.test.ts +101 -108
- package/test/util/dependency-graph.ts +2 -2
- package/tsconfig.build.json +11 -0
- package/tsconfig.json +5 -7
- package/types/index.d.ts +3 -3
- package/types/src/classes/collection.d.ts +9 -10
- package/types/src/classes/computed.d.ts +17 -20
- package/types/src/classes/list.d.ts +8 -6
- package/types/src/classes/ref.d.ts +8 -12
- package/types/src/classes/state.d.ts +5 -8
- package/types/src/classes/store.d.ts +14 -13
- package/types/src/effect.d.ts +1 -2
- package/types/src/errors.d.ts +2 -1
- package/types/src/signal.d.ts +3 -2
- package/types/src/system.d.ts +47 -34
- package/types/src/util.d.ts +1 -2
- package/src/classes/composite.ts +0 -176
- package/types/src/classes/composite.d.ts +0 -15
package/.ai-context.md
CHANGED
|
@@ -279,6 +279,19 @@ const finalResults = processedItems.deriveCollection(item =>
|
|
|
279
279
|
// Ref signal manual notifications
|
|
280
280
|
elementRef.notify() // Notify when DOM element changes externally
|
|
281
281
|
cacheRef.notify() // Notify when Map/Set changes externally
|
|
282
|
+
|
|
283
|
+
// Resource management with watch callbacks
|
|
284
|
+
const endpoint = new State('https://api.example.com', {
|
|
285
|
+
watched: () => {
|
|
286
|
+
console.log('Setting up API client...')
|
|
287
|
+
const resource = createResource(endpoint.get())
|
|
288
|
+
},
|
|
289
|
+
unwatched: () => {
|
|
290
|
+
console.log('Cleaning up API client...')
|
|
291
|
+
resource.cleanup()
|
|
292
|
+
}
|
|
293
|
+
})
|
|
294
|
+
|
|
282
295
|
```
|
|
283
296
|
|
|
284
297
|
## Build and Development
|
|
@@ -173,6 +173,10 @@ const activeUserSummaries = users
|
|
|
173
173
|
.filter(Boolean)
|
|
174
174
|
```
|
|
175
175
|
|
|
176
|
+
## Resource Management
|
|
177
|
+
|
|
178
|
+
All signals support `watched` and `unwatched` callbacks in signal configuration (optional second parameter) for lazy resource allocation. Resources are only created when signals are accessed by effects and automatically cleaned up when no longer watched.
|
|
179
|
+
|
|
176
180
|
## When suggesting code:
|
|
177
181
|
1. Follow the established patterns for signal creation and usage
|
|
178
182
|
2. Use proper TypeScript types and generics
|
package/CLAUDE.md
CHANGED
|
@@ -78,19 +78,14 @@ Key patterns:
|
|
|
78
78
|
|
|
79
79
|
**Store signals** (`createStore`): Transform objects into reactive data structures
|
|
80
80
|
- Each property becomes its own signal via Proxy
|
|
81
|
-
-
|
|
81
|
+
- Lazy signal creation and automatic cleanup
|
|
82
82
|
- Dynamic property addition/removal with proper reactivity
|
|
83
83
|
|
|
84
84
|
**List signals** (`new List`): Arrays with stable keys and reactive items
|
|
85
|
-
- Maintains stable keys that survive sorting and
|
|
85
|
+
- Maintains stable keys that survive sorting and splicing
|
|
86
86
|
- Built on `Composite` class for consistent signal management
|
|
87
87
|
- Provides `byKey()`, `keyAt()`, `indexOfKey()` for key-based access
|
|
88
88
|
|
|
89
|
-
**Composite Architecture**: Shared foundation for Store and List
|
|
90
|
-
- `Map<string, Signal>` for property/item signals
|
|
91
|
-
- Event system for granular add/change/remove notifications
|
|
92
|
-
- Lazy signal creation and automatic cleanup
|
|
93
|
-
|
|
94
89
|
### Computed Signal Memoization Strategy
|
|
95
90
|
|
|
96
91
|
Computed signals implement smart memoization:
|
|
@@ -100,6 +95,45 @@ Computed signals implement smart memoization:
|
|
|
100
95
|
- **Error Handling**: Preserves error states and prevents cascade failures
|
|
101
96
|
- **Reducer Capabilities**: Access to previous value enables state accumulation and transitions
|
|
102
97
|
|
|
98
|
+
## Resource Management with Watch Callbacks
|
|
99
|
+
|
|
100
|
+
All signals support the `watched` and `unwatched` callbacks for lazy resource management. Resources are allocated only when a signal is first accessed by an effect and automatically cleaned up when no effects are watching:
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
// Basic watch callbacks pattern
|
|
104
|
+
const config = new State({ apiUrl: 'https://api.example.com' }, {
|
|
105
|
+
watched: () => {
|
|
106
|
+
console.log('Setting up API client...')
|
|
107
|
+
const client = new ApiClient(config.get().apiUrl)
|
|
108
|
+
},
|
|
109
|
+
unwatched: () => {
|
|
110
|
+
console.log('Cleaning up API client...')
|
|
111
|
+
client.disconnect()
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
// Resource is only created when effect runs
|
|
116
|
+
const cleanup = createEffect(() => {
|
|
117
|
+
console.log('API URL:', config.get().apiUrl) // Triggers watched callback
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
cleanup() // Triggers unwatched callback
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**Practical Use Cases**:
|
|
124
|
+
- Event listeners that activate only when data is watched
|
|
125
|
+
- Network connections established on-demand
|
|
126
|
+
- Expensive computations that pause when not needed
|
|
127
|
+
- External subscriptions (WebSocket, Server-Sent Events)
|
|
128
|
+
- Database connections tied to data access patterns
|
|
129
|
+
|
|
130
|
+
**Watch Lifecycle**:
|
|
131
|
+
1. First effect accesses signal → `watched` callback executed
|
|
132
|
+
3. Last effect stops watching → `unwatched` callback executed
|
|
133
|
+
4. New effect accesses signal → `watched` callback executed again
|
|
134
|
+
|
|
135
|
+
This pattern enables **lazy resource allocation** - resources are only consumed when actually needed and automatically freed when no longer used.
|
|
136
|
+
|
|
103
137
|
## Advanced Patterns and Best Practices
|
|
104
138
|
|
|
105
139
|
### When to Use Each Signal Type
|
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Cause & Effect
|
|
2
2
|
|
|
3
|
-
Version 0.17.
|
|
3
|
+
Version 0.17.3
|
|
4
4
|
|
|
5
|
-
**Cause & Effect** is a tiny (~5kB gzipped), dependency-free reactive state library for JavaScript. It uses fine-grained signals so derived values and side effects update automatically when their dependencies change.
|
|
5
|
+
**Cause & Effect** is a tiny (~5kB gzipped), dependency-free reactive state management library for JavaScript. It uses fine-grained signals so derived values and side effects update automatically when their dependencies change.
|
|
6
6
|
|
|
7
7
|
## What is Cause & Effect?
|
|
8
8
|
|
|
@@ -159,35 +159,23 @@ user.preferences.theme.set('light')
|
|
|
159
159
|
createEffect(() => console.log('User:', user.get()))
|
|
160
160
|
```
|
|
161
161
|
|
|
162
|
-
|
|
162
|
+
Iterator for keys using reactive `.keys()` method to observe structural changes:
|
|
163
163
|
|
|
164
164
|
```js
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
settings.remove('timeout')
|
|
165
|
+
for (const key of user.keys()) {
|
|
166
|
+
console.log(key)
|
|
167
|
+
}
|
|
169
168
|
```
|
|
170
169
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
```js
|
|
174
|
-
const user = createStore({ name: 'Alice', age: 30 })
|
|
175
|
-
|
|
176
|
-
const offChange = user.on('change', changed => console.log(changed))
|
|
177
|
-
const offAdd = user.on('add', added => console.log(added))
|
|
178
|
-
const offRemove = user.on('remove', removed => console.log(removed))
|
|
170
|
+
Access items by key using `.byKey()` or via direct property access like `user.name` (enabled by the Proxy `createStore()` returns).
|
|
179
171
|
|
|
180
|
-
|
|
181
|
-
user.add('email', 'alice@example.com') // Logs: "Added properties: ['email']"
|
|
182
|
-
user.age.set(31) // Logs: "Changed properties: ['age']"
|
|
183
|
-
user.remove('email') // Logs: "Removed properties: ['email']"
|
|
184
|
-
|
|
185
|
-
To stop listening to notifications, call the returned cleanup functions:
|
|
172
|
+
Dynamic properties using the `.add()` and `.remove()` methods:
|
|
186
173
|
|
|
187
174
|
```js
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
175
|
+
const settings = createStore({ autoSave: true })
|
|
176
|
+
|
|
177
|
+
settings.add('timeout', 5000)
|
|
178
|
+
settings.remove('timeout')
|
|
191
179
|
```
|
|
192
180
|
|
|
193
181
|
### List
|
|
@@ -206,6 +194,8 @@ items.splice(1, 1, 'orange')
|
|
|
206
194
|
items.sort()
|
|
207
195
|
```
|
|
208
196
|
|
|
197
|
+
Access items by key using `.byKey()` or by index using `.at()`. `.indexOfKey()` returns the current index of an item in the list, while `.keyAt()` returns the key of an item at a given position.
|
|
198
|
+
|
|
209
199
|
Keys are stable across reordering:
|
|
210
200
|
|
|
211
201
|
```js
|
|
@@ -217,7 +207,7 @@ console.log(items.byKey(key)) // 'orange'
|
|
|
217
207
|
console.log(items.indexOfKey(key)) // current index
|
|
218
208
|
```
|
|
219
209
|
|
|
220
|
-
Lists have `.
|
|
210
|
+
Lists have `.keys()`, `.add()`, and `.remove()` methods like stores. Additionally, they have `.sort()`, `.splice()`, and a reactive `.length` property. But unlike stores, deeply nested properties in items are not converted to individual signals. Lists have no Proxy layer and don't support direct property access like `items[0].name`.
|
|
221
211
|
|
|
222
212
|
### Collection
|
|
223
213
|
|
|
@@ -398,6 +388,39 @@ cleanup() // Logs: 'Cleanup' and unsubscribes from signal `user`
|
|
|
398
388
|
user.set({ name: 'Bob', age: 28 }) // Won't trigger the effect anymore
|
|
399
389
|
```
|
|
400
390
|
|
|
391
|
+
### Resource Management with Watch Callbacks
|
|
392
|
+
|
|
393
|
+
All signals support a options object with `watched` and `unwatched` callbacks for lazy resource management. Resources are only allocated when the signal is first accessed by an effect, and automatically cleaned up when no effects are watching:
|
|
394
|
+
|
|
395
|
+
```js
|
|
396
|
+
import { State, createEffect } from '@zeix/cause-effect'
|
|
397
|
+
|
|
398
|
+
const config = new State({ apiUrl: 'https://api.example.com' }, {
|
|
399
|
+
watched: () => {
|
|
400
|
+
console.log('Setting up API client...')
|
|
401
|
+
const client = new ApiClient(config.get().apiUrl)
|
|
402
|
+
},
|
|
403
|
+
unwatched: () => {
|
|
404
|
+
console.log('Cleaning up API client...')
|
|
405
|
+
client.disconnect()
|
|
406
|
+
}
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
// Resource is created only when effect runs
|
|
410
|
+
const cleanup = createEffect(() => {
|
|
411
|
+
console.log('API URL:', config.get().apiUrl)
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
// Resource is cleaned up when effect stops
|
|
415
|
+
cleanup()
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
This pattern is ideal for:
|
|
419
|
+
- Event listeners that should only be active when data is being watched
|
|
420
|
+
- Network connections that can be lazily established
|
|
421
|
+
- Expensive computations that should pause when not needed
|
|
422
|
+
- External subscriptions (WebSocket, Server-Sent Events, etc.)
|
|
423
|
+
|
|
401
424
|
### resolve()
|
|
402
425
|
|
|
403
426
|
Extract signal values:
|
package/archive/benchmark.ts
CHANGED
|
@@ -159,7 +159,6 @@ const benchmarkFactory = async () => {
|
|
|
159
159
|
const memoryStores = await measureMemory('Factory Memory Usage', () => {
|
|
160
160
|
const tempStores = []
|
|
161
161
|
for (let i = 0; i < ITERATIONS; i++)
|
|
162
|
-
// @ts-expect-error ignore
|
|
163
162
|
tempStores.push(createFactoryStore({ ...testData, id: i }))
|
|
164
163
|
return tempStores
|
|
165
164
|
})
|
|
@@ -226,7 +225,6 @@ const benchmarkFactoryList = async () => {
|
|
|
226
225
|
const tempLists = []
|
|
227
226
|
for (let i = 0; i < ITERATIONS; i++) {
|
|
228
227
|
tempLists.push(
|
|
229
|
-
// @ts-expect-error ignore
|
|
230
228
|
createFactoryList([
|
|
231
229
|
...testListData.map(item => ({
|
|
232
230
|
...item,
|
|
@@ -305,7 +303,6 @@ const benchmarkDirectClassList = async () => {
|
|
|
305
303
|
const tempLists = []
|
|
306
304
|
for (let i = 0; i < ITERATIONS; i++) {
|
|
307
305
|
tempLists.push(
|
|
308
|
-
// @ts-expect-error ignore
|
|
309
306
|
new List([
|
|
310
307
|
...testListData.map(item => ({
|
|
311
308
|
...item,
|
|
@@ -375,7 +372,6 @@ const benchmarkClass = async () => {
|
|
|
375
372
|
const memoryStores = await measureMemory('Class Memory Usage', () => {
|
|
376
373
|
const tempStores = []
|
|
377
374
|
for (let i = 0; i < ITERATIONS; i++)
|
|
378
|
-
// @ts-expect-error ignore
|
|
379
375
|
tempStores.push(createClassStore({ ...testData, id: i }))
|
|
380
376
|
return tempStores
|
|
381
377
|
})
|
|
@@ -436,7 +432,6 @@ const benchmarkDirectClass = async () => {
|
|
|
436
432
|
() => {
|
|
437
433
|
const tempStores = []
|
|
438
434
|
for (let i = 0; i < ITERATIONS; i++)
|
|
439
|
-
// @ts-expect-error ignore
|
|
440
435
|
tempStores.push(new BaseStore({ ...testData, id: i }))
|
|
441
436
|
return tempStores
|
|
442
437
|
},
|
package/archive/collection.ts
CHANGED
|
@@ -3,18 +3,13 @@ import { match } from '../src/match'
|
|
|
3
3
|
import { resolve } from '../src/resolve'
|
|
4
4
|
import type { Signal } from '../src/signal'
|
|
5
5
|
import {
|
|
6
|
-
type Cleanup,
|
|
7
6
|
createWatcher,
|
|
8
|
-
emitNotification,
|
|
9
|
-
type Listener,
|
|
10
|
-
type Listeners,
|
|
11
|
-
type Notifications,
|
|
12
7
|
notifyWatchers,
|
|
13
8
|
subscribeActiveWatcher,
|
|
14
|
-
|
|
9
|
+
UNSET,
|
|
15
10
|
type Watcher,
|
|
16
11
|
} from '../src/system'
|
|
17
|
-
import { isAsyncFunction, isObjectOfType, isSymbol
|
|
12
|
+
import { isAsyncFunction, isObjectOfType, isSymbol } from '../src/util'
|
|
18
13
|
import { type Computed, createComputed } from './computed'
|
|
19
14
|
import type { List } from './list'
|
|
20
15
|
|
|
@@ -39,7 +34,6 @@ type Collection<T extends {}> = {
|
|
|
39
34
|
get(): T[]
|
|
40
35
|
keyAt(index: number): string | undefined
|
|
41
36
|
indexOfKey(key: string): number
|
|
42
|
-
on<K extends keyof Notifications>(type: K, listener: Listener<K>): Cleanup
|
|
43
37
|
sort(compareFn?: (a: T, b: T) => number): void
|
|
44
38
|
}
|
|
45
39
|
|
|
@@ -66,12 +60,6 @@ const createCollection = <T extends {}, O extends {}>(
|
|
|
66
60
|
callback: CollectionCallback<T, O>,
|
|
67
61
|
): Collection<T> => {
|
|
68
62
|
const watchers = new Set<Watcher>()
|
|
69
|
-
const listeners: Listeners = {
|
|
70
|
-
add: new Set<Listener<'add'>>(),
|
|
71
|
-
change: new Set<Listener<'change'>>(),
|
|
72
|
-
remove: new Set<Listener<'remove'>>(),
|
|
73
|
-
sort: new Set<Listener<'sort'>>(),
|
|
74
|
-
}
|
|
75
63
|
const signals = new Map<string, Signal<T>>()
|
|
76
64
|
const signalWatchers = new Map<string, Watcher>()
|
|
77
65
|
|
|
@@ -118,60 +106,23 @@ const createCollection = <T extends {}, O extends {}>(
|
|
|
118
106
|
// Set internal states
|
|
119
107
|
signals.set(key, signal)
|
|
120
108
|
if (!order.includes(key)) order.push(key)
|
|
121
|
-
const watcher = createWatcher(
|
|
122
|
-
|
|
109
|
+
const watcher = createWatcher(
|
|
110
|
+
() => {
|
|
123
111
|
signal.get() // Subscribe to the signal
|
|
124
|
-
|
|
125
|
-
}
|
|
112
|
+
},
|
|
113
|
+
() => {},
|
|
126
114
|
)
|
|
127
115
|
watcher()
|
|
128
116
|
signalWatchers.set(key, watcher)
|
|
129
117
|
return true
|
|
130
118
|
}
|
|
131
119
|
|
|
132
|
-
// Remove nested signal and effect
|
|
133
|
-
const removeProperty = (key: string) => {
|
|
134
|
-
// Remove signal for key
|
|
135
|
-
const ok = signals.delete(key)
|
|
136
|
-
if (!ok) return
|
|
137
|
-
|
|
138
|
-
// Clean up internal states
|
|
139
|
-
const index = order.indexOf(key)
|
|
140
|
-
if (index >= 0) order.splice(index, 1)
|
|
141
|
-
const watcher = signalWatchers.get(key)
|
|
142
|
-
if (watcher) {
|
|
143
|
-
watcher.stop()
|
|
144
|
-
signalWatchers.delete(key)
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
120
|
// Initialize properties
|
|
149
121
|
for (let i = 0; i < origin.length; i++) {
|
|
150
122
|
const key = origin.keyAt(i)
|
|
151
123
|
if (!key) continue
|
|
152
124
|
addProperty(key)
|
|
153
125
|
}
|
|
154
|
-
origin.on('add', additions => {
|
|
155
|
-
for (const key of additions) {
|
|
156
|
-
if (!signals.has(key)) addProperty(key)
|
|
157
|
-
}
|
|
158
|
-
notifyWatchers(watchers)
|
|
159
|
-
emitNotification(listeners.add, additions)
|
|
160
|
-
})
|
|
161
|
-
origin.on('remove', removals => {
|
|
162
|
-
for (const key of Object.keys(removals)) {
|
|
163
|
-
if (!signals.has(key)) continue
|
|
164
|
-
removeProperty(key)
|
|
165
|
-
}
|
|
166
|
-
order = order.filter(() => true) // Compact array
|
|
167
|
-
notifyWatchers(watchers)
|
|
168
|
-
emitNotification(listeners.remove, removals)
|
|
169
|
-
})
|
|
170
|
-
origin.on('sort', newOrder => {
|
|
171
|
-
order = [...newOrder]
|
|
172
|
-
notifyWatchers(watchers)
|
|
173
|
-
emitNotification(listeners.sort, newOrder)
|
|
174
|
-
})
|
|
175
126
|
|
|
176
127
|
// Get signal by key or index
|
|
177
128
|
const getSignal = (prop: string): Signal<T> | undefined => {
|
|
@@ -247,16 +198,6 @@ const createCollection = <T extends {}, O extends {}>(
|
|
|
247
198
|
order = entries.map(([_, key]) => key)
|
|
248
199
|
|
|
249
200
|
notifyWatchers(watchers)
|
|
250
|
-
emitNotification(listeners.sort, order)
|
|
251
|
-
},
|
|
252
|
-
},
|
|
253
|
-
on: {
|
|
254
|
-
value: <K extends keyof Listeners>(
|
|
255
|
-
type: K,
|
|
256
|
-
listener: Listener<K>,
|
|
257
|
-
): Cleanup => {
|
|
258
|
-
listeners[type].add(listener)
|
|
259
|
-
return () => listeners[type].delete(listener)
|
|
260
201
|
},
|
|
261
202
|
},
|
|
262
203
|
length: {
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { DiffResult, UnknownRecord } from '../src/diff'
|
|
2
|
+
import { guardMutableSignal } from '../src/errors'
|
|
3
|
+
import type { Signal } from '../src/signal'
|
|
4
|
+
import { batch } from '../src/system'
|
|
5
|
+
|
|
6
|
+
/* === Class Definitions === */
|
|
7
|
+
|
|
8
|
+
class Composite<T extends UnknownRecord, S extends Signal<T[keyof T] & {}>> {
|
|
9
|
+
signals = new Map<string, S>()
|
|
10
|
+
#validate: <K extends keyof T & string>(
|
|
11
|
+
key: K,
|
|
12
|
+
value: unknown,
|
|
13
|
+
) => value is T[K] & {}
|
|
14
|
+
#create: <V extends T[keyof T] & {}>(value: V) => S
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
values: T,
|
|
18
|
+
validate: <K extends keyof T & string>(
|
|
19
|
+
key: K,
|
|
20
|
+
value: unknown,
|
|
21
|
+
) => value is T[K] & {},
|
|
22
|
+
create: <V extends T[keyof T] & {}>(value: V) => S,
|
|
23
|
+
) {
|
|
24
|
+
this.#validate = validate
|
|
25
|
+
this.#create = create
|
|
26
|
+
this.change({
|
|
27
|
+
add: values,
|
|
28
|
+
change: {},
|
|
29
|
+
remove: {},
|
|
30
|
+
changed: true,
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
add<K extends keyof T & string>(key: K, value: T[K]): boolean {
|
|
35
|
+
if (!this.#validate(key, value)) return false
|
|
36
|
+
|
|
37
|
+
this.signals.set(key, this.#create(value))
|
|
38
|
+
return true
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
remove<K extends keyof T & string>(key: K): boolean {
|
|
42
|
+
return this.signals.delete(key)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
change(changes: DiffResult): boolean {
|
|
46
|
+
// Additions
|
|
47
|
+
if (Object.keys(changes.add).length) {
|
|
48
|
+
for (const key in changes.add)
|
|
49
|
+
this.add(
|
|
50
|
+
key as Extract<keyof T, string>,
|
|
51
|
+
changes.add[key] as T[Extract<keyof T, string>] & {},
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Changes
|
|
56
|
+
if (Object.keys(changes.change).length) {
|
|
57
|
+
batch(() => {
|
|
58
|
+
for (const key in changes.change) {
|
|
59
|
+
const value = changes.change[key]
|
|
60
|
+
if (!this.#validate(key as keyof T & string, value))
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
const signal = this.signals.get(key)
|
|
64
|
+
if (guardMutableSignal(`list item "${key}"`, value, signal))
|
|
65
|
+
signal.set(value)
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Removals
|
|
71
|
+
if (Object.keys(changes.remove).length) {
|
|
72
|
+
for (const key in changes.remove)
|
|
73
|
+
this.remove(key as keyof T & string)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return changes.changed
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
clear(): boolean {
|
|
80
|
+
this.signals.clear()
|
|
81
|
+
return true
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export { Composite }
|
package/archive/computed.ts
CHANGED
|
@@ -7,10 +7,10 @@ import {
|
|
|
7
7
|
} from '../src/errors'
|
|
8
8
|
import {
|
|
9
9
|
createWatcher,
|
|
10
|
-
|
|
10
|
+
flush,
|
|
11
11
|
notifyWatchers,
|
|
12
12
|
subscribeActiveWatcher,
|
|
13
|
-
|
|
13
|
+
UNSET,
|
|
14
14
|
type Watcher,
|
|
15
15
|
} from '../src/system'
|
|
16
16
|
import {
|
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
isAsyncFunction,
|
|
19
19
|
isFunction,
|
|
20
20
|
isObjectOfType,
|
|
21
|
-
UNSET,
|
|
22
21
|
} from '../src/util'
|
|
23
22
|
|
|
24
23
|
/* === Types === */
|
|
@@ -96,19 +95,14 @@ const createComputed = <T extends {}>(
|
|
|
96
95
|
}
|
|
97
96
|
|
|
98
97
|
// Own watcher: called when notified from sources (push)
|
|
99
|
-
const watcher = createWatcher(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
// Called when requested by dependencies (pull)
|
|
110
|
-
const compute = () =>
|
|
111
|
-
trackSignalReads(watcher, () => {
|
|
98
|
+
const watcher = createWatcher(
|
|
99
|
+
() => {
|
|
100
|
+
dirty = true
|
|
101
|
+
controller?.abort()
|
|
102
|
+
if (watchers.size) notifyWatchers(watchers)
|
|
103
|
+
else watcher.stop()
|
|
104
|
+
},
|
|
105
|
+
() => {
|
|
112
106
|
if (computing) throw new CircularDependencyError('computed')
|
|
113
107
|
changed = false
|
|
114
108
|
if (isAsyncFunction(callback)) {
|
|
@@ -120,7 +114,7 @@ const createComputed = <T extends {}>(
|
|
|
120
114
|
() => {
|
|
121
115
|
computing = false
|
|
122
116
|
controller = undefined
|
|
123
|
-
|
|
117
|
+
watcher.run() // Retry computation with updated state
|
|
124
118
|
},
|
|
125
119
|
{
|
|
126
120
|
once: true,
|
|
@@ -143,7 +137,11 @@ const createComputed = <T extends {}>(
|
|
|
143
137
|
else if (null == result || UNSET === result) nil()
|
|
144
138
|
else ok(result)
|
|
145
139
|
computing = false
|
|
146
|
-
}
|
|
140
|
+
},
|
|
141
|
+
)
|
|
142
|
+
watcher.onCleanup(() => {
|
|
143
|
+
controller?.abort()
|
|
144
|
+
})
|
|
147
145
|
|
|
148
146
|
const computed: Record<PropertyKey, unknown> = {}
|
|
149
147
|
Object.defineProperties(computed, {
|
|
@@ -153,8 +151,8 @@ const createComputed = <T extends {}>(
|
|
|
153
151
|
get: {
|
|
154
152
|
value: (): T => {
|
|
155
153
|
subscribeActiveWatcher(watchers)
|
|
156
|
-
|
|
157
|
-
if (dirty)
|
|
154
|
+
flush()
|
|
155
|
+
if (dirty) watcher.run()
|
|
158
156
|
if (error) throw error
|
|
159
157
|
return value
|
|
160
158
|
},
|