pull/18674/merge
Kamil Jakubus 20 hours ago committed by GitHub
commit dd1cf7677e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: prevent stale reads from new dependencies during pending async work

@ -176,6 +176,14 @@ export class Batch {
*/ */
#new_effects = []; #new_effects = [];
/**
* Values that were first read by a reaction while this batch was time travelling
* over an earlier batch that changed them. These reads connect the batches even
* though this batch did not write to the values itself.
* @type {Set<Value>}
*/
#new_dependencies = new Set();
/** /**
* Deferred effects (which run after async work has completed) that are DIRTY * Deferred effects (which run after async work has completed) that are DIRTY
* @type {Set<Effect>} * @type {Set<Effect>}
@ -540,6 +548,12 @@ export class Batch {
while (batch !== null) { while (batch !== null) {
if (!batch.is_fork) { if (!batch.is_fork) {
for (const value of this.#new_dependencies) {
if (batch.current.has(value)) {
return batch;
}
}
// if the batches are connected, break // if the batches are connected, break
for (const [value, [, is_derived]] of this.current) { for (const [value, [, is_derived]] of this.current) {
if (batch.current.has(value) && !is_derived) { if (batch.current.has(value) && !is_derived) {
@ -558,6 +572,10 @@ export class Batch {
* @param {Batch} batch * @param {Batch} batch
*/ */
#merge(batch) { #merge(batch) {
for (const value of batch.#new_dependencies) {
this.#new_dependencies.add(value);
}
for (const [source, value] of batch.current) { for (const [source, value] of batch.current) {
if (!this.previous.has(source) && batch.previous.has(source)) { if (!this.previous.has(source) && batch.previous.has(source)) {
this.previous.set(source, batch.previous.get(source)); this.previous.set(source, batch.previous.get(source));
@ -656,6 +674,28 @@ export class Batch {
} }
} }
/**
* If a reaction discovers a dependency that was changed by an earlier pending
* batch, connect the two batches and expose the current value while traversing.
* The current batch will be merged into the earlier one before it can commit.
* @param {Value} value
*/
capture_dependency(value) {
if (this.is_fork || this.current.has(value)) return;
var batch = this.#prev;
while (batch !== null) {
if (!batch.is_fork && batch.current.has(value)) {
this.#new_dependencies.add(value);
batch_values?.set(value, /** @type {[any, boolean]} */ (batch.current.get(value))[0]);
return;
}
batch = batch.#prev;
}
}
activate() { activate() {
current_batch = this; current_batch = this;
} }

@ -548,6 +548,14 @@ export function settled() {
export function get(signal) { export function get(signal) {
var flags = signal.f; var flags = signal.f;
var is_derived = (flags & DERIVED) !== 0; var is_derived = (flags & DERIVED) !== 0;
var is_new_dependency =
batch_values !== null &&
!untracking &&
((active_reaction !== null &&
(active_reaction.deps === null || !includes.call(active_reaction.deps, signal))) ||
(active_reaction === null &&
active_effect !== null &&
(active_effect.f & REACTION_RAN) === 0));
captured_signals?.add(signal); captured_signals?.add(signal);
@ -707,6 +715,10 @@ export function get(signal) {
} }
if (batch_values?.has(signal)) { if (batch_values?.has(signal)) {
if (is_new_dependency) {
(current_batch ?? previous_batch)?.capture_dependency(signal);
}
return batch_values.get(signal); return batch_values.get(signal);
} }

@ -0,0 +1,29 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client'],
async test({ assert, target }) {
await tick();
const [update, show, resolve] = target.querySelectorAll('button');
update.click();
await tick();
show.click();
await tick();
resolve.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>update</button>
<button>show</button>
<button>resolve</button>
<p>1</p>
`
);
}
});

@ -0,0 +1,17 @@
<script>
let value = $state();
let show = $state(false);
const deferred = Promise.withResolvers();
function wait(value) {
return value === undefined ? '' : deferred.promise;
}
</script>
<button onclick={() => (value = { x: 1 })}>update</button>
<button onclick={() => (show = true)}>show</button>
<button onclick={() => deferred.resolve('')}>resolve</button>
{await wait(value)}
<p>{show ? value.x : ''}</p>

@ -16,20 +16,12 @@ export default test({
<button>x</button> <button>x</button>
<button>y++</button> <button>y++</button>
<button>resolve</button> <button>resolve</button>
world `
` // if this does not show world - that would also be ok
); );
resolve.click(); resolve.click();
await tick(); await tick();
assert.deepEqual(logs, [ assert.deepEqual(logs, ['universe', 'universe', '$effect: universe', '$effect: universe']);
'universe',
'world',
'$effect: world',
'$effect: universe',
'$effect: universe'
]);
// assert.deepEqual(logs, ['universe', 'universe', '$effect: universe', '$effect: universe']); // this would also be ok
assert.htmlEqual( assert.htmlEqual(
target.innerHTML, target.innerHTML,
` `

@ -17,13 +17,7 @@ export default test({
<button>y++</button> <button>y++</button>
<button>resolve</button> <button>resolve</button>
<hr> <hr>
world `
"world"
world
world
world
"world"
` // if this does not show world "world" world world world "world" - then this would also be ok
); );
resolve.click(); resolve.click();

@ -29,13 +29,7 @@ export default test({
<button>y++</button> <button>y++</button>
<button>resolve</button> <button>resolve</button>
<hr> <hr>
world `
"world"
world
world
world
"world"
` // if this does not show world "world" world world world "world" - then this would also be ok
); );
resolve.click(); resolve.click();

@ -11,26 +11,19 @@ export default test({
y.click(); y.click();
await tick(); await tick();
assert.deepEqual(logs, ['universe', 'world', '$effect: world']); assert.deepEqual(logs, ['universe', 'universe']);
assert.htmlEqual( assert.htmlEqual(
target.innerHTML, target.innerHTML,
` `
<button>x</button> <button>x</button>
<button>y++</button> <button>y++</button>
<button>resolve</button> <button>resolve</button>
world
` `
); );
resolve.click(); resolve.click();
await tick(); await tick();
assert.deepEqual(logs, [ assert.deepEqual(logs, ['universe', 'universe', '$effect: universe', '$effect: universe']);
'universe',
'world',
'$effect: world',
'$effect: universe',
'$effect: universe'
]);
assert.htmlEqual( assert.htmlEqual(
target.innerHTML, target.innerHTML,
` `

Loading…
Cancel
Save