Skip to content

Commit dae4c5f

Browse files
authored
fix: ensure signal write invalidation within effects is consistent (#14989)
* fix: ensure signal write invalidation within effects is persistent * fix: ensure signal write invalidation within effects is persistent * fix: ensure signal write invalidation within effects is persistent * address feedback
1 parent 99fdc3f commit dae4c5f

File tree

4 files changed

+93
-25
lines changed

4 files changed

+93
-25
lines changed

.changeset/tricky-spiders-collect.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: ensure signal write invalidation within effects is consistent

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

+8-14
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { DEV } from 'esm-env';
33
import {
44
component_context,
55
active_reaction,
6-
new_deps,
76
active_effect,
87
untracked_writes,
98
get,
@@ -29,7 +28,8 @@ import {
2928
INSPECT_EFFECT,
3029
UNOWNED,
3130
MAYBE_DIRTY,
32-
BLOCK_EFFECT
31+
BLOCK_EFFECT,
32+
ROOT_EFFECT
3333
} from '../constants.js';
3434
import * as e from '../errors.js';
3535
import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js';
@@ -182,26 +182,20 @@ export function internal_set(source, value) {
182182

183183
mark_reactions(source, DIRTY);
184184

185-
// If the current signal is running for the first time, it won't have any
186-
// reactions as we only allocate and assign the reactions after the signal
187-
// has fully executed. So in the case of ensuring it registers the reaction
185+
// It's possible that the current reaction might not have up-to-date dependencies
186+
// whilst it's actively running. So in the case of ensuring it registers the reaction
188187
// properly for itself, we need to ensure the current effect actually gets
189188
// scheduled. i.e: `$effect(() => x++)`
190189
if (
191190
is_runes() &&
192191
active_effect !== null &&
193192
(active_effect.f & CLEAN) !== 0 &&
194-
(active_effect.f & BRANCH_EFFECT) === 0
193+
(active_effect.f & (BRANCH_EFFECT | ROOT_EFFECT)) === 0
195194
) {
196-
if (new_deps !== null && new_deps.includes(source)) {
197-
set_signal_status(active_effect, DIRTY);
198-
schedule_effect(active_effect);
195+
if (untracked_writes === null) {
196+
set_untracked_writes([source]);
199197
} else {
200-
if (untracked_writes === null) {
201-
set_untracked_writes([source]);
202-
} else {
203-
untracked_writes.push(source);
204-
}
198+
untracked_writes.push(source);
205199
}
206200
}
207201

packages/svelte/src/internal/client/runtime.js

+44-11
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,34 @@ export function handle_error(error, effect, previous_effect, component_context)
382382
}
383383
}
384384

385+
/**
386+
* @param {Value} signal
387+
* @param {Effect} effect
388+
* @param {number} [depth]
389+
*/
390+
function schedule_possible_effect_self_invalidation(signal, effect, depth = 0) {
391+
var reactions = signal.reactions;
392+
if (reactions === null) return;
393+
394+
for (var i = 0; i < reactions.length; i++) {
395+
var reaction = reactions[i];
396+
if ((reaction.f & DERIVED) !== 0) {
397+
schedule_possible_effect_self_invalidation(
398+
/** @type {Derived} */ (reaction),
399+
effect,
400+
depth + 1
401+
);
402+
} else if (effect === reaction) {
403+
if (depth === 0) {
404+
set_signal_status(reaction, DIRTY);
405+
} else if ((reaction.f & CLEAN) !== 0) {
406+
set_signal_status(reaction, MAYBE_DIRTY);
407+
}
408+
schedule_effect(/** @type {Effect} */ (reaction));
409+
}
410+
}
411+
}
412+
385413
/**
386414
* @template V
387415
* @param {Reaction} reaction
@@ -434,6 +462,22 @@ export function update_reaction(reaction) {
434462
deps.length = skipped_deps;
435463
}
436464

465+
// If we're inside an effect and we have untracked writes, then we need to
466+
// ensure that if any of those untracked writes result in re-invalidation
467+
// of the current effect, then that happens accordingly
468+
if (
469+
is_runes() &&
470+
untracked_writes !== null &&
471+
(reaction.f & (DERIVED | MAYBE_DIRTY | DIRTY)) === 0
472+
) {
473+
for (i = 0; i < /** @type {Source[]} */ (untracked_writes).length; i++) {
474+
schedule_possible_effect_self_invalidation(
475+
untracked_writes[i],
476+
/** @type {Effect} */ (reaction)
477+
);
478+
}
479+
}
480+
437481
// If we are returning to an previous reaction then
438482
// we need to increment the read version to ensure that
439483
// any dependencies in this reaction aren't marked with
@@ -907,17 +951,6 @@ export function get(signal) {
907951
} else {
908952
new_deps.push(signal);
909953
}
910-
911-
if (
912-
untracked_writes !== null &&
913-
active_effect !== null &&
914-
(active_effect.f & CLEAN) !== 0 &&
915-
(active_effect.f & BRANCH_EFFECT) === 0 &&
916-
untracked_writes.includes(signal)
917-
) {
918-
set_signal_status(active_effect, DIRTY);
919-
schedule_effect(active_effect);
920-
}
921954
}
922955
} else if (is_derived && /** @type {Derived} */ (signal).deps === null) {
923956
var derived = /** @type {Derived} */ (signal);

packages/svelte/tests/signals/test.ts

+36
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,42 @@ describe('signals', () => {
402402
};
403403
});
404404

405+
test('schedules rerun when writing to signal before reading it from derived', (runes) => {
406+
if (!runes) return () => {};
407+
let log: any[] = [];
408+
409+
const value = state(1);
410+
const double = derived(() => $.get(value) * 2);
411+
412+
user_effect(() => {
413+
set(value, 10);
414+
log.push($.get(double));
415+
});
416+
417+
return () => {
418+
flushSync();
419+
assert.deepEqual(log, [20]);
420+
};
421+
});
422+
423+
test('schedules rerun when writing to signal after reading it from derived', (runes) => {
424+
if (!runes) return () => {};
425+
let log: any[] = [];
426+
427+
const value = state(1);
428+
const double = derived(() => $.get(value) * 2);
429+
430+
user_effect(() => {
431+
log.push($.get(double));
432+
set(value, 10);
433+
});
434+
435+
return () => {
436+
flushSync();
437+
assert.deepEqual(log, [2, 20]);
438+
};
439+
});
440+
405441
test('effect teardown is removed on re-run', () => {
406442
const count = state(0);
407443
let first = true;

0 commit comments

Comments
 (0)