motion 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/generators/motion/install_generator.rb +27 -0
- data/lib/generators/motion/templates/motion.rb +22 -0
- data/lib/generators/motion/templates/motion_controller.js +28 -0
- data/lib/motion/action_cable_extentions/declarative_streams.rb +104 -0
- data/lib/motion/action_cable_extentions/log_suppression.rb +33 -0
- data/lib/motion/action_cable_extentions.rb +13 -0
- data/lib/motion/channel.rb +88 -0
- data/lib/motion/component/broadcasts.rb +88 -0
- data/lib/motion/component/lifecycle.rb +31 -0
- data/lib/motion/component/motions.rb +60 -0
- data/lib/motion/component/rendering.rb +66 -0
- data/lib/motion/component.rb +21 -0
- data/lib/motion/component_connection.rb +98 -0
- data/lib/motion/configuration.rb +106 -0
- data/lib/motion/element.rb +70 -0
- data/lib/motion/errors.rb +148 -0
- data/lib/motion/event.rb +41 -0
- data/lib/motion/log_helper.rb +74 -0
- data/lib/motion/markup_transformer.rb +65 -0
- data/lib/motion/railtie.rb +11 -0
- data/lib/motion/serializer.rb +100 -0
- data/lib/motion/test_helpers.rb +29 -0
- data/lib/motion/version.rb +5 -0
- data/lib/motion.rb +54 -0
- metadata +103 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 2c9776bff100e3017a158810f93d3c374d97e6df42b8d9ffee43bf96da624b5b
|
4
|
+
data.tar.gz: a0764632d832cd96b58b3ffa5c28e9598d09136b0dbe6eadc66ab074bc18d5bd
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b5a0955da6087872bd9ec9940b8b3a674bf6fd2782ccad6b72460255703ff810e06e3d62ea99484a6c95d6fb48883953bbdec1cb3224af2513006ba60d5f3c54
|
7
|
+
data.tar.gz: a0360a0b1a0422b38f5bd4f9a46ba203ed9f1d3a1a6adf6073c89ff77152c0ae5cf212d097e6ad8b864f2bf220772511887db3c1692d7c4b7be3d3f54be01ec5
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/generators/base"
|
4
|
+
|
5
|
+
module Motion
|
6
|
+
module Generators
|
7
|
+
class InstallGenerator < Rails::Generators::Base
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
9
|
+
|
10
|
+
desc "Installs Motion into your application."
|
11
|
+
|
12
|
+
def copy_initializer
|
13
|
+
template(
|
14
|
+
"motion.rb",
|
15
|
+
"config/initializers/motion.rb"
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
def copy_stimlus_controller
|
20
|
+
template(
|
21
|
+
"motion_controller.js",
|
22
|
+
"app/javascript/controllers/motion_controller.js"
|
23
|
+
)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# TODO: Explain all the options.
|
4
|
+
Motion.configure do |config|
|
5
|
+
# config.secret = Rails.application.key_generator.generate_key "motion:secret"
|
6
|
+
|
7
|
+
# config.revision = `git rev-parse HEAD`.chomp
|
8
|
+
|
9
|
+
# config.renderer_for_connection_proc = ->(websocket_connection) do
|
10
|
+
# ApplicationController.renderer.new(
|
11
|
+
# websocket_connection.env.slice(
|
12
|
+
# Rack::HTTP_COOKIE,
|
13
|
+
# Rack::RACK_SESSION,
|
14
|
+
# )
|
15
|
+
# )
|
16
|
+
# end
|
17
|
+
|
18
|
+
# config.stimulus_controller_identifier = "motion"
|
19
|
+
# config.key_attribute = "data-motion-key"
|
20
|
+
# config.state_attribute = "data-motion-state"
|
21
|
+
# config.motion_attribute = "data-motion"
|
22
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
import { Controller } from "@unabridged/motion";
|
2
|
+
import consumer from "../channels/consumer";
|
3
|
+
|
4
|
+
// If you change the name of this controller (determined by the file name),
|
5
|
+
// make sure to update `Motion.config.stimulus_controller_identifier`.
|
6
|
+
export default class extends Controller {
|
7
|
+
// To avoid creating a second websocket, make sure to reuse the application's
|
8
|
+
// ActionCable consumer.
|
9
|
+
getConsumer() {
|
10
|
+
return consumer;
|
11
|
+
}
|
12
|
+
|
13
|
+
// It is possible to additionally customize the behavior of the client by
|
14
|
+
// overriding these properties and methods:
|
15
|
+
|
16
|
+
// getExtraDataForEvent(event) {} // `Motion::Event#extra_data`
|
17
|
+
|
18
|
+
// keyAttribute = "data-motion-key"; // `Motion.config.key_attribute`
|
19
|
+
// stateAttribute = "data-motion-state"; // `Motion.config.state_attribute`
|
20
|
+
// motionAttribute = "data-motion"; // `Motion.config.motion_attribute`
|
21
|
+
|
22
|
+
// beforeConnect() { /* by default, dispatches `motion:before-connect` */ }
|
23
|
+
// connected() { /* by default, dispatches `motion:connected` */ }
|
24
|
+
// connectFailed() { /* by default, dispatches `motion:connect-failed` */ }
|
25
|
+
// disconnected() { /* by default, dispatches `motion:disconnected` */ }
|
26
|
+
// beforeRender() { /* by default, dispatches `motion:before-render` */ }
|
27
|
+
// rendered() { /* by default, dispatches `motion:rendered` */ }
|
28
|
+
}
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "motion"
|
4
|
+
|
5
|
+
module Motion
|
6
|
+
module ActionCableExtentions
|
7
|
+
# Provides a `streaming_from(broadcasts, to:)` API that can be used to
|
8
|
+
# declaratively specify what `broadcasts` the channel is interested in
|
9
|
+
# receiving and `to` what method they should be routed. Additionally,
|
10
|
+
# this module extends the "at most one executor at a time" property that
|
11
|
+
# naturally comes with actions to the streams that it sets up as well.
|
12
|
+
module DeclarativeStreams
|
13
|
+
def initialize(*)
|
14
|
+
super
|
15
|
+
|
16
|
+
# Allowing actions to be bound to streams (as this module provides)
|
17
|
+
# introduces the possibiliy of multiple threads accessing user code at
|
18
|
+
# the same time. Protect user code with a Monitor so we only have to
|
19
|
+
# worry about that here.
|
20
|
+
@_declarative_stream_monitor = Monitor.new
|
21
|
+
|
22
|
+
# Streams that we are currently interested in
|
23
|
+
@_declarative_streams = Set.new
|
24
|
+
|
25
|
+
# The method we are currently routing those streams to
|
26
|
+
@_declarative_stream_target = nil
|
27
|
+
|
28
|
+
# Streams that we are setup to listen to. Sadly, there is no public API
|
29
|
+
# to stop streaming so this will only grow.
|
30
|
+
@_declarative_stream_proxies = Set.new
|
31
|
+
end
|
32
|
+
|
33
|
+
# Synchronize all ActionCable entry points (after initialization).
|
34
|
+
def subscribe_to_channel(*)
|
35
|
+
@_declarative_stream_monitor.synchronize { super }
|
36
|
+
end
|
37
|
+
|
38
|
+
def unsubscribe_from_channel(*)
|
39
|
+
@_declarative_stream_monitor.synchronize { super }
|
40
|
+
end
|
41
|
+
|
42
|
+
def perform_action(*)
|
43
|
+
@_declarative_stream_monitor.synchronize { super }
|
44
|
+
end
|
45
|
+
|
46
|
+
# Clean up declarative streams when all streams are stopped.
|
47
|
+
def stop_all_streams
|
48
|
+
super
|
49
|
+
|
50
|
+
@_declarative_streams.clear
|
51
|
+
@_declarative_stream_target = nil
|
52
|
+
|
53
|
+
@_declarative_stream_proxies.clear
|
54
|
+
end
|
55
|
+
|
56
|
+
# Declaratively routes provided broadcasts to the provided method.
|
57
|
+
def streaming_from(broadcasts, to:)
|
58
|
+
@_declarative_streams.replace(broadcasts)
|
59
|
+
@_declarative_stream_target = to
|
60
|
+
|
61
|
+
@_declarative_streams.each(&method(:_ensure_declarative_stream_proxy))
|
62
|
+
end
|
63
|
+
|
64
|
+
def declarative_stream_target
|
65
|
+
@_declarative_stream_target
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def _ensure_declarative_stream_proxy(broadcast)
|
71
|
+
return unless @_declarative_stream_proxies.add?(broadcast)
|
72
|
+
|
73
|
+
# TODO: Something about this doesn't deal with the coder correctly.
|
74
|
+
stream_from(broadcast) do |message|
|
75
|
+
_handle_incoming_broadcast_to_declarative_stream(broadcast, message)
|
76
|
+
rescue Exception => exception # rubocop:disable Lint/RescueException
|
77
|
+
# It is very, very important that we do not allow an exception to
|
78
|
+
# escape here as the internals of ActionCable will stop processing
|
79
|
+
# the broadcast.
|
80
|
+
|
81
|
+
_handle_exception_in_declarative_stream(broadcast, exception)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def _handle_incoming_broadcast_to_declarative_stream(broadcast, message)
|
86
|
+
@_declarative_stream_monitor.synchronize do
|
87
|
+
return unless @_declarative_stream_target &&
|
88
|
+
@_declarative_streams.include?(broadcast)
|
89
|
+
|
90
|
+
send(@_declarative_stream_target, broadcast, message)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def _handle_exception_in_declarative_stream(broadcast, exception)
|
95
|
+
logger.error(
|
96
|
+
"There was an exception while handling a broadcast to #{broadcast}" \
|
97
|
+
"on #{self.class}:\n" \
|
98
|
+
" #{exception.class}: #{exception.message}\n" \
|
99
|
+
"#{exception.backtrace.map { |line| " #{line}" }.join("\n")}"
|
100
|
+
)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "motion"
|
4
|
+
|
5
|
+
module Motion
|
6
|
+
module ActionCableExtentions
|
7
|
+
# By default ActionCable logs a lot. This module suppresses the debugging
|
8
|
+
# information on a _per channel_ basis.
|
9
|
+
module LogSuppression
|
10
|
+
class Suppressor < SimpleDelegator
|
11
|
+
def info(*)
|
12
|
+
end
|
13
|
+
|
14
|
+
def debug(*)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private_constant :Suppressor
|
19
|
+
|
20
|
+
def initialize(*)
|
21
|
+
super
|
22
|
+
|
23
|
+
@_logger = Suppressor.new(logger)
|
24
|
+
end
|
25
|
+
|
26
|
+
def logger
|
27
|
+
return super unless defined?(@_logger)
|
28
|
+
|
29
|
+
@_logger
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "motion"
|
4
|
+
|
5
|
+
module Motion
|
6
|
+
module ActionCableExtentions
|
7
|
+
autoload :DeclarativeStreams,
|
8
|
+
"motion/action_cable_extentions/declarative_streams"
|
9
|
+
|
10
|
+
autoload :LogSuppression,
|
11
|
+
"motion/action_cable_extentions/log_suppression"
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action_cable"
|
4
|
+
|
5
|
+
require "motion"
|
6
|
+
|
7
|
+
module Motion
|
8
|
+
class Channel < ActionCable::Channel::Base
|
9
|
+
include ActionCableExtentions::DeclarativeStreams
|
10
|
+
include ActionCableExtentions::LogSuppression
|
11
|
+
|
12
|
+
ACTION_METHODS = Set.new(["process_motion"]).freeze
|
13
|
+
private_constant :ACTION_METHODS
|
14
|
+
|
15
|
+
# Don't use the ActionCable huertistic for deciding what actions can be
|
16
|
+
# called from JavaScript. Instead, hard-code the list so we can make other
|
17
|
+
# methods public without worrying about them being called from JavaScript.
|
18
|
+
def self.action_methods
|
19
|
+
ACTION_METHODS
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_reader :component_connection
|
23
|
+
|
24
|
+
def subscribed
|
25
|
+
state, client_version = params.values_at("state", "version")
|
26
|
+
|
27
|
+
# TODO: This is too restrictive. Introduce a protocol version and support
|
28
|
+
# older versions of the client that have a compatible protocol.
|
29
|
+
unless Motion::VERSION == client_version
|
30
|
+
raise IncompatibleClientError.new(Motion::VERSION, client_version)
|
31
|
+
end
|
32
|
+
|
33
|
+
@component_connection =
|
34
|
+
ComponentConnection.from_state(state, log_helper: log_helper)
|
35
|
+
|
36
|
+
synchronize
|
37
|
+
rescue => error
|
38
|
+
reject
|
39
|
+
|
40
|
+
handle_error(error, "connecting a component")
|
41
|
+
end
|
42
|
+
|
43
|
+
def unsubscribed
|
44
|
+
component_connection&.close
|
45
|
+
|
46
|
+
@component_connection = nil
|
47
|
+
end
|
48
|
+
|
49
|
+
def process_motion(data)
|
50
|
+
motion, raw_event = data.values_at("name", "event")
|
51
|
+
|
52
|
+
component_connection.process_motion(motion, Event.from_raw(raw_event))
|
53
|
+
synchronize
|
54
|
+
end
|
55
|
+
|
56
|
+
def process_broadcast(broadcast, message)
|
57
|
+
component_connection.process_broadcast(broadcast, message)
|
58
|
+
synchronize
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def synchronize
|
64
|
+
streaming_from(component_connection.broadcasts, to: :process_broadcast)
|
65
|
+
|
66
|
+
component_connection.if_render_required do |component|
|
67
|
+
transmit(renderer.render(component))
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def handle_error(error, context)
|
72
|
+
log_helper.error("An error occurred while #{context}", error: error)
|
73
|
+
end
|
74
|
+
|
75
|
+
def log_helper
|
76
|
+
@log_helper ||= LogHelper.for_channel(self)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Memoize the renderer on the connection so that it can be shared accross
|
80
|
+
# all components. `ActionController::Renderer` is already thread-safe and
|
81
|
+
# designed to be reused.
|
82
|
+
def renderer
|
83
|
+
connection.instance_eval do
|
84
|
+
@_motion_renderer ||= Motion.build_renderer_for(self)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,88 @@
|
|
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/object/to_param"
|
6
|
+
|
7
|
+
require "motion"
|
8
|
+
|
9
|
+
module Motion
|
10
|
+
module Component
|
11
|
+
module Broadcasts
|
12
|
+
extend ActiveSupport::Concern
|
13
|
+
|
14
|
+
included do
|
15
|
+
class_attribute :_broadcast_handlers,
|
16
|
+
instance_reader: false,
|
17
|
+
instance_writer: false,
|
18
|
+
instance_predicate: false,
|
19
|
+
default: {}.freeze
|
20
|
+
end
|
21
|
+
|
22
|
+
class_methods do
|
23
|
+
def broadcast_to(model, message)
|
24
|
+
ActionCable.server.broadcast(broadcasting_for(model), message)
|
25
|
+
end
|
26
|
+
|
27
|
+
def stream_from(broadcast, handler)
|
28
|
+
self._broadcast_handlers =
|
29
|
+
_broadcast_handlers.merge(broadcast.to_s => handler.to_sym).freeze
|
30
|
+
end
|
31
|
+
|
32
|
+
def stream_for(model, handler)
|
33
|
+
stream_from(broadcasting_for(model), handler)
|
34
|
+
end
|
35
|
+
|
36
|
+
def broadcasting_for(model)
|
37
|
+
serialize_broadcasting([name, model])
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
# Taken from ActionCable::Channel::Broadcasting
|
43
|
+
def serialize_broadcasting(object)
|
44
|
+
if object.is_a?(Array)
|
45
|
+
object.map { |m| serialize_broadcasting(m) }.join(":")
|
46
|
+
elsif object.respond_to?(:to_gid_param)
|
47
|
+
object.to_gid_param
|
48
|
+
else
|
49
|
+
object.to_param
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def broadcasts
|
55
|
+
_broadcast_handlers.keys
|
56
|
+
end
|
57
|
+
|
58
|
+
def process_broadcast(broadcast, message)
|
59
|
+
return unless (handler = _broadcast_handlers[broadcast])
|
60
|
+
|
61
|
+
send(handler, message)
|
62
|
+
end
|
63
|
+
|
64
|
+
def broadcast_to(model, message)
|
65
|
+
self.class.broadcast_to(model, message)
|
66
|
+
end
|
67
|
+
|
68
|
+
def stream_from(broadcast, handler)
|
69
|
+
self._broadcast_handlers =
|
70
|
+
_broadcast_handlers.merge(broadcast.to_s => handler.to_sym).freeze
|
71
|
+
end
|
72
|
+
|
73
|
+
def stream_for(model, handler)
|
74
|
+
stream_from(self.class.broadcasting_for(model), handler)
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
attr_writer :_broadcast_handlers
|
80
|
+
|
81
|
+
def _broadcast_handlers
|
82
|
+
return @_broadcast_handlers if defined?(@_broadcast_handlers)
|
83
|
+
|
84
|
+
self.class._broadcast_handlers
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/concern"
|
4
|
+
|
5
|
+
require "motion"
|
6
|
+
|
7
|
+
module Motion
|
8
|
+
module Component
|
9
|
+
module Lifecycle
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
|
12
|
+
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
|
20
|
+
)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def connected
|
25
|
+
end
|
26
|
+
|
27
|
+
def disconnected
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/concern"
|
4
|
+
require "active_support/core_ext/class/attribute"
|
5
|
+
|
6
|
+
require "motion"
|
7
|
+
|
8
|
+
module Motion
|
9
|
+
module Component
|
10
|
+
module Motions
|
11
|
+
extend ActiveSupport::Concern
|
12
|
+
|
13
|
+
included do
|
14
|
+
class_attribute :_motion_handlers,
|
15
|
+
instance_reader: false,
|
16
|
+
instance_writer: false,
|
17
|
+
instance_predicate: false,
|
18
|
+
default: {}.freeze
|
19
|
+
end
|
20
|
+
|
21
|
+
class_methods do
|
22
|
+
def map_motion(motion, handler = motion)
|
23
|
+
self._motion_handlers =
|
24
|
+
_motion_handlers.merge(motion.to_s => handler.to_sym).freeze
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def motions
|
29
|
+
_motion_handlers.keys
|
30
|
+
end
|
31
|
+
|
32
|
+
def process_motion(motion, event = nil)
|
33
|
+
unless (handler = _motion_handlers[motion])
|
34
|
+
raise MotionNotMapped.new(self, motion)
|
35
|
+
end
|
36
|
+
|
37
|
+
if method(handler).arity.zero?
|
38
|
+
send(handler)
|
39
|
+
else
|
40
|
+
send(handler, event)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
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
|
+
private
|
50
|
+
|
51
|
+
attr_writer :_motion_handlers
|
52
|
+
|
53
|
+
def _motion_handlers
|
54
|
+
return @_motion_handlers if defined?(@_motion_handlers)
|
55
|
+
|
56
|
+
self.class._motion_handlers
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "motion"
|
4
|
+
|
5
|
+
module Motion
|
6
|
+
module Component
|
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
|
13
|
+
|
14
|
+
def rerender!
|
15
|
+
instance_variable_set(RERENDER_MARKER_IVAR, true)
|
16
|
+
end
|
17
|
+
|
18
|
+
def awaiting_forced_rerender?
|
19
|
+
instance_variable_defined?(RERENDER_MARKER_IVAR)
|
20
|
+
end
|
21
|
+
|
22
|
+
# * This can be overwritten.
|
23
|
+
# * It will _not_ be sent to the client.
|
24
|
+
# * If it doesn't change every time the component's state changes,
|
25
|
+
# things may fall out of sync unless you also call `#rerender!`
|
26
|
+
def render_hash
|
27
|
+
# TODO: This implementation is trivially correct, but very wasteful.
|
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
|
37
|
+
end
|
38
|
+
|
39
|
+
def render_in(view_context)
|
40
|
+
raise BlockNotAllowedError, self if block_given?
|
41
|
+
clear_awaiting_forced_rerender!
|
42
|
+
|
43
|
+
html = view_context.capture { without_new_instance_variables { super } }
|
44
|
+
|
45
|
+
Motion.markup_transformer.add_state_to_html(self, html)
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def clear_awaiting_forced_rerender!
|
51
|
+
return unless awaiting_forced_rerender?
|
52
|
+
|
53
|
+
remove_instance_variable(RERENDER_MARKER_IVAR)
|
54
|
+
end
|
55
|
+
|
56
|
+
def without_new_instance_variables
|
57
|
+
existing_instance_variables = instance_variables
|
58
|
+
|
59
|
+
yield
|
60
|
+
ensure
|
61
|
+
(instance_variables - existing_instance_variables)
|
62
|
+
.each(&method(:remove_instance_variable))
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/concern"
|
4
|
+
|
5
|
+
require "motion"
|
6
|
+
|
7
|
+
require "motion/component/broadcasts"
|
8
|
+
require "motion/component/lifecycle"
|
9
|
+
require "motion/component/motions"
|
10
|
+
require "motion/component/rendering"
|
11
|
+
|
12
|
+
module Motion
|
13
|
+
module Component
|
14
|
+
extend ActiveSupport::Concern
|
15
|
+
|
16
|
+
include Broadcasts
|
17
|
+
include Lifecycle
|
18
|
+
include Motions
|
19
|
+
include Rendering
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "motion"
|
4
|
+
|
5
|
+
module Motion
|
6
|
+
class ComponentConnection
|
7
|
+
def self.from_state(
|
8
|
+
state,
|
9
|
+
serializer: Motion.serializer,
|
10
|
+
log_helper: LogHelper.new,
|
11
|
+
**kargs
|
12
|
+
)
|
13
|
+
component = serializer.deserialize(state)
|
14
|
+
|
15
|
+
new(component, log_helper: log_helper.for_component(component), **kargs)
|
16
|
+
end
|
17
|
+
|
18
|
+
attr_reader :component
|
19
|
+
|
20
|
+
def initialize(component, log_helper: LogHelper.for_component(component))
|
21
|
+
@component = component
|
22
|
+
@log_helper = log_helper
|
23
|
+
|
24
|
+
timing("Connected") do
|
25
|
+
@render_hash = component.render_hash
|
26
|
+
|
27
|
+
component.connected
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def close
|
32
|
+
timing("Disconnected") do
|
33
|
+
component.disconnected
|
34
|
+
end
|
35
|
+
|
36
|
+
true
|
37
|
+
rescue => error
|
38
|
+
handle_error(error, "disconnecting the component")
|
39
|
+
|
40
|
+
false
|
41
|
+
end
|
42
|
+
|
43
|
+
def process_motion(motion, event = nil)
|
44
|
+
timing("Proccessed #{motion}") do
|
45
|
+
component.process_motion(motion, event)
|
46
|
+
end
|
47
|
+
|
48
|
+
true
|
49
|
+
rescue => error
|
50
|
+
handle_error(error, "processing #{motion}")
|
51
|
+
|
52
|
+
false
|
53
|
+
end
|
54
|
+
|
55
|
+
def process_broadcast(broadcast, message)
|
56
|
+
timing("Proccessed broadcast to #{broadcast}") do
|
57
|
+
component.process_broadcast broadcast, message
|
58
|
+
end
|
59
|
+
|
60
|
+
true
|
61
|
+
rescue => error
|
62
|
+
handle_error(error, "processing a broadcast to #{broadcast}")
|
63
|
+
|
64
|
+
false
|
65
|
+
end
|
66
|
+
|
67
|
+
def if_render_required(&block)
|
68
|
+
timing("Rendered") do
|
69
|
+
next_render_hash = component.render_hash
|
70
|
+
|
71
|
+
return if @render_hash == next_render_hash &&
|
72
|
+
!component.awaiting_forced_rerender?
|
73
|
+
|
74
|
+
yield(component)
|
75
|
+
|
76
|
+
@render_hash = next_render_hash
|
77
|
+
end
|
78
|
+
rescue => error
|
79
|
+
handle_error(error, "rendering the component")
|
80
|
+
end
|
81
|
+
|
82
|
+
def broadcasts
|
83
|
+
component.broadcasts
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
attr_reader :log_helper
|
89
|
+
|
90
|
+
def timing(context, &block)
|
91
|
+
log_helper.timing(context, &block)
|
92
|
+
end
|
93
|
+
|
94
|
+
def handle_error(error, context)
|
95
|
+
log_helper.error("An error occurred while #{context}", error: error)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|