@@ -64,6 +64,10 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re
64
64
65
65
/** @type {null | import('./types.js').EffectSignal } */
66
66
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
+
67
71
block . r =
68
72
/** @param {import('./types.js').Transition } transition */
69
73
( transition ) => {
@@ -144,12 +148,30 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re
144
148
: maybe_array == null
145
149
? [ ]
146
150
: Array . from ( maybe_array ) ;
151
+
147
152
if ( key_fn !== null ) {
148
153
keys = array . map ( key_fn ) ;
149
154
} else if ( ( flags & EACH_KEYED ) === 0 ) {
150
155
array . map ( no_op ) ;
151
156
}
157
+
152
158
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
+
153
175
if ( fallback_fn !== null ) {
154
176
if ( length === 0 ) {
155
177
if ( block . v . length !== 0 || render === null ) {
@@ -170,6 +192,7 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re
170
192
}
171
193
}
172
194
}
195
+
173
196
if ( render !== null ) {
174
197
execute_effect ( render ) ;
175
198
}
@@ -180,6 +203,11 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re
180
203
181
204
render = render_effect ( clear_each , block , true ) ;
182
205
206
+ if ( mismatch ) {
207
+ // Set a fragment so that Svelte continues to operate in hydration mode
208
+ set_current_hydration_fragment ( [ ] ) ;
209
+ }
210
+
183
211
push_destroy_fn ( each , ( ) => {
184
212
const flags = block . f ;
185
213
const anchor_node = block . a ;
@@ -287,55 +315,70 @@ function reconcile_indexed_array(
287
315
}
288
316
} else {
289
317
var item ;
318
+ var is_hydrating = current_hydration_fragment !== null ;
290
319
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 ] ;
294
326
for ( ; index < length ; index ++ ) {
295
- // Hydrate block
296
- item = is_proxied_array ? lazy_property ( array , index ) : array [ index ] ;
297
327
var fragment = /** @type {Array<Text | Comment | Element> } */ (
298
328
get_hydration_fragment ( hydrating_node )
299
329
) ;
300
330
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 } */ (
302
342
/** @type {Node } */ ( /** @type {Node } */ ( fragment . at ( - 1 ) ) . nextSibling ) . nextSibling
303
343
) ;
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 ] ;
304
353
block = each_item_block ( item , null , index , render_fn , flags ) ;
305
354
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 ) ;
306
366
}
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 ( [ ] ) ;
327
372
}
328
373
}
329
374
330
375
each_block . v = b_blocks ;
331
376
}
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
- //
337
377
338
378
/**
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
339
382
* @template V
340
383
* @param {Array<V> } array
341
384
* @param {import('./types.js').EachBlock } each_block
@@ -391,30 +434,43 @@ function reconcile_tracked_array(
391
434
var key ;
392
435
var item ;
393
436
var idx ;
437
+ var is_hydrating = current_hydration_fragment !== null ;
394
438
b_blocks = Array ( b ) ;
395
- if ( current_hydration_fragment !== null ) {
439
+ if ( is_hydrating ) {
440
+ // Hydrate block
396
441
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 ] ;
400
446
while ( b > 0 ) {
401
- // Hydrate block
402
- idx = b_end - -- b ;
403
- item = array [ idx ] ;
404
- key = is_computed_key ? keys [ idx ] : item ;
405
447
fragment = /** @type {Array<Text | Comment | Element> } */ (
406
448
get_hydration_fragment ( hydrating_node )
407
449
) ;
408
450
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
+
409
463
// Get the <!--ssr:..--> tag of the next item in the list
410
464
// 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 } */ (
412
466
/** @type {Node } */ ( ( fragment . at ( - 1 ) || hydrating_node ) . nextSibling ) . nextSibling
413
467
) ;
414
- block = each_item_block ( item , key , idx , render_fn , flags ) ;
415
- b_blocks [ idx ] = block ;
416
468
}
417
- } else if ( a === 0 ) {
469
+
470
+ remove_excess_hydration_nodes ( hydration_list , hydrating_node ) ;
471
+ }
472
+
473
+ if ( a === 0 ) {
418
474
// Create new blocks
419
475
while ( b > 0 ) {
420
476
idx = b_end - -- b ;
@@ -546,11 +602,30 @@ function reconcile_tracked_array(
546
602
}
547
603
}
548
604
}
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
+ }
549
610
}
550
611
551
612
each_block . v = b_blocks ;
552
613
}
553
614
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
+
554
629
/**
555
630
* Longest Increased Subsequence algorithm
556
631
* @param {Int32Array } a
0 commit comments