motion 0.2.1 → 0.4.2
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/templates/motion.js +8 -1
- data/lib/generators/motion/templates/motion.rb +26 -8
- data/lib/motion.rb +6 -0
- data/lib/motion/action_cable_extentions/declarative_notifications.rb +6 -1
- data/lib/motion/callback.rb +35 -0
- data/lib/motion/channel.rb +4 -4
- data/lib/motion/component.rb +2 -0
- data/lib/motion/component/broadcasts.rb +6 -4
- data/lib/motion/component/callbacks.rb +19 -0
- data/lib/motion/component/lifecycle.rb +91 -9
- data/lib/motion/component/motions.rb +6 -4
- data/lib/motion/component/periodic_timers.rb +3 -1
- data/lib/motion/component/rendering.rb +24 -6
- data/lib/motion/component_connection.rb +2 -2
- data/lib/motion/configuration.rb +18 -10
- data/lib/motion/errors.rb +96 -68
- data/lib/motion/log_helper.rb +2 -0
- data/lib/motion/markup_transformer.rb +2 -1
- data/lib/motion/revision_calculator.rb +48 -0
- data/lib/motion/serializer.rb +11 -2
- data/lib/motion/version.rb +1 -1
- metadata +20 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 00d428a43d143191e5faba15d7b7aa25376bf375820eacc283c0aeb4148653e1
|
4
|
+
data.tar.gz: 3471947a7f72af5df84b13a2d995998201102c8174fd7a7dfee6a27a08a86a79
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ea47401782fe8331ee73c8201263de1ad75853842d14c57f68a416d821ae92416ac3c9bd5e6859bd6a10dc8785f8ecfe71a8e84417f02461afef3a8fda69ab0f
|
7
|
+
data.tar.gz: 8ce1dc79f7344bbf14b8537c806ff1b66e2dd6cfdddfc995925081edb2e7fdbc3ca10a9fdd5b04d9addbd78a8ba1333e7c324bb0a50644d71fbd515548d11f3b
|
@@ -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
|
-
#
|
9
|
-
#
|
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
|
-
#
|
12
|
+
# To change or add to your revision paths, uncomment this line:
|
12
13
|
#
|
13
|
-
#
|
14
|
-
#
|
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
|
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"
|
data/lib/motion.rb
CHANGED
@@ -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.
|
@@ -62,7 +62,7 @@ module Motion
|
|
62
62
|
#
|
63
63
|
# See `ActionCable::Channel::PeriodicTimers` for details.
|
64
64
|
def _setup_declarative_notifcation_timer(notification, interval)
|
65
|
-
return if
|
65
|
+
return if _stubbed_connection? ||
|
66
66
|
@_declarative_notifications_timers.include?(notification)
|
67
67
|
|
68
68
|
callback = proc do
|
@@ -77,6 +77,11 @@ module Motion
|
|
77
77
|
active_periodic_timers << timer
|
78
78
|
end
|
79
79
|
|
80
|
+
def _stubbed_connection?
|
81
|
+
defined?(ActionCable::Channel::ConnectionStub) &&
|
82
|
+
connection.is_a?(ActionCable::Channel::ConnectionStub)
|
83
|
+
end
|
84
|
+
|
80
85
|
def _shutdown_declarative_notifcation_timer(notification, *)
|
81
86
|
timer = @_declarative_notifications_timers.delete(notification)
|
82
87
|
return unless timer
|
@@ -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
|
data/lib/motion/channel.rb
CHANGED
@@ -65,15 +65,15 @@ module Motion
|
|
65
65
|
private
|
66
66
|
|
67
67
|
def synchronize
|
68
|
+
component_connection.if_render_required do |component|
|
69
|
+
transmit(renderer.render(component))
|
70
|
+
end
|
71
|
+
|
68
72
|
streaming_from component_connection.broadcasts,
|
69
73
|
to: :process_broadcast
|
70
74
|
|
71
75
|
periodically_notify component_connection.periodic_timers,
|
72
76
|
via: :process_periodic_timer
|
73
|
-
|
74
|
-
component_connection.if_render_required do |component|
|
75
|
-
transmit(renderer.render(component))
|
76
|
-
end
|
77
77
|
end
|
78
78
|
|
79
79
|
def handle_error(error, context)
|
data/lib/motion/component.rb
CHANGED
@@ -5,6 +5,7 @@ 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"
|
10
11
|
require "motion/component/periodic_timers"
|
@@ -15,6 +16,7 @@ module Motion
|
|
15
16
|
extend ActiveSupport::Concern
|
16
17
|
|
17
18
|
include Broadcasts
|
19
|
+
include Callbacks
|
18
20
|
include Lifecycle
|
19
21
|
include Motions
|
20
22
|
include PeriodicTimers
|
@@ -75,10 +75,12 @@ module Motion
|
|
75
75
|
def process_broadcast(broadcast, message)
|
76
76
|
return unless (handler = _broadcast_handlers[broadcast])
|
77
77
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
78
|
+
_run_action_callbacks(context: handler) do
|
79
|
+
if method(handler).arity.zero?
|
80
|
+
send(handler)
|
81
|
+
else
|
82
|
+
send(handler, message)
|
83
|
+
end
|
82
84
|
end
|
83
85
|
end
|
84
86
|
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "securerandom"
|
4
|
+
|
5
|
+
require "motion"
|
6
|
+
|
7
|
+
module Motion
|
8
|
+
module Component
|
9
|
+
module Callbacks
|
10
|
+
def bind(method)
|
11
|
+
Callback.new(self, method)
|
12
|
+
end
|
13
|
+
|
14
|
+
def stable_instance_identifier_for_callbacks
|
15
|
+
@_stable_instance_identifier_for_callbacks ||= SecureRandom.uuid
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
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
|
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
|
@@ -47,10 +47,12 @@ module Motion
|
|
47
47
|
raise MotionNotMapped.new(self, motion)
|
48
48
|
end
|
49
49
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
50
|
+
_run_action_callbacks(context: handler) do
|
51
|
+
if method(handler).arity.zero?
|
52
|
+
send(handler)
|
53
|
+
else
|
54
|
+
send(handler, event)
|
55
|
+
end
|
54
56
|
end
|
55
57
|
end
|
56
58
|
|
@@ -11,6 +11,15 @@ module Motion
|
|
11
11
|
RERENDER_MARKER_IVAR = :@__awaiting_forced_rerender__
|
12
12
|
private_constant :RERENDER_MARKER_IVAR
|
13
13
|
|
14
|
+
# Some changes to Motion's state are specifically supported during render.
|
15
|
+
ALLOWED_NEW_IVARS_DURING_RENDER = %i[
|
16
|
+
@_broadcast_handlers
|
17
|
+
@_stable_instance_identifier_for_callbacks
|
18
|
+
@_motion_handlers
|
19
|
+
@_periodic_timers
|
20
|
+
].freeze
|
21
|
+
private_constant :ALLOWED_NEW_IVARS_DURING_RENDER
|
22
|
+
|
14
23
|
def rerender!
|
15
24
|
instance_variable_set(RERENDER_MARKER_IVAR, true)
|
16
25
|
end
|
@@ -29,28 +38,37 @@ module Motion
|
|
29
38
|
|
30
39
|
def render_in(view_context)
|
31
40
|
raise BlockNotAllowedError, self if block_given?
|
32
|
-
clear_awaiting_forced_rerender!
|
33
41
|
|
34
|
-
html =
|
42
|
+
html =
|
43
|
+
_run_action_callbacks(context: :render) {
|
44
|
+
_clear_awaiting_forced_rerender!
|
45
|
+
|
46
|
+
view_context.capture { _without_new_instance_variables { super } }
|
47
|
+
}
|
48
|
+
|
49
|
+
raise RenderAborted, self if html == false
|
35
50
|
|
36
51
|
Motion.markup_transformer.add_state_to_html(self, html)
|
37
52
|
end
|
38
53
|
|
39
54
|
private
|
40
55
|
|
41
|
-
def
|
56
|
+
def _clear_awaiting_forced_rerender!
|
42
57
|
return unless awaiting_forced_rerender?
|
43
58
|
|
44
59
|
remove_instance_variable(RERENDER_MARKER_IVAR)
|
45
60
|
end
|
46
61
|
|
47
|
-
def
|
62
|
+
def _without_new_instance_variables
|
48
63
|
existing_instance_variables = instance_variables
|
49
64
|
|
50
65
|
yield
|
51
66
|
ensure
|
52
|
-
(
|
53
|
-
|
67
|
+
(
|
68
|
+
instance_variables -
|
69
|
+
existing_instance_variables -
|
70
|
+
ALLOWED_NEW_IVARS_DURING_RENDER
|
71
|
+
).each(&method(:remove_instance_variable))
|
54
72
|
end
|
55
73
|
end
|
56
74
|
end
|
@@ -24,13 +24,13 @@ module Motion
|
|
24
24
|
timing("Connected") do
|
25
25
|
@render_hash = component.render_hash
|
26
26
|
|
27
|
-
component.
|
27
|
+
component.process_connect
|
28
28
|
end
|
29
29
|
end
|
30
30
|
|
31
31
|
def close
|
32
32
|
timing("Disconnected") do
|
33
|
-
component.
|
33
|
+
component.process_disconnect
|
34
34
|
end
|
35
35
|
|
36
36
|
true
|
data/lib/motion/configuration.rb
CHANGED
@@ -59,18 +59,23 @@ module Motion
|
|
59
59
|
Rails.application.key_generator.generate_key("motion:secret")
|
60
60
|
end
|
61
61
|
|
62
|
+
option :revision_paths do
|
63
|
+
require "rails"
|
64
|
+
|
65
|
+
Rails.application.config.paths.dup.tap do |paths|
|
66
|
+
paths.add "bin", glob: "*"
|
67
|
+
paths.add "Gemfile.lock"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
62
71
|
option :revision do
|
63
|
-
|
64
|
-
Motion is automatically inferring the application's revision from git.
|
65
|
-
Depending on your deployment, this may not work for you in production.
|
66
|
-
If it does, add "config.revision = `git rev-parse HEAD`.chomp" to your
|
67
|
-
Motion initializer. If it does not, do something else (probably read an
|
68
|
-
env var or something).
|
69
|
-
MSG
|
70
|
-
|
71
|
-
`git rev-parse HEAD`.chomp
|
72
|
+
RevisionCalculator.new(revision_paths: revision_paths).perform
|
72
73
|
end
|
73
74
|
|
75
|
+
# TODO: Is this always the correct key?
|
76
|
+
WARDEN_ENV = "warden"
|
77
|
+
private_constant :WARDEN_ENV
|
78
|
+
|
74
79
|
option :renderer_for_connection_proc do
|
75
80
|
->(websocket_connection) do
|
76
81
|
require "rack"
|
@@ -89,12 +94,15 @@ module Motion
|
|
89
94
|
controller.renderer.new(
|
90
95
|
websocket_connection.env.slice(
|
91
96
|
Rack::HTTP_COOKIE,
|
92
|
-
Rack::RACK_SESSION
|
97
|
+
Rack::RACK_SESSION,
|
98
|
+
WARDEN_ENV
|
93
99
|
)
|
94
100
|
)
|
95
101
|
end
|
96
102
|
end
|
97
103
|
|
104
|
+
option(:error_notification_proc) { nil }
|
105
|
+
|
98
106
|
option(:key_attribute) { "data-motion-key" }
|
99
107
|
option(:state_attribute) { "data-motion-state" }
|
100
108
|
|
data/lib/motion/errors.rb
CHANGED
@@ -10,6 +10,7 @@ module Motion
|
|
10
10
|
|
11
11
|
def initialize(component, message = nil)
|
12
12
|
super(message)
|
13
|
+
|
13
14
|
@component = component
|
14
15
|
end
|
15
16
|
end
|
@@ -20,13 +21,13 @@ module Motion
|
|
20
21
|
attr_reader :motion
|
21
22
|
|
22
23
|
def initialize(component, motion)
|
23
|
-
super(
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
map_motion :#{motion}
|
29
|
-
|
24
|
+
super(
|
25
|
+
component,
|
26
|
+
"No component motion handler mapped for motion `#{motion}` in " \
|
27
|
+
"component `#{component.class}`.\n" \
|
28
|
+
"\n" \
|
29
|
+
"Hint: Consider adding `map_motion :#{motion}` to `#{component.class}`."
|
30
|
+
)
|
30
31
|
|
31
32
|
@motion = motion
|
32
33
|
end
|
@@ -34,20 +35,32 @@ module Motion
|
|
34
35
|
|
35
36
|
class BlockNotAllowedError < ComponentRenderingError
|
36
37
|
def initialize(component)
|
37
|
-
super(
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
38
|
+
super(
|
39
|
+
component,
|
40
|
+
"Motion does not support rendering with a block.\n" \
|
41
|
+
"\n" \
|
42
|
+
"Hint: Try wrapping a plain component with a motion component."
|
43
|
+
)
|
42
44
|
end
|
43
45
|
end
|
44
46
|
|
45
47
|
class MultipleRootsError < ComponentRenderingError
|
46
48
|
def initialize(component)
|
47
|
-
super(
|
48
|
-
|
49
|
+
super(
|
50
|
+
component,
|
51
|
+
"The template for #{component.class} can only have one root " \
|
52
|
+
"element.\n" \
|
53
|
+
"\n" \
|
54
|
+
"Hint: Wrap all elements in a single element, such as `<div>` or " \
|
55
|
+
"`<section>`."
|
56
|
+
)
|
57
|
+
end
|
58
|
+
end
|
49
59
|
|
50
|
-
|
60
|
+
class RenderAborted < ComponentRenderingError
|
61
|
+
def initialize(component)
|
62
|
+
super(component, <<~MSG)
|
63
|
+
Rendering #{component.class} was aborted by a callback.
|
51
64
|
MSG
|
52
65
|
end
|
53
66
|
end
|
@@ -56,18 +69,19 @@ module Motion
|
|
56
69
|
|
57
70
|
class UnrepresentableStateError < InvalidComponentStateError
|
58
71
|
def initialize(component, cause)
|
59
|
-
super(
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
more
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
72
|
+
super(
|
73
|
+
component,
|
74
|
+
"Some state prevented `#{component.class}` from being serialized " \
|
75
|
+
"into a string. Motion components must be serializable using " \
|
76
|
+
"`Marshal.dump`. Many types of objects are not serializable " \
|
77
|
+
"including procs, references to anonymous classes, and more. See the " \
|
78
|
+
"documentation for `Marshal.dump` for more information.\n" \
|
79
|
+
"\n" \
|
80
|
+
"The specific error from `Marshal.dump` was: #{cause}\n" \
|
81
|
+
"\n" \
|
82
|
+
"Hint: Ensure that any exotic state variables in " \
|
83
|
+
"`#{component.class}` are removed or replaced."
|
84
|
+
)
|
71
85
|
end
|
72
86
|
end
|
73
87
|
|
@@ -75,44 +89,48 @@ module Motion
|
|
75
89
|
|
76
90
|
class InvalidSerializedStateError < SerializedComponentError
|
77
91
|
def initialize
|
78
|
-
super(
|
79
|
-
The serialized state of your component is not valid
|
80
|
-
|
81
|
-
|
82
|
-
|
92
|
+
super(
|
93
|
+
"The serialized state of your component is not valid.\n" \
|
94
|
+
"\n" \
|
95
|
+
"Hint: Ensure that you have not tampered with the contents of data " \
|
96
|
+
"attributes added by Motion in the DOM or changed the value of " \
|
97
|
+
"`Motion.config.secret`."
|
98
|
+
)
|
83
99
|
end
|
84
100
|
end
|
85
101
|
|
86
|
-
class
|
87
|
-
attr_reader :
|
88
|
-
:
|
89
|
-
|
90
|
-
def initialize(
|
91
|
-
super(
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
102
|
+
class UpgradeNotImplementedError < ComponentError
|
103
|
+
attr_reader :previous_revision,
|
104
|
+
:current_revision
|
105
|
+
|
106
|
+
def initialize(component, previous_revision, current_revision)
|
107
|
+
super(
|
108
|
+
component,
|
109
|
+
"Cannot upgrade `#{component.class}` from a previous revision of the " \
|
110
|
+
"application (#{previous_revision}) to the current revision of the " \
|
111
|
+
"application (#{current_revision})\n" \
|
112
|
+
"\n" \
|
113
|
+
"By default, Motion does not allow components from other revisions " \
|
114
|
+
"of the application to be mounted because new code with old state " \
|
115
|
+
"can lead to unpredictable and unsafe behavior.\n" \
|
116
|
+
"\n" \
|
117
|
+
"Hint: If you would like to allow this component to surive " \
|
118
|
+
"deployments, consider providing an alternative implimentation for " \
|
119
|
+
"`#{component.class}.upgrade_from`."
|
120
|
+
)
|
121
|
+
|
122
|
+
@previous_revision = previous_revision
|
123
|
+
@current_revision = current_revision
|
106
124
|
end
|
107
125
|
end
|
108
126
|
|
109
127
|
class AlreadyConfiguredError < Error
|
110
128
|
def initialize
|
111
|
-
super(
|
112
|
-
Motion is already configured
|
113
|
-
|
114
|
-
|
115
|
-
|
129
|
+
super(
|
130
|
+
"Motion is already configured.\n" \
|
131
|
+
"\n" \
|
132
|
+
"Hint: Move all Motion config to `config/initializers/motion.rb`."
|
133
|
+
)
|
116
134
|
end
|
117
135
|
end
|
118
136
|
|
@@ -120,12 +138,12 @@ module Motion
|
|
120
138
|
attr_reader :server_version, :client_version
|
121
139
|
|
122
140
|
def initialize(server_version, client_version)
|
123
|
-
super(
|
124
|
-
The client version (#{client_version}) is newer than the server
|
125
|
-
(#{server_version}). Please upgrade the Motion gem
|
126
|
-
|
127
|
-
|
128
|
-
|
141
|
+
super(
|
142
|
+
"The client version (#{client_version}) is newer than the server " \
|
143
|
+
"version (#{server_version}). Please upgrade the Motion gem.\n" \
|
144
|
+
"\n" \
|
145
|
+
"Hint: Run `bundle add motion --version \">= #{client_version}\"`."
|
146
|
+
)
|
129
147
|
|
130
148
|
@server_version = server_version
|
131
149
|
@client_version = client_version
|
@@ -136,16 +154,26 @@ module Motion
|
|
136
154
|
attr_reader :minimum_bytes
|
137
155
|
|
138
156
|
def initialize(minimum_bytes)
|
139
|
-
super(
|
140
|
-
The secret that you provided is not long enough. It must
|
141
|
-
#{minimum_bytes} bytes.
|
142
|
-
|
157
|
+
super(
|
158
|
+
"The secret that you provided is not long enough. It must be at " \
|
159
|
+
"least #{minimum_bytes} bytes long."
|
160
|
+
)
|
143
161
|
end
|
144
162
|
end
|
145
163
|
|
146
164
|
class BadRevisionError < Error
|
147
165
|
def initialize
|
148
|
-
super("The revision cannot contain a NULL byte")
|
166
|
+
super("The revision cannot contain a NULL byte.")
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
class BadRevisionPathsError < Error
|
171
|
+
def initialize
|
172
|
+
super(
|
173
|
+
"Revision paths must be a `Rails::Paths::Root` object or an object " \
|
174
|
+
"that responds to `all_paths.flat_map(&:existent)` and returns an " \
|
175
|
+
"Array of strings representing full paths."
|
176
|
+
)
|
149
177
|
end
|
150
178
|
end
|
151
179
|
end
|
data/lib/motion/log_helper.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "nokogiri"
|
4
|
+
require "active_support/core_ext/object/blank"
|
4
5
|
|
5
6
|
require "motion"
|
6
7
|
|
@@ -21,7 +22,7 @@ module Motion
|
|
21
22
|
end
|
22
23
|
|
23
24
|
def add_state_to_html(component, html)
|
24
|
-
return
|
25
|
+
return if html.blank?
|
25
26
|
|
26
27
|
key, state = serializer.serialize(component)
|
27
28
|
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "digest"
|
4
|
+
require "motion"
|
5
|
+
|
6
|
+
module Motion
|
7
|
+
class RevisionCalculator
|
8
|
+
attr_reader :revision_paths
|
9
|
+
|
10
|
+
def initialize(revision_paths:)
|
11
|
+
@revision_paths = revision_paths
|
12
|
+
end
|
13
|
+
|
14
|
+
def perform
|
15
|
+
derive_file_hash
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def derive_file_hash
|
21
|
+
digest = Digest::MD5.new
|
22
|
+
|
23
|
+
files.each do |file|
|
24
|
+
digest << file # include filename as well as contents
|
25
|
+
digest << File.read(file)
|
26
|
+
end
|
27
|
+
|
28
|
+
digest.hexdigest
|
29
|
+
end
|
30
|
+
|
31
|
+
def existent_paths
|
32
|
+
@existent_paths ||=
|
33
|
+
begin
|
34
|
+
revision_paths.all_paths.flat_map(&:existent)
|
35
|
+
rescue
|
36
|
+
raise BadRevisionPathsError
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def existent_files(path)
|
41
|
+
Dir["#{path}/**/*", path].reject { |f| File.directory?(f) }.uniq
|
42
|
+
end
|
43
|
+
|
44
|
+
def files
|
45
|
+
@files ||= existent_paths.flat_map { |path| existent_files(path) }.sort
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/motion/serializer.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "digest"
|
4
|
+
require "lz4-ruby"
|
4
5
|
require "active_support/message_encryptor"
|
5
6
|
|
6
7
|
require "motion"
|
@@ -37,7 +38,7 @@ module Motion
|
|
37
38
|
end
|
38
39
|
|
39
40
|
def serialize(component)
|
40
|
-
state = dump(component)
|
41
|
+
state = deflate(dump(component))
|
41
42
|
state_with_revision = "#{revision}#{NULL_BYTE}#{state}"
|
42
43
|
|
43
44
|
[
|
@@ -49,7 +50,7 @@ module Motion
|
|
49
50
|
def deserialize(serialized_component)
|
50
51
|
state_with_revision = decrypt_and_verify(serialized_component)
|
51
52
|
serialized_revision, state = state_with_revision.split(NULL_BYTE, 2)
|
52
|
-
component = load(state)
|
53
|
+
component = load(inflate(state))
|
53
54
|
|
54
55
|
if revision == serialized_revision
|
55
56
|
component
|
@@ -70,6 +71,14 @@ module Motion
|
|
70
71
|
Marshal.load(state)
|
71
72
|
end
|
72
73
|
|
74
|
+
def deflate(dumped_component)
|
75
|
+
LZ4.compress(dumped_component)
|
76
|
+
end
|
77
|
+
|
78
|
+
def inflate(deflated_state)
|
79
|
+
LZ4.uncompress(deflated_state)
|
80
|
+
end
|
81
|
+
|
73
82
|
def encrypt_and_sign(cleartext)
|
74
83
|
encryptor.encrypt_and_sign(cleartext)
|
75
84
|
end
|
data/lib/motion/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: motion
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2
|
4
|
+
version: 0.4.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alec Larsen
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2020-
|
12
|
+
date: 2020-09-02 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: nokogiri
|
@@ -39,6 +39,20 @@ dependencies:
|
|
39
39
|
- - ">="
|
40
40
|
- !ruby/object:Gem::Version
|
41
41
|
version: '5.2'
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: lz4-ruby
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: 0.3.3
|
49
|
+
type: :runtime
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: 0.3.3
|
42
56
|
description: |
|
43
57
|
Motion extends Github's `view_component` to allow you to build reactive,
|
44
58
|
real-time frontend UI components in your Rails application using pure Ruby.
|
@@ -59,9 +73,11 @@ files:
|
|
59
73
|
- lib/motion/action_cable_extentions/declarative_streams.rb
|
60
74
|
- lib/motion/action_cable_extentions/log_suppression.rb
|
61
75
|
- lib/motion/action_cable_extentions/synchronization.rb
|
76
|
+
- lib/motion/callback.rb
|
62
77
|
- lib/motion/channel.rb
|
63
78
|
- lib/motion/component.rb
|
64
79
|
- lib/motion/component/broadcasts.rb
|
80
|
+
- lib/motion/component/callbacks.rb
|
65
81
|
- lib/motion/component/lifecycle.rb
|
66
82
|
- lib/motion/component/motions.rb
|
67
83
|
- lib/motion/component/periodic_timers.rb
|
@@ -74,6 +90,7 @@ files:
|
|
74
90
|
- lib/motion/log_helper.rb
|
75
91
|
- lib/motion/markup_transformer.rb
|
76
92
|
- lib/motion/railtie.rb
|
93
|
+
- lib/motion/revision_calculator.rb
|
77
94
|
- lib/motion/serializer.rb
|
78
95
|
- lib/motion/test_helpers.rb
|
79
96
|
- lib/motion/version.rb
|
@@ -85,7 +102,7 @@ metadata:
|
|
85
102
|
source_code_uri: https://github.com/unabridged/motion
|
86
103
|
post_install_message: |
|
87
104
|
Friendly reminder: When updating the motion gem, don't forget to update the
|
88
|
-
NPM package as well (`bin/yarn add '@unabridged/motion@0.2
|
105
|
+
NPM package as well (`bin/yarn add '@unabridged/motion@0.4.2'`).
|
89
106
|
rdoc_options: []
|
90
107
|
require_paths:
|
91
108
|
- lib
|