From 98727ab1e57935e7f8c50b9a53cf5083922e1a70 Mon Sep 17 00:00:00 2001 From: hhaensel Date: Sat, 9 Nov 2024 23:47:46 +0100 Subject: [PATCH 01/13] support multi-model pages --- assets/js/watchers.js | 4 ++-- src/Elements.jl | 11 +++++++---- src/Layout.jl | 18 ++++++++++++------ src/Stipple.jl | 9 ++++++--- src/stipple/rendering.jl | 2 +- 5 files changed, 28 insertions(+), 16 deletions(-) diff --git a/assets/js/watchers.js b/assets/js/watchers.js index 6d87f2d6..a9b6214e 100644 --- a/assets/js/watchers.js +++ b/assets/js/watchers.js @@ -67,7 +67,7 @@ const watcherMixin = { }, push: function (field) { - Genie.WebChannels.sendMessageTo(CHANNEL, 'watchers', {'payload': { + Genie.WebChannels.sendMessageTo(this.channel_, 'watchers', {'payload': { 'field': field, 'newval': this[field], 'oldval': null, @@ -110,7 +110,7 @@ const eventMixin = { if (event_data === undefined) { event_data = {} } console.debug('event: ' + JSON.stringify(event_data) + ":" + event_handler) if (mode=='addclient') { event_data._addclient = true} - Genie.WebChannels.sendMessageTo(window.CHANNEL, 'events', { + Genie.WebChannels.sendMessageTo(this.channel_, 'events', { 'event': { 'name': event_handler, 'event': event_data diff --git a/src/Elements.jl b/src/Elements.jl index 08c61980..e2152fd3 100644 --- a/src/Elements.jl +++ b/src/Elements.jl @@ -176,6 +176,10 @@ function vue_integration(::Type{M}; app.config.globalProperties[key] = value }); window.$vue_app_name = window.GENIEMODEL = app.mount(rootSelector); + window.channelIndex = window.channelIndex || 0; + $vue_app_name.WebChannel = Genie.AllWebChannels[channelIndex]; + $vue_app_name.channel_ = $vue_app_name.WebChannel.channel; + channelIndex++; } // end of initStipple " @@ -209,8 +213,7 @@ function vue_integration(::Type{M}; } } - function app_ready() { - $vue_app_name.channel_ = window.CHANNEL; + function app_$(vue_app_name)_ready() { $vue_app_name.isready = true; Genie.Revivers.addReviver(window.$(vue_app_name).revive_jsfunction); $(transport == Genie.WebChannels && @@ -236,8 +239,8 @@ function vue_integration(::Type{M}; initStipple('#$vue_app_name'); initWatchers(); - Genie.WebChannels.subscriptionHandlers.push(function(event) { - app_ready(); + $vue_app_name.WebChannel.subscriptionHandlers.push(function(event) { + app_$(vue_app_name)_ready(); }); } """ diff --git a/src/Layout.jl b/src/Layout.jl index 86be5b9a..4d1d03e2 100644 --- a/src/Layout.jl +++ b/src/Layout.jl @@ -41,18 +41,19 @@ julia> layout([ "Hello\n" ``` """ -function layout(output::Union{S,Vector}, m::M; +function layout(output::Union{S,Vector}, m::Union{M, Vector{M}}; partial::Bool = false, title::String = "", class::String = "", style::String = "", head_content::Union{AbstractString, Vector{<:AbstractString}} = "", channel::String = Stipple.channel_js_name, core_theme::Bool = true)::ParsedHTMLString where {M<:ReactiveModel, S<:AbstractString} isa(output, Vector) && (output = join(output, '\n')) + m isa Vector || (m = [m]) content = [ output theme(; core_theme) - Stipple.deps(m) - ] + Stipple.deps.(m)... + ] |> union partial && return content @@ -84,17 +85,22 @@ julia> page(:elemid, [ "\n
Hello
\n" ``` """ -function page(model::M, args...; +function page(model::Union{M, Vector{M}}, args...; partial::Bool = false, title::String = "", class::String = "container", style::String = "", channel::String = Genie.config.webchannels_default_route, head_content::Union{AbstractString, Vector{<:AbstractString}} = "", prepend::Union{S,Vector} = "", append::Union{T,Vector} = [], core_theme::Bool = true, kwargs...)::ParsedHTMLString where {M<:Stipple.ReactiveModel, S<:AbstractString,T<:AbstractString} - + model isa Vector || (model = [model]) + uis = if !isempty(args) + args[1] isa Vector ? args[1] : [args[1]] + else + "" + end layout( [ join(prepend) - Genie.Renderer.Html.div(id = vm(M), args...; class = class, kwargs...) + [Genie.Renderer.Html.div(id = vm(m), ui, args[2:end]...; class = class, kwargs...) for (m, ui) in zip(model, uis)]... join(append) ], model; partial = partial, title = title, style = style, head_content = head_content, channel = channel, diff --git a/src/Stipple.jl b/src/Stipple.jl index 71ec16ac..b2463f7f 100644 --- a/src/Stipple.jl +++ b/src/Stipple.jl @@ -342,10 +342,10 @@ changed on the frontend, it is pushed over to the backend using `channel`, at a function watch(vue_app_name::String, fieldname::Symbol, channel::String, debounce::Int, model::M; jsfunction::String = "")::String where {M<:ReactiveModel} js_channel = isempty(channel) ? "window.Genie.Settings.webchannels_default_route" : - (channel == Stipple.channel_js_name ? Stipple.channel_js_name : "'$channel'") + "$vue_app_name.channel_" isempty(jsfunction) && - (jsfunction = "Genie.WebChannels.sendMessageTo($js_channel, 'watchers', {'payload': {'field':'$fieldname', 'newval': newVal, 'oldval': oldVal, 'sesstoken': document.querySelector(\"meta[name='sesstoken']\")?.getAttribute('content')}});") + (jsfunction = "$vue_app_name.WebChannel.sendMessageTo($js_channel, 'watchers', {'payload': {'field':'$fieldname', 'newval': newVal, 'oldval': oldVal, 'sesstoken': document.querySelector(\"meta[name='sesstoken']\")?.getAttribute('content')}});") output = IOBuffer() if fieldname == :isready @@ -847,7 +847,10 @@ end function channelscript(channel::String) :: String - Genie.Renderer.Html.script(["window.CHANNEL = '$(channel)';"]) + Genie.Renderer.Html.script([""" + window.CHANNEL = '$(channel)'; + if (window.Genie) Genie.init_webchannel('$(channel)'); + """]) end diff --git a/src/stipple/rendering.jl b/src/stipple/rendering.jl index 156d078c..fc9c7913 100644 --- a/src/stipple/rendering.jl +++ b/src/stipple/rendering.jl @@ -135,7 +135,7 @@ function Stipple.render(app::M)::Dict{Symbol,Any} where {M<:ReactiveModel} for field in fieldnames(typeof(app)) f = getfield(app, field) - occursin(SETTINGS.private_pattern, String(field)) && continue + field != CHANNELFIELDNAME && occursin(SETTINGS.private_pattern, String(field)) && continue f isa Reactive && f.r_mode == PRIVATE && continue result[field] = Stipple.jsrender(f, field) From 737c2e4740fb9aa9d1a3ce62f6ab46326f4e03a0 Mon Sep 17 00:00:00 2001 From: hhaensel Date: Sun, 10 Nov 2024 00:21:04 +0100 Subject: [PATCH 02/13] support pagetemplate, rename init_webchannel --- src/Layout.jl | 3 ++- src/Stipple.jl | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Layout.jl b/src/Layout.jl index 4d1d03e2..adc27eff 100644 --- a/src/Layout.jl +++ b/src/Layout.jl @@ -86,6 +86,7 @@ julia> page(:elemid, [ ``` """ function page(model::Union{M, Vector{M}}, args...; + pagetemplate = (x...) -> join([x...], '\n'), partial::Bool = false, title::String = "", class::String = "container", style::String = "", channel::String = Genie.config.webchannels_default_route, head_content::Union{AbstractString, Vector{<:AbstractString}} = "", prepend::Union{S,Vector} = "", append::Union{T,Vector} = [], @@ -100,7 +101,7 @@ function page(model::Union{M, Vector{M}}, args...; layout( [ join(prepend) - [Genie.Renderer.Html.div(id = vm(m), ui, args[2:end]...; class = class, kwargs...) for (m, ui) in zip(model, uis)]... + pagetemplate([Genie.Renderer.Html.div(id = vm(m), ui, args[2:end]...; class = class, kwargs...) for (m, ui) in zip(model, uis)]...) join(append) ], model; partial = partial, title = title, style = style, head_content = head_content, channel = channel, diff --git a/src/Stipple.jl b/src/Stipple.jl index b2463f7f..d99b4778 100644 --- a/src/Stipple.jl +++ b/src/Stipple.jl @@ -849,7 +849,7 @@ end function channelscript(channel::String) :: String Genie.Renderer.Html.script([""" window.CHANNEL = '$(channel)'; - if (window.Genie) Genie.init_webchannel('$(channel)'); + if (window.Genie) Genie.initWebChannel('$(channel)'); """]) end From 14bb9b19966c98f5bac56c6b392347341ef0431f Mon Sep 17 00:00:00 2001 From: hhaensel Date: Sun, 10 Nov 2024 21:45:48 +0100 Subject: [PATCH 03/13] support multiple models of the same type --- src/Elements.jl | 51 ++++++++++++++++++++++++++++++------------------- src/Layout.jl | 13 ++++++++++--- src/Stipple.jl | 7 +++++++ 3 files changed, 48 insertions(+), 23 deletions(-) diff --git a/src/Elements.jl b/src/Elements.jl index e2152fd3..eea9850d 100644 --- a/src/Elements.jl +++ b/src/Elements.jl @@ -130,7 +130,7 @@ function vue_integration(::Type{M}; debounce::Int = Stipple.JS_DEBOUNCE_TIME, transport::Module = Genie.WebChannels)::String where {M<:ReactiveModel} model = Base.invokelatest(M) - + app = "window[appName]" vue_app = json(model |> Stipple.render) vue_app = replace(vue_app, "\"$(getchannel(model))\"" => Stipple.channel_js_name) @@ -159,7 +159,7 @@ function vue_integration(::Type{M}; string( " - function initStipple(rootSelector){ + function initStipple$vue_app_name(appName, rootSelector){ // components = Stipple.init($( core_theme ? "{theme: '$theme'}" : "" )); const app = Vue.createApp($( replace(vue_app, "'$(Stipple.UNDEFINED_PLACEHOLDER)'"=>Stipple.UNDEFINED_VALUE) )) /* Object.entries(components).forEach(([key, value]) => { @@ -175,10 +175,12 @@ function vue_integration(::Type{M}; Object.entries(app.prototype).forEach(([key, value]) => { app.config.globalProperties[key] = value }); - window.$vue_app_name = window.GENIEMODEL = app.mount(rootSelector); + $app = window.GENIEMODEL = app.mount(rootSelector); window.channelIndex = window.channelIndex || 0; - $vue_app_name.WebChannel = Genie.AllWebChannels[channelIndex]; - $vue_app_name.channel_ = $vue_app_name.WebChannel.channel; + $app.WebChannel = Genie.AllWebChannels[channelIndex]; + $app.WebChannel.parent = $app; + $app.channel_ = $app.WebChannel.channel; + channelIndex++; } // end of initStipple @@ -188,12 +190,12 @@ function vue_integration(::Type{M}; " - function initWatchers(){ + function initWatchers$vue_app_name(app){ " , join( - [Stipple.watch(string("window.", vue_app_name), field, Stipple.channel_js_name, debounce, model) for field in fieldnames(Stipple.get_concrete_type(M)) + [Stipple.watch("app", field, Stipple.channel_js_name, debounce, model) for field in fieldnames(Stipple.get_concrete_type(M)) if Stipple.has_frontend_watcher(field, model)] ) , @@ -207,21 +209,21 @@ function vue_integration(::Type{M}; """ - window.parse_payload = function(payload){ + window.parse_payload = function(WebChannel, payload){ if (payload.key) { - window.$(vue_app_name).updateField(payload.key, payload.value); + WebChannel.parent.updateField(payload.key, payload.value); } } - function app_$(vue_app_name)_ready() { - $vue_app_name.isready = true; - Genie.Revivers.addReviver(window.$(vue_app_name).revive_jsfunction); + function app_ready(app) { + app.isready = true; + Genie.Revivers.addReviver(app.revive_jsfunction); $(transport == Genie.WebChannels && " try { if (Genie.Settings.webchannels_keepalive_frequency > 0) { - clearInterval($vue_app_name.keepalive_interval); - $vue_app_name.keepalive_interval = setInterval(keepalive, Genie.Settings.webchannels_keepalive_frequency); + clearInterval(app.keepalive_interval); + app.keepalive_interval = setInterval(keepalive, Genie.Settings.webchannels_keepalive_frequency); } } catch (e) { if (Genie.Settings.env === 'dev') { @@ -235,14 +237,23 @@ function vue_integration(::Type{M}; } }; - if ( window.autorun === undefined || window.autorun === true ) { - initStipple('#$vue_app_name'); - initWatchers(); + function create$vue_app_name() { + window.counter$vue_app_name = window.counter$vue_app_name || 0 + appName = '$vue_app_name' + ((counter$vue_app_name == 0) ? '' : '-' + window.counter$vue_app_name) + counter$vue_app_name++ - $vue_app_name.WebChannel.subscriptionHandlers.push(function(event) { - app_$(vue_app_name)_ready(); - }); + if ( window.autorun === undefined || window.autorun === true ) { + initStipple$vue_app_name(appName, '#' + appName); + initWatchers$vue_app_name($app); + + $app.WebChannel.subscriptionHandlers.push(function(event) { + app_ready($app); + }); + } } + + // create$vue_app_name() + // is called via scipt with addEventListener to support multiple apps """ ) diff --git a/src/Layout.jl b/src/Layout.jl index adc27eff..292ef69b 100644 --- a/src/Layout.jl +++ b/src/Layout.jl @@ -98,14 +98,21 @@ function page(model::Union{M, Vector{M}}, args...; else "" end + counter = Dict{DataType, Int}() + + function rootselector(m::M) where M <:ReactiveModel + AM = Stipple.get_abstract_type(M) + counter[AM] = get(counter, AM, -1) + 1 + return (counter[AM] == 0) ? vm(m) : "$(vm(m))-$(counter[AM])" + end + layout( [ join(prepend) - pagetemplate([Genie.Renderer.Html.div(id = vm(m), ui, args[2:end]...; class = class, kwargs...) for (m, ui) in zip(model, uis)]...) + pagetemplate([Genie.Renderer.Html.div(id = rootselector(m), ui, args[2:end]...; class = class, kwargs...) for (m, ui) in zip(model, uis)]...) join(append) ], model; - partial = partial, title = title, style = style, head_content = head_content, channel = channel, - core_theme = core_theme) + partial, title, style, head_content, channel, core_theme) end const app = page diff --git a/src/Stipple.jl b/src/Stipple.jl index d99b4778..9f05783a 100644 --- a/src/Stipple.jl +++ b/src/Stipple.jl @@ -853,6 +853,12 @@ function channelscript(channel::String) :: String """]) end +function initscript(vue_app_name) :: String + Genie.Renderer.Html.script([""" + // script id: $(randstring(64)) + document.addEventListener("DOMContentLoaded", () => window.create$vue_app_name() ); + """]) +end """ function deps(channel::String = Genie.config.webchannels_default_route) @@ -863,6 +869,7 @@ function deps(m::M) :: Vector{String} where {M<:ReactiveModel} channel = getchannel(m) output = [ channelscript(channel), + initscript(vm(m)), (is_channels_webtransport() ? Genie.Assets.channels_script_tag(channel) : Genie.Assets.webthreads_script_tag(channel)), Genie.Renderer.Html.script(src = Genie.Assets.asset_path(assets_config, :js, file="underscore-min")), Genie.Renderer.Html.script(src = Genie.Assets.asset_path(assets_config, :js, file=(Genie.Configuration.isprod() ? "vue.global.prod" : "vue.global"))), From ad4ab5e362e4a5fb3f92bfc91e0289df153f100c Mon Sep 17 00:00:00 2001 From: hhaensel Date: Sun, 10 Nov 2024 22:00:11 +0100 Subject: [PATCH 04/13] defer setup of WebChannels --- src/Stipple.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Stipple.jl b/src/Stipple.jl index 9f05783a..56b82e4a 100644 --- a/src/Stipple.jl +++ b/src/Stipple.jl @@ -848,8 +848,8 @@ end function channelscript(channel::String) :: String Genie.Renderer.Html.script([""" - window.CHANNEL = '$(channel)'; - if (window.Genie) Genie.initWebChannel('$(channel)'); + window.CHANNEL = '$(channel)'; // probably no longer required, but in runtests still used + document.addEventListener("DOMContentLoaded", () => Genie.initWebChannel('$(channel)') ); """]) end From e816fa53d338fc03867d56e33cd3133fe8e63f24 Mon Sep 17 00:00:00 2001 From: hhaensel Date: Sun, 10 Nov 2024 22:29:12 +0100 Subject: [PATCH 05/13] change numbering of models --- src/Elements.jl | 7 ++++--- src/Layout.jl | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Elements.jl b/src/Elements.jl index eea9850d..86bffa32 100644 --- a/src/Elements.jl +++ b/src/Elements.jl @@ -238,12 +238,13 @@ function vue_integration(::Type{M}; }; function create$vue_app_name() { - window.counter$vue_app_name = window.counter$vue_app_name || 0 - appName = '$vue_app_name' + ((counter$vue_app_name == 0) ? '' : '-' + window.counter$vue_app_name) + window.counter$vue_app_name = window.counter$vue_app_name || 1 + appName = '$vue_app_name' + ((counter$vue_app_name == 1) ? '' : '_' + window.counter$vue_app_name) + rootSelector = '#$vue_app_name' + ((counter$vue_app_name == 1) ? '' : '-' + window.counter$vue_app_name) counter$vue_app_name++ if ( window.autorun === undefined || window.autorun === true ) { - initStipple$vue_app_name(appName, '#' + appName); + initStipple$vue_app_name(appName, rootSelector); initWatchers$vue_app_name($app); $app.WebChannel.subscriptionHandlers.push(function(event) { diff --git a/src/Layout.jl b/src/Layout.jl index 292ef69b..03ac188f 100644 --- a/src/Layout.jl +++ b/src/Layout.jl @@ -102,8 +102,8 @@ function page(model::Union{M, Vector{M}}, args...; function rootselector(m::M) where M <:ReactiveModel AM = Stipple.get_abstract_type(M) - counter[AM] = get(counter, AM, -1) + 1 - return (counter[AM] == 0) ? vm(m) : "$(vm(m))-$(counter[AM])" + counter[AM] = get(counter, AM, 0) + 1 + return (counter[AM] == 1) ? vm(m) : "$(vm(m))-$(counter[AM])" end layout( From eac16909e87ad45d4af622c033ecf69aaa53a9a5 Mon Sep 17 00:00:00 2001 From: hhaensel Date: Sun, 10 Nov 2024 23:28:58 +0100 Subject: [PATCH 06/13] replace sendMessageTo by push, remove window.CHANNEL --- src/Stipple.jl | 7 +++---- src/stipple/jsmethods.jl | 3 ++- test/runtests.jl | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Stipple.jl b/src/Stipple.jl index 56b82e4a..e56815ce 100644 --- a/src/Stipple.jl +++ b/src/Stipple.jl @@ -113,7 +113,7 @@ using .NamedTuples export JSONParser, JSONText, json, @json, jsfunction, @jsfunction_str const config = Genie.config -const channel_js_name = "window.CHANNEL" +const channel_js_name = "'not_assigned'" const OptDict = OrderedDict{Symbol, Any} opts(;kwargs...) = OptDict(kwargs...) @@ -345,7 +345,7 @@ function watch(vue_app_name::String, fieldname::Symbol, channel::String, debounc "$vue_app_name.channel_" isempty(jsfunction) && - (jsfunction = "$vue_app_name.WebChannel.sendMessageTo($js_channel, 'watchers', {'payload': {'field':'$fieldname', 'newval': newVal, 'oldval': oldVal, 'sesstoken': document.querySelector(\"meta[name='sesstoken']\")?.getAttribute('content')}});") + (jsfunction = "$vue_app_name.push('$fieldname')") output = IOBuffer() if fieldname == :isready @@ -848,8 +848,7 @@ end function channelscript(channel::String) :: String Genie.Renderer.Html.script([""" - window.CHANNEL = '$(channel)'; // probably no longer required, but in runtests still used - document.addEventListener("DOMContentLoaded", () => Genie.initWebChannel('$(channel)') ); + document.addEventListener("DOMContentLoaded", () => window.Genie.initWebChannel('$(channel)') ); """]) end diff --git a/src/stipple/jsmethods.jl b/src/stipple/jsmethods.jl index 646435dd..fb4017f4 100644 --- a/src/stipple/jsmethods.jl +++ b/src/stipple/jsmethods.jl @@ -36,10 +36,11 @@ function js_methods(app::T)::String where {T<:ReactiveModel} "" end +# deprecated, now part of the model function js_methods_events()::String """ handle_event: function (event, handler) { - Genie.WebChannels.sendMessageTo(window.CHANNEL, 'events', { + Genie.WebChannels.sendMessageTo(GENIEMODEL.channel_, 'events', { 'event': { 'name': handler, 'event': event diff --git a/test/runtests.jl b/test/runtests.jl index dffaf611..909837e3 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -10,7 +10,7 @@ function string_get(x; kwargs...) end function get_channel(s::String) - match(r"window.CHANNEL = '([^']+)'", s).captures[1] + match(r"initWebChannel\('([^']+)'", s).captures[1] end function get_debounce(port, modelname) From b75ca84ee2cadbc1a14e2ee51cc3f7642173b677 Mon Sep 17 00:00:00 2001 From: hhaensel Date: Mon, 11 Nov 2024 00:07:39 +0100 Subject: [PATCH 07/13] support keepalive for multi-model pages --- assets/js/keepalive.js | 9 +++++---- src/Elements.jl | 6 +++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/assets/js/keepalive.js b/assets/js/keepalive.js index 449e890b..01a5d36e 100644 --- a/assets/js/keepalive.js +++ b/assets/js/keepalive.js @@ -4,18 +4,19 @@ ** where x = Genie.config.webchannels_keepalive_frequency */ -function keepalive() { - if (window._lastMessageAt !== undefined) { - if (Date.now() - window._lastMessageAt < Genie.Settings.webchannels_keepalive_frequency) { +function keepalive(WebChannel) { + if (WebChannel.lastMessageAt !== undefined) { + if (Date.now() - WebChannel.lastMessageAt + 200 < Genie.Settings.webchannels_keepalive_frequency) { return } } if (Genie.Settings.env == 'dev') { console.info('Keeping connection alive'); + console.log(WebChannel.parent.i) } - Genie.WebChannels.sendMessageTo(CHANNEL, 'keepalive', { + WebChannel.sendMessageTo(WebChannel.channel, 'keepalive', { 'payload': {} }); } diff --git a/src/Elements.jl b/src/Elements.jl index 86bffa32..2301df9c 100644 --- a/src/Elements.jl +++ b/src/Elements.jl @@ -222,8 +222,8 @@ function vue_integration(::Type{M}; " try { if (Genie.Settings.webchannels_keepalive_frequency > 0) { - clearInterval(app.keepalive_interval); - app.keepalive_interval = setInterval(keepalive, Genie.Settings.webchannels_keepalive_frequency); + clearInterval(app.WebChannel.keepalive_interval); + app.WebChannel.keepalive_interval = setInterval(() => keepalive(app.WebChannel), Genie.Settings.webchannels_keepalive_frequency); } } catch (e) { if (Genie.Settings.env === 'dev') { @@ -239,7 +239,7 @@ function vue_integration(::Type{M}; function create$vue_app_name() { window.counter$vue_app_name = window.counter$vue_app_name || 1 - appName = '$vue_app_name' + ((counter$vue_app_name == 1) ? '' : '_' + window.counter$vue_app_name) + const appName = '$vue_app_name' + ((counter$vue_app_name == 1) ? '' : '_' + window.counter$vue_app_name) rootSelector = '#$vue_app_name' + ((counter$vue_app_name == 1) ? '' : '-' + window.counter$vue_app_name) counter$vue_app_name++ From d2072bfcfc92690cc8807a133d389243deece849 Mon Sep 17 00:00:00 2001 From: hhaensel Date: Mon, 11 Nov 2024 08:15:17 +0100 Subject: [PATCH 08/13] restart keepalive interval with reduced delay after user input --- assets/js/keepalive.js | 18 ++++++++++++++---- src/Elements.jl | 3 +-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/assets/js/keepalive.js b/assets/js/keepalive.js index 01a5d36e..e5774014 100644 --- a/assets/js/keepalive.js +++ b/assets/js/keepalive.js @@ -1,22 +1,32 @@ /* -** keepalive.js // v1.0.0 // 6th January 2022 +** keepalive.js // v1.1.0 // 11 November 2024 ** Keeps alive the websocket connection by sending a ping every x seconds ** where x = Genie.config.webchannels_keepalive_frequency */ function keepalive(WebChannel) { if (WebChannel.lastMessageAt !== undefined) { - if (Date.now() - WebChannel.lastMessageAt + 200 < Genie.Settings.webchannels_keepalive_frequency) { - return + dt = Date.now() - WebChannel.lastMessageAt; + // allow for a 200ms buffer + if (dt + 200 < Genie.Settings.webchannels_keepalive_frequency) { + keepaliveTimer(WebChannel, Genie.Settings.webchannels_keepalive_frequency - dt); + return; } } if (Genie.Settings.env == 'dev') { console.info('Keeping connection alive'); - console.log(WebChannel.parent.i) } WebChannel.sendMessageTo(WebChannel.channel, 'keepalive', { 'payload': {} }); } + +function keepaliveTimer(WebChannel, startDelay = Genie.Settings.webchannels_keepalive_frequency) { + clearInterval(WebChannel.keepalive_interval); + setTimeout(() => { + keepalive(WebChannel); + WebChannel.keepalive_interval = setInterval(() => keepalive(WebChannel), Genie.Settings.webchannels_keepalive_frequency); + }, startDelay) +} \ No newline at end of file diff --git a/src/Elements.jl b/src/Elements.jl index 2301df9c..200ffeea 100644 --- a/src/Elements.jl +++ b/src/Elements.jl @@ -222,8 +222,7 @@ function vue_integration(::Type{M}; " try { if (Genie.Settings.webchannels_keepalive_frequency > 0) { - clearInterval(app.WebChannel.keepalive_interval); - app.WebChannel.keepalive_interval = setInterval(() => keepalive(app.WebChannel), Genie.Settings.webchannels_keepalive_frequency); + keepaliveTimer(app.WebChannel, 0); } } catch (e) { if (Genie.Settings.env === 'dev') { From 3ab7cb519d78943cdb16fed2e7b771b3965ae2f0 Mon Sep 17 00:00:00 2001 From: hhaensel Date: Mon, 11 Nov 2024 09:59:51 +0100 Subject: [PATCH 09/13] fix adding of revivers and initscripts, improve deps filtering --- assets/js/watchers.js | 4 ++-- src/Elements.jl | 2 +- src/Layout.jl | 23 ++++++++++++++++++++++- src/Stipple.jl | 1 - src/stipple/jsmethods.jl | 8 ++++---- 5 files changed, 29 insertions(+), 9 deletions(-) diff --git a/assets/js/watchers.js b/assets/js/watchers.js index a9b6214e..9f973cc9 100644 --- a/assets/js/watchers.js +++ b/assets/js/watchers.js @@ -67,7 +67,7 @@ const watcherMixin = { }, push: function (field) { - Genie.WebChannels.sendMessageTo(this.channel_, 'watchers', {'payload': { + this.WebChannel.sendMessageTo(this.channel_, 'watchers', {'payload': { 'field': field, 'newval': this[field], 'oldval': null, @@ -110,7 +110,7 @@ const eventMixin = { if (event_data === undefined) { event_data = {} } console.debug('event: ' + JSON.stringify(event_data) + ":" + event_handler) if (mode=='addclient') { event_data._addclient = true} - Genie.WebChannels.sendMessageTo(this.channel_, 'events', { + this.WebChannel.sendMessageTo(this.channel_, 'events', { 'event': { 'name': event_handler, 'event': event_data diff --git a/src/Elements.jl b/src/Elements.jl index 200ffeea..9c478aa6 100644 --- a/src/Elements.jl +++ b/src/Elements.jl @@ -177,7 +177,7 @@ function vue_integration(::Type{M}; }); $app = window.GENIEMODEL = app.mount(rootSelector); window.channelIndex = window.channelIndex || 0; - $app.WebChannel = Genie.AllWebChannels[channelIndex]; + $app.WebChannel = Genie.WebChannels; $app.WebChannel.parent = $app; $app.channel_ = $app.WebChannel.channel; diff --git a/src/Layout.jl b/src/Layout.jl index 03ac188f..4f02fc77 100644 --- a/src/Layout.jl +++ b/src/Layout.jl @@ -16,6 +16,25 @@ const THEMES = Ref(Function[]) const FLEXGRID_KWARGS = [:col, :xs, :sm, :md, :lg, :xl, :gutter, :xgutter, :ygutter] +""" + make_unique!(src::Vector, condition::Union{Nothing, Function} = nothing) + +Utility function for removing duplicates from a vector that fulfill a given condition. +""" +function make_unique!(src::Vector, condition::Union{Nothing, Function} = nothing) + seen = Int[] + dups = Int[] + for (i, name) in enumerate(src) + if name ∈ view(src, seen) && (condition === nothing || condition(name)) + push!(dups, i) + else + push!(seen, i) + end + end + + deleteat!(src, dups) +end + """ function layout(output::Union{String,Vector}; partial::Bool = false, title::String = "", class::String = "", style::String = "", head_content::String = "", channel::String = Genie.config.webchannels_default_route) :: String @@ -53,7 +72,9 @@ function layout(output::Union{S,Vector}, m::Union{M, Vector{M}}; output theme(; core_theme) Stipple.deps.(m)... - ] |> union + ] + + make_unique!(content, contains(r"src=|href="i)) partial && return content diff --git a/src/Stipple.jl b/src/Stipple.jl index e56815ce..2f22cabf 100644 --- a/src/Stipple.jl +++ b/src/Stipple.jl @@ -854,7 +854,6 @@ end function initscript(vue_app_name) :: String Genie.Renderer.Html.script([""" - // script id: $(randstring(64)) document.addEventListener("DOMContentLoaded", () => window.create$vue_app_name() ); """]) end diff --git a/src/stipple/jsmethods.jl b/src/stipple/jsmethods.jl index fb4017f4..7d6bc24c 100644 --- a/src/stipple/jsmethods.jl +++ b/src/stipple/jsmethods.jl @@ -204,9 +204,9 @@ myreviver: function(key, value) { return (key.endsWith('_onebased') ? value - 1 """ function js_add_reviver(revivername::String) """ - Genie.WebChannels.subscriptionHandlers.push(function(event) { + document.addEventListener("DOMContentLoaded", () => Genie.WebChannels.subscriptionHandlers.push(function(event) { Genie.Revivers.addReviver($revivername); - }); + })); """ end @@ -222,9 +222,9 @@ It needs to be added to the dependencies of an app in order to be executed, e.g. """ function js_initscript(initscript::String) """ - Genie.WebChannels.subscriptionHandlers.push(function(event) { + document.addEventListener("DOMContentLoaded", () => Genie.WebChannels.subscriptionHandlers.push(function(event) { $(initscript) - }); + })); """ end From e9e2c7e022b827224eccef9f66f5ee4e2f8ec62b Mon Sep 17 00:00:00 2001 From: hhaensel Date: Tue, 12 Nov 2024 08:49:48 +0100 Subject: [PATCH 10/13] shorter init, avoid duplicate revivers part II, turn off keepalive logs when disconnected --- assets/js/keepalive.js | 13 +++++++------ src/Elements.jl | 40 ++++++++++++++++++++-------------------- src/Stipple.jl | 10 +++++----- src/stipple/jsmethods.jl | 4 ++-- 4 files changed, 34 insertions(+), 33 deletions(-) diff --git a/assets/js/keepalive.js b/assets/js/keepalive.js index e5774014..ea84b6b5 100644 --- a/assets/js/keepalive.js +++ b/assets/js/keepalive.js @@ -14,13 +14,14 @@ function keepalive(WebChannel) { } } - if (Genie.Settings.env == 'dev') { - console.info('Keeping connection alive'); + if (!WebChannel.wsconnectionalert_triggered) { + if (Genie.Settings.env == 'dev') { + console.info('Keeping connection alive'); + } + WebChannel.sendMessageTo(WebChannel.channel, 'keepalive', { + 'payload': {} + }); } - - WebChannel.sendMessageTo(WebChannel.channel, 'keepalive', { - 'payload': {} - }); } function keepaliveTimer(WebChannel, startDelay = Genie.Settings.webchannels_keepalive_frequency) { diff --git a/src/Elements.jl b/src/Elements.jl index 9c478aa6..9b9dc12c 100644 --- a/src/Elements.jl +++ b/src/Elements.jl @@ -159,7 +159,7 @@ function vue_integration(::Type{M}; string( " - function initStipple$vue_app_name(appName, rootSelector){ + function initStipple$vue_app_name(appName, rootSelector, channel){ // components = Stipple.init($( core_theme ? "{theme: '$theme'}" : "" )); const app = Vue.createApp($( replace(vue_app, "'$(Stipple.UNDEFINED_PLACEHOLDER)'"=>Stipple.UNDEFINED_VALUE) )) /* Object.entries(components).forEach(([key, value]) => { @@ -177,9 +177,9 @@ function vue_integration(::Type{M}; }); $app = window.GENIEMODEL = app.mount(rootSelector); window.channelIndex = window.channelIndex || 0; - $app.WebChannel = Genie.WebChannels; + $app.WebChannel = Genie.initWebChannel(channel); $app.WebChannel.parent = $app; - $app.channel_ = $app.WebChannel.channel; + $app.channel_ = channel; channelIndex++; } // end of initStipple @@ -216,34 +216,34 @@ function vue_integration(::Type{M}; } function app_ready(app) { - app.isready = true; - Genie.Revivers.addReviver(app.revive_jsfunction); - $(transport == Genie.WebChannels && - " - try { - if (Genie.Settings.webchannels_keepalive_frequency > 0) { - keepaliveTimer(app.WebChannel, 0); - } - } catch (e) { - if (Genie.Settings.env === 'dev') { - console.error('Error setting WebSocket keepalive interval: ' + e); - } + if (app.WebChannel == Genie.AllWebChannels[0]) Genie.Revivers.addReviver(app.revive_jsfunction); + app.isready = true; + $(transport == Genie.WebChannels && + " + try { + if (Genie.Settings.webchannels_keepalive_frequency > 0) { + keepaliveTimer(app.WebChannel, 0); } - ") - + } catch (e) { if (Genie.Settings.env === 'dev') { - console.info('App starting'); + console.error('Error setting WebSocket keepalive interval: ' + e); } + } + ") + + if (Genie.Settings.env === 'dev') { + console.info('App starting'); + } }; - function create$vue_app_name() { + function create$vue_app_name(channel) { window.counter$vue_app_name = window.counter$vue_app_name || 1 const appName = '$vue_app_name' + ((counter$vue_app_name == 1) ? '' : '_' + window.counter$vue_app_name) rootSelector = '#$vue_app_name' + ((counter$vue_app_name == 1) ? '' : '-' + window.counter$vue_app_name) counter$vue_app_name++ if ( window.autorun === undefined || window.autorun === true ) { - initStipple$vue_app_name(appName, rootSelector); + initStipple$vue_app_name(appName, rootSelector, channel); initWatchers$vue_app_name($app); $app.WebChannel.subscriptionHandlers.push(function(event) { diff --git a/src/Stipple.jl b/src/Stipple.jl index 2f22cabf..0f2873ae 100644 --- a/src/Stipple.jl +++ b/src/Stipple.jl @@ -848,13 +848,13 @@ end function channelscript(channel::String) :: String Genie.Renderer.Html.script([""" - document.addEventListener("DOMContentLoaded", () => window.Genie.initWebChannel('$(channel)') ); + document.addEventListener('DOMContentLoaded', () => window.Genie.initWebChannel('$dchannel') ); """]) end -function initscript(vue_app_name) :: String +function initscript(vue_app_name, channel) :: String Genie.Renderer.Html.script([""" - document.addEventListener("DOMContentLoaded", () => window.create$vue_app_name() ); + document.addEventListener('DOMContentLoaded', () => window.create$vue_app_name('$channel') ); """]) end @@ -866,8 +866,8 @@ Outputs the HTML code necessary for injecting the dependencies in the page (the function deps(m::M) :: Vector{String} where {M<:ReactiveModel} channel = getchannel(m) output = [ - channelscript(channel), - initscript(vm(m)), + # channelscript(channel), + initscript(vm(m), channel), (is_channels_webtransport() ? Genie.Assets.channels_script_tag(channel) : Genie.Assets.webthreads_script_tag(channel)), Genie.Renderer.Html.script(src = Genie.Assets.asset_path(assets_config, :js, file="underscore-min")), Genie.Renderer.Html.script(src = Genie.Assets.asset_path(assets_config, :js, file=(Genie.Configuration.isprod() ? "vue.global.prod" : "vue.global"))), diff --git a/src/stipple/jsmethods.jl b/src/stipple/jsmethods.jl index 7d6bc24c..38ca1656 100644 --- a/src/stipple/jsmethods.jl +++ b/src/stipple/jsmethods.jl @@ -204,7 +204,7 @@ myreviver: function(key, value) { return (key.endsWith('_onebased') ? value - 1 """ function js_add_reviver(revivername::String) """ - document.addEventListener("DOMContentLoaded", () => Genie.WebChannels.subscriptionHandlers.push(function(event) { + document.addEventListener('DOMContentLoaded', () => Genie.WebChannels.subscriptionHandlers.push(function(event) { Genie.Revivers.addReviver($revivername); })); """ @@ -222,7 +222,7 @@ It needs to be added to the dependencies of an app in order to be executed, e.g. """ function js_initscript(initscript::String) """ - document.addEventListener("DOMContentLoaded", () => Genie.WebChannels.subscriptionHandlers.push(function(event) { + document.addEventListener('DOMContentLoaded', () => Genie.WebChannels.subscriptionHandlers.push(function(event) { $(initscript) })); """ From 573ff4a084482032435d715266ae9a94d4d76dde Mon Sep 17 00:00:00 2001 From: hhaensel Date: Tue, 12 Nov 2024 09:57:47 +0100 Subject: [PATCH 11/13] cleanup --- src/Elements.jl | 158 ++++++++++++++++++++++-------------------------- src/Stipple.jl | 3 +- 2 files changed, 72 insertions(+), 89 deletions(-) diff --git a/src/Elements.jl b/src/Elements.jl index 9b9dc12c..4f4947c7 100644 --- a/src/Elements.jl +++ b/src/Elements.jl @@ -130,7 +130,6 @@ function vue_integration(::Type{M}; debounce::Int = Stipple.JS_DEBOUNCE_TIME, transport::Module = Genie.WebChannels)::String where {M<:ReactiveModel} model = Base.invokelatest(M) - app = "window[appName]" vue_app = json(model |> Stipple.render) vue_app = replace(vue_app, "\"$(getchannel(model))\"" => Stipple.channel_js_name) @@ -157,103 +156,88 @@ function vue_integration(::Type{M}; output = string( - " - - function initStipple$vue_app_name(appName, rootSelector, channel){ - // components = Stipple.init($( core_theme ? "{theme: '$theme'}" : "" )); - const app = Vue.createApp($( replace(vue_app, "'$(Stipple.UNDEFINED_PLACEHOLDER)'"=>Stipple.UNDEFINED_VALUE) )) - /* Object.entries(components).forEach(([key, value]) => { - app.component(key, value) - }); */ - Stipple.init( app, $( core_theme ? "{theme: '$theme'}" : "" )); - $globalcomps - $comps - // gather legacy global options - app.prototype = {} - $(plugins(M)) - // apply legacy global options - Object.entries(app.prototype).forEach(([key, value]) => { - app.config.globalProperties[key] = value - }); - $app = window.GENIEMODEL = app.mount(rootSelector); - window.channelIndex = window.channelIndex || 0; - $app.WebChannel = Genie.initWebChannel(channel); - $app.WebChannel.parent = $app; - $app.channel_ = channel; - - channelIndex++; - } // end of initStipple - - " - - , - - " - - function initWatchers$vue_app_name(app){ - " - - , - join( - [Stipple.watch("app", field, Stipple.channel_js_name, debounce, model) for field in fieldnames(Stipple.get_concrete_type(M)) - if Stipple.has_frontend_watcher(field, model)] - ) - , - - " - } // end of initWatchers - - " - - , - """ - - window.parse_payload = function(WebChannel, payload){ - if (payload.key) { - WebChannel.parent.updateField(payload.key, payload.value); + window.parse_payload = function(WebChannel, payload){ + if (payload.key) { + WebChannel.parent.updateField(payload.key, payload.value); + } } - } - - function app_ready(app) { - if (app.WebChannel == Genie.AllWebChannels[0]) Genie.Revivers.addReviver(app.revive_jsfunction); - app.isready = true; - $(transport == Genie.WebChannels && - " - try { - if (Genie.Settings.webchannels_keepalive_frequency > 0) { - keepaliveTimer(app.WebChannel, 0); + + function app_ready(app) { + if (app.WebChannel == Genie.AllWebChannels[0]) Genie.Revivers.addReviver(app.revive_jsfunction); + app.isready = true; + """, + transport == Genie.WebChannels && + """ + try { + if (Genie.Settings.webchannels_keepalive_frequency > 0) { + keepaliveTimer(app.WebChannel, 0); + } + } catch (e) { + if (Genie.Settings.env === 'dev') { + console.error('Error setting WebSocket keepalive interval: ' + e); + } } - } catch (e) { + """, + """ if (Genie.Settings.env === 'dev') { - console.error('Error setting WebSocket keepalive interval: ' + e); + console.info('App starting'); } - } - ") + }; - if (Genie.Settings.env === 'dev') { - console.info('App starting'); - } - }; + function initStipple$vue_app_name(appName, rootSelector, channel){ + // components = Stipple.init($( core_theme ? "{theme: '$theme'}" : "" )); + const app = Vue.createApp($( replace(vue_app, "'$(Stipple.UNDEFINED_PLACEHOLDER)'"=>Stipple.UNDEFINED_VALUE) )) + /* Object.entries(components).forEach(([key, value]) => { + app.component(key, value) + }); */ + Stipple.init( app, $( core_theme ? "{theme: '$theme'}" : "" )); + $globalcomps + $comps + // gather legacy global options + app.prototype = {} + $(plugins(M)) + // apply legacy global options + Object.entries(app.prototype).forEach(([key, value]) => { + app.config.globalProperties[key] = value + }); + + const stippleApp = window[appName] = window.GENIEMODEL = app.mount(rootSelector); + stippleApp.WebChannel = Genie.initWebChannel(channel); + stippleApp.WebChannel.parent = stippleApp; + stippleApp.channel_ = channel; + + return stippleApp; + } // end of initStipple + + function initWatchers$vue_app_name(app){ + """, + join( + [Stipple.watch("app", field, Stipple.channel_js_name, debounce, model) for field in fieldnames(Stipple.get_concrete_type(M)) + if Stipple.has_frontend_watcher(field, model)] + ), + + """ + } // end of initWatchers - function create$vue_app_name(channel) { - window.counter$vue_app_name = window.counter$vue_app_name || 1 - const appName = '$vue_app_name' + ((counter$vue_app_name == 1) ? '' : '_' + window.counter$vue_app_name) - rootSelector = '#$vue_app_name' + ((counter$vue_app_name == 1) ? '' : '-' + window.counter$vue_app_name) - counter$vue_app_name++ + function create$vue_app_name(channel) { + window.counter$vue_app_name = window.counter$vue_app_name || 1 + const appName = '$vue_app_name' + ((counter$vue_app_name == 1) ? '' : '_' + window.counter$vue_app_name) + const rootSelector = '#$vue_app_name' + ((counter$vue_app_name == 1) ? '' : '-' + window.counter$vue_app_name) + counter$vue_app_name++ - if ( window.autorun === undefined || window.autorun === true ) { - initStipple$vue_app_name(appName, rootSelector, channel); - initWatchers$vue_app_name($app); + if ( window.autorun === undefined || window.autorun === true ) { + app = initStipple$vue_app_name(appName, rootSelector, channel); + initWatchers$vue_app_name(app); - $app.WebChannel.subscriptionHandlers.push(function(event) { - app_ready($app); - }); + app.WebChannel.subscriptionHandlers.push(function(event) { + app_ready(app); + }); + } } - } - // create$vue_app_name() - // is called via scipt with addEventListener to support multiple apps + // create$vue_app_name() + // is called via script with addEventListener to support multiple apps """ ) diff --git a/src/Stipple.jl b/src/Stipple.jl index 0f2873ae..4240aba1 100644 --- a/src/Stipple.jl +++ b/src/Stipple.jl @@ -845,7 +845,7 @@ function injectdeps(output::Vector{AbstractString}, M::Type{<:ReactiveModel}) :: output end - +# no longer needed, replaced by initscript function channelscript(channel::String) :: String Genie.Renderer.Html.script([""" document.addEventListener('DOMContentLoaded', () => window.Genie.initWebChannel('$dchannel') ); @@ -866,7 +866,6 @@ Outputs the HTML code necessary for injecting the dependencies in the page (the function deps(m::M) :: Vector{String} where {M<:ReactiveModel} channel = getchannel(m) output = [ - # channelscript(channel), initscript(vm(m), channel), (is_channels_webtransport() ? Genie.Assets.channels_script_tag(channel) : Genie.Assets.webthreads_script_tag(channel)), Genie.Renderer.Html.script(src = Genie.Assets.asset_path(assets_config, :js, file="underscore-min")), From 4783f0954f4ad417482aa69d03f278e8c6b61ac1 Mon Sep 17 00:00:00 2001 From: hhaensel Date: Tue, 12 Nov 2024 10:43:28 +0100 Subject: [PATCH 12/13] adapt channel tests, fix typo --- src/Stipple.jl | 2 +- test/runtests.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Stipple.jl b/src/Stipple.jl index 4240aba1..e424feed 100644 --- a/src/Stipple.jl +++ b/src/Stipple.jl @@ -848,7 +848,7 @@ end # no longer needed, replaced by initscript function channelscript(channel::String) :: String Genie.Renderer.Html.script([""" - document.addEventListener('DOMContentLoaded', () => window.Genie.initWebChannel('$dchannel') ); + document.addEventListener('DOMContentLoaded', () => window.Genie.initWebChannel('$channel') ); """]) end diff --git a/test/runtests.jl b/test/runtests.jl index 909837e3..22e17548 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -10,7 +10,7 @@ function string_get(x; kwargs...) end function get_channel(s::String) - match(r"initWebChannel\('([^']+)'", s).captures[1] + match(r"\(\) => window.create[^']+'([^']+)'", s).captures[1] end function get_debounce(port, modelname) From df8ec0009e9aa6c95992fb78cf3040a6f971e267 Mon Sep 17 00:00:00 2001 From: hhaensel Date: Tue, 12 Nov 2024 11:18:42 +0100 Subject: [PATCH 13/13] fix renaming in keepalive.js --- assets/js/keepalive.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/keepalive.js b/assets/js/keepalive.js index ea84b6b5..58200dc2 100644 --- a/assets/js/keepalive.js +++ b/assets/js/keepalive.js @@ -14,7 +14,7 @@ function keepalive(WebChannel) { } } - if (!WebChannel.wsconnectionalert_triggered) { + if (!WebChannel.ws_disconnected) { if (Genie.Settings.env == 'dev') { console.info('Keeping connection alive'); }