@fictjs/runtime 0.0.11 → 0.0.12
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 +243 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.dev.js +258 -43
- package/dist/index.dev.js.map +1 -1
- package/dist/index.js +243 -45
- package/dist/index.js.map +1 -1
- package/dist/slim.cjs +221 -35
- package/dist/slim.cjs.map +1 -1
- package/dist/slim.js +221 -35
- package/dist/slim.js.map +1 -1
- package/package.json +1 -1
- package/src/binding.ts +3 -1
- package/src/dev.d.ts +5 -0
- package/src/dom.ts +20 -2
- package/src/error-boundary.ts +11 -2
- package/src/lifecycle.ts +4 -1
- package/src/list-helpers.ts +243 -22
- package/src/props.ts +2 -4
- package/src/reconcile.ts +4 -0
- package/src/signal.ts +69 -24
- package/src/suspense.ts +24 -7
- package/src/transition.ts +4 -1
package/package.json
CHANGED
package/src/binding.ts
CHANGED
|
@@ -1124,7 +1124,9 @@ export function bindEvent(
|
|
|
1124
1124
|
const fn = resolveHandler()
|
|
1125
1125
|
callEventHandler(fn as EventListenerOrEventListenerObject, args[0] as Event, el)
|
|
1126
1126
|
} catch (err) {
|
|
1127
|
-
handleError(err, { source: 'event', eventName }, rootRef)
|
|
1127
|
+
if (!handleError(err, { source: 'event', eventName }, rootRef)) {
|
|
1128
|
+
throw err
|
|
1129
|
+
}
|
|
1128
1130
|
}
|
|
1129
1131
|
}
|
|
1130
1132
|
|
package/src/dev.d.ts
ADDED
package/src/dom.ts
CHANGED
|
@@ -54,6 +54,10 @@ type NamespaceContext = 'svg' | 'mathml' | null
|
|
|
54
54
|
|
|
55
55
|
const SVG_NS = 'http://www.w3.org/2000/svg'
|
|
56
56
|
const MATHML_NS = 'http://www.w3.org/1998/Math/MathML'
|
|
57
|
+
const isDev =
|
|
58
|
+
typeof __DEV__ !== 'undefined'
|
|
59
|
+
? __DEV__
|
|
60
|
+
: typeof process === 'undefined' || process.env?.NODE_ENV !== 'production'
|
|
57
61
|
|
|
58
62
|
// ============================================================================
|
|
59
63
|
// Main Render Function
|
|
@@ -431,10 +435,17 @@ function applyRef(el: Element, value: unknown): void {
|
|
|
431
435
|
refFn(el)
|
|
432
436
|
|
|
433
437
|
// Match React behavior: call ref(null) on unmount
|
|
434
|
-
|
|
438
|
+
const root = getCurrentRoot()
|
|
439
|
+
if (root) {
|
|
435
440
|
registerRootCleanup(() => {
|
|
436
441
|
refFn(null)
|
|
437
442
|
})
|
|
443
|
+
} else if (isDev) {
|
|
444
|
+
console.warn(
|
|
445
|
+
'[fict] Ref applied outside of a root context. ' +
|
|
446
|
+
'The ref cleanup (setting to null) will not run automatically. ' +
|
|
447
|
+
'Consider using createRoot() or ensure the element is created within a component.',
|
|
448
|
+
)
|
|
438
449
|
}
|
|
439
450
|
} else if (value && typeof value === 'object' && 'current' in value) {
|
|
440
451
|
// Object ref
|
|
@@ -442,10 +453,17 @@ function applyRef(el: Element, value: unknown): void {
|
|
|
442
453
|
refObj.current = el
|
|
443
454
|
|
|
444
455
|
// Auto-cleanup on unmount
|
|
445
|
-
|
|
456
|
+
const root = getCurrentRoot()
|
|
457
|
+
if (root) {
|
|
446
458
|
registerRootCleanup(() => {
|
|
447
459
|
refObj.current = null
|
|
448
460
|
})
|
|
461
|
+
} else if (isDev) {
|
|
462
|
+
console.warn(
|
|
463
|
+
'[fict] Ref applied outside of a root context. ' +
|
|
464
|
+
'The ref cleanup (setting to null) will not run automatically. ' +
|
|
465
|
+
'Consider using createRoot() or ensure the element is created within a component.',
|
|
466
|
+
)
|
|
449
467
|
}
|
|
450
468
|
}
|
|
451
469
|
}
|
package/src/error-boundary.ts
CHANGED
|
@@ -72,13 +72,22 @@ export function ErrorBoundary(props: ErrorBoundaryProps): FictNode {
|
|
|
72
72
|
if (renderingFallback) {
|
|
73
73
|
throw err
|
|
74
74
|
}
|
|
75
|
+
// nested errors. If fallback rendering also throws, we should NOT reset
|
|
76
|
+
// the flag until we're sure no more recursion is happening.
|
|
75
77
|
renderingFallback = true
|
|
76
78
|
try {
|
|
77
79
|
renderValue(toView(err))
|
|
78
|
-
|
|
80
|
+
// Only reset if successful - if renderValue threw, we want to keep
|
|
81
|
+
// renderingFallback = true to prevent infinite recursion
|
|
79
82
|
renderingFallback = false
|
|
83
|
+
props.onError?.(err)
|
|
84
|
+
} catch (fallbackErr) {
|
|
85
|
+
// Fallback rendering failed - keep renderingFallback = true
|
|
86
|
+
// to prevent further attempts, then rethrow
|
|
87
|
+
// If fallback fails, report both errors
|
|
88
|
+
props.onError?.(err)
|
|
89
|
+
throw fallbackErr
|
|
80
90
|
}
|
|
81
|
-
props.onError?.(err)
|
|
82
91
|
return
|
|
83
92
|
}
|
|
84
93
|
popRoot(prev)
|
package/src/lifecycle.ts
CHANGED
|
@@ -237,7 +237,10 @@ export function handleError(err: unknown, info?: ErrorInfo, startRoot?: RootCont
|
|
|
237
237
|
}
|
|
238
238
|
}
|
|
239
239
|
}
|
|
240
|
-
|
|
240
|
+
// The caller (e.g., runCleanupList) can decide whether to rethrow.
|
|
241
|
+
// This makes the API consistent: handleError always returns a boolean
|
|
242
|
+
// indicating whether the error was handled.
|
|
243
|
+
return false
|
|
241
244
|
}
|
|
242
245
|
|
|
243
246
|
export function handleSuspend(
|
package/src/list-helpers.ts
CHANGED
|
@@ -19,12 +19,17 @@ 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, flush, setActiveSub, type Signal } from './signal'
|
|
22
|
+
import { createSignal, effectScope, 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
|
|
26
26
|
export { insertNodesBefore, removeNodes, toNodeArray }
|
|
27
27
|
|
|
28
|
+
const isDev =
|
|
29
|
+
typeof __DEV__ !== 'undefined'
|
|
30
|
+
? __DEV__
|
|
31
|
+
: typeof process === 'undefined' || process.env?.NODE_ENV !== 'production'
|
|
32
|
+
|
|
28
33
|
// ============================================================================
|
|
29
34
|
// Types
|
|
30
35
|
// ============================================================================
|
|
@@ -201,6 +206,9 @@ function removeBlockRange(block: MarkerBlock): void {
|
|
|
201
206
|
}
|
|
202
207
|
}
|
|
203
208
|
|
|
209
|
+
// Number.MAX_SAFE_INTEGER is 2^53 - 1, but we reset earlier to avoid any precision issues
|
|
210
|
+
const MAX_SAFE_VERSION = 0x1fffffffffffff // 2^53 - 1
|
|
211
|
+
|
|
204
212
|
export function createVersionedSignalAccessor<T>(initialValue: T): Signal<T> {
|
|
205
213
|
let current = initialValue
|
|
206
214
|
let version = 0
|
|
@@ -212,7 +220,8 @@ export function createVersionedSignalAccessor<T>(initialValue: T): Signal<T> {
|
|
|
212
220
|
return current
|
|
213
221
|
}
|
|
214
222
|
current = value as T
|
|
215
|
-
version
|
|
223
|
+
// This is safe because we only care about version changes, not absolute values
|
|
224
|
+
version = version >= MAX_SAFE_VERSION ? 1 : version + 1
|
|
216
225
|
track(version)
|
|
217
226
|
}
|
|
218
227
|
|
|
@@ -317,24 +326,36 @@ export function createKeyedBlock<T>(
|
|
|
317
326
|
}) as Signal<number>)
|
|
318
327
|
const root = createRootContext(hostRoot)
|
|
319
328
|
const prevRoot = pushRoot(root)
|
|
329
|
+
// maintaining proper cleanup chain. The scope will be disposed when
|
|
330
|
+
// the root is destroyed, ensuring nested effects are properly cleaned up.
|
|
331
|
+
let nodes: Node[] = []
|
|
332
|
+
let scopeDispose: (() => void) | undefined
|
|
320
333
|
|
|
321
|
-
//
|
|
322
|
-
//
|
|
334
|
+
// First, isolate from parent effect to prevent child effects from being
|
|
335
|
+
// purged when the outer effect (e.g., performDiff) re-runs
|
|
323
336
|
const prevSub = setActiveSub(undefined)
|
|
324
337
|
|
|
325
|
-
let nodes: Node[] = []
|
|
326
338
|
try {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
339
|
+
// Create an effectScope that will track all effects created during render
|
|
340
|
+
scopeDispose = effectScope(() => {
|
|
341
|
+
const rendered = render(itemSig, indexSig, key)
|
|
342
|
+
// If render returns real DOM nodes/arrays, preserve them to avoid
|
|
343
|
+
// reparenting side-effects (tests may pre-insert them).
|
|
344
|
+
if (
|
|
345
|
+
rendered instanceof Node ||
|
|
346
|
+
(Array.isArray(rendered) && rendered.every(n => n instanceof Node))
|
|
347
|
+
) {
|
|
348
|
+
nodes = toNodeArray(rendered)
|
|
349
|
+
} else {
|
|
350
|
+
const element = createElement(rendered as unknown as FictNode)
|
|
351
|
+
nodes = toNodeArray(element)
|
|
352
|
+
}
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
// Register the scope cleanup with the root so effects are cleaned up
|
|
356
|
+
// when the block is destroyed
|
|
357
|
+
if (scopeDispose) {
|
|
358
|
+
root.cleanups.push(scopeDispose)
|
|
338
359
|
}
|
|
339
360
|
} finally {
|
|
340
361
|
setActiveSub(prevSub)
|
|
@@ -379,6 +400,107 @@ export function isNodeBetweenMarkers(
|
|
|
379
400
|
return false
|
|
380
401
|
}
|
|
381
402
|
|
|
403
|
+
function reorderBySwap<T>(
|
|
404
|
+
parent: ParentNode & Node,
|
|
405
|
+
first: KeyedBlock<T>,
|
|
406
|
+
second: KeyedBlock<T>,
|
|
407
|
+
): boolean {
|
|
408
|
+
if (first === second) return false
|
|
409
|
+
const firstNodes = first.nodes
|
|
410
|
+
const secondNodes = second.nodes
|
|
411
|
+
if (firstNodes.length === 0 || secondNodes.length === 0) return false
|
|
412
|
+
const lastFirst = firstNodes[firstNodes.length - 1]!
|
|
413
|
+
const lastSecond = secondNodes[secondNodes.length - 1]!
|
|
414
|
+
const afterFirst = lastFirst.nextSibling
|
|
415
|
+
const afterSecond = lastSecond.nextSibling
|
|
416
|
+
moveNodesBefore(parent, firstNodes, afterSecond)
|
|
417
|
+
moveNodesBefore(parent, secondNodes, afterFirst)
|
|
418
|
+
return true
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function getLISIndices(sequence: number[]): number[] {
|
|
422
|
+
const predecessors = new Array<number>(sequence.length)
|
|
423
|
+
const result: number[] = []
|
|
424
|
+
|
|
425
|
+
for (let i = 0; i < sequence.length; i++) {
|
|
426
|
+
const value = sequence[i]!
|
|
427
|
+
if (value < 0) {
|
|
428
|
+
predecessors[i] = -1
|
|
429
|
+
continue
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
let low = 0
|
|
433
|
+
let high = result.length
|
|
434
|
+
while (low < high) {
|
|
435
|
+
const mid = (low + high) >> 1
|
|
436
|
+
if (sequence[result[mid]!]! < value) {
|
|
437
|
+
low = mid + 1
|
|
438
|
+
} else {
|
|
439
|
+
high = mid
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
predecessors[i] = low > 0 ? result[low - 1]! : -1
|
|
444
|
+
if (low === result.length) {
|
|
445
|
+
result.push(i)
|
|
446
|
+
} else {
|
|
447
|
+
result[low] = i
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const lis: number[] = new Array(result.length)
|
|
452
|
+
let k = result.length > 0 ? result[result.length - 1]! : -1
|
|
453
|
+
for (let i = result.length - 1; i >= 0; i--) {
|
|
454
|
+
lis[i] = k
|
|
455
|
+
k = predecessors[k]!
|
|
456
|
+
}
|
|
457
|
+
return lis
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function reorderByLIS<T>(
|
|
461
|
+
parent: ParentNode & Node,
|
|
462
|
+
endMarker: Comment,
|
|
463
|
+
prev: KeyedBlock<T>[],
|
|
464
|
+
next: KeyedBlock<T>[],
|
|
465
|
+
): boolean {
|
|
466
|
+
const positions = new Map<KeyedBlock<T>, number>()
|
|
467
|
+
for (let i = 0; i < prev.length; i++) {
|
|
468
|
+
positions.set(prev[i]!, i)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const sequence = new Array<number>(next.length)
|
|
472
|
+
for (let i = 0; i < next.length; i++) {
|
|
473
|
+
const position = positions.get(next[i]!)
|
|
474
|
+
if (position === undefined) return false
|
|
475
|
+
sequence[i] = position
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const lisIndices = getLISIndices(sequence)
|
|
479
|
+
if (lisIndices.length === sequence.length) return true
|
|
480
|
+
|
|
481
|
+
const inLIS = new Array<boolean>(sequence.length).fill(false)
|
|
482
|
+
for (let i = 0; i < lisIndices.length; i++) {
|
|
483
|
+
inLIS[lisIndices[i]!] = true
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
let anchor: Node | null = endMarker
|
|
487
|
+
let moved = false
|
|
488
|
+
for (let i = next.length - 1; i >= 0; i--) {
|
|
489
|
+
const block = next[i]!
|
|
490
|
+
const nodes = block.nodes
|
|
491
|
+
if (nodes.length === 0) continue
|
|
492
|
+
if (inLIS[i]) {
|
|
493
|
+
anchor = nodes[0]!
|
|
494
|
+
continue
|
|
495
|
+
}
|
|
496
|
+
moveNodesBefore(parent, nodes, anchor)
|
|
497
|
+
anchor = nodes[0]!
|
|
498
|
+
moved = true
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return moved
|
|
502
|
+
}
|
|
503
|
+
|
|
382
504
|
// ============================================================================
|
|
383
505
|
// High-Level List Binding (for compiler-generated code)
|
|
384
506
|
// ============================================================================
|
|
@@ -443,10 +565,6 @@ function createFineGrainedKeyedList<T>(
|
|
|
443
565
|
const prevOrderedBlocks = container.orderedBlocks
|
|
444
566
|
const nextOrderedBlocks = container.nextOrderedBlocks
|
|
445
567
|
const orderedIndexByKey = container.orderedIndexByKey
|
|
446
|
-
newBlocks.clear()
|
|
447
|
-
nextOrderedBlocks.length = 0
|
|
448
|
-
orderedIndexByKey.clear()
|
|
449
|
-
const createdBlocks: KeyedBlock<T>[] = []
|
|
450
568
|
const newItems = getItems()
|
|
451
569
|
|
|
452
570
|
if (newItems.length === 0) {
|
|
@@ -473,8 +591,45 @@ function createFineGrainedKeyedList<T>(
|
|
|
473
591
|
}
|
|
474
592
|
|
|
475
593
|
const prevCount = prevOrderedBlocks.length
|
|
594
|
+
if (prevCount > 0 && newItems.length === prevCount && orderedIndexByKey.size === prevCount) {
|
|
595
|
+
let stableOrder = true
|
|
596
|
+
const seen = new Set<string | number>()
|
|
597
|
+
for (let i = 0; i < prevCount; i++) {
|
|
598
|
+
const item = newItems[i]!
|
|
599
|
+
const key = keyFn(item, i)
|
|
600
|
+
if (seen.has(key) || prevOrderedBlocks[i]!.key !== key) {
|
|
601
|
+
stableOrder = false
|
|
602
|
+
break
|
|
603
|
+
}
|
|
604
|
+
seen.add(key)
|
|
605
|
+
}
|
|
606
|
+
if (stableOrder) {
|
|
607
|
+
for (let i = 0; i < prevCount; i++) {
|
|
608
|
+
const item = newItems[i]!
|
|
609
|
+
const block = prevOrderedBlocks[i]!
|
|
610
|
+
if (block.rawItem !== item) {
|
|
611
|
+
block.rawItem = item
|
|
612
|
+
block.item(item)
|
|
613
|
+
}
|
|
614
|
+
if (needsIndex && block.rawIndex !== i) {
|
|
615
|
+
block.rawIndex = i
|
|
616
|
+
block.index(i)
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
return
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
newBlocks.clear()
|
|
624
|
+
nextOrderedBlocks.length = 0
|
|
625
|
+
orderedIndexByKey.clear()
|
|
626
|
+
const createdBlocks: KeyedBlock<T>[] = []
|
|
476
627
|
let appendCandidate = prevCount > 0 && newItems.length >= prevCount
|
|
477
628
|
const appendedBlocks: KeyedBlock<T>[] = []
|
|
629
|
+
let mismatchCount = 0
|
|
630
|
+
let mismatchFirst = -1
|
|
631
|
+
let mismatchSecond = -1
|
|
632
|
+
let hasDuplicateKey = false
|
|
478
633
|
|
|
479
634
|
// Phase 1: Build new blocks map (reuse or create)
|
|
480
635
|
newItems.forEach((item, index) => {
|
|
@@ -502,6 +657,12 @@ function createFineGrainedKeyedList<T>(
|
|
|
502
657
|
// If newBlocks already has this key (duplicate key case), clean up the previous block
|
|
503
658
|
const existingBlock = newBlocks.get(key)
|
|
504
659
|
if (existingBlock) {
|
|
660
|
+
if (isDev) {
|
|
661
|
+
console.warn(
|
|
662
|
+
`[fict] Duplicate key "${String(key)}" detected in list rendering. ` +
|
|
663
|
+
`Each item should have a unique key. The previous item with this key will be replaced.`,
|
|
664
|
+
)
|
|
665
|
+
}
|
|
505
666
|
destroyRoot(existingBlock.root)
|
|
506
667
|
removeNodes(existingBlock.nodes)
|
|
507
668
|
}
|
|
@@ -518,6 +679,7 @@ function createFineGrainedKeyedList<T>(
|
|
|
518
679
|
const position = orderedIndexByKey.get(key)
|
|
519
680
|
if (position !== undefined) {
|
|
520
681
|
appendCandidate = false
|
|
682
|
+
hasDuplicateKey = true
|
|
521
683
|
const prior = nextOrderedBlocks[position]
|
|
522
684
|
if (prior && prior !== resolvedBlock) {
|
|
523
685
|
destroyRoot(prior.root)
|
|
@@ -534,8 +696,20 @@ function createFineGrainedKeyedList<T>(
|
|
|
534
696
|
appendCandidate = false
|
|
535
697
|
}
|
|
536
698
|
}
|
|
537
|
-
|
|
699
|
+
const nextIndex = nextOrderedBlocks.length
|
|
700
|
+
orderedIndexByKey.set(key, nextIndex)
|
|
538
701
|
nextOrderedBlocks.push(resolvedBlock)
|
|
702
|
+
if (
|
|
703
|
+
mismatchCount < 3 &&
|
|
704
|
+
(nextIndex >= prevCount || prevOrderedBlocks[nextIndex] !== resolvedBlock)
|
|
705
|
+
) {
|
|
706
|
+
if (mismatchCount === 0) {
|
|
707
|
+
mismatchFirst = nextIndex
|
|
708
|
+
} else if (mismatchCount === 1) {
|
|
709
|
+
mismatchSecond = nextIndex
|
|
710
|
+
}
|
|
711
|
+
mismatchCount++
|
|
712
|
+
}
|
|
539
713
|
}
|
|
540
714
|
|
|
541
715
|
if (appendCandidate && index >= prevCount) {
|
|
@@ -587,8 +761,41 @@ function createFineGrainedKeyedList<T>(
|
|
|
587
761
|
oldBlocks.clear()
|
|
588
762
|
}
|
|
589
763
|
|
|
764
|
+
const canReorderInPlace =
|
|
765
|
+
createdBlocks.length === 0 &&
|
|
766
|
+
oldBlocks.size === 0 &&
|
|
767
|
+
nextOrderedBlocks.length === prevOrderedBlocks.length
|
|
768
|
+
|
|
769
|
+
let skipReconcile = false
|
|
770
|
+
let updateNodeBuffer = true
|
|
771
|
+
|
|
772
|
+
if (canReorderInPlace && nextOrderedBlocks.length > 0 && !hasDuplicateKey) {
|
|
773
|
+
if (mismatchCount === 0) {
|
|
774
|
+
skipReconcile = true
|
|
775
|
+
updateNodeBuffer = false
|
|
776
|
+
} else if (
|
|
777
|
+
mismatchCount === 2 &&
|
|
778
|
+
prevOrderedBlocks[mismatchFirst] === nextOrderedBlocks[mismatchSecond] &&
|
|
779
|
+
prevOrderedBlocks[mismatchSecond] === nextOrderedBlocks[mismatchFirst]
|
|
780
|
+
) {
|
|
781
|
+
if (
|
|
782
|
+
reorderBySwap(
|
|
783
|
+
parent,
|
|
784
|
+
prevOrderedBlocks[mismatchFirst]!,
|
|
785
|
+
prevOrderedBlocks[mismatchSecond]!,
|
|
786
|
+
)
|
|
787
|
+
) {
|
|
788
|
+
skipReconcile = true
|
|
789
|
+
}
|
|
790
|
+
} else if (
|
|
791
|
+
reorderByLIS(parent, container.endMarker, prevOrderedBlocks, nextOrderedBlocks)
|
|
792
|
+
) {
|
|
793
|
+
skipReconcile = true
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
590
797
|
// Phase 3: Reconcile DOM with buffered node arrays
|
|
591
|
-
if (newBlocks.size > 0 || container.currentNodes.length > 0) {
|
|
798
|
+
if (!skipReconcile && (newBlocks.size > 0 || container.currentNodes.length > 0)) {
|
|
592
799
|
const prevNodes = container.currentNodes
|
|
593
800
|
const nextNodes = container.nextNodes
|
|
594
801
|
nextNodes.length = 0
|
|
@@ -608,6 +815,20 @@ function createFineGrainedKeyedList<T>(
|
|
|
608
815
|
// Swap buffers to reuse arrays on next diff
|
|
609
816
|
container.currentNodes = nextNodes
|
|
610
817
|
container.nextNodes = prevNodes
|
|
818
|
+
} else if (skipReconcile && updateNodeBuffer) {
|
|
819
|
+
const prevNodes = container.currentNodes
|
|
820
|
+
const nextNodes = container.nextNodes
|
|
821
|
+
nextNodes.length = 0
|
|
822
|
+
nextNodes.push(container.startMarker)
|
|
823
|
+
for (let i = 0; i < nextOrderedBlocks.length; i++) {
|
|
824
|
+
const nodes = nextOrderedBlocks[i]!.nodes
|
|
825
|
+
for (let j = 0; j < nodes.length; j++) {
|
|
826
|
+
nextNodes.push(nodes[j]!)
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
nextNodes.push(container.endMarker)
|
|
830
|
+
container.currentNodes = nextNodes
|
|
831
|
+
container.nextNodes = prevNodes
|
|
611
832
|
}
|
|
612
833
|
|
|
613
834
|
// Swap block maps for reuse
|
package/src/props.ts
CHANGED
|
@@ -124,10 +124,7 @@ export function mergeProps<T extends Record<string, unknown>>(
|
|
|
124
124
|
|
|
125
125
|
return new Proxy({} as Record<string, unknown>, {
|
|
126
126
|
get(_, prop) {
|
|
127
|
-
//
|
|
128
|
-
if (typeof prop === 'symbol') {
|
|
129
|
-
return undefined
|
|
130
|
-
}
|
|
127
|
+
// Only return undefined if no source has this Symbol property
|
|
131
128
|
// Search sources in reverse order (last wins)
|
|
132
129
|
for (let i = validSources.length - 1; i >= 0; i--) {
|
|
133
130
|
const src = validSources[i]!
|
|
@@ -136,6 +133,7 @@ export function mergeProps<T extends Record<string, unknown>>(
|
|
|
136
133
|
|
|
137
134
|
const value = (raw as Record<string | symbol, unknown>)[prop]
|
|
138
135
|
// Preserve prop getters - let child component's createPropsProxy unwrap lazily
|
|
136
|
+
// Note: For Symbol properties, we still wrap in getter if source is dynamic
|
|
139
137
|
if (typeof src === 'function' && !isPropGetter(value)) {
|
|
140
138
|
return __fictProp(() => {
|
|
141
139
|
const latest = resolveSource(src)
|
package/src/reconcile.ts
CHANGED
|
@@ -21,6 +21,10 @@
|
|
|
21
21
|
* @param a - The old array of nodes (currently in DOM)
|
|
22
22
|
* @param b - The new array of nodes (target state)
|
|
23
23
|
*
|
|
24
|
+
* **Note:** This function may mutate the input array `a` during the swap
|
|
25
|
+
* optimization (step 5a). If you need to preserve the original array,
|
|
26
|
+
* pass a shallow copy: `reconcileArrays(parent, [...oldNodes], newNodes)`.
|
|
27
|
+
*
|
|
24
28
|
* @example
|
|
25
29
|
* ```ts
|
|
26
30
|
* const oldNodes = [node1, node2, node3]
|