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.
@@ -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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LiveCable
4
+ module Delegation
5
+ module Model
6
+ extend Methods
7
+
8
+ decorate_mutator :assign_attributes
9
+ decorate_mutator :update
10
+ end
11
+ end
12
+ 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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LiveCable
4
+ class Error < StandardError; end
5
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LiveCable
4
+ module ModelObserver
5
+ include ObserverTracking
6
+
7
+ def _write_attribute(...)
8
+ notify_live_cable_observers
9
+
10
+ super
11
+ end
12
+ end
13
+ 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: []