@fictjs/runtime 0.0.11 → 0.0.13
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 +2373 -3048
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -141
- package/dist/index.d.ts +11 -141
- package/dist/index.dev.js +2558 -2653
- package/dist/index.dev.js.map +1 -1
- package/dist/index.js +2374 -3042
- package/dist/index.js.map +1 -1
- package/package.json +1 -6
- package/src/binding.ts +28 -423
- package/src/constants.ts +368 -344
- package/src/cycle-guard.ts +124 -97
- package/src/dev.d.ts +5 -0
- package/src/dom.ts +39 -27
- package/src/effect.ts +4 -0
- package/src/error-boundary.ts +11 -2
- package/src/hooks.ts +9 -1
- package/src/index.ts +1 -19
- package/src/lifecycle.ts +17 -3
- package/src/list-helpers.ts +248 -86
- package/src/props.ts +2 -4
- package/src/reconcile.ts +4 -0
- package/src/signal.ts +128 -63
- package/src/suspense.ts +24 -7
- package/src/transition.ts +4 -1
- package/dist/slim.cjs +0 -3668
- package/dist/slim.cjs.map +0 -1
- package/dist/slim.d.cts +0 -504
- package/dist/slim.d.ts +0 -504
- package/dist/slim.js +0 -3616
- package/dist/slim.js.map +0 -1
- package/src/slim.ts +0 -69
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
|
// ============================================================================
|
|
@@ -32,7 +37,7 @@ export { insertNodesBefore, removeNodes, toNodeArray }
|
|
|
32
37
|
/**
|
|
33
38
|
* A keyed block represents a single item in a list with its associated DOM nodes and state
|
|
34
39
|
*/
|
|
35
|
-
|
|
40
|
+
interface KeyedBlock<T = unknown> {
|
|
36
41
|
/** Unique key for this block */
|
|
37
42
|
key: string | number
|
|
38
43
|
/** DOM nodes belonging to this block */
|
|
@@ -52,7 +57,7 @@ export interface KeyedBlock<T = unknown> {
|
|
|
52
57
|
/**
|
|
53
58
|
* Container for managing keyed list blocks
|
|
54
59
|
*/
|
|
55
|
-
|
|
60
|
+
interface KeyedListContainer<T = unknown> {
|
|
56
61
|
/** Start marker comment node */
|
|
57
62
|
startMarker: Comment
|
|
58
63
|
/** End marker comment node */
|
|
@@ -97,15 +102,6 @@ type FineGrainedRenderItem<T> = (
|
|
|
97
102
|
key: string | number,
|
|
98
103
|
) => Node[]
|
|
99
104
|
|
|
100
|
-
/**
|
|
101
|
-
* A block identified by start/end comment markers.
|
|
102
|
-
*/
|
|
103
|
-
export interface MarkerBlock {
|
|
104
|
-
start: Comment
|
|
105
|
-
end: Comment
|
|
106
|
-
root?: RootContext
|
|
107
|
-
}
|
|
108
|
-
|
|
109
105
|
// ============================================================================
|
|
110
106
|
// DOM Manipulation Primitives
|
|
111
107
|
// ============================================================================
|
|
@@ -124,7 +120,8 @@ export function moveNodesBefore(parent: Node, nodes: Node[], anchor: Node | null
|
|
|
124
120
|
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
125
121
|
const node = nodes[i]!
|
|
126
122
|
if (!node || !(node instanceof Node)) {
|
|
127
|
-
|
|
123
|
+
const message = isDev ? 'Invalid node in moveNodesBefore' : 'FICT:E_NODE'
|
|
124
|
+
throw new Error(message)
|
|
128
125
|
}
|
|
129
126
|
// Only move if not already in correct position
|
|
130
127
|
if (node.nextSibling !== anchor) {
|
|
@@ -157,49 +154,8 @@ export function moveNodesBefore(parent: Node, nodes: Node[], anchor: Node | null
|
|
|
157
154
|
*
|
|
158
155
|
* @param nodes - Array of nodes to remove
|
|
159
156
|
*/
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
*/
|
|
163
|
-
export function moveMarkerBlock(parent: Node, block: MarkerBlock, anchor: Node | null): void {
|
|
164
|
-
const nodes = collectBlockNodes(block)
|
|
165
|
-
if (nodes.length === 0) return
|
|
166
|
-
moveNodesBefore(parent, nodes, anchor)
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Destroy a marker-delimited block, removing nodes and destroying the associated root.
|
|
171
|
-
*/
|
|
172
|
-
export function destroyMarkerBlock(block: MarkerBlock): void {
|
|
173
|
-
if (block.root) {
|
|
174
|
-
destroyRoot(block.root)
|
|
175
|
-
}
|
|
176
|
-
removeBlockRange(block)
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function collectBlockNodes(block: MarkerBlock): Node[] {
|
|
180
|
-
const nodes: Node[] = []
|
|
181
|
-
let cursor: Node | null = block.start
|
|
182
|
-
while (cursor) {
|
|
183
|
-
nodes.push(cursor)
|
|
184
|
-
if (cursor === block.end) {
|
|
185
|
-
break
|
|
186
|
-
}
|
|
187
|
-
cursor = cursor.nextSibling
|
|
188
|
-
}
|
|
189
|
-
return nodes
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function removeBlockRange(block: MarkerBlock): void {
|
|
193
|
-
let cursor: Node | null = block.start
|
|
194
|
-
while (cursor) {
|
|
195
|
-
const next: Node | null = cursor.nextSibling
|
|
196
|
-
cursor.parentNode?.removeChild(cursor)
|
|
197
|
-
if (cursor === block.end) {
|
|
198
|
-
break
|
|
199
|
-
}
|
|
200
|
-
cursor = next
|
|
201
|
-
}
|
|
202
|
-
}
|
|
157
|
+
// Number.MAX_SAFE_INTEGER is 2^53 - 1, but we reset earlier to avoid any precision issues
|
|
158
|
+
const MAX_SAFE_VERSION = 0x1fffffffffffff // 2^53 - 1
|
|
203
159
|
|
|
204
160
|
export function createVersionedSignalAccessor<T>(initialValue: T): Signal<T> {
|
|
205
161
|
let current = initialValue
|
|
@@ -212,7 +168,8 @@ export function createVersionedSignalAccessor<T>(initialValue: T): Signal<T> {
|
|
|
212
168
|
return current
|
|
213
169
|
}
|
|
214
170
|
current = value as T
|
|
215
|
-
version
|
|
171
|
+
// This is safe because we only care about version changes, not absolute values
|
|
172
|
+
version = version >= MAX_SAFE_VERSION ? 1 : version + 1
|
|
216
173
|
track(version)
|
|
217
174
|
}
|
|
218
175
|
|
|
@@ -229,7 +186,7 @@ export function createVersionedSignalAccessor<T>(initialValue: T): Signal<T> {
|
|
|
229
186
|
*
|
|
230
187
|
* @returns Container object with markers, blocks map, and dispose function
|
|
231
188
|
*/
|
|
232
|
-
|
|
189
|
+
function createKeyedListContainer<T = unknown>(): KeyedListContainer<T> {
|
|
233
190
|
const startMarker = document.createComment('fict:list:start')
|
|
234
191
|
const endMarker = document.createComment('fict:list:end')
|
|
235
192
|
|
|
@@ -297,7 +254,7 @@ export function createKeyedListContainer<T = unknown>(): KeyedListContainer<T> {
|
|
|
297
254
|
* @param render - Function that creates the DOM nodes and sets up bindings
|
|
298
255
|
* @returns New KeyedBlock
|
|
299
256
|
*/
|
|
300
|
-
|
|
257
|
+
function createKeyedBlock<T>(
|
|
301
258
|
key: string | number,
|
|
302
259
|
item: T,
|
|
303
260
|
index: number,
|
|
@@ -317,24 +274,36 @@ export function createKeyedBlock<T>(
|
|
|
317
274
|
}) as Signal<number>)
|
|
318
275
|
const root = createRootContext(hostRoot)
|
|
319
276
|
const prevRoot = pushRoot(root)
|
|
277
|
+
// maintaining proper cleanup chain. The scope will be disposed when
|
|
278
|
+
// the root is destroyed, ensuring nested effects are properly cleaned up.
|
|
279
|
+
let nodes: Node[] = []
|
|
280
|
+
let scopeDispose: (() => void) | undefined
|
|
320
281
|
|
|
321
|
-
//
|
|
322
|
-
//
|
|
282
|
+
// First, isolate from parent effect to prevent child effects from being
|
|
283
|
+
// purged when the outer effect (e.g., performDiff) re-runs
|
|
323
284
|
const prevSub = setActiveSub(undefined)
|
|
324
285
|
|
|
325
|
-
let nodes: Node[] = []
|
|
326
286
|
try {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
287
|
+
// Create an effectScope that will track all effects created during render
|
|
288
|
+
scopeDispose = effectScope(() => {
|
|
289
|
+
const rendered = render(itemSig, indexSig, key)
|
|
290
|
+
// If render returns real DOM nodes/arrays, preserve them to avoid
|
|
291
|
+
// reparenting side-effects (tests may pre-insert them).
|
|
292
|
+
if (
|
|
293
|
+
rendered instanceof Node ||
|
|
294
|
+
(Array.isArray(rendered) && rendered.every(n => n instanceof Node))
|
|
295
|
+
) {
|
|
296
|
+
nodes = toNodeArray(rendered)
|
|
297
|
+
} else {
|
|
298
|
+
const element = createElement(rendered as unknown as FictNode)
|
|
299
|
+
nodes = toNodeArray(element)
|
|
300
|
+
}
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
// Register the scope cleanup with the root so effects are cleaned up
|
|
304
|
+
// when the block is destroyed
|
|
305
|
+
if (scopeDispose) {
|
|
306
|
+
root.cleanups.push(scopeDispose)
|
|
338
307
|
}
|
|
339
308
|
} finally {
|
|
340
309
|
setActiveSub(prevSub)
|
|
@@ -356,13 +325,6 @@ export function createKeyedBlock<T>(
|
|
|
356
325
|
// Utilities
|
|
357
326
|
// ============================================================================
|
|
358
327
|
|
|
359
|
-
/**
|
|
360
|
-
* Find the first node after the start marker (for getting current anchor)
|
|
361
|
-
*/
|
|
362
|
-
export function getFirstNodeAfter(marker: Comment): Node | null {
|
|
363
|
-
return marker.nextSibling
|
|
364
|
-
}
|
|
365
|
-
|
|
366
328
|
/**
|
|
367
329
|
* Check if a node is between two markers
|
|
368
330
|
*/
|
|
@@ -379,6 +341,107 @@ export function isNodeBetweenMarkers(
|
|
|
379
341
|
return false
|
|
380
342
|
}
|
|
381
343
|
|
|
344
|
+
function reorderBySwap<T>(
|
|
345
|
+
parent: ParentNode & Node,
|
|
346
|
+
first: KeyedBlock<T>,
|
|
347
|
+
second: KeyedBlock<T>,
|
|
348
|
+
): boolean {
|
|
349
|
+
if (first === second) return false
|
|
350
|
+
const firstNodes = first.nodes
|
|
351
|
+
const secondNodes = second.nodes
|
|
352
|
+
if (firstNodes.length === 0 || secondNodes.length === 0) return false
|
|
353
|
+
const lastFirst = firstNodes[firstNodes.length - 1]!
|
|
354
|
+
const lastSecond = secondNodes[secondNodes.length - 1]!
|
|
355
|
+
const afterFirst = lastFirst.nextSibling
|
|
356
|
+
const afterSecond = lastSecond.nextSibling
|
|
357
|
+
moveNodesBefore(parent, firstNodes, afterSecond)
|
|
358
|
+
moveNodesBefore(parent, secondNodes, afterFirst)
|
|
359
|
+
return true
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function getLISIndices(sequence: number[]): number[] {
|
|
363
|
+
const predecessors = new Array<number>(sequence.length)
|
|
364
|
+
const result: number[] = []
|
|
365
|
+
|
|
366
|
+
for (let i = 0; i < sequence.length; i++) {
|
|
367
|
+
const value = sequence[i]!
|
|
368
|
+
if (value < 0) {
|
|
369
|
+
predecessors[i] = -1
|
|
370
|
+
continue
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
let low = 0
|
|
374
|
+
let high = result.length
|
|
375
|
+
while (low < high) {
|
|
376
|
+
const mid = (low + high) >> 1
|
|
377
|
+
if (sequence[result[mid]!]! < value) {
|
|
378
|
+
low = mid + 1
|
|
379
|
+
} else {
|
|
380
|
+
high = mid
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
predecessors[i] = low > 0 ? result[low - 1]! : -1
|
|
385
|
+
if (low === result.length) {
|
|
386
|
+
result.push(i)
|
|
387
|
+
} else {
|
|
388
|
+
result[low] = i
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const lis: number[] = new Array(result.length)
|
|
393
|
+
let k = result.length > 0 ? result[result.length - 1]! : -1
|
|
394
|
+
for (let i = result.length - 1; i >= 0; i--) {
|
|
395
|
+
lis[i] = k
|
|
396
|
+
k = predecessors[k]!
|
|
397
|
+
}
|
|
398
|
+
return lis
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function reorderByLIS<T>(
|
|
402
|
+
parent: ParentNode & Node,
|
|
403
|
+
endMarker: Comment,
|
|
404
|
+
prev: KeyedBlock<T>[],
|
|
405
|
+
next: KeyedBlock<T>[],
|
|
406
|
+
): boolean {
|
|
407
|
+
const positions = new Map<KeyedBlock<T>, number>()
|
|
408
|
+
for (let i = 0; i < prev.length; i++) {
|
|
409
|
+
positions.set(prev[i]!, i)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const sequence = new Array<number>(next.length)
|
|
413
|
+
for (let i = 0; i < next.length; i++) {
|
|
414
|
+
const position = positions.get(next[i]!)
|
|
415
|
+
if (position === undefined) return false
|
|
416
|
+
sequence[i] = position
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const lisIndices = getLISIndices(sequence)
|
|
420
|
+
if (lisIndices.length === sequence.length) return true
|
|
421
|
+
|
|
422
|
+
const inLIS = new Array<boolean>(sequence.length).fill(false)
|
|
423
|
+
for (let i = 0; i < lisIndices.length; i++) {
|
|
424
|
+
inLIS[lisIndices[i]!] = true
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
let anchor: Node | null = endMarker
|
|
428
|
+
let moved = false
|
|
429
|
+
for (let i = next.length - 1; i >= 0; i--) {
|
|
430
|
+
const block = next[i]!
|
|
431
|
+
const nodes = block.nodes
|
|
432
|
+
if (nodes.length === 0) continue
|
|
433
|
+
if (inLIS[i]) {
|
|
434
|
+
anchor = nodes[0]!
|
|
435
|
+
continue
|
|
436
|
+
}
|
|
437
|
+
moveNodesBefore(parent, nodes, anchor)
|
|
438
|
+
anchor = nodes[0]!
|
|
439
|
+
moved = true
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return moved
|
|
443
|
+
}
|
|
444
|
+
|
|
382
445
|
// ============================================================================
|
|
383
446
|
// High-Level List Binding (for compiler-generated code)
|
|
384
447
|
// ============================================================================
|
|
@@ -443,10 +506,6 @@ function createFineGrainedKeyedList<T>(
|
|
|
443
506
|
const prevOrderedBlocks = container.orderedBlocks
|
|
444
507
|
const nextOrderedBlocks = container.nextOrderedBlocks
|
|
445
508
|
const orderedIndexByKey = container.orderedIndexByKey
|
|
446
|
-
newBlocks.clear()
|
|
447
|
-
nextOrderedBlocks.length = 0
|
|
448
|
-
orderedIndexByKey.clear()
|
|
449
|
-
const createdBlocks: KeyedBlock<T>[] = []
|
|
450
509
|
const newItems = getItems()
|
|
451
510
|
|
|
452
511
|
if (newItems.length === 0) {
|
|
@@ -473,8 +532,45 @@ function createFineGrainedKeyedList<T>(
|
|
|
473
532
|
}
|
|
474
533
|
|
|
475
534
|
const prevCount = prevOrderedBlocks.length
|
|
535
|
+
if (prevCount > 0 && newItems.length === prevCount && orderedIndexByKey.size === prevCount) {
|
|
536
|
+
let stableOrder = true
|
|
537
|
+
const seen = new Set<string | number>()
|
|
538
|
+
for (let i = 0; i < prevCount; i++) {
|
|
539
|
+
const item = newItems[i]!
|
|
540
|
+
const key = keyFn(item, i)
|
|
541
|
+
if (seen.has(key) || prevOrderedBlocks[i]!.key !== key) {
|
|
542
|
+
stableOrder = false
|
|
543
|
+
break
|
|
544
|
+
}
|
|
545
|
+
seen.add(key)
|
|
546
|
+
}
|
|
547
|
+
if (stableOrder) {
|
|
548
|
+
for (let i = 0; i < prevCount; i++) {
|
|
549
|
+
const item = newItems[i]!
|
|
550
|
+
const block = prevOrderedBlocks[i]!
|
|
551
|
+
if (block.rawItem !== item) {
|
|
552
|
+
block.rawItem = item
|
|
553
|
+
block.item(item)
|
|
554
|
+
}
|
|
555
|
+
if (needsIndex && block.rawIndex !== i) {
|
|
556
|
+
block.rawIndex = i
|
|
557
|
+
block.index(i)
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
newBlocks.clear()
|
|
565
|
+
nextOrderedBlocks.length = 0
|
|
566
|
+
orderedIndexByKey.clear()
|
|
567
|
+
const createdBlocks: KeyedBlock<T>[] = []
|
|
476
568
|
let appendCandidate = prevCount > 0 && newItems.length >= prevCount
|
|
477
569
|
const appendedBlocks: KeyedBlock<T>[] = []
|
|
570
|
+
let mismatchCount = 0
|
|
571
|
+
let mismatchFirst = -1
|
|
572
|
+
let mismatchSecond = -1
|
|
573
|
+
let hasDuplicateKey = false
|
|
478
574
|
|
|
479
575
|
// Phase 1: Build new blocks map (reuse or create)
|
|
480
576
|
newItems.forEach((item, index) => {
|
|
@@ -502,6 +598,12 @@ function createFineGrainedKeyedList<T>(
|
|
|
502
598
|
// If newBlocks already has this key (duplicate key case), clean up the previous block
|
|
503
599
|
const existingBlock = newBlocks.get(key)
|
|
504
600
|
if (existingBlock) {
|
|
601
|
+
if (isDev) {
|
|
602
|
+
console.warn(
|
|
603
|
+
`[fict] Duplicate key "${String(key)}" detected in list rendering. ` +
|
|
604
|
+
`Each item should have a unique key. The previous item with this key will be replaced.`,
|
|
605
|
+
)
|
|
606
|
+
}
|
|
505
607
|
destroyRoot(existingBlock.root)
|
|
506
608
|
removeNodes(existingBlock.nodes)
|
|
507
609
|
}
|
|
@@ -518,6 +620,7 @@ function createFineGrainedKeyedList<T>(
|
|
|
518
620
|
const position = orderedIndexByKey.get(key)
|
|
519
621
|
if (position !== undefined) {
|
|
520
622
|
appendCandidate = false
|
|
623
|
+
hasDuplicateKey = true
|
|
521
624
|
const prior = nextOrderedBlocks[position]
|
|
522
625
|
if (prior && prior !== resolvedBlock) {
|
|
523
626
|
destroyRoot(prior.root)
|
|
@@ -534,8 +637,20 @@ function createFineGrainedKeyedList<T>(
|
|
|
534
637
|
appendCandidate = false
|
|
535
638
|
}
|
|
536
639
|
}
|
|
537
|
-
|
|
640
|
+
const nextIndex = nextOrderedBlocks.length
|
|
641
|
+
orderedIndexByKey.set(key, nextIndex)
|
|
538
642
|
nextOrderedBlocks.push(resolvedBlock)
|
|
643
|
+
if (
|
|
644
|
+
mismatchCount < 3 &&
|
|
645
|
+
(nextIndex >= prevCount || prevOrderedBlocks[nextIndex] !== resolvedBlock)
|
|
646
|
+
) {
|
|
647
|
+
if (mismatchCount === 0) {
|
|
648
|
+
mismatchFirst = nextIndex
|
|
649
|
+
} else if (mismatchCount === 1) {
|
|
650
|
+
mismatchSecond = nextIndex
|
|
651
|
+
}
|
|
652
|
+
mismatchCount++
|
|
653
|
+
}
|
|
539
654
|
}
|
|
540
655
|
|
|
541
656
|
if (appendCandidate && index >= prevCount) {
|
|
@@ -587,8 +702,41 @@ function createFineGrainedKeyedList<T>(
|
|
|
587
702
|
oldBlocks.clear()
|
|
588
703
|
}
|
|
589
704
|
|
|
705
|
+
const canReorderInPlace =
|
|
706
|
+
createdBlocks.length === 0 &&
|
|
707
|
+
oldBlocks.size === 0 &&
|
|
708
|
+
nextOrderedBlocks.length === prevOrderedBlocks.length
|
|
709
|
+
|
|
710
|
+
let skipReconcile = false
|
|
711
|
+
let updateNodeBuffer = true
|
|
712
|
+
|
|
713
|
+
if (canReorderInPlace && nextOrderedBlocks.length > 0 && !hasDuplicateKey) {
|
|
714
|
+
if (mismatchCount === 0) {
|
|
715
|
+
skipReconcile = true
|
|
716
|
+
updateNodeBuffer = false
|
|
717
|
+
} else if (
|
|
718
|
+
mismatchCount === 2 &&
|
|
719
|
+
prevOrderedBlocks[mismatchFirst] === nextOrderedBlocks[mismatchSecond] &&
|
|
720
|
+
prevOrderedBlocks[mismatchSecond] === nextOrderedBlocks[mismatchFirst]
|
|
721
|
+
) {
|
|
722
|
+
if (
|
|
723
|
+
reorderBySwap(
|
|
724
|
+
parent,
|
|
725
|
+
prevOrderedBlocks[mismatchFirst]!,
|
|
726
|
+
prevOrderedBlocks[mismatchSecond]!,
|
|
727
|
+
)
|
|
728
|
+
) {
|
|
729
|
+
skipReconcile = true
|
|
730
|
+
}
|
|
731
|
+
} else if (
|
|
732
|
+
reorderByLIS(parent, container.endMarker, prevOrderedBlocks, nextOrderedBlocks)
|
|
733
|
+
) {
|
|
734
|
+
skipReconcile = true
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
590
738
|
// Phase 3: Reconcile DOM with buffered node arrays
|
|
591
|
-
if (newBlocks.size > 0 || container.currentNodes.length > 0) {
|
|
739
|
+
if (!skipReconcile && (newBlocks.size > 0 || container.currentNodes.length > 0)) {
|
|
592
740
|
const prevNodes = container.currentNodes
|
|
593
741
|
const nextNodes = container.nextNodes
|
|
594
742
|
nextNodes.length = 0
|
|
@@ -608,6 +756,20 @@ function createFineGrainedKeyedList<T>(
|
|
|
608
756
|
// Swap buffers to reuse arrays on next diff
|
|
609
757
|
container.currentNodes = nextNodes
|
|
610
758
|
container.nextNodes = prevNodes
|
|
759
|
+
} else if (skipReconcile && updateNodeBuffer) {
|
|
760
|
+
const prevNodes = container.currentNodes
|
|
761
|
+
const nextNodes = container.nextNodes
|
|
762
|
+
nextNodes.length = 0
|
|
763
|
+
nextNodes.push(container.startMarker)
|
|
764
|
+
for (let i = 0; i < nextOrderedBlocks.length; i++) {
|
|
765
|
+
const nodes = nextOrderedBlocks[i]!.nodes
|
|
766
|
+
for (let j = 0; j < nodes.length; j++) {
|
|
767
|
+
nextNodes.push(nodes[j]!)
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
nextNodes.push(container.endMarker)
|
|
771
|
+
container.currentNodes = nextNodes
|
|
772
|
+
container.nextNodes = prevNodes
|
|
611
773
|
}
|
|
612
774
|
|
|
613
775
|
// 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]
|