Skip to content

Commit

Permalink
fix(core): Fix nested timer serialization
Browse files Browse the repository at this point in the history
There were type mismatches and or unintended any types that were preventing nested timers from accessing the delay value during hydration annotation processing.
  • Loading branch information
thePunderWoman committed Dec 12, 2024
1 parent f3729ce commit ab48689
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 59 deletions.
2 changes: 1 addition & 1 deletion packages/core/src/defer/instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ export function ɵɵdeferHydrateOnTimer(delay: number) {
if (!shouldAttachTrigger(TriggerType.Hydrate, lView, tNode)) return;

const hydrateTriggers = getHydrateTriggers(getTView(), tNode);
hydrateTriggers.set(DeferBlockTrigger.Timer, delay);
hydrateTriggers.set(DeferBlockTrigger.Timer, {delay});

if (typeof ngServerMode !== 'undefined' && ngServerMode) {
// We are on the server and SSR for defer blocks is enabled.
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/defer/triggering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ import {
DeferBlockState,
DeferBlockTrigger,
DeferDependenciesLoadingState,
HydrateTimerTriggerDetails,
HydrateTriggerDetails,
LDeferBlockDetails,
ON_COMPLETE_FNS,
SSR_BLOCK_STATE,
Expand Down Expand Up @@ -534,7 +536,7 @@ function shouldAttachRegularTrigger(lView: LView, tNode: TNode) {
export function getHydrateTriggers(
tView: TView,
tNode: TNode,
): Map<DeferBlockTrigger, number | null> {
): Map<DeferBlockTrigger, HydrateTriggerDetails | null> {
const tDetails = getTDeferBlockDetails(tView, tNode);
return (tDetails.hydrateTriggers ??= new Map());
}
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/hydration/annotate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -484,13 +484,13 @@ function serializeHydrateTriggers(
DeferBlockTrigger.Viewport,
DeferBlockTrigger.Timer,
]);
let triggers = [];
let triggers: (DeferBlockTrigger | SerializedTriggerDetails)[] = [];
for (let [trigger, details] of triggerMap) {
if (serializableDeferBlockTrigger.has(trigger)) {
if (details === null) {
triggers.push(trigger);
} else {
triggers.push({trigger, details});
triggers.push({trigger, delay: details.delay});
}
}
}
Expand Down
191 changes: 136 additions & 55 deletions packages/platform-server/test/incremental_hydration_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1155,76 +1155,157 @@ describe('platform-server partial hydration integration', () => {
});
});

it('timer', async () => {
@Component({
selector: 'app',
template: `
<main (click)="fnA()">
@defer (hydrate on timer(500)) {
<article>
defer block rendered!
<span id="test" (click)="fnB()">{{value()}}</span>
</article>
} @placeholder {
<span>Outer block placeholder</span>
}
</main>
`,
})
class SimpleComponent {
value = signal('start');
fnA() {}
fnB() {
this.value.set('end');
describe('timer', () => {
it('top level timer', async () => {
@Component({
selector: 'app',
template: `
<main (click)="fnA()">
@defer (hydrate on timer(500)) {
<article>
defer block rendered!
<span id="test" (click)="fnB()">{{value()}}</span>
</article>
} @placeholder {
<span>Outer block placeholder</span>
}
</main>
`,
})
class SimpleComponent {
value = signal('start');
fnA() {}
fnB() {
this.value.set('end');
}
}
}

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

const html = await ssr(SimpleComponent, {envProviders: providers, hydrationFeatures});
const ssrContents = getAppContents(html);
const html = await ssr(SimpleComponent, {envProviders: providers, hydrationFeatures});
const ssrContents = getAppContents(html);

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

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

////////////////////////////////
const doc = getDocument();
const appRef = await prepareEnvironmentAndHydrate(doc, html, SimpleComponent, {
envProviders: [...providers, {provide: PLATFORM_ID, useValue: 'browser'}],
hydrationFeatures,
////////////////////////////////
const doc = getDocument();
const appRef = await prepareEnvironmentAndHydrate(doc, html, SimpleComponent, {
envProviders: [...providers, {provide: PLATFORM_ID, useValue: 'browser'}],
hydrationFeatures,
});
const compRef = getComponentRef<SimpleComponent>(appRef);
appRef.tick();
await appRef.whenStable();

const appHostNode = compRef.location.nativeElement;

expect(appHostNode.outerHTML).toContain('<article>');

await timeout(500); // wait for timer
appRef.tick();

await allPendingDynamicImports();
appRef.tick();

expect(appHostNode.outerHTML).toContain('<span id="test">start</span>');

const testElement = doc.getElementById('test')!;
const clickEvent2 = new CustomEvent('click');
testElement.dispatchEvent(clickEvent2);

appRef.tick();

expect(appHostNode.outerHTML).toContain('<span id="test">end</span>');
});
const compRef = getComponentRef<SimpleComponent>(appRef);
appRef.tick();
await appRef.whenStable();

const appHostNode = compRef.location.nativeElement;
it('nested timer', async () => {
@Component({
selector: 'app',
template: `
<main (click)="fnA()">
@defer (on viewport; hydrate on interaction) {
<div id="main" (click)="fnA()">
defer block rendered!
@defer (on viewport; hydrate on timer(500)) {
<article>
<p id="nested">Nested defer block</p>
<span id="test" (click)="fnB()">{{value()}}</span>
</article>
} @placeholder {
<span>Inner block placeholder</span>
}
</div>
} @placeholder {
<span>Outer block placeholder</span>
}
</main>
`,
})
class SimpleComponent {
value = signal('start');
fnA() {}
fnB() {
this.value.set('end');
}
}

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

await timeout(500); // wait for timer
appRef.tick();
const html = await ssr(SimpleComponent, {envProviders: providers, hydrationFeatures});
const ssrContents = getAppContents(html);

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

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

const testElement = doc.getElementById('test')!;
const clickEvent2 = new CustomEvent('click');
testElement.dispatchEvent(clickEvent2);
////////////////////////////////
const doc = getDocument();
const appRef = await prepareEnvironmentAndHydrate(doc, html, SimpleComponent, {
envProviders: [...providers, {provide: PLATFORM_ID, useValue: 'browser'}],
hydrationFeatures,
});
const compRef = getComponentRef<SimpleComponent>(appRef);
appRef.tick();
await appRef.whenStable();

appRef.tick();
const appHostNode = compRef.location.nativeElement;

expect(appHostNode.outerHTML).toContain('<span id="test">end</span>');
expect(appHostNode.outerHTML).toContain('<article>');

await timeout(500); // wait for timer
appRef.tick();

await allPendingDynamicImports();
appRef.tick();

expect(appHostNode.outerHTML).toContain('<span id="test">start</span>');

const testElement = doc.getElementById('test')!;
const clickEvent2 = new CustomEvent('click');
testElement.dispatchEvent(clickEvent2);

appRef.tick();

expect(appHostNode.outerHTML).toContain('<span id="test">end</span>');
});
});

it('when', async () => {
Expand Down

0 comments on commit ab48689

Please sign in to comment.