Merge branch 'main' into each-block-pending

async-svelte-map
Rich Harris 6 months ago
commit dac3364087

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: rebase batches after process, not during

@ -1,5 +1,11 @@
# svelte # svelte
## 5.53.10
### Patch Changes
- fix: re-process batch if new root effects were scheduled ([#17895](https://github.com/sveltejs/svelte/pull/17895))
## 5.53.9 ## 5.53.9
### Patch Changes ### Patch Changes

@ -2,7 +2,7 @@
"name": "svelte", "name": "svelte",
"description": "Cybernetically enhanced web apps", "description": "Cybernetically enhanced web apps",
"license": "MIT", "license": "MIT",
"version": "5.53.9", "version": "5.53.10",
"type": "module", "type": "module",
"types": "./types/index.d.ts", "types": "./types/index.d.ts",
"engines": { "engines": {

@ -249,6 +249,10 @@ export class Batch {
reset_branch(e, t); reset_branch(e, t);
} }
} else { } else {
if (this.#pending === 0) {
batches.delete(this);
}
// clear effects. Those that are still needed will be rescheduled through unskipping the skipped branches. // clear effects. Those that are still needed will be rescheduled through unskipping the skipped branches.
this.#dirty_effects.clear(); this.#dirty_effects.clear();
this.#maybe_dirty_effects.clear(); this.#maybe_dirty_effects.clear();
@ -262,15 +266,19 @@ export class Batch {
flush_queued_effects(effects); flush_queued_effects(effects);
previous_batch = null; previous_batch = null;
if (this.#pending === 0) {
this.#commit();
}
this.#deferred?.resolve(); this.#deferred?.resolve();
} }
var next_batch = /** @type {Batch | null} */ (/** @type {unknown} */ (current_batch)); var next_batch = /** @type {Batch | null} */ (/** @type {unknown} */ (current_batch));
// Edge case: During traversal new branches might create effects that run immediately and set state,
// causing an effect and therefore a root to be scheduled again. We need to traverse the current batch
// once more in that case - most of the time this will just clean up dirty branches.
if (this.#roots.length > 0) {
const batch = (next_batch ??= this);
batch.#roots.push(...this.#roots.filter((r) => !batch.#roots.includes(r)));
}
if (next_batch !== null) { if (next_batch !== null) {
batches.add(next_batch); batches.add(next_batch);
@ -282,6 +290,10 @@ export class Batch {
next_batch.#process(); next_batch.#process();
} }
if (!batches.has(this)) {
this.#commit();
}
} }
/** /**
@ -425,74 +437,59 @@ export class Batch {
// in other words, we re-run block/async effects with the newly // in other words, we re-run block/async effects with the newly
// committed state, unless the batch in question has a more // committed state, unless the batch in question has a more
// recent value for a given source // recent value for a given source
if (batches.size > 1) { for (const batch of batches) {
this.previous.clear(); var is_earlier = batch.id < this.id;
var previous_batch = current_batch; /** @type {Source[]} */
var previous_batch_values = batch_values; var sources = [];
var is_earlier = true;
for (const [source, value] of this.current) {
for (const batch of batches) { if (batch.current.has(source)) {
if (batch === this) { if (is_earlier && value !== batch.current.get(source)) {
is_earlier = false; // bring the value up to date
continue; batch.current.set(source, value);
} else {
// same value or later batch has more recent value,
// no need to re-run these effects
continue;
}
} }
/** @type {Source[]} */ sources.push(source);
const sources = []; }
for (const [source, value] of this.current) {
if (batch.current.has(source)) {
if (is_earlier && value !== batch.current.get(source)) {
// bring the value up to date
batch.current.set(source, value);
} else {
// same value or later batch has more recent value,
// no need to re-run these effects
continue;
}
}
sources.push(source); if (sources.length === 0) {
} continue;
}
if (sources.length === 0) { // Re-run async/block effects that depend on distinct values changed in both batches
continue; var others = [...batch.current.keys()].filter((s) => !this.current.has(s));
} if (others.length > 0) {
batch.activate();
// Re-run async/block effects that depend on distinct values changed in both batches /** @type {Set<Value>} */
const others = [...batch.current.keys()].filter((s) => !this.current.has(s)); var marked = new Set();
if (others.length > 0) {
batch.activate();
/** @type {Set<Value>} */
const marked = new Set();
/** @type {Map<Reaction, boolean>} */
const checked = new Map();
for (const source of sources) {
mark_effects(source, others, marked, checked);
}
if (batch.#roots.length > 0) { /** @type {Map<Reaction, boolean>} */
batch.apply(); var checked = new Map();
for (const root of batch.#roots) { for (var source of sources) {
batch.#traverse(root, [], []); mark_effects(source, others, marked, checked);
} }
if (batch.#roots.length > 0) {
batch.apply();
// TODO do we need to do anything with the dummy effect arrays? for (var root of batch.#roots) {
batch.#traverse(root, [], []);
} }
batch.deactivate(); // TODO do we need to do anything with the dummy effect arrays?
} }
}
current_batch = previous_batch; batch.deactivate();
batch_values = previous_batch_values; }
} }
this.#skipped_branches.clear();
batches.delete(this);
} }
/** /**
@ -559,7 +556,10 @@ export class Batch {
} }
apply() { apply() {
if (!async_mode_flag || (!this.is_fork && batches.size === 1)) return; if (!async_mode_flag || (!this.is_fork && batches.size === 1)) {
batch_values = null;
return;
}
// if there are multiple batches, we are 'time travelling' — // if there are multiple batches, we are 'time travelling' —
// we need to override values with the ones in this batch... // we need to override values with the ones in this batch...

@ -4,5 +4,5 @@
* The current version, as set in package.json. * The current version, as set in package.json.
* @type {string} * @type {string}
*/ */
export const VERSION = '5.53.9'; export const VERSION = '5.53.10';
export const PUBLIC_VERSION = '5'; export const PUBLIC_VERSION = '5';

@ -0,0 +1,46 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, logs }) {
const [open, close, increment] = target.querySelectorAll('button');
open.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>Open</button>
<button>Close</button>
<button>0</button>
<div>open (width: 42)</div>
`
);
increment.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>Open</button>
<button>Close</button>
<button>1</button>
<div>open (width: 42)</div>
`
);
close.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>Open</button>
<button>Close</button>
<button>1</button>
<div>closed</div>
`
);
assert.deepEqual(logs, ['effect ran']);
}
});

@ -0,0 +1,37 @@
<script module>
let active = $state(false);
let panelWidth = $state(null);
const store = {
get active() { return active; },
open() { active = true; },
close() { active = false; },
// This getter lazily writes $state on first read
get panelWidth() {
if (panelWidth === null) panelWidth = 42;
$effect(() => {
console.log('effect ran');
});
return panelWidth;
}
};
</script>
<script>
let counter = $state(0);
</script>
<button onclick={() => store.open()}>Open</button>
<button onclick={() => store.close()}>Close</button>
<button onclick={() => counter++}>{counter}</button>
<div>
{#if store.active}
open (width: {store.panelWidth})
{:else}
closed
{/if}
</div>

@ -0,0 +1,44 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
const [open, close, increment] = target.querySelectorAll('button');
open.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>Open</button>
<button>Close</button>
<button>0</button>
<div>open (width: 42)</div>
`
);
increment.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>Open</button>
<button>Close</button>
<button>1</button>
<div>open (width: 42)</div>
`
);
close.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>Open</button>
<button>Close</button>
<button>1</button>
<div>closed</div>
`
);
}
});

@ -0,0 +1,31 @@
<script module>
let active = $state(false);
let panelWidth = $state(null);
const store = {
get active() { return active; },
open() { active = true; },
close() { active = false; },
// This getter lazily writes $state on first read
get panelWidth() {
if (panelWidth === null) panelWidth = 42;
return panelWidth;
}
};
</script>
<script>
let counter = $state(0);
</script>
<button onclick={() => store.open()}>Open</button>
<button onclick={() => store.close()}>Close</button>
<button onclick={() => counter++}>{counter}</button>
<div>
{#if store.active}
open (width: {store.panelWidth})
{:else}
closed
{/if}
</div>
Loading…
Cancel
Save