@zeix/cause-effect 0.17.2 → 0.18.0

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 (94) hide show
  1. package/.ai-context.md +163 -226
  2. package/.cursorrules +41 -35
  3. package/.github/copilot-instructions.md +166 -116
  4. package/.zed/settings.json +3 -0
  5. package/ARCHITECTURE.md +274 -0
  6. package/CLAUDE.md +197 -202
  7. package/COLLECTION_REFACTORING.md +161 -0
  8. package/GUIDE.md +298 -0
  9. package/README.md +241 -220
  10. package/REQUIREMENTS.md +100 -0
  11. package/bench/reactivity.bench.ts +577 -0
  12. package/index.dev.js +1326 -1174
  13. package/index.js +1 -1
  14. package/index.ts +58 -85
  15. package/package.json +9 -6
  16. package/src/errors.ts +118 -70
  17. package/src/graph.ts +601 -0
  18. package/src/nodes/collection.ts +474 -0
  19. package/src/nodes/effect.ts +149 -0
  20. package/src/nodes/list.ts +588 -0
  21. package/src/nodes/memo.ts +120 -0
  22. package/src/nodes/sensor.ts +139 -0
  23. package/src/nodes/state.ts +135 -0
  24. package/src/nodes/store.ts +383 -0
  25. package/src/nodes/task.ts +146 -0
  26. package/src/signal.ts +112 -64
  27. package/src/util.ts +26 -57
  28. package/test/batch.test.ts +96 -69
  29. package/test/benchmark.test.ts +473 -485
  30. package/test/collection.test.ts +455 -955
  31. package/test/effect.test.ts +293 -696
  32. package/test/list.test.ts +332 -857
  33. package/test/memo.test.ts +380 -0
  34. package/test/regression.test.ts +156 -0
  35. package/test/scope.test.ts +191 -0
  36. package/test/sensor.test.ts +454 -0
  37. package/test/signal.test.ts +220 -213
  38. package/test/state.test.ts +217 -271
  39. package/test/store.test.ts +346 -898
  40. package/test/task.test.ts +395 -0
  41. package/test/untrack.test.ts +167 -0
  42. package/test/util/dependency-graph.ts +2 -2
  43. package/tsconfig.build.json +11 -0
  44. package/tsconfig.json +5 -7
  45. package/types/index.d.ts +13 -15
  46. package/types/src/errors.d.ts +73 -19
  47. package/types/src/graph.d.ts +208 -0
  48. package/types/src/nodes/collection.d.ts +64 -0
  49. package/types/src/nodes/effect.d.ts +48 -0
  50. package/types/src/nodes/list.d.ts +65 -0
  51. package/types/src/nodes/memo.d.ts +57 -0
  52. package/types/src/nodes/sensor.d.ts +75 -0
  53. package/types/src/nodes/state.d.ts +78 -0
  54. package/types/src/nodes/store.d.ts +51 -0
  55. package/types/src/nodes/task.d.ts +73 -0
  56. package/types/src/signal.d.ts +43 -28
  57. package/types/src/util.d.ts +9 -16
  58. package/archive/benchmark.ts +0 -688
  59. package/archive/collection.ts +0 -310
  60. package/archive/computed.ts +0 -198
  61. package/archive/list.ts +0 -544
  62. package/archive/memo.ts +0 -140
  63. package/archive/state.ts +0 -90
  64. package/archive/store.ts +0 -357
  65. package/archive/task.ts +0 -191
  66. package/src/classes/collection.ts +0 -298
  67. package/src/classes/composite.ts +0 -171
  68. package/src/classes/computed.ts +0 -392
  69. package/src/classes/list.ts +0 -310
  70. package/src/classes/ref.ts +0 -96
  71. package/src/classes/state.ts +0 -131
  72. package/src/classes/store.ts +0 -227
  73. package/src/diff.ts +0 -138
  74. package/src/effect.ts +0 -96
  75. package/src/match.ts +0 -45
  76. package/src/resolve.ts +0 -49
  77. package/src/system.ts +0 -275
  78. package/test/computed.test.ts +0 -1126
  79. package/test/diff.test.ts +0 -955
  80. package/test/match.test.ts +0 -388
  81. package/test/ref.test.ts +0 -381
  82. package/test/resolve.test.ts +0 -154
  83. package/types/src/classes/collection.d.ts +0 -47
  84. package/types/src/classes/composite.d.ts +0 -15
  85. package/types/src/classes/computed.d.ts +0 -114
  86. package/types/src/classes/list.d.ts +0 -41
  87. package/types/src/classes/ref.d.ts +0 -48
  88. package/types/src/classes/state.d.ts +0 -61
  89. package/types/src/classes/store.d.ts +0 -51
  90. package/types/src/diff.d.ts +0 -28
  91. package/types/src/effect.d.ts +0 -15
  92. package/types/src/match.d.ts +0 -21
  93. package/types/src/resolve.d.ts +0 -29
  94. package/types/src/system.d.ts +0 -81
@@ -1,298 +0,0 @@
1
- import {
2
- InvalidCollectionSourceError,
3
- InvalidHookError,
4
- validateCallback,
5
- } from '../errors'
6
- import type { Signal } from '../signal'
7
- import {
8
- type Cleanup,
9
- createWatcher,
10
- HOOK_ADD,
11
- HOOK_CHANGE,
12
- HOOK_REMOVE,
13
- HOOK_SORT,
14
- HOOK_WATCH,
15
- type Hook,
16
- type HookCallback,
17
- type HookCallbacks,
18
- isHandledHook,
19
- notifyWatchers,
20
- subscribeActiveWatcher,
21
- trackSignalReads,
22
- triggerHook,
23
- UNSET,
24
- type Watcher,
25
- } from '../system'
26
- import { isAsyncFunction, isFunction, isObjectOfType } from '../util'
27
- import { type Computed, createComputed } from './computed'
28
- import { isList, type List } from './list'
29
-
30
- /* === Types === */
31
-
32
- type CollectionSource<T extends {}> = List<T> | Collection<T>
33
-
34
- type CollectionCallback<T extends {}, U extends {}> =
35
- | ((sourceValue: U) => T)
36
- | ((sourceValue: U, abort: AbortSignal) => Promise<T>)
37
-
38
- type Collection<T extends {}> = {
39
- readonly [Symbol.toStringTag]: 'Collection'
40
- readonly [Symbol.isConcatSpreadable]: true
41
- [Symbol.iterator](): IterableIterator<Signal<T>>
42
- keys(): IterableIterator<string>
43
- get: () => T[]
44
- at: (index: number) => Signal<T> | undefined
45
- byKey: (key: string) => Signal<T> | undefined
46
- keyAt: (index: number) => string | undefined
47
- indexOfKey: (key: string) => number | undefined
48
- on: <K extends Hook>(type: K, callback: HookCallback) => Cleanup
49
- deriveCollection: <R extends {}>(
50
- callback: CollectionCallback<R, T>,
51
- ) => DerivedCollection<R, T>
52
- readonly length: number
53
- }
54
-
55
- /* === Constants === */
56
-
57
- const TYPE_COLLECTION = 'Collection' as const
58
-
59
- /* === Class === */
60
-
61
- class DerivedCollection<T extends {}, U extends {}> implements Collection<T> {
62
- #watchers = new Set<Watcher>()
63
- #source: CollectionSource<U>
64
- #callback: CollectionCallback<T, U>
65
- #signals = new Map<string, Computed<T>>()
66
- #ownWatchers = new Map<string, Watcher>()
67
- #hookCallbacks: HookCallbacks = {}
68
- #order: string[] = []
69
-
70
- constructor(
71
- source: CollectionSource<U> | (() => CollectionSource<U>),
72
- callback: CollectionCallback<T, U>,
73
- ) {
74
- validateCallback(TYPE_COLLECTION, callback)
75
-
76
- if (isFunction(source)) source = source()
77
- if (!isCollectionSource(source))
78
- throw new InvalidCollectionSourceError(TYPE_COLLECTION, source)
79
- this.#source = source
80
-
81
- this.#callback = callback
82
-
83
- for (let i = 0; i < this.#source.length; i++) {
84
- const key = this.#source.keyAt(i)
85
- if (!key) continue
86
-
87
- this.#add(key)
88
- }
89
-
90
- this.#source.on(HOOK_ADD, additions => {
91
- if (!additions) return
92
- for (const key of additions) {
93
- if (!this.#signals.has(key)) {
94
- this.#add(key)
95
- // For async computations, trigger initial computation
96
- const signal = this.#signals.get(key)
97
- if (signal && isAsyncCollectionCallback(this.#callback))
98
- signal.get()
99
- }
100
- }
101
- notifyWatchers(this.#watchers)
102
- triggerHook(this.#hookCallbacks.add, additions)
103
- })
104
-
105
- this.#source.on(HOOK_REMOVE, removals => {
106
- if (!removals) return
107
- for (const key of removals) {
108
- if (!this.#signals.has(key)) continue
109
-
110
- this.#signals.delete(key)
111
- const index = this.#order.indexOf(key)
112
- if (index >= 0) this.#order.splice(index, 1)
113
-
114
- const watcher = this.#ownWatchers.get(key)
115
- if (watcher) {
116
- watcher.stop()
117
- this.#ownWatchers.delete(key)
118
- }
119
- }
120
- this.#order = this.#order.filter(() => true) // Compact array
121
- notifyWatchers(this.#watchers)
122
- triggerHook(this.#hookCallbacks.remove, removals)
123
- })
124
-
125
- this.#source.on(HOOK_SORT, newOrder => {
126
- if (newOrder) this.#order = [...newOrder]
127
- notifyWatchers(this.#watchers)
128
- triggerHook(this.#hookCallbacks.sort, newOrder)
129
- })
130
- }
131
-
132
- #add(key: string): boolean {
133
- const computedCallback = isAsyncCollectionCallback<T>(this.#callback)
134
- ? async (_: T, abort: AbortSignal) => {
135
- const sourceValue = this.#source.byKey(key)?.get() as U
136
- if (sourceValue === UNSET) return UNSET
137
- return this.#callback(sourceValue, abort)
138
- }
139
- : () => {
140
- const sourceValue = this.#source.byKey(key)?.get() as U
141
- if (sourceValue === UNSET) return UNSET
142
- return (this.#callback as (sourceValue: U) => T)(
143
- sourceValue,
144
- )
145
- }
146
-
147
- const signal = createComputed(computedCallback)
148
-
149
- this.#signals.set(key, signal)
150
- if (!this.#order.includes(key)) this.#order.push(key)
151
- if (this.#hookCallbacks.change?.size) this.#addWatcher(key)
152
- return true
153
- }
154
-
155
- #addWatcher(key: string): void {
156
- const watcher = createWatcher(() => {
157
- trackSignalReads(watcher, () => {
158
- this.#signals.get(key)?.get() // Subscribe to the signal
159
- })
160
- })
161
- this.#ownWatchers.set(key, watcher)
162
- watcher()
163
- }
164
-
165
- get [Symbol.toStringTag](): 'Collection' {
166
- return TYPE_COLLECTION
167
- }
168
-
169
- get [Symbol.isConcatSpreadable](): true {
170
- return true
171
- }
172
-
173
- *[Symbol.iterator](): IterableIterator<Computed<T>> {
174
- for (const key of this.#order) {
175
- const signal = this.#signals.get(key)
176
- if (signal) yield signal as Computed<T>
177
- }
178
- }
179
-
180
- keys(): IterableIterator<string> {
181
- return this.#order.values()
182
- }
183
-
184
- get(): T[] {
185
- subscribeActiveWatcher(this.#watchers, this.#hookCallbacks[HOOK_WATCH])
186
- return this.#order
187
- .map(key => this.#signals.get(key)?.get())
188
- .filter(v => v != null && v !== UNSET) as T[]
189
- }
190
-
191
- at(index: number): Computed<T> | undefined {
192
- return this.#signals.get(this.#order[index])
193
- }
194
-
195
- byKey(key: string): Computed<T> | undefined {
196
- return this.#signals.get(key)
197
- }
198
-
199
- keyAt(index: number): string | undefined {
200
- return this.#order[index]
201
- }
202
-
203
- indexOfKey(key: string): number {
204
- return this.#order.indexOf(key)
205
- }
206
-
207
- on(type: Hook, callback: HookCallback): Cleanup {
208
- if (
209
- isHandledHook(type, [
210
- HOOK_ADD,
211
- HOOK_CHANGE,
212
- HOOK_REMOVE,
213
- HOOK_SORT,
214
- HOOK_WATCH,
215
- ])
216
- ) {
217
- this.#hookCallbacks[type] ||= new Set()
218
- this.#hookCallbacks[type].add(callback)
219
- if (type === HOOK_CHANGE && !this.#ownWatchers.size) {
220
- for (const key of this.#signals.keys()) this.#addWatcher(key)
221
- }
222
-
223
- return () => {
224
- this.#hookCallbacks[type]?.delete(callback)
225
- if (type === HOOK_CHANGE && !this.#hookCallbacks.change?.size) {
226
- if (this.#ownWatchers.size) {
227
- for (const watcher of this.#ownWatchers.values())
228
- watcher.stop()
229
- this.#ownWatchers.clear()
230
- }
231
- }
232
- }
233
- }
234
- throw new InvalidHookError(TYPE_COLLECTION, type)
235
- }
236
-
237
- deriveCollection<R extends {}>(
238
- callback: (sourceValue: T) => R,
239
- ): DerivedCollection<R, T>
240
- deriveCollection<R extends {}>(
241
- callback: (sourceValue: T, abort: AbortSignal) => Promise<R>,
242
- ): DerivedCollection<R, T>
243
- deriveCollection<R extends {}>(
244
- callback: CollectionCallback<R, T>,
245
- ): DerivedCollection<R, T> {
246
- return new DerivedCollection(this, callback)
247
- }
248
-
249
- get length(): number {
250
- subscribeActiveWatcher(this.#watchers, this.#hookCallbacks[HOOK_WATCH])
251
- return this.#order.length
252
- }
253
- }
254
-
255
- /* === Functions === */
256
-
257
- /**
258
- * Check if a value is a collection signal
259
- *
260
- * @since 0.17.2
261
- * @param {unknown} value - Value to check
262
- * @returns {boolean} - True if value is a collection signal, false otherwise
263
- */
264
- const isCollection = /*#__PURE__*/ <T extends {}>(
265
- value: unknown,
266
- ): value is Collection<T> => isObjectOfType(value, TYPE_COLLECTION)
267
-
268
- /**
269
- * Check if a value is a collection source
270
- *
271
- * @since 0.17.0
272
- * @param {unknown} value - Value to check
273
- * @returns {boolean} - True if value is a collection source, false otherwise
274
- */
275
- const isCollectionSource = /*#__PURE__*/ <T extends {}>(
276
- value: unknown,
277
- ): value is CollectionSource<T> => isList(value) || isCollection(value)
278
-
279
- /**
280
- * Check if the provided callback is an async function
281
- *
282
- * @since 0.17.0
283
- * @param {unknown} callback - Value to check
284
- * @returns {boolean} - True if value is an async collection callback, false otherwise
285
- */
286
- const isAsyncCollectionCallback = <T extends {}>(
287
- callback: unknown,
288
- ): callback is (sourceValue: unknown, abort: AbortSignal) => Promise<T> =>
289
- isAsyncFunction(callback)
290
-
291
- export {
292
- type Collection,
293
- type CollectionSource,
294
- type CollectionCallback,
295
- DerivedCollection,
296
- isCollection,
297
- TYPE_COLLECTION,
298
- }
@@ -1,171 +0,0 @@
1
- import type { DiffResult, UnknownRecord } from '../diff'
2
- import { guardMutableSignal, InvalidHookError } from '../errors'
3
- import type { Signal } from '../signal'
4
- import {
5
- batchSignalWrites,
6
- type Cleanup,
7
- createWatcher,
8
- HOOK_ADD,
9
- HOOK_CHANGE,
10
- HOOK_REMOVE,
11
- type HookCallback,
12
- type HookCallbacks,
13
- isHandledHook,
14
- trackSignalReads,
15
- triggerHook,
16
- type Watcher,
17
- } from '../system'
18
-
19
- /* === Types === */
20
-
21
- type CompositeHook = 'add' | 'change' | 'remove'
22
-
23
- /* === Class Definitions === */
24
-
25
- class Composite<T extends UnknownRecord, S extends Signal<T[keyof T] & {}>> {
26
- signals = new Map<string, S>()
27
- #validate: <K extends keyof T & string>(
28
- key: K,
29
- value: unknown,
30
- ) => value is T[K] & {}
31
- #create: <V extends T[keyof T] & {}>(value: V) => S
32
- #watchers = new Map<string, Watcher>()
33
- #hookCallbacks: HookCallbacks = {}
34
- #batching = false
35
-
36
- constructor(
37
- values: T,
38
- validate: <K extends keyof T & string>(
39
- key: K,
40
- value: unknown,
41
- ) => value is T[K] & {},
42
- create: <V extends T[keyof T] & {}>(value: V) => S,
43
- ) {
44
- this.#validate = validate
45
- this.#create = create
46
- this.change(
47
- {
48
- add: values,
49
- change: {},
50
- remove: {},
51
- changed: true,
52
- },
53
- true,
54
- )
55
- }
56
-
57
- #addWatcher(key: string): void {
58
- const watcher = createWatcher(() => {
59
- trackSignalReads(watcher, () => {
60
- this.signals.get(key)?.get() // Subscribe to the signal
61
- if (!this.#batching)
62
- triggerHook(this.#hookCallbacks.change, [key])
63
- })
64
- })
65
- this.#watchers.set(key, watcher)
66
- watcher()
67
- }
68
-
69
- add<K extends keyof T & string>(key: K, value: T[K]): boolean {
70
- if (!this.#validate(key, value)) return false
71
-
72
- this.signals.set(key, this.#create(value))
73
- if (this.#hookCallbacks.change?.size) this.#addWatcher(key)
74
-
75
- if (!this.#batching) triggerHook(this.#hookCallbacks.add, [key])
76
- return true
77
- }
78
-
79
- remove<K extends keyof T & string>(key: K): boolean {
80
- const ok = this.signals.delete(key)
81
- if (!ok) return false
82
-
83
- const watcher = this.#watchers.get(key)
84
- if (watcher) {
85
- watcher.stop()
86
- this.#watchers.delete(key)
87
- }
88
-
89
- if (!this.#batching) triggerHook(this.#hookCallbacks.remove, [key])
90
- return true
91
- }
92
-
93
- change(changes: DiffResult, initialRun?: boolean): boolean {
94
- this.#batching = true
95
-
96
- // Additions
97
- if (Object.keys(changes.add).length) {
98
- for (const key in changes.add)
99
- this.add(
100
- key as Extract<keyof T, string>,
101
- changes.add[key] as T[Extract<keyof T, string>] & {},
102
- )
103
-
104
- // Queue initial additions event to allow listeners to be added first
105
- const notify = () =>
106
- triggerHook(this.#hookCallbacks.add, Object.keys(changes.add))
107
- if (initialRun) setTimeout(notify, 0)
108
- else notify()
109
- }
110
-
111
- // Changes
112
- if (Object.keys(changes.change).length) {
113
- batchSignalWrites(() => {
114
- for (const key in changes.change) {
115
- const value = changes.change[key]
116
- if (!this.#validate(key as keyof T & string, value))
117
- continue
118
-
119
- const signal = this.signals.get(key)
120
- if (guardMutableSignal(`list item "${key}"`, value, signal))
121
- signal.set(value)
122
- }
123
- })
124
- triggerHook(this.#hookCallbacks.change, Object.keys(changes.change))
125
- }
126
-
127
- // Removals
128
- if (Object.keys(changes.remove).length) {
129
- for (const key in changes.remove)
130
- this.remove(key as keyof T & string)
131
- triggerHook(this.#hookCallbacks.remove, Object.keys(changes.remove))
132
- }
133
-
134
- this.#batching = false
135
- return changes.changed
136
- }
137
-
138
- clear(): boolean {
139
- const keys = Array.from(this.signals.keys())
140
- this.signals.clear()
141
- this.#watchers.clear()
142
- triggerHook(this.#hookCallbacks.remove, keys)
143
- return true
144
- }
145
-
146
- on(type: CompositeHook, callback: HookCallback): Cleanup {
147
- if (!isHandledHook(type, [HOOK_ADD, HOOK_CHANGE, HOOK_REMOVE]))
148
- throw new InvalidHookError('Composite', type)
149
-
150
- this.#hookCallbacks[type] ||= new Set()
151
- this.#hookCallbacks[type].add(callback)
152
- if (type === HOOK_CHANGE && !this.#watchers.size) {
153
- this.#batching = true
154
- for (const key of this.signals.keys()) this.#addWatcher(key)
155
- this.#batching = false
156
- }
157
-
158
- return () => {
159
- this.#hookCallbacks[type]?.delete(callback)
160
- if (type === HOOK_CHANGE && !this.#hookCallbacks.change?.size) {
161
- if (this.#watchers.size) {
162
- for (const watcher of this.#watchers.values())
163
- watcher.stop()
164
- this.#watchers.clear()
165
- }
166
- }
167
- }
168
- }
169
- }
170
-
171
- export { Composite, type CompositeHook as CompositeListeners }