@@ -64,6 +64,10 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re
6464
6565 /** @type {null | import('./types.js').EffectSignal } */
6666 let render = null ;
67+
68+ /** Whether or not there was a "rendered fallback but want to render items" (or vice versa) hydration mismatch */
69+ let mismatch = false ;
70+
6771 block . r =
6872 /** @param {import('./types.js').Transition } transition */
6973 ( transition ) => {
@@ -144,12 +148,30 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re
144148 : maybe_array == null
145149 ? [ ]
146150 : Array . from ( maybe_array ) ;
151+
147152 if ( key_fn !== null ) {
148153 keys = array . map ( key_fn ) ;
149154 } else if ( ( flags & EACH_KEYED ) === 0 ) {
150155 array . map ( no_op ) ;
151156 }
157+
152158 const length = array . length ;
159+
160+ if ( current_hydration_fragment !== null ) {
161+ const is_each_else_comment =
162+ /** @type {Comment } */ ( current_hydration_fragment ?. [ 0 ] ) ?. data === 'ssr:each_else' ;
163+ // Check for hydration mismatch which can happen if the server renders the each fallback
164+ // but the client has items, or vice versa. If so, remove everything inside the anchor and start fresh.
165+ if ( ( is_each_else_comment && length ) || ( ! is_each_else_comment && ! length ) ) {
166+ remove ( /** @type {import('./types.js').TemplateNode[] } */ ( current_hydration_fragment ) ) ;
167+ set_current_hydration_fragment ( null ) ;
168+ mismatch = true ;
169+ } else if ( is_each_else_comment ) {
170+ // Remove the each_else comment node or else it will confuse the subsequent hydration algorithm
171+ /** @type {import('./types.js').TemplateNode[] } */ ( current_hydration_fragment ) . shift ( ) ;
172+ }
173+ }
174+
153175 if ( fallback_fn !== null ) {
154176 if ( length === 0 ) {
155177 if ( block . v . length !== 0 || render === null ) {
@@ -170,6 +192,7 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re
170192 }
171193 }
172194 }
195+
173196 if ( render !== null ) {
174197 execute_effect ( render ) ;
175198 }
@@ -180,6 +203,11 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re
180203
181204 render = render_effect ( clear_each , block , true ) ;
182205
206+ if ( mismatch ) {
207+ // Set a fragment so that Svelte continues to operate in hydration mode
208+ set_current_hydration_fragment ( [ ] ) ;
209+ }
210+
183211 push_destroy_fn ( each , ( ) => {
184212 const flags = block . f ;
185213 const anchor_node = block . a ;
@@ -287,55 +315,70 @@ function reconcile_indexed_array(
287315 }
288316 } else {
289317 var item ;
318+ var is_hydrating = current_hydration_fragment !== null ;
290319 b_blocks = Array ( b ) ;
291- if ( current_hydration_fragment !== null ) {
292- /** @type {Node } */
293- var hydrating_node = current_hydration_fragment [ 0 ] ;
320+ if ( is_hydrating ) {
321+ // Hydrate block
322+ var hydration_list = /** @type {import('./types.js').TemplateNode[] } */ (
323+ current_hydration_fragment
324+ ) ;
325+ var hydrating_node = hydration_list [ 0 ] ;
294326 for ( ; index < length ; index ++ ) {
295- // Hydrate block
296- item = is_proxied_array ? lazy_property ( array , index ) : array [ index ] ;
297327 var fragment = /** @type {Array<Text | Comment | Element> } */ (
298328 get_hydration_fragment ( hydrating_node )
299329 ) ;
300330 set_current_hydration_fragment ( fragment ) ;
301- hydrating_node = /** @type {Node } */ (
331+ if ( ! fragment ) {
332+ // If fragment is null, then that means that the server rendered less items than what
333+ // the client code specifies -> break out and continue with client-side node creation
334+ break ;
335+ }
336+
337+ item = is_proxied_array ? lazy_property ( array , index ) : array [ index ] ;
338+ block = each_item_block ( item , null , index , render_fn , flags ) ;
339+ b_blocks [ index ] = block ;
340+
341+ hydrating_node = /** @type {import('./types.js').TemplateNode } */ (
302342 /** @type {Node } */ ( /** @type {Node } */ ( fragment . at ( - 1 ) ) . nextSibling ) . nextSibling
303343 ) ;
344+ }
345+
346+ remove_excess_hydration_nodes ( hydration_list , hydrating_node ) ;
347+ }
348+
349+ for ( ; index < length ; index ++ ) {
350+ if ( index >= a ) {
351+ // Add block
352+ item = is_proxied_array ? lazy_property ( array , index ) : array [ index ] ;
304353 block = each_item_block ( item , null , index , render_fn , flags ) ;
305354 b_blocks [ index ] = block ;
355+ insert_each_item_block ( block , dom , is_controlled , null ) ;
356+ } else if ( index >= b ) {
357+ // Remove block
358+ block = a_blocks [ index ] ;
359+ destroy_each_item_block ( block , active_transitions , apply_transitions ) ;
360+ } else {
361+ // Update block
362+ item = array [ index ] ;
363+ block = a_blocks [ index ] ;
364+ b_blocks [ index ] = block ;
365+ update_each_item_block ( block , item , index , flags ) ;
306366 }
307- } else {
308- for ( ; index < length ; index ++ ) {
309- if ( index >= a ) {
310- // Add block
311- item = is_proxied_array ? lazy_property ( array , index ) : array [ index ] ;
312- block = each_item_block ( item , null , index , render_fn , flags ) ;
313- b_blocks [ index ] = block ;
314- insert_each_item_block ( block , dom , is_controlled , null ) ;
315- } else if ( index >= b ) {
316- // Remove block
317- block = a_blocks [ index ] ;
318- destroy_each_item_block ( block , active_transitions , apply_transitions ) ;
319- } else {
320- // Update block
321- item = array [ index ] ;
322- block = a_blocks [ index ] ;
323- b_blocks [ index ] = block ;
324- update_each_item_block ( block , item , index , flags ) ;
325- }
326- }
367+ }
368+
369+ if ( is_hydrating && current_hydration_fragment === null ) {
370+ // Server rendered less nodes than the client -> set empty array so that Svelte continues to operate in hydration mode
371+ set_current_hydration_fragment ( [ ] ) ;
327372 }
328373 }
329374
330375 each_block . v = b_blocks ;
331376}
332- // Reconcile arrays by the equality of the elements in the array. This algorithm
333- // is based on Ivi's reconcilation logic:
334- //
335- // https://github.com/localvoid/ivi/blob/9f1bd0918f487da5b131941228604763c5d8ef56/packages/ivi/src/client/core.ts#L968
336- //
337377
338378/**
379+ * Reconcile arrays by the equality of the elements in the array. This algorithm
380+ * is based on Ivi's reconcilation logic:
381+ * https://github.com/localvoid/ivi/blob/9f1bd0918f487da5b131941228604763c5d8ef56/packages/ivi/src/client/core.ts#L968
339382 * @template V
340383 * @param {Array<V> } array
341384 * @param {import('./types.js').EachBlock } each_block
@@ -391,30 +434,43 @@ function reconcile_tracked_array(
391434 var key ;
392435 var item ;
393436 var idx ;
437+ var is_hydrating = current_hydration_fragment !== null ;
394438 b_blocks = Array ( b ) ;
395- if ( current_hydration_fragment !== null ) {
439+ if ( is_hydrating ) {
440+ // Hydrate block
396441 var fragment ;
397-
398- /** @type {Node } */
399- var hydrating_node = current_hydration_fragment [ 0 ] ;
442+ var hydration_list = /** @type {import('./types.js').TemplateNode[] } */ (
443+ current_hydration_fragment
444+ ) ;
445+ var hydrating_node = hydration_list [ 0 ] ;
400446 while ( b > 0 ) {
401- // Hydrate block
402- idx = b_end - -- b ;
403- item = array [ idx ] ;
404- key = is_computed_key ? keys [ idx ] : item ;
405447 fragment = /** @type {Array<Text | Comment | Element> } */ (
406448 get_hydration_fragment ( hydrating_node )
407449 ) ;
408450 set_current_hydration_fragment ( fragment ) ;
451+ if ( ! fragment ) {
452+ // If fragment is null, then that means that the server rendered less items than what
453+ // the client code specifies -> break out and continue with client-side node creation
454+ break ;
455+ }
456+
457+ idx = b_end - -- b ;
458+ item = array [ idx ] ;
459+ key = is_computed_key ? keys [ idx ] : item ;
460+ block = each_item_block ( item , key , idx , render_fn , flags ) ;
461+ b_blocks [ idx ] = block ;
462+
409463 // Get the <!--ssr:..--> tag of the next item in the list
410464 // The fragment array can be empty if each block has no content
411- hydrating_node = /** @type {Node } */ (
465+ hydrating_node = /** @type {import('./types.js').TemplateNode } */ (
412466 /** @type {Node } */ ( ( fragment . at ( - 1 ) || hydrating_node ) . nextSibling ) . nextSibling
413467 ) ;
414- block = each_item_block ( item , key , idx , render_fn , flags ) ;
415- b_blocks [ idx ] = block ;
416468 }
417- } else if ( a === 0 ) {
469+
470+ remove_excess_hydration_nodes ( hydration_list , hydrating_node ) ;
471+ }
472+
473+ if ( a === 0 ) {
418474 // Create new blocks
419475 while ( b > 0 ) {
420476 idx = b_end - -- b ;
@@ -546,11 +602,30 @@ function reconcile_tracked_array(
546602 }
547603 }
548604 }
605+
606+ if ( is_hydrating && current_hydration_fragment === null ) {
607+ // Server rendered less nodes than the client -> set empty array so that Svelte continues to operate in hydration mode
608+ set_current_hydration_fragment ( [ ] ) ;
609+ }
549610 }
550611
551612 each_block . v = b_blocks ;
552613}
553614
615+ /**
616+ * The server could have rendered more list items than the client specifies.
617+ * In that case, we need to remove the remaining server-rendered nodes.
618+ * @param {import('./types.js').TemplateNode[] } hydration_list
619+ * @param {import('./types.js').TemplateNode | null } next_node
620+ */
621+ function remove_excess_hydration_nodes ( hydration_list , next_node ) {
622+ if ( next_node === null ) return ;
623+ var idx = hydration_list . indexOf ( next_node ) ;
624+ if ( idx !== - 1 && hydration_list . length > idx + 1 ) {
625+ remove ( hydration_list . slice ( idx ) ) ;
626+ }
627+ }
628+
554629/**
555630 * Longest Increased Subsequence algorithm
556631 * @param {Int32Array } a
0 commit comments