svelte
svelte copied to clipboard
$derived rune breaks when L-shaped dependency updates at moderate rate
Describe the bug
I am going to call c an L-shaped dependency (but triangular may be better):
let a = $state(1);
let b = $derived(a * 10);
let c = $derived(a * b);
I have a situation where one derived value depends on another derived value and the state that that derived value is based on. I am finding it is possible for c to get "detached" after a updates rapidly. I expect circular dependencies to break but not L-shaped ones. Since a updates b, c only needs to update after b does.
Reproduction
Broken example
Steps
- Click link above
- Hover line 1 then rapidly drag it back and forth for a couple of seconds
- Try to drag line 3 line will not move but the associate rectangle will
- Retry the same step but this time only gently move line 1
- When you try to drag line 3 you should now find it works
- Observation rapidly updating
a(in bug description) breaks reactivity ofc
Work around example
Steps
- Click above link and follow same steps as before (1 - 3)
- You should find this never breaks
Here I have extracted the function that generated b and in c I call that function rather than depending on b.
let getB = a => a * 10;
let a = $state(1);
let b = $derived(getB());
let c = $derived(a * getB());
Relevant sections of code
These are in Split.svelte.js:
This is fragile:
class Handle {
split = $state();
x = $derived(this.calcX());
y = $derived(this.calcY());
width = $derived(this.split.horizontal ? this.split.handleSize : this.split.bounds.width);
height = $derived(!this.split.horizontal ? this.split.handleSize : this.split.bounds.height);
id = $derived(this.split.id);
constructor(split) {
this.split = split;
}
calcX() {
return this.split.horizontal ?
this.split.bounds.x + Math.floor(this.split.ratio * (this.split.bounds.width - this.split.handleSize)):
this.split.bounds.x
}
calcY() {
return !this.split.horizontal ?
this.split.bounds.y + Math.floor(this.split.ratio * (this.split.bounds.height - this.split.handleSize)):
this.split.bounds.y
}
}
This is robust:
class Handle {
split = $state();
x = $derived(this.calcX());
y = $derived(this.calcY());
width = $derived(this.calcWidth());
height = $derived(this.calcHeight());
id = $derived(this.split.id);
constructor(split) {
this.split = split;
}
calcWidth() {
return this.split.horizontal ? this.split.handleSize : calcWidth(this.split);
}
calcHeight() {
return !this.split.horizontal ? this.split.handleSize : calcHeight(this.split);
}
calcX() {
return this.split.horizontal ?
this.split.bounds.x + Math.floor(this.split.ratio * (calcWidth(this.split) - this.split.handleSize)):
this.split.bounds.x
}
calcY() {
return !this.split.horizontal ?
this.split.bounds.y + Math.floor(this.split.ratio * (calcHeight(this.split) - this.split.handleSize)):
this.split.bounds.y
}
}
Note there are no circular dependencies (just a a kind of L-shaped one).
Logs
No errors in console
System Info
Chrome, Mac M1
Severity
blocking an upgrade
It's really difficult to track exactly what's happening here. I wonder if there's a way we can simplify this out into another REPL that doesn't require drawing rectangles and lines so that can have something easier to debug and track?
This now seems to have been fixed.