motion 0.1.2 → 0.4.0

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: f50ecf7f319478fcd72dca2dd6e55c36a67d803e6235d24c39ac36537528bb88
4
- data.tar.gz: 10c88dabd3104c7351a5d2eeb6623564214171c1b26fdf9b25aca3d8297c2a46
3
+ metadata.gz: 54eb6afb0f21c0333d46c1a21602739146fa1e2a021d3ffda048c9763dc85e66
4
+ data.tar.gz: c3d9476c0e34869f5d4e46121316dee4f2c78d91b0a9b51d05c14a0b4d4e63ac
5
5
  SHA512:
6
- metadata.gz: 31a107b75a0e553febfec1f0b93cc878c4c727f5f61ba1d0e39b89c9c7c1a7b45d5ecf188a49b9e1293c2f4056974afb755c1cb2cd4594895e95c3166e38563f
7
- data.tar.gz: 7ad03d82e4a4ca05338368db8cda1b12813662f3f3fe2937f576baeb47b8b7c99b41b5bfc4ec9b11eb42facbce155990b7f278c6171603b0bf972b9adb641c52
6
+ metadata.gz: b17d8159ba7eae3b144a6380385e68eb9a2b49b0733926d9e285bf60c8f0767646d30841ee90d62d4b3a359f5c5f9beb62d4793c6d48dff6f1887c9fd7367a70
7
+ data.tar.gz: 0c47ed993c63378f0d562e341a44539bda1578c34d42ae8449c1dfd9dedabf7481e9e5f272c0869416893533fe31b5901368ebcc2e53c0752db120afb0e84c50
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/named_base"
4
+
5
+ module Motion
6
+ module Generators
7
+ class ComponentGenerator < Rails::Generators::NamedBase
8
+ desc "Creates a Motion-enabled component in your application."
9
+
10
+ argument :attributes, type: :array, default: [], banner: "attribute"
11
+
12
+ def generate_component
13
+ generate "component", class_name, *attributes.map(&:name)
14
+ end
15
+
16
+ def include_motion
17
+ inject_into_class component_path, "#{class_name}Component" do
18
+ " include Motion::Component\n\n"
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def component_path
25
+ @component_path ||=
26
+ File.join("app/components", class_path, "#{file_name}_component.rb")
27
+ end
28
+
29
+ def file_name
30
+ @_file_name ||= super.sub(/_component\z/i, "")
31
+ end
32
+ end
33
+ end
34
+ end
@@ -16,10 +16,17 @@ module Motion
16
16
  )
17
17
  end
18
18
 
19
- def copy_stimlus_controller
19
+ def copy_client_initializer
20
20
  template(
21
- "motion_controller.js",
22
- "app/javascript/controllers/motion_controller.js"
21
+ "motion.js",
22
+ "app/javascript/motion.js"
23
+ )
24
+ end
25
+
26
+ def add_client_to_application_pack
27
+ append_to_file(
28
+ "app/javascript/packs/application.js",
29
+ 'import "motion"'
23
30
  )
24
31
  end
25
32
  end
@@ -0,0 +1,37 @@
1
+ import { createClient } from '@unabridged/motion';
2
+ import consumer from './channels/consumer';
3
+
4
+ export default createClient({
5
+
6
+ // To avoid creating a second websocket, make sure to reuse the application's
7
+ // ActionCable consumer. If you are not otherwise using ActionCable, you can
8
+ // remove this line and the corresponding import.
9
+ consumer,
10
+
11
+ // Motion can log information about the lifecycle of components to the
12
+ // browser's console. It is recommended to turn this feature off outside of
13
+ // development.
14
+ logging: process.env["RAILS_ENV"] === "development",
15
+
16
+ // This function will be called for every motion, and the return value will be
17
+ // made available at `Motion::Event#extra_data`:
18
+ //
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:
27
+ //
28
+ // shutdownBeforeUnload: false,
29
+
30
+ // The data attributes used by Motion can be customized, but these values must
31
+ // also be updated in the Ruby initializer:
32
+ //
33
+ // keyAttribute: "data-motion-key",
34
+ // stateAttribute: "data-motion-state",
35
+ // motionAttribute: "data-motion",
36
+
37
+ });
@@ -1,22 +1,61 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # TODO: Explain all the options.
4
3
  Motion.configure do |config|
5
- # config.secret = Rails.application.key_generator.generate_key "motion:secret"
4
+ # Motion needs to be able to uniquely identify the version of the running
5
+ # version of your application. By default, the commit hash from git is used,
6
+ # but depending on your deployment, this may not be available in production.
7
+ #
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.
11
+ #
12
+ # To change or add to your revision paths, uncomment this line:
13
+ #
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.
18
+ #
19
+ # config.revision =
20
+ # ENV.fetch("MY_DEPLOYMENT_NUMBER") { `git rev-parse HEAD`.chomp }
21
+ #
22
+ # Using a value that does not change on every deployment will likely lead to
23
+ # confusing errors if components are connected during a deployment.
6
24
 
7
- # config.revision = `git rev-parse HEAD`.chomp
25
+ # This proc will be invoked by Motion in order to create a renderer for each
26
+ # websocket connection. By default, your `ApplicationController` will be used
27
+ # and the session/cookies **as they were when the websocket was first open**
28
+ # will be available:
29
+ #
30
+ # config.renderer_for_connection_proc = ->(websocket_connection) do
31
+ # ApplicationController.renderer.new(
32
+ # websocket_connection.env.slice(
33
+ # Rack::HTTP_COOKIE, # Cookies
34
+ # Rack::RACK_SESSION, # Session
35
+ # 'warden' # Warden (needed for `current_user` in Devise)
36
+ # )
37
+ # )
38
+ # end
8
39
 
9
- # config.renderer_for_connection_proc = ->(websocket_connection) do
10
- # ApplicationController.renderer.new(
11
- # websocket_connection.env.slice(
12
- # Rack::HTTP_COOKIE,
13
- # Rack::RACK_SESSION,
14
- # )
15
- # )
16
- # end
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
17
53
 
18
- # config.stimulus_controller_identifier = "motion"
19
- # config.key_attribute = "data-motion-key"
20
- # config.state_attribute = "data-motion-state"
21
- # config.motion_attribute = "data-motion"
54
+ # The data attributes used by Motion can be customized, but these values must
55
+ # also be updated in the JavaScript client configuration:
56
+ #
57
+ # config.key_attribute = "data-motion-key"
58
+ # config.state_attribute = "data-motion-state"
59
+ # config.motion_attribute = "data-motion"
60
+ #
22
61
  end
@@ -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