Skip to content

Commit

Permalink
add throttling
Browse files Browse the repository at this point in the history
  • Loading branch information
hhaensel committed Jan 1, 2025
1 parent f8a3a3f commit ab2bb74
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 16 deletions.
3 changes: 2 additions & 1 deletion src/Elements.jl
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ function vue_integration(::Type{M};
vue_app_name::String = "StippleApp",
core_theme::Bool = true,
debounce::Int = Stipple.JS_DEBOUNCE_TIME,
throttle::Int = Stipple.JS_THROTTLE_TIME,
transport::Module = Genie.WebChannels)::String where {M<:ReactiveModel}
model = Base.invokelatest(M)
vue_app = json(model |> Stipple.render)
Expand Down Expand Up @@ -213,7 +214,7 @@ function vue_integration(::Type{M};
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))
[Stipple.watch("app", field, Stipple.channel_js_name, debounce, throttle, model) for field in fieldnames(Stipple.get_concrete_type(M))
if Stipple.has_frontend_watcher(field, model)]
),

Expand Down
5 changes: 3 additions & 2 deletions src/Pages.jl
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ function Page( route::Union{Route,String};
layout::Union{Genie.Renderers.FilePath,<:AbstractString,ParsedHTMLString,Nothing,Function} = nothing,
context::Module = @__MODULE__,
debounce::Int = Stipple.JS_DEBOUNCE_TIME,
throttle::Int = Stipple.JS_THROTTLE_TIME,
transport::Module = Stipple.WEB_TRANSPORT[],
core_theme::Bool = true,
kwargs...
Expand All @@ -42,11 +43,11 @@ function Page( route::Union{Route,String};
Core.eval(context, model)
elseif isa(model, Module)
context = model
() -> Stipple.ReactiveTools.init_model(context; debounce, transport, core_theme)
() -> Stipple.ReactiveTools.init_model(context; debounce, throttle, transport, core_theme)
elseif model isa DataType
# as model is being redefined, we need to create a copy
mymodel = model
() -> Stipple.ReactiveTools.init_model(mymodel; debounce, transport, core_theme)
() -> Stipple.ReactiveTools.init_model(mymodel; debounce, throttle, transport, core_theme)
else
model
end
Expand Down
132 changes: 132 additions & 0 deletions src/ReactiveTools.jl
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export @deps, @clear_deps

# definition of field-specific debounce times
export @debounce, @clear_debounce
export @throttle, @clear_throttle

# deletion
export @clear, @clear_vars, @clear_handlers
Expand Down Expand Up @@ -348,6 +349,136 @@ macro clear_debounce()
:(Stipple.debounce(Stipple.@type(), nothing)) |> esc
end

"""
@throttle fieldname ms
@throttle App fieldname ms
Set field-specific throttle time in ms.
### Parameters
- `APP`: a subtype of ReactiveModel, e.g. `MyApp`
- `fieldname`: fieldname òr fieldnames as written in the declaration, e.g. `x`, `(x, y, z)`
- `ms`: throttle time in ms
### Example
#### Implicit apps
```
@app begin
@out quick = 12
@out slow = 12
@in s = "Hello"
end
# no throttling for fast messaging
@throttle quick 0
# long throttling for long-running tasks
@throttle (slow1, slow2) 1000
```
#### Explicit apps
```
@app MyApp begin
@out quick = 12
@out slow = 12
@in s = "Hello"
end
# no throttling for fast messaging
@throttle MyApp quick 0
# long throttling for long-running tasks
@throttle MyApp slow 1000
```
"""
macro throttle(M, fieldname, ms)
fieldname = _prepare(fieldname)
:(Stipple.throttle($M, $fieldname, $ms)) |> esc
end

macro throttle(fieldname, ms)
fieldname = _prepare(fieldname)
:(Stipple.throttle(Stipple.@type(), $fieldname, $ms)) |> esc
end

"""
@clear_throttle
@clear_throttle fieldname
@clear_throttle App
@clear_throttle App fieldname
Clear field-specific throttle time, for setting see `@throttle`.
After calling `@clear throttle` the field will be throttled by the value given in the
`@init` macro.
### Example
#### Implicit apps
```
@app begin
@out quick = 12
@out slow = 12
@in s = "Hello"
end
# set standard throttle time for all fields
@page("/", ui, model = MyApp, throttle = 100)
# no throttling for fast messaging
@throttle quick 0
@throttle slow 1000
# reset to standard value of the app
@clear_throttle quick
# clear all field-specific throttle times
@clear_throttle
```
#### Explicit apps
```
@app MyApp begin
@out quick = 12
@out slow = 12
@in s = "Hello"
end
# set standard throttle time for all fields
@page("/", ui, model = MyApp, throttle = 100)
# no throttling for fast messaging
@throttle MyApp quick 0
@clear_throttle MyApp quick
# clear all field-specific throttle times
@clear_throttle MyApp
```
"""
macro clear_throttle(M, fieldname)
fieldname = _prepare(fieldname)
:(Stipple.throttle($M, $fieldname, nothing)) |> esc
end

macro clear_throttle(expr)
quote
if $expr isa DataType && $expr <: Stipple.ReactiveModel
Stipple.throttle($expr, nothing)
else
Stipple.throttle(Stipple.@type(), $(_prepare(expr)), nothing)
end
end |> esc
end

macro clear_throttle()
:(Stipple.throttle(Stipple.@type(), nothing)) |> esc
end

import Stipple: @vars

macro vars(expr)
Expand Down Expand Up @@ -487,6 +618,7 @@ end
Create a new app with the following kwargs supported:
- `debounce::Int = JS_DEBOUNCE_TIME`
- `throttle::Int = JS_THROTTLE_TIME`
- `transport::Module = Genie.WebChannels`
- `core_theme::Bool = true`
Expand Down
59 changes: 47 additions & 12 deletions src/Stipple.jl
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ const PURGE_NUMBER_LIMIT = Ref(1000)
const PURGE_CHECK_DELAY = Ref(60)

const DEBOUNCE = LittleDict{Type{<:ReactiveModel}, LittleDict{Symbol, Any}}()
const THROTTLE = LittleDict{Type{<:ReactiveModel}, LittleDict{Symbol, Any}}()

"""
debounce(M::Type{<:ReactiveModel}, fieldnames::Union{Symbol, Vector{Symbol}}, debounce::Union{Int, Nothing} = nothing)
Expand Down Expand Up @@ -163,6 +164,39 @@ end

debounce(M::Type{<:ReactiveModel}, ::Nothing) = delete!(DEBOUNCE, M)

import Observables.throttle
"""
throttle(M::Type{<:ReactiveModel}, fieldnames::Union{Symbol, Vector{Symbol}}, debounce::Union{Int, Nothing} = nothing)
Add field-specific debounce times.
"""
function throttle(M::Type{<:ReactiveModel}, fieldnames::Union{Symbol, Vector{Symbol}, NTuple{N, Symbol} where N}, throttle::Union{Int, Nothing} = nothing)
if throttle === nothing
haskey(THROTTLE, M) || return
d = THROTTLE[M]
if fieldnames isa Symbol
delete!(d, fieldnames)
else
for v in fieldnames
delete!(d, v)
end
end
isempty(d) && delete!(THROTTLE, M)
else
d = get!(LittleDict{Symbol, Any}, THROTTLE, M)
if fieldnames isa Symbol
d[fieldnames] = throttle
else
for v in fieldnames
d[v] = throttle
end
end
end
return
end

throttle(M::Type{<:ReactiveModel}, ::Nothing) = delete!(THROTTLE, M)

"""
`function sorted_channels()`
Expand Down Expand Up @@ -343,7 +377,7 @@ include("stipple/mutators.jl")
Sets up default Vue.js watchers so that when the value `fieldname` of type `fieldtype` in model `vue_app_name` is
changed on the frontend, it is pushed over to the backend using `channel`, at a `debounce` minimum time interval.
"""
function watch(vue_app_name::String, fieldname::Symbol, channel::String, debounce::Int, model::M; jsfunction::String = "")::String where {M<:ReactiveModel}
function watch(vue_app_name::String, fieldname::Symbol, channel::String, debounce::Int, throttle::Int, model::M; jsfunction::String = "")::String where {M<:ReactiveModel}
js_channel = isempty(channel) ?
"window.Genie.Settings.webchannels_default_route" :
"$vue_app_name.channel_"
Expand All @@ -360,14 +394,13 @@ function watch(vue_app_name::String, fieldname::Symbol, channel::String, debounc
else
AM = get_abstract_type(M)
debounce = get(get(DEBOUNCE, AM, Dict{Symbol, Any}()), fieldname, debounce)
print(output, debounce == 0 ?
"""
({ignoreUpdates: $vue_app_name._ignore_$fieldname} = $vue_app_name.watchIgnorable(function(){return $vue_app_name.$fieldname}, function(newVal, oldVal){$jsfunction}, {deep: true}));
""" :
"""
({ignoreUpdates: $vue_app_name._ignore_$fieldname} = $vue_app_name.watchIgnorable(function(){return $vue_app_name.$fieldname}, _.debounce(function(newVal, oldVal){$jsfunction}, $debounce), {deep: true}));
"""
)
throttle = get(get(THROTTLE, AM, Dict{Symbol, Any}()), fieldname, throttle)
fn = "function(newVal, oldVal){$jsfunction}"
throttle > 0 && (fn = "_.throttle($fn, $throttle)")
debounce > 0 && (fn = "_.debounce($fn, $debounce)")
print(output, """
({ignoreUpdates: $vue_app_name._ignore_$fieldname} = $vue_app_name.watchIgnorable(function(){return $vue_app_name.$fieldname}, $fn, {deep: true}));
""")
end

String(take!(output))
Expand Down Expand Up @@ -485,6 +518,7 @@ end
endpoint::S = vue_app_name,
channel::Union{Any,Nothing} = nothing,
debounce::Int = JS_DEBOUNCE_TIME,
throttle::Int = JS_THROTTLE_TIME,
transport::Module = Genie.WebChannels,
core_theme::Bool = true)::M where {M<:ReactiveModel, S<:AbstractString}
Expand All @@ -502,6 +536,7 @@ function init(t::Type{M};
endpoint::S = vue_app_name,
channel::Union{Any,Nothing} = channeldefault(t),
debounce::Int = JS_DEBOUNCE_TIME,
throttle::Int = JS_THROTTLE_TIME,
transport::Module = Genie.WebChannels,
core_theme::Bool = true,
always_register_channels::Bool = ALWAYS_REGISTER_CHANNELS[])::M where {M<:ReactiveModel, S<:AbstractString}
Expand Down Expand Up @@ -613,7 +648,7 @@ function init(t::Type{M};
end
end

haskey(DEPS, AM) || (DEPS[AM] = stipple_deps(AM, vue_app_name, debounce, core_theme, endpoint, transport))
haskey(DEPS, AM) || (DEPS[AM] = stipple_deps(AM, vue_app_name, debounce, throttle, core_theme, endpoint, transport))

setup(model, channel)
end
Expand All @@ -624,12 +659,12 @@ function routename(::Type{M}) where M<:ReactiveModel
replace(s, r"[^0-9a-zA-Z_]+" => "")
end

function stipple_deps(::Type{M}, vue_app_name, debounce, core_theme, endpoint, transport)::Function where {M<:ReactiveModel}
function stipple_deps(::Type{M}, vue_app_name, debounce, throttle, core_theme, endpoint, transport)::Function where {M<:ReactiveModel}
() -> begin
if ! Genie.Assets.external_assets(assets_config)
if ! Genie.Router.isroute(Symbol(routename(M)))
Genie.Router.route(Genie.Assets.asset_route(assets_config, :js, file = endpoint), named = Symbol(routename(M))) do
Stipple.Elements.vue_integration(M; vue_app_name, debounce, core_theme, transport) |> Genie.Renderer.Js.js
Stipple.Elements.vue_integration(M; vue_app_name, debounce, throttle, core_theme, transport) |> Genie.Renderer.Js.js
end
end
end
Expand Down
9 changes: 8 additions & 1 deletion src/stipple/reactivity.jl
Original file line number Diff line number Diff line change
Expand Up @@ -540,8 +540,15 @@ Base.string(ex::MissingPropertyException) = "Entity $entity does not have requir
"""
const JS_DEBOUNCE_TIME
Debounce time used to indicate the minimum frequency for sending data payloads to the backend (for example to batch send
Debounce time used to indicate the minimum duration that an input must pause before a front-end change is sent to the backend (for example to batch send
payloads when the user types into an text field, to avoid overloading the server).
"""
const JS_DEBOUNCE_TIME = 300 #ms
"""
const JS_THROTTLE_TIME
Throttle time used to indicate the minimum duration before a new input signal is sent to the backend (for example to update a model variable with a
lower frequency, to avoid overloading the server).
"""
const JS_THROTTLE_TIME = 0 #ms
const SETTINGS = Settings()

0 comments on commit ab2bb74

Please sign in to comment.