Skip to content

Instantly share code, notes, and snippets.

@catmando
Created February 17, 2021 02:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save catmando/01fe5c31eb95ef949702ed953a6a8c05 to your computer and use it in GitHub Desktop.
Save catmando/01fe5c31eb95ef949702ed953a6a8c05 to your computer and use it in GitHub Desktop.
# app/hyperstack/libs/throttle.rb
class Throttle
# call the provided block (using the run method) no faster than the
# specified update frequency.
def initialize(update_frequency = 0.5, &block)
@update_frequency = update_frequency
@block = block
@last_start_time = Time.now - update_frequency
end
attr_reader :update_frequency
# To do this we wait for the block operation to complete or resolve
# and then wait if necessary, and then if there is another set of args
# to send repeat the process.
def throttle(args)
# wait until the end of the period then run again if there are new args
after([@update_frequency - (Time.now - @last_start_time), 0].max) do
@last_start_time = Time.now
@promise = nil
run(*@args) if @args && @args != args
end
end
def run(*args)
# copy the args and return if we already have a promise in play
return (@args = args) if @promise
# otherwise clear the @args variable,
# use load to get a promise that will resolve the block completes,
# and then throttle, and if there is a new @args value start the process
# over again.
@args = nil
@promise = Hyperstack::Model.load { @block.call(*args) }.then { throttle(args) }
end
end
# app/hyperstack/components/throttled_slider.rb
class ThrottledSlider < HyperComponent
# Generalized component that displays a range input.
# While the user is moving the slider the component will
# fire the update event at the specified frequency.
# The current value of the input is provided as the value param.
# Any other params such as the min, max, classes and styles can also
# be specified.
param :value
param frequency: 0.5
others :opts
fires :update
before_mount do
@throttle = Throttle.new(frequency) do |new_value|
update!(new_value)
end
end
# While the user is not interacting with the component we want the
# component to reflect any value changes (say from other
# sessions.) Once the user begins moving the slider we want the
# slider to act purely as an uncontrolled component. This prevents
# any jitters as we send and receive data from the IOT hub and entity.
def ignore_external_changes
# lock the value to the current param value
# note that even though locked, because its an uncontrolled component
# the user will be able to move change the value. But by keeping the
# component uncontrolled and locking the initial value we get a smooth
# UI response.
mutate @locked_value = value
end
def follow_external_changes
# Begin tracking changes external changes, but wait a bit for any
# messages to make the round trip, so the control doesn't
# jitter when the mouse is released. We assume 3 times the update frequency
# is sufficient.
after(@throttle.update_frequency * 3) { mutate @locked_value = nil }
end
def current_value
# use either the locked value or the value param.
@locked_value || value
end
render do
DIV(key: current_value) do
INPUT(opts, type: :range, defaultValue: current_value)
.on(:mouse_down) { ignore_external_changes }
.on(:mouse_up) { follow_external_changes }
.on(:change) { |e| @throttle.run(e.target.value) }
end
end
end
# example useage:
ThrottledSlider(min: 1, max: 254, style: { appearance: 'slider-vertical' }, value: entity.current_brightness)
.on(:update) do |brightness|
Sample.first.set_brightness(brightness)
Sample.first.clear_cached_values(:set_brightness)
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment