diff --git a/Project.toml b/Project.toml
index 8fbe224..83bf6af 100644
--- a/Project.toml
+++ b/Project.toml
@@ -9,6 +9,7 @@ Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
BetterFileWatching = "c9fd44ac-77b5-486c-9482-9798bd063cc6"
Configurations = "5218b696-f38b-4ac9-8b61-a12ec717816d"
Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b"
+Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
FromFile = "ff7dd447-1dcb-4ce3-b8ac-22a812192de7"
Git = "d7ba0133-e1db-5d97-8f8c-041e4b3a1eb2"
GitHubActions = "6b79fd1a-b13a-48ab-b6b0-aaee1fee41df"
@@ -16,10 +17,14 @@ Glob = "c27321d9-0574-5035-807b-f59d2c89b15c"
HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
+Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a"
+OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d"
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
Pluto = "c3e4b0f8-55cb-11ea-2926-15256bba5781"
+Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce"
Sockets = "6462fe0b-24de-5631-8697-dd941f90decc"
+Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76"
TerminalLoggers = "5d786b92-1e48-4d6f-9151-6b4477ca9bed"
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
@@ -29,18 +34,21 @@ AbstractPlutoDingetjes = "1.1"
BetterFileWatching = "^0.1.2"
Configurations = "0.16, 0.17"
FromFile = "0.1"
+Distributions = "0.25"
Git = "1"
GitHubActions = "0.1"
Glob = "1"
HTTP = "^1.0.2"
+OrderedCollections = "1"
JSON = "0.21"
Pluto = "0.19.18"
TerminalLoggers = "0.1"
julia = "1.6"
[extras]
-Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
+Deno_jll = "04572ae6-984a-583e-9378-9577a1c2574d"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
+Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
[targets]
-test = ["Test", "Random"]
+test = ["Deno_jll", "Test", "Random"]
diff --git a/src/Actions.jl b/src/Actions.jl
index a9a8fd2..9dd5aac 100644
--- a/src/Actions.jl
+++ b/src/Actions.jl
@@ -1,12 +1,15 @@
-import Pluto: Pluto, without_pluto_file_extension, generate_html, @asynclog
+import Pluto:
+ Pluto, without_pluto_file_extension, generate_html, @asynclog, withtoken, Firebasey
using Base64
using FromFile
+import HTTP.URIs
@from "./MoreAnalysis.jl" import bound_variable_connections_graph
@from "./Export.jl" import try_get_exact_pluto_version, try_fromcache, try_tocache
@from "./Types.jl" import NotebookSession, RunningNotebook, FinishedNotebook, RunResult
@from "./Configuration.jl" import PlutoDeploySettings, is_glob_match
@from "./FileHelpers.jl" import find_notebook_files_recursive
+@from "./precomputed/index.jl" import generate_precomputed_staterequests
@from "./PlutoHash.jl" import plutohash
@@ -64,8 +67,12 @@ function process(
end
keep_running =
- settings.SliderServer.enabled && !is_glob_match(path, settings.SliderServer.exclude)
- skip_cache = keep_running || is_glob_match(path, settings.Export.ignore_cache)
+ (settings.SliderServer.enabled || settings.Precompute.enabled) &&
+ !is_glob_match(path, settings.SliderServer.exclude)
+ skip_cache =
+ keep_running ||
+ is_glob_match(path, settings.Export.ignore_cache) ||
+ path ∈ settings.Export.ignore_cache
cached_state = skip_cache ? nothing : try_fromcache(settings.Export.cache_dir, new_hash)
@@ -115,9 +122,25 @@ function process(
)
end
+ new_session = NotebookSession(;
+ path=s.path,
+ current_hash=new_hash,
+ desired_hash=s.desired_hash,
+ run=run,
+ )
+ if settings.Precompute.enabled
+ generate_precomputed_staterequests(
+ new_session;
+ settings,
+ pluto_session=server_session,
+ output_dir,
+ )
+ # TODO shutdown
+ end
+
@info "### ✓ $(progress) Ready" s.path new_hash
- NotebookSession(; path, current_hash=new_hash, desired_hash=s.desired_hash, run)
+ new_session
end
###
@@ -205,7 +228,11 @@ function generate_static_export(
slider_server_running_somewhere =
settings.Export.slider_server_url !== nothing ||
- (settings.SliderServer.serve_static_export_folder && settings.SliderServer.enabled)
+ (
+ settings.SliderServer.serve_static_export_folder &&
+ settings.SliderServer.enabled
+ ) ||
+ settings.Precompute.enabled
notebookfile_js = if settings.Export.offer_binder || slider_server_running_somewhere
if settings.Export.baked_notebookfile
@@ -300,4 +327,7 @@ function remove_static_export(path; settings, output_dir)
!settings.Export.baked_notebookfile
tryrm(export_jl_path)
end
-end
\ No newline at end of file
+
+
+
+end
diff --git a/src/Configuration.jl b/src/Configuration.jl
index 1abf873..6228cff 100644
--- a/src/Configuration.jl
+++ b/src/Configuration.jl
@@ -1,7 +1,8 @@
using Configurations
import TOML
import Pluto
-export SliderServerSettings, ExportSettings, PlutoDeploySettings, get_configuration
+export SliderServerSettings,
+ ExportSettings, PrecomputeSettings, PlutoDeploySettings, get_configuration
using TerminalLoggers: TerminalLogger
using Logging: global_logger
using FromFile
@@ -24,6 +25,17 @@ import Glob
simulated_lag::Real = 0
end
+
+@extract_docs @option struct PrecomputeSettings
+ "Precompute slider server requests?"
+ enabled::Bool = false
+ "List of notebook files to skip precomputation. Provide paths relative to `start_dir`."
+ exclude::Vector{String} = String[]
+ "Whether or not to partially precompute notebooks. If `true`, notebooks will only be precomputed if **all** their sliders can be precomputed"
+ only_fully::Bool = false
+ max_filesize_per_group::Integer = 1_000_000
+end
+
@extract_docs @option struct ExportSettings
"Generate static HTML files? This setting can only be `false` if you are also running a slider server."
enabled::Bool = true
@@ -57,6 +69,7 @@ end
@option struct PlutoDeploySettings
SliderServer::SliderServerSettings = SliderServerSettings()
Export::ExportSettings = ExportSettings()
+ Precompute::PrecomputeSettings = PrecomputeSettings()
Pluto::Pluto.Configuration.Options = Pluto.Configuration.Options()
end
diff --git a/src/HTTPRouter.jl b/src/HTTPRouter.jl
index fc5478c..71b1511 100644
--- a/src/HTTPRouter.jl
+++ b/src/HTTPRouter.jl
@@ -12,6 +12,7 @@ using HTTP
using Sockets
import JSON
+@from "./run_bonds.jl" import run_bonds_get_patches
@from "./IndexJSON.jl" import generate_index_json
@from "./IndexHTML.jl" import temp_index, generate_basic_index_html
@from "./Types.jl" import NotebookSession, RunningNotebook
@@ -35,7 +36,7 @@ function make_router(
)
router = HTTP.Router()
- function get_sesh(request::HTTP.Request)
+ function get_sesh(request::HTTP.Request)::Union{Nothing,NotebookSession}
uri = HTTP.URI(request.target)
parts = HTTP.URIs.splitpath(uri.path)
@@ -62,7 +63,7 @@ function make_router(
end
end
- function get_bonds(request::HTTP.Request)
+ function get_bonds(request::HTTP.Request)::Dict{Symbol,Any}
request_body = if request.method == "POST"
IOBuffer(HTTP.payload(request))
elseif request.method == "GET"
@@ -104,67 +105,17 @@ function make_router(
lag > 0 && sleep(lag)
end
- topological_order, new_state = withtoken(sesh.run.token) do
- try
- notebook.bonds = bonds
+ ##
+ result = run_bonds_get_patches(server_session, sesh.run, bonds)
+ ##
- names::Vector{Symbol} = Symbol.(keys(bonds))
-
- topological_order = Pluto.set_bond_values_reactive(
- session=server_session,
- notebook=notebook,
- bound_sym_names=names,
- is_first_values=[false for _n in names], # because requests should be stateless. We might want to do something special for the (actual) initial request (containing every initial bond value) in the future.
- run_async=false,
- )::Pluto.TopologicalOrder
-
- new_state = Pluto.notebook_to_js(notebook)
-
- topological_order, new_state
- catch e
- @error "Failed to set bond values" exception = (e, catch_backtrace())
- nothing, nothing
- end
- end
- topological_order === nothing && return (
+ if result === nothing
HTTP.Response(500, "Failed to set bond values") |>
with_cors! |>
with_not_cacheable!
- )
-
- ids_of_cells_that_ran = [c.cell_id for c in topological_order.runnable]
-
- @debug "Finished running!" length(ids_of_cells_that_ran)
-
- # We only want to send state updates about...
- function only_relevant(state)
- new = copy(state)
- # ... the cells that just ran and ...
- new["cell_results"] = filter(state["cell_results"]) do (id, cell_state)
- id ∈ ids_of_cells_that_ran
- end
- # ... nothing about bond values, because we don't want to synchronize among clients. and...
- delete!(new, "bonds")
- # ... we ignore changes to the status tree caused by a running bonds.
- delete!(new, "status_tree")
- new
end
- patches = Firebasey.diff(
- only_relevant(sesh.run.original_state),
- only_relevant(new_state),
- )
- patches_as_dicts::Array{Dict} = Firebasey._convert(Array{Dict}, patches)
-
- HTTP.Response(
- 200,
- Pluto.pack(
- Dict{String,Any}(
- "patches" => patches_as_dicts,
- "ids_of_cells_that_ran" => ids_of_cells_that_ran,
- ),
- ),
- ) |>
+ HTTP.Response(200, Pluto.pack(result)) |>
with_cacheable! |>
with_cors! |>
with_msgpack!
diff --git a/src/PlutoSliderServer.jl b/src/PlutoSliderServer.jl
index fab0498..1d8312d 100644
--- a/src/PlutoSliderServer.jl
+++ b/src/PlutoSliderServer.jl
@@ -17,6 +17,7 @@ using FromFile
@from "./ReloadFolder.jl" import update_sessions!, select
@from "./HTTPRouter.jl" import make_router, ReferrerMiddleware
@from "./gitpull.jl" import fetch_pull
+@from "./precomputed/debug.jl" import start_debugging
@from "./PlutoHash.jl" import plutohash, base64urlencode, base64urldecode
export plutohash, base64urlencode, base64urldecode
@@ -528,4 +529,5 @@ function kind_of_debounced(f)
end
+
end
diff --git a/src/precomputed/debug.jl b/src/precomputed/debug.jl
new file mode 100644
index 0000000..a4b1ca4
--- /dev/null
+++ b/src/precomputed/debug.jl
@@ -0,0 +1,62 @@
+
+# this file is included in src/PlutoSliderServer.jl
+# yolo
+
+import Pluto: Pluto, @asynclog, tamepath, is_pluto_notebook
+import Configurations
+using FromFile
+
+@from "./index.jl" import variable_groups, generate_precomputed_staterequests_report
+@from "../Types.jl" import RunningNotebook
+@from "../Configuration.jl" import PlutoDeploySettings
+@from "../MoreAnalysis.jl" import bound_variable_connections_graph
+
+
+function start_debugging(notebook_path::String; kwargs...)
+ notebook_path = tamepath(notebook_path)
+ @assert is_pluto_notebook(notebook_path)
+
+ settings = Configurations.from_kwargs(PlutoDeploySettings; kwargs...)
+
+
+ @info "Running notebook..."
+
+ pluto_session = Pluto.ServerSession(; options=settings.Pluto)
+ notebook = Pluto.SessionActions.open(pluto_session, notebook_path; run_async=false)
+
+ @info "Notebook ready! Starting server..."
+
+ pluto_session.options.server.show_file_system = false
+ t = @asynclog Pluto.run(pluto_session)
+ sleep(1)
+
+ repeat = true
+ while repeat
+ connections = bound_variable_connections_graph(notebook)
+
+ run = RunningNotebook(;
+ path=notebook_path,
+ notebook=notebook,
+ bond_connections=connections,
+ original_state=Pluto.notebook_to_js(notebook),
+ )
+
+ groups = variable_groups(connections; pluto_session, notebook=run.notebook)
+
+ report =
+ generate_precomputed_staterequests_report(groups, run; settings, pluto_session)
+
+ for _ = 1:first(displaysize(stdout))
+ println(stdout)
+ end
+ show(stdout, MIME"text/plain"(), report)
+ println(stdout)
+ println(stdout)
+
+
+ repeat = Base.prompt("Run again? (y/n)"; default="y") == "y"
+ end
+
+ wait(t)
+
+end
\ No newline at end of file
diff --git a/src/precomputed/index.jl b/src/precomputed/index.jl
new file mode 100644
index 0000000..4a64a8f
--- /dev/null
+++ b/src/precomputed/index.jl
@@ -0,0 +1,188 @@
+import Pluto
+using Base64
+using OrderedCollections
+using FromFile
+import HTTP.URIs
+import Random
+import Statistics
+using Distributions
+import Markdown
+
+# @from "../MoreAnalysis.jl" import bound_variable_connections_graph
+@from "../Types.jl" import NotebookSession, RunningNotebook
+@from "../Configuration.jl" import PlutoDeploySettings
+@from "../run_bonds.jl" import run_bonds_get_patches
+@from "../PlutoHash.jl" import base64urlencode
+
+@from "./types.jl" import VariableGroupPossibilities, PrecomputedSampleReport, Reason
+
+
+function variable_groups(
+ connections;
+ pluto_session::Pluto.ServerSession,
+ notebook::Pluto.Notebook,
+)::Vector{VariableGroupPossibilities}
+ VariableGroupPossibilities[
+ let
+ names = sort(variable_group)
+
+ not_available = Dict{Symbol,Reason}()
+
+ possible_values = [
+ let
+ result = try
+ Pluto.possible_bond_values(
+ pluto_session::Pluto.ServerSession,
+ notebook::Pluto.Notebook,
+ n::Symbol,
+ )
+ catch e
+ Symbol("Failed ", string(e))
+ end
+ if result isa Symbol
+ # @error "Failed to get possible values for $(n)" result
+ not_available[n] = result
+ []
+ else
+ result
+ end
+ end for n in names
+ ]
+
+ VariableGroupPossibilities(;
+ names=names,
+ possible_values=possible_values,
+ not_available=not_available,
+ num_possibilities=prod(BigInt.(length.(possible_values))),
+ )
+ end for variable_group in filter(!isempty, Set(values(connections)))
+ ]
+end
+
+
+function combination_iterator(group::VariableGroupPossibilities)
+ Iterators.map(Iterators.product(group.possible_values...)) do combination
+ bonds_dict = OrderedDict{Symbol,Any}(
+ n => OrderedDict{String,Any}("value" => v) for
+ (n, v) in zip(group.names, combination)
+ )
+
+ return (combination, bonds_dict)
+ end
+end
+
+biglength(pr::Iterators.ProductIterator) = prod(BigInt[biglength(i) for i in pr.iterators])
+biglength(g::Base.Generator) = biglength(g.iter)
+biglength(x) = BigInt(length(x))
+
+function generate_precomputed_staterequests_report(
+ groups::Vector{VariableGroupPossibilities},
+ run::RunningNotebook;
+ settings::PlutoDeploySettings,
+ pluto_session::Pluto.ServerSession,
+)::PrecomputedSampleReport
+ map(groups) do group
+ stat = if !isempty(group.not_available)
+ Normal(0.0, 0.0)
+ else
+ iterator = combination_iterator(group)
+ if isempty(iterator) || isempty(group.names)
+ Normal(0.0, 0.0)
+ else
+ file_size_sample =
+ map(
+ rand(iterator, length(group.names) * 3),
+ ) do (combination, bonds_dict)
+
+ result = run_bonds_get_patches(pluto_session, run, bonds_dict)
+
+ if result !== nothing
+ length(Pluto.pack(result))
+ else
+ 0
+ end |> BigInt
+ end .* biglength(iterator) # multiply by number of combinations to get an estimate of the total file size
+
+ fit(Normal, file_size_sample)
+ end
+ end
+ VariableGroupPossibilities(;
+ names=group.names,
+ possible_values=group.possible_values,
+ num_possibilities=group.num_possibilities,
+ not_available=group.not_available,
+ file_size_sample_distribution=stat,
+ settings,
+ )
+ end |> PrecomputedSampleReport
+end
+
+
+function generate_precomputed_staterequests(
+ notebook_session::NotebookSession;
+ settings::PlutoDeploySettings,
+ pluto_session::Pluto.ServerSession,
+ output_dir=".",
+)
+
+ sesh = notebook_session
+ run = sesh.run
+ connections = run.bond_connections
+ current_hash = sesh.current_hash
+
+ @assert run isa RunningNotebook
+
+ mkpath(joinpath(output_dir, "bondconnections"))
+ mkpath(joinpath(output_dir, "staterequest", URIs.escapeuri(current_hash)))
+
+ bondconnections_path =
+ joinpath(output_dir, "bondconnections", URIs.escapeuri(current_hash))
+ write(bondconnections_path, Pluto.pack(run.bond_connections))
+ @debug "Written bond connections to " bondconnections_path
+
+ unanalyzed_groups = variable_groups(connections; pluto_session, notebook=run.notebook)
+
+ report = generate_precomputed_staterequests_report(
+ unanalyzed_groups,
+ run;
+ settings,
+ pluto_session,
+ )
+ groups = report.groups
+
+ if report.judgement.should_precompute_all
+ @info "Notebook can be fully precomputed!" report
+ elseif settings.Precompute.only_fully
+ @warn "Notebook cannot be fully precomputed - skipping it!" report
+ return
+ else
+ @warn "Notebook cannot be (fully) precomputed" report
+ end
+
+ foreach(groups) do group::VariableGroupPossibilities
+ if group.judgement.should_precompute_all
+ for (combination, bonds_dict) in combination_iterator(group)
+ filename = Pluto.pack(bonds_dict) |> base64urlencode
+ if length(filename) > 255
+ @warn "Filename is too long, stopping this group" group.names
+ break
+ end
+ result = run_bonds_get_patches(pluto_session, run, bonds_dict)
+
+ if result !== nothing
+ write_path = joinpath(
+ output_dir,
+ "staterequest",
+ URIs.escapeuri(current_hash),
+ filename,
+ )
+
+ write(write_path, Pluto.pack(result))
+
+ @debug "Written state request to " write_path values =
+ (; (zip(group.names, combination))...)
+ end
+ end
+ end
+ end
+end
diff --git a/src/precomputed/types.jl b/src/precomputed/types.jl
new file mode 100644
index 0000000..05e6e39
--- /dev/null
+++ b/src/precomputed/types.jl
@@ -0,0 +1,211 @@
+
+using FromFile
+import Random
+import Statistics
+using Distributions
+import Markdown
+
+@from "../Configuration.jl" import PlutoDeploySettings
+
+const Reason = Symbol
+
+Base.@kwdef struct Judgement
+ should_precompute_all::Bool = false
+ not_available::Bool = false
+ close_to_filesize_limit::Bool = false
+ exceeds_filesize_limit::Bool = false
+end
+
+struct VariableGroupPossibilities
+ names::Vector{Symbol}
+ possible_values::Vector
+ not_available::Dict{Symbol,Reason}
+ # size info:
+ file_size_sample_distribution::Union{Nothing,Distribution}
+ num_possibilities::BigInt
+
+ judgement::Judgement
+end
+
+function VariableGroupPossibilities(;
+ names::Vector{Symbol},
+ possible_values::Vector,
+ not_available::Dict{Symbol,Reason},
+ # size info:
+ num_possibilities::BigInt,
+ file_size_sample_distribution::Union{Nothing,Distribution}=nothing,
+ settings::Union{Nothing,PlutoDeploySettings}=nothing,
+)
+ is_not_available = !isempty(not_available)
+
+ if !isa(file_size_sample_distribution, Nothing)
+ @assert settings isa PlutoDeploySettings
+
+ limit = settings.Precompute.max_filesize_per_group
+ current = mean(file_size_sample_distribution)
+
+ exceeds_filesize_limit = current > limit
+ close_to_filesize_limit = current > limit * 0.7
+ else
+ exceeds_filesize_limit = close_to_filesize_limit = false
+ end
+
+ j = Judgement(;
+ should_precompute_all=!is_not_available && !exceeds_filesize_limit,
+ exceeds_filesize_limit,
+ close_to_filesize_limit,
+ not_available=is_not_available,
+ )
+
+ VariableGroupPossibilities(
+ names,
+ possible_values,
+ not_available,
+ file_size_sample_distribution,
+ num_possibilities,
+ j,
+ )
+end
+
+Base.@kwdef struct PrecomputedSampleReport
+ groups::Vector{VariableGroupPossibilities}
+ # size info:
+ file_size_sample_distribution::Union{Nothing,Distribution} = nothing
+ num_possibilities::BigInt
+ judgement::Judgement
+end
+
+function PrecomputedSampleReport(groups::Vector{VariableGroupPossibilities})
+ num_possibilities = sum(BigInt[g.num_possibilities for g in groups])
+
+ file_size_sample_distribution =
+ map(groups) do group
+ group.file_size_sample_distribution
+ end |> sum_distributions
+
+ judgement = Judgement(;
+ should_precompute_all=all(g.judgement.should_precompute_all for g in groups),
+ not_available=any(g.judgement.not_available for g in groups),
+ exceeds_filesize_limit=any(g.judgement.exceeds_filesize_limit for g in groups),
+ close_to_filesize_limit=any(g.judgement.close_to_filesize_limit for g in groups),
+ )
+
+ PrecomputedSampleReport(;
+ groups,
+ num_possibilities,
+ file_size_sample_distribution,
+ judgement,
+ )
+end
+
+function exceeds_limit(j::Judgement, prefix::String="")
+
+ j.exceeds_filesize_limit ? "*($(prefix)exceeding filesize limit)*" :
+ j.close_to_filesize_limit ? "*($(prefix)close to filesize limit)*" : ""
+end
+
+
+function Base.show(io::IO, m::MIME"text/plain", p::PrecomputedSampleReport)
+ groups = sort(p.groups; by=g -> mean(g.file_size_sample_distribution), rev=true)
+
+ r = Markdown.parse(
+ """
+# Precomputed state summary
+
+Total size estimate (based on a radom sample): $(p.num_possibilities) files, $(
+ p.file_size_sample_distribution |> format_filesize
+) $(exceeds_limit(p.judgement, "some groups are "))
+
+$(map(groups) do group
+total_size_dist = group.file_size_sample_distribution
+
+"""
+## $(pretty(group.judgement)) Group: $(join(["`$(n)`" for n in group.names], ", "))
+
+$(if isempty(group.not_available)
+ """
+ Size estimate for this group (based on a radom sample): $(
+ group.num_possibilities
+ ) files, $(
+ format_filesize(total_size_dist)
+ ) $(exceeds_limit(group.judgement))
+
+ | Name | Possible values |
+ |---|---|
+ $(map(zip(group.names, group.possible_values)) do (n, vs)
+ "| `$(n)` | **$(length(vs))** | \n"
+ end |> join)
+ """
+else
+ notgivens = [k for (k,v) in group.not_available if v == :NotGiven]
+ infinites = [k for (k,v) in group.not_available if v == :InfinitePossibilities]
+ remainder = setdiff(keys(group.not_available), notgivens ∪ infinites)
+
+ """
+ This group could not be precomputed because:
+ $(
+ isempty(notgivens) ? "" : "- The set of possible values for $(join(("`$(s)`" for s in notgivens), ", ")) is not known. If you are using PlutoUI, be sure to use an up-to-date version. If this input element is custom-made, take a look at `AbstractPlutoDingetjes.jl`. \n"
+ )$(
+ isempty(infinites) ? "" : "- The set of possible values for $(join(("`$(s)`" for s in infinites), ", ")) is infinite. \n"
+ )$(
+ isempty(remainder) ? "" : "- The set of possible values for $(join(("`$(s)`" for s in remainder), ", ")) could not be determined because of an unknown reason: `$(
+ join((group.not_available[k] for k in remainder), ", ")
+ )`. \n"
+ )
+ """
+end)
+
+"""
+end |> join)
+""",
+ )
+ show(io, m, r)
+end
+
+
+format_filesize(x::Real) = isnan(x) ? "NaN" : try
+ Base.format_bytes(floor(BigInt, x))
+catch
+ "$(x / 1e6) MB"
+end
+
+function format_filesize(x::Distribution)
+ m, s = mean(x), std(x)
+ if s / m > 0.05
+ "$(format_filesize(m)) (𝜎 $(format_filesize(s)))"
+ else
+ format_filesize(m)
+ end
+end
+
+
+
+
+# Some missing functionality
+
+
+function Random.rand(
+ rng::Random.AbstractRNG,
+ iterator::Random.SamplerTrivial{Base.Iterators.ProductIterator{T}},
+) where {T}
+ r(x) = rand(rng, x)
+ r.(iterator[].iterators)
+end
+
+function Random.rand(
+ rng::Random.AbstractRNG,
+ iterator::Random.SamplerTrivial{Base.Generator{T,F}},
+) where {T,F}
+ iterator[].f(rand(rng, iterator[].iter))
+end
+
+sum_distributions(ds; init=Normal(0, 0)) =
+ any(isnothing, ds) ? nothing : reduce(convolve, ds; init=init)
+
+
+pretty(j::Judgement) =
+ if j.should_precompute_all
+ j.close_to_filesize_limit ? "⚠️" : "✓"
+ else
+ "❌"
+ end
diff --git a/src/run_bonds.jl b/src/run_bonds.jl
new file mode 100644
index 0000000..de5c937
--- /dev/null
+++ b/src/run_bonds.jl
@@ -0,0 +1,66 @@
+import Pluto:
+ Pluto, without_pluto_file_extension, generate_html, @asynclog, withtoken, Firebasey
+using Base64
+using SHA
+using FromFile
+
+@from "./Types.jl" import RunningNotebook
+
+function run_bonds_get_patches(
+ server_session::Pluto.ServerSession,
+ run::RunningNotebook,
+ bonds::AbstractDict{Symbol,<:Any},
+)::Union{AbstractDict{String,Any},Nothing}
+
+ notebook = run.notebook
+
+ topological_order, new_state = withtoken(run.token) do
+ try
+ notebook.bonds = bonds
+
+ names::Vector{Symbol} = Symbol.(keys(bonds))
+
+ topological_order = Pluto.set_bond_values_reactive(
+ session=server_session,
+ notebook=notebook,
+ bound_sym_names=names,
+ is_first_values=[false for _n in names], # because requests should be stateless. We might want to do something special for the (actual) initial request (containing every initial bond value) in the future.
+ run_async=false,
+ )::Pluto.TopologicalOrder
+
+ new_state = Pluto.notebook_to_js(notebook)
+
+ topological_order, new_state
+ catch e
+ @error "Failed to set bond values" exception = (e, catch_backtrace())
+ nothing, nothing
+ end
+ end
+ if topological_order === nothing
+ return nothing
+ end
+
+ ids_of_cells_that_ran = [c.cell_id for c in topological_order.runnable]
+
+ @debug "Finished running!" length(ids_of_cells_that_ran)
+
+ # We only want to send state updates about...
+ function only_relevant(state)
+ new = copy(state)
+ # ... the cells that just ran and ...
+ new["cell_results"] = filter(state["cell_results"]) do (id, cell_state)
+ id ∈ ids_of_cells_that_ran
+ end
+ # ... nothing about bond values, because we don't want to synchronize among clients.
+ new["bonds"] = Dict{String,Dict{String,Any}}()
+ new
+ end
+
+ patches = Firebasey.diff(only_relevant(run.original_state), only_relevant(new_state))
+ patches_as_dicts::Array{Dict} = Firebasey._convert(Array{Dict}, patches)
+
+ Dict{String,Any}(
+ "patches" => patches_as_dicts,
+ "ids_of_cells_that_ran" => ids_of_cells_that_ran,
+ )
+end
diff --git a/test/basic3.jl b/test/basic3.jl
new file mode 100644
index 0000000..9bca2ce
--- /dev/null
+++ b/test/basic3.jl
@@ -0,0 +1,275 @@
+### A Pluto.jl notebook ###
+# v0.19.26
+
+using Markdown
+using InteractiveUtils
+
+# This Pluto notebook uses @bind for interactivity. When running this notebook outside of Pluto, the following 'mock version' of @bind gives bound variables a default value (instead of an error).
+macro bind(def, element)
+ quote
+ local iv = try Base.loaded_modules[Base.PkgId(Base.UUID("6e696c72-6542-2067-7265-42206c756150"), "AbstractPlutoDingetjes")].Bonds.initial_value catch; b -> missing; end
+ local el = $(esc(element))
+ global $(esc(def)) = Core.applicable(Base.get, el) ? Base.get(el) : iv(el)
+ el
+ end
+end
+
+# ╔═╡ 22dc8ce8-392e-4202-a549-f0fd46152322
+import AbstractPlutoDingetjes.Bonds
+
+# ╔═╡ 9b342ef4-fff9-46d0-b316-cc383fe71a59
+begin
+ struct CoolSlider
+ max
+ end
+ function Base.show(io::IO, ::MIME"text/html", s::CoolSlider)
+ write(io, "")
+ end
+ Bonds.initial_value(::CoolSlider) = 1
+ Bonds.possible_values(s::CoolSlider) = 1:s.max
+end
+
+# ╔═╡ 4a683caa-6910-4922-bda4-c3a00950a14b
+begin
+ struct CoolText
+ end
+ function Base.show(io::IO, ::MIME"text/html", s::CoolText)
+ write(io, "")
+ end
+ Bonds.initial_value(::CoolText) = ""
+ Bonds.possible_values(s::CoolText) = Bonds.InfinitePossibilities()
+end
+
+# ╔═╡ 635e5ebc-6567-11eb-1d9d-f98bfca7ec27
+@bind x CoolSlider(10)
+
+# ╔═╡ ad853ac9-a8a0-44ef-8d41-c8cea165ad57
+@bind y CoolSlider(20)
+
+# ╔═╡ 26025270-9b5e-4841-b295-0c47437bc7db
+repeat(string(x), y)
+
+# ╔═╡ 8fb4ff71-6f86-4643-a0af-6b6665d63634
+
+
+# ╔═╡ ea600f99-b634-492f-a1d6-7137ff896e84
+@bind z CoolSlider(15)
+
+# ╔═╡ 815f3a2d-b5f6-4b5d-84ec-fa660c3dbfe8
+z * 100
+
+# ╔═╡ 1352da54-e567-4f59-a3da-19ed3f4bb7c7
+
+
+# ╔═╡ dca76d5d-bb71-4a81-9f9e-19ddfbc258cd
+@bind a1 CoolSlider(10)
+
+# ╔═╡ a3ab6582-3d30-4594-8eb6-17f3c4103076
+@bind a2 CoolSlider(10)
+
+# ╔═╡ 6ee0893c-45f3-490e-a53c-7b3d92bc8f70
+f = () -> a1 + a2
+
+# ╔═╡ 7ad5c51b-76e4-44d3-aeca-f81a8f22f8b4
+f()
+
+# ╔═╡ 1681132d-050d-4ef7-9caa-14be6eade03d
+
+
+# ╔═╡ 6ca3eca5-2a0b-4c9c-84c7-307fd4a29eae
+
+
+# ╔═╡ 09ae27fa-525a-4211-b252-960cdbaf1c1e
+b = @bind s html""
+
+# ╔═╡ ed78a6f1-d282-4d80-8f42-40701aeadb52
+b
+
+# ╔═╡ cca2f726-0c25-43c6-85e4-c16ec192d464
+s
+
+# ╔═╡ c4f51980-3c30-4d3f-a76a-fc0f0fe16944
+
+
+# ╔═╡ 8f0bd329-36b8-45ed-b80d-24661242129a
+b2 = @bind s2 CoolText()
+
+# ╔═╡ c55a107f-5d7d-4396-b597-8c1ae07c35be
+b2
+
+# ╔═╡ a524ff27-a6a3-4f14-8ed4-f55700647bc4
+sleep(1); s2
+
+# ╔═╡ 00000000-0000-0000-0000-000000000001
+PLUTO_PROJECT_TOML_CONTENTS = """
+[deps]
+AbstractPlutoDingetjes = "6e696c72-6542-2067-7265-42206c756150"
+
+[compat]
+AbstractPlutoDingetjes = "~1.1.2"
+"""
+
+# ╔═╡ 00000000-0000-0000-0000-000000000002
+PLUTO_MANIFEST_TOML_CONTENTS = """
+# This file is machine-generated - editing it directly is not advised
+
+[[AbstractPlutoDingetjes]]
+deps = ["Pkg"]
+git-tree-sha1 = "abb72771fd8895a7ebd83d5632dc4b989b022b5b"
+uuid = "6e696c72-6542-2067-7265-42206c756150"
+version = "1.1.2"
+
+[[ArgTools]]
+uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f"
+version = "1.1.1"
+
+[[Artifacts]]
+uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33"
+
+[[Base64]]
+uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
+
+[[Dates]]
+deps = ["Printf"]
+uuid = "ade2ca70-3891-5945-98fb-dc099432e06a"
+
+[[Downloads]]
+deps = ["ArgTools", "FileWatching", "LibCURL", "NetworkOptions"]
+uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6"
+version = "1.6.0"
+
+[[FileWatching]]
+uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee"
+
+[[InteractiveUtils]]
+deps = ["Markdown"]
+uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240"
+
+[[LibCURL]]
+deps = ["LibCURL_jll", "MozillaCACerts_jll"]
+uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21"
+version = "0.6.3"
+
+[[LibCURL_jll]]
+deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll", "Zlib_jll", "nghttp2_jll"]
+uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0"
+version = "7.84.0+0"
+
+[[LibGit2]]
+deps = ["Base64", "NetworkOptions", "Printf", "SHA"]
+uuid = "76f85450-5226-5b5a-8eaa-529ad045b433"
+
+[[LibSSH2_jll]]
+deps = ["Artifacts", "Libdl", "MbedTLS_jll"]
+uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8"
+version = "1.10.2+0"
+
+[[Libdl]]
+uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb"
+
+[[Logging]]
+uuid = "56ddb016-857b-54e1-b83d-db4d58db5568"
+
+[[Markdown]]
+deps = ["Base64"]
+uuid = "d6f4376e-aef5-505a-96c1-9c027394607a"
+
+[[MbedTLS_jll]]
+deps = ["Artifacts", "Libdl"]
+uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1"
+version = "2.28.2+0"
+
+[[MozillaCACerts_jll]]
+uuid = "14a3606d-f60d-562e-9121-12d972cd8159"
+version = "2022.10.11"
+
+[[NetworkOptions]]
+uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908"
+version = "1.2.0"
+
+[[Pkg]]
+deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "Serialization", "TOML", "Tar", "UUIDs", "p7zip_jll"]
+uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
+version = "1.9.0"
+
+[[Printf]]
+deps = ["Unicode"]
+uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7"
+
+[[REPL]]
+deps = ["InteractiveUtils", "Markdown", "Sockets", "Unicode"]
+uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"
+
+[[Random]]
+deps = ["SHA", "Serialization"]
+uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
+
+[[SHA]]
+uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce"
+version = "0.7.0"
+
+[[Serialization]]
+uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
+
+[[Sockets]]
+uuid = "6462fe0b-24de-5631-8697-dd941f90decc"
+
+[[TOML]]
+deps = ["Dates"]
+uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76"
+version = "1.0.3"
+
+[[Tar]]
+deps = ["ArgTools", "SHA"]
+uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e"
+version = "1.10.0"
+
+[[UUIDs]]
+deps = ["Random", "SHA"]
+uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
+
+[[Unicode]]
+uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5"
+
+[[Zlib_jll]]
+deps = ["Libdl"]
+uuid = "83775a58-1f1d-513f-b197-d71354ab007a"
+version = "1.2.13+0"
+
+[[nghttp2_jll]]
+deps = ["Artifacts", "Libdl"]
+uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d"
+version = "1.48.0+0"
+
+[[p7zip_jll]]
+deps = ["Artifacts", "Libdl"]
+uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0"
+version = "17.4.0+0"
+"""
+
+# ╔═╡ Cell order:
+# ╠═22dc8ce8-392e-4202-a549-f0fd46152322
+# ╟─9b342ef4-fff9-46d0-b316-cc383fe71a59
+# ╟─4a683caa-6910-4922-bda4-c3a00950a14b
+# ╠═635e5ebc-6567-11eb-1d9d-f98bfca7ec27
+# ╠═ad853ac9-a8a0-44ef-8d41-c8cea165ad57
+# ╠═26025270-9b5e-4841-b295-0c47437bc7db
+# ╟─8fb4ff71-6f86-4643-a0af-6b6665d63634
+# ╠═ea600f99-b634-492f-a1d6-7137ff896e84
+# ╠═815f3a2d-b5f6-4b5d-84ec-fa660c3dbfe8
+# ╟─1352da54-e567-4f59-a3da-19ed3f4bb7c7
+# ╠═dca76d5d-bb71-4a81-9f9e-19ddfbc258cd
+# ╠═a3ab6582-3d30-4594-8eb6-17f3c4103076
+# ╠═6ee0893c-45f3-490e-a53c-7b3d92bc8f70
+# ╠═7ad5c51b-76e4-44d3-aeca-f81a8f22f8b4
+# ╠═1681132d-050d-4ef7-9caa-14be6eade03d
+# ╟─6ca3eca5-2a0b-4c9c-84c7-307fd4a29eae
+# ╠═09ae27fa-525a-4211-b252-960cdbaf1c1e
+# ╠═ed78a6f1-d282-4d80-8f42-40701aeadb52
+# ╠═cca2f726-0c25-43c6-85e4-c16ec192d464
+# ╟─c4f51980-3c30-4d3f-a76a-fc0f0fe16944
+# ╠═8f0bd329-36b8-45ed-b80d-24661242129a
+# ╠═c55a107f-5d7d-4396-b597-8c1ae07c35be
+# ╠═a524ff27-a6a3-4f14-8ed4-f55700647bc4
+# ╟─00000000-0000-0000-0000-000000000001
+# ╟─00000000-0000-0000-0000-000000000002
diff --git a/test/runtests.jl b/test/runtests.jl
index 86054d4..99162dd 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -17,6 +17,7 @@ else
cache_dir = tempname(cleanup=false)
ENV["HIDE_PLUTO_EXACT_VERSION_WARNING"] = "true"
+ include("./staterequest static.jl")
include("./plutohash.jl")
include("./configuration.jl")
include("./static export.jl")
diff --git a/test/staterequest static.jl b/test/staterequest static.jl
new file mode 100644
index 0000000..4ca42ec
--- /dev/null
+++ b/test/staterequest static.jl
@@ -0,0 +1,163 @@
+import PlutoSliderServer: PlutoSliderServer, Pluto, list_files_recursive
+import HTTP
+import Deno_jll
+using OrderedCollections
+
+import Random
+using Test
+using UUIDs
+using Base64
+
+@testset "HTTP requests" begin
+ test_dir = tempname(cleanup=false)
+ cp(@__DIR__, test_dir)
+
+ notebook_paths = ["basic3.jl"]
+ # notebook_paths = ["basic2.jl", "parallelpaths4.jl"]
+
+ port = rand(Random.RandomDevice(), 12345:65000)
+
+
+ still_booting = Ref(true)
+ ready_result = Ref{Any}(nothing)
+ function on_ready(result)
+ ready_result[] = result
+ still_booting[] = false
+ end
+
+
+ t = Pluto.@asynclog begin
+ try
+ run(
+ `$(Deno_jll.deno()) run --allow-net --allow-read https://deno.land/std@0.115.0/http/file_server.ts $(test_dir) --cors --port $(port)`,
+ )
+ catch e
+ if !(e isa TaskFailedException) && !(e isa InterruptException)
+ showerror(stderr, e, stacktrace(catch_backtrace()))
+ end
+ end
+ end
+
+ withenv("JULIA_DEBUG" => nothing) do
+ PlutoSliderServer.export_directory(
+ test_dir;
+ Precompute_enabled=true,
+ notebook_paths,
+ on_ready,
+ )
+ end
+
+ @test isdir(joinpath(test_dir, "staterequest"))
+ @test isdir(joinpath(test_dir, "bondconnections"))
+ @show list_files_recursive(joinpath(test_dir, "staterequest"))
+ @show readdir(joinpath(test_dir, "bondconnections"))
+ @test length(list_files_recursive(joinpath(test_dir, "staterequest"))) == let
+ x = 10
+ y = 20
+ z = 15
+ a1, a2 = 10, 10
+ x * y + z + a1 * a2
+ end
+
+ while !occursin(
+ "Pluto.jl notebook",
+ read(download("http://localhost:$(port)/basic3.jl"), String),
+ )
+ @info "Waiting for file server to start"
+ sleep(0.1)
+ end
+
+ while still_booting[]
+ sleep(0.1)
+ end
+
+
+ notebook_sessions = ready_result[].notebook_sessions
+
+ @show notebook_paths [
+ (s.path, typeof(s.run), s.current_hash) for s in notebook_sessions
+ ]
+
+ @testset "Bond connections - $(name)" for (i, name) in enumerate(notebook_paths)
+ s = notebook_sessions[i]
+
+ for ending in [""]
+ response = HTTP.get(
+ "http://localhost:$(port)/bondconnections/$(HTTP.URIs.escapeuri(s.current_hash))" *
+ ending,
+ )
+
+ result = Pluto.unpack(response.body)
+
+ @test result ==
+ Dict(String(k) => String.(v) for (k, v) in s.run.bond_connections)
+ end
+ end
+
+
+ @testset "State request - basic3.jl" begin
+ i = 1
+ s = notebook_sessions[i]
+
+ @testset "Method $(method)" for method in ["GET"], x = 3:7
+
+ v(x) = OrderedDict("value" => x)
+
+ bonds = OrderedDict("x" => v(x), "y" => v(7))
+
+ state = Pluto.unpack(Pluto.pack(s.run.original_state))
+
+ sum_cell_id = "26025270-9b5e-4841-b295-0c47437bc7db"
+
+ response = if method == "GET"
+ arg = Pluto.pack(bonds) |> PlutoSliderServer.base64urlencode
+
+ HTTP.request(
+ method,
+ "http://localhost:$(port)/staterequest/$(s.current_hash)/$(arg)",
+ )
+ else
+ HTTP.request(
+ method,
+ "http://localhost:$(port)/staterequest/$(s.current_hash)/",
+ [],
+ Pluto.pack(bonds),
+ )
+ end
+
+ result = Pluto.unpack(response.body)
+
+ @test sum_cell_id ∈ result["ids_of_cells_that_ran"]
+
+ for patch in result["patches"]
+ Pluto.Firebasey.applypatch!(
+ state,
+ convert(Pluto.Firebasey.JSONPatch, patch),
+ )
+ end
+
+ @test state["cell_results"][sum_cell_id]["output"]["body"] ==
+ let
+ x = bonds["x"]["value"]
+ y = bonds["y"]["value"]
+ repeat(string(x), y)
+ end |> repr
+
+ end
+ end
+
+ # close(ready_result[].serversocket)
+
+ try
+ schedule(t, InterruptException(); error=true)
+ wait(t)
+ catch e
+ if !(e isa TaskFailedException) && !(e isa InterruptException)
+ rethrow(e)
+ end
+ end
+ # schedule(t, InterruptException(), error=true)
+ @info "DONEZO"
+
+ @test true
+end
\ No newline at end of file