cable_room 0.1.2 → 0.2.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 841a40a6a2460c6be5811f2fe24e6fffc1bb7291446029bf1e62497df24ee507
4
- data.tar.gz: fff54424d08af52f0a35d23089f7437054b6d2e9a63ae66d620d7678edeadfdf
3
+ metadata.gz: 020faa2e6a7e8cd1027b4d5c7303d57ef1282ab4f298800df26d6336587082dc
4
+ data.tar.gz: fae6de782ad1a15d7e065fbef53e71665ebb4606ecd75748fa7e574bd63ae393
5
5
  SHA512:
6
- metadata.gz: d44fa0d6f66a5905e20ad96b3df974d9d44e43e9c057a8de6316def16f3d577c8f3270254c27cc5c0e1fc4a3fab0be2307566f55b77ada20a53c04f946c1834b
7
- data.tar.gz: ac558498e04bf2d82a9048a7ff1cab21324cc14c36fbcf9073311c3d7bc62cd1094e7df2a17b129cdd5e6bdb06e4befbec49bb6e57f310e4323aab4b2217d4e7
6
+ metadata.gz: 24de44e3b78d19d72af29783069bf8ddd6ed87d03bb185c4e2fae9b79e159f6c2da7701f2a575cf54cb2a6baaf0a66ea4e4d65c7e84048694f33c8d6c19e2dd4
7
+ data.tar.gz: 178caf0566a1420f7cb705d9076ae39a5cd664cd42da23619f9312f1feccc34b8afb55563aeb5acf8af700fe4046b6cad5f901b34c6d9bf2947d8c2f6ff08529
@@ -67,8 +67,11 @@ module CableRoom
67
67
  @mutex.synchronize do
68
68
  @current_state = :shutting_down
69
69
  stop_all_streams
70
- @room.send(:_shutdown)
71
- terminate!
70
+ begin
71
+ @room.send(:_shutdown)
72
+ ensure
73
+ terminate!
74
+ end
72
75
  end
73
76
  end
74
77
 
@@ -80,7 +83,9 @@ module CableRoom
80
83
  end
81
84
 
82
85
  def check_room_watchdog
83
- return if state == :dead || state == :shutting_down
86
+ @mutex.synchronize do
87
+ return if state == :dead || state == :shutting_down
88
+ end
84
89
 
85
90
  relock = CableRoom.lock_manager.lock(@lock_info[:resource], @lock_duration.in_milliseconds, extend: @lock_info)
86
91
  unless relock
@@ -121,7 +126,7 @@ module CableRoom
121
126
  end
122
127
  end
123
128
 
124
- def post_work(async: false, silent: false, &blk)
129
+ def _post_wrapped_work(async: false, silent: false, &blk)
125
130
  if async
126
131
  # Async stuff is mostly untracked - we just post it to the worker pool and forget about it
127
132
  worker_pool.executor.post(&blk)
@@ -137,6 +142,14 @@ module CableRoom
137
142
  end
138
143
  end
139
144
 
145
+ def post_work(**kwargs, &blk)
146
+ _post_wrapped_work(**kwargs) do
147
+ worker_pool.invoke(self, :instance_exec, connection: self, &blk)
148
+ rescue => e
149
+ logger.error "Error during work execution: #{e.class.name}: #{e.message}"
150
+ end
151
+ end
152
+
140
153
  def beat
141
154
  post_work(async: true) do
142
155
  check_room_watchdog
@@ -184,7 +197,7 @@ module CableRoom
184
197
 
185
198
  class DummyConnection
186
199
  attr_reader :channel
187
- delegate :server, :logger, :tenant, :transmit, :post_work, to: :channel
200
+ delegate :server, :logger, :tenant, :transmit, :post_work, :_post_wrapped_work, to: :channel
188
201
  delegate :event_loop, :pubsub, :worker_pool, to: :server
189
202
 
190
203
  attr_reader :identifiers
@@ -96,7 +96,7 @@ module CableRoom
96
96
  ActionCable::Server::Worker.connection = pconn
97
97
  end
98
98
 
99
- def async_invoke(receiver, method, *args, connection: receiver, async: nil, silent: false, &block)
99
+ def async_invoke(receiver, method, *args, connection: receiver, &block)
100
100
  # Instead of posting directly to the global pool, post to a dedicated queue for the room/"connection".
101
101
  # This makes each rooms so that they can be processed by at-most-one thread at a time, while still
102
102
  # allowing multiple rooms to be processed by the same thread-pool.
@@ -105,7 +105,7 @@ module CableRoom
105
105
 
106
106
  # "connection" here really references the Channel, since "Connections" in this context don't really exist
107
107
 
108
- connection.post_work(async:, silent:) do
108
+ connection._post_wrapped_work(async: false) do
109
109
  invoke(receiver, method, *args, connection: connection, &block)
110
110
  end
111
111
  end
@@ -50,15 +50,19 @@ module CableRoom
50
50
  end
51
51
 
52
52
  include Ports
53
+
53
54
  include Callbacks
54
55
  include Threading
55
56
  include ChannelAdapter
56
- include InputHandling
57
+
57
58
  include Lifecycle
58
59
  include Reaping
60
+ include InputHandling
59
61
 
60
- include MemberManagement
62
+ include PortScoping
63
+ include PortManagement
61
64
  include UserManagement
65
+ include Broadcasting
62
66
 
63
67
  attr_reader :key
64
68
 
@@ -0,0 +1,13 @@
1
+ module CableRoom
2
+ module Room
3
+ module Broadcasting
4
+ extend ActiveSupport::Concern
5
+
6
+ def broadcast(message, **kwargs)
7
+ with_port_scope!(**kwargs) do |client_port: nil|
8
+ ports[client_port || self.class::ROOM_OUT_CHANNEL] << message
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,16 +1,58 @@
1
1
  module CableRoom
2
2
  module Room
3
+ thread_mattr_accessor :current_message
4
+
3
5
  module InputHandling
4
6
  extend ActiveSupport::Concern
7
+ include ActiveSupport::Callbacks
8
+
9
+ class_methods do
10
+ def authorize_inbound(symbol_or_proc = nil, only: nil, except: nil, &blk)
11
+ raise ArgumentError, "Must provide either a symbol, proc, or block" unless symbol_or_proc ^ blk
12
+
13
+ only = Set.new(Array(only).map(&:to_sym)) if only
14
+ except = Set.new(Array(except).map(&:to_sym)) if except
15
+
16
+ blk = symbol_or_proc if symbol_or_proc.is_a?(Proc)
17
+ blk = -> { send(symbol_or_proc) } if symbol_or_proc.is_a?(Symbol)
18
+
19
+ set_callback(:receive_message, :before) do
20
+ type = message['type'].to_s.underscore.to_sym
21
+ if (only && !only.include?(type)) || (except && except.include?(type))
22
+ next
23
+ end
24
+
25
+ unless instance_exec(message, &blk)
26
+ logger.warn "Dropping unauthorized message: #{message.inspect}"
27
+ throw :abort
28
+ end
29
+ end
30
+ end
31
+ end
5
32
 
6
33
  included do
34
+ define_callbacks :receive_message
35
+
36
+ set_callback(:receive_message, :before) do
37
+ logger.debug "Received message: #{message.inspect}"
38
+ end
39
+
40
+ set_callback(:receive_message, :before) do
41
+ if message == "KILL"
42
+ shutdown!
43
+ throw :abort
44
+ end
45
+ end
46
+
7
47
  before_startup do
8
48
  ports[self.class::ROOM_IN_CHANNEL].stream do |message|
9
- logger.debug "Received message: #{message.inspect}"
10
- if message == "KILL"
11
- shutdown!
12
- else
13
- handle_received_message(message)
49
+ begin
50
+ Room.current_message = message
51
+ run_callbacks :receive_message do
52
+ handle_received_message(message)
53
+ end
54
+ ensure
55
+ Room.current_message = nil
14
56
  end
15
57
  end
16
58
  end
@@ -18,6 +60,10 @@ module CableRoom
18
60
 
19
61
  protected
20
62
 
63
+ def message
64
+ Room.current_message
65
+ end
66
+
21
67
  def handle_received_message(message)
22
68
  sym = :"on_#{message['type'].underscore.to_s.downcase}"
23
69
  if respond_to?(sym, true)
@@ -0,0 +1,136 @@
1
+ module CableRoom
2
+ module Room
3
+ module PortManagement
4
+ extend ActiveSupport::Concern
5
+
6
+ PORT_TIMEOUT = 30.seconds
7
+
8
+ class_methods do
9
+ def inbound_require_tag(tag, **kwargs)
10
+ tag_array = Array(tag).map(&:to_sym)
11
+ authorize_inbound(**kwargs) do |message|
12
+ (tag_array - origin_tags).empty?
13
+ end
14
+ end
15
+
16
+ def on_port_connected(...)
17
+ set_callback(:port_connected, :before, ...)
18
+ end
19
+
20
+ def on_port_disconnected(...)
21
+ set_callback(:port_disconnected, :after, ...)
22
+ end
23
+ end
24
+
25
+ included do
26
+ periodically :check_port_inactivity, every: PORT_TIMEOUT
27
+
28
+ define_callbacks :port_connected, :port_disconnected
29
+
30
+ set_callback(:receive_message, :around) do |_, blk|
31
+ previous_mtok = @current_message_origin
32
+ begin
33
+ mtok = message['mtok']
34
+ @current_message_origin = mtok
35
+ blk.call
36
+ ensure
37
+ @current_message_origin = previous_mtok
38
+ end
39
+ end
40
+ end
41
+
42
+ def initialize(...)
43
+ super
44
+ @port_data = {}
45
+ end
46
+
47
+ def _apply_port_scope(client_port: nil, tag: nil, **kwargs)
48
+ if client_port && tag
49
+ throw :abort unless port_data(client_port)[:tags]&.include?(tag)
50
+ end
51
+
52
+ { client_port: client_port || tag, **kwargs }
53
+ end
54
+
55
+ def reply(message = nil, **kwargs)
56
+ raise ArgumentError, "Can only use reply when handling a message" unless origin_port
57
+ raise ArgumentError, "Must provide message or block" unless message || block_given?
58
+ raise ArgumentError, "Cannot specify client_port: when using reply" if kwargs.key?(:client_port)
59
+
60
+ if block_given?
61
+ with_port_scope(client_port: origin_port) do
62
+ yield
63
+ end
64
+ else
65
+ broadcast(message, **kwargs, client_port: origin_port)
66
+ end
67
+ end
68
+
69
+ protected
70
+
71
+ def on_port_disconnected(); end
72
+
73
+ def on_port_connected(); end
74
+
75
+ def touch_port_activity(mtok = nil)
76
+ port_data(mtok)[:last_seen_at] = Time.now
77
+ end
78
+
79
+ def port_data(mtok = nil)
80
+ mtok ||= @current_message_origin
81
+ @port_data[mtok] ||= HashWithIndifferentAccess.new
82
+ end
83
+
84
+ def origin_port
85
+ @current_message_origin
86
+ end
87
+
88
+ def origin_tags
89
+ port_data[:tags] || []
90
+ end
91
+
92
+ def origin_tagged?(tag)
93
+ origin_tags.include?(tag)
94
+ end
95
+
96
+ def handle_received_message(message)
97
+ case message['type']
98
+ when 'port_connected'
99
+ mdata = port_data
100
+ mdata[:last_seen_at] = Time.now
101
+ touch_port_activity
102
+
103
+ mdata.merge! ::ActiveJob::Arguments.deserialize(message['extra'])[0] if message['extra'].present?
104
+ mdata[:tags] = Array(message['tags']).map(&:to_sym) if message['tags'].present?
105
+
106
+ run_callbacks :port_connected do
107
+ on_port_connected
108
+ end
109
+ when 'port_disconnected'
110
+ run_callbacks :port_disconnected do
111
+ on_port_disconnected
112
+ end
113
+ @port_data.delete(origin_port)
114
+ when 'port_ping'
115
+ touch_port_activity
116
+ else
117
+ super
118
+ end
119
+ end
120
+
121
+ def check_port_inactivity
122
+ return unless @port_data
123
+
124
+ threshold = PORT_TIMEOUT.ago
125
+ @port_data.each do |mtok, data|
126
+ next unless data[:last_seen_at] && data[:last_seen_at] < threshold
127
+
128
+ run_callbacks :port_disconnected do
129
+ on_port_disconnected
130
+ end
131
+ @port_data.delete(mtok)
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,40 @@
1
+ module CableRoom
2
+ module Room
3
+ thread_mattr_accessor :current_port_scope
4
+
5
+ module PortScoping
6
+ extend ActiveSupport::Concern
7
+
8
+ def without_port_scope(&blk)
9
+ prev = Room.current_port_scope
10
+ Room.current_port_scope = nil
11
+ yield
12
+ ensure
13
+ Room.current_port_scope = prev
14
+ end
15
+
16
+ # Wrap the passed block with a specific messaging scope
17
+ def with_port_scope(**kwargs, &blk)
18
+ prev = Room.current_port_scope
19
+ Room.current_port_scope = prev ? prev.merge(kwargs) : kwargs
20
+ yield
21
+ ensure
22
+ Room.current_port_scope = prev
23
+ end
24
+
25
+ # Similar to with_port_scope, but won't call the block if no ports match
26
+ def with_port_scope!(**kwargs, &blk)
27
+ if kwargs.empty?
28
+ catch :abort do
29
+ remaining = _apply_port_scope(**Room.current_port_scope || {})
30
+ blk.call(**remaining)
31
+ end
32
+ else
33
+ with_port_scope(**kwargs) do
34
+ with_port_scope!(&blk)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -3,64 +3,95 @@ module CableRoom
3
3
  module UserManagement
4
4
  extend ActiveSupport::Concern
5
5
 
6
- def initialize(...)
7
- @_user_state_map = {}
8
- @_user_map_mutex = Monitor.new
9
- super
6
+ class_methods do
7
+ def on_user_joined(...)
8
+ set_callback(:user_joined, :before, ...)
9
+ end
10
+
11
+ def on_user_left(...)
12
+ set_callback(:user_left, :after, ...)
13
+ end
10
14
  end
11
15
 
12
- def on_user_left(user); end
16
+ included do
17
+ set_callback(:port_connected, :before, :_handle_user_setup_during_connection)
18
+ set_callback(:port_disconnected, :after, :_handle_user_cleanup_after_disconnection)
13
19
 
14
- def on_member_left(mtok)
15
- _user_state_transaction do |usm, u|
16
- next unless usm.present?
17
- usm[:member_tokens].delete(mtok)
18
- if usm[:member_tokens].empty?
19
- @_user_state_map.delete(u)
20
- on_user_left(u)
21
- end
22
- end
20
+ define_callbacks :user_joined, :user_left
21
+ end
23
22
 
23
+ def initialize(...)
24
+ @_user_state_map = {}c
25
+ @_user_map_mutex = Monitor.new
24
26
  super
25
27
  end
26
28
 
27
- def on_user_joined(user); end
29
+ def on_user_left; end
28
30
 
29
- def on_member_joined(mtok)
31
+ def on_user_joined; end
32
+
33
+ protected
34
+
35
+ def _handle_user_setup_during_connection
30
36
  _user_state_transaction do |usm, u|
31
37
  if usm.nil?
32
38
  usm = @_user_state_map[u] = {
33
- member_tokens: Set.new,
39
+ port_tokens: Set.new,
34
40
  }
35
41
  is_new = true
36
42
  end
37
- usm[:member_tokens] << mtok
38
- on_user_joined(u) if is_new
43
+ usm[:port_tokens] << origin_port
44
+ if is_new
45
+ run_callbacks :user_joined do
46
+ on_user_joined
47
+ end
48
+ end
39
49
  end
50
+ end
40
51
 
41
- super
52
+ def _handle_user_cleanup_after_disconnection
53
+ _user_state_transaction do |usm, u|
54
+ next unless usm.present?
55
+ usm[:port_tokens].delete(origin_port)
56
+ if usm[:port_tokens].empty?
57
+ run_callbacks :user_left do
58
+ on_user_left
59
+ end
60
+ @_user_state_map.delete(u)
61
+ end
62
+ end
42
63
  end
43
64
 
44
65
  def connected_users
45
- _user_state_map.keys
66
+ @_user_state_map.keys
46
67
  end
47
68
 
48
- def transmit_to_user(msg, user: nil)
49
- raise "No user specified" if user.nil? && current_user.nil?
50
- user ||= current_user
69
+ def origin_user
70
+ port_data[:as]
71
+ end
51
72
 
52
- _user_state_map[user][:member_tokens].each do |mtok|
53
- ports[mtok] << msg
73
+ def all_user_tags(user = nil)
74
+ user ||= origin_user
75
+ raw_ports = @_user_state_map[user]&.[](:port_tokens) || []
76
+ tags = Set.new
77
+ raw_ports.each do |ptok|
78
+ tags.merge(port_data(ptok)&.[](:tags) || [])
54
79
  end
80
+ tags
55
81
  end
56
82
 
57
- def current_user
58
- member_data[:as]
83
+ def _apply_port_scope(user: nil, tag: nil, **kwargs)
84
+ if user && tag
85
+ user_tags = all_user_tags(user) || []
86
+ throw :abort unless user_tags.include?(tag)
87
+ end
88
+
89
+ super(tag: user || tag, **kwargs)
59
90
  end
60
91
 
61
92
  private
62
93
 
63
- def _user_state_transaction(user = current_user)
94
+ def _user_state_transaction(user = origin_user)
64
95
  return unless user.present?
65
96
  @_user_map_mutex.synchronize do
66
97
  usm = @_user_state_map[user]
@@ -5,16 +5,18 @@ module CableRoom
5
5
  eager_autoload do
6
6
  autoload :Base
7
7
 
8
- # autoload :Ports
9
8
  autoload :Callbacks
10
9
  autoload :Threading
11
10
  autoload :ChannelAdapter
12
- autoload :InputHandling
11
+
13
12
  autoload :Lifecycle
14
13
  autoload :Reaping
14
+ autoload :InputHandling
15
15
 
16
- autoload :MemberManagement
16
+ autoload :PortScoping
17
+ autoload :PortManagement
17
18
  autoload :UserManagement
19
+ autoload :Broadcasting
18
20
  end
19
21
  end
20
22
  end
@@ -28,11 +28,6 @@ module CableRoom
28
28
  }
29
29
  end
30
30
 
31
- if blk
32
- raise ArgumentError, "Cannot specify both a block and `on_opened:`" if kwargs[:on_opened]
33
- kwargs[:on_opened] = blk
34
- end
35
-
36
31
  as = respond_to?(:current_user) ? current_user : nil if as == :not_given
37
32
 
38
33
  if as
@@ -40,7 +35,7 @@ module CableRoom
40
35
  kwargs[:extra][:as] = as
41
36
  end
42
37
 
43
- RoomMembership.new(self, room_class, room_key, **kwargs)
38
+ RoomMembership.new(self, room_class, room_key, **kwargs, &blk)
44
39
  end
45
40
 
46
41
  def ping_room_memberships
@@ -61,7 +56,9 @@ module CableRoom
61
56
  on_closed: nil,
62
57
  on_opened: nil,
63
58
  on_message: nil,
64
- extra: nil
59
+ tags: [],
60
+ extra: nil,
61
+ &preconfigure
65
62
  )
66
63
  @token = SecureRandom.hex(16)
67
64
  @has_left = false
@@ -75,6 +72,7 @@ module CableRoom
75
72
  @on_closed = on_closed
76
73
  @on_message = on_message
77
74
 
75
+ @tags = Array(tags).map(&:to_sym)
78
76
  @extra = extra
79
77
 
80
78
  @cable_channel._room_memberships << self
@@ -89,7 +87,22 @@ module CableRoom
89
87
  handle_received_message(message)
90
88
  end
91
89
 
92
- transmit_member_joined
90
+ # Listen to user-specific channel
91
+ if extra && extra[:as]
92
+ stream_port(extra[:as]) do |message|
93
+ handle_received_message(message)
94
+ end
95
+ end
96
+
97
+ @tags.each do |tag|
98
+ stream_port(tag) do |message|
99
+ handle_received_message(message)
100
+ end
101
+ end
102
+
103
+ preconfigure&.call(self)
104
+
105
+ transmit_port_connected
93
106
 
94
107
  maybe_provision_room
95
108
  end
@@ -107,7 +120,7 @@ module CableRoom
107
120
  def ping!
108
121
  return if left?
109
122
 
110
- self << { type: 'member_ping' }
123
+ self << { type: 'port_ping' }
111
124
  maybe_provision_room
112
125
  end
113
126
 
@@ -116,7 +129,7 @@ module CableRoom
116
129
 
117
130
  close_streamed_ports!
118
131
  @cable_channel._room_memberships.delete(self)
119
- self << { type: 'member_left' }
132
+ self << { type: 'port_disconnected' }
120
133
  @has_left = true
121
134
  @on_closed&.call(self)
122
135
  end
@@ -139,7 +152,7 @@ module CableRoom
139
152
 
140
153
  case message['type']
141
154
  when 'room_opened'
142
- transmit_member_joined
155
+ transmit_port_connected
143
156
  @on_opened&.call(self)
144
157
  when 'room_closed'
145
158
  @on_closed&.call(self)
@@ -153,8 +166,11 @@ module CableRoom
153
166
 
154
167
  private
155
168
 
156
- def transmit_member_joined
157
- msg = { type: 'member_joined' }
169
+ def transmit_port_connected
170
+ msg = {
171
+ type: 'port_connected',
172
+ tags: @tags,
173
+ }
158
174
  msg[:extra] = ::ActiveJob::Arguments.serialize([@extra]) if @extra
159
175
  self << msg
160
176
  end
@@ -1,3 +1,3 @@
1
1
  module CableRoom
2
- VERSION = "0.1.2".freeze
2
+ VERSION = "0.2.0".freeze
3
3
  end