motion 0.2.0 → 0.4.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: '0967eb2e920b41cf8800a494bcc514bdf4d7cbdf9811a1425a09bf80e99b65d9'
4
+ data.tar.gz: 8c36032ff83ebd1cdb5c4fb4cd7ff0c668deee2b6bf3384027b0c715cc50d53c
5
5
  SHA512:
6
- metadata.gz: 7195a8b10a5785ea41b46958b886d06ed8e19cd376879d3c1a1d7c14e5ef7be403df16b55f59d1276c1b7111d4268b6280f8d7326cc3e5f6e31aac555376e6c6
7
- data.tar.gz: 3f2df4a07139f7ac39fb2733ae3f7ccaf20d9cffd8a8e7c34b1b7e6dd9112fc69de20129e592fe4dacf7dfff5a0c12f15aab896dcea854ab093c795ab69df71d
6
+ metadata.gz: 499f1fb69d65d04b5439ccf6eb32fac722edff716d5bbd710c8e35b6f68ff5e5e821b02a38e046889a8a08c2bf78efdf87eb2e684c76e1c36cb6a22990c171f7
7
+ data.tar.gz: 517e4f3b96279f8f8b4fd21c8e5eb441d4fceee1005bea476b655f3bb673a61d45a57d81667d378a60fa7b8c05f597393d6c6d87e994678adec73aa2cda188cd
@@ -17,7 +17,15 @@ export default createClient({
17
17
  // made available at `Motion::Event#extra_data`:
18
18
  //
19
19
  // getExtraDataForEvent(event) {},
20
+
21
+ // By default, the Motion client automatically disconnects all components when
22
+ // it detects the browser navigating away to a new page. This is done to
23
+ // prevent flashes of new content in components with broadcasts because of
24
+ // some action being taken by the controller that the user is navigating to
25
+ // (like submitting a form). If you do not want or need this functionally, you
26
+ // can turn it off:
20
27
  //
28
+ // shutdownBeforeUnload: false,
21
29
 
22
30
  // The data attributes used by Motion can be customized, but these values must
23
31
  // also be updated in the Ruby initializer:
@@ -25,6 +33,5 @@ export default createClient({
25
33
  // keyAttribute: "data-motion-key",
26
34
  // stateAttribute: "data-motion-state",
27
35
  // motionAttribute: "data-motion",
28
- //
29
36
 
30
37
  });
@@ -5,13 +5,16 @@ Motion.configure do |config|
5
5
  # version of your application. By default, the commit hash from git is used,
6
6
  # but depending on your deployment, this may not be available in production.
7
7
  #
8
- # If you are sure that git is available in your production enviorment, you can
9
- # uncomment this line:
8
+ # Motion automatically calculates your revision by hashing the contents of
9
+ # files in `revision_paths` The defaults revision paths are:
10
+ # rails paths, bin, and Gemfile.lock.
10
11
  #
11
- # config.revision = `git rev-parse HEAD`.chomp
12
+ # To change or add to your revision paths, uncomment this line:
12
13
  #
13
- # If git is not available in your production enviorment, you must identify
14
- # your application version some other way:
14
+ # config.revision_paths += w(additional_path another_path)
15
+ #
16
+ # If you prefer to use git or an environmental variable for the revision
17
+ # in production, define the revision directly below.
15
18
  #
16
19
  # config.revision =
17
20
  # ENV.fetch("MY_DEPLOYMENT_NUMBER") { `git rev-parse HEAD`.chomp }
@@ -27,14 +30,29 @@ Motion.configure do |config|
27
30
  # config.renderer_for_connection_proc = ->(websocket_connection) do
28
31
  # ApplicationController.renderer.new(
29
32
  # websocket_connection.env.slice(
30
- # Rack::HTTP_COOKIE,
31
- # Rack::RACK_SESSION,
33
+ # Rack::HTTP_COOKIE, # Cookies
34
+ # Rack::RACK_SESSION, # Session
35
+ # 'warden' # Warden (needed for `current_user` in Devise)
32
36
  # )
33
37
  # )
34
38
  # end
35
39
 
40
+ # This proc will be invoked by Motion when an unhandled error occurs. By
41
+ # default, an error is logged to the application's default logger but no
42
+ # additional action is taken. If you are using an error tracking tool like
43
+ # Bugsnag, Sentry, Honeybadger, or Rollbar, you can provide a proc which
44
+ # notifies that as well:
45
+ #
46
+ # config.error_notification_proc = ->(error, message) do
47
+ # Bugsnag.notify(error) do |report|
48
+ # report.add_tab(:motion, {
49
+ # message: message
50
+ # })
51
+ # end
52
+ # end
53
+
36
54
  # The data attributes used by Motion can be customized, but these values must
37
- # also be updated in the Ruby initializer:
55
+ # also be updated in the JavaScript client configuration:
38
56
  #
39
57
  # config.key_attribute = "data-motion-key"
40
58
  # config.state_attribute = "data-motion-state"
@@ -5,6 +5,7 @@ require "motion/errors"
5
5
 
6
6
  module Motion
7
7
  autoload :ActionCableExtentions, "motion/action_cable_extentions"
8
+ autoload :Callback, "motion/callback"
8
9
  autoload :Channel, "motion/channel"
9
10
  autoload :Component, "motion/component"
10
11
  autoload :ComponentConnection, "motion/component_connection"
@@ -14,6 +15,7 @@ module Motion
14
15
  autoload :LogHelper, "motion/log_helper"
15
16
  autoload :MarkupTransformer, "motion/markup_transformer"
16
17
  autoload :Railtie, "motion/railtie"
18
+ autoload :RevisionCalculator, "motion/revision_calculator"
17
19
  autoload :Serializer, "motion/serializer"
18
20
  autoload :TestHelpers, "motion/test_helpers"
19
21
 
@@ -41,6 +43,10 @@ module Motion
41
43
  config.renderer_for_connection_proc.call(websocket_connection)
42
44
  end
43
45
 
46
+ def self.notify_error(error, message)
47
+ config.error_notification_proc&.call(error, message)
48
+ end
49
+
44
50
  # This method only exists for testing. Changing configuration while Motion is
45
51
  # in use is not supported. It is only safe to call this method when no
46
52
  # components are currently mounted.
@@ -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,101 @@
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 _stubbed_connection? ||
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 _stubbed_connection?
81
+ defined?(ActionCable::Channel::ConnectionStub) &&
82
+ connection.is_a?(ActionCable::Channel::ConnectionStub)
83
+ end
84
+
85
+ def _shutdown_declarative_notifcation_timer(notification, *)
86
+ timer = @_declarative_notifications_timers.delete(notification)
87
+ return unless timer
88
+
89
+ timer.shutdown
90
+ active_periodic_timers.delete(timer)
91
+ end
92
+
93
+ def _handle_declarative_notifcation(notification)
94
+ return unless @_declarative_notifications_target &&
95
+ @_declarative_notifications.include?(notification)
96
+
97
+ send(@_declarative_notifications_target, notification)
98
+ end
99
+ end
100
+ end
101
+ 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
@@ -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
 
@@ -56,14 +57,23 @@ 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)
63
-
64
68
  component_connection.if_render_required do |component|
65
69
  transmit(renderer.render(component))
66
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
67
77
  end
68
78
 
69
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