Skip to content

Commit 73f3319

Browse files
janechuCopilot
andcommitted
fix: remove repeat overflow/underflow checks and scanStop boundary
The overflow check (scan for extra fe:/r markers after hydration) and the scanStop boundary (bindingViewBoundaries-based scan limit) both caused false positives with nested repeats. Data-free markers make it impossible to distinguish fe:/r from this repeat vs a nested repeat at the same DOM depth. The underflow check also misfired when nested repeat items were counted instead of this repeat's items. Remove all three mechanisms. The balanced depth counting in the backward scan correctly handles nesting without them. This matches the original base branch behavior which also did not have overflow/underflow checks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent e0d46b9 commit 73f3319

3 files changed

Lines changed: 10 additions & 87 deletions

File tree

packages/fast-element/SIZES.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Bundle sizes for `@microsoft/fast-element` exports.
44

55
| Export | Minified | Gzip | Brotli |
66
|--------|----------|------|--------|
7-
| CDN Rollup Bundle | 66.24 KB | 19.46 KB | 17.46 KB |
7+
| CDN Rollup Bundle | 64.96 KB | 19.28 KB | 17.28 KB |
88
| FASTElement | 23.92 KB | 7.48 KB | 6.76 KB |
99
| Updates | 3.68 KB | 1.51 KB | 1.28 KB |
1010
| Observable | 8.20 KB | 3.01 KB | 2.69 KB |
@@ -17,4 +17,4 @@ Bundle sizes for `@microsoft/fast-element` exports.
1717
| volatile | 8.29 KB | 3.04 KB | 2.71 KB |
1818
| when | 2.40 KB | 979 B | 787 B |
1919
| html | 27.37 KB | 8.95 KB | 8.03 KB |
20-
| repeat | 32.22 KB | 10.06 KB | 9.05 KB |
20+
| repeat | 30.96 KB | 9.86 KB | 8.87 KB |

packages/fast-element/src/templating/repeat.ts

Lines changed: 6 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -407,27 +407,12 @@ export class RepeatBehavior<TSource = any> implements ViewBehavior, Subscriber {
407407
}
408408

409409
const itemCount = this.items.length;
410-
let serializer: XMLSerializer | null = null;
411-
const getSerializer = (): XMLSerializer =>
412-
serializer ?? (serializer = new XMLSerializer());
413410
this.views = Array.from({ length: itemCount }, () => null as any);
414411

415-
// Determine the scan boundary for this repeat directive.
416-
// Use the content binding boundaries (from the parent's fe:b/fe:/b
417-
// markers) to avoid scanning into sibling repeat blocks.
418-
// Set scanStop to the node BEFORE the boundary's first node so the
419-
// boundary node itself is still eligible for matching as a start marker.
420-
const boundaries =
421-
isHydratable(this.controller) &&
422-
this.controller.bindingViewBoundaries[this.directive.targetNodeId];
423-
const scanStop: Node | null = boundaries
424-
? (boundaries.first.previousSibling ?? null)
425-
: null;
426-
427412
let current = this.location.previousSibling;
428413
let itemIndex = itemCount - 1; // items render in order; walk backward
429414

430-
while (current !== null && current !== scanStop && itemIndex >= 0) {
415+
while (current !== null && itemIndex >= 0) {
431416
if (!isCommentNode(current) || current.data !== "fe:/r") {
432417
current = current.previousSibling;
433418
continue;
@@ -437,26 +422,17 @@ export class RepeatBehavior<TSource = any> implements ViewBehavior, Subscriber {
437422
current.data = "";
438423
const end = current.previousSibling;
439424
if (!end) {
440-
throw new HydrationRepeatError(
425+
throw new Error(
441426
`Error when hydrating inside "${
442427
(this.location.getRootNode() as ShadowRoot).host.nodeName
443428
}": end should never be null.`,
444-
{
445-
index: itemIndex,
446-
hydrationStage: "hydrateViews",
447-
itemsLength: itemCount,
448-
viewsState: this.views.map(v => (v ? "hydrated" : "empty")),
449-
rootNodeContent: getSerializer().serializeToString(
450-
this.location.getRootNode() as any,
451-
),
452-
},
453429
);
454430
}
455431

456432
// Find matching start marker via balanced counting
457433
let start: Node | null = end;
458434
let depth = 0;
459-
while (start !== null && start !== scanStop) {
435+
while (start !== null) {
460436
if (isCommentNode(start)) {
461437
if (start.data === "fe:/r") {
462438
depth++;
@@ -485,67 +461,14 @@ export class RepeatBehavior<TSource = any> implements ViewBehavior, Subscriber {
485461
}
486462
start = start.previousSibling;
487463
}
488-
if (!start || start === scanStop) {
489-
throw new HydrationRepeatError(
464+
if (!start) {
465+
throw new Error(
490466
`Error when hydrating inside "${
491467
(this.location.getRootNode() as ShadowRoot).host.nodeName
492-
}": start should never be null.`,
493-
{
494-
index: itemIndex,
495-
hydrationStage: "hydrateViews",
496-
itemsLength: itemCount,
497-
viewsState: this.views.map(v => (v ? "hydrated" : "empty")),
498-
rootNodeContent: getSerializer().serializeToString(
499-
this.location.getRootNode() as any,
500-
),
501-
},
468+
}": repeat start marker not found.`,
502469
);
503470
}
504471
}
505-
506-
if (itemIndex >= 0) {
507-
throw new HydrationRepeatError(
508-
`Error when hydrating inside "${
509-
(this.location.getRootNode() as ShadowRoot).host.nodeName
510-
}": expected ${itemCount} repeat items but only found ${itemCount - itemIndex - 1}.`,
511-
{
512-
index: itemIndex,
513-
hydrationStage: "hydrateViews",
514-
itemsLength: itemCount,
515-
viewsState: this.views.map(v => (v ? "hydrated" : "empty")),
516-
rootNodeContent: getSerializer().serializeToString(
517-
this.location.getRootNode() as any,
518-
),
519-
},
520-
);
521-
}
522-
523-
// Overflow check: detect extra repeat markers beyond items.length,
524-
// bounded to this directive's scan range.
525-
// Skip when itemCount is 0 — an empty repeat has no markers to overflow,
526-
// and the scan would cross into unrelated sibling content.
527-
if (itemCount > 0 && current !== null && current !== scanStop) {
528-
let remaining: Node | null = current;
529-
while (remaining !== null && remaining !== scanStop) {
530-
if (isCommentNode(remaining) && remaining.data === "fe:/r") {
531-
throw new HydrationRepeatError(
532-
`Error when hydrating inside "${
533-
(this.location.getRootNode() as ShadowRoot).host.nodeName
534-
}": found more repeat items in the DOM than expected ${itemCount}.`,
535-
{
536-
index: -1,
537-
hydrationStage: "hydrateViews",
538-
itemsLength: itemCount,
539-
viewsState: this.views.map(v => (v ? "hydrated" : "empty")),
540-
rootNodeContent: getSerializer().serializeToString(
541-
this.location.getRootNode() as any,
542-
),
543-
},
544-
);
545-
}
546-
remaining = remaining.previousSibling;
547-
}
548-
}
549472
}
550473
}
551474

sites/website/src/docs/3.x/resources/export-sizes.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Bundle sizes for `@microsoft/fast-element` exports.
1919

2020
| Export | Minified | Gzip | Brotli |
2121
|--------|----------|------|--------|
22-
| CDN Rollup Bundle | 66.24 KB | 19.46 KB | 17.46 KB |
22+
| CDN Rollup Bundle | 64.96 KB | 19.28 KB | 17.28 KB |
2323
| FASTElement | 23.92 KB | 7.48 KB | 6.76 KB |
2424
| Updates | 3.68 KB | 1.51 KB | 1.28 KB |
2525
| Observable | 8.20 KB | 3.01 KB | 2.69 KB |
@@ -32,4 +32,4 @@ Bundle sizes for `@microsoft/fast-element` exports.
3232
| volatile | 8.29 KB | 3.04 KB | 2.71 KB |
3333
| when | 2.40 KB | 979 B | 787 B |
3434
| html | 27.37 KB | 8.95 KB | 8.03 KB |
35-
| repeat | 32.22 KB | 10.06 KB | 9.05 KB |
35+
| repeat | 30.96 KB | 9.86 KB | 8.87 KB |

0 commit comments

Comments
 (0)