@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.
- package/.ai-context.md +163 -226
- package/.cursorrules +41 -35
- package/.github/copilot-instructions.md +166 -116
- package/.zed/settings.json +3 -0
- package/ARCHITECTURE.md +274 -0
- package/CLAUDE.md +197 -202
- package/COLLECTION_REFACTORING.md +161 -0
- package/GUIDE.md +298 -0
- package/README.md +241 -220
- package/REQUIREMENTS.md +100 -0
- package/bench/reactivity.bench.ts +577 -0
- package/index.dev.js +1326 -1174
- package/index.js +1 -1
- package/index.ts +58 -85
- package/package.json +9 -6
- package/src/errors.ts +118 -70
- package/src/graph.ts +601 -0
- package/src/nodes/collection.ts +474 -0
- package/src/nodes/effect.ts +149 -0
- package/src/nodes/list.ts +588 -0
- package/src/nodes/memo.ts +120 -0
- package/src/nodes/sensor.ts +139 -0
- package/src/nodes/state.ts +135 -0
- package/src/nodes/store.ts +383 -0
- package/src/nodes/task.ts +146 -0
- package/src/signal.ts +112 -64
- package/src/util.ts +26 -57
- package/test/batch.test.ts +96 -69
- package/test/benchmark.test.ts +473 -485
- package/test/collection.test.ts +455 -955
- package/test/effect.test.ts +293 -696
- package/test/list.test.ts +332 -857
- package/test/memo.test.ts +380 -0
- package/test/regression.test.ts +156 -0
- package/test/scope.test.ts +191 -0
- package/test/sensor.test.ts +454 -0
- package/test/signal.test.ts +220 -213
- package/test/state.test.ts +217 -271
- package/test/store.test.ts +346 -898
- package/test/task.test.ts +395 -0
- package/test/untrack.test.ts +167 -0
- package/test/util/dependency-graph.ts +2 -2
- package/tsconfig.build.json +11 -0
- package/tsconfig.json +5 -7
- package/types/index.d.ts +13 -15
- package/types/src/errors.d.ts +73 -19
- package/types/src/graph.d.ts +208 -0
- package/types/src/nodes/collection.d.ts +64 -0
- package/types/src/nodes/effect.d.ts +48 -0
- package/types/src/nodes/list.d.ts +65 -0
- package/types/src/nodes/memo.d.ts +57 -0
- package/types/src/nodes/sensor.d.ts +75 -0
- package/types/src/nodes/state.d.ts +78 -0
- package/types/src/nodes/store.d.ts +51 -0
- package/types/src/nodes/task.d.ts +73 -0
- package/types/src/signal.d.ts +43 -28
- package/types/src/util.d.ts +9 -16
- package/archive/benchmark.ts +0 -688
- package/archive/collection.ts +0 -310
- package/archive/computed.ts +0 -198
- package/archive/list.ts +0 -544
- package/archive/memo.ts +0 -140
- package/archive/state.ts +0 -90
- package/archive/store.ts +0 -357
- package/archive/task.ts +0 -191
- package/src/classes/collection.ts +0 -298
- package/src/classes/composite.ts +0 -171
- package/src/classes/computed.ts +0 -392
- package/src/classes/list.ts +0 -310
- package/src/classes/ref.ts +0 -96
- package/src/classes/state.ts +0 -131
- package/src/classes/store.ts +0 -227
- package/src/diff.ts +0 -138
- package/src/effect.ts +0 -96
- package/src/match.ts +0 -45
- package/src/resolve.ts +0 -49
- package/src/system.ts +0 -275
- package/test/computed.test.ts +0 -1126
- package/test/diff.test.ts +0 -955
- package/test/match.test.ts +0 -388
- package/test/ref.test.ts +0 -381
- package/test/resolve.test.ts +0 -154
- package/types/src/classes/collection.d.ts +0 -47
- package/types/src/classes/composite.d.ts +0 -15
- package/types/src/classes/computed.d.ts +0 -114
- package/types/src/classes/list.d.ts +0 -41
- package/types/src/classes/ref.d.ts +0 -48
- package/types/src/classes/state.d.ts +0 -61
- package/types/src/classes/store.d.ts +0 -51
- package/types/src/diff.d.ts +0 -28
- package/types/src/effect.d.ts +0 -15
- package/types/src/match.d.ts +0 -21
- package/types/src/resolve.d.ts +0 -29
- 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
|
-
}
|