@zeix/cause-effect 0.17.3 → 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 (89) hide show
  1. package/.ai-context.md +163 -232
  2. package/.cursorrules +41 -35
  3. package/.github/copilot-instructions.md +166 -116
  4. package/ARCHITECTURE.md +274 -0
  5. package/CLAUDE.md +199 -143
  6. package/COLLECTION_REFACTORING.md +161 -0
  7. package/GUIDE.md +298 -0
  8. package/README.md +232 -197
  9. package/REQUIREMENTS.md +100 -0
  10. package/bench/reactivity.bench.ts +577 -0
  11. package/index.dev.js +1325 -997
  12. package/index.js +1 -1
  13. package/index.ts +58 -74
  14. package/package.json +4 -1
  15. package/src/errors.ts +118 -74
  16. package/src/graph.ts +601 -0
  17. package/src/nodes/collection.ts +474 -0
  18. package/src/nodes/effect.ts +149 -0
  19. package/src/nodes/list.ts +588 -0
  20. package/src/nodes/memo.ts +120 -0
  21. package/src/nodes/sensor.ts +139 -0
  22. package/src/nodes/state.ts +135 -0
  23. package/src/nodes/store.ts +383 -0
  24. package/src/nodes/task.ts +146 -0
  25. package/src/signal.ts +112 -66
  26. package/src/util.ts +26 -57
  27. package/test/batch.test.ts +96 -62
  28. package/test/benchmark.test.ts +473 -487
  29. package/test/collection.test.ts +466 -706
  30. package/test/effect.test.ts +293 -696
  31. package/test/list.test.ts +335 -592
  32. package/test/memo.test.ts +380 -0
  33. package/test/regression.test.ts +156 -0
  34. package/test/scope.test.ts +191 -0
  35. package/test/sensor.test.ts +454 -0
  36. package/test/signal.test.ts +220 -213
  37. package/test/state.test.ts +217 -265
  38. package/test/store.test.ts +346 -446
  39. package/test/task.test.ts +395 -0
  40. package/test/untrack.test.ts +167 -0
  41. package/types/index.d.ts +13 -15
  42. package/types/src/errors.d.ts +73 -17
  43. package/types/src/graph.d.ts +208 -0
  44. package/types/src/nodes/collection.d.ts +64 -0
  45. package/types/src/nodes/effect.d.ts +48 -0
  46. package/types/src/nodes/list.d.ts +65 -0
  47. package/types/src/nodes/memo.d.ts +57 -0
  48. package/types/src/nodes/sensor.d.ts +75 -0
  49. package/types/src/nodes/state.d.ts +78 -0
  50. package/types/src/nodes/store.d.ts +51 -0
  51. package/types/src/nodes/task.d.ts +73 -0
  52. package/types/src/signal.d.ts +43 -29
  53. package/types/src/util.d.ts +9 -16
  54. package/archive/benchmark.ts +0 -683
  55. package/archive/collection.ts +0 -253
  56. package/archive/composite.ts +0 -85
  57. package/archive/computed.ts +0 -195
  58. package/archive/list.ts +0 -483
  59. package/archive/memo.ts +0 -139
  60. package/archive/state.ts +0 -90
  61. package/archive/store.ts +0 -298
  62. package/archive/task.ts +0 -189
  63. package/src/classes/collection.ts +0 -245
  64. package/src/classes/computed.ts +0 -349
  65. package/src/classes/list.ts +0 -343
  66. package/src/classes/ref.ts +0 -70
  67. package/src/classes/state.ts +0 -102
  68. package/src/classes/store.ts +0 -262
  69. package/src/diff.ts +0 -138
  70. package/src/effect.ts +0 -93
  71. package/src/match.ts +0 -45
  72. package/src/resolve.ts +0 -49
  73. package/src/system.ts +0 -257
  74. package/test/computed.test.ts +0 -1108
  75. package/test/diff.test.ts +0 -955
  76. package/test/match.test.ts +0 -388
  77. package/test/ref.test.ts +0 -353
  78. package/test/resolve.test.ts +0 -154
  79. package/types/src/classes/collection.d.ts +0 -45
  80. package/types/src/classes/computed.d.ts +0 -94
  81. package/types/src/classes/list.d.ts +0 -43
  82. package/types/src/classes/ref.d.ts +0 -35
  83. package/types/src/classes/state.d.ts +0 -49
  84. package/types/src/classes/store.d.ts +0 -52
  85. package/types/src/diff.d.ts +0 -28
  86. package/types/src/effect.d.ts +0 -15
  87. package/types/src/match.d.ts +0 -21
  88. package/types/src/resolve.d.ts +0 -29
  89. package/types/src/system.d.ts +0 -78
@@ -1,245 +0,0 @@
1
- import { InvalidCollectionSourceError, validateCallback } from '../errors'
2
- import type { Signal } from '../signal'
3
- import {
4
- createWatcher,
5
- notifyOf,
6
- registerWatchCallbacks,
7
- type SignalOptions,
8
- subscribeTo,
9
- UNSET,
10
- type Watcher,
11
- } from '../system'
12
- import { isAsyncFunction, isFunction, isObjectOfType } from '../util'
13
- import { type Computed, createComputed } from './computed'
14
- import { isList, type List } from './list'
15
-
16
- /* === Types === */
17
-
18
- type CollectionSource<T extends {}> = List<T> | Collection<T>
19
-
20
- type CollectionCallback<T extends {}, U extends {}> =
21
- | ((sourceValue: U) => T)
22
- | ((sourceValue: U, abort: AbortSignal) => Promise<T>)
23
-
24
- type Collection<T extends {}> = {
25
- readonly [Symbol.toStringTag]: 'Collection'
26
- readonly [Symbol.isConcatSpreadable]: true
27
- [Symbol.iterator](): IterableIterator<Signal<T>>
28
- keys(): IterableIterator<string>
29
- get: () => T[]
30
- at: (index: number) => Signal<T> | undefined
31
- byKey: (key: string) => Signal<T> | undefined
32
- keyAt: (index: number) => string | undefined
33
- indexOfKey: (key: string) => number | undefined
34
- deriveCollection: <R extends {}>(
35
- callback: CollectionCallback<R, T>,
36
- ) => DerivedCollection<R, T>
37
- readonly length: number
38
- }
39
-
40
- /* === Constants === */
41
-
42
- const TYPE_COLLECTION = 'Collection' as const
43
-
44
- /* === Class === */
45
-
46
- class DerivedCollection<T extends {}, U extends {}> implements Collection<T> {
47
- #source: CollectionSource<U>
48
- #callback: CollectionCallback<T, U>
49
- #signals = new Map<string, Computed<T>>()
50
- #keys: string[] = []
51
- #dirty = true
52
- #watcher: Watcher | undefined
53
-
54
- constructor(
55
- source: CollectionSource<U> | (() => CollectionSource<U>),
56
- callback: CollectionCallback<T, U>,
57
- options?: SignalOptions<T[]>,
58
- ) {
59
- validateCallback(TYPE_COLLECTION, callback)
60
-
61
- if (isFunction(source)) source = source()
62
- if (!isCollectionSource(source))
63
- throw new InvalidCollectionSourceError(TYPE_COLLECTION, source)
64
- this.#source = source
65
-
66
- this.#callback = callback
67
-
68
- for (let i = 0; i < this.#source.length; i++) {
69
- const key = this.#source.keyAt(i)
70
- if (!key) continue
71
-
72
- this.#add(key)
73
- }
74
-
75
- if (options?.watched)
76
- registerWatchCallbacks(this, options.watched, options.unwatched)
77
- }
78
-
79
- #getWatcher(): Watcher {
80
- this.#watcher ||= createWatcher(
81
- () => {
82
- this.#dirty = true
83
- if (!notifyOf(this)) this.#watcher?.stop()
84
- },
85
- () => {
86
- const newKeys = Array.from(this.#source.keys())
87
- const allKeys = new Set([...this.#keys, ...newKeys])
88
- const addedKeys: string[] = []
89
- const removedKeys: string[] = []
90
-
91
- for (const key of allKeys) {
92
- const oldHas = this.#keys.includes(key)
93
- const newHas = newKeys.includes(key)
94
-
95
- if (!oldHas && newHas) addedKeys.push(key)
96
- else if (oldHas && !newHas) removedKeys.push(key)
97
- }
98
-
99
- for (const key of removedKeys) this.#signals.delete(key)
100
- for (const key of addedKeys) this.#add(key)
101
- this.#keys = newKeys
102
- this.#dirty = false
103
- },
104
- )
105
- this.#watcher.onCleanup(() => {
106
- this.#watcher = undefined
107
- })
108
-
109
- return this.#watcher
110
- }
111
-
112
- #add(key: string): boolean {
113
- const computedCallback = isAsyncCollectionCallback<T>(this.#callback)
114
- ? async (_: T, abort: AbortSignal) => {
115
- const sourceValue = this.#source.byKey(key)?.get() as U
116
- if (sourceValue === UNSET) return UNSET
117
- return this.#callback(sourceValue, abort)
118
- }
119
- : () => {
120
- const sourceValue = this.#source.byKey(key)?.get() as U
121
- if (sourceValue === UNSET) return UNSET
122
- return (this.#callback as (sourceValue: U) => T)(
123
- sourceValue,
124
- )
125
- }
126
-
127
- const signal = createComputed(computedCallback)
128
-
129
- this.#signals.set(key, signal)
130
- if (!this.#keys.includes(key)) this.#keys.push(key)
131
- return true
132
- }
133
-
134
- get [Symbol.toStringTag](): 'Collection' {
135
- return TYPE_COLLECTION
136
- }
137
-
138
- get [Symbol.isConcatSpreadable](): true {
139
- return true
140
- }
141
-
142
- *[Symbol.iterator](): IterableIterator<Computed<T>> {
143
- for (const key of this.#keys) {
144
- const signal = this.#signals.get(key)
145
- if (signal) yield signal as Computed<T>
146
- }
147
- }
148
-
149
- keys(): IterableIterator<string> {
150
- subscribeTo(this)
151
- if (this.#dirty) this.#getWatcher().run()
152
- return this.#keys.values()
153
- }
154
-
155
- get(): T[] {
156
- subscribeTo(this)
157
-
158
- if (this.#dirty) this.#getWatcher().run()
159
- return this.#keys
160
- .map(key => this.#signals.get(key)?.get())
161
- .filter(v => v != null && v !== UNSET) as T[]
162
- }
163
-
164
- at(index: number): Computed<T> | undefined {
165
- return this.#signals.get(this.#keys[index])
166
- }
167
-
168
- byKey(key: string): Computed<T> | undefined {
169
- return this.#signals.get(key)
170
- }
171
-
172
- keyAt(index: number): string | undefined {
173
- return this.#keys[index]
174
- }
175
-
176
- indexOfKey(key: string): number {
177
- return this.#keys.indexOf(key)
178
- }
179
-
180
- deriveCollection<R extends {}>(
181
- callback: (sourceValue: T) => R,
182
- options?: SignalOptions<R[]>,
183
- ): DerivedCollection<R, T>
184
- deriveCollection<R extends {}>(
185
- callback: (sourceValue: T, abort: AbortSignal) => Promise<R>,
186
- options?: SignalOptions<R[]>,
187
- ): DerivedCollection<R, T>
188
- deriveCollection<R extends {}>(
189
- callback: CollectionCallback<R, T>,
190
- options?: SignalOptions<R[]>,
191
- ): DerivedCollection<R, T> {
192
- return new DerivedCollection(this, callback, options)
193
- }
194
-
195
- get length(): number {
196
- subscribeTo(this)
197
- if (this.#dirty) this.#getWatcher().run()
198
- return this.#keys.length
199
- }
200
- }
201
-
202
- /* === Functions === */
203
-
204
- /**
205
- * Check if a value is a collection signal
206
- *
207
- * @since 0.17.2
208
- * @param {unknown} value - Value to check
209
- * @returns {boolean} - True if value is a collection signal, false otherwise
210
- */
211
- const isCollection = /*#__PURE__*/ <T extends {}>(
212
- value: unknown,
213
- ): value is Collection<T> => isObjectOfType(value, TYPE_COLLECTION)
214
-
215
- /**
216
- * Check if a value is a collection source
217
- *
218
- * @since 0.17.0
219
- * @param {unknown} value - Value to check
220
- * @returns {boolean} - True if value is a collection source, false otherwise
221
- */
222
- const isCollectionSource = /*#__PURE__*/ <T extends {}>(
223
- value: unknown,
224
- ): value is CollectionSource<T> => isList(value) || isCollection(value)
225
-
226
- /**
227
- * Check if the provided callback is an async function
228
- *
229
- * @since 0.17.0
230
- * @param {unknown} callback - Value to check
231
- * @returns {boolean} - True if value is an async collection callback, false otherwise
232
- */
233
- const isAsyncCollectionCallback = <T extends {}>(
234
- callback: unknown,
235
- ): callback is (sourceValue: unknown, abort: AbortSignal) => Promise<T> =>
236
- isAsyncFunction(callback)
237
-
238
- export {
239
- type Collection,
240
- type CollectionSource,
241
- type CollectionCallback,
242
- DerivedCollection,
243
- isCollection,
244
- TYPE_COLLECTION,
245
- }
@@ -1,349 +0,0 @@
1
- import { isEqual } from '../diff'
2
- import {
3
- CircularDependencyError,
4
- createError,
5
- validateCallback,
6
- validateSignalValue,
7
- } from '../errors'
8
- import {
9
- createWatcher,
10
- flush,
11
- notifyOf,
12
- registerWatchCallbacks,
13
- type SignalOptions,
14
- subscribeTo,
15
- UNSET,
16
- type Watcher,
17
- } from '../system'
18
- import {
19
- isAbortError,
20
- isAsyncFunction,
21
- isObjectOfType,
22
- isSyncFunction,
23
- } from '../util'
24
-
25
- /* === Types === */
26
-
27
- type Computed<T extends {}> = {
28
- readonly [Symbol.toStringTag]: 'Computed'
29
- get(): T
30
- }
31
-
32
- type ComputedOptions<T extends {}> = SignalOptions<T> & {
33
- initialValue?: T
34
- }
35
-
36
- type MemoCallback<T extends {} & { then?: undefined }> = (oldValue: T) => T
37
-
38
- type TaskCallback<T extends {} & { then?: undefined }> = (
39
- oldValue: T,
40
- abort: AbortSignal,
41
- ) => Promise<T>
42
-
43
- /* === Constants === */
44
-
45
- const TYPE_COMPUTED = 'Computed' as const
46
-
47
- /* === Classes === */
48
-
49
- /**
50
- * Create a new memoized signal for a synchronous function.
51
- *
52
- * @since 0.17.0
53
- * @param {MemoCallback<T>} callback - Callback function to compute the memoized value
54
- * @param {T} [initialValue = UNSET] - Initial value of the signal
55
- * @throws {InvalidCallbackError} If the callback is not an sync function
56
- * @throws {InvalidSignalValueError} If the initial value is not valid
57
- */
58
- class Memo<T extends {}> {
59
- #callback: MemoCallback<T>
60
- #value: T
61
- #error: Error | undefined
62
- #dirty = true
63
- #computing = false
64
- #watcher: Watcher | undefined
65
-
66
- constructor(callback: MemoCallback<T>, options?: ComputedOptions<T>) {
67
- validateCallback(this.constructor.name, callback, isMemoCallback)
68
- const initialValue = options?.initialValue ?? UNSET
69
- validateSignalValue(this.constructor.name, initialValue, options?.guard)
70
-
71
- this.#callback = callback
72
- this.#value = initialValue
73
- if (options?.watched)
74
- registerWatchCallbacks(this, options.watched, options.unwatched)
75
- }
76
-
77
- #getWatcher(): Watcher {
78
- // Own watcher: called by notifyWatchers() in upstream signals (push)
79
- this.#watcher ||= createWatcher(
80
- () => {
81
- this.#dirty = true
82
- if (!notifyOf(this)) this.#watcher?.stop()
83
- },
84
- () => {
85
- if (this.#computing) throw new CircularDependencyError('memo')
86
-
87
- let result: T
88
- this.#computing = true
89
- try {
90
- result = this.#callback(this.#value)
91
- } catch (e) {
92
- // Err track
93
- this.#value = UNSET
94
- this.#error = createError(e)
95
- this.#computing = false
96
- return
97
- }
98
-
99
- if (null == result || UNSET === result) {
100
- // Nil track
101
- this.#value = UNSET
102
- this.#error = undefined
103
- } else {
104
- // Ok track
105
- this.#value = result
106
- this.#error = undefined
107
- this.#dirty = false
108
- }
109
- this.#computing = false
110
- },
111
- )
112
- this.#watcher.onCleanup(() => {
113
- this.#watcher = undefined
114
- })
115
-
116
- return this.#watcher
117
- }
118
-
119
- get [Symbol.toStringTag](): 'Computed' {
120
- return TYPE_COMPUTED
121
- }
122
-
123
- /**
124
- * Return the memoized value after computing it if necessary.
125
- *
126
- * @returns {T}
127
- * @throws {CircularDependencyError} If a circular dependency is detected
128
- * @throws {Error} If an error occurs during computation
129
- */
130
- get(): T {
131
- subscribeTo(this)
132
- flush()
133
-
134
- if (this.#dirty) this.#getWatcher().run()
135
- if (this.#error) throw this.#error
136
- return this.#value
137
- }
138
- }
139
-
140
- /**
141
- * Create a new task signals that memoizes the result of an asynchronous function.
142
- *
143
- * @since 0.17.0
144
- * @param {TaskCallback<T>} callback - The asynchronous function to compute the memoized value
145
- * @param {T} [initialValue = UNSET] - Initial value of the signal
146
- * @throws {InvalidCallbackError} If the callback is not an async function
147
- * @throws {InvalidSignalValueError} If the initial value is not valid
148
- */
149
- class Task<T extends {}> {
150
- #callback: TaskCallback<T>
151
- #value: T
152
- #error: Error | undefined
153
- #dirty = true
154
- #computing = false
155
- #changed = false
156
- #watcher: Watcher | undefined
157
- #controller: AbortController | undefined
158
-
159
- constructor(callback: TaskCallback<T>, options?: ComputedOptions<T>) {
160
- validateCallback(this.constructor.name, callback, isTaskCallback)
161
- const initialValue = options?.initialValue ?? UNSET
162
- validateSignalValue(this.constructor.name, initialValue, options?.guard)
163
-
164
- this.#callback = callback
165
- this.#value = initialValue
166
- if (options?.watched)
167
- registerWatchCallbacks(this, options.watched, options.unwatched)
168
- }
169
-
170
- #getWatcher(): Watcher {
171
- if (!this.#watcher) {
172
- // Functions to update internal state
173
- const ok = (v: T): undefined => {
174
- if (!isEqual(v, this.#value)) {
175
- this.#value = v
176
- this.#changed = true
177
- }
178
- this.#error = undefined
179
- this.#dirty = false
180
- }
181
- const nil = (): undefined => {
182
- this.#changed = UNSET !== this.#value
183
- this.#value = UNSET
184
- this.#error = undefined
185
- }
186
- const err = (e: unknown): undefined => {
187
- const newError = createError(e)
188
- this.#changed =
189
- !this.#error ||
190
- newError.name !== this.#error.name ||
191
- newError.message !== this.#error.message
192
- this.#value = UNSET
193
- this.#error = newError
194
- }
195
- const settle =
196
- <T>(fn: (arg: T) => void) =>
197
- (arg: T) => {
198
- this.#computing = false
199
- this.#controller = undefined
200
- fn(arg)
201
- if (this.#changed && !notifyOf(this)) this.#watcher?.stop()
202
- }
203
-
204
- // Own watcher: called by notifyOf() in upstream signals (push)
205
- this.#watcher = createWatcher(
206
- () => {
207
- this.#dirty = true
208
- this.#controller?.abort()
209
- if (!notifyOf(this)) this.#watcher?.stop()
210
- },
211
- () => {
212
- if (this.#computing)
213
- throw new CircularDependencyError('task')
214
- this.#changed = false
215
-
216
- // Return current value until promise resolves
217
- if (this.#controller) return this.#value
218
-
219
- this.#controller = new AbortController()
220
- this.#controller.signal.addEventListener(
221
- 'abort',
222
- () => {
223
- this.#computing = false
224
- this.#controller = undefined
225
-
226
- // Retry computation with updated state
227
- this.#getWatcher().run()
228
- },
229
- {
230
- once: true,
231
- },
232
- )
233
- let result: Promise<T>
234
- this.#computing = true
235
- try {
236
- result = this.#callback(
237
- this.#value,
238
- this.#controller.signal,
239
- )
240
- } catch (e) {
241
- if (isAbortError(e)) nil()
242
- else err(e)
243
- this.#computing = false
244
- return
245
- }
246
-
247
- if (result instanceof Promise)
248
- result.then(settle(ok), settle(err))
249
- else if (null == result || UNSET === result) nil()
250
- else ok(result)
251
- this.#computing = false
252
- },
253
- )
254
- this.#watcher.onCleanup(() => {
255
- this.#controller?.abort()
256
- this.#controller = undefined
257
- this.#watcher = undefined
258
- })
259
- }
260
-
261
- return this.#watcher
262
- }
263
-
264
- get [Symbol.toStringTag](): 'Computed' {
265
- return TYPE_COMPUTED
266
- }
267
-
268
- /**
269
- * Return the memoized value after executing the async function if necessary.
270
- *
271
- * @returns {T}
272
- * @throws {CircularDependencyError} If a circular dependency is detected
273
- * @throws {Error} If an error occurs during computation
274
- */
275
- get(): T {
276
- subscribeTo(this)
277
- flush()
278
-
279
- if (this.#dirty) this.#getWatcher().run()
280
- if (this.#error) throw this.#error
281
- return this.#value
282
- }
283
- }
284
-
285
- /* === Functions === */
286
-
287
- /**
288
- * Create a derived signal from existing signals
289
- *
290
- * @since 0.9.0
291
- * @param {MemoCallback<T> | TaskCallback<T>} callback - Computation callback function
292
- * @param {ComputedOptions<T>} options - Optional configuration
293
- */
294
- const createComputed = <T extends {}>(
295
- callback: TaskCallback<T> | MemoCallback<T>,
296
- options?: ComputedOptions<T>,
297
- ) =>
298
- isAsyncFunction(callback)
299
- ? new Task(callback as TaskCallback<T>, options)
300
- : new Memo(callback as MemoCallback<T>, options)
301
-
302
- /**
303
- * Check if a value is a computed signal
304
- *
305
- * @since 0.9.0
306
- * @param {unknown} value - Value to check
307
- * @returns {boolean} - True if value is a computed signal, false otherwise
308
- */
309
- const isComputed = /*#__PURE__*/ <T extends {}>(
310
- value: unknown,
311
- ): value is Memo<T> => isObjectOfType(value, TYPE_COMPUTED)
312
-
313
- /**
314
- * Check if the provided value is a callback that may be used as input for createSignal() to derive a computed state
315
- *
316
- * @since 0.12.0
317
- * @param {unknown} value - Value to check
318
- * @returns {boolean} - True if value is a sync callback, false otherwise
319
- */
320
- const isMemoCallback = /*#__PURE__*/ <T extends {} & { then?: undefined }>(
321
- value: unknown,
322
- ): value is MemoCallback<T> => isSyncFunction(value) && value.length < 2
323
-
324
- /**
325
- * Check if the provided value is a callback that may be used as input for createSignal() to derive a computed state
326
- *
327
- * @since 0.17.0
328
- * @param {unknown} value - Value to check
329
- * @returns {boolean} - True if value is an async callback, false otherwise
330
- */
331
- const isTaskCallback = /*#__PURE__*/ <T extends {}>(
332
- value: unknown,
333
- ): value is TaskCallback<T> => isAsyncFunction(value) && value.length < 3
334
-
335
- /* === Exports === */
336
-
337
- export {
338
- TYPE_COMPUTED,
339
- createComputed,
340
- isComputed,
341
- isMemoCallback,
342
- isTaskCallback,
343
- Memo,
344
- Task,
345
- type Computed,
346
- type ComputedOptions,
347
- type MemoCallback,
348
- type TaskCallback,
349
- }