motion 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7839c6a006522aecfde98e996d77d5af65d3f50a3472d54a84bb9c59c13b2a88
4
- data.tar.gz: 3011e0696be6a98232276b35310953939cca6f88d13e85c253f2004b98cc059e
3
+ metadata.gz: 63f815dc74ec8255bd6b7b07fae885691253e7903397ec2b6f1e79cc675c7261
4
+ data.tar.gz: 758239f08e068912a6e213773c0cc3d86338ecec6f738650f19f1a9d83656995
5
5
  SHA512:
6
- metadata.gz: 7195a8b10a5785ea41b46958b886d06ed8e19cd376879d3c1a1d7c14e5ef7be403df16b55f59d1276c1b7111d4268b6280f8d7326cc3e5f6e31aac555376e6c6
7
- data.tar.gz: 3f2df4a07139f7ac39fb2733ae3f7ccaf20d9cffd8a8e7c34b1b7e6dd9112fc69de20129e592fe4dacf7dfff5a0c12f15aab896dcea854ab093c795ab69df71d
6
+ metadata.gz: dfc01de3b79448af437bc4a0386ab2a9800e75ecb7d0231dc565d2bb6e3a21699fc9d7e6b58932824f0dc9ced1c59ae4426e40b9486198f7d082dcf0a5d7ee68
7
+ data.tar.gz: 86223b046b0957a69d4d7301a0a6c5d85a51cdacd4b088eaa4b65fdd57643043bde5970aaa5de55fd093257b66f5cd2e06c320b891c02f7ec255ba1b3d7a824b
@@ -4,10 +4,16 @@ require "motion"
4
4
 
5
5
  module Motion
6
6
  module ActionCableExtentions
7
+ autoload :DeclarativeNotifications,
8
+ "motion/action_cable_extentions/declarative_notifications"
9
+
7
10
  autoload :DeclarativeStreams,
8
11
  "motion/action_cable_extentions/declarative_streams"
9
12
 
10
13
  autoload :LogSuppression,
11
14
  "motion/action_cable_extentions/log_suppression"
15
+
16
+ autoload :Synchronization,
17
+ "motion/action_cable_extentions/synchronization"
12
18
  end
13
19
  end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "motion"
4
+
5
+ module Motion
6
+ module ActionCableExtentions
7
+ # Provides a `periodically_notify(broadcasts, to:)` API that can be used to
8
+ # declaratively specify when a handler should be called.
9
+ module DeclarativeNotifications
10
+ include Synchronization
11
+
12
+ def initialize(*)
13
+ super
14
+
15
+ # The current set of declarative notifications
16
+ @_declarative_notifications = {}
17
+
18
+ # The active timers for the declarative notifications
19
+ @_declarative_notifications_timers = {}
20
+
21
+ # The method we are routing declarative notifications to
22
+ @_declarative_notifications_target = nil
23
+ end
24
+
25
+ def declarative_notifications
26
+ @_declarative_notifications
27
+ end
28
+
29
+ def periodically_notify(notifications, via:)
30
+ (@_declarative_notifications.to_a - notifications.to_a)
31
+ .each do |notification, _interval|
32
+ _shutdown_declarative_notifcation_timer(notification)
33
+ end
34
+
35
+ (notifications.to_a - @_declarative_notifications.to_a)
36
+ .each do |notification, interval|
37
+ _setup_declarative_notifcation_timer(notification, interval)
38
+ end
39
+
40
+ @_declarative_notifications = notifications
41
+ @_declarative_notifications_target = via
42
+ end
43
+
44
+ private
45
+
46
+ def stop_periodic_timers
47
+ super
48
+
49
+ @_declarative_notifications.clear
50
+ @_declarative_notifications_timers.clear
51
+ @_declarative_notifications_target = nil
52
+ end
53
+
54
+ # The only public interface in ActionCable for defining periodic timers is
55
+ # exposed at the class level. Looking at the source though, it is easy to
56
+ # see that new timers can be setup with `start_periodic_timer`. To ensure
57
+ # that we do not leak any timers, it is important to store these instances
58
+ # in `active_periodic_timers` so that ActionCable cleans them up for us
59
+ # when the channel shuts down. Also, periodic timers are not supported by
60
+ # the testing adapter, so we have to skip all of this in unit tests (it
61
+ # _will_ be covered in systems tests though).
62
+ #
63
+ # See `ActionCable::Channel::PeriodicTimers` for details.
64
+ def _setup_declarative_notifcation_timer(notification, interval)
65
+ return if connection.is_a?(ActionCable::Channel::ConnectionStub) ||
66
+ @_declarative_notifications_timers.include?(notification)
67
+
68
+ callback = proc do
69
+ synchronize_entrypoint! do
70
+ _handle_declarative_notifcation(notification)
71
+ end
72
+ end
73
+
74
+ timer = start_periodic_timer(callback, every: interval)
75
+
76
+ @_declarative_notifications_timers[notification] = timer
77
+ active_periodic_timers << timer
78
+ end
79
+
80
+ def _shutdown_declarative_notifcation_timer(notification, *)
81
+ timer = @_declarative_notifications_timers.delete(notification)
82
+ return unless timer
83
+
84
+ timer.shutdown
85
+ active_periodic_timers.delete(timer)
86
+ end
87
+
88
+ def _handle_declarative_notifcation(notification)
89
+ return unless @_declarative_notifications_target &&
90
+ @_declarative_notifications.include?(notification)
91
+
92
+ send(@_declarative_notifications_target, notification)
93
+ end
94
+ end
95
+ end
96
+ end
@@ -6,19 +6,13 @@ module Motion
6
6
  module ActionCableExtentions
7
7
  # Provides a `streaming_from(broadcasts, to:)` API that can be used to
8
8
  # declaratively specify what `broadcasts` the channel is interested in
9
- # receiving and `to` what method they should be routed. Additionally,
10
- # this module extends the "at most one executor at a time" property that
11
- # naturally comes with actions to the streams that it sets up as well.
9
+ # receiving and `to` what method they should be routed.
12
10
  module DeclarativeStreams
11
+ include Synchronization
12
+
13
13
  def initialize(*)
14
14
  super
15
15
 
16
- # Allowing actions to be bound to streams (as this module provides)
17
- # introduces the possibiliy of multiple threads accessing user code at
18
- # the same time. Protect user code with a Monitor so we only have to
19
- # worry about that here.
20
- @_declarative_stream_monitor = Monitor.new
21
-
22
16
  # Streams that we are currently interested in
23
17
  @_declarative_streams = Set.new
24
18
 
@@ -30,19 +24,6 @@ module Motion
30
24
  @_declarative_stream_proxies = Set.new
31
25
  end
32
26
 
33
- # Synchronize all ActionCable entry points (after initialization).
34
- def subscribe_to_channel(*)
35
- @_declarative_stream_monitor.synchronize { super }
36
- end
37
-
38
- def unsubscribe_from_channel(*)
39
- @_declarative_stream_monitor.synchronize { super }
40
- end
41
-
42
- def perform_action(*)
43
- @_declarative_stream_monitor.synchronize { super }
44
- end
45
-
46
27
  # Clean up declarative streams when all streams are stopped.
47
28
  def stop_all_streams
48
29
  super
@@ -73,32 +54,17 @@ module Motion
73
54
  # TODO: I feel like the fact that we have to specify the coder here is
74
55
  # a bug in ActionCable. It should be the default for this karg.
75
56
  stream_from(broadcast, coder: ActiveSupport::JSON) do |message|
76
- _handle_incoming_broadcast_to_declarative_stream(broadcast, message)
77
- rescue Exception => exception # rubocop:disable Lint/RescueException
78
- # It is very, very important that we do not allow an exception to
79
- # escape here as the internals of ActionCable will stop processing
80
- # the broadcast.
81
-
82
- _handle_exception_in_declarative_stream(broadcast, exception)
57
+ synchronize_entrypoint! do
58
+ _handle_incoming_broadcast_to_declarative_stream(broadcast, message)
59
+ end
83
60
  end
84
61
  end
85
62
 
86
63
  def _handle_incoming_broadcast_to_declarative_stream(broadcast, message)
87
- @_declarative_stream_monitor.synchronize do
88
- return unless @_declarative_stream_target &&
89
- @_declarative_streams.include?(broadcast)
90
-
91
- send(@_declarative_stream_target, broadcast, message)
92
- end
93
- end
64
+ return unless @_declarative_stream_target &&
65
+ @_declarative_streams.include?(broadcast)
94
66
 
95
- def _handle_exception_in_declarative_stream(broadcast, exception)
96
- logger.error(
97
- "There was an exception while handling a broadcast to #{broadcast}" \
98
- "on #{self.class}:\n" \
99
- " #{exception.class}: #{exception.message}\n" \
100
- "#{exception.backtrace.map { |line| " #{line}" }.join("\n")}"
101
- )
67
+ send(@_declarative_stream_target, broadcast, message)
102
68
  end
103
69
  end
104
70
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "motion"
4
+
5
+ module Motion
6
+ module ActionCableExtentions
7
+ module Synchronization
8
+ def initialize(*)
9
+ super
10
+
11
+ @_monitor = Monitor.new
12
+ end
13
+
14
+ # Additional entrypoints added by other modules should wrap any entry
15
+ # points that they add with this.
16
+ def synchronize_entrypoint!(&block)
17
+ @_monitor.synchronize(&block)
18
+ end
19
+
20
+ # Synchronize all standard ActionCable entry points.
21
+ def subscribe_to_channel(*)
22
+ synchronize_entrypoint! { super }
23
+ end
24
+
25
+ def unsubscribe_from_channel(*)
26
+ synchronize_entrypoint! { super }
27
+ end
28
+
29
+ def perform_action(*)
30
+ synchronize_entrypoint! { super }
31
+ end
32
+ end
33
+ end
34
+ end
@@ -6,6 +6,7 @@ require "motion"
6
6
 
7
7
  module Motion
8
8
  class Channel < ActionCable::Channel::Base
9
+ include ActionCableExtentions::DeclarativeNotifications
9
10
  include ActionCableExtentions::DeclarativeStreams
10
11
  include ActionCableExtentions::LogSuppression
11
12
 
@@ -56,10 +57,19 @@ module Motion
56
57
  synchronize
57
58
  end
58
59
 
60
+ def process_periodic_timer(timer)
61
+ component_connection.process_periodic_timer(timer)
62
+ synchronize
63
+ end
64
+
59
65
  private
60
66
 
61
67
  def synchronize
62
- streaming_from(component_connection.broadcasts, to: :process_broadcast)
68
+ streaming_from component_connection.broadcasts,
69
+ to: :process_broadcast
70
+
71
+ periodically_notify component_connection.periodic_timers,
72
+ via: :process_periodic_timer
63
73
 
64
74
  component_connection.if_render_required do |component|
65
75
  transmit(renderer.render(component))
@@ -7,6 +7,7 @@ require "motion"
7
7
  require "motion/component/broadcasts"
8
8
  require "motion/component/lifecycle"
9
9
  require "motion/component/motions"
10
+ require "motion/component/periodic_timers"
10
11
  require "motion/component/rendering"
11
12
 
12
13
  module Motion
@@ -16,6 +17,7 @@ module Motion
16
17
  include Broadcasts
17
18
  include Lifecycle
18
19
  include Motions
20
+ include PeriodicTimers
19
21
  include Rendering
20
22
  end
21
23
  end
@@ -3,6 +3,7 @@
3
3
  require "active_support/concern"
4
4
  require "active_support/core_ext/class/attribute"
5
5
  require "active_support/core_ext/object/to_param"
6
+ require "active_support/core_ext/hash/except"
6
7
 
7
8
  require "motion"
8
9
 
@@ -11,6 +12,31 @@ module Motion
11
12
  module Broadcasts
12
13
  extend ActiveSupport::Concern
13
14
 
15
+ # Analogous to `module_function` (available on both class and instance)
16
+ module ModuleFunctions
17
+ def stream_from(broadcast, handler)
18
+ self._broadcast_handlers =
19
+ _broadcast_handlers.merge(broadcast.to_s => handler.to_sym).freeze
20
+ end
21
+
22
+ def stop_streaming_from(broadcast)
23
+ self._broadcast_handlers =
24
+ _broadcast_handlers.except(broadcast.to_s).freeze
25
+ end
26
+
27
+ def stream_for(model, handler)
28
+ stream_from(broadcasting_for(model), handler)
29
+ end
30
+
31
+ def stop_streaming_for(model)
32
+ stop_streaming_from(broadcasting_for(model))
33
+ end
34
+
35
+ def broadcasts
36
+ _broadcast_handlers.keys
37
+ end
38
+ end
39
+
14
40
  included do
15
41
  class_attribute :_broadcast_handlers,
16
42
  instance_reader: false,
@@ -20,26 +46,19 @@ module Motion
20
46
  end
21
47
 
22
48
  class_methods do
49
+ include ModuleFunctions
50
+
23
51
  def broadcast_to(model, message)
24
52
  ActionCable.server.broadcast(broadcasting_for(model), message)
25
53
  end
26
54
 
27
- def stream_from(broadcast, handler)
28
- self._broadcast_handlers =
29
- _broadcast_handlers.merge(broadcast.to_s => handler.to_sym).freeze
30
- end
31
-
32
- def stream_for(model, handler)
33
- stream_from(broadcasting_for(model), handler)
34
- end
35
-
36
55
  def broadcasting_for(model)
37
56
  serialize_broadcasting([name, model])
38
57
  end
39
58
 
40
59
  private
41
60
 
42
- # Taken from ActionCable::Channel::Broadcasting
61
+ # This definition is copied from ActionCable::Channel::Broadcasting
43
62
  def serialize_broadcasting(object)
44
63
  if object.is_a?(Array)
45
64
  object.map { |m| serialize_broadcasting(m) }.join(":")
@@ -51,31 +70,24 @@ module Motion
51
70
  end
52
71
  end
53
72
 
54
- def broadcasts
55
- _broadcast_handlers.keys
56
- end
73
+ include ModuleFunctions
57
74
 
58
75
  def process_broadcast(broadcast, message)
59
76
  return unless (handler = _broadcast_handlers[broadcast])
60
77
 
61
- send(handler, message)
62
- end
63
-
64
- def broadcast_to(model, message)
65
- self.class.broadcast_to(model, message)
78
+ if method(handler).arity.zero?
79
+ send(handler)
80
+ else
81
+ send(handler, message)
82
+ end
66
83
  end
67
84
 
68
- def stream_from(broadcast, handler)
69
- self._broadcast_handlers =
70
- _broadcast_handlers.merge(broadcast.to_s => handler.to_sym).freeze
71
- end
85
+ private
72
86
 
73
- def stream_for(model, handler)
74
- stream_from(self.class.broadcasting_for(model), handler)
87
+ def broadcasting_for(model)
88
+ self.class.broadcasting_for(model)
75
89
  end
76
90
 
77
- private
78
-
79
91
  attr_writer :_broadcast_handlers
80
92
 
81
93
  def _broadcast_handlers
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "active_support/concern"
4
4
  require "active_support/core_ext/class/attribute"
5
+ require "active_support/core_ext/hash/except"
5
6
 
6
7
  require "motion"
7
8
 
@@ -10,6 +11,23 @@ module Motion
10
11
  module Motions
11
12
  extend ActiveSupport::Concern
12
13
 
14
+ # Analogous to `module_function` (available on both class and instance)
15
+ module ModuleFunctions
16
+ def map_motion(motion, handler = motion)
17
+ self._motion_handlers =
18
+ _motion_handlers.merge(motion.to_s => handler.to_sym).freeze
19
+ end
20
+
21
+ def unmap_motion(motion)
22
+ self._motion_handlers =
23
+ _motion_handlers.except(motion.to_s).freeze
24
+ end
25
+
26
+ def motions
27
+ _motion_handlers.keys
28
+ end
29
+ end
30
+
13
31
  included do
14
32
  class_attribute :_motion_handlers,
15
33
  instance_reader: false,
@@ -19,15 +37,10 @@ module Motion
19
37
  end
20
38
 
21
39
  class_methods do
22
- def map_motion(motion, handler = motion)
23
- self._motion_handlers =
24
- _motion_handlers.merge(motion.to_s => handler.to_sym).freeze
25
- end
40
+ include ModuleFunctions
26
41
  end
27
42
 
28
- def motions
29
- _motion_handlers.keys
30
- end
43
+ include ModuleFunctions
31
44
 
32
45
  def process_motion(motion, event = nil)
33
46
  unless (handler = _motion_handlers[motion])
@@ -41,11 +54,6 @@ module Motion
41
54
  end
42
55
  end
43
56
 
44
- def map_motion(motion, handler = motion)
45
- self._motion_handlers =
46
- _motion_handlers.merge(motion.to_s => handler.to_sym).freeze
47
- end
48
-
49
57
  private
50
58
 
51
59
  attr_writer :_motion_handlers
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+ require "active_support/core_ext/class/attribute"
5
+ require "active_support/core_ext/hash/except"
6
+
7
+ require "motion"
8
+
9
+ module Motion
10
+ module Component
11
+ module PeriodicTimers
12
+ extend ActiveSupport::Concern
13
+
14
+ # Analogous to `module_function` (available on both class and instance)
15
+ module ModuleFunctions
16
+ def every(interval, handler, name: handler)
17
+ periodic_timer(name, handler, every: interval)
18
+ end
19
+
20
+ def periodic_timer(name, handler = name, every:)
21
+ self._periodic_timers =
22
+ _periodic_timers.merge(name.to_s => [handler.to_sym, every]).freeze
23
+ end
24
+
25
+ def stop_periodic_timer(name)
26
+ self._periodic_timers =
27
+ _periodic_timers.except(name.to_s).freeze
28
+ end
29
+
30
+ def periodic_timers
31
+ _periodic_timers.transform_values { |_handler, interval| interval }
32
+ end
33
+ end
34
+
35
+ included do
36
+ class_attribute :_periodic_timers,
37
+ instance_reader: false,
38
+ instance_writer: false,
39
+ instance_predicate: false,
40
+ default: {}.freeze
41
+ end
42
+
43
+ class_methods do
44
+ include ModuleFunctions
45
+ end
46
+
47
+ include ModuleFunctions
48
+
49
+ def process_periodic_timer(name)
50
+ return unless (handler, _interval = _periodic_timers[name])
51
+
52
+ send(handler)
53
+ end
54
+
55
+ private
56
+
57
+ attr_writer :_periodic_timers
58
+
59
+ def _periodic_timers
60
+ return @_periodic_timers if defined?(@_periodic_timers)
61
+
62
+ self.class._periodic_timers
63
+ end
64
+ end
65
+ end
66
+ end
@@ -24,16 +24,7 @@ module Motion
24
24
  # * If it doesn't change every time the component's state changes,
25
25
  # things may fall out of sync unless you also call `#rerender!`
26
26
  def render_hash
27
- # TODO: This implementation is trivially correct, but very wasteful.
28
- #
29
- # Is something with Ruby's built-in `hash` Good Enough(TM)?
30
- #
31
- # instance_variables
32
- # .map { |ivar| instance_variable_get(ivar).hash }
33
- # .reduce(0, &:^)
34
-
35
- key, _state = Motion.serializer.serialize(self)
36
- key
27
+ Motion.serializer.weak_digest(self)
37
28
  end
38
29
 
39
30
  def render_in(view_context)
@@ -64,6 +64,18 @@ module Motion
64
64
  false
65
65
  end
66
66
 
67
+ def process_periodic_timer(timer)
68
+ timing("Proccessed periodic timer #{timer}") do
69
+ component.process_periodic_timer timer
70
+ end
71
+
72
+ true
73
+ rescue => error
74
+ handle_error(error, "processing periodic timer #{timer}")
75
+
76
+ false
77
+ end
78
+
67
79
  def if_render_required(&block)
68
80
  timing("Rendered") do
69
81
  next_render_hash = component.render_hash
@@ -83,6 +95,10 @@ module Motion
83
95
  component.broadcasts
84
96
  end
85
97
 
98
+ def periodic_timers
99
+ component.periodic_timers
100
+ end
101
+
86
102
  private
87
103
 
88
104
  attr_reader :log_helper
@@ -34,8 +34,16 @@ module Motion
34
34
  @target = Motion::Element.from_raw(raw["target"])
35
35
  end
36
36
 
37
+ def current_target
38
+ return @current_target if defined?(@current_target)
39
+
40
+ @current_target = Motion::Element.from_raw(raw["currentTarget"])
41
+ end
42
+
43
+ alias element current_target
44
+
37
45
  def form_data
38
- target&.form_data
46
+ element&.form_data
39
47
  end
40
48
  end
41
49
  end
@@ -21,6 +21,8 @@ module Motion
21
21
  end
22
22
 
23
23
  def add_state_to_html(component, html)
24
+ return html if html.blank?
25
+
24
26
  key, state = serializer.serialize(component)
25
27
 
26
28
  transform_root(component, html) do |root|
@@ -35,7 +37,9 @@ module Motion
35
37
  fragment = Nokogiri::HTML::DocumentFragment.parse(html)
36
38
  root, *unexpected_others = fragment.children
37
39
 
38
- raise MultipleRootsError, component if unexpected_others.any?(&:present?)
40
+ if !root || unexpected_others.any?(&:present?)
41
+ raise MultipleRootsError, component
42
+ end
39
43
 
40
44
  yield root
41
45
 
@@ -32,6 +32,10 @@ module Motion
32
32
  @revision = revision
33
33
  end
34
34
 
35
+ def weak_digest(component)
36
+ dump(component).hash
37
+ end
38
+
35
39
  def serialize(component)
36
40
  state = dump(component)
37
41
  state_with_revision = "#{revision}#{NULL_BYTE}#{state}"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Motion
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: motion
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alec Larsen
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2020-06-29 00:00:00.000000000 Z
12
+ date: 2020-07-03 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: nokogiri
@@ -55,13 +55,16 @@ files:
55
55
  - lib/generators/motion/templates/motion.rb
56
56
  - lib/motion.rb
57
57
  - lib/motion/action_cable_extentions.rb
58
+ - lib/motion/action_cable_extentions/declarative_notifications.rb
58
59
  - lib/motion/action_cable_extentions/declarative_streams.rb
59
60
  - lib/motion/action_cable_extentions/log_suppression.rb
61
+ - lib/motion/action_cable_extentions/synchronization.rb
60
62
  - lib/motion/channel.rb
61
63
  - lib/motion/component.rb
62
64
  - lib/motion/component/broadcasts.rb
63
65
  - lib/motion/component/lifecycle.rb
64
66
  - lib/motion/component/motions.rb
67
+ - lib/motion/component/periodic_timers.rb
65
68
  - lib/motion/component/rendering.rb
66
69
  - lib/motion/component_connection.rb
67
70
  - lib/motion/configuration.rb
@@ -82,7 +85,7 @@ metadata:
82
85
  source_code_uri: https://github.com/unabridged/motion
83
86
  post_install_message: |
84
87
  Friendly reminder: When updating the motion gem, don't forget to update the
85
- NPM package as well (`bin/yarn add '@unabridged/motion@0.2.0'`).
88
+ NPM package as well (`bin/yarn add '@unabridged/motion@0.2.1'`).
86
89
  rdoc_options: []
87
90
  require_paths:
88
91
  - lib