Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

💁 API to make objects available inside JS #1124

Merged
merged 16 commits into from
Apr 27, 2021
21 changes: 18 additions & 3 deletions frontend/components/Cell.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { html, useState, useEffect, useMemo, useRef, useContext } from "../imports/Preact.js"
import { html, useState, useEffect, useMemo, useRef, useContext, useLayoutEffect } from "../imports/Preact.js"

import { CellOutput } from "./CellOutput.js"
import { CellInput } from "./CellInput.js"
Expand All @@ -20,7 +20,7 @@ import { PlutoContext } from "../common/PlutoContext.js"
* }} props
* */
export const Cell = ({
cell_result: { queued, running, runtime, errored, output },
cell_result: { queued, running, runtime, errored, output, published_objects },
cell_input: { cell_id, code, code_folded },
cell_input_local,
notebook_id,
Expand Down Expand Up @@ -73,8 +73,23 @@ export const Cell = ({
// during the initial page load, force_hide_input === true, so that cell outputs render fast, and codemirrors are loaded after
let show_input = !force_hide_input && (errored || class_code_differs || !class_code_folded)

const node_ref = useRef(null)

const [cell_api_ready, set_cell_api_ready] = useState(false)
const published_objects_ref = useRef(published_objects)
published_objects_ref.current = published_objects

useLayoutEffect(() => {
Object.assign(node_ref.current, {
getPublishedObject: (id) => published_objects_ref.current[id],
})

set_cell_api_ready(true)
})

return html`
<pluto-cell
ref=${node_ref}
onDragOver=${handler}
onDrop=${handler}
onDragEnter=${handler}
Expand Down Expand Up @@ -119,7 +134,7 @@ export const Cell = ({
>
<span></span>
</button>
<${CellOutput} ...${output} cell_id=${cell_id} />
${cell_api_ready ? html`<${CellOutput} ...${output} cell_id=${cell_id} />` : html``}
<${CellInput}
local_code=${cell_input_local?.code ?? code}
remote_code=${code}
Expand Down
2 changes: 2 additions & 0 deletions frontend/components/CellOutput.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,11 +245,13 @@ const execute_scripttags = async ({ root_node, script_nodes, previous_results_ma
if (is_displayable(old_result)) {
node.parentElement.insertBefore(old_result, node)
}

let result = await execute_dynamic_function({
environment: {
this: script_id ? old_result : window,
currentScript: node,
invalidation: invalidation,
getPublishedObject: (id) => node.closest("pluto-cell").getPublishedObject(id),
...observablehq_for_cells,
},
code: node.innerText,
Expand Down
1 change: 1 addition & 0 deletions frontend/components/Editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ const first_true_key = (obj) => {
* mime: string,
* rootassignee: ?string,
* }
* published_objects: object,
* }}
*/

Expand Down
1 change: 1 addition & 0 deletions src/analysis/Errors.jl
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ function relay_reactivity_error!(cell::Cell, error::Exception)
last_run_timestamp=time(),
persist_js_state=false,
)
cell.published_objects = Dict{String,Any}()
cell.runtime = nothing
cell.errored = true
end
1 change: 1 addition & 0 deletions src/evaluation/Run.jl
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ function set_output!(cell::Cell, run, expr_cache::ExprAnalysisCache; persist_js_
last_run_timestamp=time(),
persist_js_state=persist_js_state,
)
cell.published_objects = run.published_objects
cell.runtime = run.runtime
cell.errored = run.errored
end
Expand Down
7 changes: 5 additions & 2 deletions src/evaluation/WorkspaceManager.jl
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ function distributed_exception_result(exs::CompositeException, workspace::Worksp
interrupted=true,
process_exited=false,
runtime=nothing,
published_objects=Dict{String,Any}(),
)
elseif ex isa Distributed.ProcessExitedException
(
Expand All @@ -177,6 +178,7 @@ function distributed_exception_result(exs::CompositeException, workspace::Worksp
interrupted=true,
process_exited=true && !workspace.discarded, # don't report a process exit if the workspace was discarded on purpose
runtime=nothing,
published_objects=Dict{String,Any}(),
)
else
@error "Unkown error during eval_format_fetch_in_workspace" ex
Expand All @@ -186,6 +188,7 @@ function distributed_exception_result(exs::CompositeException, workspace::Worksp
interrupted=true,
process_exited=false,
runtime=nothing,
published_objects=Dict{String,Any}(),
)
end
end
Expand All @@ -194,7 +197,7 @@ end
"Evaluate expression inside the workspace - output is fetched and formatted, errors are caught and formatted. Returns formatted output and error flags.

`expr` has to satisfy `ExpressionExplorer.is_toplevel_expr`."
function eval_format_fetch_in_workspace(session_notebook::Union{SN,Workspace}, expr::Expr, cell_id::UUID, ends_with_semicolon::Bool=false, function_wrapped_info::Union{Nothing,Tuple}=nothing)::NamedTuple{(:output_formatted, :errored, :interrupted, :process_exited, :runtime),Tuple{PlutoRunner.MimedOutput,Bool,Bool,Bool,Union{UInt64,Nothing}}}
function eval_format_fetch_in_workspace(session_notebook::Union{SN,Workspace}, expr::Expr, cell_id::UUID, ends_with_semicolon::Bool=false, function_wrapped_info::Union{Nothing,Tuple}=nothing)::NamedTuple{(:output_formatted, :errored, :interrupted, :process_exited, :runtime, :published_objects),Tuple{PlutoRunner.MimedOutput,Bool,Bool,Bool,Union{UInt64,Nothing},Dict{String,Any}}}
workspace = get_workspace(session_notebook)

# if multiple notebooks run on the same process, then we need to `cd` between the different notebook paths
Expand Down Expand Up @@ -230,7 +233,7 @@ function eval_in_workspace(session_notebook::Union{SN,Workspace}, expr)
nothing
end

function format_fetch_in_workspace(session_notebook::Union{SN,Workspace}, cell_id, ends_with_semicolon, showmore_id::Union{PlutoRunner.ObjectDimPair,Nothing}=nothing)::NamedTuple{(:output_formatted, :errored, :interrupted, :process_exited, :runtime),Tuple{PlutoRunner.MimedOutput,Bool,Bool,Bool,Union{UInt64,Nothing}}}
function format_fetch_in_workspace(session_notebook::Union{SN,Workspace}, cell_id, ends_with_semicolon, showmore_id::Union{PlutoRunner.ObjectDimPair,Nothing}=nothing)::NamedTuple{(:output_formatted, :errored, :interrupted, :process_exited, :runtime, :published_objects),Tuple{PlutoRunner.MimedOutput,Bool,Bool,Bool,Union{UInt64,Nothing},Dict{String,Any}}}
workspace = get_workspace(session_notebook)

# instead of fetching the output value (which might not make sense in our context, since the user can define structs, types, functions, etc), we format the cell output on the worker, and fetch the formatted output.
Expand Down
2 changes: 2 additions & 0 deletions src/notebook/Cell.jl
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ Base.@kwdef mutable struct Cell
output::CellOutput=CellOutput()
queued::Bool=false
running::Bool=false

published_objects::Dict{String,Any}=Dict{String,Any}()

errored::Bool=false
runtime::Union{Nothing,UInt64}=nothing
Expand Down
82 changes: 77 additions & 5 deletions src/runner/PlutoRunner.jl
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@ import Base64
import FuzzyCompletions: Completion, ModuleCompletion, PropertyCompletion, FieldCompletion, completions, completion_text, score
import Base: show, istextmime
import UUIDs: UUID, uuid4
import Dates: DateTime
import Logging

export @bind

MimedOutput = Tuple{Union{String,Vector{UInt8},Dict{Symbol,Any}},MIME}
ObjectID = typeof(objectid("hello computer"))
ObjectDimPair = Tuple{ObjectID,Int64}
const ObjectID = typeof(objectid("hello computer"))
const ObjectDimPair = Tuple{ObjectID,Int64}



Expand Down Expand Up @@ -195,6 +196,9 @@ If the third argument is a `Tuple{Set{Symbol}, Set{Symbol}}` containing the refe
This function is memoized: running the same expression a second time will simply call the same generated function again. This is much faster than evaluating the expression, because the function only needs to be Julia-compiled once. See https://github.com/fonsp/Pluto.jl/pull/720
"""
function run_expression(expr::Any, cell_id::UUID, function_wrapped_info::Union{Nothing,Tuple{Set{Symbol},Set{Symbol}}}=nothing)
currently_running_cell_id[] = cell_id
cell_published_objects[cell_id] = Dict{String,Any}()

result, runtime = if function_wrapped_info === nothing
proof = ReturnProof()
wrapped = timed_expr(expr, proof)
Expand Down Expand Up @@ -402,6 +406,7 @@ const alive_world_val = getfield(methods(Base.sqrt).ms[1], deleted_world) # type
# TODO: clear key when a cell is deleted furever
const cell_results = Dict{UUID,Any}()
const cell_runtimes = Dict{UUID,Union{Nothing,UInt64}}()
const cell_published_objects = Dict{UUID,Dict{String,Any}}()

const tree_display_limit = 30
const tree_display_limit_increase = 40
Expand All @@ -412,7 +417,7 @@ const table_column_display_limit_increase = 30

const tree_display_extra_items = Dict{UUID,Dict{ObjectDimPair,Int64}}()

function formatted_result_of(id::UUID, ends_with_semicolon::Bool, showmore::Union{ObjectDimPair,Nothing}=nothing)::NamedTuple{(:output_formatted, :errored, :interrupted, :process_exited, :runtime),Tuple{PlutoRunner.MimedOutput,Bool,Bool,Bool,Union{UInt64,Nothing}}}
function formatted_result_of(id::UUID, ends_with_semicolon::Bool, showmore::Union{ObjectDimPair,Nothing}=nothing)::NamedTuple{(:output_formatted, :errored, :interrupted, :process_exited, :runtime, :published_objects),Tuple{PlutoRunner.MimedOutput,Bool,Bool,Bool,Union{UInt64,Nothing},Dict{String,Any}}}
load_integration_if_needed.(integrations)

extra_items = if showmore === nothing
Expand All @@ -432,11 +437,12 @@ function formatted_result_of(id::UUID, ends_with_semicolon::Bool, showmore::Unio
("", MIME"text/plain"())
end
return (
output_formatted = output_formatted,
output_formatted = output_formatted,
errored = errored,
interrupted = false,
process_exited = false,
runtime = get(cell_runtimes, id, nothing)
runtime = get(cell_runtimes, id, nothing),
published_objects = get(cell_published_objects, id, Dict{String,Any}()),
)
end

Expand Down Expand Up @@ -843,6 +849,14 @@ end
trynameof(x::DataType) = nameof(x)
trynameof(x::Any) = Symbol()









###
# TABLE VIEWER
##
Expand Down Expand Up @@ -1206,6 +1220,64 @@ end"""



###
# PUBLISHED OBJECTS
###

const currently_running_cell_id = Ref{UUID}(uuid4())

function publish(x)::String
if !packable(x)
throw(ArgumentError("Only simple objects can be shared with JS, like vectors and dictionaries."))
end
d = get!(Dict{String,Any}, cell_published_objects, currently_running_cell_id[])
id = string(notebook_id[], "/", currently_running_cell_id[], "/", string(objectid(x), base=16))
d[id] = x
return id
end

"""
publish_to_js(x)

Make the object `x` available to the JS runtime of this cell. The returned string is a JS command that, when executed in this cell's output, gives the object.

!!! warning

This function is not yet public API, it will become public in the next weeks. Only use for experiments.

# Example
```julia
let
x = Dict(
"data" => rand(Float64, 20),
"name" => "juliette",
)

HTML("\""
<script>
// we interpolate into JavaScript:
const x = \$(PlutoRunner.publish_to_js(x))

console.log(x.name, x.data)
</script>
"\"")
end
```
"""
function publish_to_js(x)::String
id = publish(x)
return "/* See the documentation for PlutoRunner.publish_to_js */ getPublishedObject(\"$(id)\")"
end

const Packable = Union{Nothing,Missing,String,Int64,Int32,Int16,Int8,UInt64,UInt32,UInt16,UInt8,Float32,Float64,Bool,MIME,UUID,DateTime}
packable(::Packable) = true
packable(::Any) = false
packable(::Vector{<:Packable}) = true
packable(::Dict{<:Packable,<:Packable}) = true
packable(x::Vector) = all(packable, x)
packable(d::Dict) = all(packable, keys(d)) && all(packable, values(d))
packable(t::Tuple) = all(packable, t)
packable(t::NamedTuple) = all(packable, t)



Expand Down
1 change: 1 addition & 0 deletions src/webserver/Dynamic.jl
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ function notebook_to_js(notebook::Notebook)
"last_run_timestamp" => cell.output.last_run_timestamp,
"persist_js_state" => cell.output.persist_js_state,
),
"published_objects" => cell.published_objects,
"queued" => cell.queued,
"running" => cell.running,
"errored" => cell.errored,
Expand Down
34 changes: 33 additions & 1 deletion test/Dynamic.jl
Original file line number Diff line number Diff line change
Expand Up @@ -157,12 +157,44 @@ end

notebook = Notebook([
Cell("PlutoRunner.notebook_id[] |> Text"),
Cell("""
let
a = PlutoRunner.publish(Dict(
"hello" => "world",
"xx" => UInt8[6,7,8],
))
b = PlutoRunner.publish("cool")
Text((a, b))
end
"""),
Cell("3"),
Cell("PlutoRunner.publish_to_js(Ref(4))"),
Cell("PlutoRunner.publish_to_js((ref=4,))"),
])
fakeclient.connected_notebook = notebook

update_save_run!(🍭, notebook, notebook.cells)
@test notebook.cells[1].output.body == notebook.notebook_id |> string


@test !notebook.cells[2].errored
a, b = Meta.parse(notebook.cells[2].output.body) |> eval
p = notebook.cells[2].published_objects
@test sort(collect(keys(p))) == sort([a,b])
@test isempty(notebook.cells[3].published_objects)

@test p[a] == Dict(
"hello" => "world",
"xx" => UInt8[6,7,8],
)
@test p[b] == "cool"

setcode(notebook.cells[2], "2")
update_save_run!(🍭, notebook, notebook.cells)
@test isempty(notebook.cells[2].published_objects)

@test notebook.cells[4].errored
@test !notebook.cells[5].errored

WorkspaceManager.unmake_workspace((🍭, notebook))
end
end