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.
- checksums.yaml +4 -4
- data/lib/generators/motion/component_generator.rb +34 -0
- data/lib/generators/motion/install_generator.rb +10 -3
- data/lib/generators/motion/templates/motion.js +37 -0
- data/lib/generators/motion/templates/motion.rb +54 -15
- data/lib/motion.rb +5 -0
- data/lib/motion/action_cable_extentions.rb +6 -0
- data/lib/motion/action_cable_extentions/declarative_notifications.rb +101 -0
- data/lib/motion/action_cable_extentions/declarative_streams.rb +12 -45
- data/lib/motion/action_cable_extentions/synchronization.rb +34 -0
- data/lib/motion/channel.rb +12 -4
- data/lib/motion/component.rb +2 -0
- data/lib/motion/component/broadcasts.rb +40 -26
- data/lib/motion/component/lifecycle.rb +91 -9
- data/lib/motion/component/motions.rb +26 -16
- data/lib/motion/component/periodic_timers.rb +68 -0
- data/lib/motion/component/rendering.rb +11 -14
- data/lib/motion/component_connection.rb +18 -2
- data/lib/motion/configuration.rb +18 -11
- data/lib/motion/errors.rb +102 -71
- data/lib/motion/event.rb +9 -1
- data/lib/motion/log_helper.rb +2 -0
- data/lib/motion/markup_transformer.rb +6 -21
- data/lib/motion/railtie.rb +1 -0
- data/lib/motion/revision_calculator.rb +48 -0
- data/lib/motion/serializer.rb +4 -0
- data/lib/motion/version.rb +1 -1
- metadata +12 -7
- data/lib/generators/motion/templates/motion_controller.js +0 -28
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7645381b6b2749a4a9ba90a15c93916636e1b9edfd7b3a6a1bf7d47429fedb3f
|
4
|
+
data.tar.gz: 46c929b8aa2d1de8c714eb8b8db1db8524aec892576a13ad1582b7fa858565f7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b8c99a480f51e9d818f5d22de3efbf8923155943d1815188e19332c54b8f24b909bc371abc7282d3a9caac754cf1b0aee4bb3193dad75b7f1d86a9112ed13983
|
7
|
+
data.tar.gz: a12e9213ba0ae38cbc5d839ae1408a5aab064be055896300f19a2296ebcd74b49922b96636bc52bd7b7aef62be9868ea47437544938f20facaad45534744bc4e
|
@@ -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
|
19
|
+
def copy_client_initializer
|
20
20
|
template(
|
21
|
-
"
|
22
|
-
"app/javascript/
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
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
|
-
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
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
|
data/lib/motion.rb
CHANGED
@@ -14,6 +14,7 @@ module Motion
|
|
14
14
|
autoload :LogHelper, "motion/log_helper"
|
15
15
|
autoload :MarkupTransformer, "motion/markup_transformer"
|
16
16
|
autoload :Railtie, "motion/railtie"
|
17
|
+
autoload :RevisionCalculator, "motion/revision_calculator"
|
17
18
|
autoload :Serializer, "motion/serializer"
|
18
19
|
autoload :TestHelpers, "motion/test_helpers"
|
19
20
|
|
@@ -41,6 +42,10 @@ module Motion
|
|
41
42
|
config.renderer_for_connection_proc.call(websocket_connection)
|
42
43
|
end
|
43
44
|
|
45
|
+
def self.notify_error(error, message)
|
46
|
+
config.error_notification_proc&.call(error, message)
|
47
|
+
end
|
48
|
+
|
44
49
|
# This method only exists for testing. Changing configuration while Motion is
|
45
50
|
# in use is not supported. It is only safe to call this method when no
|
46
51
|
# 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.
|
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
|
@@ -70,34 +51,20 @@ module Motion
|
|
70
51
|
def _ensure_declarative_stream_proxy(broadcast)
|
71
52
|
return unless @_declarative_stream_proxies.add?(broadcast)
|
72
53
|
|
73
|
-
# TODO:
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
# the broadcast.
|
80
|
-
|
81
|
-
_handle_exception_in_declarative_stream(broadcast, exception)
|
54
|
+
# TODO: I feel like the fact that we have to specify the coder here is
|
55
|
+
# a bug in ActionCable. It should be the default for this karg.
|
56
|
+
stream_from(broadcast, coder: ActiveSupport::JSON) do |message|
|
57
|
+
synchronize_entrypoint! do
|
58
|
+
_handle_incoming_broadcast_to_declarative_stream(broadcast, message)
|
59
|
+
end
|
82
60
|
end
|
83
61
|
end
|
84
62
|
|
85
63
|
def _handle_incoming_broadcast_to_declarative_stream(broadcast, message)
|
86
|
-
@
|
87
|
-
|
88
|
-
@_declarative_streams.include?(broadcast)
|
89
|
-
|
90
|
-
send(@_declarative_stream_target, broadcast, message)
|
91
|
-
end
|
92
|
-
end
|
64
|
+
return unless @_declarative_stream_target &&
|
65
|
+
@_declarative_streams.include?(broadcast)
|
93
66
|
|
94
|
-
|
95
|
-
logger.error(
|
96
|
-
"There was an exception while handling a broadcast to #{broadcast}" \
|
97
|
-
"on #{self.class}:\n" \
|
98
|
-
" #{exception.class}: #{exception.message}\n" \
|
99
|
-
"#{exception.backtrace.map { |line| " #{line}" }.join("\n")}"
|
100
|
-
)
|
67
|
+
send(@_declarative_stream_target, broadcast, message)
|
101
68
|
end
|
102
69
|
end
|
103
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
|
data/lib/motion/channel.rb
CHANGED
@@ -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
|
-
|
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,10 +57,19 @@ 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
|
68
|
+
streaming_from component_connection.broadcasts,
|
69
|
+
to: :process_broadcast
|
70
|
+
|
71
|
+
periodically_notify component_connection.periodic_timers,
|
72
|
+
via: :process_periodic_timer
|
65
73
|
|
66
74
|
component_connection.if_render_required do |component|
|
67
75
|
transmit(renderer.render(component))
|