activematrix 0.0.1 → 0.0.2
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 +4 -4
- data/README.md +218 -51
- data/lib/active_matrix/agent_manager.rb +275 -0
- data/lib/active_matrix/agent_registry.rb +154 -0
- data/lib/active_matrix/bot/multi_instance_base.rb +189 -0
- data/lib/active_matrix/client.rb +5 -15
- data/lib/active_matrix/client_pool.rb +194 -0
- data/lib/active_matrix/event_router.rb +215 -0
- data/lib/active_matrix/memory/agent_memory.rb +128 -0
- data/lib/active_matrix/memory/base.rb +101 -0
- data/lib/active_matrix/memory/conversation_memory.rb +161 -0
- data/lib/active_matrix/memory/global_memory.rb +153 -0
- data/lib/active_matrix/memory.rb +28 -0
- data/lib/active_matrix/room.rb +131 -51
- data/lib/active_matrix/rooms/space.rb +1 -5
- data/lib/active_matrix/user.rb +10 -0
- data/lib/active_matrix/util/account_data_cache.rb +62 -24
- data/lib/active_matrix/util/cacheable.rb +73 -0
- data/lib/active_matrix/util/extensions.rb +4 -0
- data/lib/active_matrix/util/state_event_cache.rb +106 -31
- data/lib/active_matrix/version.rb +1 -1
- data/lib/active_matrix.rb +51 -3
- data/lib/generators/active_matrix/bot/bot_generator.rb +38 -0
- data/lib/generators/active_matrix/bot/templates/bot.rb.erb +111 -0
- data/lib/generators/active_matrix/bot/templates/bot_spec.rb.erb +68 -0
- data/lib/generators/active_matrix/install/install_generator.rb +44 -0
- data/lib/generators/active_matrix/install/templates/README +30 -0
- data/lib/generators/active_matrix/install/templates/active_matrix.rb +33 -0
- data/lib/generators/active_matrix/install/templates/agent_memory.rb +47 -0
- data/lib/generators/active_matrix/install/templates/conversation_context.rb +72 -0
- data/lib/generators/active_matrix/install/templates/create_agent_memories.rb +17 -0
- data/lib/generators/active_matrix/install/templates/create_conversation_contexts.rb +21 -0
- data/lib/generators/active_matrix/install/templates/create_global_memories.rb +20 -0
- data/lib/generators/active_matrix/install/templates/create_matrix_agents.rb +26 -0
- data/lib/generators/active_matrix/install/templates/global_memory.rb +70 -0
- data/lib/generators/active_matrix/install/templates/matrix_agent.rb +127 -0
- metadata +110 -4
- data/lib/active_matrix/util/rails_cache_adapter.rb +0 -37
- data/lib/active_matrix/util/tinycache.rb +0 -145
- data/lib/active_matrix/util/tinycache_adapter.rb +0 -87
@@ -0,0 +1,154 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'singleton'
|
4
|
+
require 'concurrent'
|
5
|
+
|
6
|
+
module ActiveMatrix
|
7
|
+
# Thread-safe registry for managing running bot agents
|
8
|
+
class AgentRegistry
|
9
|
+
include Singleton
|
10
|
+
include ActiveMatrix::Logging
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@agents = Concurrent::Hash.new
|
14
|
+
@mutex = Mutex.new
|
15
|
+
end
|
16
|
+
|
17
|
+
# Register a running agent
|
18
|
+
def register(agent_record, bot_instance)
|
19
|
+
@mutex.synchronize do
|
20
|
+
raise AgentAlreadyRunningError, "Agent #{agent_record.name} is already running" if @agents.key?(agent_record.id)
|
21
|
+
|
22
|
+
@agents[agent_record.id] = {
|
23
|
+
record: agent_record,
|
24
|
+
instance: bot_instance,
|
25
|
+
thread: Thread.current,
|
26
|
+
started_at: Time.current
|
27
|
+
}
|
28
|
+
|
29
|
+
logger.info "Registered agent: #{agent_record.name} (#{agent_record.id})"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Unregister an agent
|
34
|
+
def unregister(agent_record)
|
35
|
+
@mutex.synchronize do
|
36
|
+
entry = @agents.delete(agent_record.id)
|
37
|
+
logger.info "Unregistered agent: #{agent_record.name} (#{agent_record.id})" if entry
|
38
|
+
entry
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Get a running agent by ID
|
43
|
+
def get(agent_id)
|
44
|
+
@agents[agent_id]
|
45
|
+
end
|
46
|
+
|
47
|
+
# Get agent by name
|
48
|
+
def get_by_name(name)
|
49
|
+
@agents.values.find { |entry| entry[:record].name == name }
|
50
|
+
end
|
51
|
+
|
52
|
+
# Get all running agents
|
53
|
+
def all
|
54
|
+
@agents.values
|
55
|
+
end
|
56
|
+
|
57
|
+
# Get all agent records
|
58
|
+
def all_records
|
59
|
+
@agents.values.map { |entry| entry[:record] }
|
60
|
+
end
|
61
|
+
|
62
|
+
# Get all bot instances
|
63
|
+
def all_instances
|
64
|
+
@agents.values.map { |entry| entry[:instance] }
|
65
|
+
end
|
66
|
+
|
67
|
+
# Check if an agent is running
|
68
|
+
def running?(agent_record)
|
69
|
+
@agents.key?(agent_record.id)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Get agents by state
|
73
|
+
def by_state(state)
|
74
|
+
@agents.values.select { |entry| entry[:record].state == state.to_s }
|
75
|
+
end
|
76
|
+
|
77
|
+
# Get agents by bot class
|
78
|
+
def by_class(bot_class)
|
79
|
+
class_name = bot_class.is_a?(Class) ? bot_class.name : bot_class.to_s
|
80
|
+
@agents.values.select { |entry| entry[:record].bot_class == class_name }
|
81
|
+
end
|
82
|
+
|
83
|
+
# Get agents by homeserver
|
84
|
+
def by_homeserver(homeserver)
|
85
|
+
@agents.values.select { |entry| entry[:record].homeserver == homeserver }
|
86
|
+
end
|
87
|
+
|
88
|
+
# Broadcast to all agents
|
89
|
+
def broadcast
|
90
|
+
all_instances.each do |instance|
|
91
|
+
yield instance
|
92
|
+
rescue StandardError => e
|
93
|
+
logger.error "Error broadcasting to agent: #{e.message}"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Broadcast to specific agents
|
98
|
+
def broadcast_to(selector)
|
99
|
+
agents = case selector
|
100
|
+
when Symbol
|
101
|
+
by_state(selector)
|
102
|
+
when String
|
103
|
+
by_name_pattern(selector)
|
104
|
+
when Class
|
105
|
+
by_class(selector)
|
106
|
+
when Proc
|
107
|
+
@agents.values.select { |entry| selector.call(entry[:record]) }
|
108
|
+
else
|
109
|
+
[]
|
110
|
+
end
|
111
|
+
|
112
|
+
agents.each do |entry|
|
113
|
+
yield entry[:instance]
|
114
|
+
rescue StandardError => e
|
115
|
+
logger.error "Error broadcasting to agent #{entry[:record].name}: #{e.message}"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Get count of running agents
|
120
|
+
def count
|
121
|
+
@agents.size
|
122
|
+
end
|
123
|
+
|
124
|
+
# Clear all agents (used for testing)
|
125
|
+
def clear!
|
126
|
+
@mutex.synchronize do
|
127
|
+
@agents.clear
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Get health status of all agents
|
132
|
+
def health_status
|
133
|
+
@agents.map do |id, entry|
|
134
|
+
{
|
135
|
+
id: id,
|
136
|
+
name: entry[:record].name,
|
137
|
+
state: entry[:record].state,
|
138
|
+
thread_alive: entry[:thread]&.alive?,
|
139
|
+
uptime: Time.current - entry[:started_at],
|
140
|
+
last_active: entry[:record].last_active_at
|
141
|
+
}
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
private
|
146
|
+
|
147
|
+
def by_name_pattern(pattern)
|
148
|
+
regex = Regexp.new(pattern, Regexp::IGNORECASE)
|
149
|
+
@agents.values.select { |entry| entry[:record].name =~ regex }
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
class AgentAlreadyRunningError < StandardError; end
|
154
|
+
end
|
@@ -0,0 +1,189 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveMatrix::Bot
|
4
|
+
# Base class for multi-instance bot support
|
5
|
+
class MultiInstanceBase < Base
|
6
|
+
attr_reader :agent_record
|
7
|
+
|
8
|
+
def initialize(client_or_agent, **params)
|
9
|
+
# Handle both client and agent record initialization
|
10
|
+
if client_or_agent.respond_to?(:homeserver) && !client_or_agent.respond_to?(:client) # It's a client
|
11
|
+
super
|
12
|
+
@agent_record = params[:agent_record]
|
13
|
+
else # It's an agent record
|
14
|
+
@agent_record = client_or_agent
|
15
|
+
super(@agent_record.client, **params)
|
16
|
+
end
|
17
|
+
|
18
|
+
setup_agent_context if @agent_record
|
19
|
+
end
|
20
|
+
|
21
|
+
# Access agent-specific memory
|
22
|
+
def memory
|
23
|
+
@memory ||= ActiveMatrix::Memory.for_agent(@agent_record)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Access conversation memory
|
27
|
+
def conversation_memory(user_id = nil, room_id = nil)
|
28
|
+
user_id ||= event[:sender] if in_event?
|
29
|
+
room_id ||= event[:room_id] if in_event?
|
30
|
+
|
31
|
+
return nil unless user_id && room_id
|
32
|
+
|
33
|
+
@conversation_memories ||= {}
|
34
|
+
@conversation_memories["#{user_id}/#{room_id}"] ||=
|
35
|
+
ActiveMatrix::Memory.for_conversation(@agent_record, user_id, room_id)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Access global memory
|
39
|
+
def global_memory
|
40
|
+
ActiveMatrix::Memory.global
|
41
|
+
end
|
42
|
+
|
43
|
+
# Get current conversation context
|
44
|
+
def conversation_context
|
45
|
+
conversation_memory&.context || {}
|
46
|
+
end
|
47
|
+
|
48
|
+
# Update conversation context
|
49
|
+
def update_context(data)
|
50
|
+
conversation_memory&.update_context(data)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Remember something in conversation
|
54
|
+
def remember_in_conversation(key, &)
|
55
|
+
conversation_memory&.remember(key, &)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Agent state helpers
|
59
|
+
def agent_name
|
60
|
+
@agent_record&.name || settings.bot_name
|
61
|
+
end
|
62
|
+
|
63
|
+
def agent_state
|
64
|
+
@agent_record&.state || 'unknown'
|
65
|
+
end
|
66
|
+
|
67
|
+
def mark_busy!
|
68
|
+
@agent_record&.start_processing! if @agent_record&.may_start_processing?
|
69
|
+
end
|
70
|
+
|
71
|
+
def mark_idle!
|
72
|
+
@agent_record&.finish_processing! if @agent_record&.may_finish_processing?
|
73
|
+
end
|
74
|
+
|
75
|
+
# Inter-agent communication
|
76
|
+
def broadcast_to_agents(selector, data)
|
77
|
+
return unless defined?(AgentRegistry)
|
78
|
+
|
79
|
+
registry = AgentRegistry.instance
|
80
|
+
registry.broadcast_to(selector) do |agent_bot|
|
81
|
+
agent_bot.receive_broadcast(data, from: self)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def send_to_agent(agent_name, data)
|
86
|
+
return unless defined?(AgentRegistry)
|
87
|
+
|
88
|
+
registry = AgentRegistry.instance
|
89
|
+
entry = registry.get_by_name(agent_name)
|
90
|
+
|
91
|
+
if entry
|
92
|
+
entry[:instance].receive_message(data, from: self)
|
93
|
+
true
|
94
|
+
else
|
95
|
+
logger.warn "Agent #{agent_name} not found or not running"
|
96
|
+
false
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Receive inter-agent messages (override in subclasses)
|
101
|
+
def receive_message(data, from:)
|
102
|
+
logger.debug "Received message from #{from.agent_name}: #{data.inspect}"
|
103
|
+
end
|
104
|
+
|
105
|
+
def receive_broadcast(data, from:)
|
106
|
+
logger.debug "Received broadcast from #{from.agent_name}: #{data.inspect}"
|
107
|
+
end
|
108
|
+
|
109
|
+
# Override event handling to track conversation
|
110
|
+
def _handle_message(event)
|
111
|
+
# Mark as busy while processing
|
112
|
+
mark_busy!
|
113
|
+
|
114
|
+
# Track conversation if we have an agent record
|
115
|
+
conversation_memory(event[:sender], event[:room_id])&.add_message(event) if @agent_record && event[:type] == 'm.room.message'
|
116
|
+
|
117
|
+
super
|
118
|
+
ensure
|
119
|
+
# Mark as idle when done
|
120
|
+
mark_idle!
|
121
|
+
end
|
122
|
+
|
123
|
+
def _handle_event(event)
|
124
|
+
mark_busy!
|
125
|
+
super
|
126
|
+
ensure
|
127
|
+
mark_idle!
|
128
|
+
end
|
129
|
+
|
130
|
+
# Enhanced settings with agent-specific overrides
|
131
|
+
def settings
|
132
|
+
return self.class unless @agent_record
|
133
|
+
|
134
|
+
# Merge agent-specific settings with class settings
|
135
|
+
@settings ||= begin
|
136
|
+
base_settings = self.class.settings
|
137
|
+
agent_settings = @agent_record.settings || {}
|
138
|
+
|
139
|
+
# Create a settings proxy that checks agent settings first
|
140
|
+
SettingsProxy.new(base_settings, agent_settings)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
private
|
145
|
+
|
146
|
+
def setup_agent_context
|
147
|
+
# Set up agent-specific logging
|
148
|
+
if @agent_record
|
149
|
+
@logger = ActiveMatrix.logger.dup
|
150
|
+
@logger.progname = "[#{@agent_record.name}]"
|
151
|
+
end
|
152
|
+
|
153
|
+
# Load any agent-specific configuration
|
154
|
+
return unless @agent_record.settings['commands_disabled']
|
155
|
+
|
156
|
+
@agent_record.settings['commands_disabled'].each do |cmd|
|
157
|
+
# Temporarily disable commands for this instance
|
158
|
+
@disabled_commands ||= []
|
159
|
+
@disabled_commands << cmd
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# Settings proxy to merge class and instance settings
|
164
|
+
class SettingsProxy
|
165
|
+
def initialize(base_settings, agent_settings)
|
166
|
+
@base_settings = base_settings
|
167
|
+
@agent_settings = agent_settings
|
168
|
+
end
|
169
|
+
|
170
|
+
def method_missing(method, *, &)
|
171
|
+
method_name = method.to_s.gsub(/\?$/, '')
|
172
|
+
|
173
|
+
# Check agent settings first
|
174
|
+
if @agent_settings.key?(method_name)
|
175
|
+
value = @agent_settings[method_name]
|
176
|
+
return method.to_s.end_with?('?') ? !value.nil? : value
|
177
|
+
end
|
178
|
+
|
179
|
+
# Fall back to base settings
|
180
|
+
@base_settings.send(method, *, &)
|
181
|
+
end
|
182
|
+
|
183
|
+
def respond_to_missing?(method, include_private = false)
|
184
|
+
method_name = method.to_s.gsub(/\?$/, '')
|
185
|
+
@agent_settings.key?(method_name) || @base_settings.respond_to?(method, include_private)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
data/lib/active_matrix/client.rb
CHANGED
@@ -150,7 +150,7 @@ module ActiveMatrix
|
|
150
150
|
|
151
151
|
break if data[:next_batch].nil?
|
152
152
|
|
153
|
-
since = data
|
153
|
+
since = data[:next_batch]
|
154
154
|
end
|
155
155
|
|
156
156
|
rooms
|
@@ -617,10 +617,9 @@ module ActiveMatrix
|
|
617
617
|
|
618
618
|
def handle_sync_response(data)
|
619
619
|
data.dig(:account_data, :events)&.each do |account_data|
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
end
|
620
|
+
# Store the account data in cache
|
621
|
+
self.account_data.write(account_data[:type], account_data[:content]) if account_data[:type]
|
622
|
+
|
624
623
|
fire_account_data(MatrixEvent.new(self, account_data))
|
625
624
|
end
|
626
625
|
|
@@ -676,16 +675,7 @@ module ActiveMatrix
|
|
676
675
|
end
|
677
676
|
end
|
678
677
|
|
679
|
-
|
680
|
-
account_data.tinycache_adapter.cleanup if instance_variable_defined?(:@account_data) && @account_data
|
681
|
-
@rooms.each_value do |room|
|
682
|
-
# Clean up old cache data after every sync
|
683
|
-
# TODO Run this in a thread?
|
684
|
-
room.tinycache_adapter.cleanup
|
685
|
-
room.account_data.tinycache_adapter.cleanup
|
686
|
-
room.room_state.tinycache_adapter.cleanup
|
687
|
-
end
|
688
|
-
end
|
678
|
+
# Rails.cache handles its own cleanup/expiration
|
689
679
|
|
690
680
|
nil
|
691
681
|
end
|
@@ -0,0 +1,194 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'singleton'
|
4
|
+
require 'concurrent'
|
5
|
+
|
6
|
+
module ActiveMatrix
|
7
|
+
# Manages a pool of Matrix client connections for efficiency
|
8
|
+
class ClientPool
|
9
|
+
include Singleton
|
10
|
+
include ActiveMatrix::Logging
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@pools = Concurrent::Hash.new
|
14
|
+
@config = ActiveMatrix.config
|
15
|
+
@mutex = Mutex.new
|
16
|
+
end
|
17
|
+
|
18
|
+
# Get or create a client for a homeserver
|
19
|
+
def get_client(homeserver, **)
|
20
|
+
@mutex.synchronize do
|
21
|
+
pool = get_or_create_pool(homeserver)
|
22
|
+
pool.checkout(**)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Return a client to the pool
|
27
|
+
def checkin(client)
|
28
|
+
homeserver = client.homeserver
|
29
|
+
pool = @pools[homeserver]
|
30
|
+
pool&.checkin(client)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Get pool statistics
|
34
|
+
def stats
|
35
|
+
@pools.map do |homeserver, pool|
|
36
|
+
{
|
37
|
+
homeserver: homeserver,
|
38
|
+
size: pool.size,
|
39
|
+
available: pool.available_count,
|
40
|
+
in_use: pool.in_use_count
|
41
|
+
}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Clear all pools
|
46
|
+
def clear!
|
47
|
+
@mutex.synchronize do
|
48
|
+
@pools.each_value(&:clear!)
|
49
|
+
@pools.clear
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Shutdown all pools
|
54
|
+
def shutdown
|
55
|
+
clear!
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def get_or_create_pool(homeserver)
|
61
|
+
@pools[homeserver] ||= HomeserverPool.new(
|
62
|
+
homeserver,
|
63
|
+
max_size: @config&.max_clients_per_homeserver || 5,
|
64
|
+
timeout: @config&.client_idle_timeout || 5.minutes
|
65
|
+
)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Pool for a specific homeserver
|
69
|
+
class HomeserverPool
|
70
|
+
include ActiveMatrix::Logging
|
71
|
+
|
72
|
+
attr_reader :homeserver, :max_size, :timeout
|
73
|
+
|
74
|
+
def initialize(homeserver, max_size:, timeout:)
|
75
|
+
@homeserver = homeserver
|
76
|
+
@max_size = max_size
|
77
|
+
@timeout = timeout
|
78
|
+
@available = []
|
79
|
+
@in_use = {}
|
80
|
+
@mutex = Mutex.new
|
81
|
+
@condition = ConditionVariable.new
|
82
|
+
end
|
83
|
+
|
84
|
+
def checkout(**)
|
85
|
+
@mutex.synchronize do
|
86
|
+
# Try to find an available client
|
87
|
+
client = find_available_client
|
88
|
+
|
89
|
+
# Create new client if needed and pool not full
|
90
|
+
client = create_client(**) if client.nil? && @in_use.size < @max_size
|
91
|
+
|
92
|
+
# Wait for a client if pool is full
|
93
|
+
while client.nil?
|
94
|
+
@condition.wait(@mutex, 1)
|
95
|
+
client = find_available_client
|
96
|
+
end
|
97
|
+
|
98
|
+
# Mark as in use
|
99
|
+
@available.delete(client)
|
100
|
+
@in_use[client.object_id] = {
|
101
|
+
client: client,
|
102
|
+
checked_out_at: Time.current
|
103
|
+
}
|
104
|
+
|
105
|
+
client
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def checkin(client)
|
110
|
+
@mutex.synchronize do
|
111
|
+
entry = @in_use.delete(client.object_id)
|
112
|
+
return unless entry
|
113
|
+
|
114
|
+
# Add back to available pool if still valid
|
115
|
+
if client_valid?(client)
|
116
|
+
@available << client
|
117
|
+
else
|
118
|
+
# Client is no longer valid, don't return to pool
|
119
|
+
logger.debug "Discarding invalid client for #{@homeserver}"
|
120
|
+
end
|
121
|
+
|
122
|
+
# Signal waiting threads
|
123
|
+
@condition.signal
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def size
|
128
|
+
@mutex.synchronize { @available.size + @in_use.size }
|
129
|
+
end
|
130
|
+
|
131
|
+
def available_count
|
132
|
+
@mutex.synchronize { @available.size }
|
133
|
+
end
|
134
|
+
|
135
|
+
def in_use_count
|
136
|
+
@mutex.synchronize { @in_use.size }
|
137
|
+
end
|
138
|
+
|
139
|
+
def clear!
|
140
|
+
@mutex.synchronize do
|
141
|
+
# Stop all clients
|
142
|
+
(@available + @in_use.values.map { |e| e[:client] }).each do |client|
|
143
|
+
client.stop_listener_thread if client.listening?
|
144
|
+
client.logout if client.logged_in?
|
145
|
+
rescue StandardError => e
|
146
|
+
logger.error "Error cleaning up client: #{e.message}"
|
147
|
+
end
|
148
|
+
|
149
|
+
@available.clear
|
150
|
+
@in_use.clear
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
private
|
155
|
+
|
156
|
+
def find_available_client
|
157
|
+
# Remove any expired clients
|
158
|
+
@available.select! { |client| client_valid?(client) }
|
159
|
+
|
160
|
+
# Return first available
|
161
|
+
@available.first
|
162
|
+
end
|
163
|
+
|
164
|
+
def create_client(**)
|
165
|
+
logger.debug "Creating new client for #{@homeserver}"
|
166
|
+
|
167
|
+
ActiveMatrix::Client.new(
|
168
|
+
@homeserver,
|
169
|
+
client_cache: :some,
|
170
|
+
sync_filter_limit: 20,
|
171
|
+
**
|
172
|
+
)
|
173
|
+
end
|
174
|
+
|
175
|
+
def client_valid?(client)
|
176
|
+
# Check if client is still connected and responsive
|
177
|
+
return false unless client
|
178
|
+
|
179
|
+
# Could add more validation here
|
180
|
+
true
|
181
|
+
rescue StandardError
|
182
|
+
false
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# Monkey patch Client to support pooling
|
188
|
+
class Client
|
189
|
+
# Checkin this client back to the pool
|
190
|
+
def checkin_to_pool
|
191
|
+
ClientPool.instance.checkin(self) if defined?(ClientPool)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|