@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
package/src/match.ts DELETED
@@ -1,45 +0,0 @@
1
- import { createError } from './errors'
2
- import type { ResolveResult } from './resolve'
3
- import type { SignalValues, UnknownSignalRecord } from './signal'
4
-
5
- /* === Types === */
6
-
7
- type MatchHandlers<S extends UnknownSignalRecord> = {
8
- ok: (values: SignalValues<S>) => void
9
- err?: (errors: readonly Error[]) => void
10
- nil?: () => void
11
- }
12
-
13
- /* === Functions === */
14
-
15
- /**
16
- * Match on resolve result and call appropriate handler for side effects
17
- *
18
- * This is a utility function for those who prefer the handler pattern.
19
- * All handlers are for side effects only and return void. If you need
20
- * cleanup logic, use a hoisted let variable in your effect.
21
- *
22
- * @since 0.15.0
23
- * @param {ResolveResult<S>} result - Result from resolve()
24
- * @param {MatchHandlers<S>} handlers - Handlers for different states (side effects only)
25
- * @returns {void} - Always returns void
26
- */
27
- function match<S extends UnknownSignalRecord>(
28
- result: ResolveResult<S>,
29
- handlers: MatchHandlers<S>,
30
- ): void {
31
- try {
32
- if (result.pending) handlers.nil?.()
33
- else if (result.errors) handlers.err?.(result.errors)
34
- else if (result.ok) handlers.ok(result.values)
35
- } catch (e) {
36
- const error = createError(e)
37
- if (handlers.err && (!result.errors || !result.errors.includes(error)))
38
- handlers.err(result.errors ? [...result.errors, error] : [error])
39
- else throw error
40
- }
41
- }
42
-
43
- /* === Exports === */
44
-
45
- export { match, type MatchHandlers }
package/src/resolve.ts DELETED
@@ -1,49 +0,0 @@
1
- import type { UnknownRecord } from './diff'
2
- import { createError } from './errors'
3
- import type { SignalValues, UnknownSignalRecord } from './signal'
4
- import { UNSET } from './system'
5
-
6
- /* === Types === */
7
-
8
- type ResolveResult<S extends UnknownSignalRecord> =
9
- | { ok: true; values: SignalValues<S>; errors?: never; pending?: never }
10
- | { ok: false; errors: readonly Error[]; values?: never; pending?: never }
11
- | { ok: false; pending: true; values?: never; errors?: never }
12
-
13
- /* === Functions === */
14
-
15
- /**
16
- * Resolve signal values with perfect type inference
17
- *
18
- * Always returns a discriminated union result, regardless of whether
19
- * handlers are provided or not. This ensures a predictable API.
20
- *
21
- * @since 0.15.0
22
- * @param {S} signals - Signals to resolve
23
- * @returns {ResolveResult<S>} - Discriminated union result
24
- */
25
- function resolve<S extends UnknownSignalRecord>(signals: S): ResolveResult<S> {
26
- const errors: Error[] = []
27
- let pending = false
28
- const values: UnknownRecord = {}
29
-
30
- // Collect values and errors
31
- for (const [key, signal] of Object.entries(signals)) {
32
- try {
33
- const value = signal.get()
34
- if (value === UNSET) pending = true
35
- else values[key] = value
36
- } catch (e) {
37
- errors.push(createError(e))
38
- }
39
- }
40
-
41
- // Return discriminated union
42
- if (pending) return { ok: false, pending: true }
43
- if (errors.length > 0) return { ok: false, errors }
44
- return { ok: true, values: values as SignalValues<S> }
45
- }
46
-
47
- /* === Exports === */
48
-
49
- export { resolve, type ResolveResult }
package/src/system.ts DELETED
@@ -1,275 +0,0 @@
1
- /* === Types === */
2
-
3
- import { createError, InvalidHookError } from './errors'
4
- import { isFunction } from './util'
5
-
6
- type Cleanup = () => void
7
-
8
- // biome-ignore lint/suspicious/noConfusingVoidType: optional Cleanup return type
9
- type MaybeCleanup = Cleanup | undefined | void
10
-
11
- type Hook = 'add' | 'change' | 'cleanup' | 'remove' | 'sort' | 'watch'
12
- type CleanupHook = 'cleanup'
13
- type WatchHook = 'watch'
14
-
15
- type HookCallback = (payload?: readonly string[]) => MaybeCleanup
16
-
17
- type HookCallbacks = {
18
- [K in Hook]?: Set<HookCallback>
19
- }
20
-
21
- type Watcher = {
22
- (): void
23
- on(type: CleanupHook, cleanup: Cleanup): void
24
- stop(): void
25
- }
26
-
27
- /* === Internal === */
28
-
29
- // Currently active watcher
30
- let activeWatcher: Watcher | undefined
31
-
32
- // Map of signal watchers to their cleanup functions
33
- const unwatchMap = new WeakMap<Set<Watcher>, Set<Cleanup>>()
34
-
35
- // Queue of pending watcher reactions for batched change notifications
36
- const pendingReactions = new Set<() => void>()
37
- let batchDepth = 0
38
-
39
- /* === Constants === */
40
-
41
- // biome-ignore lint/suspicious/noExplicitAny: Deliberately using any to be used as a placeholder value in any signal
42
- const UNSET: any = Symbol()
43
-
44
- const HOOK_ADD = 'add'
45
- const HOOK_CHANGE = 'change'
46
- const HOOK_CLEANUP = 'cleanup'
47
- const HOOK_REMOVE = 'remove'
48
- const HOOK_SORT = 'sort'
49
- const HOOK_WATCH = 'watch'
50
-
51
- /* === Functions === */
52
-
53
- /**
54
- * Create a watcher to observe changes to a signal.
55
- *
56
- * A watcher is a reaction function with onCleanup and stop methods
57
- *
58
- * @since 0.14.1
59
- * @param {() => void} react - Function to be called when the state changes
60
- * @returns {Watcher} - Watcher object with off and cleanup methods
61
- */
62
- const createWatcher = (react: () => void): Watcher => {
63
- const cleanups = new Set<Cleanup>()
64
- const watcher = react as Partial<Watcher>
65
- watcher.on = (type: CleanupHook, cleanup: Cleanup) => {
66
- if (type === HOOK_CLEANUP) cleanups.add(cleanup)
67
- else throw new InvalidHookError('watcher', type)
68
- }
69
- watcher.stop = () => {
70
- try {
71
- for (const cleanup of cleanups) cleanup()
72
- } finally {
73
- cleanups.clear()
74
- }
75
- }
76
- return watcher as Watcher
77
- }
78
-
79
- /**
80
- * Subscribe by adding active watcher to the Set of watchers of a signal.
81
- *
82
- * @param {Set<Watcher>} watchers - Watchers of the signal
83
- * @param {Set<HookCallback>} watchHookCallbacks - HOOK_WATCH callbacks of the signal
84
- */
85
- const subscribeActiveWatcher = (
86
- watchers: Set<Watcher>,
87
- watchHookCallbacks?: Set<HookCallback>,
88
- ): void => {
89
- // Check if we need to trigger HOOK_WATCH callbacks
90
- if (!watchers.size && watchHookCallbacks?.size) {
91
- const unwatch = triggerHook(watchHookCallbacks)
92
- if (unwatch) {
93
- const unwatchCallbacks =
94
- unwatchMap.get(watchers) ?? new Set<Cleanup>()
95
- unwatchCallbacks.add(unwatch)
96
- if (!unwatchMap.has(watchers))
97
- unwatchMap.set(watchers, unwatchCallbacks)
98
- }
99
- }
100
-
101
- // Only if active watcher is not already subscribed
102
- if (activeWatcher && !watchers.has(activeWatcher)) {
103
- const watcher = activeWatcher
104
-
105
- watcher.on(HOOK_CLEANUP, () => {
106
- // Remove the watcher from the Set of watchers
107
- watchers.delete(watcher)
108
-
109
- // If it was the last watcher, call unwatch callbacks
110
- if (!watchers.size) {
111
- const unwatchCallbacks = unwatchMap.get(watchers)
112
- if (unwatchCallbacks) {
113
- try {
114
- for (const unwatch of unwatchCallbacks) unwatch()
115
- } finally {
116
- unwatchCallbacks.clear()
117
- unwatchMap.delete(watchers)
118
- }
119
- }
120
- }
121
- })
122
-
123
- // Here the active watcher is added to the Set of watchers
124
- watchers.add(watcher)
125
- }
126
- }
127
-
128
- /**
129
- * Notify watchers of a signal change.
130
- *
131
- * @param {Set<Watcher>} watchers - Watchers of the signal
132
- * @returns {boolean} - Whether any watchers were notified
133
- */
134
- const notifyWatchers = (watchers: Set<Watcher>): boolean => {
135
- if (!watchers.size) return false
136
- for (const react of watchers) {
137
- if (batchDepth) pendingReactions.add(react)
138
- else react()
139
- }
140
- return true
141
- }
142
-
143
- /**
144
- * Flush all pending reactions of enqueued watchers.
145
- */
146
- const flushPendingReactions = () => {
147
- while (pendingReactions.size) {
148
- const watchers = Array.from(pendingReactions)
149
- pendingReactions.clear()
150
- for (const watcher of watchers) watcher()
151
- }
152
- }
153
-
154
- /**
155
- * Batch multiple signal writes.
156
- *
157
- * @param {() => void} callback - Function with multiple signal writes to be batched
158
- */
159
- const batchSignalWrites = (callback: () => void) => {
160
- batchDepth++
161
- try {
162
- callback()
163
- } finally {
164
- flushPendingReactions()
165
- batchDepth--
166
- }
167
- }
168
-
169
- /**
170
- * Run a function with signal reads in a tracking context (or temporarily untrack).
171
- *
172
- * @param {Watcher | false} watcher - Watcher to be called when the signal changes
173
- * or false for temporary untracking while inserting auto-hydrating DOM nodes
174
- * that might read signals (e.g., Web Components)
175
- * @param {() => void} run - Function to run the computation or effect
176
- */
177
- const trackSignalReads = (watcher: Watcher | false, run: () => void): void => {
178
- const prev = activeWatcher
179
- activeWatcher = watcher || undefined
180
- try {
181
- run()
182
- } finally {
183
- activeWatcher = prev
184
- }
185
- }
186
-
187
- /**
188
- * Trigger a hook.
189
- *
190
- * @param {Set<HookCallback> | undefined} callbacks - Callbacks to be called when the hook is triggered
191
- * @param {readonly string[] | undefined} payload - Payload to be sent to listeners
192
- * @return {Cleanup | undefined} Cleanup function to be called when the hook is unmounted
193
- */
194
- const triggerHook = (
195
- callbacks: Set<HookCallback> | undefined,
196
- payload?: readonly string[],
197
- ): Cleanup | undefined => {
198
- if (!callbacks) return
199
-
200
- const cleanups: Cleanup[] = []
201
- const errors: Error[] = []
202
-
203
- const throwError = (inCleanup?: boolean) => {
204
- if (errors.length) {
205
- if (errors.length === 1) throw errors[0]
206
- throw new AggregateError(
207
- errors,
208
- `Errors in hook ${inCleanup ? 'cleanup' : 'callback'}:`,
209
- )
210
- }
211
- }
212
-
213
- for (const callback of callbacks) {
214
- try {
215
- const cleanup = callback(payload)
216
- if (isFunction(cleanup)) cleanups.push(cleanup)
217
- } catch (error) {
218
- errors.push(createError(error))
219
- }
220
- }
221
- throwError()
222
-
223
- if (!cleanups.length) return
224
- if (cleanups.length === 1) return cleanups[0]
225
- return () => {
226
- for (const cleanup of cleanups) {
227
- try {
228
- cleanup()
229
- } catch (error) {
230
- errors.push(createError(error))
231
- }
232
- }
233
- throwError(true)
234
- }
235
- }
236
-
237
- /**
238
- * Check whether a hook type is handled in a signal.
239
- *
240
- * @param {Hook} type - Type of hook to check
241
- * @param {T} handled - List of handled hook types
242
- * @returns {type is T[number]} - Whether the hook type is handled
243
- */
244
- const isHandledHook = <T extends readonly Hook[]>(
245
- type: Hook,
246
- handled: T,
247
- ): type is T[number] => handled.includes(type)
248
-
249
- /* === Exports === */
250
-
251
- export {
252
- type Cleanup,
253
- type MaybeCleanup,
254
- type Watcher,
255
- type Hook,
256
- type CleanupHook,
257
- type WatchHook,
258
- type HookCallback,
259
- type HookCallbacks,
260
- HOOK_ADD,
261
- HOOK_CHANGE,
262
- HOOK_CLEANUP,
263
- HOOK_REMOVE,
264
- HOOK_SORT,
265
- HOOK_WATCH,
266
- UNSET,
267
- createWatcher,
268
- subscribeActiveWatcher,
269
- notifyWatchers,
270
- flushPendingReactions,
271
- batchSignalWrites,
272
- trackSignalReads,
273
- triggerHook,
274
- isHandledHook,
275
- }