svelte icon indicating copy to clipboard operation
svelte copied to clipboard

$derived rune breaks when L-shaped dependency updates at moderate rate

Open philholden opened this issue 1 year ago • 1 comments

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

  1. Click link above
  2. Hover line 1 then rapidly drag it back and forth for a couple of seconds image
  3. Try to drag line 3 line will not move but the associate rectangle will image
  4. Retry the same step but this time only gently move line 1
  5. When you try to drag line 3 you should now find it works
  6. Observation rapidly updating a (in bug description) breaks reactivity of c

Work around example

Steps

  1. Click above link and follow same steps as before (1 - 3)
  2. 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

philholden avatar Jan 24 '24 10:01 philholden

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?

trueadm avatar Jan 30 '24 14:01 trueadm

This now seems to have been fixed.

trueadm avatar May 09 '24 19:05 trueadm