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

Direct communication link between user JS and JL for JSServe support #2392

Closed
wants to merge 29 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
6fad130
Working (with Require.jl)
dralletje Mar 10, 2021
76ecf2d
Woop woop
dralletje Mar 11, 2021
7f94405
Merge branch 'master' into webio
dralletje Mar 11, 2021
7fe9b01
Fun
dralletje Mar 11, 2021
e149418
Merge branch 'webio' of https://github.com/fonsp/Pluto.jl into webio
dralletje Mar 11, 2021
490efde
*Integrations
dralletje Mar 11, 2021
3270792
"Universal" hooks from notebook
dralletje Mar 13, 2021
3a7b7f5
I'm sorry, PlutoRunnerDistributedTypes
dralletje Mar 14, 2021
1b4b959
POST requests as well
dralletje Mar 14, 2021
1a604ea
Hmm
dralletje Mar 14, 2021
433f224
Polishing
dralletje Mar 14, 2021
111aa11
Last small tiny fixes
dralletje Mar 14, 2021
3b76b55
go go go
dralletje Mar 15, 2021
83560f6
add function name so that the stack trace is leesbaar
fonsp Mar 16, 2021
1d0b872
typo
fonsp Mar 16, 2021
bbd136e
documentatation
fonsp Mar 16, 2021
c103561
Merge branch 'main' into webio
dralletje Apr 9, 2021
5517401
Nicer api
dralletje Apr 9, 2021
8ddc8ec
Rebuilding ym container so putting there here for a second
dralletje Apr 9, 2021
39c7f04
Aaaand there gone
dralletje Apr 9, 2021
b8b0d4e
typo
fonsp Jun 2, 2021
fc6f3d3
Merge branch 'webio' of https://github.com/fonsp/Pluto.jl into webio
dralletje Jul 28, 2021
8039bec
remove WebIO code
fonsp Nov 24, 2022
434a61b
Merge branch 'main' into js-jl-link-for-jsserve
fonsp Nov 24, 2022
6894cce
merge part 2
fonsp Nov 24, 2022
861af04
remove AssetRegistry integration
fonsp Nov 24, 2022
a609679
Delete .gitignore
fonsp Nov 24, 2022
2b3785c
remove AssetRegistry part 2
fonsp Nov 24, 2022
0b472ea
move file
fonsp Nov 24, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions frontend/components/Editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -716,13 +716,95 @@ patch: ${JSON.stringify(
})

this.apply_notebook_patches = apply_notebook_patches

// If you are a happy notebook maker/developer,
// and you see these window.Pluto.onIntegrationMessage and you're like WOW!
// Let me write an integration with other code!! Please don't. Sure, try it out as you wish,
// but I will 100% change this name and structure, so please come to the Zulip chat and connect with us.
if ("Pluto" in window) {
// prettier-ignore
console.warn("Pluto global already exists on window, will replace it but this surely breaks something")
}
// Trying out if this works with browsers native EventTarget,
// but isn't part of the public-ish api so can change it to something different later
class IntegrationsMessageToClientEvent extends Event {
/**
* @param {{
* module_name: string,
* body: any,
* }} props
*/
constructor({ module_name, body }) {
super("integrations_message_to_client")
this.module_name = module_name
this.body = body
this.handled = false
}
}
let pluto_api_event_target = new EventTarget()
// @ts-ignore
window.Pluto = {
/**
* @param {String} module_name
* @param {(message: any) => void} fn
*/
onIntegrationsMessage: (module_name, fn) => {
if (typeof module_name === "function") {
throw new Error(`You called Pluto.onIntegrationsMessage without a module name.`)
}
/** @param {IntegrationsMessageToClientEvent} event */
let handle_fn = (event) => {
if (event.module_name == module_name) {
// @ts-ignore
event.handled = true
fn(event.body)
}
}
pluto_api_event_target.addEventListener("integrations_message_to_client", handle_fn)
return () => {
pluto_api_event_target.removeEventListener("integrations_message_to_client", handle_fn)
}
},
/**
* @param {String} module_name
* @param {any} message
*/
sendIntegrationsMessage: (module_name, message) => {
this.client.send(
"integrations_message_to_server",
{
module_name: module_name,
body: message,
},
{ notebook_id: this.state.notebook.notebook_id },
false
)
},

/** @private */
pluto_api_event_target: pluto_api_event_target,
}

// these are update message that are _not_ a response to a `send(*, *, {create_promise: true})`
const on_update = (update, by_me) => {
if (this.state.notebook.notebook_id === update.notebook_id) {
const show_debugs = launch_params.binder_url != null
if (show_debugs) console.debug("on_update", update, by_me)
const message = update.message
switch (update.type) {
case "integrations":
let event = new IntegrationsMessageToClientEvent({
module_name: message.module_name,
body: message.body,
})
// @ts-ignore
window.Pluto.pluto_api_event_target.dispatchEvent(event)

// @ts-ignore
if (event.handled == false) {
console.warn(`Unknown integrations message "${message.module_name}"`)
}
break
case "notebook_diff":
let apply_promise = Promise.resolve()
if (message?.response?.from_reset) {
Expand Down
21 changes: 21 additions & 0 deletions src/evaluation/WorkspaceManager.jl
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ function make_workspace((session, notebook)::SN; is_offline_renderer::Bool=false
run_channel = Core.eval(Main, quote
$(Distributed).RemoteChannel(() -> eval(:(Main.PlutoRunner.run_channel)), $pid)
end)

integrations_channel = Core.eval(Main, quote
$(Distributed).RemoteChannel(() -> eval(:(Main.PlutoRunner.IntegrationsWithOtherPackages.message_channel)), $pid)
end)

module_name = create_emptyworkspacemodule(pid)

Expand All @@ -104,6 +108,8 @@ function make_workspace((session, notebook)::SN; is_offline_renderer::Bool=false

@async start_relaying_logs((session, notebook), remote_log_channel)
@async start_relaying_self_updates((session, notebook), run_channel)
@async start_relaying_integrations((session, notebook), integrations_channel)

cd_workspace(workspace, notebook.path)
use_nbpkg_environment((session, notebook), workspace)

Expand Down Expand Up @@ -224,6 +230,21 @@ function start_relaying_logs((session, notebook)::SN, log_channel::Distributed.R
end
end

function start_relaying_integrations((session, notebook)::SN, channel::Distributed.RemoteChannel)
while true
try
next_message = take!(channel)
putnotebookupdates!(session, notebook, UpdateMessage(:integrations, next_message, notebook))
catch e
if !isopen(channel)
break
end
@error "Failed to relay integrations message" exception=(e, catch_backtrace())
end
end
end


"Call `cd(\$path)` inside the workspace. This is done when creating a workspace, and whenever the notebook changes path."
function cd_workspace(workspace, path::AbstractString)
eval_in_workspace(workspace, quote
Expand Down
53 changes: 53 additions & 0 deletions src/runner/PlutoRunner.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1637,6 +1637,59 @@ function load_integration(integration::Integration)
end
end

module IntegrationsWithOtherPackages

import ..notebook_id

export handle_websocket_message, message_channel
"Called directly (through Distributed) from the main Pluto process"
function handle_websocket_message(message)
try
result = on_websocket_message(Val(Symbol(message[:module_name])), message[:body])
if result !== nothing
@warn """
Integrations `on_websocket_message(:$(message[:module_name]), ...)` returned a value, but is expected to return `nothing`.

If you want to send something back to the client, use `IntegrationsWithOtherPackages.message_channel`.
"""
end
catch ex
bt = stacktrace(catch_backtrace())
@error "Dispatching integrations websocket message failed:" message=message exception=(ex, bt)
end
nothing
end

"""
A [`Channel`](@ref) to send messages on demand to JS running in cell outputs. The message should be structured like the example below, and you can use any `MsgPack.jl`-encodable object in the body (including a `Vector{UInt8}` if that's your thing 👀).

# Example
```julia
put!(message_channel, Dict(
:module_name => "MyPackage",
:body => mydata,
))
```
"""
const message_channel = Channel{Dict{Symbol,Any}}(10)

"""
Integrations should implement this to capture incoming websocket messages.
We force returning nothing, because returning might give you the idea that
the result is sent back to the client, which (right now) it isn't.
If you want to send something back to the client, use [`IntegrationsWithOtherPackages.message_channel`](@ref).

Do not call this function directly from notebook/package code!

function on_websocket_message(::Val{:MyModule}, body)::Nothing
# ...
end
"""
function on_websocket_message(module_name, body)::Nothing
error("No websocket message handler defined for '$(module_name)'")
end

end

###
# REPL THINGS
Expand Down
15 changes: 15 additions & 0 deletions src/webserver/Dynamic.jl
Original file line number Diff line number Diff line change
Expand Up @@ -509,3 +509,18 @@ responses[:pkg_update] = function response_pkg_update(🙋::ClientRequest)
update_nbpkg(🙋.session, 🙋.notebook)
putclientupdates!(🙋.session, 🙋.initiator, UpdateMessage(:🦆, Dict(), nothing, nothing, 🙋.initiator))
end

# Third party messages, passing on to handlers inside the PlutoRunner process
responses[:integrations_message_to_server] = function response_integrations(🙋::ClientRequest)
@assert (haskey(🙋.body, "module_name")) "Integrations message needs a `module_name` property"
@assert (haskey(🙋.body, "body")) "Integrations message needs a `body` property"

# Transform as Dict because Distributed doesn't understand ANYTHING
message = Dict(
:module_name => 🙋.body["module_name"],
:body => 🙋.body["body"],
)
WorkspaceManager.eval_in_workspace((🙋.session, 🙋.notebook), quote
Main.PlutoRunner.IntegrationsWithOtherPackages.handle_websocket_message($(message))
end)
end