@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,310 +0,0 @@
1
- import type { UnknownArray } from '../src/diff'
2
- import { match } from '../src/match'
3
- import { resolve } from '../src/resolve'
4
- import type { Signal } from '../src/signal'
5
- import {
6
- type Cleanup,
7
- createWatcher,
8
- triggerHook,
9
- type HookCallback,
10
- type HookCallbacks,
11
- type Hook,
12
- notifyWatchers,
13
- subscribeActiveWatcher,
14
- trackSignalReads,
15
- type Watcher,
16
- UNSET,
17
- } from '../src/system'
18
- import { isAsyncFunction, isObjectOfType, isSymbol } from '../src/util'
19
- import { type Computed, createComputed } from './computed'
20
- import type { List } from './list'
21
-
22
- /* === Types === */
23
-
24
- type CollectionKeySignal<T extends {}> = T extends UnknownArray
25
- ? Collection<T>
26
- : Computed<T>
27
-
28
- type CollectionCallback<T extends {} & { then?: undefined }, O extends {}> =
29
- | ((originValue: O, abort: AbortSignal) => Promise<T>)
30
- | ((originValue: O) => T)
31
-
32
- type Collection<T extends {}> = {
33
- readonly [Symbol.toStringTag]: typeof TYPE_COLLECTION
34
- readonly [Symbol.isConcatSpreadable]: boolean
35
- [Symbol.iterator](): IterableIterator<CollectionKeySignal<T>>
36
- readonly [n: number]: CollectionKeySignal<T>
37
- readonly length: number
38
-
39
- byKey(key: string): CollectionKeySignal<T> | undefined
40
- get(): T[]
41
- keyAt(index: number): string | undefined
42
- indexOfKey(key: string): number
43
- on(type: Hook, callback: HookCallback): Cleanup
44
- sort(compareFn?: (a: T, b: T) => number): void
45
- }
46
-
47
- /* === Constants === */
48
-
49
- const TYPE_COLLECTION = 'Collection' as const
50
-
51
- /* === Exported Functions === */
52
-
53
- /**
54
- * Collections - Read-Only Derived Array-Like Stores
55
- *
56
- * Collections are the read-only, derived counterpart to array-like Stores.
57
- * They provide reactive, memoized, and lazily-evaluated array transformations
58
- * while maintaining the familiar array-like store interface.
59
- *
60
- * @since 0.16.2
61
- * @param {List<O> | Collection<O>} origin - Origin of collection to derive values from
62
- * @param {ComputedCallback<ArrayItem<T>>} callback - Callback function to transform array items
63
- * @returns {Collection<T>} - New collection with reactive properties that preserves the original type T
64
- */
65
- const createCollection = <T extends {}, O extends {}>(
66
- origin: List<O> | Collection<O>,
67
- callback: CollectionCallback<T, O>,
68
- ): Collection<T> => {
69
- const watchers = new Set<Watcher>()
70
- const hookCallbacks: HookCallbacks = {}
71
- const signals = new Map<string, Signal<T>>()
72
- const signalWatchers = new Map<string, Watcher>()
73
-
74
- let order: string[] = []
75
-
76
- // Add nested signal and effect
77
- const addProperty = (key: string): boolean => {
78
- const computedCallback = isAsyncFunction(callback)
79
- ? async (_: T, abort: AbortSignal) => {
80
- const originSignal = origin.byKey(key)
81
- if (!originSignal) return UNSET
82
-
83
- let result = UNSET
84
- match(resolve({ originSignal }), {
85
- ok: async ({ originSignal: originValue }) => {
86
- result = await callback(originValue, abort)
87
- },
88
- err: (errors: readonly Error[]) => {
89
- console.log(errors)
90
- },
91
- })
92
- return result
93
- }
94
- : () => {
95
- const originSignal = origin.byKey(key)
96
- if (!originSignal) return UNSET
97
-
98
- let result = UNSET
99
- match(resolve({ originSignal }), {
100
- ok: ({ originSignal: originValue }) => {
101
- result = (callback as (originValue: O) => T)(
102
- originValue as unknown as O,
103
- )
104
- },
105
- err: (errors: readonly Error[]) => {
106
- console.log(errors)
107
- },
108
- })
109
- return result
110
- }
111
-
112
- const signal = createComputed(computedCallback)
113
-
114
- // Set internal states
115
- signals.set(key, signal)
116
- if (!order.includes(key)) order.push(key)
117
- const watcher = createWatcher(() =>
118
- trackSignalReads(watcher, () => {
119
- signal.get() // Subscribe to the signal
120
- triggerHook(hookCallbacks.change, [key])
121
- }),
122
- )
123
- watcher()
124
- signalWatchers.set(key, watcher)
125
- return true
126
- }
127
-
128
- // Remove nested signal and effect
129
- const removeProperty = (key: string) => {
130
- // Remove signal for key
131
- const ok = signals.delete(key)
132
- if (!ok) return
133
-
134
- // Clean up internal states
135
- const index = order.indexOf(key)
136
- if (index >= 0) order.splice(index, 1)
137
- const watcher = signalWatchers.get(key)
138
- if (watcher) {
139
- watcher.stop()
140
- signalWatchers.delete(key)
141
- }
142
- }
143
-
144
- // Initialize properties
145
- for (let i = 0; i < origin.length; i++) {
146
- const key = origin.keyAt(i)
147
- if (!key) continue
148
- addProperty(key)
149
- }
150
- origin.on('add', additions => {
151
- if (!additions?.length) return
152
- for (const key of additions) {
153
- if (!signals.has(key)) addProperty(key)
154
- }
155
- notifyWatchers(watchers)
156
- triggerHook(hookCallbacks.add, additions)
157
- })
158
- origin.on('remove', removals => {
159
- if (!removals?.length) return
160
- for (const key of Object.keys(removals)) {
161
- if (!signals.has(key)) continue
162
- removeProperty(key)
163
- }
164
- order = order.filter(() => true) // Compact array
165
- notifyWatchers(watchers)
166
- triggerHook(hookCallbacks.remove, removals)
167
- })
168
- origin.on('sort', newOrder => {
169
- if (newOrder) {
170
- order = [...newOrder]
171
- notifyWatchers(watchers)
172
- triggerHook(hookCallbacks.sort, newOrder)
173
- }
174
- })
175
-
176
- // Get signal by key or index
177
- const getSignal = (prop: string): Signal<T> | undefined => {
178
- let key = prop
179
- const index = Number(prop)
180
- if (Number.isInteger(index) && index >= 0) key = order[index] ?? prop
181
- return signals.get(key)
182
- }
183
-
184
- // Get current array
185
- const current = (): T =>
186
- order
187
- .map(key => signals.get(key)?.get())
188
- .filter(v => v !== UNSET) as unknown as T
189
-
190
- // Methods and Properties
191
- const collection: Record<PropertyKey, unknown> = {}
192
- Object.defineProperties(collection, {
193
- [Symbol.toStringTag]: {
194
- value: TYPE_COLLECTION,
195
- },
196
- [Symbol.isConcatSpreadable]: {
197
- value: true,
198
- },
199
- [Symbol.iterator]: {
200
- value: function* () {
201
- for (const key of order) {
202
- const signal = signals.get(key)
203
- if (signal) yield signal
204
- }
205
- },
206
- },
207
- byKey: {
208
- value(key: string) {
209
- return getSignal(key)
210
- },
211
- },
212
- keyAt: {
213
- value(index: number): string | undefined {
214
- return order[index]
215
- },
216
- },
217
- indexOfKey: {
218
- value(key: string): number {
219
- return order.indexOf(key)
220
- },
221
- },
222
- get: {
223
- value: (): T => {
224
- subscribeActiveWatcher(watchers)
225
- return current()
226
- },
227
- },
228
- sort: {
229
- value: (compareFn?: (a: T, b: T) => number): void => {
230
- const entries = order
231
- .map((key, index) => {
232
- const signal = signals.get(key)
233
- return [
234
- index,
235
- key,
236
- signal ? signal.get() : undefined,
237
- ] as [number, string, T]
238
- })
239
- .sort(
240
- compareFn
241
- ? (a, b) => compareFn(a[2], b[2])
242
- : (a, b) =>
243
- String(a[2]).localeCompare(String(b[2])),
244
- )
245
-
246
- // Set new order
247
- order = entries.map(([_, key]) => key)
248
-
249
- notifyWatchers(watchers)
250
- triggerHook(hookCallbacks.sort, order)
251
- },
252
- },
253
- on: {
254
- value: (type: Hook, callback: HookCallback): Cleanup => {
255
- hookCallbacks[type] ||= new Set()
256
- hookCallbacks[type].add(callback)
257
- return () => hookCallbacks[type]?.delete(callback)
258
- },
259
- },
260
- length: {
261
- get(): number {
262
- subscribeActiveWatcher(watchers)
263
- return signals.size
264
- },
265
- },
266
- })
267
-
268
- // Return proxy directly with integrated signal methods
269
- return new Proxy(collection as Collection<T>, {
270
- get(target, prop) {
271
- if (prop in target) return Reflect.get(target, prop)
272
- if (!isSymbol(prop)) return getSignal(prop)
273
- },
274
- has(target, prop) {
275
- if (prop in target) return true
276
- return signals.has(String(prop))
277
- },
278
- ownKeys(target) {
279
- const staticKeys = Reflect.ownKeys(target)
280
- return [...new Set([...order, ...staticKeys])]
281
- },
282
- getOwnPropertyDescriptor(target, prop) {
283
- if (prop in target)
284
- return Reflect.getOwnPropertyDescriptor(target, prop)
285
- if (isSymbol(prop)) return undefined
286
-
287
- const signal = getSignal(prop)
288
- return signal
289
- ? {
290
- enumerable: true,
291
- configurable: true,
292
- writable: true,
293
- value: signal,
294
- }
295
- : undefined
296
- },
297
- })
298
- }
299
-
300
- const isCollection = /*#__PURE__*/ <T extends UnknownArray>(
301
- value: unknown,
302
- ): value is Collection<T> => isObjectOfType(value, TYPE_COLLECTION)
303
-
304
- export {
305
- type Collection,
306
- type CollectionCallback,
307
- createCollection,
308
- isCollection,
309
- TYPE_COLLECTION,
310
- }
@@ -1,198 +0,0 @@
1
- import { isEqual } from '../src/diff'
2
- import {
3
- CircularDependencyError,
4
- createError,
5
- InvalidCallbackError,
6
- NullishSignalValueError,
7
- } from '../src/errors'
8
- import {
9
- createWatcher,
10
- flushPendingReactions,
11
- HOOK_CLEANUP,
12
- notifyWatchers,
13
- subscribeActiveWatcher,
14
- trackSignalReads,
15
- UNSET,
16
- type Watcher,
17
- } from '../src/system'
18
- import {
19
- isAbortError,
20
- isAsyncFunction,
21
- isFunction,
22
- isObjectOfType,
23
- } from '../src/util'
24
-
25
- /* === Types === */
26
-
27
- type Computed<T extends {}> = {
28
- readonly [Symbol.toStringTag]: 'Computed'
29
- get(): T
30
- }
31
-
32
- type ComputedCallback<T extends {} & { then?: undefined }> =
33
- | ((oldValue: T, abort: AbortSignal) => Promise<T>)
34
- | ((oldValue: T) => T)
35
-
36
- /* === Constants === */
37
-
38
- const TYPE_COMPUTED = 'Computed' as const
39
-
40
- /* === Functions === */
41
-
42
- /**
43
- * Create a derived signal from existing signals
44
- *
45
- * @since 0.9.0
46
- * @param {ComputedCallback<T>} callback - Computation callback function
47
- * @returns {Computed<T>} - Computed signal
48
- */
49
- const createComputed = <T extends {}>(
50
- callback: ComputedCallback<T>,
51
- initialValue: T = UNSET,
52
- ): Computed<T> => {
53
- if (!isComputedCallback(callback))
54
- throw new InvalidCallbackError('computed', callback)
55
- if (initialValue == null) throw new NullishSignalValueError('computed')
56
-
57
- const watchers: Set<Watcher> = new Set()
58
-
59
- // Internal state
60
- let value: T = initialValue
61
- let error: Error | undefined
62
- let controller: AbortController | undefined
63
- let dirty = true
64
- let changed = false
65
- let computing = false
66
-
67
- // Functions to update internal state
68
- const ok = (v: T): undefined => {
69
- if (!isEqual(v, value)) {
70
- value = v
71
- changed = true
72
- }
73
- error = undefined
74
- dirty = false
75
- }
76
- const nil = (): undefined => {
77
- changed = UNSET !== value
78
- value = UNSET
79
- error = undefined
80
- }
81
- const err = (e: unknown): undefined => {
82
- const newError = createError(e)
83
- changed =
84
- !error ||
85
- newError.name !== error.name ||
86
- newError.message !== error.message
87
- value = UNSET
88
- error = newError
89
- }
90
- const settle =
91
- <T>(fn: (arg: T) => void) =>
92
- (arg: T) => {
93
- computing = false
94
- controller = undefined
95
- fn(arg)
96
- if (changed) notifyWatchers(watchers)
97
- }
98
-
99
- // Own watcher: called when notified from sources (push)
100
- const watcher = createWatcher(() => {
101
- dirty = true
102
- controller?.abort()
103
- if (watchers.size) notifyWatchers(watchers)
104
- else watcher.stop()
105
- })
106
- watcher.on(HOOK_CLEANUP, () => {
107
- controller?.abort()
108
- })
109
-
110
- // Called when requested by dependencies (pull)
111
- const compute = () =>
112
- trackSignalReads(watcher, () => {
113
- if (computing) throw new CircularDependencyError('computed')
114
- changed = false
115
- if (isAsyncFunction(callback)) {
116
- // Return current value until promise resolves
117
- if (controller) return value
118
- controller = new AbortController()
119
- controller.signal.addEventListener(
120
- 'abort',
121
- () => {
122
- computing = false
123
- controller = undefined
124
- compute() // Retry computation with updated state
125
- },
126
- {
127
- once: true,
128
- },
129
- )
130
- }
131
- let result: T | Promise<T>
132
- computing = true
133
- try {
134
- result = controller
135
- ? callback(value, controller.signal)
136
- : (callback as (oldValue: T) => T)(value)
137
- } catch (e) {
138
- if (isAbortError(e)) nil()
139
- else err(e)
140
- computing = false
141
- return
142
- }
143
- if (result instanceof Promise) result.then(settle(ok), settle(err))
144
- else if (null == result || UNSET === result) nil()
145
- else ok(result)
146
- computing = false
147
- })
148
-
149
- const computed: Record<PropertyKey, unknown> = {}
150
- Object.defineProperties(computed, {
151
- [Symbol.toStringTag]: {
152
- value: TYPE_COMPUTED,
153
- },
154
- get: {
155
- value: (): T => {
156
- subscribeActiveWatcher(watchers)
157
- flushPendingReactions()
158
- if (dirty) compute()
159
- if (error) throw error
160
- return value
161
- },
162
- },
163
- })
164
- return computed as Computed<T>
165
- }
166
-
167
- /**
168
- * Check if a value is a computed signal
169
- *
170
- * @since 0.9.0
171
- * @param {unknown} value - Value to check
172
- * @returns {boolean} - true if value is a computed signal, false otherwise
173
- */
174
- const isComputed = /*#__PURE__*/ <T extends {}>(
175
- value: unknown,
176
- ): value is Computed<T> => isObjectOfType(value, TYPE_COMPUTED)
177
-
178
- /**
179
- * Check if the provided value is a callback that may be used as input for toSignal() to derive a computed state
180
- *
181
- * @since 0.12.0
182
- * @param {unknown} value - Value to check
183
- * @returns {boolean} - true if value is a callback or callbacks object, false otherwise
184
- */
185
- const isComputedCallback = /*#__PURE__*/ <T extends {}>(
186
- value: unknown,
187
- ): value is ComputedCallback<T> => isFunction(value) && value.length < 3
188
-
189
- /* === Exports === */
190
-
191
- export {
192
- TYPE_COMPUTED,
193
- createComputed,
194
- isComputed,
195
- isComputedCallback,
196
- type Computed,
197
- type ComputedCallback,
198
- }