diff --git a/frontend/components/Editor.js b/frontend/components/Editor.js index a6e56ae78f..d9376cbd71 100644 --- a/frontend/components/Editor.js +++ b/frontend/components/Editor.js @@ -651,15 +651,36 @@ patch: ${JSON.stringify( const message = update.message switch (update.type) { case "notebook_diff": + let apply_promise = Promise.resolve() if (message?.response?.from_reset) { console.log("Trying to reset state after failure") - apply_notebook_patches(message.patches, initial_notebook()).catch((e) => { + apply_promise = apply_notebook_patches(message.patches, initial_notebook()).catch((e) => { alert("Oopsie!! please refresh your browser and everything will be alright!") throw e }) } else if (message.patches.length !== 0) { - apply_notebook_patches(message.patches) + apply_promise = apply_notebook_patches(message.patches) } + + const set_waiting = () => { + let from_update = message?.response?.update_went_well != null + let is_just_acknowledgement = from_update && message.patches.length === 0 + // console.log("Received patches!", message.patches, message.response, is_just_acknowledgement) + + if (!is_just_acknowledgement) { + this.waiting_for_bond_to_trigger_execution = false + } + } + apply_promise + .then(set_waiting) + .catch((e) => { + set_waiting() + throw e + }) + .then(() => { + this.send_queued_bond_changes() + }) + break default: console.error("Received unknown update type!", update) @@ -788,6 +809,8 @@ patch: ${JSON.stringify( }) } } + /** Whether we just set a bond value which will trigger a cell to run, but we are still waiting for the server to process the bond value (and run the cell). See https://github.com/fonsp/Pluto.jl/issues/1891 for more info. */ + this.waiting_for_bond_to_trigger_execution = false /** Number of local updates that have not yet been applied to the server's state. */ this.pending_local_updates = 0 this.js_init_set = new SetWithEmptyCallback(() => { @@ -797,6 +820,7 @@ patch: ${JSON.stringify( /** Is the notebook ready to execute code right now? (i.e. are no cells queued or running?) */ this.notebook_is_idle = () => { return !( + this.waiting_for_bond_to_trigger_execution || this.pending_local_updates > 0 || // a cell is running: Object.values(this.state.notebook.cell_results).some((cell) => cell.running || cell.queued) || @@ -808,6 +832,15 @@ patch: ${JSON.stringify( this.is_process_ready = () => this.state.notebook.process_status === ProcessStatus.starting || this.state.notebook.process_status === ProcessStatus.ready + let bond_will_trigger_evaluation = (/** @type {string|PropertyKey} */ sym) => + Object.entries(this.state.notebook.cell_dependencies).some(([cell_id, deps]) => { + // if the other cell depends on the variable `sym`... + if (deps.upstream_cells_map.hasOwnProperty(sym)) { + // and the cell is not disabled + return !(this.state.notebook.cell_inputs[cell_id]?.running_disabled ?? true) + } + }) + let last_update_notebook_task = Promise.resolve() /** @param {(notebook: NotebookData) => void} mutate_fn */ let update_notebook = (mutate_fn) => { @@ -821,13 +854,13 @@ patch: ${JSON.stringify( mutate_fn(notebook) }) - // If "notebook is not idle" I seperate and store the bonds updates, + // If "notebook is not idle" we seperate and store the bonds updates, // to send when the notebook is idle. This delays the updating of the bond for performance, // but when the server can discard bond updates itself (now it executes them one by one, even if there is a newer update ready) // this will no longer be necessary - // console.log(`this.notebook_is_idle():`, this.notebook_is_idle()) - if (!this.notebook_is_idle()) { - let changes_involving_bonds = changes.filter((x) => x.path[0] === "bonds") + let is_idle = this.notebook_is_idle() + let changes_involving_bonds = changes.filter((x) => x.path[0] === "bonds") + if (!is_idle) { this.bonds_changes_to_apply_when_done = [...this.bonds_changes_to_apply_when_done, ...changes_involving_bonds] changes = changes.filter((x) => x.path[0] !== "bonds") } @@ -838,18 +871,24 @@ patch: ${JSON.stringify( console.log(`Changes to send to server from "${previous_function_name}":`, changes) } catch (error) {} } - if (changes.length === 0) { - return - } - for (let change of changes) { if (change.path.some((x) => typeof x === "number")) { throw new Error("This sounds like it is editing an array...") } } + + if (changes.length === 0) { + return + } + if (is_idle) { + this.waiting_for_bond_to_trigger_execution ||= changes_involving_bonds.some( + (x) => x.path.length >= 1 && bond_will_trigger_evaluation(x.path[1]) + ) + } this.pending_local_updates++ this.on_patches_hook(changes) try { + // console.log("Sending changes to server:", changes) await Promise.all([ this.client.send("update_notebook", { updates: changes }, { notebook_id: this.state.notebook.notebook_id }, false).then((response) => { if (response.message?.response?.update_went_well === "👎") { diff --git a/src/evaluation/RunBonds.jl b/src/evaluation/RunBonds.jl index 9e6aad6790..293dbac18a 100644 --- a/src/evaluation/RunBonds.jl +++ b/src/evaluation/RunBonds.jl @@ -1,4 +1,10 @@ -function set_bond_values_reactive(; session::ServerSession, notebook::Notebook, bound_sym_names::AbstractVector{Symbol}, is_first_values::AbstractVector{Bool}=[true for x in bound_sym_names], kwargs...)::Union{Task,TopologicalOrder} +function set_bond_values_reactive(; + session::ServerSession, notebook::Notebook, + bound_sym_names::AbstractVector{Symbol}, + is_first_values::AbstractVector{Bool}=[true for x in bound_sym_names], + initiator=nothing, + kwargs... +)::Union{Task,TopologicalOrder} # filter out the bonds that don't need to be set to_set = filter(zip(bound_sym_names, is_first_values) |> collect) do (bound_sym, is_first_value) new_value = notebook.bonds[bound_sym].value @@ -25,6 +31,7 @@ function set_bond_values_reactive(; session::ServerSession, notebook::Notebook, end .|> first if isempty(to_set) || !will_run_code(notebook) + send_notebook_changes!(ClientRequest(; session, notebook, initiator)) return TopologicalOrder(notebook.topology, Cell[], Dict{Cell, ReactivityError}()) end diff --git a/src/webserver/Dynamic.jl b/src/webserver/Dynamic.jl index a351100b27..fc2aa2786c 100644 --- a/src/webserver/Dynamic.jl +++ b/src/webserver/Dynamic.jl @@ -327,10 +327,11 @@ responses[:update_notebook] = function response_update_notebook(🙋::ClientRequ bound_sym_names=bound_sym_names, is_first_values=is_first_values, run_async=true, + initiator=🙋.initiator, ) end - send_notebook_changes!(🙋; commentary=Dict(:update_went_well => :👍)) + send_notebook_changes!(🙋; commentary=Dict(:update_went_well => :👍)) catch ex @error "Update notebook failed" 🙋.body["updates"] exception=(ex, stacktrace(catch_backtrace())) response = Dict(