Skip to content

Commit d5e372d

Browse files
committed
fix: improve handling of unowned derived signals
1 parent 0afb8d4 commit d5e372d

File tree

4 files changed

+61
-7
lines changed

4 files changed

+61
-7
lines changed

.changeset/green-tigers-judge.md

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: improve handling of unowned derived signals

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

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,8 @@ function create_source_signal(flags, value) {
167167
f: flags,
168168
// value
169169
v: value,
170+
// write version
171+
w: 0,
170172
// this is for DEV only
171173
inspect: new Set()
172174
};
@@ -179,7 +181,9 @@ function create_source_signal(flags, value) {
179181
// flags
180182
f: flags,
181183
// value
182-
v: value
184+
v: value,
185+
// write version
186+
w: 0
183187
};
184188
}
185189

@@ -211,6 +215,8 @@ function create_computation_signal(flags, value, block) {
211215
r: null,
212216
// value
213217
v: value,
218+
// write version
219+
w: 0,
214220
// context: We can remove this if we get rid of beforeUpdate/afterUpdate
215221
x: null,
216222
// destroy
@@ -239,6 +245,8 @@ function create_computation_signal(flags, value, block) {
239245
r: null,
240246
// value
241247
v: value,
248+
// write version
249+
w: 0,
242250
// context: We can remove this if we get rid of beforeUpdate/afterUpdate
243251
x: null,
244252
// destroy
@@ -296,6 +304,17 @@ function is_signal_dirty(signal) {
296304
return true;
297305
}
298306
}
307+
// If we're workig with an unowned derived signal, then we need to check
308+
// if our dependency write version is higher. If is is then we can assume
309+
// that state has changed to a newer version and thus this unowned signal
310+
// is also dirty.
311+
const is_unowned = (flags & UNOWNED) !== 0;
312+
const write_version = signal.w;
313+
const dep_write_version = dependency.w;
314+
if (is_unowned && dep_write_version > write_version) {
315+
signal.w = dep_write_version;
316+
return true;
317+
}
299318
}
300319
}
301320
}
@@ -825,7 +844,9 @@ function update_derived(signal, force_schedule) {
825844
const value = execute_signal_fn(signal);
826845
updating_derived = previous_updating_derived;
827846
const status =
828-
(current_skip_consumer || (signal.f & UNOWNED) !== 0) && signal.d !== null ? DIRTY : CLEAN;
847+
(current_skip_consumer || (signal.f & UNOWNED) !== 0) && signal.d !== null
848+
? MAYBE_DIRTY
849+
: CLEAN;
829850
set_signal_status(signal, status);
830851
const equals = /** @type {import('./types.js').EqualsFunctions} */ (signal.e);
831852
if (!equals(value, signal.v)) {
@@ -1153,18 +1174,18 @@ function mark_signal_consumers(signal, to_status, force_schedule) {
11531174
const consumer = consumers[i];
11541175
const flags = consumer.f;
11551176
const unowned = (flags & UNOWNED) !== 0;
1156-
const dirty = (flags & DIRTY) !== 0;
11571177
// We skip any effects that are already dirty (but not unowned). Additionally, we also
11581178
// skip if the consumer is the same as the current effect (except if we're not in runes or we
11591179
// are in force schedule mode).
1160-
if ((dirty && !unowned) || ((!force_schedule || !runes) && consumer === current_effect)) {
1180+
if ((!force_schedule || !runes) && consumer === current_effect) {
11611181
continue;
11621182
}
11631183
set_signal_status(consumer, to_status);
11641184
// If the signal is not clean, then skip over it – with the exception of unowned signals that
1165-
// are already dirty. Unowned signals might be dirty because they are not captured as part of an
1185+
// are already maybe dirty. Unowned signals might be dirty because they are not captured as part of an
11661186
// effect.
1167-
if ((flags & CLEAN) !== 0 || (dirty && unowned)) {
1187+
const maybe_dirty = (flags & MAYBE_DIRTY) !== 0;
1188+
if ((flags & CLEAN) !== 0 || (maybe_dirty && unowned)) {
11681189
if ((consumer.f & IS_EFFECT) !== 0) {
11691190
schedule_effect(/** @type {import('./types.js').EffectSignal} */ (consumer), false);
11701191
} else {
@@ -1203,6 +1224,8 @@ export function set_signal_value(signal, value) {
12031224
!(/** @type {import('./types.js').EqualsFunctions} */ (signal.e)(value, signal.v))
12041225
) {
12051226
signal.v = value;
1227+
// Increment write version so that unowned signals can properly track dirtyness
1228+
signal.w++;
12061229
// If the current signal is running for the first time, it won't have any
12071230
// consumers as we only allocate and assign the consumers after the signal
12081231
// has fully executed. So in the case of ensuring it registers the consumer

packages/svelte/src/internal/client/types.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ export type SourceSignal<V = unknown> = {
7777
f: SignalFlags;
7878
/** value: The latest value for this signal */
7979
v: V;
80+
// write version
81+
w: number,
8082
};
8183

8284
export type SourceSignalDebug = {
@@ -111,6 +113,8 @@ export type ComputationSignal<V = unknown> = {
111113
v: V;
112114
/** level: the depth from the root signal, used for ordering render/pre-effects topologically **/
113115
l: number;
116+
/** write version: used for unowned signals to track if their depdendencies are dirty or not **/
117+
w: number;
114118
};
115119

116120
export type Signal<V = unknown> = SourceSignal<V> | ComputationSignal<V>;

packages/svelte/tests/signals/test.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ describe('signals', () => {
227227
// Ensure we're not leaking dependencies
228228
assert.deepEqual(
229229
nested.slice(0, -2).map((s) => s.d),
230-
[null, null, null, null]
230+
[null, null]
231231
);
232232
};
233233
});
@@ -284,6 +284,28 @@ describe('signals', () => {
284284
};
285285
});
286286

287+
let some_state = $.source({});
288+
let some_deps = $.derived(() => {
289+
return [$.get(some_state)];
290+
});
291+
292+
test('two effects with an unowned derived that has some depedencies', () => {
293+
const log: Array<Array<any>> = [];
294+
295+
$.render_effect(() => {
296+
log.push($.get(some_deps));
297+
});
298+
299+
$.render_effect(() => {
300+
log.push($.get(some_deps));
301+
});
302+
303+
return () => {
304+
$.flushSync();
305+
assert.deepEqual(log, [[{}], [{}]]);
306+
};
307+
});
308+
287309
test('schedules rerun when writing to signal before reading it', (runes) => {
288310
if (!runes) return () => {};
289311

0 commit comments

Comments
 (0)