Intro example bug: previous cells continue running

In the last example here, the animation code continues running on previous versions of the cells:

You can see this in the console by replacing with the following code:

{
  const w = Math.min(640, width);
  const h = 320;
  const r = 20;
  const t = 1500;
  const svg = d3.select(DOM.svg(w, h));
  const circle = svg.append("circle").attr("r", r).attr("cx", w / 4).attr("cy", h / 4);
  yield svg.node();
  let i=0;
  while (true) {
    console.log("loop", i++); // <-- continues printing
    await circle.transition().duration(t).attr("cy", h * 3 / 4).end();
    await circle.transition().duration(t).attr("cx", w * 3 / 4).end();
    await circle.transition().duration(t).attr("cy", h * 1 / 4).end();
    await circle.transition().duration(t).attr("cx", w * 1 / 4).end();

    // quickfix: stop the loop of our node has been unmounted (i.e. parentElement is null)
    if (!svg.node().parentElement) break;
  }
}

I noticed this was warned against here when mentioning the difficulty of cleaning up timeouts:

I’m making use of the quickfix above so I can stop the loop by detecting when the element is no longer mounted.

Yes, this was a bug in the example which I’ve now fixed. Thank you for the report. (Arguably it’s “fine” in the sense it works if the cell is never re-evaluated, but it should still be considered an antipattern.)

The problem is that promises are not cancellable, so there’s no way for the Observable runtime to cancel an asynchronous function that goes into an infinite loop as this one did.

Your fix works, but I chose instead to move the yield inside the infinite loop. Unlike promises, generators are cancellable and are terminated automatically by the Observable runtime using generator.return.

In the near future, we’re planning on implementing an invalidation promise in the standard library that would resolve when a cell’s definition or inputs change. This would replace Promises.never (which leaks) and provide a new mechanism for disposing of resources when a cell is re-evaluated.

1 Like

Unlike promises, generators are cancellable

That’s a great description that cleared up a lot of my confusion. I want to stick my neck out and say that I find all these yields and awaits and promises to be very confusing. I think just showing a simple contrived example like this could go a long way to help clarify what’s going on:

{
  yield "foo";                // set cell value to "foo"
  await Promises.delay(1000); // wait 1 second
  yield "bar";                // set cell value to "bar"
}

As a side note, I’ve always wondered how to cancel async functions—like Unity’s StopCoroutine. I’m glad to see that yield acts as a safe way for async functions to sprinkle safe spots to pull the plug so to speak.

Maybe it’s worth mentioning that async cells can only be interrupted at yield points?

Mike shipped the invalidation promise this afternoon:

… so you can now clean up whatever you need to (timers, mutated DOM, external stateful libraries) before the cell is re-evaluated:

{
  var resource = library.allocSomethingBig();
  invalidation.then(() => {
    resource.dispose();
  });
  return resource;
}
2 Likes

I might start using this pattern if I want to cancel long async functions:

{
  const w = Math.min(640, width);
  const h = 320;
  const r = 20;
  const t = 1500;
  const svg = d3.select(DOM.svg(w, h));
  const circle = svg.append("circle").attr("r", r).attr("cx", w / 4).attr("cy", h / 4);
  let i=0;
  const tick = () => console.log(i++);

  // use `yield await` to allow the animation to be canceled
  async function* anim() {
    yield await circle.transition().duration(t).attr("cy", h * 3 / 4).end(); tick();
    yield await circle.transition().duration(t).attr("cx", w * 3 / 4).end(); tick();
    yield await circle.transition().duration(t).attr("cy", h * 1 / 4).end(); tick();
    yield await circle.transition().duration(t).attr("cx", w * 1 / 4).end(); tick();
  }

  yield svg.node();
  while (true) {
    for await (let step of anim()) {
      yield svg.node(); // yield after every step
    }
  }
}

Overkill for this simple case, but good to know I can do this when needed.

I was looking at,

and noticed it uses invalidation, whereas,

uses Generators.disposable. I don’t quite understand the intended uses of these two, or when we should prefer or the other in idiomatic usage.