@zeix/cause-effect 0.17.3 → 0.18.1
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 +169 -227
- package/.cursorrules +41 -35
- package/.github/copilot-instructions.md +176 -116
- package/ARCHITECTURE.md +276 -0
- package/CHANGELOG.md +29 -0
- package/CLAUDE.md +201 -143
- package/GUIDE.md +298 -0
- package/README.md +246 -193
- package/REQUIREMENTS.md +100 -0
- package/bench/reactivity.bench.ts +577 -0
- package/context7.json +4 -0
- package/examples/events-sensor.ts +187 -0
- package/examples/selector-sensor.ts +173 -0
- package/index.dev.js +1390 -1008
- package/index.js +1 -1
- package/index.ts +60 -74
- package/package.json +5 -2
- package/skills/changelog-keeper/SKILL.md +59 -0
- package/skills/changelog-keeper/agents/openai.yaml +4 -0
- package/src/errors.ts +118 -74
- package/src/graph.ts +612 -0
- package/src/nodes/collection.ts +512 -0
- package/src/nodes/effect.ts +149 -0
- package/src/nodes/list.ts +589 -0
- package/src/nodes/memo.ts +148 -0
- package/src/nodes/sensor.ts +149 -0
- package/src/nodes/state.ts +135 -0
- package/src/nodes/store.ts +378 -0
- package/src/nodes/task.ts +174 -0
- package/src/signal.ts +112 -66
- package/src/util.ts +26 -57
- package/test/batch.test.ts +96 -62
- package/test/benchmark.test.ts +473 -487
- package/test/collection.test.ts +456 -707
- package/test/effect.test.ts +293 -696
- package/test/list.test.ts +335 -592
- package/test/memo.test.ts +574 -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 -265
- package/test/store.test.ts +346 -446
- package/test/task.test.ts +529 -0
- package/test/untrack.test.ts +167 -0
- package/types/index.d.ts +13 -15
- package/types/src/errors.d.ts +73 -17
- package/types/src/graph.d.ts +218 -0
- package/types/src/nodes/collection.d.ts +69 -0
- package/types/src/nodes/effect.d.ts +48 -0
- package/types/src/nodes/list.d.ts +66 -0
- package/types/src/nodes/memo.d.ts +63 -0
- package/types/src/nodes/sensor.d.ts +81 -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 +79 -0
- package/types/src/signal.d.ts +43 -29
- package/types/src/util.d.ts +9 -16
- package/archive/benchmark.ts +0 -683
- package/archive/collection.ts +0 -253
- package/archive/composite.ts +0 -85
- package/archive/computed.ts +0 -195
- package/archive/list.ts +0 -483
- package/archive/memo.ts +0 -139
- package/archive/state.ts +0 -90
- package/archive/store.ts +0 -298
- package/archive/task.ts +0 -189
- package/src/classes/collection.ts +0 -245
- package/src/classes/computed.ts +0 -349
- package/src/classes/list.ts +0 -343
- package/src/classes/ref.ts +0 -70
- package/src/classes/state.ts +0 -102
- package/src/classes/store.ts +0 -262
- package/src/diff.ts +0 -138
- package/src/effect.ts +0 -93
- package/src/match.ts +0 -45
- package/src/resolve.ts +0 -49
- package/src/system.ts +0 -257
- package/test/computed.test.ts +0 -1108
- package/test/diff.test.ts +0 -955
- package/test/match.test.ts +0 -388
- package/test/ref.test.ts +0 -353
- package/test/resolve.test.ts +0 -154
- package/types/src/classes/collection.d.ts +0 -45
- package/types/src/classes/computed.d.ts +0 -94
- package/types/src/classes/list.d.ts +0 -43
- package/types/src/classes/ref.d.ts +0 -35
- package/types/src/classes/state.d.ts +0 -49
- package/types/src/classes/store.d.ts +0 -52
- 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 -78
package/src/graph.ts
ADDED
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
import { CircularDependencyError, type Guard } from './errors'
|
|
2
|
+
|
|
3
|
+
/* === Internal Types === */
|
|
4
|
+
|
|
5
|
+
type SourceFields<T extends {}> = {
|
|
6
|
+
value: T
|
|
7
|
+
sinks: Edge | null
|
|
8
|
+
sinksTail: Edge | null
|
|
9
|
+
stop?: Cleanup
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type OptionsFields<T extends {}> = {
|
|
13
|
+
equals: (a: T, b: T) => boolean
|
|
14
|
+
guard?: Guard<T>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type SinkFields = {
|
|
18
|
+
fn: unknown
|
|
19
|
+
flags: number
|
|
20
|
+
sources: Edge | null
|
|
21
|
+
sourcesTail: Edge | null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type OwnerFields = {
|
|
25
|
+
cleanup: Cleanup | Cleanup[] | null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type AsyncFields = {
|
|
29
|
+
controller: AbortController | undefined
|
|
30
|
+
error: Error | undefined
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type StateNode<T extends {}> = SourceFields<T> & OptionsFields<T>
|
|
34
|
+
|
|
35
|
+
type MemoNode<T extends {}> = SourceFields<T> &
|
|
36
|
+
OptionsFields<T> &
|
|
37
|
+
SinkFields & {
|
|
38
|
+
fn: MemoCallback<T>
|
|
39
|
+
error: Error | undefined
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type TaskNode<T extends {}> = SourceFields<T> &
|
|
43
|
+
OptionsFields<T> &
|
|
44
|
+
SinkFields &
|
|
45
|
+
AsyncFields & {
|
|
46
|
+
fn: (prev: T, abort: AbortSignal) => Promise<T>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type EffectNode = SinkFields &
|
|
50
|
+
OwnerFields & {
|
|
51
|
+
fn: EffectCallback
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type Scope = OwnerFields
|
|
55
|
+
|
|
56
|
+
type SourceNode = SourceFields<unknown & {}>
|
|
57
|
+
type SinkNode = MemoNode<unknown & {}> | TaskNode<unknown & {}> | EffectNode
|
|
58
|
+
type OwnerNode = EffectNode | Scope
|
|
59
|
+
|
|
60
|
+
type Edge = {
|
|
61
|
+
source: SourceNode
|
|
62
|
+
sink: SinkNode
|
|
63
|
+
nextSource: Edge | null
|
|
64
|
+
prevSink: Edge | null
|
|
65
|
+
nextSink: Edge | null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* === Public API Types === */
|
|
69
|
+
|
|
70
|
+
type Signal<T extends {}> = {
|
|
71
|
+
get(): T
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* A cleanup function that can be called to dispose of resources.
|
|
76
|
+
*/
|
|
77
|
+
type Cleanup = () => void
|
|
78
|
+
|
|
79
|
+
// biome-ignore lint/suspicious/noConfusingVoidType: optional Cleanup return type
|
|
80
|
+
type MaybeCleanup = Cleanup | undefined | void
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Options for configuring signal behavior.
|
|
84
|
+
*
|
|
85
|
+
* @template T - The type of value in the signal
|
|
86
|
+
*/
|
|
87
|
+
type SignalOptions<T extends {}> = {
|
|
88
|
+
/**
|
|
89
|
+
* Optional type guard to validate values.
|
|
90
|
+
* If provided, will throw an error if an invalid value is set.
|
|
91
|
+
*/
|
|
92
|
+
guard?: Guard<T>
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Optional custom equality function.
|
|
96
|
+
* Used to determine if a new value is different from the old value.
|
|
97
|
+
* Defaults to reference equality (===).
|
|
98
|
+
*/
|
|
99
|
+
equals?: (a: T, b: T) => boolean
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
type ComputedOptions<T extends {}> = SignalOptions<T> & {
|
|
103
|
+
/**
|
|
104
|
+
* Optional initial value.
|
|
105
|
+
* Useful for reducer patterns so that calculations start with a value of correct type.
|
|
106
|
+
*/
|
|
107
|
+
value?: T
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Optional callback invoked when the signal is first watched by an effect.
|
|
111
|
+
* Receives an `invalidate` function that marks the signal dirty and triggers re-evaluation.
|
|
112
|
+
* Must return a cleanup function that is called when the signal is no longer watched.
|
|
113
|
+
*
|
|
114
|
+
* This enables lazy resource activation for computed signals that need to
|
|
115
|
+
* react to external events (e.g. DOM mutations, timers) in addition to
|
|
116
|
+
* tracked signal dependencies.
|
|
117
|
+
*/
|
|
118
|
+
watched?: (invalidate: () => void) => Cleanup
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* A callback function for memos that computes a value based on the previous value.
|
|
123
|
+
*
|
|
124
|
+
* @template T - The type of value computed
|
|
125
|
+
* @param prev - The previous computed value
|
|
126
|
+
* @returns The new computed value
|
|
127
|
+
*/
|
|
128
|
+
type MemoCallback<T extends {}> = (prev: T | undefined) => T
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* A callback function for tasks that asynchronously computes a value.
|
|
132
|
+
*
|
|
133
|
+
* @template T - The type of value computed
|
|
134
|
+
* @param prev - The previous computed value
|
|
135
|
+
* @param signal - An AbortSignal that will be triggered if the task is aborted
|
|
136
|
+
* @returns A promise that resolves to the new computed value
|
|
137
|
+
*/
|
|
138
|
+
type TaskCallback<T extends {}> = (
|
|
139
|
+
prev: T | undefined,
|
|
140
|
+
signal: AbortSignal,
|
|
141
|
+
) => Promise<T>
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* A callback function for effects that can perform side effects.
|
|
145
|
+
*
|
|
146
|
+
* @returns An optional cleanup function that will be called before the effect re-runs or is disposed
|
|
147
|
+
*/
|
|
148
|
+
type EffectCallback = () => MaybeCleanup
|
|
149
|
+
|
|
150
|
+
/* === Constants === */
|
|
151
|
+
|
|
152
|
+
const TYPE_STATE = 'State'
|
|
153
|
+
const TYPE_MEMO = 'Memo'
|
|
154
|
+
const TYPE_TASK = 'Task'
|
|
155
|
+
const TYPE_SENSOR = 'Sensor'
|
|
156
|
+
const TYPE_LIST = 'List'
|
|
157
|
+
const TYPE_COLLECTION = 'Collection'
|
|
158
|
+
const TYPE_STORE = 'Store'
|
|
159
|
+
|
|
160
|
+
const FLAG_CLEAN = 0
|
|
161
|
+
const FLAG_CHECK = 1 << 0
|
|
162
|
+
const FLAG_DIRTY = 1 << 1
|
|
163
|
+
const FLAG_RUNNING = 1 << 2
|
|
164
|
+
|
|
165
|
+
/* === Module State === */
|
|
166
|
+
|
|
167
|
+
let activeSink: SinkNode | null = null
|
|
168
|
+
let activeOwner: OwnerNode | null = null
|
|
169
|
+
const queuedEffects: EffectNode[] = []
|
|
170
|
+
let batchDepth = 0
|
|
171
|
+
let flushing = false
|
|
172
|
+
|
|
173
|
+
/* === Utility Functions === */
|
|
174
|
+
|
|
175
|
+
const DEFAULT_EQUALITY = <T extends {}>(a: T, b: T): boolean => a === b
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Equality function that always returns false, causing propagation on every update.
|
|
179
|
+
* Use with `createSensor` for observing mutable objects where the reference stays the same
|
|
180
|
+
* but internal state changes (e.g., DOM elements observed via MutationObserver).
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* ```ts
|
|
184
|
+
* const el = createSensor<HTMLElement>((set) => {
|
|
185
|
+
* const node = document.getElementById('box')!;
|
|
186
|
+
* set(node);
|
|
187
|
+
* const obs = new MutationObserver(() => set(node));
|
|
188
|
+
* obs.observe(node, { attributes: true });
|
|
189
|
+
* return () => obs.disconnect();
|
|
190
|
+
* }, { value: node, equals: SKIP_EQUALITY });
|
|
191
|
+
* ```
|
|
192
|
+
*/
|
|
193
|
+
const SKIP_EQUALITY = (_a?: unknown, _b?: unknown): boolean => false
|
|
194
|
+
|
|
195
|
+
/* === Link Management === */
|
|
196
|
+
|
|
197
|
+
function isValidEdge(checkEdge: Edge, node: SinkNode): boolean {
|
|
198
|
+
const sourcesTail = node.sourcesTail
|
|
199
|
+
if (sourcesTail) {
|
|
200
|
+
let edge = node.sources
|
|
201
|
+
while (edge) {
|
|
202
|
+
if (edge === checkEdge) return true
|
|
203
|
+
if (edge === sourcesTail) break
|
|
204
|
+
edge = edge.nextSource
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return false
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function link(source: SourceNode, sink: SinkNode): void {
|
|
211
|
+
const prevSource = sink.sourcesTail
|
|
212
|
+
if (prevSource?.source === source) return
|
|
213
|
+
|
|
214
|
+
let nextSource: Edge | null = null
|
|
215
|
+
const isRecomputing = sink.flags & FLAG_RUNNING
|
|
216
|
+
if (isRecomputing) {
|
|
217
|
+
nextSource = prevSource ? prevSource.nextSource : sink.sources
|
|
218
|
+
if (nextSource?.source === source) {
|
|
219
|
+
sink.sourcesTail = nextSource
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const prevSink = source.sinksTail
|
|
225
|
+
if (
|
|
226
|
+
prevSink?.sink === sink &&
|
|
227
|
+
(!isRecomputing || isValidEdge(prevSink, sink))
|
|
228
|
+
)
|
|
229
|
+
return
|
|
230
|
+
|
|
231
|
+
const newEdge = { source, sink, nextSource, prevSink, nextSink: null }
|
|
232
|
+
sink.sourcesTail = source.sinksTail = newEdge
|
|
233
|
+
if (prevSource) prevSource.nextSource = newEdge
|
|
234
|
+
else sink.sources = newEdge
|
|
235
|
+
if (prevSink) prevSink.nextSink = newEdge
|
|
236
|
+
else source.sinks = newEdge
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function unlink(edge: Edge): Edge | null {
|
|
240
|
+
const { source, nextSource, nextSink, prevSink } = edge
|
|
241
|
+
|
|
242
|
+
if (nextSink) nextSink.prevSink = prevSink
|
|
243
|
+
else source.sinksTail = prevSink
|
|
244
|
+
if (prevSink) prevSink.nextSink = nextSink
|
|
245
|
+
else source.sinks = nextSink
|
|
246
|
+
|
|
247
|
+
if (!source.sinks && source.stop) {
|
|
248
|
+
source.stop()
|
|
249
|
+
source.stop = undefined
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return nextSource
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function trimSources(node: SinkNode): void {
|
|
256
|
+
const tail = node.sourcesTail
|
|
257
|
+
let source = tail ? tail.nextSource : node.sources
|
|
258
|
+
while (source) source = unlink(source)
|
|
259
|
+
if (tail) tail.nextSource = null
|
|
260
|
+
else node.sources = null
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/* === Propagation === */
|
|
264
|
+
|
|
265
|
+
function propagate(node: SinkNode, newFlag = FLAG_DIRTY): void {
|
|
266
|
+
const flags = node.flags
|
|
267
|
+
|
|
268
|
+
if ('sinks' in node) {
|
|
269
|
+
if ((flags & (FLAG_DIRTY | FLAG_CHECK)) >= newFlag) return
|
|
270
|
+
|
|
271
|
+
node.flags = flags | newFlag
|
|
272
|
+
|
|
273
|
+
// Abort in-flight work when sources change
|
|
274
|
+
if ('controller' in node && node.controller) {
|
|
275
|
+
node.controller.abort()
|
|
276
|
+
node.controller = undefined
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Propagate Check to sinks
|
|
280
|
+
for (let e = node.sinks; e; e = e.nextSink)
|
|
281
|
+
propagate(e.sink, FLAG_CHECK)
|
|
282
|
+
} else {
|
|
283
|
+
if (flags & FLAG_DIRTY) return
|
|
284
|
+
|
|
285
|
+
// Enqueue effect for later execution
|
|
286
|
+
node.flags = FLAG_DIRTY
|
|
287
|
+
queuedEffects.push(node as EffectNode)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/* === State Management === */
|
|
292
|
+
|
|
293
|
+
function setState<T extends {}>(node: StateNode<T>, next: T): void {
|
|
294
|
+
if (node.equals(node.value, next)) return
|
|
295
|
+
|
|
296
|
+
node.value = next
|
|
297
|
+
for (let e = node.sinks; e; e = e.nextSink) propagate(e.sink)
|
|
298
|
+
if (batchDepth === 0) flush()
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/* === Cleanup Management === */
|
|
302
|
+
|
|
303
|
+
function registerCleanup(owner: OwnerNode, fn: Cleanup): void {
|
|
304
|
+
if (!owner.cleanup) owner.cleanup = fn
|
|
305
|
+
else if (Array.isArray(owner.cleanup)) owner.cleanup.push(fn)
|
|
306
|
+
else owner.cleanup = [owner.cleanup, fn]
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function runCleanup(owner: OwnerNode): void {
|
|
310
|
+
if (!owner.cleanup) return
|
|
311
|
+
|
|
312
|
+
if (Array.isArray(owner.cleanup))
|
|
313
|
+
for (let i = 0; i < owner.cleanup.length; i++) owner.cleanup[i]()
|
|
314
|
+
else owner.cleanup()
|
|
315
|
+
owner.cleanup = null
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/* === Recomputation === */
|
|
319
|
+
|
|
320
|
+
function recomputeMemo(node: MemoNode<unknown & {}>): void {
|
|
321
|
+
const prevWatcher = activeSink
|
|
322
|
+
activeSink = node
|
|
323
|
+
node.sourcesTail = null
|
|
324
|
+
node.flags = FLAG_RUNNING
|
|
325
|
+
|
|
326
|
+
let changed = false
|
|
327
|
+
try {
|
|
328
|
+
const next = node.fn(node.value)
|
|
329
|
+
if (node.error || !node.equals(next, node.value)) {
|
|
330
|
+
node.value = next
|
|
331
|
+
node.error = undefined
|
|
332
|
+
changed = true
|
|
333
|
+
}
|
|
334
|
+
} catch (err: unknown) {
|
|
335
|
+
changed = true
|
|
336
|
+
node.error = err instanceof Error ? err : new Error(String(err))
|
|
337
|
+
} finally {
|
|
338
|
+
activeSink = prevWatcher
|
|
339
|
+
trimSources(node)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (changed) {
|
|
343
|
+
for (let e = node.sinks; e; e = e.nextSink)
|
|
344
|
+
if (e.sink.flags & FLAG_CHECK) e.sink.flags |= FLAG_DIRTY
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
node.flags = FLAG_CLEAN
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function recomputeTask(node: TaskNode<unknown & {}>): void {
|
|
351
|
+
node.controller?.abort()
|
|
352
|
+
|
|
353
|
+
const controller = new AbortController()
|
|
354
|
+
node.controller = controller
|
|
355
|
+
node.error = undefined
|
|
356
|
+
|
|
357
|
+
const prevWatcher = activeSink
|
|
358
|
+
activeSink = node
|
|
359
|
+
node.sourcesTail = null
|
|
360
|
+
node.flags = FLAG_RUNNING
|
|
361
|
+
|
|
362
|
+
let promise: Promise<unknown & {}>
|
|
363
|
+
try {
|
|
364
|
+
promise = node.fn(node.value, controller.signal)
|
|
365
|
+
} catch (err) {
|
|
366
|
+
node.controller = undefined
|
|
367
|
+
node.error = err instanceof Error ? err : new Error(String(err))
|
|
368
|
+
return
|
|
369
|
+
} finally {
|
|
370
|
+
activeSink = prevWatcher
|
|
371
|
+
trimSources(node)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
promise.then(
|
|
375
|
+
next => {
|
|
376
|
+
if (controller.signal.aborted) return
|
|
377
|
+
|
|
378
|
+
node.controller = undefined
|
|
379
|
+
if (node.error || !node.equals(next, node.value)) {
|
|
380
|
+
node.value = next
|
|
381
|
+
node.error = undefined
|
|
382
|
+
for (let e = node.sinks; e; e = e.nextSink) propagate(e.sink)
|
|
383
|
+
if (batchDepth === 0) flush()
|
|
384
|
+
}
|
|
385
|
+
},
|
|
386
|
+
(err: unknown) => {
|
|
387
|
+
if (controller.signal.aborted) return
|
|
388
|
+
|
|
389
|
+
node.controller = undefined
|
|
390
|
+
const error = err instanceof Error ? err : new Error(String(err))
|
|
391
|
+
if (
|
|
392
|
+
!node.error ||
|
|
393
|
+
error.name !== node.error.name ||
|
|
394
|
+
error.message !== node.error.message
|
|
395
|
+
) {
|
|
396
|
+
// We don't clear old value on errors
|
|
397
|
+
node.error = error
|
|
398
|
+
for (let e = node.sinks; e; e = e.nextSink) propagate(e.sink)
|
|
399
|
+
if (batchDepth === 0) flush()
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
node.flags = FLAG_CLEAN
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function runEffect(node: EffectNode): void {
|
|
408
|
+
runCleanup(node)
|
|
409
|
+
const prevContext = activeSink
|
|
410
|
+
const prevOwner = activeOwner
|
|
411
|
+
activeSink = activeOwner = node
|
|
412
|
+
node.sourcesTail = null
|
|
413
|
+
node.flags = FLAG_RUNNING
|
|
414
|
+
|
|
415
|
+
try {
|
|
416
|
+
const out = node.fn()
|
|
417
|
+
if (typeof out === 'function') registerCleanup(node, out)
|
|
418
|
+
} finally {
|
|
419
|
+
activeSink = prevContext
|
|
420
|
+
activeOwner = prevOwner
|
|
421
|
+
trimSources(node)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
node.flags = FLAG_CLEAN
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function refresh(node: SinkNode): void {
|
|
428
|
+
if (node.flags & FLAG_CHECK) {
|
|
429
|
+
for (let e = node.sources; e; e = e.nextSource) {
|
|
430
|
+
if ('fn' in e.source) refresh(e.source as SinkNode)
|
|
431
|
+
if (node.flags & FLAG_DIRTY) break
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (node.flags & FLAG_RUNNING) {
|
|
436
|
+
throw new CircularDependencyError(
|
|
437
|
+
'controller' in node
|
|
438
|
+
? TYPE_TASK
|
|
439
|
+
: 'value' in node
|
|
440
|
+
? TYPE_MEMO
|
|
441
|
+
: 'Effect',
|
|
442
|
+
)
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (node.flags & FLAG_DIRTY) {
|
|
446
|
+
if ('controller' in node) recomputeTask(node)
|
|
447
|
+
else if ('value' in node) recomputeMemo(node)
|
|
448
|
+
else runEffect(node)
|
|
449
|
+
} else {
|
|
450
|
+
node.flags = FLAG_CLEAN
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/* === Batching === */
|
|
455
|
+
|
|
456
|
+
function flush(): void {
|
|
457
|
+
if (flushing) return
|
|
458
|
+
flushing = true
|
|
459
|
+
try {
|
|
460
|
+
for (let i = 0; i < queuedEffects.length; i++) {
|
|
461
|
+
const effect = queuedEffects[i]
|
|
462
|
+
if (effect.flags & FLAG_DIRTY) refresh(effect)
|
|
463
|
+
}
|
|
464
|
+
queuedEffects.length = 0
|
|
465
|
+
} finally {
|
|
466
|
+
flushing = false
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Batches multiple signal updates together.
|
|
472
|
+
* Effects will not run until the batch completes.
|
|
473
|
+
* Batches can be nested; effects run when the outermost batch completes.
|
|
474
|
+
*
|
|
475
|
+
* @param fn - The function to execute within the batch
|
|
476
|
+
*
|
|
477
|
+
* @example
|
|
478
|
+
* ```ts
|
|
479
|
+
* const count = createState(0);
|
|
480
|
+
* const double = createMemo(() => count.get() * 2);
|
|
481
|
+
*
|
|
482
|
+
* batch(() => {
|
|
483
|
+
* count.set(1);
|
|
484
|
+
* count.set(2);
|
|
485
|
+
* count.set(3);
|
|
486
|
+
* // Effects run only once at the end with count = 3
|
|
487
|
+
* });
|
|
488
|
+
* ```
|
|
489
|
+
*/
|
|
490
|
+
function batch(fn: () => void): void {
|
|
491
|
+
batchDepth++
|
|
492
|
+
try {
|
|
493
|
+
fn()
|
|
494
|
+
} finally {
|
|
495
|
+
batchDepth--
|
|
496
|
+
if (batchDepth === 0) flush()
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Runs a callback without tracking dependencies.
|
|
502
|
+
* Any signal reads inside the callback will not create edges to the current active sink.
|
|
503
|
+
*
|
|
504
|
+
* @param fn - The function to execute without tracking
|
|
505
|
+
* @returns The return value of the function
|
|
506
|
+
*
|
|
507
|
+
* @example
|
|
508
|
+
* ```ts
|
|
509
|
+
* const count = createState(0);
|
|
510
|
+
* const label = createState('Count');
|
|
511
|
+
*
|
|
512
|
+
* createEffect(() => {
|
|
513
|
+
* // Only re-runs when count changes, not when label changes
|
|
514
|
+
* const name = untrack(() => label.get());
|
|
515
|
+
* console.log(`${name}: ${count.get()}`);
|
|
516
|
+
* });
|
|
517
|
+
* ```
|
|
518
|
+
*/
|
|
519
|
+
function untrack<T>(fn: () => T): T {
|
|
520
|
+
const prev = activeSink
|
|
521
|
+
activeSink = null
|
|
522
|
+
try {
|
|
523
|
+
return fn()
|
|
524
|
+
} finally {
|
|
525
|
+
activeSink = prev
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/* === Scope Management === */
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Creates a new ownership scope for managing cleanup of nested effects and resources.
|
|
533
|
+
* All effects created within the scope will be automatically disposed when the scope is disposed.
|
|
534
|
+
* Scopes can be nested - disposing a parent scope disposes all child scopes.
|
|
535
|
+
*
|
|
536
|
+
* @param fn - The function to execute within the scope, may return a cleanup function
|
|
537
|
+
* @returns A dispose function that cleans up the scope
|
|
538
|
+
*
|
|
539
|
+
* @example
|
|
540
|
+
* ```ts
|
|
541
|
+
* const dispose = createScope(() => {
|
|
542
|
+
* const count = createState(0);
|
|
543
|
+
*
|
|
544
|
+
* createEffect(() => {
|
|
545
|
+
* console.log(count.get());
|
|
546
|
+
* });
|
|
547
|
+
*
|
|
548
|
+
* return () => console.log('Scope disposed');
|
|
549
|
+
* });
|
|
550
|
+
*
|
|
551
|
+
* dispose(); // Cleans up the effect and runs cleanup callbacks
|
|
552
|
+
* ```
|
|
553
|
+
*/
|
|
554
|
+
function createScope(fn: () => MaybeCleanup): Cleanup {
|
|
555
|
+
const prevOwner = activeOwner
|
|
556
|
+
const scope: Scope = { cleanup: null }
|
|
557
|
+
activeOwner = scope
|
|
558
|
+
|
|
559
|
+
try {
|
|
560
|
+
const out = fn()
|
|
561
|
+
if (typeof out === 'function') registerCleanup(scope, out)
|
|
562
|
+
const dispose = () => runCleanup(scope)
|
|
563
|
+
if (prevOwner) registerCleanup(prevOwner, dispose)
|
|
564
|
+
return dispose
|
|
565
|
+
} finally {
|
|
566
|
+
activeOwner = prevOwner
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
export {
|
|
571
|
+
type Cleanup,
|
|
572
|
+
type ComputedOptions,
|
|
573
|
+
type EffectCallback,
|
|
574
|
+
type EffectNode,
|
|
575
|
+
type MaybeCleanup,
|
|
576
|
+
type MemoCallback,
|
|
577
|
+
type MemoNode,
|
|
578
|
+
type Scope,
|
|
579
|
+
type Signal,
|
|
580
|
+
type SignalOptions,
|
|
581
|
+
type SinkNode,
|
|
582
|
+
type StateNode,
|
|
583
|
+
type TaskCallback,
|
|
584
|
+
type TaskNode,
|
|
585
|
+
activeOwner,
|
|
586
|
+
activeSink,
|
|
587
|
+
batch,
|
|
588
|
+
batchDepth,
|
|
589
|
+
createScope,
|
|
590
|
+
DEFAULT_EQUALITY,
|
|
591
|
+
SKIP_EQUALITY,
|
|
592
|
+
FLAG_CLEAN,
|
|
593
|
+
FLAG_DIRTY,
|
|
594
|
+
flush,
|
|
595
|
+
link,
|
|
596
|
+
propagate,
|
|
597
|
+
refresh,
|
|
598
|
+
registerCleanup,
|
|
599
|
+
runCleanup,
|
|
600
|
+
runEffect,
|
|
601
|
+
setState,
|
|
602
|
+
trimSources,
|
|
603
|
+
TYPE_COLLECTION,
|
|
604
|
+
TYPE_LIST,
|
|
605
|
+
TYPE_MEMO,
|
|
606
|
+
TYPE_SENSOR,
|
|
607
|
+
TYPE_STATE,
|
|
608
|
+
TYPE_STORE,
|
|
609
|
+
TYPE_TASK,
|
|
610
|
+
unlink,
|
|
611
|
+
untrack,
|
|
612
|
+
}
|