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,8 @@
1
+ # Default ignored files
2
+ /shelf/
3
+ /workspace.xml
4
+ # Editor-based HTTP Client requests
5
+ /httpRequests/
6
+ # Datasource local storage ignored files
7
+ /dataSources/
8
+ /dataSources.local.xml
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="MarkdownSettings">
4
+ <option name="fileGroupingEnabled" value="true" />
5
+ </component>
6
+ </project>
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectRootManager" version="2" project-jdk-name="ruby-2.6.10-p210" project-jdk-type="RUBY_SDK" />
4
+ </project>
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectModuleManager">
4
+ <modules>
5
+ <module fileurl="file://$PROJECT_DIR$/.idea/smart_message.iml" filepath="$PROJECT_DIR$/.idea/smart_message.iml" />
6
+ </modules>
7
+ </component>
8
+ </project>
@@ -0,0 +1,16 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <module type="RUBY_MODULE" version="4">
3
+ <component name="ModuleRunConfigurationManager">
4
+ <shared />
5
+ </component>
6
+ <component name="NewModuleRootManager">
7
+ <content url="file://$MODULE_DIR$">
8
+ <sourceFolder url="file://$MODULE_DIR$/features" isTestSource="true" />
9
+ <sourceFolder url="file://$MODULE_DIR$/spec" isTestSource="true" />
10
+ <sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
11
+ </content>
12
+ <orderEntry type="inheritedJdk" />
13
+ <orderEntry type="sourceFolder" forTests="false" />
14
+ <orderEntry type="library" scope="PROVIDED" name="bundler (v1.17.2, ruby-2.6.10-p210) [gem]" level="application" />
15
+ </component>
16
+ </module>
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="VcsDirectoryMappings">
4
+ <mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
5
+ </component>
6
+ </project>
@@ -202,6 +202,11 @@ module SmartMessage
202
202
  end
203
203
  end
204
204
 
205
+ # Setter method for from - allows ClassName.from = 'value' syntax
206
+ def from=(entity_id)
207
+ from(entity_id)
208
+ end
209
+
205
210
  def from_configured?; !from.nil?; end
206
211
  def from_missing?; from.nil?; end
207
212
  def reset_from;
@@ -224,6 +229,11 @@ module SmartMessage
224
229
  end
225
230
  end
226
231
 
232
+ # Setter method for to - allows ClassName.to = 'value' syntax
233
+ def to=(entity_id)
234
+ to(entity_id)
235
+ end
236
+
227
237
  def to_configured?; !to.nil?; end
228
238
  def to_missing?; to.nil?; end
229
239
  def reset_to;
@@ -246,6 +256,11 @@ module SmartMessage
246
256
  end
247
257
  end
248
258
 
259
+ # Setter method for reply_to - allows ClassName.reply_to = 'value' syntax
260
+ def reply_to=(entity_id)
261
+ reply_to(entity_id)
262
+ end
263
+
249
264
  def reply_to_configured?; !reply_to.nil?; end
250
265
  def reply_to_missing?; reply_to.nil?; end
251
266
  def reset_reply_to;
@@ -162,8 +162,6 @@ module SmartMessage
162
162
  # @param serialized_message [String] The serialized message content
163
163
  # @return [SmartMessage::Base] The decoded message instance
164
164
  def decode(serialized_message)
165
- logger = SmartMessage::Logger.default
166
-
167
165
  begin
168
166
  (self.logger || SmartMessage::Logger.default).info { "[SmartMessage] Received: #{self.name} (#{serialized_message.bytesize} bytes)" }
169
167
 
@@ -191,7 +191,7 @@ module SmartMessage
191
191
  # Framework's built-in default serializer (JSON)
192
192
  def framework_default_serializer
193
193
  SmartMessage::Serializer::Json.new
194
- rescue => e
194
+ rescue
195
195
  # Fallback if JSON serializer is not available
196
196
  nil
197
197
  end
@@ -9,15 +9,26 @@ require_relative 'logger/lumberjack'
9
9
  module SmartMessage
10
10
  module Logger
11
11
  class << self
12
- # Global default logger instance
13
- def default
14
- @default ||= Lumberjack.new
12
+ # Global default logger instance - uses configuration if available
13
+ def default(options = {})
14
+ # Always check current configuration first (don't cache when config is available)
15
+ if defined?(SmartMessage.configuration) && SmartMessage.configuration.logger_configured?
16
+ SmartMessage.configuration.default_logger
17
+ else
18
+ # Cache the framework default logger only when no configuration
19
+ @default ||= Lumberjack.new(**options)
20
+ end
15
21
  end
16
22
 
17
23
  # Set the default logger
18
24
  def default=(logger)
19
25
  @default = logger
20
26
  end
27
+
28
+ # Reset the cached default logger
29
+ def reset!
30
+ @default = nil
31
+ end
21
32
  end
22
33
  end
23
- end
34
+ end
@@ -105,8 +105,11 @@ module SmartMessage
105
105
  end
106
106
  end
107
107
 
108
- def logger_configured?; !logger.nil?; end
109
- def logger_missing?; logger.nil?; end
108
+ def logger_configured?; !logger_missing?; end
109
+ def logger_missing?
110
+ # Check if class-level logger is explicitly configured (without fallback to defaults)
111
+ (class_variable_get(:@@logger) rescue nil).nil?
112
+ end
110
113
  def reset_logger; class_variable_set(:@@logger, nil); end
111
114
 
112
115
  #########################################################
@@ -0,0 +1,14 @@
1
+ # lib/smart_message/serializer.rb
2
+ # encoding: utf-8
3
+ # frozen_string_literal: true
4
+
5
+ module SmartMessage
6
+ module Serializer
7
+ class << self
8
+ def default
9
+ # Check global configuration first, then fall back to framework default
10
+ SmartMessage.configuration.default_serializer
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,399 @@
1
+ # lib/smart_message/transport/redis_transport_enhanced.rb
2
+ # Enhanced Redis transport with routing intelligence similar to RabbitMQ
3
+
4
+ require_relative 'redis_transport'
5
+
6
+ module SmartMessage
7
+ module Transport
8
+ class RedisEnhancedTransport < RedisTransport
9
+
10
+ # Enhanced publish with structured channel names
11
+ def do_publish(message_class, serialized_message)
12
+ # Extract routing information from message
13
+ routing_info = extract_routing_info(serialized_message)
14
+
15
+ # Build enhanced channel name with routing info
16
+ channel = build_enhanced_channel(message_class, routing_info)
17
+
18
+ begin
19
+ # Publish to both simple channel (backwards compatibility) and enhanced channel
20
+ @redis_pub.publish(message_class, serialized_message) # Original format
21
+ @redis_pub.publish(channel, serialized_message) # Enhanced format
22
+
23
+ logger.debug { "[Redis Enhanced] Published to channels: #{message_class} and #{channel}" }
24
+ rescue Redis::ConnectionError
25
+ retry_with_reconnect('publish') do
26
+ @redis_pub.publish(message_class, serialized_message)
27
+ @redis_pub.publish(channel, serialized_message)
28
+ end
29
+ end
30
+ end
31
+
32
+ # Enhanced subscription with pattern support
33
+ def subscribe_pattern(pattern)
34
+ @mutex.synchronize do
35
+ @pattern_subscriptions ||= Set.new
36
+ @pattern_subscriptions.add(pattern)
37
+ restart_subscriber if @running
38
+ end
39
+
40
+ logger.info { "[Redis Enhanced] Subscribed to pattern: #{pattern}" }
41
+ end
42
+
43
+ # Convenience methods similar to RabbitMQ
44
+ def subscribe_to_recipient(recipient_id)
45
+ pattern = "*.*.#{sanitize_for_channel(recipient_id)}"
46
+ subscribe_pattern(pattern)
47
+ end
48
+
49
+ def subscribe_from_sender(sender_id)
50
+ pattern = "*.#{sanitize_for_channel(sender_id)}.*"
51
+ subscribe_pattern(pattern)
52
+ end
53
+
54
+ def subscribe_to_type(message_type)
55
+ base_type = message_type.to_s.gsub('::', '.').downcase
56
+ pattern = "#{base_type}.*.*"
57
+ subscribe_pattern(pattern)
58
+ end
59
+
60
+ def subscribe_to_alerts
61
+ patterns = [
62
+ "emergency.*.*",
63
+ "*alert*.*.*",
64
+ "*alarm*.*.*",
65
+ "*critical*.*.*"
66
+ ]
67
+
68
+ patterns.each { |pattern| subscribe_pattern(pattern) }
69
+ end
70
+
71
+ def subscribe_to_broadcasts
72
+ pattern = "*.*.broadcast"
73
+ subscribe_pattern(pattern)
74
+ end
75
+
76
+ # Fluent API
77
+ def where
78
+ RedisSubscriptionBuilder.new(self)
79
+ end
80
+
81
+ private
82
+
83
+ def extract_routing_info(serialized_message)
84
+ begin
85
+ message_data = JSON.parse(serialized_message)
86
+ header = message_data['_sm_header'] || {}
87
+
88
+ {
89
+ from: sanitize_for_channel(header['from'] || 'anonymous'),
90
+ to: sanitize_for_channel(header['to'] || 'broadcast')
91
+ }
92
+ rescue JSON::ParserError
93
+ logger.warn { "[Redis Enhanced] Could not parse message for routing info, using defaults" }
94
+ { from: 'anonymous', to: 'broadcast' }
95
+ end
96
+ end
97
+
98
+ def build_enhanced_channel(message_class, routing_info)
99
+ # Format: message_type.from.to (simplified vs RabbitMQ's 4-part)
100
+ base_channel = message_class.to_s.gsub('::', '.').downcase
101
+ "#{base_channel}.#{routing_info[:from]}.#{routing_info[:to]}"
102
+ end
103
+
104
+ def sanitize_for_channel(value)
105
+ # Redis channels can contain most characters, but standardize format
106
+ value.to_s.gsub(/[^a-zA-Z0-9_\-]/, '_').downcase
107
+ end
108
+
109
+ # Override to handle both regular and pattern subscriptions
110
+ def subscribe_to_channels
111
+ channels = @subscribed_channels.to_a
112
+ patterns = @pattern_subscriptions&.to_a || []
113
+
114
+ return unless channels.any? || patterns.any?
115
+
116
+ begin
117
+ # Handle both regular subscriptions and pattern subscriptions
118
+ if patterns.any?
119
+ subscribe_with_patterns(channels, patterns)
120
+ elsif channels.any?
121
+ @redis_sub.subscribe(*channels) do |on|
122
+ setup_subscription_handlers(on)
123
+ end
124
+ end
125
+ rescue => e
126
+ logger.error { "[Redis Enhanced] Error in subscription: #{e.class.name} - #{e.message}" }
127
+ retry_subscriber if @running
128
+ end
129
+ end
130
+
131
+ def subscribe_with_patterns(channels, patterns)
132
+ # Redis doesn't support mixing SUBSCRIBE and PSUBSCRIBE in same connection
133
+ # So we handle them in separate threads or use PSUBSCRIBE for everything
134
+
135
+ if channels.any?
136
+ # Convert regular channels to patterns for unified handling
137
+ channel_patterns = channels.map { |ch| ch } # Exact match patterns
138
+ all_patterns = patterns + channel_patterns
139
+ else
140
+ all_patterns = patterns
141
+ end
142
+
143
+ @redis_sub.psubscribe(*all_patterns) do |on|
144
+ on.pmessage do |pattern, channel, serialized_message|
145
+ begin
146
+ # Determine message class from channel name
147
+ message_class = extract_message_class_from_channel(channel)
148
+
149
+ # Process the message if we have a handler
150
+ if message_class && (@dispatcher.subscribers[message_class] || pattern_matches_handler?(channel))
151
+ receive(message_class, serialized_message)
152
+ else
153
+ logger.debug { "[Redis Enhanced] No handler for channel: #{channel}" }
154
+ end
155
+ rescue => e
156
+ logger.error { "[Redis Enhanced] Error processing pattern message: #{e.message}" }
157
+ end
158
+ end
159
+
160
+ on.psubscribe do |pattern, subscriptions|
161
+ logger.debug { "[Redis Enhanced] Subscribed to pattern: #{pattern} (#{subscriptions} total)" }
162
+ end
163
+
164
+ on.punsubscribe do |pattern, subscriptions|
165
+ logger.debug { "[Redis Enhanced] Unsubscribed from pattern: #{pattern} (#{subscriptions} total)" }
166
+ end
167
+ end
168
+ end
169
+
170
+ def setup_subscription_handlers(on)
171
+ on.message do |channel, serialized_message|
172
+ begin
173
+ # Handle regular subscription
174
+ receive(channel, serialized_message)
175
+ rescue => e
176
+ logger.error { "[Redis Enhanced] Error processing regular message: #{e.message}" }
177
+ end
178
+ end
179
+
180
+ on.subscribe do |channel, subscriptions|
181
+ logger.debug { "[Redis Enhanced] Subscribed to channel: #{channel} (#{subscriptions} total)" }
182
+ end
183
+
184
+ on.unsubscribe do |channel, subscriptions|
185
+ logger.debug { "[Redis Enhanced] Unsubscribed from channel: #{channel} (#{subscriptions} total)" }
186
+ end
187
+ end
188
+
189
+ def extract_message_class_from_channel(channel)
190
+ # Handle both original format and enhanced format
191
+ parts = channel.split('.')
192
+
193
+ if parts.length >= 3
194
+ # Enhanced format: message_type.from.to
195
+ # Extract just the message type part
196
+ message_parts = parts[0..-3] if parts.length > 3
197
+ message_parts ||= [parts[0]]
198
+
199
+ # Convert back to class name format
200
+ message_parts.map(&:capitalize).join('::')
201
+ else
202
+ # Original format: just use the channel name as class
203
+ channel
204
+ end
205
+ end
206
+
207
+ def pattern_matches_handler?(channel)
208
+ # Check if any registered patterns would match this channel
209
+ return false unless @pattern_subscriptions
210
+
211
+ @pattern_subscriptions.any? do |pattern|
212
+ File.fnmatch(pattern, channel)
213
+ end
214
+ end
215
+ end
216
+
217
+ # Fluent API builder for Redis patterns
218
+ class RedisSubscriptionBuilder
219
+ def initialize(transport)
220
+ @transport = transport
221
+ @conditions = {}
222
+ end
223
+
224
+ def from(sender_id)
225
+ @conditions[:from] = sender_id
226
+ self
227
+ end
228
+
229
+ def to(recipient_id)
230
+ @conditions[:to] = recipient_id
231
+ self
232
+ end
233
+
234
+ def type(message_type)
235
+ @conditions[:type] = message_type
236
+ self
237
+ end
238
+
239
+ def build
240
+ pattern_parts = []
241
+
242
+ # Build pattern based on conditions (3-part format for Redis)
243
+ pattern_parts << (@conditions[:type]&.to_s&.gsub('::', '.')&.downcase || '*')
244
+ pattern_parts << (@conditions[:from] || '*')
245
+ pattern_parts << (@conditions[:to] || '*')
246
+
247
+ pattern_parts.join('.')
248
+ end
249
+
250
+ def subscribe
251
+ pattern = build
252
+ @transport.subscribe_pattern(pattern)
253
+ end
254
+ end
255
+ end
256
+ end
257
+
258
+ # Alternative: Redis Streams implementation
259
+ module SmartMessage
260
+ module Transport
261
+ class RedisStreamsTransport < Base
262
+
263
+ def default_options
264
+ {
265
+ url: 'redis://localhost:6379',
266
+ db: 0,
267
+ stream_prefix: 'smart_message',
268
+ consumer_group: 'smart_message_workers',
269
+ consumer_id: Socket.gethostname + '_' + Process.pid.to_s,
270
+ max_len: 10000, # Trim streams to prevent unbounded growth
271
+ block_time: 1000 # 1 second blocking read
272
+ }
273
+ end
274
+
275
+ def configure
276
+ @redis = Redis.new(url: @options[:url], db: @options[:db])
277
+ @streams = {}
278
+ @consumers = {}
279
+ @running = false
280
+ end
281
+
282
+ def do_publish(message_class, serialized_message)
283
+ stream_key = derive_stream_key(message_class)
284
+ routing_info = extract_routing_info(serialized_message)
285
+
286
+ @redis.xadd(
287
+ stream_key,
288
+ {
289
+ data: serialized_message,
290
+ from: routing_info[:from],
291
+ to: routing_info[:to],
292
+ message_class: message_class.to_s,
293
+ timestamp: Time.now.to_f
294
+ },
295
+ maxlen: @options[:max_len],
296
+ approximate: true
297
+ )
298
+
299
+ logger.debug { "[Redis Streams] Published to stream: #{stream_key}" }
300
+ end
301
+
302
+ def subscribe(message_class, process_method, filter_options = {})
303
+ super(message_class, process_method, filter_options)
304
+
305
+ stream_key = derive_stream_key(message_class)
306
+ setup_consumer_group(stream_key)
307
+ start_consumer(stream_key, message_class, filter_options)
308
+ end
309
+
310
+ private
311
+
312
+ def derive_stream_key(message_class)
313
+ "#{@options[:stream_prefix]}:#{message_class.to_s.gsub('::', ':').downcase}"
314
+ end
315
+
316
+ def setup_consumer_group(stream_key)
317
+ begin
318
+ @redis.xgroup(
319
+ :create,
320
+ stream_key,
321
+ @options[:consumer_group],
322
+ '$', # Start from new messages
323
+ mkstream: true
324
+ )
325
+ rescue Redis::CommandError => e
326
+ # Consumer group might already exist
327
+ logger.debug { "[Redis Streams] Consumer group exists: #{e.message}" }
328
+ end
329
+ end
330
+
331
+ def start_consumer(stream_key, message_class, filter_options)
332
+ return if @consumers[stream_key]
333
+
334
+ @consumers[stream_key] = Thread.new do
335
+ while @running
336
+ begin
337
+ # Read from consumer group with blocking
338
+ messages = @redis.xread_group(
339
+ @options[:consumer_group],
340
+ @options[:consumer_id],
341
+ { stream_key => '>' },
342
+ block: @options[:block_time],
343
+ count: 10
344
+ )
345
+
346
+ messages&.each do |stream, stream_messages|
347
+ stream_messages.each do |message_id, fields|
348
+ process_stream_message(message_id, fields, message_class, filter_options)
349
+ end
350
+ end
351
+ rescue => e
352
+ logger.error { "[Redis Streams] Consumer error: #{e.message}" }
353
+ sleep(1)
354
+ end
355
+ end
356
+ end
357
+ end
358
+
359
+ def process_stream_message(message_id, fields, message_class, filter_options)
360
+ # Apply filtering based on from/to if specified
361
+ from_filter = filter_options[:from]
362
+ to_filter = filter_options[:to]
363
+
364
+ if from_filter && fields['from'] != from_filter
365
+ @redis.xack(@options[:stream_prefix], @options[:consumer_group], message_id)
366
+ return
367
+ end
368
+
369
+ if to_filter && fields['to'] != to_filter
370
+ @redis.xack(@options[:stream_prefix], @options[:consumer_group], message_id)
371
+ return
372
+ end
373
+
374
+ # Process the message
375
+ begin
376
+ receive(fields['message_class'], fields['data'])
377
+ @redis.xack(@options[:stream_prefix], @options[:consumer_group], message_id)
378
+ rescue => e
379
+ logger.error { "[Redis Streams] Message processing error: #{e.message}" }
380
+ # Message will remain unacknowledged and can be retried
381
+ end
382
+ end
383
+
384
+ def extract_routing_info(serialized_message)
385
+ begin
386
+ message_data = JSON.parse(serialized_message)
387
+ header = message_data['_sm_header'] || {}
388
+
389
+ {
390
+ from: header['from'] || 'anonymous',
391
+ to: header['to'] || 'broadcast'
392
+ }
393
+ rescue JSON::ParserError
394
+ { from: 'anonymous', to: 'broadcast' }
395
+ end
396
+ end
397
+ end
398
+ end
399
+ end