fix: keep dependencies of `$state.eager`/`pending` (alternative approach) (#18218)

#18108, with two differences:

- we use a global map
- we use the parent reaction as the key, rather than traversing upwards
for a branch

I think this has the same outcome?

### Before submitting the PR, please make sure you do the following

- [x] 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
- [x] Prefix your PR title with `feat:`, `fix:`, `chore:`, or `docs:`.
- [x] This message body should clearly illustrate what problems it
solves.
- [x] Ideally, include a test that fails without this PR but passes with
it.
- [x] If this PR changes code within `packages/svelte/src`, add a
changeset (`npx changeset`).

### Tests and linting

- [x] Run the tests with `pnpm test` and lint the project with `pnpm
lint`

---------

Co-authored-by: Simon Holthausen <simon.holthausen@vercel.com>
Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
pull/18158/head
Rich Harris 3 months ago committed by GitHub
parent ef4b97dfab
commit a10e8e47a5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: keep dependencies of `$state.eager/pending`

@ -16,7 +16,8 @@ import {
EAGER_EFFECT,
ERROR_VALUE,
MANAGED_EFFECT,
REACTION_RAN
REACTION_RAN,
DESTROYING
} from '#client/constants';
import { async_mode_flag } from '../../flags/index.js';
import { deferred, define_property, includes } from '../../shared/utils.js';
@ -33,7 +34,7 @@ import { flush_tasks, queue_micro_task } from '../dom/task.js';
import { DEV } from 'esm-env';
import { invoke_error_boundary } from '../error-handling.js';
import { flush_eager_effects, old_values, set_eager_effects, source, update } from './sources.js';
import { eager_effect, unlink_effect } from './effects.js';
import { eager_effect, teardown, unlink_effect } from './effects.js';
import { defer_effect } from './utils.js';
import { UNINITIALIZED } from '../../../constants.js';
import { set_signal_status } from './status.js';
@ -1234,6 +1235,9 @@ function eager_flush() {
});
}
/** @type {Map<Reaction, Source<number>>} */
var version_map = new Map();
/**
* Implementation of `$state.eager(fn())`
* @template T
@ -1241,10 +1245,22 @@ function eager_flush() {
* @returns {T}
*/
export function eager(fn) {
var version = source(0);
var initial = true;
var value = /** @type {T} */ (undefined);
if (active_reaction === null) {
return fn();
}
let parent = active_reaction;
let version = version_map.get(parent) ?? source(0);
version_map.set(parent, version);
teardown(() => {
if (parent.f & DESTROYING) version_map.delete(parent);
});
get(version);
eager_effect(() => {

@ -0,0 +1,23 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [increment, resolve] = target.querySelectorAll('button');
increment.click();
await tick();
assert.htmlEqual(
target.innerHTML,
'<button>increment</button> <button>resolve</button> 0 <p>loading...</p>'
);
resolve.click();
await tick();
assert.htmlEqual(
target.innerHTML,
'<button>increment</button> <button>resolve</button> 1 <p>1</p>'
);
}
});

@ -0,0 +1,20 @@
<script>
let count = $state(0);
const queued = [];
async function delay(v) {
if (!v) return v;
return new Promise(r => queued.push(() => r(v)));
}
</script>
<button onclick={() => count++}>increment</button>
<button onclick={() => queued.shift()?.()}>resolve</button>
{await delay(count)}
{#if $state.eager(count) !== count}
<p>loading...</p>
{:else}
<p>{count}</p>
{/if}

@ -0,0 +1,32 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, logs }) {
await tick();
const [increment, resolve] = target.querySelectorAll('button');
logs.length = 0;
increment.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<button>increment</button> <button>resolve</button>
<ul><li>0 / 0</li><li>0 / loading...</li><li>0 / 0</li></ul>`
);
resolve.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<button>increment</button> <button>resolve</button>
<ul><li>0 / 0</li><li>1 / 1</li><li>0 / 0</li></ul>`
);
assert.equal(
logs.some((l) => l.toString().includes('0 ') || l.toString().includes('2')),
false,
'only the second $state.eager should have been evaluated'
);
}
});

@ -0,0 +1,24 @@
<script>
let counts = $state([0, 0, 0]);
const queued = [];
async function delay(v) {
if (!v) return v;
return new Promise(r => queued.push(() => r(v)));
}
</script>
<button onclick={() => counts[1]++}>increment</button>
<button onclick={() => queued.shift()?.()}>resolve</button>
<ul>
{#each counts as count, i}
<li>
{await delay(count)} /
{#if console.log(i) || $state.eager(count) !== count}
loading...
{:else}
{count}
{/if}
</li>
{/each}
</ul>
Loading…
Cancel
Save