motion 0.2.2 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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
  - - ">="