Synchronously call javascript function from python

I have built a custom widget to show 3d Objects (in the past I used pythreejs, but since this project isn’t updated for quite some time, e.g. no recent version of threejs, I decided to move away and build my logic directly with threejs wrapped in an ipywidget)

I now want to call some of the Javascript functions from Python, i.e. control the object from Python or get values like positions back. At the moment I use custom messages (Widget.send) which work well if there is no relevant return value.
If I need the return value from Javascript, e.g. for getting the position, the Javascript method synchs a widget traitlet called “result”.

Firstly, this doesn’t feel like a clean way, and secondly, this is an asynchronous pattern which seems to make it difficult to evaluate in one JupyterLab cell . The only way that works in notebook cell I found is via callbacks - which “stops” the python kernel so that the widget state can be synched and then allows the kernel to call the callback (which includes my result value):

from ipywidgets import Output
out = Output()

def action(result):
    with out:
        print(json.loads(result))           
widget.get("camera.zoom", callback=action)
out

This also means that I cannot use the value easily in the next cell. Looks like when I “Run all” the python kernel is too fast to allow the widget state to be synched before the next cell wants to access the traitlet - at least that is my explanation for the observed bahavior.

Is there a way with ipywidgets to get the result synchronously, as in

zoom = widget.get("camera.zoom")

In both examples, widget.get should ask the frontend for the value zoom of the javascript object camera

It is possible to use some of the asynchronous widgets patterns. If you can avoid threads (the very last section)… do!

For example, use a custom events, a la Button, update when the thing changes, and hold until the queue gets emptied out, all wrapped up in an async function.

This would be in await-land, and would “block” execution continuing until the next cell so there wouldn’t be any cell race conditions. run_until_complete is kind of a bummer, though, as it can actually block the python process from getting anything else done… like processing kernel interrupt messages!

Thanks @bollwyvl This is what I tried before, but it doesn’t seem to work in JupyterLab:

If I run Cell1 and Cell2 and move the slider, it works. Running Cell3 afterwards returns the slider value as expected.
If I run Cell1, Cell2 and Cell3, I can move the slider, however the result message does not reach the python kernel since the await blocks it. So Cell3 waits forever and I have to restart the kernel.

Am I misunderstanding something? Or can I force (e.g in a separate thread) the processing of the kernel messages?

Cell1

import asyncio
from ipywidgets import IntSlider

def wait_for_change(widget, value):
    future = asyncio.Future()
    
    def getvalue(change):
        future.set_result(change.new)
        widget.unobserve(getvalue, value)
        
    widget.observe(getvalue, value)
    return future

slider = IntSlider()
slider

Cell2

async def f():
    return await wait_for_change(slider, 'value')

task = asyncio.create_task(f())

Cell3

await task