@zeix/cause-effect 0.17.1 → 0.17.2
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 +7 -0
- package/.github/copilot-instructions.md +4 -0
- package/CLAUDE.md +96 -1
- package/README.md +44 -7
- package/archive/collection.ts +23 -25
- package/archive/computed.ts +3 -2
- package/archive/list.ts +21 -28
- package/archive/memo.ts +2 -1
- package/archive/state.ts +2 -1
- package/archive/store.ts +21 -32
- package/archive/task.ts +4 -7
- package/index.dev.js +356 -198
- package/index.js +1 -1
- package/index.ts +15 -6
- package/package.json +1 -1
- package/src/classes/collection.ts +69 -53
- package/src/classes/composite.ts +28 -33
- package/src/classes/computed.ts +87 -28
- package/src/classes/list.ts +31 -26
- package/src/classes/ref.ts +33 -5
- package/src/classes/state.ts +41 -8
- package/src/classes/store.ts +47 -30
- package/src/diff.ts +2 -1
- package/src/effect.ts +19 -9
- package/src/errors.ts +10 -1
- package/src/resolve.ts +1 -1
- package/src/signal.ts +0 -1
- package/src/system.ts +159 -43
- package/src/util.ts +0 -6
- package/test/collection.test.ts +279 -20
- package/test/computed.test.ts +268 -11
- package/test/effect.test.ts +2 -2
- package/test/list.test.ts +249 -21
- package/test/ref.test.ts +154 -0
- package/test/state.test.ts +13 -13
- package/test/store.test.ts +473 -28
- package/types/index.d.ts +3 -3
- package/types/src/classes/collection.d.ts +8 -7
- package/types/src/classes/composite.d.ts +4 -4
- package/types/src/classes/computed.d.ts +17 -0
- package/types/src/classes/list.d.ts +2 -2
- package/types/src/classes/ref.d.ts +10 -1
- package/types/src/classes/state.d.ts +9 -0
- package/types/src/classes/store.d.ts +4 -4
- package/types/src/effect.d.ts +1 -2
- package/types/src/errors.d.ts +4 -1
- package/types/src/system.d.ts +40 -24
- package/types/src/util.d.ts +1 -2
package/.ai-context.md
CHANGED
|
@@ -279,6 +279,13 @@ 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 hooks
|
|
284
|
+
signal.on('watch', () => {
|
|
285
|
+
console.log('Setting up resource...')
|
|
286
|
+
const resource = createResource()
|
|
287
|
+
return () => resource.cleanup() // Called when no effects watch this signal
|
|
288
|
+
})
|
|
282
289
|
```
|
|
283
290
|
|
|
284
291
|
## 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 `.on('watch', callback)` 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
|
@@ -88,7 +88,7 @@ Key patterns:
|
|
|
88
88
|
|
|
89
89
|
**Composite Architecture**: Shared foundation for Store and List
|
|
90
90
|
- `Map<string, Signal>` for property/item signals
|
|
91
|
-
-
|
|
91
|
+
- Hook system for granular add/change/remove notifications
|
|
92
92
|
- Lazy signal creation and automatic cleanup
|
|
93
93
|
|
|
94
94
|
### Computed Signal Memoization Strategy
|
|
@@ -100,6 +100,101 @@ Computed signals implement smart memoization:
|
|
|
100
100
|
- **Error Handling**: Preserves error states and prevents cascade failures
|
|
101
101
|
- **Reducer Capabilities**: Access to previous value enables state accumulation and transitions
|
|
102
102
|
|
|
103
|
+
## Resource Management with Watch Hooks
|
|
104
|
+
|
|
105
|
+
All signals support the `watch` hook 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
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
// Basic watch hook pattern
|
|
109
|
+
const config = new State({ apiUrl: 'https://api.example.com' })
|
|
110
|
+
|
|
111
|
+
config.on('watch', () => {
|
|
112
|
+
console.log('Setting up API client...')
|
|
113
|
+
const client = new ApiClient(config.get().apiUrl)
|
|
114
|
+
|
|
115
|
+
return () => {
|
|
116
|
+
console.log('Cleaning up API client...')
|
|
117
|
+
client.disconnect()
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
// Resource is only created when effect runs
|
|
122
|
+
createEffect(() => {
|
|
123
|
+
console.log('API URL:', config.get().apiUrl) // Triggers hook
|
|
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')
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
// Accessing collection item triggers source item hook
|
|
178
|
+
createEffect(() => {
|
|
179
|
+
const value = doubled.at(0).get() // Triggers source item hook
|
|
180
|
+
})
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**Practical Use Cases**:
|
|
184
|
+
- Event listeners that activate only when data is watched
|
|
185
|
+
- Network connections established on-demand
|
|
186
|
+
- Expensive computations that pause when not needed
|
|
187
|
+
- External subscriptions (WebSocket, Server-Sent Events)
|
|
188
|
+
- Database connections tied to data access patterns
|
|
189
|
+
|
|
190
|
+
**Hook Lifecycle**:
|
|
191
|
+
1. First effect accesses signal → `watch` hook callback executed
|
|
192
|
+
2. Hook callback can return cleanup function
|
|
193
|
+
3. Last effect stops watching → cleanup function called
|
|
194
|
+
4. New effect accesses signal → hook callback executed again
|
|
195
|
+
|
|
196
|
+
This pattern enables **lazy resource allocation** - resources are only consumed when actually needed and automatically freed when no longer used.
|
|
197
|
+
|
|
103
198
|
## Advanced Patterns and Best Practices
|
|
104
199
|
|
|
105
200
|
### When to Use Each Signal Type
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Cause & Effect
|
|
2
2
|
|
|
3
|
-
Version 0.17.
|
|
3
|
+
Version 0.17.2
|
|
4
4
|
|
|
5
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.
|
|
6
6
|
|
|
@@ -168,7 +168,7 @@ settings.add('timeout', 5000)
|
|
|
168
168
|
settings.remove('timeout')
|
|
169
169
|
```
|
|
170
170
|
|
|
171
|
-
|
|
171
|
+
Subscribe to hooks using the `.on()` method:
|
|
172
172
|
|
|
173
173
|
```js
|
|
174
174
|
const user = createStore({ name: 'Alice', age: 30 })
|
|
@@ -177,17 +177,18 @@ const offChange = user.on('change', changed => console.log(changed))
|
|
|
177
177
|
const offAdd = user.on('add', added => console.log(added))
|
|
178
178
|
const offRemove = user.on('remove', removed => console.log(removed))
|
|
179
179
|
|
|
180
|
-
// These will trigger the respective
|
|
180
|
+
// These will trigger the respective hooks:
|
|
181
181
|
user.add('email', 'alice@example.com') // Logs: "Added properties: ['email']"
|
|
182
182
|
user.age.set(31) // Logs: "Changed properties: ['age']"
|
|
183
183
|
user.remove('email') // Logs: "Removed properties: ['email']"
|
|
184
|
+
```
|
|
184
185
|
|
|
185
|
-
To
|
|
186
|
+
To unregister hooks, call the returned cleanup functions:
|
|
186
187
|
|
|
187
188
|
```js
|
|
188
|
-
offAdd() // Stop listening to add
|
|
189
|
-
offChange() // Stop listening to change
|
|
190
|
-
offRemove() // Stop listening to remove
|
|
189
|
+
offAdd() // Stop listening to add hook
|
|
190
|
+
offChange() // Stop listening to change hook
|
|
191
|
+
offRemove() // Stop listening to remove hook
|
|
191
192
|
```
|
|
192
193
|
|
|
193
194
|
### List
|
|
@@ -398,6 +399,42 @@ cleanup() // Logs: 'Cleanup' and unsubscribes from signal `user`
|
|
|
398
399
|
user.set({ name: 'Bob', age: 28 }) // Won't trigger the effect anymore
|
|
399
400
|
```
|
|
400
401
|
|
|
402
|
+
### Resource Management with Hooks
|
|
403
|
+
|
|
404
|
+
All signals support the `watch` hook 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
|
+
|
|
406
|
+
```js
|
|
407
|
+
import { State, createEffect } from '@zeix/cause-effect'
|
|
408
|
+
|
|
409
|
+
const config = new State({ apiUrl: 'https://api.example.com' })
|
|
410
|
+
|
|
411
|
+
// Set up lazy resource management
|
|
412
|
+
config.on('watch', () => {
|
|
413
|
+
console.log('Setting up API client...')
|
|
414
|
+
const client = new ApiClient(config.get().apiUrl)
|
|
415
|
+
|
|
416
|
+
// Return cleanup function
|
|
417
|
+
return () => {
|
|
418
|
+
console.log('Cleaning up API client...')
|
|
419
|
+
client.disconnect()
|
|
420
|
+
}
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
// Resource is created only when effect runs
|
|
424
|
+
const cleanup = createEffect(() => {
|
|
425
|
+
console.log('API URL:', config.get().apiUrl)
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
// Resource is cleaned up when effect stops
|
|
429
|
+
cleanup()
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
This pattern is ideal for:
|
|
433
|
+
- Event listeners that should only be active when data is being watched
|
|
434
|
+
- Network connections that can be lazily established
|
|
435
|
+
- Expensive computations that should pause when not needed
|
|
436
|
+
- External subscriptions (WebSocket, Server-Sent Events, etc.)
|
|
437
|
+
|
|
401
438
|
### resolve()
|
|
402
439
|
|
|
403
440
|
Extract signal values:
|
package/archive/collection.ts
CHANGED
|
@@ -5,16 +5,17 @@ import type { Signal } from '../src/signal'
|
|
|
5
5
|
import {
|
|
6
6
|
type Cleanup,
|
|
7
7
|
createWatcher,
|
|
8
|
-
|
|
9
|
-
type
|
|
10
|
-
type
|
|
11
|
-
type
|
|
8
|
+
triggerHook,
|
|
9
|
+
type HookCallback,
|
|
10
|
+
type HookCallbacks,
|
|
11
|
+
type Hook,
|
|
12
12
|
notifyWatchers,
|
|
13
13
|
subscribeActiveWatcher,
|
|
14
14
|
trackSignalReads,
|
|
15
15
|
type Watcher,
|
|
16
|
+
UNSET,
|
|
16
17
|
} from '../src/system'
|
|
17
|
-
import { isAsyncFunction, isObjectOfType, isSymbol
|
|
18
|
+
import { isAsyncFunction, isObjectOfType, isSymbol } from '../src/util'
|
|
18
19
|
import { type Computed, createComputed } from './computed'
|
|
19
20
|
import type { List } from './list'
|
|
20
21
|
|
|
@@ -39,7 +40,7 @@ type Collection<T extends {}> = {
|
|
|
39
40
|
get(): T[]
|
|
40
41
|
keyAt(index: number): string | undefined
|
|
41
42
|
indexOfKey(key: string): number
|
|
42
|
-
on
|
|
43
|
+
on(type: Hook, callback: HookCallback): Cleanup
|
|
43
44
|
sort(compareFn?: (a: T, b: T) => number): void
|
|
44
45
|
}
|
|
45
46
|
|
|
@@ -66,12 +67,7 @@ const createCollection = <T extends {}, O extends {}>(
|
|
|
66
67
|
callback: CollectionCallback<T, O>,
|
|
67
68
|
): Collection<T> => {
|
|
68
69
|
const watchers = new Set<Watcher>()
|
|
69
|
-
const
|
|
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
|
-
}
|
|
70
|
+
const hookCallbacks: HookCallbacks = {}
|
|
75
71
|
const signals = new Map<string, Signal<T>>()
|
|
76
72
|
const signalWatchers = new Map<string, Watcher>()
|
|
77
73
|
|
|
@@ -121,7 +117,7 @@ const createCollection = <T extends {}, O extends {}>(
|
|
|
121
117
|
const watcher = createWatcher(() =>
|
|
122
118
|
trackSignalReads(watcher, () => {
|
|
123
119
|
signal.get() // Subscribe to the signal
|
|
124
|
-
|
|
120
|
+
triggerHook(hookCallbacks.change, [key])
|
|
125
121
|
}),
|
|
126
122
|
)
|
|
127
123
|
watcher()
|
|
@@ -152,25 +148,29 @@ const createCollection = <T extends {}, O extends {}>(
|
|
|
152
148
|
addProperty(key)
|
|
153
149
|
}
|
|
154
150
|
origin.on('add', additions => {
|
|
151
|
+
if (!additions?.length) return
|
|
155
152
|
for (const key of additions) {
|
|
156
153
|
if (!signals.has(key)) addProperty(key)
|
|
157
154
|
}
|
|
158
155
|
notifyWatchers(watchers)
|
|
159
|
-
|
|
156
|
+
triggerHook(hookCallbacks.add, additions)
|
|
160
157
|
})
|
|
161
158
|
origin.on('remove', removals => {
|
|
159
|
+
if (!removals?.length) return
|
|
162
160
|
for (const key of Object.keys(removals)) {
|
|
163
161
|
if (!signals.has(key)) continue
|
|
164
162
|
removeProperty(key)
|
|
165
163
|
}
|
|
166
164
|
order = order.filter(() => true) // Compact array
|
|
167
165
|
notifyWatchers(watchers)
|
|
168
|
-
|
|
166
|
+
triggerHook(hookCallbacks.remove, removals)
|
|
169
167
|
})
|
|
170
168
|
origin.on('sort', newOrder => {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
169
|
+
if (newOrder) {
|
|
170
|
+
order = [...newOrder]
|
|
171
|
+
notifyWatchers(watchers)
|
|
172
|
+
triggerHook(hookCallbacks.sort, newOrder)
|
|
173
|
+
}
|
|
174
174
|
})
|
|
175
175
|
|
|
176
176
|
// Get signal by key or index
|
|
@@ -247,16 +247,14 @@ const createCollection = <T extends {}, O extends {}>(
|
|
|
247
247
|
order = entries.map(([_, key]) => key)
|
|
248
248
|
|
|
249
249
|
notifyWatchers(watchers)
|
|
250
|
-
|
|
250
|
+
triggerHook(hookCallbacks.sort, order)
|
|
251
251
|
},
|
|
252
252
|
},
|
|
253
253
|
on: {
|
|
254
|
-
value:
|
|
255
|
-
type
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
listeners[type].add(listener)
|
|
259
|
-
return () => listeners[type].delete(listener)
|
|
254
|
+
value: (type: Hook, callback: HookCallback): Cleanup => {
|
|
255
|
+
hookCallbacks[type] ||= new Set()
|
|
256
|
+
hookCallbacks[type].add(callback)
|
|
257
|
+
return () => hookCallbacks[type]?.delete(callback)
|
|
260
258
|
},
|
|
261
259
|
},
|
|
262
260
|
length: {
|
package/archive/computed.ts
CHANGED
|
@@ -8,9 +8,11 @@ import {
|
|
|
8
8
|
import {
|
|
9
9
|
createWatcher,
|
|
10
10
|
flushPendingReactions,
|
|
11
|
+
HOOK_CLEANUP,
|
|
11
12
|
notifyWatchers,
|
|
12
13
|
subscribeActiveWatcher,
|
|
13
14
|
trackSignalReads,
|
|
15
|
+
UNSET,
|
|
14
16
|
type Watcher,
|
|
15
17
|
} from '../src/system'
|
|
16
18
|
import {
|
|
@@ -18,7 +20,6 @@ import {
|
|
|
18
20
|
isAsyncFunction,
|
|
19
21
|
isFunction,
|
|
20
22
|
isObjectOfType,
|
|
21
|
-
UNSET,
|
|
22
23
|
} from '../src/util'
|
|
23
24
|
|
|
24
25
|
/* === Types === */
|
|
@@ -102,7 +103,7 @@ const createComputed = <T extends {}>(
|
|
|
102
103
|
if (watchers.size) notifyWatchers(watchers)
|
|
103
104
|
else watcher.stop()
|
|
104
105
|
})
|
|
105
|
-
watcher.
|
|
106
|
+
watcher.on(HOOK_CLEANUP, () => {
|
|
106
107
|
controller?.abort()
|
|
107
108
|
})
|
|
108
109
|
|
package/archive/list.ts
CHANGED
|
@@ -10,13 +10,14 @@ import {
|
|
|
10
10
|
batchSignalWrites,
|
|
11
11
|
type Cleanup,
|
|
12
12
|
createWatcher,
|
|
13
|
-
|
|
14
|
-
type
|
|
15
|
-
type
|
|
16
|
-
type Notifications,
|
|
13
|
+
type Hook,
|
|
14
|
+
type HookCallback,
|
|
15
|
+
type HookCallbacks,
|
|
17
16
|
notifyWatchers,
|
|
18
17
|
subscribeActiveWatcher,
|
|
19
18
|
trackSignalReads,
|
|
19
|
+
triggerHook,
|
|
20
|
+
UNSET,
|
|
20
21
|
type Watcher,
|
|
21
22
|
} from '../src/system'
|
|
22
23
|
import {
|
|
@@ -26,7 +27,6 @@ import {
|
|
|
26
27
|
isRecord,
|
|
27
28
|
isString,
|
|
28
29
|
isSymbol,
|
|
29
|
-
UNSET,
|
|
30
30
|
} from '../src/util'
|
|
31
31
|
import {
|
|
32
32
|
type Collection,
|
|
@@ -63,7 +63,7 @@ type List<T extends {}> = {
|
|
|
63
63
|
update(fn: (value: T) => T): void
|
|
64
64
|
sort<U = T>(compareFn?: (a: U, b: U) => number): void
|
|
65
65
|
splice(start: number, deleteCount?: number, ...items: T[]): T[]
|
|
66
|
-
on
|
|
66
|
+
on(type: Hook, callback: HookCallback): Cleanup
|
|
67
67
|
remove(index: number): void
|
|
68
68
|
}
|
|
69
69
|
|
|
@@ -90,12 +90,7 @@ const createList = <T extends {}>(
|
|
|
90
90
|
if (initialValue == null) throw new NullishSignalValueError('store')
|
|
91
91
|
|
|
92
92
|
const watchers = new Set<Watcher>()
|
|
93
|
-
const
|
|
94
|
-
add: new Set<Listener<'add'>>(),
|
|
95
|
-
change: new Set<Listener<'change'>>(),
|
|
96
|
-
remove: new Set<Listener<'remove'>>(),
|
|
97
|
-
sort: new Set<Listener<'sort'>>(),
|
|
98
|
-
}
|
|
93
|
+
const hookCallbacks: HookCallbacks = {}
|
|
99
94
|
const signals = new Map<string, MutableSignal<T>>()
|
|
100
95
|
const ownWatchers = new Map<string, Watcher>()
|
|
101
96
|
|
|
@@ -163,7 +158,7 @@ const createList = <T extends {}>(
|
|
|
163
158
|
const watcher = createWatcher(() => {
|
|
164
159
|
trackSignalReads(watcher, () => {
|
|
165
160
|
signal.get() // Subscribe to the signal
|
|
166
|
-
|
|
161
|
+
triggerHook(hookCallbacks.change, [key])
|
|
167
162
|
})
|
|
168
163
|
})
|
|
169
164
|
ownWatchers.set(key, watcher)
|
|
@@ -191,11 +186,11 @@ const createList = <T extends {}>(
|
|
|
191
186
|
signals.set(key, signal)
|
|
192
187
|
if (!order.includes(key)) order.push(key)
|
|
193
188
|
// @ts-expect-error ignore
|
|
194
|
-
if (
|
|
189
|
+
if (hookCallbacks.change?.size) addOwnWatcher(key, signal)
|
|
195
190
|
|
|
196
191
|
if (single) {
|
|
197
192
|
notifyWatchers(watchers)
|
|
198
|
-
|
|
193
|
+
triggerHook(hookCallbacks.add, [key])
|
|
199
194
|
}
|
|
200
195
|
return true
|
|
201
196
|
}
|
|
@@ -218,7 +213,7 @@ const createList = <T extends {}>(
|
|
|
218
213
|
if (single) {
|
|
219
214
|
order = order.filter(() => true) // Compact array
|
|
220
215
|
notifyWatchers(watchers)
|
|
221
|
-
|
|
216
|
+
triggerHook(hookCallbacks.remove, [key])
|
|
222
217
|
}
|
|
223
218
|
}
|
|
224
219
|
|
|
@@ -233,9 +228,9 @@ const createList = <T extends {}>(
|
|
|
233
228
|
// Queue initial additions event to allow listeners to be added first
|
|
234
229
|
if (initialRun)
|
|
235
230
|
setTimeout(() => {
|
|
236
|
-
|
|
231
|
+
triggerHook(hookCallbacks.add, Object.keys(changes.add))
|
|
237
232
|
}, 0)
|
|
238
|
-
else
|
|
233
|
+
else triggerHook(hookCallbacks.add, Object.keys(changes.add))
|
|
239
234
|
}
|
|
240
235
|
|
|
241
236
|
// Changes
|
|
@@ -249,7 +244,7 @@ const createList = <T extends {}>(
|
|
|
249
244
|
if (isMutableSignal(signal)) signal.set(value)
|
|
250
245
|
else throw new ReadonlySignalError(key, value)
|
|
251
246
|
}
|
|
252
|
-
|
|
247
|
+
triggerHook(hookCallbacks.change, Object.keys(changes.change))
|
|
253
248
|
})
|
|
254
249
|
}
|
|
255
250
|
|
|
@@ -257,7 +252,7 @@ const createList = <T extends {}>(
|
|
|
257
252
|
if (Object.keys(changes.remove).length) {
|
|
258
253
|
for (const key in changes.remove) removeProperty(key)
|
|
259
254
|
order = order.filter(() => true)
|
|
260
|
-
|
|
255
|
+
triggerHook(hookCallbacks.remove, Object.keys(changes.remove))
|
|
261
256
|
}
|
|
262
257
|
|
|
263
258
|
return changes.changed
|
|
@@ -401,7 +396,7 @@ const createList = <T extends {}>(
|
|
|
401
396
|
if (!isEqual(newOrder, order)) {
|
|
402
397
|
order = newOrder
|
|
403
398
|
notifyWatchers(watchers)
|
|
404
|
-
|
|
399
|
+
triggerHook(hookCallbacks.sort, order)
|
|
405
400
|
}
|
|
406
401
|
},
|
|
407
402
|
},
|
|
@@ -473,18 +468,16 @@ const createList = <T extends {}>(
|
|
|
473
468
|
},
|
|
474
469
|
},
|
|
475
470
|
on: {
|
|
476
|
-
value:
|
|
477
|
-
type
|
|
478
|
-
|
|
479
|
-
): Cleanup => {
|
|
480
|
-
listeners[type].add(listener)
|
|
471
|
+
value: (type: Hook, callback: HookCallback): Cleanup => {
|
|
472
|
+
hookCallbacks[type] ||= new Set()
|
|
473
|
+
hookCallbacks[type].add(callback)
|
|
481
474
|
if (type === 'change' && !ownWatchers.size) {
|
|
482
475
|
for (const [key, signal] of signals)
|
|
483
476
|
addOwnWatcher(key, signal)
|
|
484
477
|
}
|
|
485
478
|
return () => {
|
|
486
|
-
|
|
487
|
-
if (type === 'change' && !
|
|
479
|
+
hookCallbacks[type]?.delete(callback)
|
|
480
|
+
if (type === 'change' && !hookCallbacks.change?.size) {
|
|
488
481
|
if (ownWatchers.size) {
|
|
489
482
|
for (const watcher of ownWatchers.values())
|
|
490
483
|
watcher.stop()
|
package/archive/memo.ts
CHANGED
|
@@ -10,9 +10,10 @@ import {
|
|
|
10
10
|
notifyWatchers,
|
|
11
11
|
subscribeActiveWatcher,
|
|
12
12
|
trackSignalReads,
|
|
13
|
+
UNSET,
|
|
13
14
|
type Watcher,
|
|
14
15
|
} from '../src/system'
|
|
15
|
-
import { isObjectOfType, isSyncFunction
|
|
16
|
+
import { isObjectOfType, isSyncFunction } from '../src/util'
|
|
16
17
|
|
|
17
18
|
/* === Types === */
|
|
18
19
|
|
package/archive/state.ts
CHANGED
|
@@ -3,9 +3,10 @@ import { InvalidCallbackError, NullishSignalValueError } from '../src/errors'
|
|
|
3
3
|
import {
|
|
4
4
|
notifyWatchers,
|
|
5
5
|
subscribeActiveWatcher,
|
|
6
|
+
UNSET,
|
|
6
7
|
type Watcher,
|
|
7
8
|
} from '../src/system'
|
|
8
|
-
import { isFunction, isObjectOfType
|
|
9
|
+
import { isFunction, isObjectOfType } from '../src/util'
|
|
9
10
|
|
|
10
11
|
/* === Types === */
|
|
11
12
|
|
package/archive/store.ts
CHANGED
|
@@ -10,22 +10,17 @@ import {
|
|
|
10
10
|
batchSignalWrites,
|
|
11
11
|
type Cleanup,
|
|
12
12
|
createWatcher,
|
|
13
|
-
|
|
14
|
-
type
|
|
15
|
-
type
|
|
16
|
-
type Notifications,
|
|
13
|
+
type HookCallback,
|
|
14
|
+
type HookCallbacks,
|
|
15
|
+
type Hook,
|
|
17
16
|
notifyWatchers,
|
|
18
17
|
subscribeActiveWatcher,
|
|
19
18
|
trackSignalReads,
|
|
20
19
|
type Watcher,
|
|
21
|
-
} from '../src/system'
|
|
22
|
-
import {
|
|
23
|
-
isFunction,
|
|
24
|
-
isObjectOfType,
|
|
25
|
-
isRecord,
|
|
26
|
-
isSymbol,
|
|
27
20
|
UNSET,
|
|
28
|
-
|
|
21
|
+
triggerHook,
|
|
22
|
+
} from '../src/system'
|
|
23
|
+
import { isFunction, isObjectOfType, isRecord, isSymbol } from '../src/util'
|
|
29
24
|
import { isComputed } from './computed'
|
|
30
25
|
import { createList, isList, type List } from './list'
|
|
31
26
|
import { createState, isState, type State } from './state'
|
|
@@ -62,7 +57,7 @@ type Store<T extends UnknownRecord> = {
|
|
|
62
57
|
sort<U = T[Extract<keyof T, string>]>(
|
|
63
58
|
compareFn?: (a: U, b: U) => number,
|
|
64
59
|
): void
|
|
65
|
-
on
|
|
60
|
+
on(type: Hook, callback: HookCallback): Cleanup
|
|
66
61
|
remove<K extends Extract<keyof T, string>>(key: K): void
|
|
67
62
|
}
|
|
68
63
|
|
|
@@ -92,11 +87,7 @@ const createStore = <T extends UnknownRecord>(initialValue: T): Store<T> => {
|
|
|
92
87
|
if (initialValue == null) throw new NullishSignalValueError('store')
|
|
93
88
|
|
|
94
89
|
const watchers = new Set<Watcher>()
|
|
95
|
-
const
|
|
96
|
-
add: new Set<Listener<'add'>>(),
|
|
97
|
-
change: new Set<Listener<'change'>>(),
|
|
98
|
-
remove: new Set<Listener<'remove'>>(),
|
|
99
|
-
}
|
|
90
|
+
const hookCallbacks: HookCallbacks = {}
|
|
100
91
|
const signals = new Map<
|
|
101
92
|
string,
|
|
102
93
|
MutableSignal<T[Extract<keyof T, string>] & {}>
|
|
@@ -131,7 +122,7 @@ const createStore = <T extends UnknownRecord>(initialValue: T): Store<T> => {
|
|
|
131
122
|
const watcher = createWatcher(() => {
|
|
132
123
|
trackSignalReads(watcher, () => {
|
|
133
124
|
signal.get() // Subscribe to the signal
|
|
134
|
-
|
|
125
|
+
triggerHook(hookCallbacks.change, [key])
|
|
135
126
|
})
|
|
136
127
|
})
|
|
137
128
|
ownWatchers.set(key, watcher)
|
|
@@ -159,11 +150,11 @@ const createStore = <T extends UnknownRecord>(initialValue: T): Store<T> => {
|
|
|
159
150
|
// Set internal states
|
|
160
151
|
// @ts-expect-error non-matching signal types
|
|
161
152
|
signals.set(key, signal)
|
|
162
|
-
if (
|
|
153
|
+
if (hookCallbacks.change?.size) addOwnWatcher(key, signal)
|
|
163
154
|
|
|
164
155
|
if (single) {
|
|
165
156
|
notifyWatchers(watchers)
|
|
166
|
-
|
|
157
|
+
triggerHook(hookCallbacks.add, [key])
|
|
167
158
|
}
|
|
168
159
|
return true
|
|
169
160
|
}
|
|
@@ -183,7 +174,7 @@ const createStore = <T extends UnknownRecord>(initialValue: T): Store<T> => {
|
|
|
183
174
|
|
|
184
175
|
if (single) {
|
|
185
176
|
notifyWatchers(watchers)
|
|
186
|
-
|
|
177
|
+
triggerHook(hookCallbacks.remove, [key])
|
|
187
178
|
}
|
|
188
179
|
}
|
|
189
180
|
|
|
@@ -201,9 +192,9 @@ const createStore = <T extends UnknownRecord>(initialValue: T): Store<T> => {
|
|
|
201
192
|
// Queue initial additions event to allow listeners to be added first
|
|
202
193
|
if (initialRun)
|
|
203
194
|
setTimeout(() => {
|
|
204
|
-
|
|
195
|
+
triggerHook(hookCallbacks.add, Object.keys(changes.add))
|
|
205
196
|
}, 0)
|
|
206
|
-
else
|
|
197
|
+
else triggerHook(hookCallbacks.add, Object.keys(changes.add))
|
|
207
198
|
}
|
|
208
199
|
|
|
209
200
|
// Changes
|
|
@@ -220,14 +211,14 @@ const createStore = <T extends UnknownRecord>(initialValue: T): Store<T> => {
|
|
|
220
211
|
if (isMutableSignal(signal)) signal.set(value)
|
|
221
212
|
else throw new ReadonlySignalError(key, value)
|
|
222
213
|
}
|
|
223
|
-
|
|
214
|
+
triggerHook(hookCallbacks.change, Object.keys(changes.change))
|
|
224
215
|
})
|
|
225
216
|
}
|
|
226
217
|
|
|
227
218
|
// Removals
|
|
228
219
|
if (Object.keys(changes.remove).length) {
|
|
229
220
|
for (const key in changes.remove) removeProperty(key)
|
|
230
|
-
|
|
221
|
+
triggerHook(hookCallbacks.remove, Object.keys(changes.remove))
|
|
231
222
|
}
|
|
232
223
|
|
|
233
224
|
return changes.changed
|
|
@@ -296,19 +287,17 @@ const createStore = <T extends UnknownRecord>(initialValue: T): Store<T> => {
|
|
|
296
287
|
},
|
|
297
288
|
},
|
|
298
289
|
on: {
|
|
299
|
-
value:
|
|
300
|
-
type
|
|
301
|
-
|
|
302
|
-
): Cleanup => {
|
|
303
|
-
listeners[type].add(listener)
|
|
290
|
+
value: (type: Hook, callback: HookCallback): Cleanup => {
|
|
291
|
+
hookCallbacks[type] ||= new Set()
|
|
292
|
+
hookCallbacks[type].add(callback)
|
|
304
293
|
if (type === 'change' && !ownWatchers.size) {
|
|
305
294
|
for (const [key, signal] of signals)
|
|
306
295
|
// @ts-expect-error ignore
|
|
307
296
|
addOwnWatcher(key, signal)
|
|
308
297
|
}
|
|
309
298
|
return () => {
|
|
310
|
-
|
|
311
|
-
if (type === 'change' && !
|
|
299
|
+
hookCallbacks[type]?.delete(callback)
|
|
300
|
+
if (type === 'change' && !hookCallbacks.change?.size) {
|
|
312
301
|
if (ownWatchers.size) {
|
|
313
302
|
for (const watcher of ownWatchers.values())
|
|
314
303
|
watcher.stop()
|