motion 0.2.2 → 0.4.3

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: c1b1922ab9d0ef3ead51f333bf255e48f1f470a475ccc2463726e7d805d26344
4
- data.tar.gz: 57204e95779dfffb7413bb5080da1fb97f8d0bd519a6ae5f07cce80e4bad58f7
3
+ metadata.gz: d25829b24f8c2781bcc44f9132061532d3c5b3466e60e69ab646407d27c32519
4
+ data.tar.gz: c3e69af9595e1fe7b659a6ed51a57c252d2028581aa4f154e4a150ee2a74b12c
5
5
  SHA512:
6
- metadata.gz: 3240417ce4c5928aa52c17fb1eb89aa641983d1c119d0ad38b7ec05e60c29172ac95fac9b1ab5cba6ef35ea94b7df819b2b917213cfb193dbb749484b485a33a
7
- data.tar.gz: b7c973c8a93f0c9fac94fffa21a6b597635e064c4363b55d6718704bfdc5936125956e9116ebafb2936d5c555c95c34dff7e474a873605d49b36b7e0bebefeac
6
+ metadata.gz: f1bb6c0022e3636d62d6ba054aeac9e16c353bc5d646aa83c460a0eaf1f0fc0774efd71b03d3eb47456a033a66521907fc3e23e29fc02c4d09124a03732e940c
7
+ data.tar.gz: aa336213b2487da46b868b8c89f74ff55ec745078ffdb1e75fdeafe7d7707870f8f140013d8d9218e68998f89911813f9e0437d75f9261ccc29ec957990b6b02
@@ -1,5 +1,5 @@
1
- import { createClient } from '@unabridged/motion';
2
- import consumer from './channels/consumer';
1
+ import { createClient } from '@unabridged/motion'
2
+ import consumer from './channels/consumer'
3
3
 
4
4
  export default createClient({
5
5
 
@@ -11,20 +11,27 @@ export default createClient({
11
11
  // Motion can log information about the lifecycle of components to the
12
12
  // browser's console. It is recommended to turn this feature off outside of
13
13
  // development.
14
- logging: process.env["RAILS_ENV"] === "development",
14
+ logging: process.env.RAILS_ENV === 'development'
15
15
 
16
16
  // This function will be called for every motion, and the return value will be
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:
24
32
  //
25
- // keyAttribute: "data-motion-key",
26
- // stateAttribute: "data-motion-state",
27
- // motionAttribute: "data-motion",
28
- //
33
+ // keyAttribute: 'data-motion-key',
34
+ // stateAttribute: 'data-motion-state',
35
+ // motionAttribute: 'data-motion',
29
36
 
30
- });
37
+ })
@@ -30,14 +30,29 @@ Motion.configure do |config|
30
30
  # config.renderer_for_connection_proc = ->(websocket_connection) do
31
31
  # ApplicationController.renderer.new(
32
32
  # websocket_connection.env.slice(
33
- # Rack::HTTP_COOKIE,
34
- # Rack::RACK_SESSION,
33
+ # Rack::HTTP_COOKIE, # Cookies
34
+ # Rack::RACK_SESSION, # Session
35
+ # 'warden' # Warden (needed for `current_user` in Devise)
35
36
  # )
36
37
  # )
37
38
  # end
38
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
+
39
54
  # The data attributes used by Motion can be customized, but these values must
40
- # also be updated in the Ruby initializer:
55
+ # also be updated in the JavaScript client configuration:
41
56
  #
42
57
  # config.key_attribute = "data-motion-key"
43
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"
@@ -18,37 +19,43 @@ module Motion
18
19
  autoload :Serializer, "motion/serializer"
19
20
  autoload :TestHelpers, "motion/test_helpers"
20
21
 
21
- def self.configure(&block)
22
- raise AlreadyConfiguredError if @config
22
+ class << self
23
+ def configure(&block)
24
+ raise AlreadyConfiguredError if @config
23
25
 
24
- @config = Configuration.new(&block)
25
- end
26
+ @config = Configuration.new(&block)
27
+ end
26
28
 
27
- def self.config
28
- @config ||= Configuration.default
29
- end
29
+ def config
30
+ @config ||= Configuration.default
31
+ end
30
32
 
31
- singleton_class.alias_method :configuration, :config
33
+ alias_method :configuration, :config
32
34
 
33
- def self.serializer
34
- @serializer ||= Serializer.new
35
- end
35
+ def serializer
36
+ @serializer ||= Serializer.new
37
+ end
36
38
 
37
- def self.markup_transformer
38
- @markup_transformer ||= MarkupTransformer.new
39
- end
39
+ def markup_transformer
40
+ @markup_transformer ||= MarkupTransformer.new
41
+ end
40
42
 
41
- def self.build_renderer_for(websocket_connection)
42
- config.renderer_for_connection_proc.call(websocket_connection)
43
- end
43
+ def build_renderer_for(websocket_connection)
44
+ config.renderer_for_connection_proc.call(websocket_connection)
45
+ end
46
+
47
+ def notify_error(error, message)
48
+ config.error_notification_proc&.call(error, message)
49
+ end
44
50
 
45
- # This method only exists for testing. Changing configuration while Motion is
46
- # in use is not supported. It is only safe to call this method when no
47
- # components are currently mounted.
48
- def self.reset_internal_state_for_testing!(new_configuration = nil)
49
- @config = new_configuration
50
- @serializer = nil
51
- @markup_transformer = nil
51
+ # This method only exists for testing. Changing configuration while Motion
52
+ # is in use is not supported. It is only safe to call this method when no
53
+ # components are currently mounted.
54
+ def reset_internal_state_for_testing!(new_configuration = nil)
55
+ @config = new_configuration
56
+ @serializer = nil
57
+ @markup_transformer = nil
58
+ end
52
59
  end
53
60
  end
54
61
 
@@ -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
@@ -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)
@@ -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
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/concern"
4
- require "active_support/core_ext/class/attribute"
5
4
  require "active_support/core_ext/object/to_param"
6
5
  require "active_support/core_ext/hash/except"
7
6
 
@@ -12,6 +11,9 @@ module Motion
12
11
  module Broadcasts
13
12
  extend ActiveSupport::Concern
14
13
 
14
+ DEFAULT = {}.freeze
15
+ private_constant :DEFAULT
16
+
15
17
  # Analogous to `module_function` (available on both class and instance)
16
18
  module ModuleFunctions
17
19
  def stream_from(broadcast, handler)
@@ -37,14 +39,6 @@ module Motion
37
39
  end
38
40
  end
39
41
 
40
- included do
41
- class_attribute :_broadcast_handlers,
42
- instance_reader: false,
43
- instance_writer: false,
44
- instance_predicate: false,
45
- default: {}.freeze
46
- end
47
-
48
42
  class_methods do
49
43
  include ModuleFunctions
50
44
 
@@ -56,6 +50,15 @@ module Motion
56
50
  serialize_broadcasting([name, model])
57
51
  end
58
52
 
53
+ attr_writer :_broadcast_handlers
54
+
55
+ def _broadcast_handlers
56
+ return @_broadcast_handlers if defined?(@_broadcast_handlers)
57
+ return superclass._broadcast_handlers if superclass.respond_to?(:_broadcast_handlers)
58
+
59
+ DEFAULT
60
+ end
61
+
59
62
  private
60
63
 
61
64
  # This definition is copied from ActionCable::Channel::Broadcasting
@@ -75,10 +78,12 @@ module Motion
75
78
  def process_broadcast(broadcast, message)
76
79
  return unless (handler = _broadcast_handlers[broadcast])
77
80
 
78
- if method(handler).arity.zero?
79
- send(handler)
80
- else
81
- send(handler, message)
81
+ _run_action_callbacks(context: handler) do
82
+ if method(handler).arity.zero?
83
+ send(handler)
84
+ else
85
+ send(handler, message)
86
+ end
82
87
  end
83
88
  end
84
89
 
@@ -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,101 @@ 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
28
109
  end
29
110
  end
30
111
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/concern"
4
- require "active_support/core_ext/class/attribute"
5
4
  require "active_support/core_ext/hash/except"
6
5
 
7
6
  require "motion"
@@ -11,6 +10,9 @@ module Motion
11
10
  module Motions
12
11
  extend ActiveSupport::Concern
13
12
 
13
+ DEFAULT = {}.freeze
14
+ private_constant :DEFAULT
15
+
14
16
  # Analogous to `module_function` (available on both class and instance)
15
17
  module ModuleFunctions
16
18
  def map_motion(motion, handler = motion)
@@ -28,16 +30,17 @@ module Motion
28
30
  end
29
31
  end
30
32
 
31
- included do
32
- class_attribute :_motion_handlers,
33
- instance_reader: false,
34
- instance_writer: false,
35
- instance_predicate: false,
36
- default: {}.freeze
37
- end
38
-
39
33
  class_methods do
40
34
  include ModuleFunctions
35
+
36
+ attr_writer :_motion_handlers
37
+
38
+ def _motion_handlers
39
+ return @_motion_handlers if defined?(@_motion_handlers)
40
+ return superclass._motion_handlers if superclass.respond_to?(:_motion_handlers)
41
+
42
+ DEFAULT
43
+ end
41
44
  end
42
45
 
43
46
  include ModuleFunctions
@@ -47,10 +50,12 @@ module Motion
47
50
  raise MotionNotMapped.new(self, motion)
48
51
  end
49
52
 
50
- if method(handler).arity.zero?
51
- send(handler)
52
- else
53
- send(handler, event)
53
+ _run_action_callbacks(context: handler) do
54
+ if method(handler).arity.zero?
55
+ send(handler)
56
+ else
57
+ send(handler, event)
58
+ end
54
59
  end
55
60
  end
56
61
 
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/concern"
4
- require "active_support/core_ext/class/attribute"
5
4
  require "active_support/core_ext/hash/except"
6
5
 
7
6
  require "motion"
@@ -11,6 +10,9 @@ module Motion
11
10
  module PeriodicTimers
12
11
  extend ActiveSupport::Concern
13
12
 
13
+ DEFAULT = {}.freeze
14
+ private_constant :DEFAULT
15
+
14
16
  # Analogous to `module_function` (available on both class and instance)
15
17
  module ModuleFunctions
16
18
  def every(interval, handler, name: handler)
@@ -32,16 +34,17 @@ module Motion
32
34
  end
33
35
  end
34
36
 
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
37
  class_methods do
44
38
  include ModuleFunctions
39
+
40
+ attr_writer :_periodic_timers
41
+
42
+ def _periodic_timers
43
+ return @_periodic_timers if defined?(@_periodic_timers)
44
+ return superclass._periodic_timers if superclass.respond_to?(:_periodic_timers)
45
+
46
+ DEFAULT
47
+ end
45
48
  end
46
49
 
47
50
  include ModuleFunctions
@@ -49,7 +52,9 @@ module Motion
49
52
  def process_periodic_timer(name)
50
53
  return unless (handler, _interval = _periodic_timers[name])
51
54
 
52
- send(handler)
55
+ _run_action_callbacks(context: handler) do
56
+ send(handler)
57
+ end
53
58
  end
54
59
 
55
60
  private
@@ -5,18 +5,34 @@ require "motion"
5
5
  module Motion
6
6
  module Component
7
7
  module Rendering
8
- # Use the presence/absence of the ivar instead of true/false to avoid
9
- # extra serialized state (Note that in this scheme, the presence of
10
- # the ivar will never be serialized).
11
- RERENDER_MARKER_IVAR = :@__awaiting_forced_rerender__
12
- private_constant :RERENDER_MARKER_IVAR
8
+ STATE_EXCLUDED_IVARS = %i[
9
+ @_action_callback_context
10
+ @_awaiting_forced_rerender
11
+ @_routes
12
+
13
+ @view_context
14
+ @lookup_context
15
+ @view_renderer
16
+ @view_flow
17
+ @virtual_path
18
+ @variant
19
+ @current_template
20
+ @output_buffer
21
+
22
+ @helpers
23
+ @controller
24
+ @request
25
+ @tag_builder
26
+ ].freeze
27
+
28
+ private_constant :STATE_EXCLUDED_IVARS
13
29
 
14
30
  def rerender!
15
- instance_variable_set(RERENDER_MARKER_IVAR, true)
31
+ @_awaiting_forced_rerender = true
16
32
  end
17
33
 
18
34
  def awaiting_forced_rerender?
19
- instance_variable_defined?(RERENDER_MARKER_IVAR)
35
+ @_awaiting_forced_rerender
20
36
  end
21
37
 
22
38
  # * This can be overwritten.
@@ -29,28 +45,35 @@ module Motion
29
45
 
30
46
  def render_in(view_context)
31
47
  raise BlockNotAllowedError, self if block_given?
32
- clear_awaiting_forced_rerender!
33
48
 
34
- html = view_context.capture { without_new_instance_variables { super } }
49
+ html =
50
+ _run_action_callbacks(context: :render) {
51
+ _clear_awaiting_forced_rerender!
52
+
53
+ view_context.capture { super }
54
+ }
55
+
56
+ raise RenderAborted, self if html == false
35
57
 
36
58
  Motion.markup_transformer.add_state_to_html(self, html)
37
59
  end
38
60
 
39
61
  private
40
62
 
41
- def clear_awaiting_forced_rerender!
42
- return unless awaiting_forced_rerender?
43
-
44
- remove_instance_variable(RERENDER_MARKER_IVAR)
63
+ def _clear_awaiting_forced_rerender!
64
+ @_awaiting_forced_rerender = false
45
65
  end
46
66
 
47
- def without_new_instance_variables
48
- existing_instance_variables = instance_variables
67
+ def marshal_dump
68
+ (instance_variables - STATE_EXCLUDED_IVARS)
69
+ .map { |ivar| [ivar, instance_variable_get(ivar)] }
70
+ .to_h
71
+ end
49
72
 
50
- yield
51
- ensure
52
- (instance_variables - existing_instance_variables)
53
- .each(&method(:remove_instance_variable))
73
+ def marshal_load(instance_variables)
74
+ instance_variables.each do |ivar, value|
75
+ instance_variable_set(ivar, value)
76
+ end
54
77
  end
55
78
  end
56
79
  end
@@ -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
@@ -72,6 +72,10 @@ module Motion
72
72
  RevisionCalculator.new(revision_paths: revision_paths).perform
73
73
  end
74
74
 
75
+ # TODO: Is this always the correct key?
76
+ WARDEN_ENV = "warden"
77
+ private_constant :WARDEN_ENV
78
+
75
79
  option :renderer_for_connection_proc do
76
80
  ->(websocket_connection) do
77
81
  require "rack"
@@ -90,12 +94,15 @@ module Motion
90
94
  controller.renderer.new(
91
95
  websocket_connection.env.slice(
92
96
  Rack::HTTP_COOKIE,
93
- Rack::RACK_SESSION
97
+ Rack::RACK_SESSION,
98
+ WARDEN_ENV
94
99
  )
95
100
  )
96
101
  end
97
102
  end
98
103
 
104
+ option(:error_notification_proc) { nil }
105
+
99
106
  option(:key_attribute) { "data-motion-key" }
100
107
  option(:state_attribute) { "data-motion-state" }
101
108
 
@@ -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(component, <<~MSG)
24
- No component motion handler mapped for motion '#{motion}' in component #{component.class}.
25
-
26
- Fix: Add the following to #{component.class}:
27
-
28
- map_motion :#{motion}
29
- MSG
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(component, <<~MSG)
38
- Motion does not support rendering with a block.
39
-
40
- Fix: Use a plain component and wrap with a motion component.
41
- MSG
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(component, <<~MSG)
48
- The template for #{component.class} can only have one root element.
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
- Fix: Wrap all elements in a single element, such as <div> or <section>.
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(component, <<~MSG)
60
- Some state prevented #{component.class} from being serialized into a
61
- string. Motion components must be serializable using Marshal.dump. Many
62
- types of objects are not serializable including procs, references to
63
- anonymous classes, and more. See the documentation for Marshal.dump for
64
- more information.
65
-
66
- Fix: Ensure that any exotic state variables in #{component.class} are
67
- removed or replaced.
68
-
69
- The specific (but probably useless) error from Marshal was: #{cause}
70
- MSG
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(<<~MSG)
79
- The serialized state of your component is not valid.
80
-
81
- Fix: Ensure that you have not tampered with the DOM.
82
- MSG
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 IncorrectRevisionError < SerializedComponentError
87
- attr_reader :expected_revision,
88
- :actual_revision
89
-
90
- def initialize(expected_revision, actual_revision)
91
- super(<<~MSG)
92
- Cannot mount a component from another version of the application.
93
-
94
- Expected revision `#{expected_revision}`;
95
- Got `#{actual_revision}`
96
-
97
- Read more: https://github.com/unabridged/motion/wiki/IncorrectRevisionError
98
-
99
- Fix:
100
- * Avoid tampering with Motion DOM elements and data attributes (e.g. data-motion-state).
101
- * In production, enforce a page refresh for pages with Motion components on deploy.
102
- MSG
103
-
104
- @expected_revision = expected_revision
105
- @actual_revision = actual_revision
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(<<~MSG)
112
- Motion is already configured.
113
-
114
- Fix: Move all Motion config to config/initializers/motion.rb.
115
- MSG
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(<<~MSG)
124
- The client version (#{client_version}) is newer than the server version
125
- (#{server_version}). Please upgrade the Motion gem.
126
-
127
- Fix: Run `bundle update motion`
128
- MSG
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,26 +154,26 @@ module Motion
136
154
  attr_reader :minimum_bytes
137
155
 
138
156
  def initialize(minimum_bytes)
139
- super(<<~MSG)
140
- The secret that you provided is not long enough. It must have at least
141
- #{minimum_bytes} bytes.
142
- MSG
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.")
149
167
  end
150
168
  end
151
169
 
152
170
  class BadRevisionPathsError < Error
153
171
  def initialize
154
- super(<<~MSG)
155
- Revision paths must be a Rails::Paths::Root object or an object
156
- that responds to `all_paths.flat_map(&:existent)` and returns an
157
- Array of strings representing full paths.
158
- MSG
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
+ )
159
177
  end
160
178
  end
161
179
  end
@@ -18,7 +18,7 @@ module Motion
18
18
  raw["type"]
19
19
  end
20
20
 
21
- alias name type
21
+ alias_method :name, :type
22
22
 
23
23
  def details
24
24
  raw.fetch("details", {})
@@ -34,14 +34,12 @@ module Motion
34
34
  @target = Motion::Element.from_raw(raw["target"])
35
35
  end
36
36
 
37
- def current_target
38
- return @current_target if defined?(@current_target)
37
+ def element
38
+ return @element if defined?(@element)
39
39
 
40
- @current_target = Motion::Element.from_raw(raw["currentTarget"])
40
+ @element = Motion::Element.from_raw(raw["element"])
41
41
  end
42
42
 
43
- alias element current_target
44
-
45
43
  def form_data
46
44
  element&.form_data
47
45
  end
@@ -29,6 +29,8 @@ module Motion
29
29
  error_info = error ? ":\n#{indent(format_exception(error))}" : ""
30
30
 
31
31
  logger.error("[#{tag}] #{message}#{error_info}")
32
+
33
+ Motion.notify_error(error, message)
32
34
  end
33
35
 
34
36
  def info(message)
@@ -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 html if html.blank?
25
+ return if html.blank?
25
26
 
26
27
  key, state = serializer.serialize(component)
27
28
 
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Motion
4
- VERSION = "0.2.2"
4
+ VERSION = "0.4.3"
5
5
  end
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.2
4
+ version: 0.4.3
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-07-08 00:00:00.000000000 Z
12
+ date: 2020-09-22 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: nokogiri
@@ -31,14 +31,28 @@ dependencies:
31
31
  requirements:
32
32
  - - ">="
33
33
  - !ruby/object:Gem::Version
34
- version: '5.2'
34
+ version: '5.1'
35
35
  type: :runtime
36
36
  prerelease: false
37
37
  version_requirements: !ruby/object:Gem::Requirement
38
38
  requirements:
39
39
  - - ">="
40
40
  - !ruby/object:Gem::Version
41
- version: '5.2'
41
+ version: '5.1'
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
@@ -86,7 +102,7 @@ metadata:
86
102
  source_code_uri: https://github.com/unabridged/motion
87
103
  post_install_message: |
88
104
  Friendly reminder: When updating the motion gem, don't forget to update the
89
- NPM package as well (`bin/yarn add '@unabridged/motion@0.2.2'`).
105
+ NPM package as well (`bin/yarn add '@unabridged/motion@0.4.3'`).
90
106
  rdoc_options: []
91
107
  require_paths:
92
108
  - lib
@@ -94,7 +110,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
94
110
  requirements:
95
111
  - - ">="
96
112
  - !ruby/object:Gem::Version
97
- version: 2.3.0
113
+ version: 2.5.0
98
114
  required_rubygems_version: !ruby/object:Gem::Requirement
99
115
  requirements:
100
116
  - - ">="