Skip to content

Commit 36ae062

Browse files
authored
fix: always unset reactivity context after restoring it (#18453)
When calling `save`, we restore the context after the promise resolves. But we do not unset it after the subsequent synchronous execution. That means that until `unset_context` runs in `async_derived` we will not have the correct (nulled) context. That causes problems if the `save` isn't the last promise contributing to the `async_derived`, because it means the context is not properly unset until the promise _after_ the `save` within the `async_derived` settles. This can cause all sorts of mixups, including a wrong mutation error. fixes #18441
1 parent a6985bc commit 36ae062

4 files changed

Lines changed: 79 additions & 0 deletions

File tree

.changeset/late-geese-fix.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: always unset reactivity context after restoring it

packages/svelte/src/internal/client/reactivity/async.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
set_reactivity_loss_tracker
2626
} from './deriveds.js';
2727
import { aborted } from './effects.js';
28+
import { queue_micro_task } from '../dom/task.js';
2829

2930
/**
3031
* @param {Blocker[]} blockers
@@ -169,6 +170,7 @@ export async function save(promise) {
169170

170171
return () => {
171172
restore();
173+
queue_micro_task(unset_context);
172174
return value;
173175
};
174176
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { tick } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
mode: ['client'],
6+
7+
compileOptions: {
8+
dev: true
9+
},
10+
11+
async test({ assert, target, errors }) {
12+
await tick();
13+
const [increment, update, resolve] = target.querySelectorAll('button');
14+
15+
increment.click();
16+
await tick();
17+
18+
resolve.click();
19+
await tick();
20+
21+
update.click();
22+
await tick();
23+
24+
resolve.click();
25+
await tick();
26+
27+
assert.deepEqual(
28+
errors.filter((error) => error.includes('state_unsafe_mutation')),
29+
[]
30+
);
31+
assert.htmlEqual(
32+
target.innerHTML,
33+
`
34+
<button>increment</button>
35+
<button>update</button>
36+
<button>resolve</button>
37+
<p>count: 1</p>
38+
<p>submits: 1</p>
39+
<p>pending: 0</p>
40+
<p>2</p>
41+
`
42+
);
43+
}
44+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<script>
2+
const queued = [];
3+
4+
function push(value) {
5+
if(!value) return value;
6+
return new Promise((resolve) => queued.push(() => resolve(value)));
7+
}
8+
9+
let count = $state(0);
10+
let submits = $state(0);
11+
12+
const a = $derived(push(count));
13+
const b = $derived(push(count));
14+
15+
async function updateAfterPromise() {
16+
await Promise.resolve();
17+
submits += 1;
18+
}
19+
</script>
20+
21+
<button onclick={() => (count += 1)}>increment</button>
22+
<button onclick={updateAfterPromise}>update</button>
23+
<button onclick={() => queued.shift()?.()}>resolve</button>
24+
25+
<p>count: {count}</p>
26+
<p>submits: {submits}</p>
27+
<p>pending: {$effect.pending()}</p>
28+
<p>{await a + await b}</p>

0 commit comments

Comments
 (0)