Notification when cell is re-run by the user?

I’m playing around a bit with Pluto and web-audio. Right now I’m just creating an oscillator with the frequency controlled by a slider. The code I have to do this is:

htl"""
<script id="osctest">
const div = this == null ? document.createElement("div") : this
const ctx = this == null ? new AudioContext() : div.ctx
const osc = this == null ? new OscillatorNode(ctx) : div.osc
osc.frequency.setValueAtTime($freq, ctx.currentTime)
if(this == null) {osc.connect(ctx.destination); osc.start()}
// invalidation.then(() => {
// 	osc.stop()
// 	ctx.close()
// 	console.log("invalidated")
// })

div.osc = osc
div.ctx = ctx
return div
</script>
"""

This works well, but the problem is that if I re-run the cell, this is null again so I end up creating a brand new oscillator and context, and now both are playing, rather than replacing the original.

As you can see in the commented section, I tried using the invalidate promise, but then realized that it gets called on a value update, not just a user-generated re-run. One solution I think would be to attach the context upstream in the DOM or to a global variable or something, but then it can’t be a re-useable function.

If there were something like invalidate that only got triggered on a user update, I could do the cleanup there.

@ssfrr

Here is a minimal working example I managed to make work using the MutationObserver API

@htl """
<script id="osctest">
let div = this ?? document.createElement("div")
if (this == null) {
	div.classList.toggle('oscillator-container',true)
	let id = $(join(rand("abcdefghilmnopqrstuvz",8)))
	div.id = id

	div.ctx = new AudioContext()
	div.osc = new OscillatorNode(div.ctx)
	div.osc.connect(div.ctx.destination); div.osc.start()

	
	const observer = new MutationObserver((entries) => {
		for (const entry of entries) {
			for (const added of entry.addedNodes) {
				if (added instanceof Element && added.classList.contains('oscillator-container') && added.id != id) {
					console.log('Found a manual rerun! Disconnecting...')
					div.osc.stop()
					div.ctx.close()
					observer.disconnect()
				}
			}
		}
	})
	observer.observe(currentScript.closest('div.raw-html-wrapper'), {childList: true})
}
div.osc.frequency.setValueAtTime($freq, div.ctx.currentTime)

return div
</script>
"""

The main idea is to assign a unique id to the div you are generating in the script only upon manual re-run (when this == null) and then attach a MutationObserver to the pluto-output wrapper that monitors addition and deletion of children.
As soon as a new div with the oscillator-container class is added and has an id which is different from the one you generated together with the MutationObserver, I assume that a manual rerun has been triggered and I can disconnect everything.

Seems to work, don’t know if there are better ways to achieve this though.

Had a similar problem with plotting - wanted to have movable isosurfaces etc. My solution is here (Julia) and here (JS).
Essentially you create a div labeled with an uuid which you can later refer to in the javascript.

Thanks both! @disberd that seems pretty clean.

On the one hand it seems like it might be nice for Pluto to have some built-in support for this kind of thing, but also it probably is only coming up here because of the weird way that the lifetime of WebAudio objects works.