motion 0.1.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 +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
|