@zeix/cause-effect 0.17.1 → 0.17.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/.ai-context.md +13 -0
  2. package/.github/copilot-instructions.md +4 -0
  3. package/.zed/settings.json +3 -0
  4. package/CLAUDE.md +41 -7
  5. package/README.md +48 -25
  6. package/archive/benchmark.ts +0 -5
  7. package/archive/collection.ts +6 -65
  8. package/archive/composite.ts +85 -0
  9. package/archive/computed.ts +18 -20
  10. package/archive/list.ts +7 -75
  11. package/archive/memo.ts +15 -15
  12. package/archive/state.ts +2 -1
  13. package/archive/store.ts +8 -78
  14. package/archive/task.ts +20 -25
  15. package/index.dev.js +508 -526
  16. package/index.js +1 -1
  17. package/index.ts +9 -11
  18. package/package.json +6 -6
  19. package/src/classes/collection.ts +70 -107
  20. package/src/classes/computed.ts +165 -149
  21. package/src/classes/list.ts +145 -107
  22. package/src/classes/ref.ts +19 -17
  23. package/src/classes/state.ts +21 -17
  24. package/src/classes/store.ts +125 -73
  25. package/src/diff.ts +2 -1
  26. package/src/effect.ts +17 -10
  27. package/src/errors.ts +14 -1
  28. package/src/resolve.ts +1 -1
  29. package/src/signal.ts +3 -2
  30. package/src/system.ts +159 -61
  31. package/src/util.ts +0 -6
  32. package/test/batch.test.ts +4 -11
  33. package/test/benchmark.test.ts +4 -2
  34. package/test/collection.test.ts +106 -107
  35. package/test/computed.test.ts +351 -112
  36. package/test/effect.test.ts +2 -2
  37. package/test/list.test.ts +62 -102
  38. package/test/ref.test.ts +128 -2
  39. package/test/state.test.ts +16 -22
  40. package/test/store.test.ts +101 -108
  41. package/test/util/dependency-graph.ts +2 -2
  42. package/tsconfig.build.json +11 -0
  43. package/tsconfig.json +5 -7
  44. package/types/index.d.ts +3 -3
  45. package/types/src/classes/collection.d.ts +9 -10
  46. package/types/src/classes/computed.d.ts +17 -20
  47. package/types/src/classes/list.d.ts +8 -6
  48. package/types/src/classes/ref.d.ts +8 -12
  49. package/types/src/classes/state.d.ts +5 -8
  50. package/types/src/classes/store.d.ts +14 -13
  51. package/types/src/effect.d.ts +1 -2
  52. package/types/src/errors.d.ts +2 -1
  53. package/types/src/signal.d.ts +3 -2
  54. package/types/src/system.d.ts +47 -34
  55. package/types/src/util.d.ts +1 -2
  56. package/src/classes/composite.ts +0 -176
  57. package/types/src/classes/composite.d.ts +0 -15
package/archive/list.ts CHANGED
@@ -7,16 +7,10 @@ import {
7
7
  } from '../src/errors'
8
8
  import { isMutableSignal, type MutableSignal } from '../src/signal'
9
9
  import {
10
- batchSignalWrites,
11
- type Cleanup,
12
- createWatcher,
13
- emitNotification,
14
- type Listener,
15
- type Listeners,
16
- type Notifications,
10
+ batch,
17
11
  notifyWatchers,
18
12
  subscribeActiveWatcher,
19
- trackSignalReads,
13
+ UNSET,
20
14
  type Watcher,
21
15
  } from '../src/system'
22
16
  import {
@@ -26,7 +20,6 @@ import {
26
20
  isRecord,
27
21
  isString,
28
22
  isSymbol,
29
- UNSET,
30
23
  } from '../src/util'
31
24
  import {
32
25
  type Collection,
@@ -63,7 +56,6 @@ type List<T extends {}> = {
63
56
  update(fn: (value: T) => T): void
64
57
  sort<U = T>(compareFn?: (a: U, b: U) => number): void
65
58
  splice(start: number, deleteCount?: number, ...items: T[]): T[]
66
- on<K extends keyof Notifications>(type: K, listener: Listener<K>): Cleanup
67
59
  remove(index: number): void
68
60
  }
69
61
 
@@ -90,12 +82,6 @@ const createList = <T extends {}>(
90
82
  if (initialValue == null) throw new NullishSignalValueError('store')
91
83
 
92
84
  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
- }
99
85
  const signals = new Map<string, MutableSignal<T>>()
100
86
  const ownWatchers = new Map<string, Watcher>()
101
87
 
@@ -158,17 +144,6 @@ const createList = <T extends {}>(
158
144
  return true
159
145
  }
160
146
 
161
- // Add own watcher for nested signal
162
- const addOwnWatcher = (key: string, signal: MutableSignal<T>) => {
163
- const watcher = createWatcher(() => {
164
- trackSignalReads(watcher, () => {
165
- signal.get() // Subscribe to the signal
166
- emitNotification(listeners.change, [key])
167
- })
168
- })
169
- ownWatchers.set(key, watcher)
170
- }
171
-
172
147
  // Add nested signal and own watcher
173
148
  const addProperty = <K extends keyof T & string>(
174
149
  key: K,
@@ -190,12 +165,9 @@ const createList = <T extends {}>(
190
165
  // @ts-expect-error ignore
191
166
  signals.set(key, signal)
192
167
  if (!order.includes(key)) order.push(key)
193
- // @ts-expect-error ignore
194
- if (listeners.change.size) addOwnWatcher(key, signal)
195
168
 
196
169
  if (single) {
197
170
  notifyWatchers(watchers)
198
- emitNotification(listeners.add, [key])
199
171
  }
200
172
  return true
201
173
  }
@@ -218,29 +190,21 @@ const createList = <T extends {}>(
218
190
  if (single) {
219
191
  order = order.filter(() => true) // Compact array
220
192
  notifyWatchers(watchers)
221
- emitNotification(listeners.remove, [key])
222
193
  }
223
194
  }
224
195
 
225
196
  // Commit batched changes and emit notifications
226
- const batchChanges = (changes: DiffResult, initialRun?: boolean) => {
197
+ const batchChanges = (changes: DiffResult) => {
227
198
  // Additions
228
199
  if (Object.keys(changes.add).length) {
229
200
  for (const key in changes.add)
230
201
  // @ts-expect-error ignore
231
202
  addProperty(key, changes.add[key] as T, false)
232
-
233
- // Queue initial additions event to allow listeners to be added first
234
- if (initialRun)
235
- setTimeout(() => {
236
- emitNotification(listeners.add, Object.keys(changes.add))
237
- }, 0)
238
- else emitNotification(listeners.add, Object.keys(changes.add))
239
203
  }
240
204
 
241
205
  // Changes
242
206
  if (Object.keys(changes.change).length) {
243
- batchSignalWrites(() => {
207
+ batch(() => {
244
208
  for (const key in changes.change) {
245
209
  const value = changes.change[key] as T
246
210
  if (!isValidValue(key, value)) continue
@@ -249,7 +213,6 @@ const createList = <T extends {}>(
249
213
  if (isMutableSignal(signal)) signal.set(value)
250
214
  else throw new ReadonlySignalError(key, value)
251
215
  }
252
- emitNotification(listeners.change, Object.keys(changes.change))
253
216
  })
254
217
  }
255
218
 
@@ -257,25 +220,17 @@ const createList = <T extends {}>(
257
220
  if (Object.keys(changes.remove).length) {
258
221
  for (const key in changes.remove) removeProperty(key)
259
222
  order = order.filter(() => true)
260
- emitNotification(listeners.remove, Object.keys(changes.remove))
261
223
  }
262
224
 
263
225
  return changes.changed
264
226
  }
265
227
 
266
228
  // Reconcile data and dispatch events
267
- const reconcile = (
268
- oldValue: T[],
269
- newValue: T[],
270
- initialRun?: boolean,
271
- ): boolean =>
272
- batchChanges(
273
- diff(arrayToRecord(oldValue), arrayToRecord(newValue)),
274
- initialRun,
275
- )
229
+ const reconcile = (oldValue: T[], newValue: T[]): boolean =>
230
+ batchChanges(diff(arrayToRecord(oldValue), arrayToRecord(newValue)))
276
231
 
277
232
  // Initialize data
278
- reconcile([] as T[], initialValue, true)
233
+ reconcile([] as T[], initialValue)
279
234
 
280
235
  // Methods and Properties
281
236
  const prototype: Record<PropertyKey, unknown> = {}
@@ -401,7 +356,6 @@ const createList = <T extends {}>(
401
356
  if (!isEqual(newOrder, order)) {
402
357
  order = newOrder
403
358
  notifyWatchers(watchers)
404
- emitNotification(listeners.sort, order)
405
359
  }
406
360
  },
407
361
  },
@@ -472,28 +426,6 @@ const createList = <T extends {}>(
472
426
  return Object.values(remove) as T[]
473
427
  },
474
428
  },
475
- on: {
476
- value: <K extends keyof Notifications>(
477
- type: K,
478
- listener: Listener<K>,
479
- ): Cleanup => {
480
- listeners[type].add(listener)
481
- if (type === 'change' && !ownWatchers.size) {
482
- for (const [key, signal] of signals)
483
- addOwnWatcher(key, signal)
484
- }
485
- return () => {
486
- listeners[type].delete(listener)
487
- if (type === 'change' && !listeners.change.size) {
488
- if (ownWatchers.size) {
489
- for (const watcher of ownWatchers.values())
490
- watcher.stop()
491
- ownWatchers.clear()
492
- }
493
- }
494
- }
495
- },
496
- },
497
429
  })
498
430
 
499
431
  // Return proxy directly with integrated signal methods
package/archive/memo.ts CHANGED
@@ -6,13 +6,13 @@ import {
6
6
  } from '../src/errors'
7
7
  import {
8
8
  createWatcher,
9
- flushPendingReactions,
9
+ flush,
10
10
  notifyWatchers,
11
11
  subscribeActiveWatcher,
12
- trackSignalReads,
12
+ UNSET,
13
13
  type Watcher,
14
14
  } from '../src/system'
15
- import { isObjectOfType, isSyncFunction, UNSET } from '../src/util'
15
+ import { isObjectOfType, isSyncFunction } from '../src/util'
16
16
 
17
17
  /* === Types === */
18
18
 
@@ -53,15 +53,13 @@ const createMemo = <T extends {}>(
53
53
  let computing = false
54
54
 
55
55
  // Own watcher: called when notified from sources (push)
56
- const watcher = createWatcher(() => {
57
- dirty = true
58
- if (watchers.size) notifyWatchers(watchers)
59
- else watcher.stop()
60
- })
61
-
62
- // Called when requested by dependencies (pull)
63
- const compute = () =>
64
- trackSignalReads(watcher, () => {
56
+ const watcher = createWatcher(
57
+ () => {
58
+ dirty = true
59
+ if (watchers.size) notifyWatchers(watchers)
60
+ else watcher.stop()
61
+ },
62
+ () => {
65
63
  if (computing) throw new CircularDependencyError('memo')
66
64
  let result: T
67
65
  computing = true
@@ -86,7 +84,8 @@ const createMemo = <T extends {}>(
86
84
  dirty = false
87
85
  }
88
86
  computing = false
89
- })
87
+ },
88
+ )
90
89
 
91
90
  const memo: Record<PropertyKey, unknown> = {}
92
91
  Object.defineProperties(memo, {
@@ -96,8 +95,9 @@ const createMemo = <T extends {}>(
96
95
  get: {
97
96
  value: (): T => {
98
97
  subscribeActiveWatcher(watchers)
99
- flushPendingReactions()
100
- if (dirty) compute()
98
+ flush()
99
+
100
+ if (dirty) watcher.run()
101
101
  if (error) throw error
102
102
  return value
103
103
  },
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
@@ -7,25 +7,13 @@ import {
7
7
  } from '../src/errors'
8
8
  import { isMutableSignal, type MutableSignal } from '../src/signal'
9
9
  import {
10
- batchSignalWrites,
11
- type Cleanup,
12
- createWatcher,
13
- emitNotification,
14
- type Listener,
15
- type Listeners,
16
- type Notifications,
10
+ batch,
17
11
  notifyWatchers,
18
12
  subscribeActiveWatcher,
19
- trackSignalReads,
13
+ UNSET,
20
14
  type Watcher,
21
15
  } from '../src/system'
22
- import {
23
- isFunction,
24
- isObjectOfType,
25
- isRecord,
26
- isSymbol,
27
- UNSET,
28
- } from '../src/util'
16
+ import { isFunction, isObjectOfType, isRecord, isSymbol } from '../src/util'
29
17
  import { isComputed } from './computed'
30
18
  import { createList, isList, type List } from './list'
31
19
  import { createState, isState, type State } from './state'
@@ -62,7 +50,6 @@ type Store<T extends UnknownRecord> = {
62
50
  sort<U = T[Extract<keyof T, string>]>(
63
51
  compareFn?: (a: U, b: U) => number,
64
52
  ): void
65
- on<K extends keyof Notifications>(type: K, listener: Listener<K>): Cleanup
66
53
  remove<K extends Extract<keyof T, string>>(key: K): void
67
54
  }
68
55
 
@@ -92,11 +79,6 @@ const createStore = <T extends UnknownRecord>(initialValue: T): Store<T> => {
92
79
  if (initialValue == null) throw new NullishSignalValueError('store')
93
80
 
94
81
  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
- }
100
82
  const signals = new Map<
101
83
  string,
102
84
  MutableSignal<T[Extract<keyof T, string>] & {}>
@@ -123,20 +105,6 @@ const createStore = <T extends UnknownRecord>(initialValue: T): Store<T> => {
123
105
  return true
124
106
  }
125
107
 
126
- // Add own watcher for nested signal
127
- const addOwnWatcher = <K extends keyof T & string>(
128
- key: K,
129
- signal: MutableSignal<T[K] & {}>,
130
- ) => {
131
- const watcher = createWatcher(() => {
132
- trackSignalReads(watcher, () => {
133
- signal.get() // Subscribe to the signal
134
- emitNotification(listeners.change, [key])
135
- })
136
- })
137
- ownWatchers.set(key, watcher)
138
- }
139
-
140
108
  // Add nested signal and effect
141
109
  const addProperty = <K extends keyof T & string>(
142
110
  key: K,
@@ -159,11 +127,9 @@ const createStore = <T extends UnknownRecord>(initialValue: T): Store<T> => {
159
127
  // Set internal states
160
128
  // @ts-expect-error non-matching signal types
161
129
  signals.set(key, signal)
162
- if (listeners.change.size) addOwnWatcher(key, signal)
163
130
 
164
131
  if (single) {
165
132
  notifyWatchers(watchers)
166
- emitNotification(listeners.add, [key])
167
133
  }
168
134
  return true
169
135
  }
@@ -183,12 +149,11 @@ const createStore = <T extends UnknownRecord>(initialValue: T): Store<T> => {
183
149
 
184
150
  if (single) {
185
151
  notifyWatchers(watchers)
186
- emitNotification(listeners.remove, [key])
187
152
  }
188
153
  }
189
154
 
190
155
  // Commit batched changes and emit notifications
191
- const batchChanges = (changes: DiffResult, initialRun?: boolean) => {
156
+ const batchChanges = (changes: DiffResult) => {
192
157
  // Additions
193
158
  if (Object.keys(changes.add).length) {
194
159
  for (const key in changes.add)
@@ -197,18 +162,11 @@ const createStore = <T extends UnknownRecord>(initialValue: T): Store<T> => {
197
162
  changes.add[key] as T[Extract<keyof T, string>] & {},
198
163
  false,
199
164
  )
200
-
201
- // Queue initial additions event to allow listeners to be added first
202
- if (initialRun)
203
- setTimeout(() => {
204
- emitNotification(listeners.add, Object.keys(changes.add))
205
- }, 0)
206
- else emitNotification(listeners.add, Object.keys(changes.add))
207
165
  }
208
166
 
209
167
  // Changes
210
168
  if (Object.keys(changes.change).length) {
211
- batchSignalWrites(() => {
169
+ batch(() => {
212
170
  for (const key in changes.change) {
213
171
  const value = changes.change[key] as T[Extract<
214
172
  keyof T,
@@ -220,28 +178,23 @@ const createStore = <T extends UnknownRecord>(initialValue: T): Store<T> => {
220
178
  if (isMutableSignal(signal)) signal.set(value)
221
179
  else throw new ReadonlySignalError(key, value)
222
180
  }
223
- emitNotification(listeners.change, Object.keys(changes.change))
224
181
  })
225
182
  }
226
183
 
227
184
  // Removals
228
185
  if (Object.keys(changes.remove).length) {
229
186
  for (const key in changes.remove) removeProperty(key)
230
- emitNotification(listeners.remove, Object.keys(changes.remove))
231
187
  }
232
188
 
233
189
  return changes.changed
234
190
  }
235
191
 
236
192
  // Reconcile data and dispatch events
237
- const reconcile = (
238
- oldValue: T,
239
- newValue: T,
240
- initialRun?: boolean,
241
- ): boolean => batchChanges(diff(oldValue, newValue), initialRun)
193
+ const reconcile = (oldValue: T, newValue: T): boolean =>
194
+ batchChanges(diff(oldValue, newValue))
242
195
 
243
196
  // Initialize data
244
- reconcile({} as T, initialValue, true)
197
+ reconcile({} as T, initialValue)
245
198
 
246
199
  // Methods and Properties
247
200
  const prototype: Record<PropertyKey, unknown> = {}
@@ -295,29 +248,6 @@ const createStore = <T extends UnknownRecord>(initialValue: T): Store<T> => {
295
248
  store.set(fn(current()))
296
249
  },
297
250
  },
298
- on: {
299
- value: <K extends keyof Omit<Listeners, 'sort'>>(
300
- type: K,
301
- listener: Listener<K>,
302
- ): Cleanup => {
303
- listeners[type].add(listener)
304
- if (type === 'change' && !ownWatchers.size) {
305
- for (const [key, signal] of signals)
306
- // @ts-expect-error ignore
307
- addOwnWatcher(key, signal)
308
- }
309
- return () => {
310
- listeners[type].delete(listener)
311
- if (type === 'change' && !listeners.change.size) {
312
- if (ownWatchers.size) {
313
- for (const watcher of ownWatchers.values())
314
- watcher.stop()
315
- ownWatchers.clear()
316
- }
317
- }
318
- }
319
- },
320
- },
321
251
  })
322
252
 
323
253
  // Return proxy directly with integrated signal methods
package/archive/task.ts CHANGED
@@ -7,18 +7,13 @@ import {
7
7
  } from '../src/errors'
8
8
  import {
9
9
  createWatcher,
10
- flushPendingReactions,
10
+ flush,
11
11
  notifyWatchers,
12
12
  subscribeActiveWatcher,
13
- trackSignalReads,
13
+ UNSET,
14
14
  type Watcher,
15
15
  } from '../src/system'
16
- import {
17
- isAbortError,
18
- isAsyncFunction,
19
- isObjectOfType,
20
- UNSET,
21
- } from '../src/util'
16
+ import { isAbortError, isAsyncFunction, isObjectOfType } from '../src/util'
22
17
 
23
18
  /* === Types === */
24
19
 
@@ -96,19 +91,14 @@ const createTask = <T extends {}>(
96
91
  }
97
92
 
98
93
  // Own watcher: called when notified from sources (push)
99
- const watcher = createWatcher(() => {
100
- dirty = true
101
- controller?.abort()
102
- if (watchers.size) notifyWatchers(watchers)
103
- else watcher.stop()
104
- })
105
- watcher.onCleanup(() => {
106
- controller?.abort()
107
- })
108
-
109
- // Called when requested by dependencies (pull)
110
- const compute = () =>
111
- trackSignalReads(watcher, () => {
94
+ const watcher = createWatcher(
95
+ () => {
96
+ dirty = true
97
+ controller?.abort()
98
+ if (watchers.size) notifyWatchers(watchers)
99
+ else watcher.stop()
100
+ },
101
+ () => {
112
102
  if (computing) throw new CircularDependencyError('computed')
113
103
  changed = false
114
104
  // Return current value until promise resolves
@@ -120,7 +110,7 @@ const createTask = <T extends {}>(
120
110
  () => {
121
111
  computing = false
122
112
  controller = undefined
123
- compute() // Retry computation with updated state
113
+ watcher.run() // Retry computation with updated state
124
114
  },
125
115
  {
126
116
  once: true,
@@ -141,7 +131,11 @@ const createTask = <T extends {}>(
141
131
  else if (null == result || UNSET === result) nil()
142
132
  else ok(result)
143
133
  computing = false
144
- })
134
+ },
135
+ )
136
+ watcher.onCleanup(() => {
137
+ controller?.abort()
138
+ })
145
139
 
146
140
  const task: Record<PropertyKey, unknown> = {}
147
141
  Object.defineProperties(task, {
@@ -151,8 +145,9 @@ const createTask = <T extends {}>(
151
145
  get: {
152
146
  value: (): T => {
153
147
  subscribeActiveWatcher(watchers)
154
- flushPendingReactions()
155
- if (dirty) compute()
148
+ flush()
149
+
150
+ if (dirty) watcher.run()
156
151
  if (error) throw error
157
152
  return value
158
153
  },