Pending async effects breaking synchronous state/effects
#17050
Development PRs
This one's hell, and has revelead three different bugs:
Bug 1
commit in an each could happen long after the original context of the block effect is gone. That means get_boundary() in run might not get the correct active effect, maybe it's even null at that moment. To fix it I think we need to capture then restore the context. Loads of tests failed with that approach, so I added another capture-restore within the commit function itself but there's still one test failing - still trying to find out why.
Bug 2
Each blocks get out of order because of race conditions and block effects not rescheduling when they should. Reproducible by clicking fast three times on the button in the reproduction of #17033. This is what happens:
- run batch 1
- run batch 2
- run batch
- batch 1 completes, wants to rebase batch 2/3.
- Does update and run batch 2 first.
That reruns each block because each blocks rely on the newer values (because of proxy each entry is a separate source).
Since each's
arraymethod is not time traveling it's always the value of whatever batch ran last. Rerun also causes reinit of Circle.svelte because each logic destroys the old one - Does NOT run batch 3 because no overlap according to our "depends on distinct values" logic (since each block just reran and now has more dependencies on it so false negative).
- Does update and run batch 2 first.
That reruns each block because each blocks rely on the newer values (because of proxy each entry is a separate source).
Since each's
- batch 3 Circle.run completes, decrement 0, runs commit
- Does NOT rebase batch 2 because of same reasons as 4.
- batch 2 Circle.run completes, decrement 0, runs commit -> wrong end result
The reason for this is that block effects can change dependencies after a rerun and so a subsequent batch rebase could have a false positive or negative effect reschedule. To fix it we need to split up the rebase logic into two stages: First collect all effects of all batches that should rerun, then run them in a second loop.
Bug 3
each state is not properly time-travel-ready. Something goes wrong for keyed each blocks in #17033 still. The problem is that fine grained proxy means some array entries may appear undefined for subsequent runs when they shouldn't, but no further insights yet and no idea how to fix yet.
Before submitting the PR, please make sure you do the following
- It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
- Prefix your PR title with
feat:,fix:,chore:, ordocs:. - This message body should clearly illustrate what problems it solves.
- Ideally, include a test that fails without this PR but passes with it.
- If this PR changes code within
packages/svelte/src, add a changeset (npx changeset).
Tests and linting
- Run the tests with
pnpm testand lint the project withpnpm lint
As noted in #17126, each blocks are a little buggy when there's async stuff involved. #16977 fixed a bunch of stuff around async blocks/branches, but excluded each blocks because, well, they're complicated. But the time has come to deal with it.
As the commit messages suggest this is very WIP — a bunch of tests are failing, and while it fixes part of the problem (item effects and the fallback effect both need to be created immediately inside the block effect, not later upon commit), it doesn't yet fix the problems relating to overlapping async batches. But I thought I'd open the PR in this unfinished state anyway. Pleasingly, so far it's a net reduction in code and complexity.
Before submitting the PR, please make sure you do the following
- It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
- Prefix your PR title with
feat:,fix:,chore:, ordocs:. - This message body should clearly illustrate what problems it solves.
- Ideally, include a test that fails without this PR but passes with it.
- If this PR changes code within
packages/svelte/src, add a changeset (npx changeset).
Tests and linting
- Run the tests with
pnpm testand lint the project withpnpm lint
This fixes an awkward bug with each blocks containing await, especially keyed each blocks.
If you do array.push(...) multiple times in distinct batches, something weird happens — to the each block, the array looks this...
[1]
...then this...
[undefined, 2]
...then this...
[undefined, undefined, 3]
...and so on. That's because as far as Svelte's reactivity is concerned, what we're really doing is assigning to array[0] then array[1] then array[2]. Those (along with array.length) are each backed by independent sources, which we can rewind individually when we need to 'apply' a batch. When it comes to sources that actually are independent, this is useful, since we apply the changes from batch B to the DOM while we're still waiting for a promise in batch A to resolve. But in this case it's not ideal, because you would expect these changes to accumulate.
In particular, this fails with keyed each blocks because duplicate keys are disallowed (assuming the key function doesn't break on undefined before the duplicate check happens).
This PR fixes it by stacking batches that are interconnected. Specifically, if a later batch has some (but not all) sources in common with an earlier batch, then when we apply the batch we include the sources from the earlier batch, and block it until the earlier batch commits. When the earlier batch commits, it will check to see if doing so unblocks any later batches, and if so process them.
In the course of working on this I realised that SvelteSet and SvelteMap aren't async-ready — will follow up this PR with ones for those.
Fixes #17050
Issue
Describe the bug
When I have pending async work happening (e.g. mounting an async component) I am getting strange and erroneous results while modifying state elsewhere.
See the reproduction for examples.
Reproduction
Reproduction: https://svelte.dev/playground/c0f672d625bb47f28808234647d8a5f3?version=5.42.2
This example contains a list of incrementing numbers which can be added to by clicking the button. Each list item will also cause an async component to be rendered.
If you rapidly click the "Add to list" button you will be appending to the list while async work in happening from mounting new the components.
When rapidly clicking you will observe
- The original list will temporarily have gaps in it
- The derived list showing the type of each element will have
undefinedwhere the gaps are
- The printed results should eventually reconcile
- Printing the lists as a text expression
{list}gives different results than printing inside of an#{each}block until the results reconcile
The primary issue here is (1) (and by extension (2)) where we temporarily have incorrect records. In the real world this leads to unexpected errors such as trying to access properties of undefined.
Logs
System Info
System:
OS: Linux 6.15 Arch Linux
CPU: (12) x64 AMD Ryzen 5 2600 Six-Core Processor
Memory: 4.59 GB / 15.54 GB
Container: Yes
Shell: 5.3.3 - /bin/bash
Binaries:
Node: 22.17.0 - /home/yung/.nvm/versions/node/v22.17.0/bin/node
Yarn: 1.22.22 - /usr/bin/yarn
npm: 10.9.2 - /home/yung/.nvm/versions/node/v22.17.0/bin/npm
pnpm: 10.13.1 - /home/yung/.local/share/pnpm/pnpm
Browsers:
Chromium: 139.0.7258.66
Firefox: 141.0
Firefox Developer Edition: 141.0
npmPackages:
svelte: ^5.41.4 => 5.41.4
Severity
blocking an upgrade
Info
Possibly related #17033
Pro tip: You can prefix GitHub URLs of issues, PRs or discussions with svcl.dev/ to view them on this page! Also try it on a GitHub release ;)