motion 0.1.1 → 0.3.0

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