diff --git a/assets/js/keepalive.js b/assets/js/keepalive.js index 449e890b..58200dc2 100644 --- a/assets/js/keepalive.js +++ b/assets/js/keepalive.js @@ -1,21 +1,33 @@ /* -** 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() { - if (window._lastMessageAt !== undefined) { - if (Date.now() - window._lastMessageAt < Genie.Settings.webchannels_keepalive_frequency) { - return +function keepalive(WebChannel) { + if (WebChannel.lastMessageAt !== undefined) { + 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'); + if (!WebChannel.ws_disconnected) { + if (Genie.Settings.env == 'dev') { + console.info('Keeping connection alive'); + } + WebChannel.sendMessageTo(WebChannel.channel, 'keepalive', { + 'payload': {} + }); } - - Genie.WebChannels.sendMessageTo(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/assets/js/watchers.js b/assets/js/watchers.js index 6d87f2d6..9f973cc9 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': { + 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(window.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 08c61980..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) - vue_app = json(model |> Stipple.render) vue_app = replace(vue_app, "\"$(getchannel(model))\"" => Stipple.channel_js_name) @@ -157,89 +156,88 @@ function vue_integration(::Type{M}; output = string( - " - - function initStipple(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]) => { - 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 - }); - window.$vue_app_name = window.GENIEMODEL = app.mount(rootSelector); - } // end of initStipple - - " - - , - - " - - function initWatchers(){ - " - - , - join( - [Stipple.watch(string("window.", vue_app_name), 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(payload){ - if (payload.key) { - window.$(vue_app_name).updateField(payload.key, payload.value); + window.parse_payload = function(WebChannel, payload){ + if (payload.key) { + WebChannel.parent.updateField(payload.key, payload.value); + } } - } - - function app_ready() { - $vue_app_name.channel_ = window.CHANNEL; - $vue_app_name.isready = true; - Genie.Revivers.addReviver(window.$(vue_app_name).revive_jsfunction); - $(transport == Genie.WebChannels && - " + + 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) { - clearInterval($vue_app_name.keepalive_interval); - $vue_app_name.keepalive_interval = setInterval(keepalive, Genie.Settings.webchannels_keepalive_frequency); + keepaliveTimer(app.WebChannel, 0); } } catch (e) { if (Genie.Settings.env === 'dev') { console.error('Error setting WebSocket keepalive interval: ' + e); } } - ") - + """, + """ if (Genie.Settings.env === 'dev') { console.info('App starting'); } - }; + }; - if ( window.autorun === undefined || window.autorun === true ) { - initStipple('#$vue_app_name'); - initWatchers(); + 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) + 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 ) { + app = initStipple$vue_app_name(appName, rootSelector, channel); + initWatchers$vue_app_name(app); + + app.WebChannel.subscriptionHandlers.push(function(event) { + app_ready(app); + }); + } + } - Genie.WebChannels.subscriptionHandlers.push(function(event) { - app_ready(); - }); - } + // create$vue_app_name() + // is called via script with addEventListener to support multiple apps """ ) diff --git a/src/Layout.jl b/src/Layout.jl index 86be5b9a..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 @@ -41,19 +60,22 @@ 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)... ] + make_unique!(content, contains(r"src=|href="i)) + partial && return content Genie.Renderer.Html.doc( @@ -84,21 +106,34 @@ julia> page(:elemid, [ "\n