motion 0.1.1 → 0.3.0

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.
@@ -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,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
@@ -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
@@ -24,36 +24,33 @@ 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)
40
31
  raise BlockNotAllowedError, self if block_given?
41
- clear_awaiting_forced_rerender!
42
32
 
43
- html = view_context.capture { without_new_instance_variables { super } }
33
+ html =
34
+ _run_action_callbacks(context: :render) {
35
+ _clear_awaiting_forced_rerender!
36
+
37
+ view_context.capture { _without_new_instance_variables { super } }
38
+ }
39
+
40
+ raise RenderAborted, self if html == false
44
41
 
45
42
  Motion.markup_transformer.add_state_to_html(self, html)
46
43
  end
47
44
 
48
45
  private
49
46
 
50
- def clear_awaiting_forced_rerender!
47
+ def _clear_awaiting_forced_rerender!
51
48
  return unless awaiting_forced_rerender?
52
49
 
53
50
  remove_instance_variable(RERENDER_MARKER_IVAR)
54
51
  end
55
52
 
56
- def without_new_instance_variables
53
+ def _without_new_instance_variables
57
54
  existing_instance_variables = instance_variables
58
55
 
59
56
  yield
@@ -24,13 +24,13 @@ module Motion
24
24
  timing("Connected") do
25
25
  @render_hash = component.render_hash
26
26
 
27
- component.connected
27
+ component.process_connect
28
28
  end
29
29
  end
30
30
 
31
31
  def close
32
32
  timing("Disconnected") do
33
- component.disconnected
33
+ component.process_disconnect
34
34
  end
35
35
 
36
36
  true
@@ -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