@fictjs/runtime 0.0.2
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/README.md +17 -0
- package/dist/index.cjs +4224 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1572 -0
- package/dist/index.d.ts +1572 -0
- package/dist/index.dev.js +4240 -0
- package/dist/index.dev.js.map +1 -0
- package/dist/index.js +4133 -0
- package/dist/index.js.map +1 -0
- package/dist/jsx-dev-runtime.cjs +44 -0
- package/dist/jsx-dev-runtime.cjs.map +1 -0
- package/dist/jsx-dev-runtime.js +14 -0
- package/dist/jsx-dev-runtime.js.map +1 -0
- package/dist/jsx-runtime.cjs +44 -0
- package/dist/jsx-runtime.cjs.map +1 -0
- package/dist/jsx-runtime.js +14 -0
- package/dist/jsx-runtime.js.map +1 -0
- package/dist/slim.cjs +3384 -0
- package/dist/slim.cjs.map +1 -0
- package/dist/slim.d.cts +475 -0
- package/dist/slim.d.ts +475 -0
- package/dist/slim.js +3335 -0
- package/dist/slim.js.map +1 -0
- package/package.json +68 -0
- package/src/binding.ts +2127 -0
- package/src/constants.ts +456 -0
- package/src/cycle-guard.ts +134 -0
- package/src/devtools.ts +17 -0
- package/src/dom.ts +683 -0
- package/src/effect.ts +83 -0
- package/src/error-boundary.ts +118 -0
- package/src/hooks.ts +72 -0
- package/src/index.ts +184 -0
- package/src/jsx-dev-runtime.ts +2 -0
- package/src/jsx-runtime.ts +2 -0
- package/src/jsx.ts +786 -0
- package/src/lifecycle.ts +273 -0
- package/src/list-helpers.ts +619 -0
- package/src/memo.ts +14 -0
- package/src/node-ops.ts +185 -0
- package/src/props.ts +212 -0
- package/src/reconcile.ts +151 -0
- package/src/ref.ts +25 -0
- package/src/scheduler.ts +12 -0
- package/src/signal.ts +1278 -0
- package/src/slim.ts +68 -0
- package/src/store.ts +210 -0
- package/src/suspense.ts +187 -0
- package/src/transition.ts +128 -0
- package/src/types.ts +172 -0
- package/src/versioned-signal.ts +58 -0
package/src/signal.ts
ADDED
|
@@ -0,0 +1,1278 @@
|
|
|
1
|
+
import { beginFlushGuard, beforeEffectRunGuard, endFlushGuard } from './cycle-guard'
|
|
2
|
+
import { getDevtoolsHook } from './devtools'
|
|
3
|
+
import { registerRootCleanup } from './lifecycle'
|
|
4
|
+
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// Type Definitions
|
|
7
|
+
// ============================================================================
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Reactive node that can be either a signal, computed, effect, or effect scope
|
|
11
|
+
*/
|
|
12
|
+
export type ReactiveNode =
|
|
13
|
+
| SignalNode<unknown>
|
|
14
|
+
| ComputedNode<unknown>
|
|
15
|
+
| EffectNode
|
|
16
|
+
| EffectScopeNode
|
|
17
|
+
| SubscriberNode
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Link between a dependency and a subscriber in the reactive graph
|
|
21
|
+
*/
|
|
22
|
+
export interface Link {
|
|
23
|
+
/** Version/cycle when this link was created */
|
|
24
|
+
version: number
|
|
25
|
+
/** The dependency being tracked */
|
|
26
|
+
dep: ReactiveNode
|
|
27
|
+
/** The subscriber tracking this dependency */
|
|
28
|
+
sub: ReactiveNode
|
|
29
|
+
/** Previous dependency link in the subscriber's dependency list */
|
|
30
|
+
prevDep: Link | undefined
|
|
31
|
+
/** Next dependency link in the subscriber's dependency list */
|
|
32
|
+
nextDep: Link | undefined
|
|
33
|
+
/** Previous subscriber link in the dependency's subscriber list */
|
|
34
|
+
prevSub: Link | undefined
|
|
35
|
+
/** Next subscriber link in the dependency's subscriber list */
|
|
36
|
+
nextSub: Link | undefined
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Stack frame for traversing the reactive graph
|
|
41
|
+
*/
|
|
42
|
+
export interface StackFrame {
|
|
43
|
+
/** The link value at this stack level */
|
|
44
|
+
value: Link | undefined
|
|
45
|
+
/** Previous stack frame */
|
|
46
|
+
prev: StackFrame | undefined
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Base interface for all reactive nodes
|
|
51
|
+
*/
|
|
52
|
+
export interface BaseNode {
|
|
53
|
+
/** First subscriber link */
|
|
54
|
+
subs: Link | undefined
|
|
55
|
+
/** Last subscriber link */
|
|
56
|
+
subsTail: Link | undefined
|
|
57
|
+
/** Reactive flags (Mutable, Watching, Running, etc.) */
|
|
58
|
+
flags: number
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Signal node - mutable reactive value
|
|
63
|
+
*/
|
|
64
|
+
export interface SignalNode<T = unknown> extends BaseNode {
|
|
65
|
+
/** Current committed value */
|
|
66
|
+
currentValue: T
|
|
67
|
+
/** Pending value to be committed */
|
|
68
|
+
pendingValue: T
|
|
69
|
+
/** Signals don't have dependencies */
|
|
70
|
+
deps?: undefined
|
|
71
|
+
depsTail?: undefined
|
|
72
|
+
getter?: undefined
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Computed node - derived reactive value
|
|
77
|
+
*/
|
|
78
|
+
export interface ComputedNode<T = unknown> extends BaseNode {
|
|
79
|
+
/** Current computed value */
|
|
80
|
+
value: T
|
|
81
|
+
/** First dependency link */
|
|
82
|
+
deps: Link | undefined
|
|
83
|
+
/** Last dependency link */
|
|
84
|
+
depsTail: Link | undefined
|
|
85
|
+
/** Getter function to compute the value */
|
|
86
|
+
getter: (oldValue: T | undefined) => T
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Effect node - side effect that runs when dependencies change
|
|
91
|
+
*/
|
|
92
|
+
export interface EffectNode extends BaseNode {
|
|
93
|
+
/** Effect function to execute */
|
|
94
|
+
fn: () => void
|
|
95
|
+
/** First dependency link */
|
|
96
|
+
deps: Link | undefined
|
|
97
|
+
/** Last dependency link */
|
|
98
|
+
depsTail: Link | undefined
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Effect scope node - manages multiple effects
|
|
103
|
+
*/
|
|
104
|
+
export interface EffectScopeNode extends BaseNode {
|
|
105
|
+
/** First dependency link */
|
|
106
|
+
deps: Link | undefined
|
|
107
|
+
/** Last dependency link */
|
|
108
|
+
depsTail: Link | undefined
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Subscriber node used in trigger
|
|
113
|
+
*/
|
|
114
|
+
export interface SubscriberNode {
|
|
115
|
+
/** First dependency link */
|
|
116
|
+
deps: Link | undefined
|
|
117
|
+
/** Last dependency link */
|
|
118
|
+
depsTail: Link | undefined
|
|
119
|
+
/** Reactive flags */
|
|
120
|
+
flags: number
|
|
121
|
+
subs?: undefined
|
|
122
|
+
subsTail?: undefined
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Signal accessor - function to get/set signal value
|
|
127
|
+
*/
|
|
128
|
+
export interface SignalAccessor<T> {
|
|
129
|
+
(): T
|
|
130
|
+
(value: T): void
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Computed accessor - function to get computed value
|
|
135
|
+
*/
|
|
136
|
+
export type ComputedAccessor<T> = () => T
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Effect disposer - function to dispose an effect
|
|
140
|
+
*/
|
|
141
|
+
export type EffectDisposer = () => void
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Effect scope disposer - function to dispose an effect scope
|
|
145
|
+
*/
|
|
146
|
+
export type EffectScopeDisposer = () => void
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Options for creating a custom reactive system
|
|
150
|
+
*/
|
|
151
|
+
export interface ReactiveSystemOptions {
|
|
152
|
+
/** Update function for reactive nodes */
|
|
153
|
+
update: (node: ReactiveNode) => boolean
|
|
154
|
+
/** Notify function when a subscriber needs to be notified */
|
|
155
|
+
notify: (sub: ReactiveNode) => void
|
|
156
|
+
/** Callback when a dependency becomes unwatched */
|
|
157
|
+
unwatched: (dep: ReactiveNode) => void
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Custom reactive system methods
|
|
162
|
+
*/
|
|
163
|
+
export interface ReactiveSystem {
|
|
164
|
+
/** Link a dependency to a subscriber */
|
|
165
|
+
link: typeof link
|
|
166
|
+
/** Unlink a dependency from a subscriber */
|
|
167
|
+
unlink: (lnk: Link, sub?: ReactiveNode) => Link | undefined
|
|
168
|
+
/** Propagate changes through the reactive graph */
|
|
169
|
+
propagate: (firstLink: Link) => void
|
|
170
|
+
/** Check if a node is dirty */
|
|
171
|
+
checkDirty: (firstLink: Link, sub: ReactiveNode) => boolean
|
|
172
|
+
/** Shallow propagate changes */
|
|
173
|
+
shallowPropagate: (firstLink: Link) => void
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ============================================================================
|
|
177
|
+
// Flags
|
|
178
|
+
// ============================================================================
|
|
179
|
+
const Mutable = 1
|
|
180
|
+
const Watching = 2
|
|
181
|
+
const Running = 4
|
|
182
|
+
const Recursed = 8
|
|
183
|
+
const Dirty = 16
|
|
184
|
+
const Pending = 32
|
|
185
|
+
// Pre-computed combinations
|
|
186
|
+
const MutableDirty = 17
|
|
187
|
+
const MutablePending = 33
|
|
188
|
+
const MutableRunning = 5
|
|
189
|
+
const WatchingRunning = 6
|
|
190
|
+
// Global state
|
|
191
|
+
let cycle = 0
|
|
192
|
+
let batchDepth = 0
|
|
193
|
+
let activeSub: ReactiveNode | undefined
|
|
194
|
+
let flushScheduled = false
|
|
195
|
+
// Dual-priority queue for scheduler
|
|
196
|
+
const highPriorityQueue: EffectNode[] = []
|
|
197
|
+
const lowPriorityQueue: EffectNode[] = []
|
|
198
|
+
let isInTransition = false
|
|
199
|
+
const enqueueMicrotask =
|
|
200
|
+
typeof queueMicrotask === 'function'
|
|
201
|
+
? queueMicrotask
|
|
202
|
+
: (fn: () => void) => {
|
|
203
|
+
Promise.resolve().then(fn)
|
|
204
|
+
}
|
|
205
|
+
export const ReactiveFlags = {
|
|
206
|
+
None: 0,
|
|
207
|
+
Mutable,
|
|
208
|
+
Watching,
|
|
209
|
+
RecursedCheck: Running,
|
|
210
|
+
Recursed,
|
|
211
|
+
Dirty,
|
|
212
|
+
Pending,
|
|
213
|
+
}
|
|
214
|
+
// ============================================================================
|
|
215
|
+
// createReactiveSystem - Support for custom systems
|
|
216
|
+
// ============================================================================
|
|
217
|
+
/**
|
|
218
|
+
* Create a custom reactive system with custom update, notify, and unwatched handlers
|
|
219
|
+
* @param options - Reactive system options
|
|
220
|
+
* @returns Custom reactive system methods
|
|
221
|
+
*/
|
|
222
|
+
export function createReactiveSystem({
|
|
223
|
+
update,
|
|
224
|
+
notify: notifyFn,
|
|
225
|
+
unwatched: unwatchedFn,
|
|
226
|
+
}: ReactiveSystemOptions): ReactiveSystem {
|
|
227
|
+
function customPropagate(firstLink: Link): void {
|
|
228
|
+
let link = firstLink
|
|
229
|
+
let next = link.nextSub
|
|
230
|
+
let stack: StackFrame | undefined
|
|
231
|
+
|
|
232
|
+
top: for (;;) {
|
|
233
|
+
const sub = link.sub
|
|
234
|
+
let flags = sub.flags
|
|
235
|
+
|
|
236
|
+
if (!(flags & 60)) {
|
|
237
|
+
sub.flags = flags | Pending
|
|
238
|
+
} else if (!(flags & 12)) {
|
|
239
|
+
flags = 0
|
|
240
|
+
} else if (!(flags & Running)) {
|
|
241
|
+
sub.flags = (flags & ~Recursed) | Pending
|
|
242
|
+
} else if (!(flags & 48)) {
|
|
243
|
+
let vlink = sub.depsTail
|
|
244
|
+
let valid = false
|
|
245
|
+
while (vlink !== undefined) {
|
|
246
|
+
if (vlink === link) {
|
|
247
|
+
valid = true
|
|
248
|
+
break
|
|
249
|
+
}
|
|
250
|
+
vlink = vlink.prevDep
|
|
251
|
+
}
|
|
252
|
+
if (valid) {
|
|
253
|
+
sub.flags = flags | 40
|
|
254
|
+
flags &= Mutable
|
|
255
|
+
} else {
|
|
256
|
+
flags = 0
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
flags = 0
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (flags & Watching) notifyFn(sub)
|
|
263
|
+
|
|
264
|
+
if (flags & Mutable) {
|
|
265
|
+
const subSubs = sub.subs
|
|
266
|
+
if (subSubs !== undefined) {
|
|
267
|
+
const nextSub = subSubs.nextSub
|
|
268
|
+
if (nextSub !== undefined) {
|
|
269
|
+
stack = { value: next, prev: stack }
|
|
270
|
+
next = nextSub
|
|
271
|
+
}
|
|
272
|
+
link = subSubs
|
|
273
|
+
continue
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (next !== undefined) {
|
|
278
|
+
link = next
|
|
279
|
+
next = link.nextSub
|
|
280
|
+
continue
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
while (stack !== undefined) {
|
|
284
|
+
link = stack.value!
|
|
285
|
+
stack = stack.prev
|
|
286
|
+
if (link !== undefined) {
|
|
287
|
+
next = link.nextSub
|
|
288
|
+
continue top
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
break
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
function customCheckDirty(firstLink: Link, sub: ReactiveNode): boolean {
|
|
295
|
+
let link = firstLink
|
|
296
|
+
let stack: StackFrame | undefined
|
|
297
|
+
let checkDepth = 0
|
|
298
|
+
let dirty = false
|
|
299
|
+
|
|
300
|
+
top: for (;;) {
|
|
301
|
+
const dep = link.dep
|
|
302
|
+
const depFlags = dep.flags
|
|
303
|
+
|
|
304
|
+
if (sub.flags & Dirty) {
|
|
305
|
+
dirty = true
|
|
306
|
+
} else if ((depFlags & MutableDirty) === MutableDirty) {
|
|
307
|
+
if (update(dep)) {
|
|
308
|
+
const subs = dep.subs
|
|
309
|
+
if (subs !== undefined && subs.nextSub !== undefined) {
|
|
310
|
+
customShallowPropagate(subs)
|
|
311
|
+
}
|
|
312
|
+
dirty = true
|
|
313
|
+
}
|
|
314
|
+
} else if ((depFlags & MutablePending) === MutablePending) {
|
|
315
|
+
if (link.nextSub !== undefined || link.prevSub !== undefined) {
|
|
316
|
+
stack = { value: link, prev: stack }
|
|
317
|
+
}
|
|
318
|
+
link = dep.deps!
|
|
319
|
+
sub = dep
|
|
320
|
+
++checkDepth
|
|
321
|
+
continue
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (!dirty) {
|
|
325
|
+
const nextDep = link.nextDep
|
|
326
|
+
if (nextDep !== undefined) {
|
|
327
|
+
link = nextDep
|
|
328
|
+
continue
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
while (checkDepth-- > 0) {
|
|
333
|
+
const firstSub = sub.subs!
|
|
334
|
+
const hasMultipleSubs = firstSub.nextSub !== undefined
|
|
335
|
+
|
|
336
|
+
if (hasMultipleSubs) {
|
|
337
|
+
link = stack!.value!
|
|
338
|
+
stack = stack!.prev
|
|
339
|
+
} else link = firstSub
|
|
340
|
+
|
|
341
|
+
if (dirty) {
|
|
342
|
+
if (update(sub)) {
|
|
343
|
+
if (hasMultipleSubs) customShallowPropagate(firstSub)
|
|
344
|
+
sub = link.sub
|
|
345
|
+
continue
|
|
346
|
+
}
|
|
347
|
+
dirty = false
|
|
348
|
+
} else {
|
|
349
|
+
sub.flags &= ~Pending
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
sub = link.sub
|
|
353
|
+
const nextDep = link.nextDep
|
|
354
|
+
if (nextDep !== undefined) {
|
|
355
|
+
link = nextDep
|
|
356
|
+
continue top
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return dirty
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
function customShallowPropagate(firstLink: Link): void {
|
|
364
|
+
let link: Link | undefined = firstLink
|
|
365
|
+
do {
|
|
366
|
+
const sub = link.sub
|
|
367
|
+
const flags = sub.flags
|
|
368
|
+
if ((flags & 48) === Pending) {
|
|
369
|
+
sub.flags = flags | Dirty
|
|
370
|
+
if ((flags & 6) === Watching) notifyFn(sub)
|
|
371
|
+
}
|
|
372
|
+
link = link.nextSub
|
|
373
|
+
} while (link !== undefined)
|
|
374
|
+
}
|
|
375
|
+
function customUnlink(lnk: Link, sub: ReactiveNode = lnk.sub): Link | undefined {
|
|
376
|
+
const dep = lnk.dep
|
|
377
|
+
const prevDep = lnk.prevDep
|
|
378
|
+
const nextDep = lnk.nextDep
|
|
379
|
+
const nextSub = lnk.nextSub
|
|
380
|
+
const prevSub = lnk.prevSub
|
|
381
|
+
|
|
382
|
+
if (nextDep !== undefined) nextDep.prevDep = prevDep
|
|
383
|
+
else sub.depsTail = prevDep
|
|
384
|
+
if (prevDep !== undefined) prevDep.nextDep = nextDep
|
|
385
|
+
else sub.deps = nextDep
|
|
386
|
+
|
|
387
|
+
if (nextSub !== undefined) nextSub.prevSub = prevSub
|
|
388
|
+
else dep.subsTail = prevSub
|
|
389
|
+
if (prevSub !== undefined) prevSub.nextSub = nextSub
|
|
390
|
+
else if ((dep.subs = nextSub) === undefined) unwatchedFn(dep)
|
|
391
|
+
|
|
392
|
+
return nextDep
|
|
393
|
+
}
|
|
394
|
+
return {
|
|
395
|
+
link,
|
|
396
|
+
unlink: customUnlink,
|
|
397
|
+
propagate: customPropagate,
|
|
398
|
+
checkDirty: customCheckDirty,
|
|
399
|
+
shallowPropagate: customShallowPropagate,
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
// ============================================================================
|
|
403
|
+
// Core functions
|
|
404
|
+
// ============================================================================
|
|
405
|
+
/**
|
|
406
|
+
* Create a link between a dependency and a subscriber
|
|
407
|
+
* @param dep - The dependency node
|
|
408
|
+
* @param sub - The subscriber node
|
|
409
|
+
* @param version - The cycle version
|
|
410
|
+
*/
|
|
411
|
+
function link(dep: ReactiveNode, sub: ReactiveNode, version: number): void {
|
|
412
|
+
const prevDep = sub.depsTail
|
|
413
|
+
if (prevDep !== undefined && prevDep.dep === dep) return
|
|
414
|
+
|
|
415
|
+
const nextDep = prevDep !== undefined ? prevDep.nextDep : sub.deps
|
|
416
|
+
if (nextDep !== undefined && nextDep.dep === dep) {
|
|
417
|
+
nextDep.version = version
|
|
418
|
+
sub.depsTail = nextDep
|
|
419
|
+
return
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const prevSub = dep.subsTail
|
|
423
|
+
if (prevSub !== undefined && prevSub.version === version && prevSub.sub === sub) return
|
|
424
|
+
|
|
425
|
+
const newLink = { version, dep, sub, prevDep, nextDep, prevSub, nextSub: undefined }
|
|
426
|
+
sub.depsTail = newLink
|
|
427
|
+
dep.subsTail = newLink
|
|
428
|
+
|
|
429
|
+
if (nextDep !== undefined) nextDep.prevDep = newLink
|
|
430
|
+
if (prevDep !== undefined) prevDep.nextDep = newLink
|
|
431
|
+
else sub.deps = newLink
|
|
432
|
+
if (prevSub !== undefined) prevSub.nextSub = newLink
|
|
433
|
+
else dep.subs = newLink
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Remove a link between a dependency and a subscriber
|
|
437
|
+
* @param lnk - The link to remove
|
|
438
|
+
* @param sub - The subscriber node (defaults to lnk.sub)
|
|
439
|
+
* @returns The next dependency link
|
|
440
|
+
*/
|
|
441
|
+
function unlink(lnk: Link, sub: ReactiveNode = lnk.sub): Link | undefined {
|
|
442
|
+
const dep = lnk.dep
|
|
443
|
+
const prevDep = lnk.prevDep
|
|
444
|
+
const nextDep = lnk.nextDep
|
|
445
|
+
const nextSub = lnk.nextSub
|
|
446
|
+
const prevSub = lnk.prevSub
|
|
447
|
+
|
|
448
|
+
if (nextDep !== undefined) nextDep.prevDep = prevDep
|
|
449
|
+
else sub.depsTail = prevDep
|
|
450
|
+
if (prevDep !== undefined) prevDep.nextDep = nextDep
|
|
451
|
+
else sub.deps = nextDep
|
|
452
|
+
|
|
453
|
+
if (nextSub !== undefined) nextSub.prevSub = prevSub
|
|
454
|
+
else dep.subsTail = prevSub
|
|
455
|
+
if (prevSub !== undefined) prevSub.nextSub = nextSub
|
|
456
|
+
else if ((dep.subs = nextSub) === undefined) unwatched(dep)
|
|
457
|
+
|
|
458
|
+
return nextDep
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Handle when a dependency becomes unwatched
|
|
462
|
+
* @param dep - The dependency node
|
|
463
|
+
*/
|
|
464
|
+
function unwatched(dep: ReactiveNode): void {
|
|
465
|
+
if (!(dep.flags & Mutable)) {
|
|
466
|
+
disposeNode(dep)
|
|
467
|
+
} else if ('getter' in dep && dep.getter !== undefined) {
|
|
468
|
+
dep.depsTail = undefined
|
|
469
|
+
dep.flags = MutableDirty
|
|
470
|
+
purgeDeps(dep)
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Propagate changes through the reactive graph
|
|
475
|
+
* @param firstLink - The first link to propagate from
|
|
476
|
+
*/
|
|
477
|
+
function propagate(firstLink: Link): void {
|
|
478
|
+
let link = firstLink
|
|
479
|
+
let next = link.nextSub
|
|
480
|
+
let stack: StackFrame | undefined
|
|
481
|
+
|
|
482
|
+
top: for (;;) {
|
|
483
|
+
const sub = link.sub
|
|
484
|
+
let flags = sub.flags
|
|
485
|
+
|
|
486
|
+
if (!(flags & 60)) {
|
|
487
|
+
sub.flags = flags | Pending
|
|
488
|
+
} else if (!(flags & 12)) {
|
|
489
|
+
flags = 0
|
|
490
|
+
} else if (!(flags & Running)) {
|
|
491
|
+
sub.flags = (flags & ~Recursed) | Pending
|
|
492
|
+
} else if (!(flags & 48)) {
|
|
493
|
+
let vlink = sub.depsTail
|
|
494
|
+
let valid = false
|
|
495
|
+
while (vlink !== undefined) {
|
|
496
|
+
if (vlink === link) {
|
|
497
|
+
valid = true
|
|
498
|
+
break
|
|
499
|
+
}
|
|
500
|
+
vlink = vlink.prevDep
|
|
501
|
+
}
|
|
502
|
+
if (valid) {
|
|
503
|
+
sub.flags = flags | 40
|
|
504
|
+
flags &= Mutable
|
|
505
|
+
} else {
|
|
506
|
+
flags = 0
|
|
507
|
+
}
|
|
508
|
+
} else {
|
|
509
|
+
flags = 0
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (flags & Watching) notify(sub)
|
|
513
|
+
|
|
514
|
+
if (flags & Mutable) {
|
|
515
|
+
const subSubs = sub.subs
|
|
516
|
+
if (subSubs !== undefined) {
|
|
517
|
+
const nextSub = subSubs.nextSub
|
|
518
|
+
if (nextSub !== undefined) {
|
|
519
|
+
stack = { value: next, prev: stack }
|
|
520
|
+
next = nextSub
|
|
521
|
+
}
|
|
522
|
+
link = subSubs
|
|
523
|
+
continue
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (next !== undefined) {
|
|
528
|
+
link = next
|
|
529
|
+
next = link.nextSub
|
|
530
|
+
continue
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
while (stack !== undefined) {
|
|
534
|
+
link = stack.value!
|
|
535
|
+
stack = stack.prev
|
|
536
|
+
if (link !== undefined) {
|
|
537
|
+
next = link.nextSub
|
|
538
|
+
continue top
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
break
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Check if a node is dirty by traversing its dependencies
|
|
546
|
+
* @param firstLink - The first link to check
|
|
547
|
+
* @param sub - The subscriber node
|
|
548
|
+
* @returns True if the node is dirty
|
|
549
|
+
*/
|
|
550
|
+
function checkDirty(firstLink: Link, sub: ReactiveNode): boolean {
|
|
551
|
+
let link = firstLink
|
|
552
|
+
let stack: StackFrame | undefined
|
|
553
|
+
let checkDepth = 0
|
|
554
|
+
let dirty = false
|
|
555
|
+
|
|
556
|
+
top: for (;;) {
|
|
557
|
+
const dep = link.dep
|
|
558
|
+
const depFlags = dep.flags
|
|
559
|
+
|
|
560
|
+
if (sub.flags & Dirty) {
|
|
561
|
+
dirty = true
|
|
562
|
+
} else if ((depFlags & MutableDirty) === MutableDirty) {
|
|
563
|
+
if (update(dep)) {
|
|
564
|
+
const subs = dep.subs
|
|
565
|
+
if (subs !== undefined && subs.nextSub !== undefined) shallowPropagate(subs)
|
|
566
|
+
dirty = true
|
|
567
|
+
}
|
|
568
|
+
} else if ((depFlags & MutablePending) === MutablePending) {
|
|
569
|
+
if (link.nextSub !== undefined || link.prevSub !== undefined) {
|
|
570
|
+
stack = { value: link, prev: stack }
|
|
571
|
+
}
|
|
572
|
+
link = dep.deps!
|
|
573
|
+
sub = dep
|
|
574
|
+
++checkDepth
|
|
575
|
+
continue
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (!dirty) {
|
|
579
|
+
const nextDep = link.nextDep
|
|
580
|
+
if (nextDep !== undefined) {
|
|
581
|
+
link = nextDep
|
|
582
|
+
continue
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
while (checkDepth-- > 0) {
|
|
587
|
+
const firstSub = sub.subs!
|
|
588
|
+
const hasMultipleSubs = firstSub.nextSub !== undefined
|
|
589
|
+
|
|
590
|
+
if (hasMultipleSubs) {
|
|
591
|
+
link = stack!.value!
|
|
592
|
+
stack = stack!.prev
|
|
593
|
+
} else {
|
|
594
|
+
link = firstSub
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (dirty) {
|
|
598
|
+
if (update(sub)) {
|
|
599
|
+
if (hasMultipleSubs) shallowPropagate(firstSub)
|
|
600
|
+
sub = link.sub
|
|
601
|
+
continue
|
|
602
|
+
}
|
|
603
|
+
dirty = false
|
|
604
|
+
} else {
|
|
605
|
+
sub.flags &= ~Pending
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
sub = link.sub
|
|
609
|
+
const nextDep = link.nextDep
|
|
610
|
+
if (nextDep !== undefined) {
|
|
611
|
+
link = nextDep
|
|
612
|
+
continue top
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
return dirty
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Shallow propagate changes without traversing deeply
|
|
621
|
+
* @param firstLink - The first link to propagate from
|
|
622
|
+
*/
|
|
623
|
+
function shallowPropagate(firstLink: Link): void {
|
|
624
|
+
let link: Link | undefined = firstLink
|
|
625
|
+
do {
|
|
626
|
+
const sub = link.sub
|
|
627
|
+
const flags = sub.flags
|
|
628
|
+
if ((flags & 48) === Pending) {
|
|
629
|
+
sub.flags = flags | Dirty
|
|
630
|
+
if ((flags & 6) === Watching) notify(sub)
|
|
631
|
+
}
|
|
632
|
+
link = link.nextSub
|
|
633
|
+
} while (link !== undefined)
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Update a reactive node (signal or computed)
|
|
637
|
+
* @param node - The node to update
|
|
638
|
+
* @returns True if the value changed
|
|
639
|
+
*/
|
|
640
|
+
function update(node: ReactiveNode): boolean {
|
|
641
|
+
return 'getter' in node && node.getter !== undefined
|
|
642
|
+
? updateComputed(node as ComputedNode)
|
|
643
|
+
: updateSignal(node as SignalNode)
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Notify an effect and add it to the queue
|
|
647
|
+
* @param effect - The effect to notify
|
|
648
|
+
*/
|
|
649
|
+
function notify(effect: ReactiveNode): void {
|
|
650
|
+
effect.flags &= ~Watching
|
|
651
|
+
const effects: EffectNode[] = []
|
|
652
|
+
|
|
653
|
+
for (;;) {
|
|
654
|
+
effects.push(effect as EffectNode)
|
|
655
|
+
const nextLink = effect.subs
|
|
656
|
+
if (nextLink === undefined) break
|
|
657
|
+
effect = nextLink.sub
|
|
658
|
+
if (effect === undefined || !(effect.flags & Watching)) break
|
|
659
|
+
effect.flags &= ~Watching
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Route effects to appropriate queue based on transition context
|
|
663
|
+
const targetQueue = isInTransition ? lowPriorityQueue : highPriorityQueue
|
|
664
|
+
for (let i = effects.length - 1; i >= 0; i--) {
|
|
665
|
+
targetQueue.push(effects[i]!)
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Purge all dependencies from a subscriber
|
|
670
|
+
* @param sub - The subscriber node
|
|
671
|
+
*/
|
|
672
|
+
function purgeDeps(sub: ReactiveNode): void {
|
|
673
|
+
const depsTail = sub.depsTail
|
|
674
|
+
let dep = depsTail !== undefined ? depsTail.nextDep : sub.deps
|
|
675
|
+
while (dep !== undefined) dep = unlink(dep, sub)
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Dispose a reactive node
|
|
679
|
+
* @param node - The node to dispose
|
|
680
|
+
*/
|
|
681
|
+
function disposeNode(node: ReactiveNode): void {
|
|
682
|
+
node.depsTail = undefined
|
|
683
|
+
node.flags = 0
|
|
684
|
+
purgeDeps(node)
|
|
685
|
+
const sub = node.subs
|
|
686
|
+
if (sub !== undefined) unlink(sub, node)
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Update a signal node
|
|
690
|
+
* @param s - The signal node
|
|
691
|
+
* @returns True if the value changed
|
|
692
|
+
*/
|
|
693
|
+
function updateSignal(s: SignalNode): boolean {
|
|
694
|
+
s.flags = Mutable
|
|
695
|
+
const current = s.currentValue
|
|
696
|
+
const pending = s.pendingValue
|
|
697
|
+
if (current !== pending) {
|
|
698
|
+
s.currentValue = pending
|
|
699
|
+
return true
|
|
700
|
+
}
|
|
701
|
+
return false
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Update a computed node
|
|
705
|
+
* @param c - The computed node
|
|
706
|
+
* @returns True if the value changed
|
|
707
|
+
*/
|
|
708
|
+
function updateComputed<T>(c: ComputedNode<T>): boolean {
|
|
709
|
+
++cycle
|
|
710
|
+
const oldValue = c.value
|
|
711
|
+
c.depsTail = undefined
|
|
712
|
+
c.flags = MutableRunning
|
|
713
|
+
const prevSub = activeSub
|
|
714
|
+
activeSub = c
|
|
715
|
+
|
|
716
|
+
try {
|
|
717
|
+
const newValue = c.getter(oldValue)
|
|
718
|
+
activeSub = prevSub
|
|
719
|
+
c.flags &= ~Running
|
|
720
|
+
purgeDeps(c)
|
|
721
|
+
if (oldValue !== newValue) {
|
|
722
|
+
c.value = newValue
|
|
723
|
+
return true
|
|
724
|
+
}
|
|
725
|
+
return false
|
|
726
|
+
} catch (e) {
|
|
727
|
+
activeSub = prevSub
|
|
728
|
+
c.flags &= ~Running
|
|
729
|
+
throw e
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Run an effect
|
|
734
|
+
* @param e - The effect node
|
|
735
|
+
*/
|
|
736
|
+
function runEffect(e: EffectNode): void {
|
|
737
|
+
const flags = e.flags
|
|
738
|
+
if (flags & Dirty || (flags & Pending && e.deps && checkDirty(e.deps, e))) {
|
|
739
|
+
++cycle
|
|
740
|
+
effectRunDevtools(e)
|
|
741
|
+
e.depsTail = undefined
|
|
742
|
+
e.flags = WatchingRunning
|
|
743
|
+
const prevSub = activeSub
|
|
744
|
+
activeSub = e
|
|
745
|
+
try {
|
|
746
|
+
e.fn()
|
|
747
|
+
activeSub = prevSub
|
|
748
|
+
e.flags = Watching
|
|
749
|
+
purgeDeps(e)
|
|
750
|
+
} catch (err) {
|
|
751
|
+
activeSub = prevSub
|
|
752
|
+
e.flags = Watching
|
|
753
|
+
throw err
|
|
754
|
+
}
|
|
755
|
+
} else {
|
|
756
|
+
e.flags = Watching
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Schedule a flush in a microtask to coalesce synchronous writes
|
|
761
|
+
*/
|
|
762
|
+
export function scheduleFlush(): void {
|
|
763
|
+
const hasWork = highPriorityQueue.length > 0 || lowPriorityQueue.length > 0
|
|
764
|
+
if (flushScheduled || !hasWork) return
|
|
765
|
+
if (batchDepth > 0) return
|
|
766
|
+
flushScheduled = true
|
|
767
|
+
enqueueMicrotask(() => {
|
|
768
|
+
flush()
|
|
769
|
+
})
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Flush all queued effects with priority-based scheduling
|
|
773
|
+
* High priority effects execute first; low priority can be interrupted
|
|
774
|
+
*/
|
|
775
|
+
function flush(): void {
|
|
776
|
+
beginFlushGuard()
|
|
777
|
+
if (batchDepth > 0) {
|
|
778
|
+
// If batching is active, defer until the batch completes
|
|
779
|
+
scheduleFlush()
|
|
780
|
+
endFlushGuard()
|
|
781
|
+
return
|
|
782
|
+
}
|
|
783
|
+
const hasWork = highPriorityQueue.length > 0 || lowPriorityQueue.length > 0
|
|
784
|
+
if (!hasWork) {
|
|
785
|
+
flushScheduled = false
|
|
786
|
+
endFlushGuard()
|
|
787
|
+
return
|
|
788
|
+
}
|
|
789
|
+
flushScheduled = false
|
|
790
|
+
|
|
791
|
+
// 1. Process all high-priority effects first
|
|
792
|
+
let highIndex = 0
|
|
793
|
+
while (highIndex < highPriorityQueue.length) {
|
|
794
|
+
const e = highPriorityQueue[highIndex]!
|
|
795
|
+
if (!beforeEffectRunGuard()) {
|
|
796
|
+
if (highIndex > 0) {
|
|
797
|
+
highPriorityQueue.copyWithin(0, highIndex)
|
|
798
|
+
highPriorityQueue.length -= highIndex
|
|
799
|
+
}
|
|
800
|
+
endFlushGuard()
|
|
801
|
+
return
|
|
802
|
+
}
|
|
803
|
+
highIndex++
|
|
804
|
+
runEffect(e)
|
|
805
|
+
}
|
|
806
|
+
highPriorityQueue.length = 0
|
|
807
|
+
|
|
808
|
+
// 2. Process low-priority effects, interruptible by high priority
|
|
809
|
+
let lowIndex = 0
|
|
810
|
+
while (lowIndex < lowPriorityQueue.length) {
|
|
811
|
+
// Check if high priority work arrived during low priority execution
|
|
812
|
+
if (highPriorityQueue.length > 0) {
|
|
813
|
+
if (lowIndex > 0) {
|
|
814
|
+
lowPriorityQueue.copyWithin(0, lowIndex)
|
|
815
|
+
lowPriorityQueue.length -= lowIndex
|
|
816
|
+
}
|
|
817
|
+
scheduleFlush()
|
|
818
|
+
endFlushGuard()
|
|
819
|
+
return
|
|
820
|
+
}
|
|
821
|
+
const e = lowPriorityQueue[lowIndex]!
|
|
822
|
+
if (!beforeEffectRunGuard()) {
|
|
823
|
+
if (lowIndex > 0) {
|
|
824
|
+
lowPriorityQueue.copyWithin(0, lowIndex)
|
|
825
|
+
lowPriorityQueue.length -= lowIndex
|
|
826
|
+
}
|
|
827
|
+
endFlushGuard()
|
|
828
|
+
return
|
|
829
|
+
}
|
|
830
|
+
lowIndex++
|
|
831
|
+
runEffect(e)
|
|
832
|
+
}
|
|
833
|
+
lowPriorityQueue.length = 0
|
|
834
|
+
|
|
835
|
+
endFlushGuard()
|
|
836
|
+
}
|
|
837
|
+
// ============================================================================
|
|
838
|
+
// Signal - Inline optimized version
|
|
839
|
+
// ============================================================================
|
|
840
|
+
/**
|
|
841
|
+
* Create a reactive signal
|
|
842
|
+
* @param initialValue - The initial value
|
|
843
|
+
* @returns A signal accessor function
|
|
844
|
+
*/
|
|
845
|
+
export function signal<T>(initialValue: T): SignalAccessor<T> {
|
|
846
|
+
const s = {
|
|
847
|
+
currentValue: initialValue,
|
|
848
|
+
pendingValue: initialValue,
|
|
849
|
+
subs: undefined,
|
|
850
|
+
subsTail: undefined,
|
|
851
|
+
flags: Mutable,
|
|
852
|
+
__id: undefined as number | undefined,
|
|
853
|
+
}
|
|
854
|
+
registerSignalDevtools(initialValue, s)
|
|
855
|
+
return signalOper.bind(s) as SignalAccessor<T>
|
|
856
|
+
}
|
|
857
|
+
function signalOper<T>(this: SignalNode<T>, value?: T): T | void {
|
|
858
|
+
if (arguments.length > 0) {
|
|
859
|
+
if (this.pendingValue !== value) {
|
|
860
|
+
this.pendingValue = value as T
|
|
861
|
+
this.flags = MutableDirty
|
|
862
|
+
updateSignalDevtools(this, value)
|
|
863
|
+
const subs = this.subs
|
|
864
|
+
if (subs !== undefined) {
|
|
865
|
+
propagate(subs)
|
|
866
|
+
if (!batchDepth) scheduleFlush()
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
return
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
const flags = this.flags
|
|
873
|
+
if (flags & Dirty) {
|
|
874
|
+
if (updateSignal(this)) {
|
|
875
|
+
const subs = this.subs
|
|
876
|
+
if (subs !== undefined) shallowPropagate(subs)
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
let sub = activeSub
|
|
881
|
+
while (sub !== undefined) {
|
|
882
|
+
if (sub.flags & 3) {
|
|
883
|
+
link(this, sub, cycle)
|
|
884
|
+
break
|
|
885
|
+
}
|
|
886
|
+
const subSubs = sub.subs
|
|
887
|
+
sub = subSubs !== undefined ? subSubs.sub : undefined
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
return this.currentValue
|
|
891
|
+
}
|
|
892
|
+
// ============================================================================
|
|
893
|
+
// Computed
|
|
894
|
+
// ============================================================================
|
|
895
|
+
/**
|
|
896
|
+
* Create a computed reactive value
|
|
897
|
+
* @param getter - The getter function
|
|
898
|
+
* @returns A computed accessor function
|
|
899
|
+
*/
|
|
900
|
+
export function computed<T>(getter: (oldValue?: T) => T): ComputedAccessor<T> {
|
|
901
|
+
const c: ComputedNode<T> = {
|
|
902
|
+
value: undefined as unknown as T,
|
|
903
|
+
subs: undefined,
|
|
904
|
+
subsTail: undefined,
|
|
905
|
+
deps: undefined,
|
|
906
|
+
depsTail: undefined,
|
|
907
|
+
flags: 0,
|
|
908
|
+
getter,
|
|
909
|
+
}
|
|
910
|
+
const bound = (computedOper as (this: ComputedNode<T>) => T).bind(c)
|
|
911
|
+
return bound as ComputedAccessor<T>
|
|
912
|
+
}
|
|
913
|
+
function computedOper<T>(this: ComputedNode<T>): T {
|
|
914
|
+
const flags = this.flags
|
|
915
|
+
|
|
916
|
+
if (flags & Dirty) {
|
|
917
|
+
if (updateComputed(this)) {
|
|
918
|
+
const subs = this.subs
|
|
919
|
+
if (subs !== undefined) shallowPropagate(subs)
|
|
920
|
+
}
|
|
921
|
+
} else if (flags & Pending) {
|
|
922
|
+
if (this.deps && checkDirty(this.deps, this)) {
|
|
923
|
+
if (updateComputed(this)) {
|
|
924
|
+
const subs = this.subs
|
|
925
|
+
if (subs !== undefined) shallowPropagate(subs)
|
|
926
|
+
}
|
|
927
|
+
} else {
|
|
928
|
+
this.flags = flags & ~Pending
|
|
929
|
+
}
|
|
930
|
+
} else if (!flags) {
|
|
931
|
+
this.flags = MutableRunning
|
|
932
|
+
const prevSub = setActiveSub(this)
|
|
933
|
+
try {
|
|
934
|
+
this.value = this.getter(undefined)
|
|
935
|
+
} finally {
|
|
936
|
+
setActiveSub(prevSub)
|
|
937
|
+
this.flags &= ~Running
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
if (activeSub !== undefined) link(this, activeSub, cycle)
|
|
942
|
+
return this.value
|
|
943
|
+
}
|
|
944
|
+
// ============================================================================
|
|
945
|
+
// Effect
|
|
946
|
+
// ============================================================================
|
|
947
|
+
/**
|
|
948
|
+
* Create a reactive effect
|
|
949
|
+
* @param fn - The effect function
|
|
950
|
+
* @returns An effect disposer function
|
|
951
|
+
*/
|
|
952
|
+
export function effect(fn: () => void): EffectDisposer {
|
|
953
|
+
const e = {
|
|
954
|
+
fn,
|
|
955
|
+
subs: undefined,
|
|
956
|
+
subsTail: undefined,
|
|
957
|
+
deps: undefined,
|
|
958
|
+
depsTail: undefined,
|
|
959
|
+
flags: WatchingRunning,
|
|
960
|
+
__id: undefined as number | undefined,
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
registerEffectDevtools(e)
|
|
964
|
+
|
|
965
|
+
const prevSub = activeSub
|
|
966
|
+
if (prevSub !== undefined) link(e, prevSub, 0)
|
|
967
|
+
activeSub = e
|
|
968
|
+
|
|
969
|
+
try {
|
|
970
|
+
effectRunDevtools(e)
|
|
971
|
+
fn()
|
|
972
|
+
} finally {
|
|
973
|
+
activeSub = prevSub
|
|
974
|
+
e.flags &= ~Running
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
return effectOper.bind(e) as EffectDisposer
|
|
978
|
+
}
|
|
979
|
+
function effectOper(this: EffectNode): void {
|
|
980
|
+
disposeNode(this)
|
|
981
|
+
}
|
|
982
|
+
// ============================================================================
|
|
983
|
+
// Effect Scope
|
|
984
|
+
// ============================================================================
|
|
985
|
+
/**
|
|
986
|
+
* Create a reactive effect scope
|
|
987
|
+
* @param fn - The scope function
|
|
988
|
+
* @returns An effect scope disposer function
|
|
989
|
+
*/
|
|
990
|
+
export function effectScope(fn: () => void): EffectScopeDisposer {
|
|
991
|
+
const e = { deps: undefined, depsTail: undefined, subs: undefined, subsTail: undefined, flags: 0 }
|
|
992
|
+
|
|
993
|
+
const prevSub = activeSub
|
|
994
|
+
if (prevSub !== undefined) link(e, prevSub, 0)
|
|
995
|
+
activeSub = e
|
|
996
|
+
|
|
997
|
+
try {
|
|
998
|
+
fn()
|
|
999
|
+
} finally {
|
|
1000
|
+
activeSub = prevSub
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
return effectScopeOper.bind(e) as EffectScopeDisposer
|
|
1004
|
+
}
|
|
1005
|
+
function effectScopeOper(this: EffectScopeNode): void {
|
|
1006
|
+
disposeNode(this)
|
|
1007
|
+
}
|
|
1008
|
+
// ============================================================================
|
|
1009
|
+
// Trigger
|
|
1010
|
+
// ============================================================================
|
|
1011
|
+
/**
|
|
1012
|
+
* Trigger a reactive computation without creating a persistent subscription
|
|
1013
|
+
* @param fn - The function to run
|
|
1014
|
+
*/
|
|
1015
|
+
export function trigger(fn: () => void): void {
|
|
1016
|
+
const sub: SubscriberNode = { deps: undefined, depsTail: undefined, flags: Watching }
|
|
1017
|
+
const prevSub = activeSub
|
|
1018
|
+
activeSub = sub as ReactiveNode
|
|
1019
|
+
|
|
1020
|
+
try {
|
|
1021
|
+
fn()
|
|
1022
|
+
} finally {
|
|
1023
|
+
activeSub = prevSub
|
|
1024
|
+
let lnk = sub.deps
|
|
1025
|
+
while (lnk !== undefined) {
|
|
1026
|
+
const dep = lnk.dep
|
|
1027
|
+
lnk = unlink(lnk, sub)
|
|
1028
|
+
const subs = dep.subs
|
|
1029
|
+
if (subs !== undefined) {
|
|
1030
|
+
sub.flags = 0
|
|
1031
|
+
propagate(subs)
|
|
1032
|
+
shallowPropagate(subs)
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
if (!batchDepth) scheduleFlush()
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
// ============================================================================
|
|
1039
|
+
// Batch processing & Utility API
|
|
1040
|
+
// ============================================================================
|
|
1041
|
+
/**
|
|
1042
|
+
* Start a batch of updates
|
|
1043
|
+
*/
|
|
1044
|
+
export function startBatch(): void {
|
|
1045
|
+
++batchDepth
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
1048
|
+
* End a batch of updates and flush effects
|
|
1049
|
+
*/
|
|
1050
|
+
export function endBatch(): void {
|
|
1051
|
+
if (--batchDepth === 0) flush()
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
1054
|
+
* Execute a function in a batch
|
|
1055
|
+
* @param fn - The function to execute
|
|
1056
|
+
* @returns The return value of the function
|
|
1057
|
+
*/
|
|
1058
|
+
export function batch<T>(fn: () => T): T {
|
|
1059
|
+
++batchDepth
|
|
1060
|
+
try {
|
|
1061
|
+
return fn()
|
|
1062
|
+
} finally {
|
|
1063
|
+
if (--batchDepth === 0) flush()
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
/**
|
|
1067
|
+
* Get the current active subscriber
|
|
1068
|
+
* @returns The active subscriber or undefined
|
|
1069
|
+
*/
|
|
1070
|
+
export function getActiveSub(): ReactiveNode | undefined {
|
|
1071
|
+
return activeSub
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Set the active subscriber
|
|
1075
|
+
* @param sub - The new active subscriber
|
|
1076
|
+
* @returns The previous active subscriber
|
|
1077
|
+
*/
|
|
1078
|
+
export function setActiveSub(sub: ReactiveNode | undefined): ReactiveNode | undefined {
|
|
1079
|
+
const prev = activeSub
|
|
1080
|
+
activeSub = sub
|
|
1081
|
+
return prev
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* Get the current batch depth
|
|
1085
|
+
* @returns The current batch depth
|
|
1086
|
+
*/
|
|
1087
|
+
export function getBatchDepth(): number {
|
|
1088
|
+
return batchDepth
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Execute a function without tracking dependencies
|
|
1092
|
+
* @param fn - The function to execute
|
|
1093
|
+
* @returns The return value of the function
|
|
1094
|
+
*/
|
|
1095
|
+
export function untrack<T>(fn: () => T): T {
|
|
1096
|
+
const prev = activeSub
|
|
1097
|
+
activeSub = undefined
|
|
1098
|
+
try {
|
|
1099
|
+
return fn()
|
|
1100
|
+
} finally {
|
|
1101
|
+
activeSub = prev
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
/**
|
|
1105
|
+
* Peek at a reactive value without tracking it as a dependency
|
|
1106
|
+
* @param accessor - The accessor function
|
|
1107
|
+
* @returns The value
|
|
1108
|
+
*/
|
|
1109
|
+
export function peek<T>(accessor: () => T): T {
|
|
1110
|
+
return untrack(accessor)
|
|
1111
|
+
}
|
|
1112
|
+
// Type detection - Fixed: using Function.name
|
|
1113
|
+
/**
|
|
1114
|
+
* Check if a function is a signal accessor
|
|
1115
|
+
* @param fn - The function to check
|
|
1116
|
+
* @returns True if the function is a signal accessor
|
|
1117
|
+
*/
|
|
1118
|
+
export function isSignal(fn: unknown): fn is SignalAccessor<unknown> {
|
|
1119
|
+
return typeof fn === 'function' && fn.name === 'bound signalOper'
|
|
1120
|
+
}
|
|
1121
|
+
/**
|
|
1122
|
+
* Check if a function is a computed accessor
|
|
1123
|
+
* @param fn - The function to check
|
|
1124
|
+
* @returns True if the function is a computed accessor
|
|
1125
|
+
*/
|
|
1126
|
+
export function isComputed(fn: unknown): fn is ComputedAccessor<unknown> {
|
|
1127
|
+
return typeof fn === 'function' && fn.name === 'bound computedOper'
|
|
1128
|
+
}
|
|
1129
|
+
/**
|
|
1130
|
+
* Check if a function is an effect disposer
|
|
1131
|
+
* @param fn - The function to check
|
|
1132
|
+
* @returns True if the function is an effect disposer
|
|
1133
|
+
*/
|
|
1134
|
+
export function isEffect(fn: unknown): fn is EffectDisposer {
|
|
1135
|
+
return typeof fn === 'function' && fn.name === 'bound effectOper'
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Check if a function is an effect scope disposer
|
|
1139
|
+
* @param fn - The function to check
|
|
1140
|
+
* @returns True if the function is an effect scope disposer
|
|
1141
|
+
*/
|
|
1142
|
+
export function isEffectScope(fn: unknown): fn is EffectScopeDisposer {
|
|
1143
|
+
return typeof fn === 'function' && fn.name === 'bound effectScopeOper'
|
|
1144
|
+
}
|
|
1145
|
+
// ============================================================================
|
|
1146
|
+
// Transition Context (for priority scheduling)
|
|
1147
|
+
// ============================================================================
|
|
1148
|
+
/**
|
|
1149
|
+
* Set the transition context
|
|
1150
|
+
* @param value - Whether we're inside a transition
|
|
1151
|
+
* @returns The previous transition context value
|
|
1152
|
+
*/
|
|
1153
|
+
export function setTransitionContext(value: boolean): boolean {
|
|
1154
|
+
const prev = isInTransition
|
|
1155
|
+
isInTransition = value
|
|
1156
|
+
return prev
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* Get the current transition context
|
|
1160
|
+
* @returns True if currently inside a transition
|
|
1161
|
+
*/
|
|
1162
|
+
export function getTransitionContext(): boolean {
|
|
1163
|
+
return isInTransition
|
|
1164
|
+
}
|
|
1165
|
+
// Export aliases for API compatibility
|
|
1166
|
+
export { signal as createSignal }
|
|
1167
|
+
export type { SignalAccessor as Signal }
|
|
1168
|
+
|
|
1169
|
+
export { flush, link, unlink, propagate, checkDirty, shallowPropagate }
|
|
1170
|
+
export default {
|
|
1171
|
+
signal,
|
|
1172
|
+
computed,
|
|
1173
|
+
effect,
|
|
1174
|
+
effectScope,
|
|
1175
|
+
trigger,
|
|
1176
|
+
batch,
|
|
1177
|
+
startBatch,
|
|
1178
|
+
endBatch,
|
|
1179
|
+
flush,
|
|
1180
|
+
untrack,
|
|
1181
|
+
peek,
|
|
1182
|
+
isSignal,
|
|
1183
|
+
isComputed,
|
|
1184
|
+
isEffect,
|
|
1185
|
+
isEffectScope,
|
|
1186
|
+
getActiveSub,
|
|
1187
|
+
setActiveSub,
|
|
1188
|
+
getBatchDepth,
|
|
1189
|
+
link,
|
|
1190
|
+
unlink,
|
|
1191
|
+
propagate,
|
|
1192
|
+
checkDirty,
|
|
1193
|
+
shallowPropagate,
|
|
1194
|
+
createReactiveSystem,
|
|
1195
|
+
ReactiveFlags,
|
|
1196
|
+
}
|
|
1197
|
+
export const $state = signal as <T>(value: T) => T
|
|
1198
|
+
|
|
1199
|
+
let devtoolsSignalId = 0
|
|
1200
|
+
let devtoolsEffectId = 0
|
|
1201
|
+
|
|
1202
|
+
interface DevtoolsIdentifiable {
|
|
1203
|
+
__id?: number
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
function registerSignalDevtools(value: unknown, node: SignalNode): number | undefined {
|
|
1207
|
+
const hook = getDevtoolsHook()
|
|
1208
|
+
if (!hook) return undefined
|
|
1209
|
+
const id = ++devtoolsSignalId
|
|
1210
|
+
hook.registerSignal(id, value)
|
|
1211
|
+
;(node as SignalNode & DevtoolsIdentifiable).__id = id
|
|
1212
|
+
return id
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
function updateSignalDevtools(node: SignalNode, value: unknown): void {
|
|
1216
|
+
const hook = getDevtoolsHook()
|
|
1217
|
+
if (!hook) return
|
|
1218
|
+
const id = (node as SignalNode & DevtoolsIdentifiable).__id
|
|
1219
|
+
if (id) hook.updateSignal(id, value)
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
function registerEffectDevtools(node: EffectNode): number | undefined {
|
|
1223
|
+
const hook = getDevtoolsHook()
|
|
1224
|
+
if (!hook) return undefined
|
|
1225
|
+
const id = ++devtoolsEffectId
|
|
1226
|
+
hook.registerEffect(id)
|
|
1227
|
+
;(node as EffectNode & DevtoolsIdentifiable).__id = id
|
|
1228
|
+
return id
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
function effectRunDevtools(node: EffectNode): void {
|
|
1232
|
+
const hook = getDevtoolsHook()
|
|
1233
|
+
if (!hook) return
|
|
1234
|
+
const id = (node as EffectNode & DevtoolsIdentifiable).__id
|
|
1235
|
+
if (id) hook.effectRun(id)
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// ============================================================================
|
|
1239
|
+
// Selector
|
|
1240
|
+
// ============================================================================
|
|
1241
|
+
/**
|
|
1242
|
+
* Create a selector signal that efficiently updates only when the selected key matches.
|
|
1243
|
+
* Useful for large lists where only one item is selected.
|
|
1244
|
+
*
|
|
1245
|
+
* @param source - The source signal returning the current key
|
|
1246
|
+
* @param equalityFn - Optional equality function
|
|
1247
|
+
* @returns A selector function that takes a key and returns a boolean signal accessor
|
|
1248
|
+
*/
|
|
1249
|
+
export function createSelector<T>(
|
|
1250
|
+
source: () => T,
|
|
1251
|
+
equalityFn: (a: T, b: T) => boolean = (a, b) => a === b,
|
|
1252
|
+
): (key: T) => boolean {
|
|
1253
|
+
let current = source()
|
|
1254
|
+
const observers = new Map<T, SignalAccessor<boolean>>()
|
|
1255
|
+
|
|
1256
|
+
effect(() => {
|
|
1257
|
+
const next = source()
|
|
1258
|
+
if (equalityFn(current, next)) return
|
|
1259
|
+
|
|
1260
|
+
const prevSig = observers.get(current)
|
|
1261
|
+
if (prevSig) prevSig(false)
|
|
1262
|
+
|
|
1263
|
+
const nextSig = observers.get(next)
|
|
1264
|
+
if (nextSig) nextSig(true)
|
|
1265
|
+
|
|
1266
|
+
current = next
|
|
1267
|
+
})
|
|
1268
|
+
|
|
1269
|
+
return (key: T) => {
|
|
1270
|
+
let sig = observers.get(key)
|
|
1271
|
+
if (!sig) {
|
|
1272
|
+
sig = signal(equalityFn(key, current))
|
|
1273
|
+
observers.set(key, sig)
|
|
1274
|
+
registerRootCleanup(() => observers.delete(key))
|
|
1275
|
+
}
|
|
1276
|
+
return sig()
|
|
1277
|
+
}
|
|
1278
|
+
}
|