Skip to content

Commit 23e2bb3

Browse files
fix: unset context on stale promises (#16935)
* fix: unset context on stale promises When a stale promise is rejected in `async_derived`, and the promise eventually resolves, `d.resolve` will be noop and `d.promise.then(handler, ...)` will never run. That in turns means any restored context (via `(await save(..))()`) will never be unset. We have to handle this case and unset the context to prevent errors such as false-positive state mutation errors * fix: unset context on stale promises (slightly different approach) (#16936) * slightly different approach to #16935 * move unset_context call * get rid of logs --------- Co-authored-by: Rich Harris <[email protected]>
1 parent b05e12f commit 23e2bb3

File tree

4 files changed

+68
-3
lines changed

4 files changed

+68
-3
lines changed

.changeset/major-beans-fry.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: unset context on stale promises

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,9 +126,11 @@ export function async_derived(fn, location) {
126126
try {
127127
// If this code is changed at some point, make sure to still access the then property
128128
// of fn() to read any signals it might access, so that we track them as dependencies.
129-
Promise.resolve(fn()).then(d.resolve, d.reject);
129+
// We call `unset_context` to undo any `save` calls that happen inside `fn()`
130+
Promise.resolve(fn()).then(d.resolve, d.reject).then(unset_context);
130131
} catch (error) {
131132
d.reject(error);
133+
unset_context();
132134
}
133135

134136
if (DEV) current_async_effect = null;
@@ -185,8 +187,6 @@ export function async_derived(fn, location) {
185187
boundary.update_pending_count(-1);
186188
if (!pending) batch.decrement();
187189
}
188-
189-
unset_context();
190190
};
191191

192192
d.promise.then(handler, (e) => handler(null, e || 'unknown'));
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
async test({ assert, target }) {
5+
// We gotta wait a bit more in this test because of the macrotasks in App.svelte
6+
function macrotask(t = 3) {
7+
return new Promise((r) => setTimeout(r, t));
8+
}
9+
10+
await macrotask();
11+
assert.htmlEqual(target.innerHTML, '<input> 1 | ');
12+
13+
const [input] = target.querySelectorAll('input');
14+
15+
input.value = '1';
16+
input.dispatchEvent(new Event('input', { bubbles: true }));
17+
await macrotask();
18+
assert.htmlEqual(target.innerHTML, '<input> 1 | ');
19+
20+
input.value = '12';
21+
input.dispatchEvent(new Event('input', { bubbles: true }));
22+
await macrotask(6);
23+
// TODO this is wrong (separate bug), this should be 3 | 12
24+
assert.htmlEqual(target.innerHTML, '<input> 5 | 12');
25+
}
26+
});
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<script>
2+
let count = $state(0);
3+
let value = $state('');
4+
let prev;
5+
6+
function asd(v) {
7+
const r = Promise.withResolvers();
8+
9+
if (prev || v === '') {
10+
Promise.resolve().then(async () => {
11+
count++;
12+
r.resolve(v);
13+
await new Promise(r => setTimeout(r, 0));
14+
// TODO with a microtask like below it still throws a mutation error
15+
// await Promise.resolve();
16+
prev?.resolve();
17+
});
18+
} else {
19+
prev = Promise.withResolvers();
20+
prev.promise.then(() => {
21+
count++;
22+
r.resolve(v)
23+
});
24+
}
25+
26+
return r.promise;
27+
}
28+
29+
const x = $derived(await asd(value))
30+
</script>
31+
32+
<input bind:value />
33+
34+
{count} | {x}

0 commit comments

Comments
 (0)