motion 0.2.0 → 0.4.1

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.
@@ -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,16 +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.")
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
@@ -34,8 +34,16 @@ 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)
39
+
40
+ @current_target = Motion::Element.from_raw(raw["currentTarget"])
41
+ end
42
+
43
+ alias element current_target
44
+
37
45
  def form_data
38
- target&.form_data
46
+ element&.form_data
39
47
  end
40
48
  end
41
49
  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,6 +22,8 @@ module Motion
21
22
  end
22
23
 
23
24
  def add_state_to_html(component, html)
25
+ return if html.blank?
26
+
24
27
  key, state = serializer.serialize(component)
25
28
 
26
29
  transform_root(component, html) do |root|
@@ -35,7 +38,9 @@ module Motion
35
38
  fragment = Nokogiri::HTML::DocumentFragment.parse(html)
36
39
  root, *unexpected_others = fragment.children
37
40
 
38
- raise MultipleRootsError, component if unexpected_others.any?(&:present?)
41
+ if !root || unexpected_others.any?(&:present?)
42
+ raise MultipleRootsError, component
43
+ end
39
44
 
40
45
  yield root
41
46
 
@@ -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
@@ -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"
@@ -32,8 +33,12 @@ module Motion
32
33
  @revision = revision
33
34
  end
34
35
 
36
+ def weak_digest(component)
37
+ dump(component).hash
38
+ end
39
+
35
40
  def serialize(component)
36
- state = dump(component)
41
+ state = deflate(dump(component))
37
42
  state_with_revision = "#{revision}#{NULL_BYTE}#{state}"
38
43
 
39
44
  [
@@ -45,7 +50,7 @@ module Motion
45
50
  def deserialize(serialized_component)
46
51
  state_with_revision = decrypt_and_verify(serialized_component)
47
52
  serialized_revision, state = state_with_revision.split(NULL_BYTE, 2)
48
- component = load(state)
53
+ component = load(inflate(state))
49
54
 
50
55
  if revision == serialized_revision
51
56
  component
@@ -66,6 +71,14 @@ module Motion
66
71
  Marshal.load(state)
67
72
  end
68
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
+
69
82
  def encrypt_and_sign(cleartext)
70
83
  encryptor.encrypt_and_sign(cleartext)
71
84
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Motion
4
- VERSION = "0.2.0"
4
+ VERSION = "0.4.1"
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.0
4
+ version: 0.4.1
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-06-29 00:00:00.000000000 Z
12
+ date: 2020-08-21 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.
@@ -55,13 +69,18 @@ files:
55
69
  - lib/generators/motion/templates/motion.rb
56
70
  - lib/motion.rb
57
71
  - lib/motion/action_cable_extentions.rb
72
+ - lib/motion/action_cable_extentions/declarative_notifications.rb
58
73
  - lib/motion/action_cable_extentions/declarative_streams.rb
59
74
  - lib/motion/action_cable_extentions/log_suppression.rb
75
+ - lib/motion/action_cable_extentions/synchronization.rb
76
+ - lib/motion/callback.rb
60
77
  - lib/motion/channel.rb
61
78
  - lib/motion/component.rb
62
79
  - lib/motion/component/broadcasts.rb
80
+ - lib/motion/component/callbacks.rb
63
81
  - lib/motion/component/lifecycle.rb
64
82
  - lib/motion/component/motions.rb
83
+ - lib/motion/component/periodic_timers.rb
65
84
  - lib/motion/component/rendering.rb
66
85
  - lib/motion/component_connection.rb
67
86
  - lib/motion/configuration.rb
@@ -71,6 +90,7 @@ files:
71
90
  - lib/motion/log_helper.rb
72
91
  - lib/motion/markup_transformer.rb
73
92
  - lib/motion/railtie.rb
93
+ - lib/motion/revision_calculator.rb
74
94
  - lib/motion/serializer.rb
75
95
  - lib/motion/test_helpers.rb
76
96
  - lib/motion/version.rb
@@ -82,7 +102,7 @@ metadata:
82
102
  source_code_uri: https://github.com/unabridged/motion
83
103
  post_install_message: |
84
104
  Friendly reminder: When updating the motion gem, don't forget to update the
85
- NPM package as well (`bin/yarn add '@unabridged/motion@0.2.0'`).
105
+ NPM package as well (`bin/yarn add '@unabridged/motion@0.4.1'`).
86
106
  rdoc_options: []
87
107
  require_paths:
88
108
  - lib