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
pull/18435/head
Simon H 3 weeks ago committed by GitHub
parent a6985bcd24
commit 36ae0622a8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: always unset reactivity context after restoring it

@ -25,6 +25,7 @@ import {
set_reactivity_loss_tracker
} from './deriveds.js';
import { aborted } from './effects.js';
import { queue_micro_task } from '../dom/task.js';
/**
* @param {Blocker[]} blockers
@ -169,6 +170,7 @@ export async function save(promise) {
return () => {
restore();
queue_micro_task(unset_context);
return value;
};
}

@ -0,0 +1,44 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client'],
compileOptions: {
dev: true
},
async test({ assert, target, errors }) {
await tick();
const [increment, update, resolve] = target.querySelectorAll('button');
increment.click();
await tick();
resolve.click();
await tick();
update.click();
await tick();
resolve.click();
await tick();
assert.deepEqual(
errors.filter((error) => error.includes('state_unsafe_mutation')),
[]
);
assert.htmlEqual(
target.innerHTML,
`
<button>increment</button>
<button>update</button>
<button>resolve</button>
<p>count: 1</p>
<p>submits: 1</p>
<p>pending: 0</p>
<p>2</p>
`
);
}
});

@ -0,0 +1,28 @@
<script>
const queued = [];
function push(value) {
if(!value) return value;
return new Promise((resolve) => queued.push(() => resolve(value)));
}
let count = $state(0);
let submits = $state(0);
const a = $derived(push(count));
const b = $derived(push(count));
async function updateAfterPromise() {
await Promise.resolve();
submits += 1;
}
</script>
<button onclick={() => (count += 1)}>increment</button>
<button onclick={updateAfterPromise}>update</button>
<button onclick={() => queued.shift()?.()}>resolve</button>
<p>count: {count}</p>
<p>submits: {submits}</p>
<p>pending: {$effect.pending()}</p>
<p>{await a + await b}</p>
Loading…
Cancel
Save