Skip to content

Commit cf89f14

Browse files
thePunderWomanAndrewKushnir
authored andcommitted
fix(core): Fix nested timer serialization (#59173)
There were type mismatches and or unintended any types that were preventing nested timers from accessing the delay value during hydration annotation processing. PR Close #59173
1 parent 1f1fff3 commit cf89f14

File tree

4 files changed

+141
-60
lines changed

4 files changed

+141
-60
lines changed

packages/core/src/defer/instructions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -451,7 +451,7 @@ export function ɵɵdeferHydrateOnTimer(delay: number) {
451451
if (!shouldAttachTrigger(TriggerType.Hydrate, lView, tNode)) return;
452452

453453
const hydrateTriggers = getHydrateTriggers(getTView(), tNode);
454-
hydrateTriggers.set(DeferBlockTrigger.Timer, delay);
454+
hydrateTriggers.set(DeferBlockTrigger.Timer, {delay});
455455

456456
if (typeof ngServerMode !== 'undefined' && ngServerMode) {
457457
// We are on the server and SSR for defer blocks is enabled.

packages/core/src/defer/triggering.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ import {
3939
DeferBlockState,
4040
DeferBlockTrigger,
4141
DeferDependenciesLoadingState,
42+
HydrateTriggerDetails,
4243
LDeferBlockDetails,
4344
ON_COMPLETE_FNS,
44-
SSR_BLOCK_STATE,
4545
SSR_UNIQUE_ID,
4646
TDeferBlockDetails,
4747
TDeferDetailsFlags,
@@ -534,7 +534,7 @@ function shouldAttachRegularTrigger(lView: LView, tNode: TNode) {
534534
export function getHydrateTriggers(
535535
tView: TView,
536536
tNode: TNode,
537-
): Map<DeferBlockTrigger, number | null> {
537+
): Map<DeferBlockTrigger, HydrateTriggerDetails | null> {
538538
const tDetails = getTDeferBlockDetails(tView, tNode);
539539
return (tDetails.hydrateTriggers ??= new Map());
540540
}

packages/core/src/hydration/annotate.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -482,13 +482,13 @@ function serializeHydrateTriggers(
482482
DeferBlockTrigger.Viewport,
483483
DeferBlockTrigger.Timer,
484484
]);
485-
let triggers = [];
485+
let triggers: (DeferBlockTrigger | SerializedTriggerDetails)[] = [];
486486
for (let [trigger, details] of triggerMap) {
487487
if (serializableDeferBlockTrigger.has(trigger)) {
488488
if (details === null) {
489489
triggers.push(trigger);
490490
} else {
491-
triggers.push({trigger, details});
491+
triggers.push({trigger, delay: details.delay});
492492
}
493493
}
494494
}

packages/platform-server/test/incremental_hydration_spec.ts

Lines changed: 136 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1155,76 +1155,157 @@ describe('platform-server partial hydration integration', () => {
11551155
});
11561156
});
11571157

1158-
it('timer', async () => {
1159-
@Component({
1160-
selector: 'app',
1161-
template: `
1162-
<main (click)="fnA()">
1163-
@defer (hydrate on timer(500)) {
1164-
<article>
1165-
defer block rendered!
1166-
<span id="test" (click)="fnB()">{{value()}}</span>
1167-
</article>
1168-
} @placeholder {
1169-
<span>Outer block placeholder</span>
1170-
}
1171-
</main>
1172-
`,
1173-
})
1174-
class SimpleComponent {
1175-
value = signal('start');
1176-
fnA() {}
1177-
fnB() {
1178-
this.value.set('end');
1158+
describe('timer', () => {
1159+
it('top level timer', async () => {
1160+
@Component({
1161+
selector: 'app',
1162+
template: `
1163+
<main (click)="fnA()">
1164+
@defer (hydrate on timer(500)) {
1165+
<article>
1166+
defer block rendered!
1167+
<span id="test" (click)="fnB()">{{value()}}</span>
1168+
</article>
1169+
} @placeholder {
1170+
<span>Outer block placeholder</span>
1171+
}
1172+
</main>
1173+
`,
1174+
})
1175+
class SimpleComponent {
1176+
value = signal('start');
1177+
fnA() {}
1178+
fnB() {
1179+
this.value.set('end');
1180+
}
11791181
}
1180-
}
11811182

1182-
const appId = 'custom-app-id';
1183-
const providers = [{provide: APP_ID, useValue: appId}];
1184-
const hydrationFeatures = () => [withIncrementalHydration()];
1183+
const appId = 'custom-app-id';
1184+
const providers = [{provide: APP_ID, useValue: appId}];
1185+
const hydrationFeatures = () => [withIncrementalHydration()];
11851186

1186-
const html = await ssr(SimpleComponent, {envProviders: providers, hydrationFeatures});
1187-
const ssrContents = getAppContents(html);
1187+
const html = await ssr(SimpleComponent, {envProviders: providers, hydrationFeatures});
1188+
const ssrContents = getAppContents(html);
11881189

1189-
// <main> uses "eager" `custom-app-id` namespace.
1190-
expect(ssrContents).toContain('<main jsaction="click:;');
1191-
// <div>s inside a defer block have `d0` as a namespace.
1192-
expect(ssrContents).toContain('<article>');
1193-
// Outer defer block is rendered.
1194-
expect(ssrContents).toContain('defer block rendered');
1190+
// <main> uses "eager" `custom-app-id` namespace.
1191+
expect(ssrContents).toContain('<main jsaction="click:;');
1192+
// <div>s inside a defer block have `d0` as a namespace.
1193+
expect(ssrContents).toContain('<article>');
1194+
// Outer defer block is rendered.
1195+
expect(ssrContents).toContain('defer block rendered');
11951196

1196-
// Internal cleanup before we do server->client transition in this test.
1197-
resetTViewsFor(SimpleComponent);
1197+
// Internal cleanup before we do server->client transition in this test.
1198+
resetTViewsFor(SimpleComponent);
11981199

1199-
////////////////////////////////
1200-
const doc = getDocument();
1201-
const appRef = await prepareEnvironmentAndHydrate(doc, html, SimpleComponent, {
1202-
envProviders: [...providers, {provide: PLATFORM_ID, useValue: 'browser'}],
1203-
hydrationFeatures,
1200+
////////////////////////////////
1201+
const doc = getDocument();
1202+
const appRef = await prepareEnvironmentAndHydrate(doc, html, SimpleComponent, {
1203+
envProviders: [...providers, {provide: PLATFORM_ID, useValue: 'browser'}],
1204+
hydrationFeatures,
1205+
});
1206+
const compRef = getComponentRef<SimpleComponent>(appRef);
1207+
appRef.tick();
1208+
await appRef.whenStable();
1209+
1210+
const appHostNode = compRef.location.nativeElement;
1211+
1212+
expect(appHostNode.outerHTML).toContain('<article>');
1213+
1214+
await timeout(500); // wait for timer
1215+
appRef.tick();
1216+
1217+
await allPendingDynamicImports();
1218+
appRef.tick();
1219+
1220+
expect(appHostNode.outerHTML).toContain('<span id="test">start</span>');
1221+
1222+
const testElement = doc.getElementById('test')!;
1223+
const clickEvent2 = new CustomEvent('click');
1224+
testElement.dispatchEvent(clickEvent2);
1225+
1226+
appRef.tick();
1227+
1228+
expect(appHostNode.outerHTML).toContain('<span id="test">end</span>');
12041229
});
1205-
const compRef = getComponentRef<SimpleComponent>(appRef);
1206-
appRef.tick();
1207-
await appRef.whenStable();
12081230

1209-
const appHostNode = compRef.location.nativeElement;
1231+
it('nested timer', async () => {
1232+
@Component({
1233+
selector: 'app',
1234+
template: `
1235+
<main (click)="fnA()">
1236+
@defer (on viewport; hydrate on interaction) {
1237+
<div id="main" (click)="fnA()">
1238+
defer block rendered!
1239+
@defer (on viewport; hydrate on timer(500)) {
1240+
<article>
1241+
<p id="nested">Nested defer block</p>
1242+
<span id="test" (click)="fnB()">{{value()}}</span>
1243+
</article>
1244+
} @placeholder {
1245+
<span>Inner block placeholder</span>
1246+
}
1247+
</div>
1248+
} @placeholder {
1249+
<span>Outer block placeholder</span>
1250+
}
1251+
</main>
1252+
`,
1253+
})
1254+
class SimpleComponent {
1255+
value = signal('start');
1256+
fnA() {}
1257+
fnB() {
1258+
this.value.set('end');
1259+
}
1260+
}
12101261

1211-
expect(appHostNode.outerHTML).toContain('<article>');
1262+
const appId = 'custom-app-id';
1263+
const providers = [{provide: APP_ID, useValue: appId}];
1264+
const hydrationFeatures = () => [withIncrementalHydration()];
12121265

1213-
await timeout(500); // wait for timer
1214-
appRef.tick();
1266+
const html = await ssr(SimpleComponent, {envProviders: providers, hydrationFeatures});
1267+
const ssrContents = getAppContents(html);
12151268

1216-
await allPendingDynamicImports();
1217-
appRef.tick();
1269+
// <main> uses "eager" `custom-app-id` namespace.
1270+
expect(ssrContents).toContain('<main jsaction="click:;');
1271+
// <div>s inside a defer block have `d0` as a namespace.
1272+
expect(ssrContents).toContain('<article>');
1273+
// Outer defer block is rendered.
1274+
expect(ssrContents).toContain('defer block rendered');
12181275

1219-
expect(appHostNode.outerHTML).toContain('<span id="test">start</span>');
1276+
// Internal cleanup before we do server->client transition in this test.
1277+
resetTViewsFor(SimpleComponent);
12201278

1221-
const testElement = doc.getElementById('test')!;
1222-
const clickEvent2 = new CustomEvent('click');
1223-
testElement.dispatchEvent(clickEvent2);
1279+
////////////////////////////////
1280+
const doc = getDocument();
1281+
const appRef = await prepareEnvironmentAndHydrate(doc, html, SimpleComponent, {
1282+
envProviders: [...providers, {provide: PLATFORM_ID, useValue: 'browser'}],
1283+
hydrationFeatures,
1284+
});
1285+
const compRef = getComponentRef<SimpleComponent>(appRef);
1286+
appRef.tick();
1287+
await appRef.whenStable();
12241288

1225-
appRef.tick();
1289+
const appHostNode = compRef.location.nativeElement;
12261290

1227-
expect(appHostNode.outerHTML).toContain('<span id="test">end</span>');
1291+
expect(appHostNode.outerHTML).toContain('<article>');
1292+
1293+
await timeout(500); // wait for timer
1294+
appRef.tick();
1295+
1296+
await allPendingDynamicImports();
1297+
appRef.tick();
1298+
1299+
expect(appHostNode.outerHTML).toContain('<span id="test">start</span>');
1300+
1301+
const testElement = doc.getElementById('test')!;
1302+
const clickEvent2 = new CustomEvent('click');
1303+
testElement.dispatchEvent(clickEvent2);
1304+
1305+
appRef.tick();
1306+
1307+
expect(appHostNode.outerHTML).toContain('<span id="test">end</span>');
1308+
});
12281309
});
12291310

12301311
it('when', async () => {

0 commit comments

Comments
 (0)