observablehq / stdlib

The Observable standard library.
https://observablehq.com/@observablehq/standard-library
ISC License
961 stars 83 forks source link

Add promises.never as alternative to returning an unresolved promise. #218

Closed tomlarkworthy closed 3 years ago

tomlarkworthy commented 3 years ago

Add promises.never as alternative to returning an unresolved promise.

I often return new Promise(() => {}) to hold up the dataflow, but this is ugly. Its not just me, see https://talk.observablehq.com/t/audio-worklet-example/5138 So this is a more readable alternative that I think belongs with the Promises utilities.

mbostock commented 3 years ago

You should use the invalidation promise for this. Promises.never, as designed, will never resolve, and thus tends to leak associated resources. If you use the invalidation promise instead, those resources can at least be cleaned up if the cell is re-evaluated.

skybrian commented 3 years ago

I'm fine with my code as-is but maybe I should explain what I'm doing and there's a better solution?

I do use invalidation to clean up resources, but I'm using the unresolvable promise for something else. Effectively, the notebook has two states, uninitialized and running (recording sound). The straightforward way to handle this is to write code in each cell to handle both states wherever it matters. To avoid cluttering up the code with null checks, I decided to return an unresolvable promise for the audioContext when the uninitialized state, which prevents downstream cells from running at all.

(The logic is slightly messy because the state machine has a transition from running back to uninitialized and downstream cells won't be rerun in that case, but it happens to be harmless.)

Perhaps that's too clever by half? The uninitialized state exists because browsers seem to disallow audio without user interaction, but the notebook isn't really about the state machine. It seems like notebooks are a bit of an awkward fit for writing apps with a global state machine, since each cell needs to handle all states.

mbostock commented 3 years ago

@skybrian I think you could do this, and it would work:

if (!running) return invalidation; // Spin until this cell reruns for some reason.
skybrian commented 3 years ago

Okay, I understand now. Yes it does work. Thank you!

This is a separate issue, but it occurs to me that the Observable runtime could be modified to provide a more elegant way of cleanly returning to the page's uninitialized state. Suppose a cell could return a special value that causes the runtime to reset all downstream cells to undefined without evaluating their definitions? (Also, any downstream invalidation promises should be run.)

tomlarkworthy commented 3 years ago

Wow using the invalidation promise is a great idea.