Bokeh callback conflict

Hi, I wanted to try using a ColumnDataSource to be able to discriminate when a js_on_change callback is triggered by manually changing the value of the object itself, or when it is triggered by another callback changing the object value. Here is the example code:

from bokeh.plotting import figure
from bokeh.models import NumericInput, ColumnDataSource, CustomJS, Row, HoverTool
from bokeh.resources import CDN
from bokeh.embed import file_html

manual_source = ColumnDataSource(data={"flag": [1]})
scatter_source = ColumnDataSource(data={"x": list(range(5)), "y": list(range(5))})

inp = NumericInput(value=None)
inp.js_on_change(
    "value",
    CustomJS(
        args={"manual_source": manual_source},
        code="""
    if (manual_source.data["flag"][0]==1) {
        console.log("Triggered by manual input to inp");
    } else {
        console.log("Triggered by another callback");
        manual_source.data["flag"][0] = 1;
        manual_source.change.emit();
    }
    console.log("inp: " + manual_source.data["flag"][0]);
    """,
    ),
)

fig = figure(tools=["hover"])
hover = fig.select_one(HoverTool)
hover.callback = CustomJS(
    args={
        "inp": inp,
        "manual_source": manual_source,
    },
    code="""
    const selected = cb_data["index"].indices;

    if (selected.length>0) {
        manual_source.data["flag"][0] = 0;
        inp.value = selected[selected.length-1];
        console.log("hover: " + manual_source.data["flag"][0]);
    }   
    """,
)


fig.scatter("x", "y", source=scatter_source)


layout = Row(fig, inp)

with open("test.html", "w") as out:
    out.write(file_html(layout, CDN, "test", suppress_callback_warning=True))

It kind of works, except that after a hover, the value of the manual_source is 0 instead of the 1 I was expecting, despite it being 1 at the end of the inp callback.
I also attach a screen capture of the issue with the console messages, on hover, the inp log shows 1 but the hover log (which should happen “after” and i think that may be my misunderstanding of the callbacks inner working) shows 0.

So after the hover, I have to manually change the input value twice before manual_source.data[‘flag’][0] == 1.

Anything I can do to have the value as 1 at the console.log("hover line ?

It seems the error may be linked to how fast the hovertool callback fires. It was triggering multiple time on hover over a same point, but after the first time setting the inp.value would not trigger the inp callback, so the hover callback was just setting the flag to 0. If I add a condition to change the input value only when the numeric input is different, I get the expected behaviour (although the log statement in the hover needs a settimeout to reflect the actual change).

here is the updated code with the expected behaviour, I also opted to use a window parameter instead of a columndatasource to store the manual flag

from bokeh.plotting import figure
from bokeh.models import NumericInput, ColumnDataSource, CustomJS, Row, HoverTool
from bokeh.resources import CDN
from bokeh.embed import file_html

scatter_source = ColumnDataSource(data={"x": list(range(5)), "y": list(range(5))})

inp = NumericInput(value=None)
inp.js_on_change(
    "value",
    CustomJS(
        code="""
    if (typeof window.manual_flag==="undefined") {
        window.manual_flag = 1;
    }

    if (window.manual_flag==0) {
        console.log("Triggered by another callback");
        window.manual_flag = 1;
    } else {
        console.log("Triggered by manual input to inp");
    }
    console.log("inp: " + window.manual_flag);
    """,
    ),
)

fig = figure(tools=["hover"])
hover = fig.select_one(HoverTool)
hover.callback = CustomJS(
    args={"inp": inp},
    code="""
    const selected = cb_data["index"].indices;
    const index = selected[selected.length-1];

    if (selected.length>0 && inp.value != index) {
        window.manual_flag = 0;
        inp.value = index;
        setTimeout(function() {
            console.log("hover: " + window.manual_flag);
        },500);
    }
    """,
)


fig.scatter("x", "y", source=scatter_source)


layout = Row(fig, inp)

with open("test.html", "w") as out:
    out.write(file_html(layout, CDN, "test", suppress_callback_warning=True))