fix: use right batch/branch on first run

Fixes the regression that was uncovered by a SvelteKit test. It consistens of two parts:

1. Restoring the latest, not the initial batch in `flatten`: At the beginning `flatten` stores the current batch, and once everything async is finished it restores it. That falls down if one of the async deriveds reruns during that time. Now the batch associated with that rerun should be used because its `current` map contains a later value. We gotta use that. The hacky fix for now is to set the latest batch onto the source that the async derived has. That could mess with perf since the source object shape is not the same all the time anymore. There's probably a better way to write this but it's getting late here. This fix solves the regression part where it shows the wrong string for the url pathname.
2. branches deletes older batches and then bails when it comes across itself. The logic assumes that batches are in the map in ascending order but that's not always true. Making the logic robust to that fixes the part where it keeps showing the "should never see this" string from the obsolete `+page.svelte`. I wasn't able to make a test for this yet.
batch-branches-fix
Simon Holthausen 6 days ago
parent 572444a696
commit 3f7bdda5ce

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: use right batch/branch on first run

@ -109,11 +109,14 @@ export class BranchManager {
}
for (const [b, k] of this.#batches) {
// Keep values for newer batches. Insertion order is not always chronological:
// an older batch can re-run after a newer one has already registered.
if (b.id > batch.id) continue;
this.#batches.delete(b);
if (b === batch) {
// keep values for newer batches
break;
continue;
}
const offscreen = this.#offscreen.get(k);

@ -55,7 +55,14 @@ export function flatten(blockers, sync, async, fn) {
/** @param {Value[]} values */
function finish(values) {
restore();
var batch = get_latest_async_batch(values);
if (batch) {
restore(false);
batch.activate();
batch.apply();
} else {
restore();
}
try {
fn(values);
@ -95,6 +102,24 @@ export function flatten(blockers, sync, async, fn) {
}
}
/**
* @param {Value[]} values
* @returns {Batch | null}
*/
function get_latest_async_batch(values) {
/** @type {Batch | null} */
var latest = null;
for (const value of values) {
var batch = /** @type {Value & { async_batch?: Batch }} */ (value).async_batch;
if (batch && (!latest || batch.id > latest.id)) {
latest = batch;
}
}
return latest;
}
/**
* @param {Blocker[]} blockers
* @param {(values: Value[]) => any} fn

@ -215,6 +215,7 @@ export function async_derived(fn, label, location) {
}
batch.activate();
/** @type {Source<V> & { async_batch?: Batch }} */ (signal).async_batch = batch;
if (error) {
signal.f |= ERROR_VALUE;

@ -7,6 +7,7 @@ import type {
TransitionManager
} from '#client';
import type { Boundary } from '../dom/blocks/boundary';
import type { Batch } from './batch';
export interface Signal {
/** Flags bitmask */
@ -24,6 +25,8 @@ export interface Value<V = unknown> extends Signal {
rv: number;
/** The latest value for this signal */
v: V;
/** The batch in which an async derived most recently resolved */
async_batch?: Batch; // TODO if this is only set a few times this might mess with perf (object shape etc)
// dev-only
/** A label (e.g. the `foo` in `let foo = $state(...)`) used for `$inspect.trace()` */

@ -0,0 +1,20 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
const [increment, pop] = target.querySelectorAll('button');
increment.click();
await tick();
increment.click();
await tick();
pop.click();
await tick();
assert.htmlEqual(target.innerHTML, `<button>increment</button> <button>pop</button> 2 2 1`);
pop.click();
await tick();
assert.htmlEqual(target.innerHTML, `<button>increment</button> <button>pop</button> 2 2 1`);
}
});

@ -0,0 +1,26 @@
<script>
let count = $state(0);
let other = $state(0);
const queue = [];
function push(v) {
return new Promise((r,e) => queue.push(() => v === 1 ? e(v) : r(v)));
}
</script>
<button onclick={() => {
if (count === 0) {
other++;
count++;
} else {
count++
}
}}>increment</button>
<button onclick={() => queue.pop()?.()}>pop</button>
{#if count > 0}
<svelte:boundary>
{await push(count)} {count} {other}
{#snippet failed()}boom{/snippet}
</svelte:boundary>
{/if}
Loading…
Cancel
Save