smart_message 0.0.10 → 0.0.12

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.
Files changed (169) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/deploy-github-pages.yml +38 -0
  3. data/.gitignore +5 -0
  4. data/CHANGELOG.md +30 -0
  5. data/Gemfile.lock +35 -4
  6. data/README.md +169 -71
  7. data/Rakefile +29 -4
  8. data/docs/assets/images/ddq_architecture.svg +130 -0
  9. data/docs/assets/images/dlq_architecture.svg +115 -0
  10. data/docs/assets/images/enhanced-dual-publishing.svg +136 -0
  11. data/docs/assets/images/enhanced-fluent-api.svg +149 -0
  12. data/docs/assets/images/enhanced-microservices-routing.svg +115 -0
  13. data/docs/assets/images/enhanced-pattern-matching.svg +107 -0
  14. data/docs/assets/images/fluent-api-demo.svg +59 -0
  15. data/docs/assets/images/performance-comparison.svg +161 -0
  16. data/docs/assets/images/redis-basic-architecture.svg +53 -0
  17. data/docs/assets/images/redis-enhanced-architecture.svg +88 -0
  18. data/docs/assets/images/redis-queue-architecture.svg +101 -0
  19. data/docs/assets/images/smart_message.jpg +0 -0
  20. data/docs/assets/images/smart_message_walking.jpg +0 -0
  21. data/docs/assets/images/smartmessage_architecture_overview.svg +173 -0
  22. data/docs/assets/images/transport-comparison-matrix.svg +171 -0
  23. data/docs/assets/javascripts/mathjax.js +17 -0
  24. data/docs/assets/stylesheets/extra.css +51 -0
  25. data/docs/{addressing.md → core-concepts/addressing.md} +5 -7
  26. data/docs/{architecture.md → core-concepts/architecture.md} +78 -138
  27. data/docs/{dispatcher.md → core-concepts/dispatcher.md} +21 -21
  28. data/docs/{message_filtering.md → core-concepts/message-filtering.md} +2 -3
  29. data/docs/{message_processing.md → core-concepts/message-processing.md} +17 -17
  30. data/docs/{troubleshooting.md → development/troubleshooting.md} +7 -7
  31. data/docs/{examples.md → getting-started/examples.md} +115 -89
  32. data/docs/{getting-started.md → getting-started/quick-start.md} +47 -18
  33. data/docs/guides/redis-queue-getting-started.md +697 -0
  34. data/docs/guides/redis-queue-patterns.md +889 -0
  35. data/docs/guides/redis-queue-production.md +1091 -0
  36. data/docs/index.md +64 -0
  37. data/docs/{dead_letter_queue.md → reference/dead-letter-queue.md} +2 -3
  38. data/docs/{logging.md → reference/logging.md} +1 -1
  39. data/docs/{message_deduplication.md → reference/message-deduplication.md} +1 -0
  40. data/docs/{proc_handlers_summary.md → reference/proc-handlers.md} +7 -6
  41. data/docs/{serializers.md → reference/serializers.md} +3 -5
  42. data/docs/{transports.md → reference/transports.md} +133 -11
  43. data/docs/transports/memory-transport.md +374 -0
  44. data/docs/transports/redis-enhanced-transport.md +524 -0
  45. data/docs/transports/redis-queue-transport.md +1304 -0
  46. data/docs/transports/redis-transport-comparison.md +496 -0
  47. data/docs/transports/redis-transport.md +509 -0
  48. data/examples/README.md +98 -5
  49. data/examples/city_scenario/911_emergency_call_flow.svg +99 -0
  50. data/examples/city_scenario/README.md +515 -0
  51. data/examples/city_scenario/ai_visitor_intelligence_flow.svg +108 -0
  52. data/examples/city_scenario/citizen.rb +195 -0
  53. data/examples/city_scenario/city_diagram.svg +125 -0
  54. data/examples/city_scenario/common/health_monitor.rb +80 -0
  55. data/examples/city_scenario/common/logger.rb +30 -0
  56. data/examples/city_scenario/emergency_dispatch_center.rb +270 -0
  57. data/examples/city_scenario/fire_department.rb +446 -0
  58. data/examples/city_scenario/fire_emergency_flow.svg +95 -0
  59. data/examples/city_scenario/health_department.rb +100 -0
  60. data/examples/city_scenario/health_monitoring_system.svg +130 -0
  61. data/examples/city_scenario/house.rb +244 -0
  62. data/examples/city_scenario/local_bank.rb +217 -0
  63. data/examples/city_scenario/messages/emergency_911_message.rb +81 -0
  64. data/examples/city_scenario/messages/emergency_resolved_message.rb +43 -0
  65. data/examples/city_scenario/messages/fire_dispatch_message.rb +43 -0
  66. data/examples/city_scenario/messages/fire_emergency_message.rb +45 -0
  67. data/examples/city_scenario/messages/health_check_message.rb +22 -0
  68. data/examples/city_scenario/messages/health_status_message.rb +35 -0
  69. data/examples/city_scenario/messages/police_dispatch_message.rb +46 -0
  70. data/examples/city_scenario/messages/silent_alarm_message.rb +38 -0
  71. data/examples/city_scenario/police_department.rb +316 -0
  72. data/examples/city_scenario/redis_monitor.rb +129 -0
  73. data/examples/city_scenario/redis_stats.rb +743 -0
  74. data/examples/city_scenario/room_for_improvement.md +240 -0
  75. data/examples/city_scenario/security_emergency_flow.svg +95 -0
  76. data/examples/city_scenario/service_internal_architecture.svg +154 -0
  77. data/examples/city_scenario/smart_message_ai_agent.rb +364 -0
  78. data/examples/city_scenario/start_demo.sh +236 -0
  79. data/examples/city_scenario/stop_demo.sh +106 -0
  80. data/examples/city_scenario/visitor.rb +631 -0
  81. data/examples/{10_message_deduplication.rb → memory/01_message_deduplication_demo.rb} +1 -1
  82. data/examples/{09_dead_letter_queue_demo.rb → memory/02_dead_letter_queue_demo.rb} +13 -40
  83. data/examples/{01_point_to_point_orders.rb → memory/03_point_to_point_orders.rb} +1 -1
  84. data/examples/{02_publish_subscribe_events.rb → memory/04_publish_subscribe_events.rb} +2 -2
  85. data/examples/{03_many_to_many_chat.rb → memory/05_many_to_many_chat.rb} +4 -4
  86. data/examples/{show_me.rb → memory/06_pretty_print_demo.rb} +1 -1
  87. data/examples/{05_proc_handlers.rb → memory/07_proc_handlers_demo.rb} +2 -2
  88. data/examples/{06_custom_logger_example.rb → memory/08_custom_logger_demo.rb} +17 -14
  89. data/examples/{07_error_handling_scenarios.rb → memory/09_error_handling_demo.rb} +4 -4
  90. data/examples/{08_entity_addressing_basic.rb → memory/10_entity_addressing_basic.rb} +8 -8
  91. data/examples/{08_entity_addressing_with_filtering.rb → memory/11_entity_addressing_with_filtering.rb} +6 -6
  92. data/examples/{09_regex_filtering_microservices.rb → memory/12_regex_filtering_microservices.rb} +2 -2
  93. data/examples/{10_header_block_configuration.rb → memory/13_header_block_configuration.rb} +6 -6
  94. data/examples/{11_global_configuration_example.rb → memory/14_global_configuration_demo.rb} +19 -8
  95. data/examples/{show_logger.rb → memory/15_logger_demo.rb} +1 -1
  96. data/examples/memory/README.md +163 -0
  97. data/examples/memory/memory_transport_architecture.svg +90 -0
  98. data/examples/memory/point_to_point_pattern.svg +94 -0
  99. data/examples/memory/publish_subscribe_pattern.svg +125 -0
  100. data/examples/{04_redis_smart_home_iot.rb → redis/01_smart_home_iot_demo.rb} +5 -5
  101. data/examples/redis/README.md +230 -0
  102. data/examples/redis/alert_system_flow.svg +127 -0
  103. data/examples/redis/dashboard_status_flow.svg +107 -0
  104. data/examples/redis/device_command_flow.svg +113 -0
  105. data/examples/redis/redis_transport_architecture.svg +115 -0
  106. data/examples/{smart_home_iot_dataflow.md → redis/smart_home_iot_dataflow.md} +4 -116
  107. data/examples/redis/smart_home_system_architecture.svg +133 -0
  108. data/examples/redis_enhanced/README.md +319 -0
  109. data/examples/redis_enhanced/enhanced_01_basic_patterns.rb +233 -0
  110. data/examples/redis_enhanced/enhanced_02_fluent_api.rb +331 -0
  111. data/examples/redis_enhanced/enhanced_03_dual_publishing.rb +281 -0
  112. data/examples/redis_enhanced/enhanced_04_advanced_routing.rb +419 -0
  113. data/examples/redis_queue/01_basic_messaging.rb +221 -0
  114. data/examples/redis_queue/01_comprehensive_examples.rb +508 -0
  115. data/examples/redis_queue/02_pattern_routing.rb +405 -0
  116. data/examples/redis_queue/03_fluent_api.rb +422 -0
  117. data/examples/redis_queue/04_load_balancing.rb +486 -0
  118. data/examples/redis_queue/05_microservices.rb +735 -0
  119. data/examples/redis_queue/06_emergency_alerts.rb +777 -0
  120. data/examples/redis_queue/07_queue_management.rb +587 -0
  121. data/examples/redis_queue/README.md +366 -0
  122. data/examples/redis_queue/enhanced_01_basic_patterns.rb +233 -0
  123. data/examples/redis_queue/enhanced_02_fluent_api.rb +331 -0
  124. data/examples/redis_queue/enhanced_03_dual_publishing.rb +281 -0
  125. data/examples/redis_queue/enhanced_04_advanced_routing.rb +419 -0
  126. data/examples/redis_queue/redis_queue_architecture.svg +148 -0
  127. data/ideas/README.md +41 -0
  128. data/ideas/agents.md +1001 -0
  129. data/ideas/database_transport.md +980 -0
  130. data/ideas/improvement.md +359 -0
  131. data/ideas/meshage.md +1788 -0
  132. data/ideas/message_discovery.md +178 -0
  133. data/ideas/message_schema.md +1381 -0
  134. data/lib/smart_message/.idea/.gitignore +8 -0
  135. data/lib/smart_message/.idea/markdown.xml +6 -0
  136. data/lib/smart_message/.idea/misc.xml +4 -0
  137. data/lib/smart_message/.idea/modules.xml +8 -0
  138. data/lib/smart_message/.idea/smart_message.iml +16 -0
  139. data/lib/smart_message/.idea/vcs.xml +6 -0
  140. data/lib/smart_message/addressing.rb +15 -0
  141. data/lib/smart_message/base.rb +0 -2
  142. data/lib/smart_message/configuration.rb +1 -1
  143. data/lib/smart_message/logger.rb +15 -4
  144. data/lib/smart_message/plugins.rb +5 -2
  145. data/lib/smart_message/serializer.rb +14 -0
  146. data/lib/smart_message/transport/redis_enhanced_transport.rb +399 -0
  147. data/lib/smart_message/transport/redis_queue_transport.rb +555 -0
  148. data/lib/smart_message/transport/registry.rb +1 -0
  149. data/lib/smart_message/transport.rb +34 -1
  150. data/lib/smart_message/version.rb +1 -1
  151. data/lib/smart_message.rb +5 -52
  152. data/mkdocs.yml +184 -0
  153. data/p2p_plan.md +326 -0
  154. data/p2p_roadmap.md +287 -0
  155. data/smart_message.gemspec +2 -0
  156. data/smart_message.svg +51 -0
  157. metadata +170 -44
  158. data/docs/README.md +0 -57
  159. data/examples/dead_letters.jsonl +0 -12
  160. data/examples/temp.txt +0 -94
  161. data/examples/tmux_chat/README.md +0 -283
  162. data/examples/tmux_chat/bot_agent.rb +0 -278
  163. data/examples/tmux_chat/human_agent.rb +0 -199
  164. data/examples/tmux_chat/room_monitor.rb +0 -160
  165. data/examples/tmux_chat/shared_chat_system.rb +0 -328
  166. data/examples/tmux_chat/start_chat_demo.sh +0 -190
  167. data/examples/tmux_chat/stop_chat_demo.sh +0 -22
  168. /data/docs/{properties.md → core-concepts/properties.md} +0 -0
  169. /data/docs/{ideas_to_think_about.md → development/ideas.md} +0 -0
@@ -0,0 +1,555 @@
1
+ # lib/smart_message/transport/redis_queue_transport_async.rb
2
+ # encoding: utf-8
3
+ # frozen_string_literal: true
4
+
5
+ require 'async'
6
+ require 'async/redis'
7
+ require 'json'
8
+
9
+ module SmartMessage
10
+ module Transport
11
+ # Redis Queue Transport - Async-powered routing with RabbitMQ-style patterns
12
+ # This transport provides intelligent routing using Redis Lists as queues with pattern matching
13
+ # Built on Ruby's Async framework for modern concurrency and testing
14
+ #
15
+ # Key Features:
16
+ # - Async/Fiber-based concurrency (thousands of subscriptions)
17
+ # - RabbitMQ-style topic exchange pattern matching (#.*.my_uuid)
18
+ # - Load balancing via consumer groups
19
+ # - Queue persistence using Redis Lists
20
+ # - FIFO message ordering
21
+ # - 10x faster than RabbitMQ with same routing intelligence
22
+ # - Test-friendly with proper async lifecycle management
23
+ #
24
+ # Usage:
25
+ # Async do
26
+ # transport = SmartMessage::Transport::RedisQueueTransport.new
27
+ # transport.subscribe_pattern("#.*.my_service") do |msg_class, data|
28
+ # puts "Processing: #{msg_class}"
29
+ # end
30
+ # end
31
+ class RedisQueueTransport < Base
32
+
33
+ DEFAULT_CONFIG = {
34
+ url: 'redis://localhost:6379',
35
+ db: 0,
36
+ exchange_name: 'smart_message',
37
+ queue_prefix: 'smart_message.queue',
38
+ routing_prefix: 'smart_message.routing',
39
+ consumer_timeout: 1, # 1 second blocking pop timeout
40
+ max_queue_size: 10000, # Max messages per queue (circular buffer)
41
+ cleanup_on_disconnect: true, # Remove queues on shutdown
42
+ reconnect_attempts: 5,
43
+ reconnect_delay: 1,
44
+ async_timeout: 30 # Global async task timeout
45
+ }.freeze
46
+
47
+ attr_reader :redis, :exchange_name, :active_queues, :consumer_tasks
48
+
49
+ def initialize(**options)
50
+ @active_queues = {} # queue_name => consumer_info
51
+ @consumer_tasks = {} # queue_name => async task
52
+ @routing_table = {} # pattern => [queue_names]
53
+ @shutdown = false
54
+ # Don't initialize @redis to nil here since super() calls configure which sets it
55
+ super(**options)
56
+ end
57
+
58
+ def configure
59
+ @exchange_name = @options[:exchange_name]
60
+
61
+ # Setup Redis connection synchronously for immediate availability
62
+ begin
63
+ redis_client = nil
64
+ Async do
65
+ endpoint = Async::Redis::Endpoint.parse(@options[:url], db: @options[:db])
66
+ redis_client = Async::Redis::Client.new(endpoint)
67
+
68
+ # Test connection
69
+ redis_client.call('PING')
70
+
71
+ # Initialize exchange metadata with local redis client
72
+ @redis = redis_client # Set instance variable
73
+ setup_exchange
74
+
75
+ logger.info { "[RedisQueue] Async transport configured with exchange: #{@exchange_name}" }
76
+ end.wait
77
+
78
+ # Ensure @redis is set outside the async block too
79
+ @redis = redis_client if redis_client
80
+ rescue => e
81
+ logger.error { "[RedisQueue] Failed to configure transport: #{e.message}" }
82
+ @redis = nil
83
+ raise
84
+ end
85
+ end
86
+
87
+ def default_options
88
+ DEFAULT_CONFIG
89
+ end
90
+
91
+ # Publish message with intelligent routing to matching queues (Async)
92
+ # @param message_class [String] The message class name
93
+ # @param serialized_message [String] The serialized message content
94
+ def do_publish(message_class, serialized_message)
95
+ async_task do
96
+ routing_info = extract_routing_info(serialized_message)
97
+ routing_key = build_enhanced_routing_key(message_class, routing_info)
98
+
99
+ # Find all queues that match this routing key (like RabbitMQ topic exchange)
100
+ matching_queues = find_matching_queues(routing_key)
101
+
102
+ if matching_queues.empty?
103
+ logger.debug { "[RedisQueue] No queues match routing key: #{routing_key}" }
104
+ next
105
+ end
106
+
107
+ # Create message envelope with metadata
108
+ message_envelope = {
109
+ routing_key: routing_key,
110
+ message_class: message_class.to_s,
111
+ data: serialized_message,
112
+ timestamp: Time.now.to_f,
113
+ headers: routing_info
114
+ }.to_json
115
+
116
+ # Publish to all matching queues atomically using async redis
117
+ published_count = 0
118
+
119
+ # Use pipelined operations for better performance
120
+ commands = []
121
+ matching_queues.each do |queue_name|
122
+ commands << [:lpush, queue_name, message_envelope]
123
+ commands << [:ltrim, queue_name, 0, @options[:max_queue_size] - 1]
124
+ published_count += 1
125
+ end
126
+
127
+ # Execute all commands in pipeline
128
+ @redis.pipelined(commands) if commands.any?
129
+
130
+ logger.debug { "[RedisQueue] Published to #{published_count} queues with key: #{routing_key}" }
131
+ rescue => e
132
+ logger.error { "[RedisQueue] Publish error: #{e.message}" }
133
+ raise
134
+ end
135
+ end
136
+
137
+ # Subscribe to messages using RabbitMQ-style pattern matching (Async)
138
+ # @param pattern [String] Routing pattern (e.g., "#.*.my_service", "order.#.*.*")
139
+ # @param process_method [String] Method identifier for processing
140
+ # @param filter_options [Hash] Additional filtering options
141
+ # @param block [Proc] Optional block for message processing
142
+ def subscribe_pattern(pattern, process_method = :process, filter_options = {}, &block)
143
+ queue_name = derive_queue_name(pattern, filter_options)
144
+
145
+ # Add pattern to routing table (no mutex needed with Fibers)
146
+ @routing_table[pattern] ||= []
147
+ @routing_table[pattern] << queue_name unless @routing_table[pattern].include?(queue_name)
148
+
149
+ # Store queue metadata
150
+ @active_queues[queue_name] = {
151
+ pattern: pattern,
152
+ process_method: process_method,
153
+ filter_options: filter_options,
154
+ created_at: Time.now,
155
+ block_handler: block
156
+ }
157
+
158
+ # Start async consumer task for this queue (unless in test mode)
159
+ start_queue_consumer(queue_name) unless @options[:test_mode]
160
+
161
+ logger.info { "[RedisQueue] Subscribed to pattern '#{pattern}' via queue '#{queue_name}'" }
162
+ end
163
+
164
+ # Subscribe to all messages sent to a specific recipient
165
+ # @param recipient_id [String] The recipient identifier
166
+ def subscribe_to_recipient(recipient_id, process_method = :process, &block)
167
+ pattern = "#.*.#{sanitize_for_routing_key(recipient_id)}"
168
+ subscribe_pattern(pattern, process_method, {}, &block)
169
+ end
170
+
171
+ # Subscribe to all messages from a specific sender
172
+ # @param sender_id [String] The sender identifier
173
+ def subscribe_from_sender(sender_id, process_method = :process, &block)
174
+ pattern = "#.#{sanitize_for_routing_key(sender_id)}.*"
175
+ subscribe_pattern(pattern, process_method, {}, &block)
176
+ end
177
+
178
+ # Subscribe to all messages of a specific type regardless of routing
179
+ # @param message_type [String] The message class name
180
+ def subscribe_to_type(message_type, process_method = :process, &block)
181
+ pattern = "*.#{message_type.to_s.gsub('::', '.').downcase}.*.*"
182
+ subscribe_pattern(pattern, process_method, {}, &block)
183
+ end
184
+
185
+ # Subscribe to all alert/emergency messages
186
+ def subscribe_to_alerts(process_method = :process, &block)
187
+ patterns = [
188
+ "emergency.#.*.*",
189
+ "#.alert.*.*",
190
+ "#.alarm.*.*",
191
+ "#.critical.*.*"
192
+ ]
193
+
194
+ patterns.each { |pattern| subscribe_pattern(pattern, process_method, {}, &block) }
195
+ end
196
+
197
+ # Subscribe to all broadcast messages
198
+ def subscribe_to_broadcasts(process_method = :process, &block)
199
+ pattern = "#.*.broadcast"
200
+ subscribe_pattern(pattern, process_method, {}, &block)
201
+ end
202
+
203
+ # Test-friendly disconnect method
204
+ def disconnect
205
+ @shutdown = true
206
+
207
+ # Stop all consumer tasks
208
+ @consumer_tasks.each do |queue_name, task|
209
+ task.stop if task&.running?
210
+ rescue => e
211
+ logger.debug { "[RedisQueue] Error stopping consumer task for #{queue_name}: #{e.message}" }
212
+ end
213
+
214
+ @consumer_tasks.clear
215
+
216
+ # Close Redis connection
217
+ async_task do
218
+ @redis&.close
219
+ @redis = nil
220
+ rescue => e
221
+ logger.debug { "[RedisQueue] Error closing Redis connection: #{e.message}" }
222
+ end
223
+ end
224
+
225
+ def connected?
226
+ return false unless @redis
227
+
228
+ # Test connection with async ping
229
+ begin
230
+ if Async::Task.current?
231
+ # Already in async context
232
+ return @redis.call('PING') == 'PONG'
233
+ else
234
+ # Need to create async context
235
+ result = Async do
236
+ @redis.call('PING') == 'PONG'
237
+ end.wait
238
+ return result
239
+ end
240
+ rescue => e
241
+ logger.debug { "[RedisQueue] Connection test failed: #{e.message}" }
242
+ return false
243
+ end
244
+ end
245
+
246
+ # Get statistics about all active queues
247
+ def queue_stats
248
+ return {} unless @redis
249
+
250
+ stats = {}
251
+
252
+ begin
253
+ Async do
254
+ @active_queues.each do |queue_name, queue_info|
255
+ length = @redis.llen(queue_name).to_i
256
+ stats[queue_name] = {
257
+ length: length,
258
+ pattern: queue_info[:pattern],
259
+ created_at: queue_info[:created_at],
260
+ consumers: @consumer_tasks.key?(queue_name) ? 1 : 0
261
+ }
262
+ end
263
+ end.wait
264
+ rescue => e
265
+ logger.error { "[RedisQueue] Error getting queue stats: #{e.message}" }
266
+ end
267
+
268
+ stats
269
+ end
270
+
271
+ # Get the current routing table
272
+ def routing_table
273
+ @routing_table.dup
274
+ end
275
+
276
+ # Fluent API builder for complex subscriptions
277
+ def where
278
+ RedisQueueSubscriptionBuilder.new(self)
279
+ end
280
+
281
+ private
282
+
283
+ # Ensure async operations run in proper context
284
+ def async_task(&block)
285
+ if Async::Task.current?
286
+ # Already in async context
287
+ yield
288
+ else
289
+ # Need to create async context
290
+ Async { yield }
291
+ end
292
+ end
293
+
294
+ # Start async consumer for a queue
295
+ def start_queue_consumer(queue_name)
296
+ return if @consumer_tasks.key?(queue_name)
297
+
298
+ @consumer_tasks[queue_name] = async_task do |task|
299
+ begin
300
+ consume_from_queue(queue_name)
301
+ rescue => e
302
+ logger.error { "[RedisQueue] Consumer task error for #{queue_name}: #{e.message}" }
303
+ ensure
304
+ @consumer_tasks.delete(queue_name)
305
+ end
306
+ end
307
+ end
308
+
309
+ # Async consumer loop for a specific queue
310
+ def consume_from_queue(queue_name)
311
+ queue_info = @active_queues[queue_name]
312
+ return unless queue_info
313
+
314
+ while !@shutdown
315
+ begin
316
+ # Use BRPOP for blocking read with timeout (cooperative blocking)
317
+ result = @redis.brpop(queue_name, timeout: @options[:consumer_timeout])
318
+
319
+ if result && result.length >= 2
320
+ _, message_envelope = result
321
+ process_queue_message(message_envelope, queue_info)
322
+ end
323
+
324
+ rescue => e
325
+ logger.error { "[RedisQueue] Redis connection error in consumer: #{e.message}" }
326
+ # Async will handle reconnection automatically
327
+ sleep(1) unless @shutdown
328
+ rescue => e
329
+ logger.error { "[RedisQueue] Consumer error for #{queue_name}: #{e.message}" }
330
+ sleep(1) unless @shutdown
331
+ end
332
+ end
333
+ end
334
+
335
+ # Process a message from the queue
336
+ def process_queue_message(message_envelope, queue_info)
337
+ begin
338
+ message_data = JSON.parse(message_envelope)
339
+
340
+ message_class = message_data['message_class']
341
+ payload = message_data['data']
342
+ headers = message_data['headers'] || {}
343
+
344
+ # Apply additional filtering if specified
345
+ if should_process_message?(headers, queue_info[:filter_options])
346
+ # Use block handler if provided, otherwise route through dispatcher
347
+ if queue_info[:block_handler]
348
+ queue_info[:block_handler].call(message_class, payload)
349
+ else
350
+ # Route through dispatcher (inherited from Base)
351
+ receive(message_class, payload)
352
+ end
353
+ else
354
+ logger.debug { "[RedisQueue] Message filtered out by queue rules" }
355
+ end
356
+
357
+ rescue JSON::ParserError => e
358
+ logger.error { "[RedisQueue] Invalid message envelope: #{e.message}" }
359
+ rescue => e
360
+ logger.error { "[RedisQueue] Error processing message: #{e.message}" }
361
+ end
362
+ end
363
+
364
+ # Check if message should be processed based on filters
365
+ def should_process_message?(headers, filter_options)
366
+ return true if filter_options.empty?
367
+
368
+ # Apply from/to filters if specified
369
+ if filter_options[:from] && headers['from'] != filter_options[:from]
370
+ return false
371
+ end
372
+
373
+ if filter_options[:to] && headers['to'] != filter_options[:to]
374
+ return false
375
+ end
376
+
377
+ true
378
+ end
379
+
380
+ # Setup exchange metadata in Redis
381
+ def setup_exchange
382
+ exchange_key = "#{@options[:routing_prefix]}:#{@exchange_name}:metadata"
383
+ @redis.hset(exchange_key, 'type', 'topic')
384
+ @redis.hset(exchange_key, 'created_at', Time.now.to_f)
385
+ @redis.expire(exchange_key, 86400) # Expire in 24 hours
386
+ end
387
+
388
+ # Generate queue name from pattern and options
389
+ def derive_queue_name(pattern, filter_options = {})
390
+ base_name = pattern.gsub(/[#*]/, 'wildcard')
391
+ filter_suffix = filter_options.empty? ? '' : "_#{filter_options.hash.abs}"
392
+ "#{@options[:queue_prefix]}.#{base_name}#{filter_suffix}"
393
+ end
394
+
395
+ # Extract routing information from message
396
+ def extract_routing_info(serialized_message)
397
+ data = JSON.parse(serialized_message)
398
+ header = data['_sm_header'] || {}
399
+ {
400
+ from: header['from'],
401
+ to: header['to']
402
+ }
403
+ rescue JSON::ParserError
404
+ { from: nil, to: nil }
405
+ end
406
+
407
+ # Build enhanced routing key similar to RabbitMQ topic exchange
408
+ def build_enhanced_routing_key(message_class, routing_info)
409
+ # Create hierarchical routing key: namespace.message_type.from.to
410
+ parts = []
411
+
412
+ # Add exchange name as namespace
413
+ parts << @exchange_name
414
+
415
+ # Add message class (normalized)
416
+ normalized_class = message_class.to_s.gsub('::', '.').downcase
417
+ parts << normalized_class
418
+
419
+ # Add from/to if present (default to 'any')
420
+ parts << (routing_info[:from] || 'any')
421
+ parts << (routing_info[:to] || 'any')
422
+
423
+ parts.join('.')
424
+ end
425
+
426
+ # Find queues that match the routing key pattern
427
+ def find_matching_queues(routing_key)
428
+ matching = []
429
+
430
+ @routing_table.each do |pattern, queue_names|
431
+ if routing_key_matches_pattern?(routing_key, pattern)
432
+ matching.concat(queue_names)
433
+ end
434
+ end
435
+
436
+ matching.uniq
437
+ end
438
+
439
+ # Check if routing key matches the pattern (RabbitMQ-style)
440
+ def routing_key_matches_pattern?(routing_key, pattern)
441
+ return false if routing_key.nil? || routing_key.empty?
442
+ return false if pattern.nil? || pattern.empty?
443
+
444
+ # Split into segments for proper RabbitMQ-style matching
445
+ routing_segments = routing_key.split('.')
446
+ pattern_segments = pattern.split('.')
447
+
448
+ match_segments(routing_segments, pattern_segments)
449
+ end
450
+
451
+ # Recursively match routing key segments against pattern segments
452
+ def match_segments(routing_segments, pattern_segments)
453
+ return routing_segments.empty? && pattern_segments.empty? if pattern_segments.empty?
454
+ return pattern_segments.all? { |seg| seg == '#' } if routing_segments.empty?
455
+
456
+ pattern_seg = pattern_segments[0]
457
+
458
+ case pattern_seg
459
+ when '#'
460
+ # # matches zero or more segments
461
+ # Try matching with zero segments (skip the # pattern)
462
+ if match_segments(routing_segments, pattern_segments[1..-1])
463
+ return true
464
+ end
465
+ # Try matching with one or more segments (consume one routing segment)
466
+ return match_segments(routing_segments[1..-1], pattern_segments)
467
+
468
+ when '*'
469
+ # * matches exactly one segment
470
+ return false if routing_segments.empty?
471
+ return match_segments(routing_segments[1..-1], pattern_segments[1..-1])
472
+
473
+ else
474
+ # Literal match required
475
+ return false if routing_segments.empty? || routing_segments[0] != pattern_seg
476
+ return match_segments(routing_segments[1..-1], pattern_segments[1..-1])
477
+ end
478
+ end
479
+
480
+ # Sanitize string for use in routing key
481
+ def sanitize_for_routing_key(str)
482
+ str.to_s.gsub(/[^a-zA-Z0-9_-]/, '_').downcase
483
+ end
484
+ end
485
+
486
+ # Fluent API builder for complex subscription patterns
487
+ class RedisQueueSubscriptionBuilder
488
+ def initialize(transport)
489
+ @transport = transport
490
+ @conditions = {}
491
+ end
492
+
493
+ def from(sender_uuid)
494
+ @conditions[:from] = sender_uuid
495
+ self
496
+ end
497
+
498
+ def to(recipient_uuid)
499
+ @conditions[:to] = recipient_uuid
500
+ self
501
+ end
502
+
503
+ def type(message_type)
504
+ @conditions[:type] = message_type
505
+ self
506
+ end
507
+
508
+ def broadcast
509
+ @conditions[:to] = 'broadcast'
510
+ self
511
+ end
512
+
513
+ def alerts
514
+ @conditions[:alerts] = true
515
+ self
516
+ end
517
+
518
+ def consumer_group(group_name)
519
+ @conditions[:consumer_group] = group_name
520
+ self
521
+ end
522
+
523
+ def build
524
+ parts = []
525
+
526
+ # Build pattern based on conditions
527
+ if @conditions[:alerts]
528
+ return "alert.#.*.*"
529
+ end
530
+
531
+ if @conditions[:type]
532
+ parts << "*"
533
+ parts << @conditions[:type].to_s.gsub('::', '.').downcase
534
+ else
535
+ parts << "#"
536
+ end
537
+
538
+ parts << (@conditions[:from] || "*")
539
+ parts << (@conditions[:to] || "*")
540
+
541
+ parts.join(".")
542
+ end
543
+
544
+ def subscribe(process_method = :process, &block)
545
+ pattern = build
546
+
547
+ filter_options = {}
548
+ filter_options[:from] = @conditions[:from] if @conditions[:from]
549
+ filter_options[:to] = @conditions[:to] if @conditions[:to]
550
+
551
+ @transport.subscribe_pattern(pattern, process_method, filter_options, &block)
552
+ end
553
+ end
554
+ end
555
+ end
@@ -54,6 +54,7 @@ module SmartMessage
54
54
  register(:stdout, SmartMessage::Transport::StdoutTransport)
55
55
  register(:memory, SmartMessage::Transport::MemoryTransport)
56
56
  register(:redis, SmartMessage::Transport::RedisTransport)
57
+ register(:redis_queue, SmartMessage::Transport::RedisQueueTransport)
57
58
  end
58
59
  end
59
60
  end
@@ -6,4 +6,37 @@ require_relative 'transport/base'
6
6
  require_relative 'transport/registry'
7
7
  require_relative 'transport/stdout_transport'
8
8
  require_relative 'transport/memory_transport'
9
- require_relative 'transport/redis_transport'
9
+ require_relative 'transport/redis_transport'
10
+ require_relative 'transport/redis_queue_transport'
11
+
12
+ module SmartMessage
13
+ module Transport
14
+ class << self
15
+ def default
16
+ # Check global configuration first, then fall back to framework default
17
+ SmartMessage.configuration.default_transport
18
+ end
19
+
20
+ def registry
21
+ @registry ||= Registry.new
22
+ end
23
+
24
+ def register(name, transport_class)
25
+ registry.register(name, transport_class)
26
+ end
27
+
28
+ def get(name)
29
+ registry.get(name)
30
+ end
31
+
32
+ def create(name, **options)
33
+ transport_class = get(name)
34
+ transport_class&.new(**options)
35
+ end
36
+
37
+ def available
38
+ registry.list
39
+ end
40
+ end
41
+ end
42
+ end
@@ -3,5 +3,5 @@
3
3
  # frozen_string_literal: true
4
4
 
5
5
  module SmartMessage
6
- VERSION = '0.0.10'
6
+ VERSION = '0.0.12'
7
7
  end
data/lib/smart_message.rb CHANGED
@@ -28,7 +28,7 @@ require_relative './simple_stats'
28
28
  module SmartMessage
29
29
  class << self
30
30
  # Global configuration for SmartMessage
31
- #
31
+ #
32
32
  # Usage:
33
33
  # SmartMessage.configure do |config|
34
34
  # config.logger = MyApp::Logger.new
@@ -38,65 +38,18 @@ module SmartMessage
38
38
  def configure
39
39
  yield(configuration)
40
40
  end
41
-
41
+
42
42
  # Get the global configuration instance
43
43
  def configuration
44
44
  @configuration ||= Configuration.new
45
45
  end
46
-
46
+
47
47
  # Reset global configuration to defaults
48
48
  def reset_configuration!
49
49
  @configuration = Configuration.new
50
+ # Also reset the cached logger
51
+ SmartMessage::Logger.reset!
50
52
  end
51
53
  end
52
54
  # Module definitions for Zeitwerk to populate
53
- module Serializer
54
- class << self
55
- def default
56
- # Check global configuration first, then fall back to framework default
57
- SmartMessage.configuration.default_serializer
58
- end
59
- end
60
- end
61
-
62
- module Logger
63
- class << self
64
- def default
65
- # Check global configuration first, then fall back to framework default
66
- SmartMessage.configuration.default_logger
67
- end
68
- end
69
- end
70
-
71
- module Transport
72
- class << self
73
- def default
74
- # Check global configuration first, then fall back to framework default
75
- SmartMessage.configuration.default_transport
76
- end
77
-
78
- def registry
79
- @registry ||= Registry.new
80
- end
81
-
82
- def register(name, transport_class)
83
- registry.register(name, transport_class)
84
- end
85
-
86
- def get(name)
87
- registry.get(name)
88
- end
89
-
90
- def create(name, **options)
91
- transport_class = get(name)
92
- transport_class&.new(**options)
93
- end
94
-
95
- def available
96
- registry.list
97
- end
98
- end
99
- end
100
55
  end # module SmartMessage
101
-
102
- # Don't eager load initially - let Zeitwerk handle lazy loading