@zeix/cause-effect 0.17.2 → 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 +11 -5
- package/.github/copilot-instructions.md +1 -1
- package/.zed/settings.json +3 -0
- package/CLAUDE.md +18 -79
- package/README.md +23 -37
- package/archive/benchmark.ts +0 -5
- package/archive/collection.ts +5 -62
- package/archive/composite.ts +85 -0
- package/archive/computed.ts +17 -20
- package/archive/list.ts +6 -67
- package/archive/memo.ts +13 -14
- package/archive/store.ts +7 -66
- package/archive/task.ts +18 -20
- package/index.dev.js +438 -614
- package/index.js +1 -1
- package/index.ts +8 -19
- package/package.json +6 -6
- package/src/classes/collection.ts +59 -112
- package/src/classes/computed.ts +146 -189
- package/src/classes/list.ts +138 -105
- package/src/classes/ref.ts +16 -42
- package/src/classes/state.ts +16 -45
- package/src/classes/store.ts +107 -72
- package/src/effect.ts +9 -12
- package/src/errors.ts +12 -8
- package/src/signal.ts +3 -1
- package/src/system.ts +136 -154
- package/test/batch.test.ts +4 -11
- package/test/benchmark.test.ts +4 -2
- package/test/collection.test.ts +46 -306
- package/test/computed.test.ts +205 -223
- package/test/list.test.ts +35 -303
- package/test/ref.test.ts +38 -66
- package/test/state.test.ts +6 -12
- package/test/store.test.ts +37 -489
- package/test/util/dependency-graph.ts +2 -2
- package/tsconfig.build.json +11 -0
- package/tsconfig.json +5 -7
- package/types/index.d.ts +2 -2
- package/types/src/classes/collection.d.ts +4 -6
- package/types/src/classes/computed.d.ts +17 -37
- package/types/src/classes/list.d.ts +8 -6
- package/types/src/classes/ref.d.ts +7 -20
- package/types/src/classes/state.d.ts +5 -17
- package/types/src/classes/store.d.ts +12 -11
- package/types/src/errors.d.ts +2 -4
- package/types/src/signal.d.ts +3 -2
- package/types/src/system.d.ts +41 -44
- package/src/classes/composite.ts +0 -171
- package/types/src/classes/composite.d.ts +0 -15
package/.ai-context.md
CHANGED
|
@@ -280,12 +280,18 @@ const finalResults = processedItems.deriveCollection(item =>
|
|
|
280
280
|
elementRef.notify() // Notify when DOM element changes externally
|
|
281
281
|
cacheRef.notify() // Notify when Map/Set changes externally
|
|
282
282
|
|
|
283
|
-
// Resource management with watch
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
+
}
|
|
288
293
|
})
|
|
294
|
+
|
|
289
295
|
```
|
|
290
296
|
|
|
291
297
|
## Build and Development
|
|
@@ -175,7 +175,7 @@ const activeUserSummaries = users
|
|
|
175
175
|
|
|
176
176
|
## Resource Management
|
|
177
177
|
|
|
178
|
-
All signals support
|
|
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
179
|
|
|
180
180
|
## When suggesting code:
|
|
181
181
|
1. Follow the established patterns for signal creation and usage
|
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
|
-
- Hook 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,84 +95,29 @@ 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
|
|
|
103
|
-
## Resource Management with Watch
|
|
98
|
+
## Resource Management with Watch Callbacks
|
|
104
99
|
|
|
105
|
-
All signals support the `
|
|
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:
|
|
106
101
|
|
|
107
102
|
```typescript
|
|
108
|
-
// Basic watch
|
|
109
|
-
const config = new State({ apiUrl: 'https://api.example.com' }
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return () => {
|
|
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: () => {
|
|
116
110
|
console.log('Cleaning up API client...')
|
|
117
111
|
client.disconnect()
|
|
118
112
|
}
|
|
119
113
|
})
|
|
120
114
|
|
|
121
115
|
// Resource is only created when effect runs
|
|
122
|
-
createEffect(() => {
|
|
123
|
-
console.log('API URL:', config.get().apiUrl) // Triggers
|
|
124
|
-
})
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
**Store Watch Hooks**: Monitor entire store or nested properties
|
|
128
|
-
|
|
129
|
-
```typescript
|
|
130
|
-
const database = createStore({ host: 'localhost', port: 5432 })
|
|
131
|
-
|
|
132
|
-
// Watch entire store - triggers when any property accessed
|
|
133
|
-
database.on('watch', () => {
|
|
134
|
-
console.log('Database connection needed')
|
|
135
|
-
const connection = connect(database.host.get(), database.port.get())
|
|
136
|
-
return () => connection.close()
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
// Watch specific property
|
|
140
|
-
database.host.on('watch', () => {
|
|
141
|
-
console.log('Host property being watched')
|
|
142
|
-
return () => console.log('Host watching stopped')
|
|
143
|
-
})
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
**List Watch Hooks**: Two-tier system for collection and item resources
|
|
147
|
-
|
|
148
|
-
```typescript
|
|
149
|
-
const items = new List(['apple', 'banana'])
|
|
150
|
-
|
|
151
|
-
// List-level resource (entire collection)
|
|
152
|
-
items.on('watch', () => {
|
|
153
|
-
console.log('List observer started')
|
|
154
|
-
return () => console.log('List observer stopped')
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
// Item-level resource (individual items)
|
|
158
|
-
const firstItem = items.at(0)
|
|
159
|
-
firstItem.on('watch', () => {
|
|
160
|
-
console.log('First item being watched')
|
|
161
|
-
return () => console.log('First item watch stopped')
|
|
162
|
-
})
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
**Collection Watch Hooks**: Propagate to source List items
|
|
166
|
-
|
|
167
|
-
```typescript
|
|
168
|
-
const numbers = new List([1, 2, 3])
|
|
169
|
-
const doubled = numbers.deriveCollection(x => x * 2)
|
|
170
|
-
|
|
171
|
-
// Set up source item hook
|
|
172
|
-
numbers.at(0).on('watch', () => {
|
|
173
|
-
console.log('Source item accessed through collection')
|
|
174
|
-
return () => console.log('Source item no longer watched')
|
|
116
|
+
const cleanup = createEffect(() => {
|
|
117
|
+
console.log('API URL:', config.get().apiUrl) // Triggers watched callback
|
|
175
118
|
})
|
|
176
119
|
|
|
177
|
-
//
|
|
178
|
-
createEffect(() => {
|
|
179
|
-
const value = doubled.at(0).get() // Triggers source item hook
|
|
180
|
-
})
|
|
120
|
+
cleanup() // Triggers unwatched callback
|
|
181
121
|
```
|
|
182
122
|
|
|
183
123
|
**Practical Use Cases**:
|
|
@@ -187,11 +127,10 @@ createEffect(() => {
|
|
|
187
127
|
- External subscriptions (WebSocket, Server-Sent Events)
|
|
188
128
|
- Database connections tied to data access patterns
|
|
189
129
|
|
|
190
|
-
**
|
|
191
|
-
1. First effect accesses signal → `
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
4. New effect accesses signal → hook callback executed again
|
|
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
|
|
195
134
|
|
|
196
135
|
This pattern enables **lazy resource allocation** - resources are only consumed when actually needed and automatically freed when no longer used.
|
|
197
136
|
|
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,36 +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
|
-
|
|
170
|
+
Access items by key using `.byKey()` or via direct property access like `user.name` (enabled by the Proxy `createStore()` returns).
|
|
172
171
|
|
|
173
|
-
|
|
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))
|
|
179
|
-
|
|
180
|
-
// These will trigger the respective hooks:
|
|
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
|
-
|
|
186
|
-
To unregister hooks, call the returned cleanup functions:
|
|
172
|
+
Dynamic properties using the `.add()` and `.remove()` methods:
|
|
187
173
|
|
|
188
174
|
```js
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
175
|
+
const settings = createStore({ autoSave: true })
|
|
176
|
+
|
|
177
|
+
settings.add('timeout', 5000)
|
|
178
|
+
settings.remove('timeout')
|
|
192
179
|
```
|
|
193
180
|
|
|
194
181
|
### List
|
|
@@ -207,6 +194,8 @@ items.splice(1, 1, 'orange')
|
|
|
207
194
|
items.sort()
|
|
208
195
|
```
|
|
209
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
|
+
|
|
210
199
|
Keys are stable across reordering:
|
|
211
200
|
|
|
212
201
|
```js
|
|
@@ -218,7 +207,7 @@ console.log(items.byKey(key)) // 'orange'
|
|
|
218
207
|
console.log(items.indexOfKey(key)) // current index
|
|
219
208
|
```
|
|
220
209
|
|
|
221
|
-
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`.
|
|
222
211
|
|
|
223
212
|
### Collection
|
|
224
213
|
|
|
@@ -399,22 +388,19 @@ cleanup() // Logs: 'Cleanup' and unsubscribes from signal `user`
|
|
|
399
388
|
user.set({ name: 'Bob', age: 28 }) // Won't trigger the effect anymore
|
|
400
389
|
```
|
|
401
390
|
|
|
402
|
-
### Resource Management with
|
|
391
|
+
### Resource Management with Watch Callbacks
|
|
403
392
|
|
|
404
|
-
All signals support
|
|
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:
|
|
405
394
|
|
|
406
395
|
```js
|
|
407
396
|
import { State, createEffect } from '@zeix/cause-effect'
|
|
408
397
|
|
|
409
|
-
const config = new State({ apiUrl: 'https://api.example.com' }
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
config.
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
// Return cleanup function
|
|
417
|
-
return () => {
|
|
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: () => {
|
|
418
404
|
console.log('Cleaning up API client...')
|
|
419
405
|
client.disconnect()
|
|
420
406
|
}
|
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,17 +3,11 @@ 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
|
-
triggerHook,
|
|
9
|
-
type HookCallback,
|
|
10
|
-
type HookCallbacks,
|
|
11
|
-
type Hook,
|
|
12
7
|
notifyWatchers,
|
|
13
8
|
subscribeActiveWatcher,
|
|
14
|
-
trackSignalReads,
|
|
15
|
-
type Watcher,
|
|
16
9
|
UNSET,
|
|
10
|
+
type Watcher,
|
|
17
11
|
} from '../src/system'
|
|
18
12
|
import { isAsyncFunction, isObjectOfType, isSymbol } from '../src/util'
|
|
19
13
|
import { type Computed, createComputed } from './computed'
|
|
@@ -40,7 +34,6 @@ type Collection<T extends {}> = {
|
|
|
40
34
|
get(): T[]
|
|
41
35
|
keyAt(index: number): string | undefined
|
|
42
36
|
indexOfKey(key: string): number
|
|
43
|
-
on(type: Hook, callback: HookCallback): Cleanup
|
|
44
37
|
sort(compareFn?: (a: T, b: T) => number): void
|
|
45
38
|
}
|
|
46
39
|
|
|
@@ -67,7 +60,6 @@ const createCollection = <T extends {}, O extends {}>(
|
|
|
67
60
|
callback: CollectionCallback<T, O>,
|
|
68
61
|
): Collection<T> => {
|
|
69
62
|
const watchers = new Set<Watcher>()
|
|
70
|
-
const hookCallbacks: HookCallbacks = {}
|
|
71
63
|
const signals = new Map<string, Signal<T>>()
|
|
72
64
|
const signalWatchers = new Map<string, Watcher>()
|
|
73
65
|
|
|
@@ -114,64 +106,23 @@ const createCollection = <T extends {}, O extends {}>(
|
|
|
114
106
|
// Set internal states
|
|
115
107
|
signals.set(key, signal)
|
|
116
108
|
if (!order.includes(key)) order.push(key)
|
|
117
|
-
const watcher = createWatcher(
|
|
118
|
-
|
|
109
|
+
const watcher = createWatcher(
|
|
110
|
+
() => {
|
|
119
111
|
signal.get() // Subscribe to the signal
|
|
120
|
-
|
|
121
|
-
}
|
|
112
|
+
},
|
|
113
|
+
() => {},
|
|
122
114
|
)
|
|
123
115
|
watcher()
|
|
124
116
|
signalWatchers.set(key, watcher)
|
|
125
117
|
return true
|
|
126
118
|
}
|
|
127
119
|
|
|
128
|
-
// Remove nested signal and effect
|
|
129
|
-
const removeProperty = (key: string) => {
|
|
130
|
-
// Remove signal for key
|
|
131
|
-
const ok = signals.delete(key)
|
|
132
|
-
if (!ok) return
|
|
133
|
-
|
|
134
|
-
// Clean up internal states
|
|
135
|
-
const index = order.indexOf(key)
|
|
136
|
-
if (index >= 0) order.splice(index, 1)
|
|
137
|
-
const watcher = signalWatchers.get(key)
|
|
138
|
-
if (watcher) {
|
|
139
|
-
watcher.stop()
|
|
140
|
-
signalWatchers.delete(key)
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
120
|
// Initialize properties
|
|
145
121
|
for (let i = 0; i < origin.length; i++) {
|
|
146
122
|
const key = origin.keyAt(i)
|
|
147
123
|
if (!key) continue
|
|
148
124
|
addProperty(key)
|
|
149
125
|
}
|
|
150
|
-
origin.on('add', additions => {
|
|
151
|
-
if (!additions?.length) return
|
|
152
|
-
for (const key of additions) {
|
|
153
|
-
if (!signals.has(key)) addProperty(key)
|
|
154
|
-
}
|
|
155
|
-
notifyWatchers(watchers)
|
|
156
|
-
triggerHook(hookCallbacks.add, additions)
|
|
157
|
-
})
|
|
158
|
-
origin.on('remove', removals => {
|
|
159
|
-
if (!removals?.length) return
|
|
160
|
-
for (const key of Object.keys(removals)) {
|
|
161
|
-
if (!signals.has(key)) continue
|
|
162
|
-
removeProperty(key)
|
|
163
|
-
}
|
|
164
|
-
order = order.filter(() => true) // Compact array
|
|
165
|
-
notifyWatchers(watchers)
|
|
166
|
-
triggerHook(hookCallbacks.remove, removals)
|
|
167
|
-
})
|
|
168
|
-
origin.on('sort', newOrder => {
|
|
169
|
-
if (newOrder) {
|
|
170
|
-
order = [...newOrder]
|
|
171
|
-
notifyWatchers(watchers)
|
|
172
|
-
triggerHook(hookCallbacks.sort, newOrder)
|
|
173
|
-
}
|
|
174
|
-
})
|
|
175
126
|
|
|
176
127
|
// Get signal by key or index
|
|
177
128
|
const getSignal = (prop: string): Signal<T> | undefined => {
|
|
@@ -247,14 +198,6 @@ const createCollection = <T extends {}, O extends {}>(
|
|
|
247
198
|
order = entries.map(([_, key]) => key)
|
|
248
199
|
|
|
249
200
|
notifyWatchers(watchers)
|
|
250
|
-
triggerHook(hookCallbacks.sort, order)
|
|
251
|
-
},
|
|
252
|
-
},
|
|
253
|
-
on: {
|
|
254
|
-
value: (type: Hook, callback: HookCallback): Cleanup => {
|
|
255
|
-
hookCallbacks[type] ||= new Set()
|
|
256
|
-
hookCallbacks[type].add(callback)
|
|
257
|
-
return () => hookCallbacks[type]?.delete(callback)
|
|
258
201
|
},
|
|
259
202
|
},
|
|
260
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,11 +7,9 @@ import {
|
|
|
7
7
|
} from '../src/errors'
|
|
8
8
|
import {
|
|
9
9
|
createWatcher,
|
|
10
|
-
|
|
11
|
-
HOOK_CLEANUP,
|
|
10
|
+
flush,
|
|
12
11
|
notifyWatchers,
|
|
13
12
|
subscribeActiveWatcher,
|
|
14
|
-
trackSignalReads,
|
|
15
13
|
UNSET,
|
|
16
14
|
type Watcher,
|
|
17
15
|
} from '../src/system'
|
|
@@ -97,19 +95,14 @@ const createComputed = <T extends {}>(
|
|
|
97
95
|
}
|
|
98
96
|
|
|
99
97
|
// Own watcher: called when notified from sources (push)
|
|
100
|
-
const watcher = createWatcher(
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
// Called when requested by dependencies (pull)
|
|
111
|
-
const compute = () =>
|
|
112
|
-
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
|
+
() => {
|
|
113
106
|
if (computing) throw new CircularDependencyError('computed')
|
|
114
107
|
changed = false
|
|
115
108
|
if (isAsyncFunction(callback)) {
|
|
@@ -121,7 +114,7 @@ const createComputed = <T extends {}>(
|
|
|
121
114
|
() => {
|
|
122
115
|
computing = false
|
|
123
116
|
controller = undefined
|
|
124
|
-
|
|
117
|
+
watcher.run() // Retry computation with updated state
|
|
125
118
|
},
|
|
126
119
|
{
|
|
127
120
|
once: true,
|
|
@@ -144,7 +137,11 @@ const createComputed = <T extends {}>(
|
|
|
144
137
|
else if (null == result || UNSET === result) nil()
|
|
145
138
|
else ok(result)
|
|
146
139
|
computing = false
|
|
147
|
-
}
|
|
140
|
+
},
|
|
141
|
+
)
|
|
142
|
+
watcher.onCleanup(() => {
|
|
143
|
+
controller?.abort()
|
|
144
|
+
})
|
|
148
145
|
|
|
149
146
|
const computed: Record<PropertyKey, unknown> = {}
|
|
150
147
|
Object.defineProperties(computed, {
|
|
@@ -154,8 +151,8 @@ const createComputed = <T extends {}>(
|
|
|
154
151
|
get: {
|
|
155
152
|
value: (): T => {
|
|
156
153
|
subscribeActiveWatcher(watchers)
|
|
157
|
-
|
|
158
|
-
if (dirty)
|
|
154
|
+
flush()
|
|
155
|
+
if (dirty) watcher.run()
|
|
159
156
|
if (error) throw error
|
|
160
157
|
return value
|
|
161
158
|
},
|