Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
hhaensel committed Dec 17, 2023
2 parents 9277d3e + 9a8b35e commit 2a48285
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 67 deletions.
4 changes: 2 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "Stipple"
uuid = "4acbeb90-81a0-11ea-1966-bdaff8155998"
authors = ["Adrian <[email protected]>"]
version = "0.27.23"
version = "0.27.25"

[deps]
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
Expand All @@ -27,7 +27,7 @@ Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c"
DataFrames = "1"
Dates = "1.6"
FilePathsBase = "0.9"
Genie = "5.23.3"
Genie = "5.23.6"
GenieSession = "1"
GenieSessionFileSession = "1"
JSON = "0.20, 0.21"
Expand Down
10 changes: 5 additions & 5 deletions src/ModelStorage.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import GenieSessionFileSession

export init_from_storage

function init_from_storage(::Type{M};
channel::Union{Any,Nothing} = Stipple.channeldefault(),
function init_from_storage( t::Type{M};
channel::Union{Any,Nothing} = Stipple.channeldefault(t),
kwargs...) where M
model_id = Symbol(Stipple.routename(M))
model = Stipple.init(M; channel, kwargs...)
Expand All @@ -22,17 +22,17 @@ function init_from_storage(::Type{M};
# restore fields only if a stored model exists, if the field is not part of the internal fields and is not write protected
(
isnothing(stored_model) || f [Stipple.CHANNELFIELDNAME, Stipple.AUTOFIELDS...] || Stipple.isreadonly(f, model) || Stipple.isprivate(f, model) ||
! hasproperty(stored_model, f) || (field[!] = getfield(stored_model, f)[])
! hasproperty(stored_model, f) || ! hasproperty(model, f) || (field[!] = getfield(stored_model, f)[])
)

# register reactive handlers to automatically save model on session when model changes
if f [Stipple.CHANNELFIELDNAME, Stipple.AUTOFIELDS...]
if f [Stipple.AUTOFIELDS...]
on(field) do _
GenieSession.set!(model_id, model)
end
end
else
isnothing(stored_model) || Stipple.isprivate(f, model) || Stipple.isreadonly(f, model) || ! hasproperty(stored_model, f) || setfield!(model, f, getfield(stored_model, f))
isnothing(stored_model) || Stipple.isprivate(f, model) || Stipple.isreadonly(f, model) || ! hasproperty(stored_model, f) || ! hasproperty(model, f) || setfield!(model, f, getfield(stored_model, f))
end
end

Expand Down
38 changes: 22 additions & 16 deletions src/ReactiveTools.jl
Original file line number Diff line number Diff line change
Expand Up @@ -585,12 +585,16 @@ macro init(args...)

quote
local new_handlers = false
local initfn = if isdefined($__module__, :init_from_storage)

local initfn =
if isdefined($__module__, :init_from_storage)
$__module__.init_from_storage
else
Stipple.init
end
local handlersfn = if !$called_without_type

local handlersfn =
if !$called_without_type
# writing '$(init_kwargs[type_pos])' generates an error during a pre-evaluation
# possibly from Revise?
# we use 'get' instead of 'getindex'
Expand All @@ -606,16 +610,30 @@ macro init(args...)
identity
end
end

instance = let model = initfn($(init_args...))
new_handlers ? Base.invokelatest(handlersfn, model) : handlersfn(model)
end
for p in Stipple.Pages._pages
p.context == $__module__ && (p.model = instance)
end

instance
end |> esc
end

macro app(typename, expr, handlers_fn_name = :handlers)
# indicate to the @handlers macro that old typefields have to be cleared
# (avoids model_to_storage)
newtypename = Symbol(typename, "_!_")
quote
let model = Stipple.ReactiveTools.@handlers $newtypename $expr $handlers_fn_name
Stipple.ReactiveTools.HANDLERS_FUNCTIONS[$typename] = $handlers_fn_name
model
end
end |> esc
end

macro handlers()
handlers = init_handlers(__module__)

Expand All @@ -639,18 +657,6 @@ macro handlers(expr)
end |> esc
end

macro app(typename, expr, handlers_fn_name = :handlers)
# indicate to the @handlers macro that old typefields have to be cleared
# (avoids model_to_storage)
newtypename = Symbol(typename, "_!_")
quote
let model = Stipple.ReactiveTools.@handlers $newtypename $expr $handlers_fn_name
Stipple.ReactiveTools.HANDLERS_FUNCTIONS[$typename] = $handlers_fn_name
model
end
end |> esc
end

macro handlers(typename, expr, handlers_fn_name = :handlers)
newtype = endswith(String(typename), "_!_")
newtype && (typename = Symbol(String(typename)[1:end-3]))
Expand Down Expand Up @@ -1315,7 +1321,7 @@ Add a function f to the dependencies of the current app.
@deps M::Module
Add the dependencies of the module M to the dependencies of the current app.
"""
macro deps(expr)
Expand All @@ -1335,7 +1341,7 @@ The module needs to define a function `deps()`.
@deps(MyApp::ReactiveModel, M::Module)
Add the dependencies of the module M to the dependencies of the app MyApp.
The module needs to define a function `deps()`.
"""
Expand Down
133 changes: 89 additions & 44 deletions src/Stipple.jl
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,18 @@ end
function channeldefault() :: Union{String,Nothing}
params(CHANNELPARAM, (haskey(ENV, "$CHANNELPARAM") ? (Genie.Router.params!(CHANNELPARAM, ENV["$CHANNELPARAM"])) : nothing))
end
function channeldefault(::Type{M}) where M<:ReactiveModel
haskey(ENV, "$CHANNELPARAM") && (Genie.Router.params!(CHANNELPARAM, ENV["$CHANNELPARAM"]))
haskey(params(), CHANNELPARAM) && return params(CHANNELPARAM)

if ! haskey(Genie.Router.params(), :CHANNEL) && ! haskey(Genie.Router.params(), :ROUTE)
return nothing
end

model_id = Symbol(Stipple.routename(M))
stored_model = Stipple.ModelStorage.Sessions.GenieSession.get(model_id, nothing)
stored_model === nothing ? nothing : getfield(stored_model, Stipple.CHANNELFIELDNAME)
end

@nospecialize

Expand Down Expand Up @@ -421,10 +433,10 @@ frontend and perform the 2-way backend-frontend data sync. Returns the instance
hs_model = Stipple.init(HelloPie)
```
"""
function init(::Type{M};
function init(t::Type{M};
vue_app_name::S = Stipple.Elements.root(M),
endpoint::S = vue_app_name,
channel::Union{Any,Nothing} = channeldefault(),
channel::Union{Any,Nothing} = channeldefault(t),
debounce::Int = JS_DEBOUNCE_TIME,
transport::Module = Genie.WebChannels,
core_theme::Bool = true)::M where {M<:ReactiveModel, S<:AbstractString}
Expand All @@ -442,30 +454,31 @@ function init(::Type{M};
elseif hasproperty(model, CHANNELFIELDNAME)
getchannel(model)
else
setchannel(model, channelfactory())
setchannel(model, channel)
end

# add a timer that checks if the model is outdated and if so prepare the model to be garbage collected
LAST_ACTIVITY[Symbol(getchannel(model))] = now()

PRECOMPILE[] || Timer(setup_purge_checker(model), PURGE_CHECK_DELAY[], interval = PURGE_CHECK_DELAY[])

if is_channels_webtransport()
Genie.Assets.channels_subscribe(channel)
else
Genie.Assets.webthreads_subscribe(channel)
Genie.Assets.webthreads_push_pull(channel)
end
# register channels and routes only if within a request
if haskey(Genie.Router.params(), :CHANNEL) || haskey(Genie.Router.params(), :ROUTE)
if is_channels_webtransport()
Genie.Assets.channels_subscribe(channel)
else
Genie.Assets.webthreads_subscribe(channel)
Genie.Assets.webthreads_push_pull(channel)
end

ch = "/$channel/watchers"
if ! Genie.Router.ischannel(Symbol(ch))
Genie.Router.channel(ch, named = Symbol(ch)) do
ch = "/$channel/watchers"
Genie.Router.channel(ch, named = Router.channelname(ch)) do
payload = Genie.Requests.payload(:payload)["payload"]
client = transport == Genie.WebChannels ? Genie.WebChannels.id(Genie.Requests.wsclient()) : Genie.Requests.wtclient()

try
haskey(payload, "sesstoken") && ! isempty(payload["sesstoken"]) &&
Genie.Router.params!(:session,
Genie.Router.params!(Stipple.ModelStorage.Sessions.GenieSession.PARAMS_SESSION_KEY,
Stipple.ModelStorage.Sessions.GenieSession.load(payload["sesstoken"] |> Genie.Encryption.decrypt))
catch ex
@error ex
Expand Down Expand Up @@ -503,39 +516,71 @@ function init(::Type{M};
else
return "An error has occured -- please check the logs"
end
end

ok_response
field = Symbol(payload["field"])

#check if field exists
hasfield(CM, field) || return ok_response

valtype = Dict(zip(fieldnames(CM), CM.types))[field]
val = valtype <: Reactive ? getfield(model, field) : Ref{valtype}(getfield(model, field))

# reject non-public types
( isprivate(field, model) || isreadonly(field, model) ) && return ok_response

newval = convertvalue(val, payload["newval"])
oldval = try
convertvalue(val, payload["oldval"])
catch ex
val[]
end

push!(model, field => newval; channel = channel, except = client)
LAST_ACTIVITY[Symbol(channel)] = now()

try
update!(model, field, newval, oldval)
catch ex
# send the error to the frontend
if Genie.Configuration.isdev()
return ex
else
return "An error has occured -- please check the logs"
end
end

ok_response
end
end
end

ch = "/$channel/keepalive"
if ! Genie.Router.ischannel(Symbol(ch))
Genie.Router.channel(ch, named = Symbol(ch)) do
LAST_ACTIVITY[Symbol(channel)] = now()
ok_response
ch = "/$channel/keepalive"
if ! Genie.Router.ischannel(Router.channelname(ch))
Genie.Router.channel(ch, named = Router.channelname(ch)) do
LAST_ACTIVITY[Symbol(channel)] = now()
ok_response
end
end
end

ch = "/$channel/events"
if ! Genie.Router.ischannel(Symbol(ch))
Genie.Router.channel(ch, named = Symbol(ch)) do
# get event name
event = Genie.Requests.payload(:payload)["event"]
# form handler parameter & call event notifier
handler = Symbol(get(event, "name", nothing))
event_info = get(event, "event", nothing)

# add client id if requested
if event_info isa Dict && get(event_info, "_addclient", false)
client = transport == Genie.WebChannels ? Genie.WebChannels.id(Genie.Requests.wsclient()) : Genie.Requests.wtclient()
push!(event_info, "_client" => client)
ch = "/$channel/events"
if ! Genie.Router.ischannel(Router.channelname(ch))
Genie.Router.channel(ch, named = Router.channelname(ch)) do
# get event name
event = Genie.Requests.payload(:payload)["event"]
# form handler parameter & call event notifier
handler = Symbol(get(event, "name", nothing))
event_info = get(event, "event", nothing)

# add client id if requested
if event_info isa Dict && get(event_info, "_addclient", false)
client = transport == Genie.WebChannels ? Genie.WebChannels.id(Genie.Requests.wsclient()) : Genie.Requests.wtclient()
push!(event_info, "_client" => client)
end

isempty(methods(notify, (M, Val{handler}))) || notify(model, Val(handler))
isempty(methods(notify, (M, Val{handler}, Any))) || notify(model, Val(handler), event_info)
LAST_ACTIVITY[Symbol(channel)] = now()
ok_response
end

isempty(methods(notify, (M, Val{handler}))) || notify(model, Val(handler))
isempty(methods(notify, (M, Val{handler}, Any))) || notify(model, Val(handler), event_info)
LAST_ACTIVITY[Symbol(channel)] = now()
ok_response
end
end

Expand Down Expand Up @@ -746,7 +791,7 @@ end
function injectdeps(output::Vector{AbstractString}, M::Type{<:ReactiveModel}) :: Vector{AbstractString}
for (key, f) in DEPS
key isa DataType && key <: ReactiveModel && continue
# exclude keys starting with '_'
# exclude keys starting with '_'
key isa Symbol && startswith("$key", '_') && continue
push!(output, f()...)
end
Expand Down Expand Up @@ -805,7 +850,7 @@ end

function deps!(M::Type{<:ReactiveModel}, f::Function; extra_deps = true)
key = extra_deps ? Symbol("_$(vm(M))_$(nameof(f))") : M
DEPS[key] = f isa Function ? f : f.deps
DEPS[key] = f isa Function ? f : f.deps
end

deps!(M::Type{<:ReactiveModel}, modul::Module; extra_deps = true) = deps!(M, modul.deps; extra_deps)
Expand Down Expand Up @@ -973,13 +1018,13 @@ function attributes(kwargs::Union{Vector{<:Pair}, Base.Iterators.Pairs, Dict},
for (k,v) in kwargs
v === nothing && continue
mapped = false

k_str = string(k)

if haskey(mappings, k_str)
k_str = mappings[k_str]
end

v_isa_jsexpr = v isa Symbol || !isa(v, Union{AbstractString, Bool, Number})
attr_key = string((v_isa_jsexpr && ! startswith(k_str, ":") &&
! (endswith(k_str, "!") || startswith(k_str, "v-") || startswith(k_str, "v" * Genie.config.html_parser_char_dash)) ? ":" : ""), k_str) |> Symbol
Expand Down

0 comments on commit 2a48285

Please sign in to comment.