motion 0.2.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
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