Skip to content

Commit 5d3db10

Browse files
committed
fix: ensure async deriveds always get dependencies from thennable
When an async derived already has a previous promise that is still pending, we were not accessing the `then` property of the new promise. If that property access causes signals to be read, that meant that those dependencies were lost and as such the derived wouldn't rerun anymore when it should. The fix is to make sure to always access the thennable.
1 parent 0d48916 commit 5d3db10

File tree

4 files changed

+84
-0
lines changed

4 files changed

+84
-0
lines changed

.changeset/orange-chefs-float.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: ensure async deriveds always get dependencies from thennable

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,9 @@ export function async_derived(fn, location) {
120120

121121
try {
122122
var p = fn();
123+
// Make sure to always access the then property to read any signals
124+
// it might access, so that we track them as dependencies.
125+
if (prev) Promise.resolve(p).catch(() => {}); // avoid unhandled rejection
123126
} catch (error) {
124127
p = Promise.reject(error);
125128
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { tick } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
async test({ assert, target }) {
6+
const [increment, pop] = target.querySelectorAll('button');
7+
8+
increment.click();
9+
await tick();
10+
11+
pop.click();
12+
await tick();
13+
14+
pop.click();
15+
await tick();
16+
17+
assert.htmlEqual(
18+
target.innerHTML,
19+
`
20+
<button>increment</button>
21+
<button>pop</button>
22+
<p>1</p>
23+
`
24+
);
25+
26+
increment.click();
27+
await tick();
28+
29+
pop.click();
30+
await tick();
31+
32+
assert.htmlEqual(
33+
target.innerHTML,
34+
`
35+
<button>increment</button>
36+
<button>pop</button>
37+
<p>2</p>
38+
`
39+
);
40+
}
41+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
2+
<script>
3+
let count = $state(0);
4+
5+
let deferreds = [];
6+
7+
class X {
8+
constructor(promise) {
9+
this.promise = promise;
10+
}
11+
12+
get then() {
13+
count;
14+
15+
return (resolve) => this.promise.then(() => count).then(resolve)
16+
}
17+
}
18+
19+
function push() {
20+
const deferred = Promise.withResolvers();
21+
deferreds.push(deferred);
22+
return new X(deferred.promise);
23+
}
24+
</script>
25+
26+
<button onclick={() => count += 1}>increment</button>
27+
<button onclick={() => deferreds.pop()?.resolve(count)}>pop</button>
28+
29+
<svelte:boundary>
30+
<p>{await push()}</p>
31+
32+
{#snippet pending()}
33+
<p>loading...</p>
34+
{/snippet}
35+
</svelte:boundary>

0 commit comments

Comments
 (0)