@fictjs/runtime 0.0.9 → 0.0.10
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/dist/index.cjs +325 -198
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.dev.js +327 -198
- package/dist/index.dev.js.map +1 -1
- package/dist/index.js +325 -198
- package/dist/index.js.map +1 -1
- package/dist/slim.cjs +325 -198
- package/dist/slim.cjs.map +1 -1
- package/dist/slim.d.cts +1 -0
- package/dist/slim.d.ts +1 -0
- package/dist/slim.js +325 -198
- package/dist/slim.js.map +1 -1
- package/package.json +1 -1
- package/src/binding.ts +41 -13
- package/src/dom.ts +16 -6
- package/src/effect.ts +2 -1
- package/src/hooks.ts +14 -1
- package/src/list-helpers.ts +125 -47
- package/src/signal.ts +16 -2
package/package.json
CHANGED
package/src/binding.ts
CHANGED
|
@@ -36,9 +36,10 @@ import {
|
|
|
36
36
|
registerRootCleanup,
|
|
37
37
|
type RootContext,
|
|
38
38
|
} from './lifecycle'
|
|
39
|
+
import { createVersionedSignalAccessor } from './list-helpers'
|
|
39
40
|
import { toNodeArray, removeNodes, insertNodesBefore } from './node-ops'
|
|
41
|
+
import { batch } from './scheduler'
|
|
40
42
|
import { computed, createSignal, untrack, type Signal } from './signal'
|
|
41
|
-
import { createVersionedSignalAccessor } from './list-helpers'
|
|
42
43
|
import type { Cleanup, FictNode } from './types'
|
|
43
44
|
|
|
44
45
|
// ============================================================================
|
|
@@ -552,8 +553,18 @@ export function bindClass(
|
|
|
552
553
|
getValue: () => string | Record<string, boolean> | null | undefined,
|
|
553
554
|
): Cleanup {
|
|
554
555
|
let prev: Record<string, boolean> = {}
|
|
556
|
+
let prevString: string | undefined
|
|
555
557
|
return createRenderEffect(() => {
|
|
556
558
|
const next = getValue()
|
|
559
|
+
// P2-1: Short-circuit for string values to avoid DOM writes when unchanged
|
|
560
|
+
if (typeof next === 'string') {
|
|
561
|
+
if (next === prevString) return
|
|
562
|
+
prevString = next
|
|
563
|
+
el.className = next
|
|
564
|
+
prev = {}
|
|
565
|
+
return
|
|
566
|
+
}
|
|
567
|
+
prevString = undefined
|
|
557
568
|
prev = applyClass(el, next, prev)
|
|
558
569
|
})
|
|
559
570
|
}
|
|
@@ -914,11 +925,21 @@ export function clearDelegatedEvents(doc: Document = window.document): void {
|
|
|
914
925
|
* Walks up the DOM tree to find and call handlers stored as $$eventName properties.
|
|
915
926
|
*/
|
|
916
927
|
function globalEventHandler(e: Event): void {
|
|
917
|
-
|
|
928
|
+
const asNode = (value: unknown): Node | null =>
|
|
929
|
+
value && typeof (value as Node).nodeType === 'number' ? (value as Node) : null
|
|
930
|
+
const asElement = (value: unknown): Element | null => {
|
|
931
|
+
const n = asNode(value)
|
|
932
|
+
if (!n) return null
|
|
933
|
+
if (n.nodeType === 1) return n as Element
|
|
934
|
+
return (n as ChildNode).parentElement
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
let node = asElement(e.target)
|
|
918
938
|
const key = `$$${e.type}` as const
|
|
919
939
|
const dataKey = `${key}Data` as `$$${string}Data`
|
|
920
940
|
const oriTarget = e.target
|
|
921
941
|
const oriCurrentTarget = e.currentTarget
|
|
942
|
+
let lastHandled: Element | null = null
|
|
922
943
|
|
|
923
944
|
// Retarget helper for shadow DOM and portals
|
|
924
945
|
const retarget = (value: EventTarget) =>
|
|
@@ -947,12 +968,15 @@ function globalEventHandler(e: Event): void {
|
|
|
947
968
|
const rawData = (node as any)[dataKey] as unknown
|
|
948
969
|
const hasData = rawData !== undefined
|
|
949
970
|
const resolvedNodeData = hasData ? resolveData(rawData) : undefined
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
971
|
+
// P2-3: Wrap event handler calls in batch for synchronous flush & reduced microtasks
|
|
972
|
+
batch(() => {
|
|
973
|
+
if (typeof handler === 'function') {
|
|
974
|
+
callEventHandler(handler, e, node, hasData ? resolvedNodeData : undefined)
|
|
975
|
+
} else if (Array.isArray(handler)) {
|
|
976
|
+
const tupleData = resolveData(handler[1])
|
|
977
|
+
callEventHandler(handler[0], e, node, tupleData)
|
|
978
|
+
}
|
|
979
|
+
})
|
|
956
980
|
if (e.cancelBubble) return false
|
|
957
981
|
}
|
|
958
982
|
// Handle shadow DOM host retargeting
|
|
@@ -961,7 +985,10 @@ function globalEventHandler(e: Event): void {
|
|
|
961
985
|
shadowHost &&
|
|
962
986
|
typeof shadowHost !== 'string' &&
|
|
963
987
|
!(shadowHost as Element)._$host &&
|
|
964
|
-
|
|
988
|
+
(() => {
|
|
989
|
+
const targetNode = asNode(e.target)
|
|
990
|
+
return targetNode ? node.contains(targetNode) : false
|
|
991
|
+
})()
|
|
965
992
|
) {
|
|
966
993
|
retarget(shadowHost as EventTarget)
|
|
967
994
|
}
|
|
@@ -971,9 +998,7 @@ function globalEventHandler(e: Event): void {
|
|
|
971
998
|
// Walk up tree helper
|
|
972
999
|
const walkUpTree = (): void => {
|
|
973
1000
|
while (handleNode() && node) {
|
|
974
|
-
node = (node._$host ||
|
|
975
|
-
node.parentNode ||
|
|
976
|
-
(node as unknown as ShadowRoot).host) as Element | null
|
|
1001
|
+
node = asElement(node._$host || node.parentNode || (node as unknown as ShadowRoot).host)
|
|
977
1002
|
}
|
|
978
1003
|
}
|
|
979
1004
|
|
|
@@ -990,8 +1015,11 @@ function globalEventHandler(e: Event): void {
|
|
|
990
1015
|
const path = e.composedPath()
|
|
991
1016
|
retarget(path[0] as EventTarget)
|
|
992
1017
|
for (let i = 0; i < path.length - 2; i++) {
|
|
993
|
-
|
|
1018
|
+
const nextNode = asElement(path[i] as EventTarget)
|
|
1019
|
+
if (!nextNode || nextNode === lastHandled) continue
|
|
1020
|
+
node = nextNode
|
|
994
1021
|
if (!handleNode()) break
|
|
1022
|
+
lastHandled = node
|
|
995
1023
|
// Handle portal event bubbling
|
|
996
1024
|
if (node._$host) {
|
|
997
1025
|
node = node._$host
|
package/src/dom.ts
CHANGED
|
@@ -143,11 +143,21 @@ function createElementWithContext(node: FictNode, namespace: NamespaceContext):
|
|
|
143
143
|
if (typeof node === 'object' && node !== null && !(node instanceof Node)) {
|
|
144
144
|
// Handle BindingHandle (createList, createConditional, etc)
|
|
145
145
|
if ('marker' in node) {
|
|
146
|
-
const handle = node as { marker: unknown; dispose?: () => void }
|
|
146
|
+
const handle = node as { marker: unknown; dispose?: () => void; flush?: () => void }
|
|
147
147
|
// Register dispose cleanup if available
|
|
148
148
|
if (typeof handle.dispose === 'function') {
|
|
149
149
|
registerRootCleanup(handle.dispose)
|
|
150
150
|
}
|
|
151
|
+
if (typeof handle.flush === 'function') {
|
|
152
|
+
const runFlush = () => handle.flush && handle.flush()
|
|
153
|
+
if (typeof queueMicrotask === 'function') {
|
|
154
|
+
queueMicrotask(runFlush)
|
|
155
|
+
} else {
|
|
156
|
+
Promise.resolve()
|
|
157
|
+
.then(runFlush)
|
|
158
|
+
.catch(() => undefined)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
151
161
|
return createElement(handle.marker as FictNode)
|
|
152
162
|
}
|
|
153
163
|
|
|
@@ -210,20 +220,20 @@ function createElementWithContext(node: FictNode, namespace: NamespaceContext):
|
|
|
210
220
|
})
|
|
211
221
|
|
|
212
222
|
const props = createPropsProxy(baseProps)
|
|
223
|
+
// Create a fresh hook context for this component instance.
|
|
224
|
+
// This preserves slot state across re-renders driven by __fictRender.
|
|
225
|
+
__fictPushContext()
|
|
213
226
|
try {
|
|
214
|
-
// Create a fresh hook context for this component instance.
|
|
215
|
-
// This preserves slot state across re-renders driven by __fictRender.
|
|
216
|
-
__fictPushContext()
|
|
217
227
|
const rendered = vnode.type(props)
|
|
218
|
-
__fictPopContext()
|
|
219
228
|
return createElementWithContext(rendered as FictNode, namespace)
|
|
220
229
|
} catch (err) {
|
|
221
|
-
__fictPopContext()
|
|
222
230
|
if (handleSuspend(err as any)) {
|
|
223
231
|
return document.createComment('fict:suspend')
|
|
224
232
|
}
|
|
225
233
|
handleError(err, { source: 'render', componentName: vnode.type.name })
|
|
226
234
|
throw err
|
|
235
|
+
} finally {
|
|
236
|
+
__fictPopContext()
|
|
227
237
|
}
|
|
228
238
|
}
|
|
229
239
|
|
package/src/effect.ts
CHANGED
|
@@ -61,7 +61,8 @@ export function createRenderEffect(fn: Effect): () => void {
|
|
|
61
61
|
cleanup = maybeCleanup
|
|
62
62
|
}
|
|
63
63
|
} catch (err) {
|
|
64
|
-
|
|
64
|
+
const handled = handleError(err, { source: 'effect' }, rootForError)
|
|
65
|
+
if (handled) {
|
|
65
66
|
return
|
|
66
67
|
}
|
|
67
68
|
throw err
|
package/src/hooks.ts
CHANGED
|
@@ -5,18 +5,26 @@ import { createSignal, type SignalAccessor, type ComputedAccessor } from './sign
|
|
|
5
5
|
interface HookContext {
|
|
6
6
|
slots: unknown[]
|
|
7
7
|
cursor: number
|
|
8
|
+
rendering?: boolean
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
const ctxStack: HookContext[] = []
|
|
11
12
|
|
|
13
|
+
function assertRenderContext(ctx: HookContext, hookName: string): void {
|
|
14
|
+
if (!ctx.rendering) {
|
|
15
|
+
throw new Error(`${hookName} can only be used during render execution`)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
12
19
|
export function __fictUseContext(): HookContext {
|
|
13
20
|
if (ctxStack.length === 0) {
|
|
14
|
-
const ctx: HookContext = { slots: [], cursor: 0 }
|
|
21
|
+
const ctx: HookContext = { slots: [], cursor: 0, rendering: true }
|
|
15
22
|
ctxStack.push(ctx)
|
|
16
23
|
return ctx
|
|
17
24
|
}
|
|
18
25
|
const ctx = ctxStack[ctxStack.length - 1]!
|
|
19
26
|
ctx.cursor = 0
|
|
27
|
+
ctx.rendering = true
|
|
20
28
|
return ctx
|
|
21
29
|
}
|
|
22
30
|
|
|
@@ -35,6 +43,7 @@ export function __fictResetContext(): void {
|
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
export function __fictUseSignal<T>(ctx: HookContext, initial: T, slot?: number): SignalAccessor<T> {
|
|
46
|
+
assertRenderContext(ctx, '__fictUseSignal')
|
|
38
47
|
const index = slot ?? ctx.cursor++
|
|
39
48
|
if (!ctx.slots[index]) {
|
|
40
49
|
ctx.slots[index] = createSignal(initial)
|
|
@@ -47,6 +56,7 @@ export function __fictUseMemo<T>(
|
|
|
47
56
|
fn: () => T,
|
|
48
57
|
slot?: number,
|
|
49
58
|
): ComputedAccessor<T> {
|
|
59
|
+
assertRenderContext(ctx, '__fictUseMemo')
|
|
50
60
|
const index = slot ?? ctx.cursor++
|
|
51
61
|
if (!ctx.slots[index]) {
|
|
52
62
|
ctx.slots[index] = createMemo(fn)
|
|
@@ -55,6 +65,7 @@ export function __fictUseMemo<T>(
|
|
|
55
65
|
}
|
|
56
66
|
|
|
57
67
|
export function __fictUseEffect(ctx: HookContext, fn: () => void, slot?: number): void {
|
|
68
|
+
assertRenderContext(ctx, '__fictUseEffect')
|
|
58
69
|
const index = slot ?? ctx.cursor++
|
|
59
70
|
if (!ctx.slots[index]) {
|
|
60
71
|
ctx.slots[index] = createEffect(fn)
|
|
@@ -64,9 +75,11 @@ export function __fictUseEffect(ctx: HookContext, fn: () => void, slot?: number)
|
|
|
64
75
|
export function __fictRender<T>(ctx: HookContext, fn: () => T): T {
|
|
65
76
|
ctxStack.push(ctx)
|
|
66
77
|
ctx.cursor = 0
|
|
78
|
+
ctx.rendering = true
|
|
67
79
|
try {
|
|
68
80
|
return fn()
|
|
69
81
|
} finally {
|
|
82
|
+
ctx.rendering = false
|
|
70
83
|
ctxStack.pop()
|
|
71
84
|
}
|
|
72
85
|
}
|
package/src/list-helpers.ts
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
import { insertNodesBefore, removeNodes, toNodeArray } from './node-ops'
|
|
20
20
|
import reconcileArrays from './reconcile'
|
|
21
21
|
import { batch } from './scheduler'
|
|
22
|
-
import { createSignal, setActiveSub, type Signal } from './signal'
|
|
22
|
+
import { createSignal, flush, setActiveSub, type Signal } from './signal'
|
|
23
23
|
import type { FictNode } from './types'
|
|
24
24
|
|
|
25
25
|
// Re-export shared DOM helpers for compiler-generated code
|
|
@@ -339,7 +339,6 @@ export function createKeyedBlock<T>(
|
|
|
339
339
|
} finally {
|
|
340
340
|
setActiveSub(prevSub)
|
|
341
341
|
popRoot(prevRoot)
|
|
342
|
-
flushOnMount(root)
|
|
343
342
|
}
|
|
344
343
|
|
|
345
344
|
return {
|
|
@@ -414,16 +413,31 @@ function createFineGrainedKeyedList<T>(
|
|
|
414
413
|
const hostRoot = getCurrentRoot()
|
|
415
414
|
const fragment = document.createDocumentFragment()
|
|
416
415
|
fragment.append(container.startMarker, container.endMarker)
|
|
417
|
-
let pendingItems: T[] | null = null
|
|
418
416
|
let disposed = false
|
|
417
|
+
let effectDispose: (() => void) | undefined
|
|
418
|
+
let connectObserver: MutationObserver | null = null
|
|
419
|
+
let effectStarted = false
|
|
420
|
+
let startScheduled = false
|
|
421
|
+
|
|
422
|
+
const getConnectedParent = (): (ParentNode & Node) | null => {
|
|
423
|
+
const endParent = container.endMarker.parentNode
|
|
424
|
+
const startParent = container.startMarker.parentNode
|
|
425
|
+
if (
|
|
426
|
+
endParent &&
|
|
427
|
+
startParent &&
|
|
428
|
+
endParent === startParent &&
|
|
429
|
+
(endParent as Node).nodeType !== 11
|
|
430
|
+
) {
|
|
431
|
+
return endParent as ParentNode & Node
|
|
432
|
+
}
|
|
433
|
+
return null
|
|
434
|
+
}
|
|
419
435
|
|
|
420
436
|
const performDiff = () => {
|
|
421
437
|
if (disposed) return
|
|
422
|
-
|
|
438
|
+
const parent = getConnectedParent()
|
|
439
|
+
if (!parent) return
|
|
423
440
|
batch(() => {
|
|
424
|
-
const newItems = pendingItems || getItems()
|
|
425
|
-
pendingItems = null
|
|
426
|
-
|
|
427
441
|
const oldBlocks = container.blocks
|
|
428
442
|
const newBlocks = container.nextBlocks
|
|
429
443
|
const prevOrderedBlocks = container.orderedBlocks
|
|
@@ -432,27 +446,20 @@ function createFineGrainedKeyedList<T>(
|
|
|
432
446
|
newBlocks.clear()
|
|
433
447
|
nextOrderedBlocks.length = 0
|
|
434
448
|
orderedIndexByKey.clear()
|
|
435
|
-
|
|
436
|
-
const
|
|
437
|
-
const startParent = container.startMarker.parentNode
|
|
438
|
-
const parent =
|
|
439
|
-
endParent && startParent && endParent === startParent && (endParent as Node).isConnected
|
|
440
|
-
? (endParent as ParentNode & Node)
|
|
441
|
-
: null
|
|
442
|
-
|
|
443
|
-
// If markers aren't mounted yet, store items and retry in microtask
|
|
444
|
-
if (!parent) {
|
|
445
|
-
pendingItems = newItems
|
|
446
|
-
queueMicrotask(performDiff)
|
|
447
|
-
return
|
|
448
|
-
}
|
|
449
|
+
const createdBlocks: KeyedBlock<T>[] = []
|
|
450
|
+
const newItems = getItems()
|
|
449
451
|
|
|
450
452
|
if (newItems.length === 0) {
|
|
451
453
|
if (oldBlocks.size > 0) {
|
|
454
|
+
// Destroy all block roots first
|
|
452
455
|
for (const block of oldBlocks.values()) {
|
|
453
456
|
destroyRoot(block.root)
|
|
454
|
-
removeNodes(block.nodes)
|
|
455
457
|
}
|
|
458
|
+
// Use Range.deleteContents for efficient bulk DOM removal
|
|
459
|
+
const range = document.createRange()
|
|
460
|
+
range.setStartAfter(container.startMarker)
|
|
461
|
+
range.setEndBefore(container.endMarker)
|
|
462
|
+
range.deleteContents()
|
|
456
463
|
}
|
|
457
464
|
oldBlocks.clear()
|
|
458
465
|
newBlocks.clear()
|
|
@@ -472,8 +479,9 @@ function createFineGrainedKeyedList<T>(
|
|
|
472
479
|
// Phase 1: Build new blocks map (reuse or create)
|
|
473
480
|
newItems.forEach((item, index) => {
|
|
474
481
|
const key = keyFn(item, index)
|
|
475
|
-
|
|
482
|
+
// Micro-optimization: single Map.get instead of has+get
|
|
476
483
|
let block = oldBlocks.get(key)
|
|
484
|
+
const existed = block !== undefined
|
|
477
485
|
|
|
478
486
|
if (block) {
|
|
479
487
|
if (block.rawItem !== item) {
|
|
@@ -486,45 +494,30 @@ function createFineGrainedKeyedList<T>(
|
|
|
486
494
|
}
|
|
487
495
|
}
|
|
488
496
|
|
|
489
|
-
// If newBlocks already has this key (duplicate key case), clean up the previous block
|
|
490
|
-
const existingBlock = newBlocks.get(key)
|
|
491
|
-
if (existingBlock && existingBlock !== block) {
|
|
492
|
-
destroyRoot(existingBlock.root)
|
|
493
|
-
removeNodes(existingBlock.nodes)
|
|
494
|
-
}
|
|
495
|
-
|
|
496
497
|
if (block) {
|
|
498
|
+
// Reusing existing block from oldBlocks
|
|
497
499
|
newBlocks.set(key, block)
|
|
498
500
|
oldBlocks.delete(key)
|
|
499
501
|
} else {
|
|
502
|
+
// If newBlocks already has this key (duplicate key case), clean up the previous block
|
|
500
503
|
const existingBlock = newBlocks.get(key)
|
|
501
504
|
if (existingBlock) {
|
|
502
505
|
destroyRoot(existingBlock.root)
|
|
503
506
|
removeNodes(existingBlock.nodes)
|
|
504
507
|
}
|
|
505
|
-
|
|
506
508
|
// Create new block
|
|
507
509
|
block = createKeyedBlock(key, item, index, renderItem, needsIndex, hostRoot)
|
|
510
|
+
createdBlocks.push(block)
|
|
508
511
|
}
|
|
509
512
|
|
|
510
|
-
const resolvedBlock = block
|
|
513
|
+
const resolvedBlock = block
|
|
511
514
|
|
|
512
515
|
newBlocks.set(key, resolvedBlock)
|
|
513
516
|
|
|
517
|
+
// Micro-optimization: single Map.get instead of checking position multiple times
|
|
514
518
|
const position = orderedIndexByKey.get(key)
|
|
515
519
|
if (position !== undefined) {
|
|
516
520
|
appendCandidate = false
|
|
517
|
-
}
|
|
518
|
-
if (appendCandidate) {
|
|
519
|
-
if (index < prevCount) {
|
|
520
|
-
if (!prevOrderedBlocks[index] || prevOrderedBlocks[index]!.key !== key) {
|
|
521
|
-
appendCandidate = false
|
|
522
|
-
}
|
|
523
|
-
} else if (existed) {
|
|
524
|
-
appendCandidate = false
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
if (position !== undefined) {
|
|
528
521
|
const prior = nextOrderedBlocks[position]
|
|
529
522
|
if (prior && prior !== resolvedBlock) {
|
|
530
523
|
destroyRoot(prior.root)
|
|
@@ -532,6 +525,15 @@ function createFineGrainedKeyedList<T>(
|
|
|
532
525
|
}
|
|
533
526
|
nextOrderedBlocks[position] = resolvedBlock
|
|
534
527
|
} else {
|
|
528
|
+
if (appendCandidate) {
|
|
529
|
+
if (index < prevCount) {
|
|
530
|
+
if (!prevOrderedBlocks[index] || prevOrderedBlocks[index]!.key !== key) {
|
|
531
|
+
appendCandidate = false
|
|
532
|
+
}
|
|
533
|
+
} else if (existed) {
|
|
534
|
+
appendCandidate = false
|
|
535
|
+
}
|
|
536
|
+
}
|
|
535
537
|
orderedIndexByKey.set(key, nextOrderedBlocks.length)
|
|
536
538
|
nextOrderedBlocks.push(resolvedBlock)
|
|
537
539
|
}
|
|
@@ -568,6 +570,11 @@ function createFineGrainedKeyedList<T>(
|
|
|
568
570
|
container.nextBlocks = oldBlocks
|
|
569
571
|
container.orderedBlocks = nextOrderedBlocks
|
|
570
572
|
container.nextOrderedBlocks = prevOrderedBlocks
|
|
573
|
+
for (const block of createdBlocks) {
|
|
574
|
+
if (newBlocks.get(block.key) === block) {
|
|
575
|
+
flushOnMount(block.root)
|
|
576
|
+
}
|
|
577
|
+
}
|
|
571
578
|
return
|
|
572
579
|
}
|
|
573
580
|
|
|
@@ -608,24 +615,95 @@ function createFineGrainedKeyedList<T>(
|
|
|
608
615
|
container.nextBlocks = oldBlocks
|
|
609
616
|
container.orderedBlocks = nextOrderedBlocks
|
|
610
617
|
container.nextOrderedBlocks = prevOrderedBlocks
|
|
618
|
+
for (const block of createdBlocks) {
|
|
619
|
+
if (newBlocks.get(block.key) === block) {
|
|
620
|
+
flushOnMount(block.root)
|
|
621
|
+
}
|
|
622
|
+
}
|
|
611
623
|
})
|
|
612
624
|
}
|
|
613
625
|
|
|
614
|
-
const
|
|
626
|
+
const disconnectObserver = () => {
|
|
627
|
+
connectObserver?.disconnect()
|
|
628
|
+
connectObserver = null
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const ensureEffectStarted = (): boolean => {
|
|
632
|
+
if (disposed || effectStarted) return effectStarted
|
|
633
|
+
const parent = getConnectedParent()
|
|
634
|
+
if (!parent) return false
|
|
635
|
+
const start = () => {
|
|
636
|
+
effectDispose = createRenderEffect(performDiff)
|
|
637
|
+
effectStarted = true
|
|
638
|
+
}
|
|
639
|
+
if (hostRoot) {
|
|
640
|
+
const prev = pushRoot(hostRoot)
|
|
641
|
+
try {
|
|
642
|
+
start()
|
|
643
|
+
} finally {
|
|
644
|
+
popRoot(prev)
|
|
645
|
+
}
|
|
646
|
+
} else {
|
|
647
|
+
start()
|
|
648
|
+
}
|
|
649
|
+
return true
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const waitForConnection = () => {
|
|
653
|
+
if (connectObserver || typeof MutationObserver === 'undefined') return
|
|
654
|
+
connectObserver = new MutationObserver(() => {
|
|
655
|
+
if (disposed) return
|
|
656
|
+
if (getConnectedParent()) {
|
|
657
|
+
disconnectObserver()
|
|
658
|
+
if (ensureEffectStarted()) {
|
|
659
|
+
flush()
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
})
|
|
663
|
+
connectObserver.observe(document, { childList: true, subtree: true })
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const scheduleStart = () => {
|
|
667
|
+
if (startScheduled || disposed || effectStarted) return
|
|
668
|
+
startScheduled = true
|
|
669
|
+
const run = () => {
|
|
670
|
+
startScheduled = false
|
|
671
|
+
if (!ensureEffectStarted()) {
|
|
672
|
+
waitForConnection()
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
if (typeof queueMicrotask === 'function') {
|
|
676
|
+
queueMicrotask(run)
|
|
677
|
+
} else {
|
|
678
|
+
Promise.resolve()
|
|
679
|
+
.then(run)
|
|
680
|
+
.catch(() => undefined)
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
scheduleStart()
|
|
615
685
|
|
|
616
686
|
return {
|
|
617
|
-
marker
|
|
687
|
+
get marker() {
|
|
688
|
+
scheduleStart()
|
|
689
|
+
return fragment
|
|
690
|
+
},
|
|
618
691
|
startMarker: container.startMarker,
|
|
619
692
|
endMarker: container.endMarker,
|
|
620
693
|
// Flush pending items - call after markers are inserted into DOM
|
|
621
694
|
flush: () => {
|
|
622
|
-
if (
|
|
623
|
-
|
|
695
|
+
if (disposed) return
|
|
696
|
+
scheduleStart()
|
|
697
|
+
if (ensureEffectStarted()) {
|
|
698
|
+
flush()
|
|
699
|
+
} else {
|
|
700
|
+
waitForConnection()
|
|
624
701
|
}
|
|
625
702
|
},
|
|
626
703
|
dispose: () => {
|
|
627
704
|
disposed = true
|
|
628
705
|
effectDispose?.()
|
|
706
|
+
disconnectObserver()
|
|
629
707
|
container.dispose()
|
|
630
708
|
},
|
|
631
709
|
}
|
package/src/signal.ts
CHANGED
|
@@ -1057,10 +1057,20 @@ export function endBatch(): void {
|
|
|
1057
1057
|
*/
|
|
1058
1058
|
export function batch<T>(fn: () => T): T {
|
|
1059
1059
|
++batchDepth
|
|
1060
|
+
let _error: unknown
|
|
1061
|
+
let hasError = false
|
|
1060
1062
|
try {
|
|
1061
1063
|
return fn()
|
|
1064
|
+
} catch (e) {
|
|
1065
|
+
_error = e
|
|
1066
|
+
hasError = true
|
|
1067
|
+
throw e
|
|
1062
1068
|
} finally {
|
|
1063
|
-
|
|
1069
|
+
--batchDepth
|
|
1070
|
+
// Only flush if no error occurred to avoid interfering with error propagation
|
|
1071
|
+
if (!hasError && batchDepth === 0) {
|
|
1072
|
+
flush()
|
|
1073
|
+
}
|
|
1064
1074
|
}
|
|
1065
1075
|
}
|
|
1066
1076
|
/**
|
|
@@ -1253,7 +1263,7 @@ export function createSelector<T>(
|
|
|
1253
1263
|
let current = source()
|
|
1254
1264
|
const observers = new Map<T, SignalAccessor<boolean>>()
|
|
1255
1265
|
|
|
1256
|
-
effect(() => {
|
|
1266
|
+
const dispose = effect(() => {
|
|
1257
1267
|
const next = source()
|
|
1258
1268
|
if (equalityFn(current, next)) return
|
|
1259
1269
|
|
|
@@ -1265,6 +1275,10 @@ export function createSelector<T>(
|
|
|
1265
1275
|
|
|
1266
1276
|
current = next
|
|
1267
1277
|
})
|
|
1278
|
+
registerRootCleanup(() => {
|
|
1279
|
+
dispose()
|
|
1280
|
+
observers.clear()
|
|
1281
|
+
})
|
|
1268
1282
|
|
|
1269
1283
|
return (key: T) => {
|
|
1270
1284
|
let sig = observers.get(key)
|