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.
- 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.rb +6 -0
- data/lib/motion/action_cable_extentions/declarative_notifications.rb +101 -0
- data/lib/motion/action_cable_extentions/declarative_streams.rb +9 -43
- data/lib/motion/action_cable_extentions/synchronization.rb +34 -0
- data/lib/motion/callback.rb +35 -0
- data/lib/motion/channel.rb +12 -2
- data/lib/motion/component.rb +4 -0
- data/lib/motion/component/broadcasts.rb +40 -26
- data/lib/motion/component/callbacks.rb +19 -0
- data/lib/motion/component/lifecycle.rb +91 -9
- data/lib/motion/component/motions.rb +26 -16
- data/lib/motion/component/periodic_timers.rb +68 -0
- data/lib/motion/component/rendering.rb +25 -16
- data/lib/motion/component_connection.rb +18 -2
- data/lib/motion/configuration.rb +18 -10
- data/lib/motion/errors.rb +96 -68
- data/lib/motion/event.rb +9 -1
- data/lib/motion/log_helper.rb +2 -0
- data/lib/motion/markup_transformer.rb +6 -1
- data/lib/motion/revision_calculator.rb +48 -0
- data/lib/motion/serializer.rb +15 -2
- data/lib/motion/version.rb +1 -1
- metadata +23 -3
@@ -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
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require "active_support/concern"
|
4
4
|
require "active_support/core_ext/class/attribute"
|
5
|
+
require "active_support/core_ext/hash/except"
|
5
6
|
|
6
7
|
require "motion"
|
7
8
|
|
@@ -10,6 +11,23 @@ module Motion
|
|
10
11
|
module Motions
|
11
12
|
extend ActiveSupport::Concern
|
12
13
|
|
14
|
+
# Analogous to `module_function` (available on both class and instance)
|
15
|
+
module ModuleFunctions
|
16
|
+
def map_motion(motion, handler = motion)
|
17
|
+
self._motion_handlers =
|
18
|
+
_motion_handlers.merge(motion.to_s => handler.to_sym).freeze
|
19
|
+
end
|
20
|
+
|
21
|
+
def unmap_motion(motion)
|
22
|
+
self._motion_handlers =
|
23
|
+
_motion_handlers.except(motion.to_s).freeze
|
24
|
+
end
|
25
|
+
|
26
|
+
def motions
|
27
|
+
_motion_handlers.keys
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
13
31
|
included do
|
14
32
|
class_attribute :_motion_handlers,
|
15
33
|
instance_reader: false,
|
@@ -19,33 +37,25 @@ module Motion
|
|
19
37
|
end
|
20
38
|
|
21
39
|
class_methods do
|
22
|
-
|
23
|
-
self._motion_handlers =
|
24
|
-
_motion_handlers.merge(motion.to_s => handler.to_sym).freeze
|
25
|
-
end
|
40
|
+
include ModuleFunctions
|
26
41
|
end
|
27
42
|
|
28
|
-
|
29
|
-
_motion_handlers.keys
|
30
|
-
end
|
43
|
+
include ModuleFunctions
|
31
44
|
|
32
45
|
def process_motion(motion, event = nil)
|
33
46
|
unless (handler = _motion_handlers[motion])
|
34
47
|
raise MotionNotMapped.new(self, motion)
|
35
48
|
end
|
36
49
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
50
|
+
_run_action_callbacks(context: handler) do
|
51
|
+
if method(handler).arity.zero?
|
52
|
+
send(handler)
|
53
|
+
else
|
54
|
+
send(handler, event)
|
55
|
+
end
|
41
56
|
end
|
42
57
|
end
|
43
58
|
|
44
|
-
def map_motion(motion, handler = motion)
|
45
|
-
self._motion_handlers =
|
46
|
-
_motion_handlers.merge(motion.to_s => handler.to_sym).freeze
|
47
|
-
end
|
48
|
-
|
49
59
|
private
|
50
60
|
|
51
61
|
attr_writer :_motion_handlers
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/concern"
|
4
|
+
require "active_support/core_ext/class/attribute"
|
5
|
+
require "active_support/core_ext/hash/except"
|
6
|
+
|
7
|
+
require "motion"
|
8
|
+
|
9
|
+
module Motion
|
10
|
+
module Component
|
11
|
+
module PeriodicTimers
|
12
|
+
extend ActiveSupport::Concern
|
13
|
+
|
14
|
+
# Analogous to `module_function` (available on both class and instance)
|
15
|
+
module ModuleFunctions
|
16
|
+
def every(interval, handler, name: handler)
|
17
|
+
periodic_timer(name, handler, every: interval)
|
18
|
+
end
|
19
|
+
|
20
|
+
def periodic_timer(name, handler = name, every:)
|
21
|
+
self._periodic_timers =
|
22
|
+
_periodic_timers.merge(name.to_s => [handler.to_sym, every]).freeze
|
23
|
+
end
|
24
|
+
|
25
|
+
def stop_periodic_timer(name)
|
26
|
+
self._periodic_timers =
|
27
|
+
_periodic_timers.except(name.to_s).freeze
|
28
|
+
end
|
29
|
+
|
30
|
+
def periodic_timers
|
31
|
+
_periodic_timers.transform_values { |_handler, interval| interval }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
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
|
+
class_methods do
|
44
|
+
include ModuleFunctions
|
45
|
+
end
|
46
|
+
|
47
|
+
include ModuleFunctions
|
48
|
+
|
49
|
+
def process_periodic_timer(name)
|
50
|
+
return unless (handler, _interval = _periodic_timers[name])
|
51
|
+
|
52
|
+
_run_action_callbacks(context: handler) do
|
53
|
+
send(handler)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
attr_writer :_periodic_timers
|
60
|
+
|
61
|
+
def _periodic_timers
|
62
|
+
return @_periodic_timers if defined?(@_periodic_timers)
|
63
|
+
|
64
|
+
self.class._periodic_timers
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -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
|
@@ -24,42 +33,42 @@ module Motion
|
|
24
33
|
# * If it doesn't change every time the component's state changes,
|
25
34
|
# things may fall out of sync unless you also call `#rerender!`
|
26
35
|
def render_hash
|
27
|
-
|
28
|
-
#
|
29
|
-
# Is something with Ruby's built-in `hash` Good Enough(TM)?
|
30
|
-
#
|
31
|
-
# instance_variables
|
32
|
-
# .map { |ivar| instance_variable_get(ivar).hash }
|
33
|
-
# .reduce(0, &:^)
|
34
|
-
|
35
|
-
key, _state = Motion.serializer.serialize(self)
|
36
|
-
key
|
36
|
+
Motion.serializer.weak_digest(self)
|
37
37
|
end
|
38
38
|
|
39
39
|
def render_in(view_context)
|
40
40
|
raise BlockNotAllowedError, self if block_given?
|
41
|
-
clear_awaiting_forced_rerender!
|
42
41
|
|
43
|
-
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
|
44
50
|
|
45
51
|
Motion.markup_transformer.add_state_to_html(self, html)
|
46
52
|
end
|
47
53
|
|
48
54
|
private
|
49
55
|
|
50
|
-
def
|
56
|
+
def _clear_awaiting_forced_rerender!
|
51
57
|
return unless awaiting_forced_rerender?
|
52
58
|
|
53
59
|
remove_instance_variable(RERENDER_MARKER_IVAR)
|
54
60
|
end
|
55
61
|
|
56
|
-
def
|
62
|
+
def _without_new_instance_variables
|
57
63
|
existing_instance_variables = instance_variables
|
58
64
|
|
59
65
|
yield
|
60
66
|
ensure
|
61
|
-
(
|
62
|
-
|
67
|
+
(
|
68
|
+
instance_variables -
|
69
|
+
existing_instance_variables -
|
70
|
+
ALLOWED_NEW_IVARS_DURING_RENDER
|
71
|
+
).each(&method(:remove_instance_variable))
|
63
72
|
end
|
64
73
|
end
|
65
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
|
@@ -64,6 +64,18 @@ module Motion
|
|
64
64
|
false
|
65
65
|
end
|
66
66
|
|
67
|
+
def process_periodic_timer(timer)
|
68
|
+
timing("Proccessed periodic timer #{timer}") do
|
69
|
+
component.process_periodic_timer timer
|
70
|
+
end
|
71
|
+
|
72
|
+
true
|
73
|
+
rescue => error
|
74
|
+
handle_error(error, "processing periodic timer #{timer}")
|
75
|
+
|
76
|
+
false
|
77
|
+
end
|
78
|
+
|
67
79
|
def if_render_required(&block)
|
68
80
|
timing("Rendered") do
|
69
81
|
next_render_hash = component.render_hash
|
@@ -83,6 +95,10 @@ module Motion
|
|
83
95
|
component.broadcasts
|
84
96
|
end
|
85
97
|
|
98
|
+
def periodic_timers
|
99
|
+
component.periodic_timers
|
100
|
+
end
|
101
|
+
|
86
102
|
private
|
87
103
|
|
88
104
|
attr_reader :log_helper
|
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
|
|