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