live_cable 0.0.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/app/assets/javascript/controllers/live_controller.js +123 -0
- data/app/assets/javascript/live_cable_blessing.js +20 -0
- data/app/assets/javascript/subscriptions.js +191 -0
- data/app/channels/live_channel.rb +45 -0
- data/app/helpers/live_cable_helper.rb +70 -0
- data/config/importmap.rb +5 -0
- data/lib/live_cable/component.rb +255 -0
- data/lib/live_cable/connection.rb +205 -0
- data/lib/live_cable/container.rb +103 -0
- data/lib/live_cable/csrf_checker.rb +20 -0
- data/lib/live_cable/delegation/array.rb +81 -0
- data/lib/live_cable/delegation/hash.rb +38 -0
- data/lib/live_cable/delegation/methods.rb +37 -0
- data/lib/live_cable/delegation/model.rb +12 -0
- data/lib/live_cable/delegator.rb +114 -0
- data/lib/live_cable/engine.rb +23 -0
- data/lib/live_cable/error.rb +5 -0
- data/lib/live_cable/model_observer.rb +13 -0
- data/lib/live_cable/observer.rb +44 -0
- data/lib/live_cable/observer_tracking.rb +71 -0
- data/lib/live_cable/render_context.rb +39 -0
- data/lib/live_cable.rb +47 -0
- metadata +63 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LiveCable
|
|
4
|
+
module Delegation
|
|
5
|
+
module Methods
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def decorate_getters(methods)
|
|
9
|
+
methods.each do |method|
|
|
10
|
+
decorate_getter(method)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def decorate_getter(method)
|
|
15
|
+
define_method(method) do |*pos, **kwargs, &block|
|
|
16
|
+
create_delegator(
|
|
17
|
+
__getobj__.method(method).call(*pos, **kwargs, &block)
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def decorate_mutators(methods)
|
|
23
|
+
methods.each do |method|
|
|
24
|
+
decorate_mutator(method)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def decorate_mutator(method)
|
|
29
|
+
define_method(method) do |*pos, **kwargs, &block|
|
|
30
|
+
notify_live_cable_observers
|
|
31
|
+
|
|
32
|
+
__getobj__.method(method).call(*pos, **kwargs, &block)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LiveCable
|
|
4
|
+
# Delegation modules for different data types.
|
|
5
|
+
# Each module defines which methods should trigger change notifications.
|
|
6
|
+
module Delegation
|
|
7
|
+
extend ActiveSupport::Autoload
|
|
8
|
+
|
|
9
|
+
autoload :Array
|
|
10
|
+
autoload :Hash
|
|
11
|
+
autoload :Methods
|
|
12
|
+
autoload :Model
|
|
13
|
+
|
|
14
|
+
# Maps Ruby classes to their delegation modules.
|
|
15
|
+
# When a value of one of these types is stored in a container,
|
|
16
|
+
# it will be wrapped in a Delegator and extended with the appropriate module.
|
|
17
|
+
SUPPORTED = {
|
|
18
|
+
::ActiveRecord::Base => Delegation::Model,
|
|
19
|
+
::Array => Delegation::Array,
|
|
20
|
+
::Hash => Delegation::Hash,
|
|
21
|
+
}.freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Wraps mutable objects (Arrays, Hashes, Models) to track changes.
|
|
25
|
+
#
|
|
26
|
+
# The Delegator uses Ruby's SimpleDelegator to transparently wrap objects
|
|
27
|
+
# and intercept method calls. When mutative methods are called, observers
|
|
28
|
+
# are notified so the component can be re-rendered.
|
|
29
|
+
#
|
|
30
|
+
# @example Automatic wrapping
|
|
31
|
+
# container[:tags] = ['ruby', 'rails']
|
|
32
|
+
# # Container automatically wraps the array in a Delegator
|
|
33
|
+
# container[:tags] << 'rspec' # Triggers observer notification
|
|
34
|
+
#
|
|
35
|
+
# @example Manual creation
|
|
36
|
+
# delegator = Delegator.new(['ruby', 'rails'])
|
|
37
|
+
# delegator.add_live_cable_observer(observer, :tags)
|
|
38
|
+
# delegator << 'rspec' # Notifies observer
|
|
39
|
+
#
|
|
40
|
+
# Architecture:
|
|
41
|
+
# - Delegator extends SimpleDelegator to wrap the target object
|
|
42
|
+
# - Dynamically extends with type-specific delegation modules (Array/Hash/Model)
|
|
43
|
+
# - Each delegation module decorates mutative methods to notify observers
|
|
44
|
+
# - Getter methods are not decorated (no notification on read)
|
|
45
|
+
# - Nested structures automatically get wrapped in new Delegators with the same observers
|
|
46
|
+
#
|
|
47
|
+
# Supported Types:
|
|
48
|
+
# - Array: Tracks push, <<, delete, etc.
|
|
49
|
+
# - Hash: Tracks []=, delete, merge!, etc.
|
|
50
|
+
# - ActiveRecord::Base: Tracks assign_attributes, update, etc.
|
|
51
|
+
class Delegator < SimpleDelegator
|
|
52
|
+
include ObserverTracking
|
|
53
|
+
|
|
54
|
+
# @param value [Object] The object to wrap and track
|
|
55
|
+
def initialize(value)
|
|
56
|
+
super
|
|
57
|
+
|
|
58
|
+
# Extend with the appropriate delegation module based on value's type
|
|
59
|
+
Delegation::SUPPORTED.each do |klass, delegator|
|
|
60
|
+
if value.is_a?(klass)
|
|
61
|
+
extend delegator
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Factory method to create a Delegator only if the value's type is supported.
|
|
67
|
+
# Returns the original value unchanged if not supported.
|
|
68
|
+
#
|
|
69
|
+
# @param value [Object] The value to potentially wrap
|
|
70
|
+
# @param variable [Symbol] The reactive variable name
|
|
71
|
+
# @param observer [Observer] The observer to attach
|
|
72
|
+
# @return [Delegator, Object] Wrapped value or original value
|
|
73
|
+
def self.create_if_supported(value, variable, observer)
|
|
74
|
+
if supported?(value)
|
|
75
|
+
return new(value).tap do |delegator|
|
|
76
|
+
delegator.add_live_cable_observer(observer, variable)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
value
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Check if a value's type can be wrapped in a Delegator.
|
|
84
|
+
#
|
|
85
|
+
# @param value [Object] The value to check
|
|
86
|
+
# @return [Boolean] true if value can be delegated
|
|
87
|
+
def self.supported?(value)
|
|
88
|
+
Delegation::SUPPORTED.keys.any? { |c| value.is_a?(c) }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
# Create a new Delegator for nested values (e.g., nested arrays/hashes).
|
|
94
|
+
# Propagates all observers from the parent delegator to the child.
|
|
95
|
+
#
|
|
96
|
+
# @param value [Object] The nested value to wrap
|
|
97
|
+
# @return [Delegator, Object] Wrapped value or original if not supported
|
|
98
|
+
#
|
|
99
|
+
# @example Nested arrays
|
|
100
|
+
# outer = Delegator.new([['inner']])
|
|
101
|
+
# inner = outer[0] # Returns a Delegator wrapping ['inner']
|
|
102
|
+
# inner << 'new' # Notifies same observers as outer
|
|
103
|
+
def create_delegator(value)
|
|
104
|
+
return value unless self.class.supported?(value)
|
|
105
|
+
|
|
106
|
+
# Create new delegator and propagate all observers from parent
|
|
107
|
+
self.class.new(value).tap do |delegator|
|
|
108
|
+
live_cable_observers.each do |variable, observers|
|
|
109
|
+
observers.each { |observer| delegator.add_live_cable_observer(observer, variable) }
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LiveCable
|
|
4
|
+
class Engine < ::Rails::Engine
|
|
5
|
+
config.before_configuration do |app|
|
|
6
|
+
# Setup autoloader to use Live namespace for components
|
|
7
|
+
Rails.autoloaders.main.push_dir(app.root.join('app/live'), namespace: Live)
|
|
8
|
+
|
|
9
|
+
# Add LiveCable to importmap
|
|
10
|
+
app.config.importmap.paths << root.join('config/importmap.rb')
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
initializer 'live_cable.assets_precompile' do |app|
|
|
14
|
+
app.config.assets.precompile << %w[live_cable/**/*.js]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
initializer 'live_cable.active_record' do
|
|
18
|
+
ActiveSupport.on_load :active_record do
|
|
19
|
+
include ModelObserver
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LiveCable
|
|
4
|
+
# Observer pattern implementation for tracking changes to reactive variables.
|
|
5
|
+
#
|
|
6
|
+
# The Observer is attached to delegated values (Arrays, Hashes, ActiveRecord models)
|
|
7
|
+
# and notifies the container when changes occur. This enables automatic re-rendering
|
|
8
|
+
# of components when their data changes.
|
|
9
|
+
#
|
|
10
|
+
# @example Basic usage
|
|
11
|
+
# container = Container.new
|
|
12
|
+
# observer = Observer.new(container)
|
|
13
|
+
# observer.notify(:username) # Marks :username as dirty in the container
|
|
14
|
+
#
|
|
15
|
+
# Architecture:
|
|
16
|
+
# - Each Container has one Observer instance
|
|
17
|
+
# - Delegators (Array/Hash wrappers) hold references to observers
|
|
18
|
+
# - When a mutative method is called on a delegator, it notifies all its observers
|
|
19
|
+
# - The observer marks the variable as dirty in the container's changeset
|
|
20
|
+
# - After processing a message, the connection broadcasts changes to all dirty components
|
|
21
|
+
class Observer
|
|
22
|
+
# @param container [LiveCable::Container] The container to notify of changes
|
|
23
|
+
def initialize(container)
|
|
24
|
+
@container = container
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Notify the container that a variable has changed.
|
|
28
|
+
# This marks the variable as "dirty" so the component will be re-rendered.
|
|
29
|
+
#
|
|
30
|
+
# @param variable [Symbol] The name of the reactive variable that changed
|
|
31
|
+
# @return [void]
|
|
32
|
+
#
|
|
33
|
+
# @example
|
|
34
|
+
# observer.notify(:tags) # Component will re-render because :tags changed
|
|
35
|
+
def notify(variable)
|
|
36
|
+
container.mark_dirty(variable)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
# @return [LiveCable::Container]
|
|
42
|
+
attr_reader :container
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LiveCable
|
|
4
|
+
# Mixin for objects that need to track and notify observers of changes.
|
|
5
|
+
#
|
|
6
|
+
# This module is included in both Delegator and ModelObserver to provide
|
|
7
|
+
# a consistent interface for attaching observers and notifying them of changes.
|
|
8
|
+
#
|
|
9
|
+
# @example Usage in a Delegator
|
|
10
|
+
# class MyDelegator < SimpleDelegator
|
|
11
|
+
# include ObserverTracking
|
|
12
|
+
#
|
|
13
|
+
# def mutate!
|
|
14
|
+
# # ... perform mutation ...
|
|
15
|
+
# notify_live_cable_observers # Notify all observers
|
|
16
|
+
# end
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# Architecture:
|
|
20
|
+
# - Objects can have multiple observers attached
|
|
21
|
+
# - Each observer is associated with a specific variable name
|
|
22
|
+
# - When the object changes, all observers are notified
|
|
23
|
+
# - Observers then mark their variables as dirty in their containers
|
|
24
|
+
module ObserverTracking
|
|
25
|
+
# Attach an observer to track changes to a specific variable.
|
|
26
|
+
# The same observer can be attached multiple times for different variables.
|
|
27
|
+
#
|
|
28
|
+
# @param observer [LiveCable::Observer] The observer to notify of changes
|
|
29
|
+
# @param variable [Symbol] The reactive variable name this observer tracks
|
|
30
|
+
# @return [void]
|
|
31
|
+
#
|
|
32
|
+
# @example
|
|
33
|
+
# observer = Observer.new(container)
|
|
34
|
+
# tags_array.add_live_cable_observer(observer, :tags)
|
|
35
|
+
# tags_array << 'ruby' # Observer will be notified
|
|
36
|
+
def add_live_cable_observer(observer, variable)
|
|
37
|
+
observers = live_cable_observers_for(variable)
|
|
38
|
+
|
|
39
|
+
unless observers.include?(observer)
|
|
40
|
+
observers << observer
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# Get the hash of all observers, keyed by variable name.
|
|
47
|
+
#
|
|
48
|
+
# @return [Hash<Symbol, Array<Observer>>] Variable name => observers
|
|
49
|
+
def live_cable_observers
|
|
50
|
+
@live_cable_observers ||= {}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Get the list of observers for a specific variable.
|
|
54
|
+
#
|
|
55
|
+
# @param variable [Symbol] The variable name
|
|
56
|
+
# @return [Array<Observer>] List of observers for this variable
|
|
57
|
+
def live_cable_observers_for(variable)
|
|
58
|
+
live_cable_observers[variable] ||= []
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Notify all attached observers that this object has changed.
|
|
62
|
+
# Called automatically by mutative methods in delegation modules.
|
|
63
|
+
#
|
|
64
|
+
# @return [void]
|
|
65
|
+
def notify_live_cable_observers
|
|
66
|
+
live_cable_observers.each do |variable, observers|
|
|
67
|
+
observers.each { |observer| observer.notify(variable) }
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LiveCable
|
|
4
|
+
class RenderContext
|
|
5
|
+
def initialize(component)
|
|
6
|
+
@component = component
|
|
7
|
+
@children = []
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# @return [Array<LiveCable::Component>]
|
|
11
|
+
attr_reader :children
|
|
12
|
+
|
|
13
|
+
def reset
|
|
14
|
+
self.children = []
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def add_component(child)
|
|
18
|
+
return unless component.live_connection
|
|
19
|
+
|
|
20
|
+
component.live_connection.add_component(child)
|
|
21
|
+
children << child
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @return [LiveCable::Component, nil]
|
|
25
|
+
def get_component(live_id)
|
|
26
|
+
component.live_connection&.get_component(live_id)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @return [LiveCable::Connection]
|
|
30
|
+
def live_connection
|
|
31
|
+
component.live_connection
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
# @return [LiveCable::Component]
|
|
37
|
+
attr_reader :component
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/live_cable.rb
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LiveCable
|
|
4
|
+
extend ActiveSupport::Autoload
|
|
5
|
+
|
|
6
|
+
autoload :Error
|
|
7
|
+
autoload :Component
|
|
8
|
+
autoload :Connection
|
|
9
|
+
autoload :Container
|
|
10
|
+
autoload :CsrfChecker
|
|
11
|
+
autoload :Delegator
|
|
12
|
+
autoload :ModelObserver
|
|
13
|
+
autoload :Observer
|
|
14
|
+
autoload :ObserverTracking
|
|
15
|
+
autoload :RenderContext
|
|
16
|
+
|
|
17
|
+
def self.instance_from_string(string, id)
|
|
18
|
+
klass = Live
|
|
19
|
+
klass_string = string.camelize
|
|
20
|
+
|
|
21
|
+
begin
|
|
22
|
+
klass_string.split('::').each do |part|
|
|
23
|
+
unless klass.const_defined?(part)
|
|
24
|
+
raise Error, "Component Live::#{klass_string} not found, make sure it is located in the Live:: module"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
klass = klass.const_get(part)
|
|
28
|
+
end
|
|
29
|
+
rescue NameError
|
|
30
|
+
raise LiveCable::Error, "Invalid component name \"#{string}\" - Live::#{klass_string} not found"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
klass = "Live::#{klass_string}".safe_constantize
|
|
34
|
+
|
|
35
|
+
unless klass < LiveCable::Component
|
|
36
|
+
raise 'Components must extend LiveCable::Component'
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
klass.new(id)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
module Live
|
|
44
|
+
# For components to live in
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
require 'live_cable/engine' if defined?(Rails::Engine)
|
metadata
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: live_cable
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Craig Blanchette
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: ''
|
|
13
|
+
email: craig.blanchette@gmail.com
|
|
14
|
+
executables: []
|
|
15
|
+
extensions: []
|
|
16
|
+
extra_rdoc_files: []
|
|
17
|
+
files:
|
|
18
|
+
- app/assets/javascript/controllers/live_controller.js
|
|
19
|
+
- app/assets/javascript/live_cable_blessing.js
|
|
20
|
+
- app/assets/javascript/subscriptions.js
|
|
21
|
+
- app/channels/live_channel.rb
|
|
22
|
+
- app/helpers/live_cable_helper.rb
|
|
23
|
+
- config/importmap.rb
|
|
24
|
+
- lib/live_cable.rb
|
|
25
|
+
- lib/live_cable/component.rb
|
|
26
|
+
- lib/live_cable/connection.rb
|
|
27
|
+
- lib/live_cable/container.rb
|
|
28
|
+
- lib/live_cable/csrf_checker.rb
|
|
29
|
+
- lib/live_cable/delegation/array.rb
|
|
30
|
+
- lib/live_cable/delegation/hash.rb
|
|
31
|
+
- lib/live_cable/delegation/methods.rb
|
|
32
|
+
- lib/live_cable/delegation/model.rb
|
|
33
|
+
- lib/live_cable/delegator.rb
|
|
34
|
+
- lib/live_cable/engine.rb
|
|
35
|
+
- lib/live_cable/error.rb
|
|
36
|
+
- lib/live_cable/model_observer.rb
|
|
37
|
+
- lib/live_cable/observer.rb
|
|
38
|
+
- lib/live_cable/observer_tracking.rb
|
|
39
|
+
- lib/live_cable/render_context.rb
|
|
40
|
+
homepage: https://rubygems.org/gems/live_cable
|
|
41
|
+
licenses: []
|
|
42
|
+
metadata:
|
|
43
|
+
allowed_push_host: https://rubygems.org
|
|
44
|
+
rubygems_mfa_required: 'true'
|
|
45
|
+
source_code_uri: https://github.com/isometriks/live_cable
|
|
46
|
+
rdoc_options: []
|
|
47
|
+
require_paths:
|
|
48
|
+
- lib
|
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.4'
|
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
55
|
+
requirements:
|
|
56
|
+
- - ">="
|
|
57
|
+
- !ruby/object:Gem::Version
|
|
58
|
+
version: '0'
|
|
59
|
+
requirements: []
|
|
60
|
+
rubygems_version: 3.6.9
|
|
61
|
+
specification_version: 4
|
|
62
|
+
summary: Live Components over ActionCable
|
|
63
|
+
test_files: []
|