faye-redis-ng 1.0.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 +7 -0
- data/CHANGELOG.md +41 -0
- data/LICENSE +21 -0
- data/README.md +271 -0
- data/lib/faye/redis/client_registry.rb +155 -0
- data/lib/faye/redis/connection.rb +107 -0
- data/lib/faye/redis/logger.rb +39 -0
- data/lib/faye/redis/message_queue.rb +207 -0
- data/lib/faye/redis/pubsub_coordinator.rb +207 -0
- data/lib/faye/redis/subscription_manager.rb +189 -0
- data/lib/faye/redis/version.rb +5 -0
- data/lib/faye/redis.rb +154 -0
- data/lib/faye-redis-ng.rb +5 -0
- metadata +128 -0
@@ -0,0 +1,207 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'securerandom'
|
3
|
+
|
4
|
+
module Faye
|
5
|
+
class Redis
|
6
|
+
class MessageQueue
|
7
|
+
attr_reader :connection, :options
|
8
|
+
|
9
|
+
def initialize(connection, options = {})
|
10
|
+
@connection = connection
|
11
|
+
@options = options
|
12
|
+
end
|
13
|
+
|
14
|
+
# Enqueue a message for a client
|
15
|
+
def enqueue(client_id, message, &callback)
|
16
|
+
message_id = generate_message_id
|
17
|
+
timestamp = Time.now.to_i
|
18
|
+
|
19
|
+
message_data = {
|
20
|
+
id: message_id,
|
21
|
+
channel: message['channel'],
|
22
|
+
data: message['data'],
|
23
|
+
client_id: message['clientId'],
|
24
|
+
timestamp: timestamp
|
25
|
+
}
|
26
|
+
|
27
|
+
@connection.with_redis do |redis|
|
28
|
+
redis.multi do |multi|
|
29
|
+
# Store message data
|
30
|
+
multi.hset(message_key(message_id), message_data.transform_keys(&:to_s).transform_values { |v| v.to_json })
|
31
|
+
|
32
|
+
# Add message to client's queue
|
33
|
+
multi.rpush(queue_key(client_id), message_id)
|
34
|
+
|
35
|
+
# Set TTL on message
|
36
|
+
multi.expire(message_key(message_id), message_ttl)
|
37
|
+
|
38
|
+
# Set TTL on queue
|
39
|
+
multi.expire(queue_key(client_id), message_ttl)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
EventMachine.next_tick { callback.call(true) } if callback
|
44
|
+
rescue => e
|
45
|
+
log_error("Failed to enqueue message for client #{client_id}: #{e.message}")
|
46
|
+
EventMachine.next_tick { callback.call(false) } if callback
|
47
|
+
end
|
48
|
+
|
49
|
+
# Dequeue all messages for a client
|
50
|
+
def dequeue_all(client_id, &callback)
|
51
|
+
# Get all message IDs from queue
|
52
|
+
message_ids = @connection.with_redis do |redis|
|
53
|
+
redis.lrange(queue_key(client_id), 0, -1)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Fetch all messages using pipeline
|
57
|
+
messages = []
|
58
|
+
unless message_ids.empty?
|
59
|
+
@connection.with_redis do |redis|
|
60
|
+
redis.pipelined do |pipeline|
|
61
|
+
message_ids.each do |message_id|
|
62
|
+
pipeline.hgetall(message_key(message_id))
|
63
|
+
end
|
64
|
+
end.each do |data|
|
65
|
+
next if data.nil? || data.empty?
|
66
|
+
|
67
|
+
# Parse JSON values
|
68
|
+
parsed_data = data.transform_values do |v|
|
69
|
+
begin
|
70
|
+
JSON.parse(v)
|
71
|
+
rescue JSON::ParserError
|
72
|
+
v
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Convert to Faye message format
|
77
|
+
messages << {
|
78
|
+
'channel' => parsed_data['channel'],
|
79
|
+
'data' => parsed_data['data'],
|
80
|
+
'clientId' => parsed_data['client_id'],
|
81
|
+
'id' => parsed_data['id']
|
82
|
+
}
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Delete queue and all message data using pipeline
|
88
|
+
unless message_ids.empty?
|
89
|
+
@connection.with_redis do |redis|
|
90
|
+
redis.pipelined do |pipeline|
|
91
|
+
pipeline.del(queue_key(client_id))
|
92
|
+
message_ids.each do |message_id|
|
93
|
+
pipeline.del(message_key(message_id))
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
EventMachine.next_tick { callback.call(messages) } if callback
|
100
|
+
messages
|
101
|
+
rescue => e
|
102
|
+
log_error("Failed to dequeue messages for client #{client_id}: #{e.message}")
|
103
|
+
EventMachine.next_tick { callback.call([]) } if callback
|
104
|
+
[]
|
105
|
+
end
|
106
|
+
|
107
|
+
# Peek at messages without removing them
|
108
|
+
def peek(client_id, limit = 10, &callback)
|
109
|
+
message_ids = @connection.with_redis do |redis|
|
110
|
+
redis.lrange(queue_key(client_id), 0, limit - 1)
|
111
|
+
end
|
112
|
+
|
113
|
+
messages = message_ids.map do |message_id|
|
114
|
+
fetch_message(message_id)
|
115
|
+
end.compact
|
116
|
+
|
117
|
+
EventMachine.next_tick { callback.call(messages) } if callback
|
118
|
+
messages
|
119
|
+
rescue => e
|
120
|
+
log_error("Failed to peek messages for client #{client_id}: #{e.message}")
|
121
|
+
EventMachine.next_tick { callback.call([]) } if callback
|
122
|
+
[]
|
123
|
+
end
|
124
|
+
|
125
|
+
# Get queue size for a client
|
126
|
+
def size(client_id, &callback)
|
127
|
+
queue_size = @connection.with_redis do |redis|
|
128
|
+
redis.llen(queue_key(client_id))
|
129
|
+
end
|
130
|
+
|
131
|
+
EventMachine.next_tick { callback.call(queue_size) } if callback
|
132
|
+
queue_size
|
133
|
+
rescue => e
|
134
|
+
log_error("Failed to get queue size for client #{client_id}: #{e.message}")
|
135
|
+
EventMachine.next_tick { callback.call(0) } if callback
|
136
|
+
0
|
137
|
+
end
|
138
|
+
|
139
|
+
# Clear a client's message queue
|
140
|
+
def clear(client_id, &callback)
|
141
|
+
@connection.with_redis do |redis|
|
142
|
+
redis.del(queue_key(client_id))
|
143
|
+
end
|
144
|
+
|
145
|
+
EventMachine.next_tick { callback.call(true) } if callback
|
146
|
+
rescue => e
|
147
|
+
log_error("Failed to clear queue for client #{client_id}: #{e.message}")
|
148
|
+
EventMachine.next_tick { callback.call(false) } if callback
|
149
|
+
end
|
150
|
+
|
151
|
+
private
|
152
|
+
|
153
|
+
def fetch_message(message_id)
|
154
|
+
data = @connection.with_redis do |redis|
|
155
|
+
redis.hgetall(message_key(message_id))
|
156
|
+
end
|
157
|
+
|
158
|
+
return nil if data.empty?
|
159
|
+
|
160
|
+
# Parse JSON values
|
161
|
+
parsed_data = data.transform_values do |v|
|
162
|
+
begin
|
163
|
+
JSON.parse(v)
|
164
|
+
rescue JSON::ParserError
|
165
|
+
v
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# Convert to Faye message format
|
170
|
+
{
|
171
|
+
'channel' => parsed_data['channel'],
|
172
|
+
'data' => parsed_data['data'],
|
173
|
+
'clientId' => parsed_data['client_id'],
|
174
|
+
'id' => parsed_data['id']
|
175
|
+
}
|
176
|
+
rescue => e
|
177
|
+
log_error("Failed to fetch message #{message_id}: #{e.message}")
|
178
|
+
nil
|
179
|
+
end
|
180
|
+
|
181
|
+
def queue_key(client_id)
|
182
|
+
namespace_key("messages:#{client_id}")
|
183
|
+
end
|
184
|
+
|
185
|
+
def message_key(message_id)
|
186
|
+
namespace_key("message:#{message_id}")
|
187
|
+
end
|
188
|
+
|
189
|
+
def namespace_key(key)
|
190
|
+
namespace = @options[:namespace] || 'faye'
|
191
|
+
"#{namespace}:#{key}"
|
192
|
+
end
|
193
|
+
|
194
|
+
def message_ttl
|
195
|
+
@options[:message_ttl] || 3600
|
196
|
+
end
|
197
|
+
|
198
|
+
def generate_message_id
|
199
|
+
SecureRandom.uuid
|
200
|
+
end
|
201
|
+
|
202
|
+
def log_error(message)
|
203
|
+
puts "[Faye::Redis::MessageQueue] ERROR: #{message}" if @options[:log_level] != :silent
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
@@ -0,0 +1,207 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
module Faye
|
5
|
+
class Redis
|
6
|
+
class PubSubCoordinator
|
7
|
+
attr_reader :connection, :options
|
8
|
+
|
9
|
+
def initialize(connection, options = {})
|
10
|
+
@connection = connection
|
11
|
+
@options = options
|
12
|
+
@subscribers = []
|
13
|
+
@redis_subscriber = nil
|
14
|
+
@subscribed_channels = Set.new
|
15
|
+
@subscriber_thread = nil
|
16
|
+
@stop_subscriber = false
|
17
|
+
@reconnect_attempts = 0
|
18
|
+
# Don't setup subscriber immediately - wait until first publish/subscribe
|
19
|
+
end
|
20
|
+
|
21
|
+
# Publish a message to a channel
|
22
|
+
def publish(channel, message, &callback)
|
23
|
+
# Ensure subscriber is setup
|
24
|
+
setup_subscriber unless @subscriber_thread
|
25
|
+
|
26
|
+
message_json = message.to_json
|
27
|
+
|
28
|
+
@connection.with_redis do |redis|
|
29
|
+
redis.publish(pubsub_channel(channel), message_json)
|
30
|
+
end
|
31
|
+
|
32
|
+
EventMachine.next_tick { callback.call(true) } if callback
|
33
|
+
rescue => e
|
34
|
+
log_error("Failed to publish message to #{channel}: #{e.message}")
|
35
|
+
EventMachine.next_tick { callback.call(false) } if callback
|
36
|
+
end
|
37
|
+
|
38
|
+
# Subscribe to messages from other servers
|
39
|
+
def on_message(&block)
|
40
|
+
@subscribers << block
|
41
|
+
end
|
42
|
+
|
43
|
+
# Subscribe to a Redis pub/sub channel
|
44
|
+
def subscribe_to_channel(channel)
|
45
|
+
return if @subscribed_channels.include?(channel)
|
46
|
+
|
47
|
+
@subscribed_channels.add(channel)
|
48
|
+
|
49
|
+
if @redis_subscriber
|
50
|
+
@redis_subscriber.subscribe(pubsub_channel(channel))
|
51
|
+
end
|
52
|
+
rescue => e
|
53
|
+
log_error("Failed to subscribe to channel #{channel}: #{e.message}")
|
54
|
+
end
|
55
|
+
|
56
|
+
# Unsubscribe from a Redis pub/sub channel
|
57
|
+
def unsubscribe_from_channel(channel)
|
58
|
+
return unless @subscribed_channels.include?(channel)
|
59
|
+
|
60
|
+
@subscribed_channels.delete(channel)
|
61
|
+
|
62
|
+
if @redis_subscriber
|
63
|
+
@redis_subscriber.unsubscribe(pubsub_channel(channel))
|
64
|
+
end
|
65
|
+
rescue => e
|
66
|
+
log_error("Failed to unsubscribe from channel #{channel}: #{e.message}")
|
67
|
+
end
|
68
|
+
|
69
|
+
# Disconnect the pub/sub connection
|
70
|
+
def disconnect
|
71
|
+
@stop_subscriber = true
|
72
|
+
|
73
|
+
if @subscriber_thread
|
74
|
+
@subscriber_thread.kill
|
75
|
+
@subscriber_thread = nil
|
76
|
+
end
|
77
|
+
|
78
|
+
if @redis_subscriber
|
79
|
+
begin
|
80
|
+
@redis_subscriber.quit
|
81
|
+
rescue => e
|
82
|
+
# Ignore errors during disconnect
|
83
|
+
end
|
84
|
+
@redis_subscriber = nil
|
85
|
+
end
|
86
|
+
@subscribed_channels.clear
|
87
|
+
@subscribers.clear
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def setup_subscriber
|
93
|
+
return if @subscriber_thread&.alive?
|
94
|
+
|
95
|
+
@stop_subscriber = false
|
96
|
+
@subscriber_thread = Thread.new do
|
97
|
+
run_subscriber_loop
|
98
|
+
end
|
99
|
+
rescue => e
|
100
|
+
log_error("Failed to setup pub/sub subscriber: #{e.message}")
|
101
|
+
end
|
102
|
+
|
103
|
+
def run_subscriber_loop
|
104
|
+
max_reconnect_attempts = @options[:pubsub_max_reconnect_attempts] || 10
|
105
|
+
base_delay = @options[:pubsub_reconnect_delay] || 1
|
106
|
+
|
107
|
+
loop do
|
108
|
+
break if @stop_subscriber
|
109
|
+
|
110
|
+
begin
|
111
|
+
# Create a dedicated Redis connection for pub/sub
|
112
|
+
@redis_subscriber = @connection.create_pubsub_connection
|
113
|
+
|
114
|
+
# Subscribe to a wildcard pattern to catch all Faye messages
|
115
|
+
pattern = pubsub_channel('*')
|
116
|
+
|
117
|
+
log_info("Starting pub/sub subscriber (attempt #{@reconnect_attempts + 1})")
|
118
|
+
|
119
|
+
@redis_subscriber.psubscribe(pattern) do |on|
|
120
|
+
on.pmessage do |pattern_match, channel, message|
|
121
|
+
handle_message(channel, message)
|
122
|
+
end
|
123
|
+
|
124
|
+
on.psubscribe do |channel, subscriptions|
|
125
|
+
log_info("Subscribed to pattern: #{channel}")
|
126
|
+
@reconnect_attempts = 0 # Reset on successful subscription
|
127
|
+
end
|
128
|
+
|
129
|
+
on.punsubscribe do |channel, subscriptions|
|
130
|
+
log_info("Unsubscribed from pattern: #{channel}")
|
131
|
+
end
|
132
|
+
end
|
133
|
+
rescue => e
|
134
|
+
break if @stop_subscriber
|
135
|
+
|
136
|
+
@reconnect_attempts += 1
|
137
|
+
log_error("Pub/sub subscriber error: #{e.message} (attempt #{@reconnect_attempts})")
|
138
|
+
|
139
|
+
# Clean up failed connection
|
140
|
+
begin
|
141
|
+
@redis_subscriber&.quit
|
142
|
+
rescue
|
143
|
+
# Ignore cleanup errors
|
144
|
+
end
|
145
|
+
@redis_subscriber = nil
|
146
|
+
|
147
|
+
if @reconnect_attempts >= max_reconnect_attempts
|
148
|
+
log_error("Max reconnect attempts (#{max_reconnect_attempts}) reached, stopping pub/sub subscriber")
|
149
|
+
break
|
150
|
+
end
|
151
|
+
|
152
|
+
# Exponential backoff with jitter
|
153
|
+
delay = [base_delay * (2 ** (@reconnect_attempts - 1)), 60].min
|
154
|
+
jitter = rand * 0.3 * delay
|
155
|
+
sleep(delay + jitter)
|
156
|
+
|
157
|
+
log_info("Attempting to reconnect pub/sub subscriber...")
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def handle_message(redis_channel, message_json)
|
163
|
+
# Extract the Faye channel from the Redis channel
|
164
|
+
channel = extract_channel(redis_channel)
|
165
|
+
|
166
|
+
begin
|
167
|
+
message = JSON.parse(message_json)
|
168
|
+
|
169
|
+
# Notify all subscribers
|
170
|
+
EventMachine.next_tick do
|
171
|
+
@subscribers.each do |subscriber|
|
172
|
+
subscriber.call(channel, message)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
rescue JSON::ParserError => e
|
176
|
+
log_error("Failed to parse message from #{channel}: #{e.message}")
|
177
|
+
end
|
178
|
+
rescue => e
|
179
|
+
log_error("Failed to handle message from #{redis_channel}: #{e.message}")
|
180
|
+
end
|
181
|
+
|
182
|
+
def pubsub_channel(channel)
|
183
|
+
namespace_key("publish:#{channel}")
|
184
|
+
end
|
185
|
+
|
186
|
+
def extract_channel(redis_channel)
|
187
|
+
# Remove the namespace prefix and 'publish:' prefix
|
188
|
+
namespace = @options[:namespace] || 'faye'
|
189
|
+
prefix = "#{namespace}:publish:"
|
190
|
+
redis_channel.sub(/^#{Regexp.escape(prefix)}/, '')
|
191
|
+
end
|
192
|
+
|
193
|
+
def namespace_key(key)
|
194
|
+
namespace = @options[:namespace] || 'faye'
|
195
|
+
"#{namespace}:#{key}"
|
196
|
+
end
|
197
|
+
|
198
|
+
def log_error(message)
|
199
|
+
puts "[Faye::Redis::PubSubCoordinator] ERROR: #{message}" if @options[:log_level] != :silent
|
200
|
+
end
|
201
|
+
|
202
|
+
def log_info(message)
|
203
|
+
puts "[Faye::Redis::PubSubCoordinator] INFO: #{message}" if @options[:log_level] == :debug
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
@@ -0,0 +1,189 @@
|
|
1
|
+
module Faye
|
2
|
+
class Redis
|
3
|
+
class SubscriptionManager
|
4
|
+
attr_reader :connection, :options
|
5
|
+
|
6
|
+
def initialize(connection, options = {})
|
7
|
+
@connection = connection
|
8
|
+
@options = options
|
9
|
+
end
|
10
|
+
|
11
|
+
# Subscribe a client to a channel
|
12
|
+
def subscribe(client_id, channel, &callback)
|
13
|
+
timestamp = Time.now.to_i
|
14
|
+
|
15
|
+
@connection.with_redis do |redis|
|
16
|
+
redis.multi do |multi|
|
17
|
+
# Add channel to client's subscriptions
|
18
|
+
multi.sadd(client_subscriptions_key(client_id), channel)
|
19
|
+
|
20
|
+
# Add client to channel's subscribers
|
21
|
+
multi.sadd(channel_subscribers_key(channel), client_id)
|
22
|
+
|
23
|
+
# Store subscription metadata
|
24
|
+
multi.hset(
|
25
|
+
subscription_key(client_id, channel),
|
26
|
+
'subscribed_at', timestamp,
|
27
|
+
'channel', channel,
|
28
|
+
'client_id', client_id
|
29
|
+
)
|
30
|
+
|
31
|
+
# Handle wildcard patterns
|
32
|
+
if channel.include?('*')
|
33
|
+
multi.sadd(patterns_key, channel)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
EventMachine.next_tick { callback.call(true) } if callback
|
39
|
+
rescue => e
|
40
|
+
log_error("Failed to subscribe client #{client_id} to #{channel}: #{e.message}")
|
41
|
+
EventMachine.next_tick { callback.call(false) } if callback
|
42
|
+
end
|
43
|
+
|
44
|
+
# Unsubscribe a client from a channel
|
45
|
+
def unsubscribe(client_id, channel, &callback)
|
46
|
+
@connection.with_redis do |redis|
|
47
|
+
redis.multi do |multi|
|
48
|
+
# Remove channel from client's subscriptions
|
49
|
+
multi.srem(client_subscriptions_key(client_id), channel)
|
50
|
+
|
51
|
+
# Remove client from channel's subscribers
|
52
|
+
multi.srem(channel_subscribers_key(channel), client_id)
|
53
|
+
|
54
|
+
# Delete subscription metadata
|
55
|
+
multi.del(subscription_key(client_id, channel))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
EventMachine.next_tick { callback.call(true) } if callback
|
60
|
+
rescue => e
|
61
|
+
log_error("Failed to unsubscribe client #{client_id} from #{channel}: #{e.message}")
|
62
|
+
EventMachine.next_tick { callback.call(false) } if callback
|
63
|
+
end
|
64
|
+
|
65
|
+
# Unsubscribe a client from all channels
|
66
|
+
def unsubscribe_all(client_id, &callback)
|
67
|
+
# Get all channels the client is subscribed to
|
68
|
+
get_client_subscriptions(client_id) do |channels|
|
69
|
+
if channels.empty?
|
70
|
+
callback.call(true) if callback
|
71
|
+
else
|
72
|
+
# Unsubscribe from each channel
|
73
|
+
remaining = channels.size
|
74
|
+
channels.each do |channel|
|
75
|
+
unsubscribe(client_id, channel) do
|
76
|
+
remaining -= 1
|
77
|
+
callback.call(true) if callback && remaining == 0
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
rescue => e
|
83
|
+
log_error("Failed to unsubscribe client #{client_id} from all channels: #{e.message}")
|
84
|
+
EventMachine.next_tick { callback.call(false) } if callback
|
85
|
+
end
|
86
|
+
|
87
|
+
# Get all channels a client is subscribed to
|
88
|
+
def get_client_subscriptions(client_id, &callback)
|
89
|
+
channels = @connection.with_redis do |redis|
|
90
|
+
redis.smembers(client_subscriptions_key(client_id))
|
91
|
+
end
|
92
|
+
|
93
|
+
EventMachine.next_tick { callback.call(channels) } if callback
|
94
|
+
channels
|
95
|
+
rescue => e
|
96
|
+
log_error("Failed to get subscriptions for client #{client_id}: #{e.message}")
|
97
|
+
EventMachine.next_tick { callback.call([]) } if callback
|
98
|
+
[]
|
99
|
+
end
|
100
|
+
|
101
|
+
# Get all clients subscribed to a channel
|
102
|
+
def get_subscribers(channel, &callback)
|
103
|
+
# Get direct subscribers
|
104
|
+
direct_subscribers = @connection.with_redis do |redis|
|
105
|
+
redis.smembers(channel_subscribers_key(channel))
|
106
|
+
end
|
107
|
+
|
108
|
+
# Get pattern subscribers
|
109
|
+
pattern_subscribers = get_pattern_subscribers(channel)
|
110
|
+
|
111
|
+
all_subscribers = (direct_subscribers + pattern_subscribers).uniq
|
112
|
+
|
113
|
+
EventMachine.next_tick { callback.call(all_subscribers) } if callback
|
114
|
+
all_subscribers
|
115
|
+
rescue => e
|
116
|
+
log_error("Failed to get subscribers for channel #{channel}: #{e.message}")
|
117
|
+
EventMachine.next_tick { callback.call([]) } if callback
|
118
|
+
[]
|
119
|
+
end
|
120
|
+
|
121
|
+
# Get subscribers matching wildcard patterns
|
122
|
+
def get_pattern_subscribers(channel)
|
123
|
+
patterns = @connection.with_redis do |redis|
|
124
|
+
redis.smembers(patterns_key)
|
125
|
+
end
|
126
|
+
|
127
|
+
matching_clients = []
|
128
|
+
patterns.each do |pattern|
|
129
|
+
if channel_matches_pattern?(channel, pattern)
|
130
|
+
clients = @connection.with_redis do |redis|
|
131
|
+
redis.smembers(channel_subscribers_key(pattern))
|
132
|
+
end
|
133
|
+
matching_clients.concat(clients)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
matching_clients.uniq
|
138
|
+
rescue => e
|
139
|
+
log_error("Failed to get pattern subscribers for channel #{channel}: #{e.message}")
|
140
|
+
[]
|
141
|
+
end
|
142
|
+
|
143
|
+
# Check if a channel matches a pattern
|
144
|
+
def channel_matches_pattern?(channel, pattern)
|
145
|
+
# Convert Faye wildcard pattern to regex
|
146
|
+
# * matches one segment, ** matches multiple segments
|
147
|
+
regex_pattern = pattern
|
148
|
+
.gsub('**', '__DOUBLE_STAR__')
|
149
|
+
.gsub('*', '[^/]+')
|
150
|
+
.gsub('__DOUBLE_STAR__', '.*')
|
151
|
+
|
152
|
+
regex = Regexp.new("^#{regex_pattern}$")
|
153
|
+
!!(channel =~ regex)
|
154
|
+
end
|
155
|
+
|
156
|
+
# Clean up subscriptions for a client
|
157
|
+
def cleanup_client_subscriptions(client_id)
|
158
|
+
unsubscribe_all(client_id)
|
159
|
+
end
|
160
|
+
|
161
|
+
private
|
162
|
+
|
163
|
+
def client_subscriptions_key(client_id)
|
164
|
+
namespace_key("subscriptions:#{client_id}")
|
165
|
+
end
|
166
|
+
|
167
|
+
def channel_subscribers_key(channel)
|
168
|
+
namespace_key("channels:#{channel}")
|
169
|
+
end
|
170
|
+
|
171
|
+
def subscription_key(client_id, channel)
|
172
|
+
namespace_key("subscription:#{client_id}:#{channel}")
|
173
|
+
end
|
174
|
+
|
175
|
+
def patterns_key
|
176
|
+
namespace_key('patterns')
|
177
|
+
end
|
178
|
+
|
179
|
+
def namespace_key(key)
|
180
|
+
namespace = @options[:namespace] || 'faye'
|
181
|
+
"#{namespace}:#{key}"
|
182
|
+
end
|
183
|
+
|
184
|
+
def log_error(message)
|
185
|
+
puts "[Faye::Redis::SubscriptionManager] ERROR: #{message}" if @options[:log_level] != :silent
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|