Skip to content

Commit e51da7a

Browse files
committed
fix: hydration per-island error resilience
1 parent 7125344 commit e51da7a

1 file changed

Lines changed: 30 additions & 18 deletions

File tree

‎packages/interactivity/src/hydration.ts‎

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import 'requestidlecallback';
77
* Internal dependencies
88
*/
99
import { toVdom, hydratedIslands } from './vdom';
10-
import { createRootFragment, splitTask } from './utils';
10+
import { createRootFragment, splitTask, warn } from './utils';
1111

1212
// Keep the same root fragment for each interactive region node.
1313
const regionRootFragments = new WeakMap();
@@ -60,12 +60,19 @@ const hydrateNode = async ( node: Element ) => {
6060
if ( hydratedIslands.has( node ) ) {
6161
return;
6262
}
63-
const fragment = getRegionRootFragment( node );
64-
const vdom = toVdom( node );
65-
initialVdom.set( node, vdom );
66-
await splitTask();
67-
hydrate( vdom, fragment );
68-
await splitTask();
63+
try {
64+
const fragment = getRegionRootFragment( node );
65+
const vdom = toVdom( node );
66+
initialVdom.set( node, vdom );
67+
await splitTask();
68+
hydrate( vdom, fragment );
69+
} catch ( e ) {
70+
warn(
71+
`Failed to hydrate island: ${( e as Error ).message ?? e}`
72+
);
73+
} finally {
74+
await splitTask();
75+
}
6976
};
7077

7178
/**
@@ -88,7 +95,7 @@ export const hydrateAllRemaining = async () => {
8895
intersectionObserver?.disconnect();
8996
// Hydrate every island currently in the DOM that isn't hydrated yet.
9097
for ( const node of document.querySelectorAll( '[data-wp-interactive]' ) ) {
91-
await hydrateNode( node );
98+
await hydrateNode( node as Element );
9299
}
93100
resolveInitialVdom( initialVdom );
94101
};
@@ -121,20 +128,25 @@ export const hydrateRegions = async () => {
121128
continue;
122129
}
123130

124-
const node = entry.target;
131+
const node = entry.target as Element;
125132
intersectionObserverInstance.unobserve( node );
126133
observedNodes.delete( node );
127-
134+
// `hydrateNode` handles its own errors, so a single
135+
// failing island cannot abort the rest of the batch.
128136
await hydrateNode( node );
137+
}
129138

130-
if ( observedNodes.size === 0 ) {
131-
intersectionObserverInstance.disconnect();
132-
// All islands have been hydrated: resolve the promise with
133-
// the fully populated initialVdom so the router can start
134-
// doing the DOM diffing between the previous and next pages.
135-
idleFired = true; // Disarm the idle-time sweep.
136-
resolveInitialVdom( initialVdom );
137-
}
139+
// All observed islands have been hydrated: resolve the promise
140+
// with the fully populated initialVdom so the router can start
141+
// doing the DOM diffing between the previous and next pages.
142+
// Resolving AFTER the batch's hydration attempts (rather than
143+
// before the last node's hydrate) guarantees the router never
144+
// reads a partially hydrated island list.
145+
if ( observedNodes.size === 0 ) {
146+
intersectionObserverInstance.disconnect();
147+
// Disarm the idle-time sweep.
148+
idleFired = true;
149+
resolveInitialVdom( initialVdom );
138150
}
139151
},
140152
{

0 commit comments

Comments
 (0)