Skip to content

Commit 946fe10

Browse files
committed
fix: ensure signal write invalidation within effects is persistent
1 parent ab3290f commit 946fe10

File tree

4 files changed

+89
-21
lines changed

4 files changed

+89
-21
lines changed
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 signal write invalidation within effects is persistent

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

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ import {
2929
INSPECT_EFFECT,
3030
UNOWNED,
3131
MAYBE_DIRTY,
32-
BLOCK_EFFECT
32+
BLOCK_EFFECT,
33+
ROOT_EFFECT
3334
} from '../constants.js';
3435
import * as e from '../errors.js';
3536
import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js';
@@ -191,17 +192,13 @@ export function internal_set(source, value) {
191192
is_runes() &&
192193
active_effect !== null &&
193194
(active_effect.f & CLEAN) !== 0 &&
194-
(active_effect.f & BRANCH_EFFECT) === 0
195+
(active_effect.f & (BRANCH_EFFECT | ROOT_EFFECT)) === 0
195196
) {
196-
if (new_deps !== null && new_deps.includes(source)) {
197-
set_signal_status(active_effect, DIRTY);
198-
schedule_effect(active_effect);
197+
198+
if (untracked_writes === null) {
199+
set_untracked_writes([source]);
199200
} else {
200-
if (untracked_writes === null) {
201-
set_untracked_writes([source]);
202-
} else {
203-
untracked_writes.push(source);
204-
}
201+
untracked_writes.push(source);
205202
}
206203
}
207204

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

Lines changed: 40 additions & 11 deletions
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,18 @@ 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 we need to re-schedule the current effect
468+
if (untracked_writes !== null && (reaction.f & (DERIVED | MAYBE_DIRTY | DIRTY)) === 0) {
469+
for (i = 0; i < /** @type {Source[]} */ (untracked_writes).length; i++) {
470+
schedule_possible_effect_self_invalidation(
471+
untracked_writes[i],
472+
/** @type {Effect} */ (reaction)
473+
);
474+
}
475+
}
476+
437477
// If we are returning to an previous reaction then
438478
// we need to increment the read version to ensure that
439479
// any dependencies in this reaction aren't marked with
@@ -907,17 +947,6 @@ export function get(signal) {
907947
} else {
908948
new_deps.push(signal);
909949
}
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-
}
921950
}
922951
} else if (is_derived && /** @type {Derived} */ (signal).deps === null) {
923952
var derived = /** @type {Derived} */ (signal);

packages/svelte/tests/signals/test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,43 @@ 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+
set(value, 10);
416+
});
417+
418+
return () => {
419+
flushSync();
420+
assert.deepEqual(log, [20]);
421+
};
422+
});
423+
424+
test('schedules rerun when writing to signal after reading it from derived', (runes) => {
425+
if (!runes) return () => {};
426+
let log: any[] = [];
427+
428+
const value = state(1);
429+
const double = derived(() => $.get(value) * 2);
430+
431+
user_effect(() => {
432+
log.push($.get(double));
433+
set(value, 10);
434+
});
435+
436+
return () => {
437+
flushSync();
438+
assert.deepEqual(log, [2, 20]);
439+
};
440+
});
441+
405442
test('effect teardown is removed on re-run', () => {
406443
const count = state(0);
407444
let first = true;

0 commit comments

Comments
 (0)