motion 0.1.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "motion"
4
+
5
+ module Motion
6
+ class Callback
7
+ attr_reader :broadcast
8
+
9
+ NAMESPACE = "motion:callback"
10
+ private_constant :NAMESPACE
11
+
12
+ def self.broadcast_for(component, method)
13
+ [
14
+ NAMESPACE,
15
+ component.stable_instance_identifier_for_callbacks,
16
+ method
17
+ ].join(":")
18
+ end
19
+
20
+ def initialize(component, method)
21
+ @broadcast = self.class.broadcast_for(component, method)
22
+
23
+ component.stream_from(broadcast, method)
24
+ end
25
+
26
+ def ==(other)
27
+ other.is_a?(Callback) &&
28
+ other.broadcast == broadcast
29
+ end
30
+
31
+ def call(message = nil)
32
+ ActionCable.server.broadcast(broadcast, message)
33
+ end
34
+ end
35
+ 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
 
@@ -24,9 +25,7 @@ module Motion
24
25
  def subscribed
25
26
  state, client_version = params.values_at("state", "version")
26
27
 
27
- # TODO: This is too restrictive. Introduce a protocol version and support
28
- # older versions of the client that have a compatible protocol.
29
- unless Motion::VERSION == client_version
28
+ if Gem::Version.new(Motion::VERSION) < Gem::Version.new(client_version)
30
29
  raise IncompatibleClientError.new(Motion::VERSION, client_version)
31
30
  end
32
31
 
@@ -58,14 +57,23 @@ module Motion
58
57
  synchronize
59
58
  end
60
59
 
60
+ def process_periodic_timer(timer)
61
+ component_connection.process_periodic_timer(timer)
62
+ synchronize
63
+ end
64
+
61
65
  private
62
66
 
63
67
  def synchronize
64
- streaming_from(component_connection.broadcasts, to: :process_broadcast)
65
-
66
68
  component_connection.if_render_required do |component|
67
69
  transmit(renderer.render(component))
68
70
  end
71
+
72
+ streaming_from component_connection.broadcasts,
73
+ to: :process_broadcast
74
+
75
+ periodically_notify component_connection.periodic_timers,
76
+ via: :process_periodic_timer
69
77
  end
70
78
 
71
79
  def handle_error(error, context)
@@ -5,8 +5,10 @@ require "active_support/concern"
5
5
  require "motion"
6
6
 
7
7
  require "motion/component/broadcasts"
8
+ require "motion/component/callbacks"
8
9
  require "motion/component/lifecycle"
9
10
  require "motion/component/motions"
11
+ require "motion/component/periodic_timers"
10
12
  require "motion/component/rendering"
11
13
 
12
14
  module Motion
@@ -14,8 +16,10 @@ module Motion
14
16
  extend ActiveSupport::Concern
15
17
 
16
18
  include Broadcasts
19
+ include Callbacks
17
20
  include Lifecycle
18
21
  include Motions
22
+ include PeriodicTimers
19
23
  include Rendering
20
24
  end
21
25
  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,26 @@ 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
+ _run_action_callbacks(context: handler) do
79
+ if method(handler).arity.zero?
80
+ send(handler)
81
+ else
82
+ send(handler, message)
83
+ end
84
+ end
66
85
  end
67
86
 
68
- def stream_from(broadcast, handler)
69
- self._broadcast_handlers =
70
- _broadcast_handlers.merge(broadcast.to_s => handler.to_sym).freeze
71
- end
87
+ private
72
88
 
73
- def stream_for(model, handler)
74
- stream_from(self.class.broadcasting_for(model), handler)
89
+ def broadcasting_for(model)
90
+ self.class.broadcasting_for(model)
75
91
  end
76
92
 
77
- private
78
-
79
93
  attr_writer :_broadcast_handlers
80
94
 
81
95
  def _broadcast_handlers
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ require "motion"
6
+
7
+ module Motion
8
+ module Component
9
+ module Callbacks
10
+ def bind(method)
11
+ Callback.new(self, method)
12
+ end
13
+
14
+ def stable_instance_identifier_for_callbacks
15
+ @_stable_instance_identifier_for_callbacks ||= SecureRandom.uuid
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/callbacks"
3
4
  require "active_support/concern"
5
+ require "active_support/deprecation"
4
6
 
5
7
  require "motion"
6
8
 
@@ -9,22 +11,102 @@ module Motion
9
11
  module Lifecycle
10
12
  extend ActiveSupport::Concern
11
13
 
14
+ include ActiveSupport::Callbacks
15
+
16
+ included do
17
+ define_callbacks :action, :connect, :disconnect
18
+
19
+ # The built-in triggers defined on the target class will override ours.
20
+ remove_method(:_run_action_callbacks)
21
+ end
22
+
12
23
  class_methods do
13
- # TODO: "IncorrectRevisionError" doesn't make sense for this anymore.
14
- # It should probably be something like "CannotUpgrade" and the error
15
- # message should focus on how to handle deployments gracefully.
16
- def upgrade_from(previous_revision, _instance)
17
- raise IncorrectRevisionError.new(
18
- Motion.config.revision,
19
- previous_revision
24
+ def upgrade_from(previous_revision, instance)
25
+ raise UpgradeNotImplementedError.new(
26
+ instance,
27
+ previous_revision,
28
+ Motion.config.revision
20
29
  )
21
30
  end
31
+
32
+ def before_action(*methods, **options, &block)
33
+ set_action_callback(:before, *methods, **options, &block)
34
+ end
35
+
36
+ def around_action(*methods, **options, &block)
37
+ set_action_callback(:around, *methods, **options, &block)
38
+ end
39
+
40
+ def after_action(*methods, **options, &block)
41
+ set_action_callback(:after, *methods, **options, &block)
42
+ end
43
+
44
+ def after_connect(*methods, **options, &block)
45
+ set_callback(:connect, :after, *methods, **options, &block)
46
+ end
47
+
48
+ def after_disconnect(*methods, **options, &block)
49
+ set_callback(:disconnect, :after, *methods, **options, &block)
50
+ end
51
+
52
+ private
53
+
54
+ def set_action_callback(kind, *methods, **options, &block)
55
+ filters = Array(options.delete(:if))
56
+
57
+ if (only = Array(options.delete(:only))).any?
58
+ filters << action_callback_context_filter(only)
59
+ end
60
+
61
+ if (except = Array(options.delete(:except))).any?
62
+ filters << action_callback_context_filter(except, invert: true)
63
+ end
64
+
65
+ set_callback(:action, kind, *methods, if: filters, **options, &block)
66
+ end
67
+
68
+ def action_callback_context_filter(contexts, invert: false)
69
+ proc { contexts.include?(@_action_callback_context) ^ invert }
70
+ end
71
+ end
72
+
73
+ def process_connect
74
+ _run_connect_callbacks
75
+
76
+ # TODO: Remove at next minor release
77
+ if respond_to?(:connected)
78
+ ActiveSupport::Deprecation.warn(
79
+ "The `connected` lifecycle method is being replaced by the " \
80
+ "`after_connect` callback and will no longer be automatically " \
81
+ "invoked in the next **minor release** of Motion."
82
+ )
83
+
84
+ send(:connected)
85
+ end
22
86
  end
23
87
 
24
- def connected
88
+ def process_disconnect
89
+ _run_disconnect_callbacks
90
+
91
+ # TODO: Remove at next minor release
92
+ if respond_to?(:disconnected)
93
+ ActiveSupport::Deprecation.warn(
94
+ "The `disconnected` lifecycle method is being replaced by the " \
95
+ "`after_disconnect` callback and will no longer be automatically " \
96
+ "invoked in the next **minor release** of Motion."
97
+ )
98
+
99
+ send(:disconnected)
100
+ end
25
101
  end
26
102
 
27
- def disconnected
103
+ def _run_action_callbacks(context:, &block)
104
+ @_action_callback_context = context
105
+
106
+ run_callbacks(:action, &block)
107
+ ensure
108
+ # `@_action_callback_context = nil` would still appear in the state
109
+ remove_instance_variable(:@_action_callback_context)
28
110
  end
29
111
  end
30
112
  end
@@ -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,33 +37,25 @@ 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])
34
47
  raise MotionNotMapped.new(self, motion)
35
48
  end
36
49
 
37
- if method(handler).arity.zero?
38
- send(handler)
39
- else
40
- send(handler, event)
50
+ _run_action_callbacks(context: handler) do
51
+ if method(handler).arity.zero?
52
+ send(handler)
53
+ else
54
+ send(handler, event)
55
+ end
41
56
  end
42
57
  end
43
58
 
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
59
  private
50
60
 
51
61
  attr_writer :_motion_handlers
@@ -0,0 +1,68 @@
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
+ _run_action_callbacks(context: handler) do
53
+ send(handler)
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ attr_writer :_periodic_timers
60
+
61
+ def _periodic_timers
62
+ return @_periodic_timers if defined?(@_periodic_timers)
63
+
64
+ self.class._periodic_timers
65
+ end
66
+ end
67
+ end
68
+ end