@zeix/cause-effect 0.17.0 → 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 (50) hide show
  1. package/.ai-context.md +26 -5
  2. package/.cursorrules +8 -3
  3. package/.github/copilot-instructions.md +13 -4
  4. package/CLAUDE.md +191 -262
  5. package/README.md +268 -420
  6. package/archive/collection.ts +23 -25
  7. package/archive/computed.ts +5 -4
  8. package/archive/list.ts +21 -28
  9. package/archive/memo.ts +4 -2
  10. package/archive/state.ts +2 -1
  11. package/archive/store.ts +21 -32
  12. package/archive/task.ts +6 -9
  13. package/index.dev.js +411 -220
  14. package/index.js +1 -1
  15. package/index.ts +25 -8
  16. package/package.json +1 -1
  17. package/src/classes/collection.ts +103 -77
  18. package/src/classes/composite.ts +28 -33
  19. package/src/classes/computed.ts +90 -31
  20. package/src/classes/list.ts +39 -33
  21. package/src/classes/ref.ts +96 -0
  22. package/src/classes/state.ts +41 -8
  23. package/src/classes/store.ts +47 -30
  24. package/src/diff.ts +2 -1
  25. package/src/effect.ts +19 -9
  26. package/src/errors.ts +31 -1
  27. package/src/match.ts +5 -12
  28. package/src/resolve.ts +3 -2
  29. package/src/signal.ts +0 -1
  30. package/src/system.ts +159 -43
  31. package/src/util.ts +0 -10
  32. package/test/collection.test.ts +383 -67
  33. package/test/computed.test.ts +268 -11
  34. package/test/effect.test.ts +2 -2
  35. package/test/list.test.ts +249 -21
  36. package/test/ref.test.ts +381 -0
  37. package/test/state.test.ts +13 -13
  38. package/test/store.test.ts +473 -28
  39. package/types/index.d.ts +6 -5
  40. package/types/src/classes/collection.d.ts +27 -12
  41. package/types/src/classes/composite.d.ts +4 -4
  42. package/types/src/classes/computed.d.ts +17 -0
  43. package/types/src/classes/list.d.ts +6 -6
  44. package/types/src/classes/ref.d.ts +48 -0
  45. package/types/src/classes/state.d.ts +9 -0
  46. package/types/src/classes/store.d.ts +4 -4
  47. package/types/src/effect.d.ts +1 -2
  48. package/types/src/errors.d.ts +9 -1
  49. package/types/src/system.d.ts +40 -24
  50. package/types/src/util.d.ts +1 -3
@@ -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: {
@@ -1,15 +1,18 @@
1
1
  import { isEqual } from '../src/diff'
2
2
  import {
3
3
  CircularDependencyError,
4
+ createError,
4
5
  InvalidCallbackError,
5
6
  NullishSignalValueError,
6
7
  } from '../src/errors'
7
8
  import {
8
9
  createWatcher,
9
10
  flushPendingReactions,
11
+ HOOK_CLEANUP,
10
12
  notifyWatchers,
11
13
  subscribeActiveWatcher,
12
14
  trackSignalReads,
15
+ UNSET,
13
16
  type Watcher,
14
17
  } from '../src/system'
15
18
  import {
@@ -17,8 +20,6 @@ import {
17
20
  isAsyncFunction,
18
21
  isFunction,
19
22
  isObjectOfType,
20
- toError,
21
- UNSET,
22
23
  } from '../src/util'
23
24
 
24
25
  /* === Types === */
@@ -78,7 +79,7 @@ const createComputed = <T extends {}>(
78
79
  error = undefined
79
80
  }
80
81
  const err = (e: unknown): undefined => {
81
- const newError = toError(e)
82
+ const newError = createError(e)
82
83
  changed =
83
84
  !error ||
84
85
  newError.name !== error.name ||
@@ -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
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  CircularDependencyError,
3
+ createError,
3
4
  InvalidCallbackError,
4
5
  NullishSignalValueError,
5
6
  } from '../src/errors'
@@ -9,9 +10,10 @@ import {
9
10
  notifyWatchers,
10
11
  subscribeActiveWatcher,
11
12
  trackSignalReads,
13
+ UNSET,
12
14
  type Watcher,
13
15
  } from '../src/system'
14
- import { isObjectOfType, isSyncFunction, toError, UNSET } from '../src/util'
16
+ import { isObjectOfType, isSyncFunction } from '../src/util'
15
17
 
16
18
  /* === Types === */
17
19
 
@@ -69,7 +71,7 @@ const createMemo = <T extends {}>(
69
71
  } catch (e) {
70
72
  // Err track
71
73
  value = UNSET
72
- error = toError(e)
74
+ error = createError(e)
73
75
  computing = false
74
76
  return
75
77
  }
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()
package/archive/task.ts CHANGED
@@ -1,24 +1,21 @@
1
1
  import { isEqual } from '../src/diff'
2
2
  import {
3
3
  CircularDependencyError,
4
+ createError,
4
5
  InvalidCallbackError,
5
6
  NullishSignalValueError,
6
7
  } from '../src/errors'
7
8
  import {
8
9
  createWatcher,
9
10
  flushPendingReactions,
11
+ HOOK_CLEANUP,
10
12
  notifyWatchers,
11
13
  subscribeActiveWatcher,
12
14
  trackSignalReads,
15
+ UNSET,
13
16
  type Watcher,
14
17
  } from '../src/system'
15
- import {
16
- isAbortError,
17
- isAsyncFunction,
18
- isObjectOfType,
19
- toError,
20
- UNSET,
21
- } from '../src/util'
18
+ import { isAbortError, isAsyncFunction, isObjectOfType } from '../src/util'
22
19
 
23
20
  /* === Types === */
24
21
 
@@ -78,7 +75,7 @@ const createTask = <T extends {}>(
78
75
  error = undefined
79
76
  }
80
77
  const err = (e: unknown): undefined => {
81
- const newError = toError(e)
78
+ const newError = createError(e)
82
79
  changed =
83
80
  !error ||
84
81
  newError.name !== error.name ||
@@ -102,7 +99,7 @@ const createTask = <T extends {}>(
102
99
  if (watchers.size) notifyWatchers(watchers)
103
100
  else watcher.stop()
104
101
  })
105
- watcher.onCleanup(() => {
102
+ watcher.on(HOOK_CLEANUP, () => {
106
103
  controller?.abort()
107
104
  })
108
105