@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.
Files changed (48) hide show
  1. package/.ai-context.md +7 -0
  2. package/.github/copilot-instructions.md +4 -0
  3. package/CLAUDE.md +96 -1
  4. package/README.md +44 -7
  5. package/archive/collection.ts +23 -25
  6. package/archive/computed.ts +3 -2
  7. package/archive/list.ts +21 -28
  8. package/archive/memo.ts +2 -1
  9. package/archive/state.ts +2 -1
  10. package/archive/store.ts +21 -32
  11. package/archive/task.ts +4 -7
  12. package/index.dev.js +356 -198
  13. package/index.js +1 -1
  14. package/index.ts +15 -6
  15. package/package.json +1 -1
  16. package/src/classes/collection.ts +69 -53
  17. package/src/classes/composite.ts +28 -33
  18. package/src/classes/computed.ts +87 -28
  19. package/src/classes/list.ts +31 -26
  20. package/src/classes/ref.ts +33 -5
  21. package/src/classes/state.ts +41 -8
  22. package/src/classes/store.ts +47 -30
  23. package/src/diff.ts +2 -1
  24. package/src/effect.ts +19 -9
  25. package/src/errors.ts +10 -1
  26. package/src/resolve.ts +1 -1
  27. package/src/signal.ts +0 -1
  28. package/src/system.ts +159 -43
  29. package/src/util.ts +0 -6
  30. package/test/collection.test.ts +279 -20
  31. package/test/computed.test.ts +268 -11
  32. package/test/effect.test.ts +2 -2
  33. package/test/list.test.ts +249 -21
  34. package/test/ref.test.ts +154 -0
  35. package/test/state.test.ts +13 -13
  36. package/test/store.test.ts +473 -28
  37. package/types/index.d.ts +3 -3
  38. package/types/src/classes/collection.d.ts +8 -7
  39. package/types/src/classes/composite.d.ts +4 -4
  40. package/types/src/classes/computed.d.ts +17 -0
  41. package/types/src/classes/list.d.ts +2 -2
  42. package/types/src/classes/ref.d.ts +10 -1
  43. package/types/src/classes/state.d.ts +9 -0
  44. package/types/src/classes/store.d.ts +4 -4
  45. package/types/src/effect.d.ts +1 -2
  46. package/types/src/errors.d.ts +4 -1
  47. package/types/src/system.d.ts +40 -24
  48. 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
- - Event system for granular add/change/remove notifications
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.1
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
- Change Notifications using the `.on()` method:
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 notifications:
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 stop listening to notifications, call the returned cleanup functions:
186
+ To unregister hooks, call the returned cleanup functions:
186
187
 
187
188
  ```js
188
- offAdd() // Stop listening to add notifications
189
- offChange() // Stop listening to change notifications
190
- offRemove() // Stop listening to remove notifications
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:
@@ -5,16 +5,17 @@ import type { Signal } from '../src/signal'
5
5
  import {
6
6
  type Cleanup,
7
7
  createWatcher,
8
- emitNotification,
9
- type Listener,
10
- type Listeners,
11
- type Notifications,
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, UNSET } from '../src/util'
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<K extends keyof Notifications>(type: K, listener: Listener<K>): Cleanup
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 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
- }
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
- emitNotification(listeners.change, [key])
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
- emitNotification(listeners.add, additions)
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
- emitNotification(listeners.remove, removals)
166
+ triggerHook(hookCallbacks.remove, removals)
169
167
  })
170
168
  origin.on('sort', newOrder => {
171
- order = [...newOrder]
172
- notifyWatchers(watchers)
173
- emitNotification(listeners.sort, newOrder)
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
- emitNotification(listeners.sort, order)
250
+ triggerHook(hookCallbacks.sort, order)
251
251
  },
252
252
  },
253
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)
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: {
@@ -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.onCleanup(() => {
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
- emitNotification,
14
- type Listener,
15
- type Listeners,
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<K extends keyof Notifications>(type: K, listener: Listener<K>): Cleanup
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 listeners: Listeners = {
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
- emitNotification(listeners.change, [key])
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 (listeners.change.size) addOwnWatcher(key, signal)
189
+ if (hookCallbacks.change?.size) addOwnWatcher(key, signal)
195
190
 
196
191
  if (single) {
197
192
  notifyWatchers(watchers)
198
- emitNotification(listeners.add, [key])
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
- emitNotification(listeners.remove, [key])
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
- emitNotification(listeners.add, Object.keys(changes.add))
231
+ triggerHook(hookCallbacks.add, Object.keys(changes.add))
237
232
  }, 0)
238
- else emitNotification(listeners.add, Object.keys(changes.add))
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
- emitNotification(listeners.change, Object.keys(changes.change))
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
- emitNotification(listeners.remove, Object.keys(changes.remove))
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
- emitNotification(listeners.sort, order)
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: <K extends keyof Notifications>(
477
- type: K,
478
- listener: Listener<K>,
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
- listeners[type].delete(listener)
487
- if (type === 'change' && !listeners.change.size) {
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, UNSET } from '../src/util'
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, UNSET } from '../src/util'
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
- emitNotification,
14
- type Listener,
15
- type Listeners,
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
- } from '../src/util'
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<K extends keyof Notifications>(type: K, listener: Listener<K>): Cleanup
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 listeners: Omit<Listeners, 'sort'> = {
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
- emitNotification(listeners.change, [key])
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 (listeners.change.size) addOwnWatcher(key, signal)
153
+ if (hookCallbacks.change?.size) addOwnWatcher(key, signal)
163
154
 
164
155
  if (single) {
165
156
  notifyWatchers(watchers)
166
- emitNotification(listeners.add, [key])
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
- emitNotification(listeners.remove, [key])
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
- emitNotification(listeners.add, Object.keys(changes.add))
195
+ triggerHook(hookCallbacks.add, Object.keys(changes.add))
205
196
  }, 0)
206
- else emitNotification(listeners.add, Object.keys(changes.add))
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
- emitNotification(listeners.change, Object.keys(changes.change))
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
- emitNotification(listeners.remove, Object.keys(changes.remove))
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: <K extends keyof Omit<Listeners, 'sort'>>(
300
- type: K,
301
- listener: Listener<K>,
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
- listeners[type].delete(listener)
311
- if (type === 'change' && !listeners.change.size) {
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()