@zeix/cause-effect 0.17.2 → 0.17.3

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