smart_message 0.0.2 → 0.0.3

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.
@@ -16,6 +16,9 @@ module SmartMessage
16
16
  @@transport = nil
17
17
  @@serializer = nil
18
18
  @@logger = nil
19
+
20
+ # Registry for proc-based message handlers
21
+ @@proc_handlers = {}
19
22
 
20
23
  include Hashie::Extensions::Dash::PropertyTranslation
21
24
 
@@ -184,6 +187,44 @@ module SmartMessage
184
187
  end
185
188
 
186
189
 
190
+ #########################################################
191
+ ## proc handler management
192
+
193
+ # Register a proc handler and return a unique identifier for it
194
+ # @param message_class [String] The message class name
195
+ # @param handler_proc [Proc] The proc to register
196
+ # @return [String] Unique identifier for this handler
197
+ def register_proc_handler(message_class, handler_proc)
198
+ handler_id = "#{message_class}.proc_#{SecureRandom.hex(8)}"
199
+ @@proc_handlers[handler_id] = handler_proc
200
+ handler_id
201
+ end
202
+
203
+ # Call a registered proc handler
204
+ # @param handler_id [String] The handler identifier
205
+ # @param message_header [SmartMessage::Header] The message header
206
+ # @param message_payload [String] The message payload
207
+ def call_proc_handler(handler_id, message_header, message_payload)
208
+ handler_proc = @@proc_handlers[handler_id]
209
+ return unless handler_proc
210
+
211
+ handler_proc.call(message_header, message_payload)
212
+ end
213
+
214
+ # Remove a proc handler from the registry
215
+ # @param handler_id [String] The handler identifier to remove
216
+ def unregister_proc_handler(handler_id)
217
+ @@proc_handlers.delete(handler_id)
218
+ end
219
+
220
+ # Check if a handler ID refers to a proc handler
221
+ # @param handler_id [String] The handler identifier
222
+ # @return [Boolean] True if this is a proc handler
223
+ def proc_handler?(handler_id)
224
+ @@proc_handlers.key?(handler_id)
225
+ end
226
+
227
+
187
228
  #########################################################
188
229
  ## class-level transport configuration
189
230
 
@@ -226,25 +267,74 @@ module SmartMessage
226
267
  # Add this message class to the transport's catalog of
227
268
  # subscribed messages. If the transport is missing, raise
228
269
  # an exception.
229
- def subscribe(process_method = nil)
230
- message_class = whoami
231
- process_method = message_class + '.process' if process_method.nil?
270
+ #
271
+ # @param process_method [String, Proc, nil] The processing method:
272
+ # - String: Method name like "MyService.handle_message"
273
+ # - Proc: A proc/lambda that accepts (message_header, message_payload)
274
+ # - nil: Uses default "MessageClass.process" method
275
+ # @param block [Proc] Alternative way to pass a processing block
276
+ # @return [String] The identifier used for this subscription
277
+ #
278
+ # @example Using default handler
279
+ # MyMessage.subscribe
280
+ #
281
+ # @example Using custom method name
282
+ # MyMessage.subscribe("MyService.handle_message")
283
+ #
284
+ # @example Using a block
285
+ # MyMessage.subscribe do |header, payload|
286
+ # data = JSON.parse(payload)
287
+ # puts "Received: #{data}"
288
+ # end
289
+ #
290
+ # @example Using a proc
291
+ # handler = proc { |header, payload| puts "Processing..." }
292
+ # MyMessage.subscribe(handler)
293
+ def subscribe(process_method = nil, &block)
294
+ message_class = whoami
295
+
296
+ # Handle different parameter types
297
+ if block_given?
298
+ # Block was passed - use it as the handler
299
+ handler_proc = block
300
+ process_method = register_proc_handler(message_class, handler_proc)
301
+ elsif process_method.respond_to?(:call)
302
+ # Proc/lambda was passed as first parameter
303
+ handler_proc = process_method
304
+ process_method = register_proc_handler(message_class, handler_proc)
305
+ elsif process_method.nil?
306
+ # Use default handler
307
+ process_method = message_class + '.process'
308
+ end
309
+ # If process_method is a String, use it as-is
232
310
 
233
311
  # TODO: Add proper logging here
234
312
 
235
313
  raise Errors::TransportNotConfigured if transport_missing?
236
314
  transport.subscribe(message_class, process_method)
315
+
316
+ process_method
237
317
  end
238
318
 
239
319
 
240
320
  # Remove this process_method for this message class from the
241
321
  # subscribers list.
322
+ # @param process_method [String, nil] The processing method identifier to remove
323
+ # - String: Method name like "MyService.handle_message" or proc handler ID
324
+ # - nil: Uses default "MessageClass.process" method
242
325
  def unsubscribe(process_method = nil)
243
326
  message_class = whoami
244
327
  process_method = message_class + '.process' if process_method.nil?
245
328
  # TODO: Add proper logging here
246
329
 
247
- transport.unsubscribe(message_class, process_method) if transport_configured?
330
+ if transport_configured?
331
+ transport.unsubscribe(message_class, process_method)
332
+
333
+ # If this was a proc handler, clean it up from the registry
334
+ if proc_handler?(process_method)
335
+ unregister_proc_handler(process_method)
336
+ end
337
+ end
248
338
  end
249
339
 
250
340
 
@@ -118,13 +118,20 @@ module SmartMessage
118
118
  @subscribers[message_klass].each do |message_processor|
119
119
  SS.add(message_klass, message_processor, 'routed' )
120
120
  @router_pool.post do
121
- parts = message_processor.split('.')
122
- target_klass = parts[0]
123
- class_method = parts[1]
124
121
  begin
125
- target_klass.constantize
126
- .method(class_method)
127
- .call(message_header, message_payload)
122
+ # Check if this is a proc handler or a regular method call
123
+ if proc_handler?(message_processor)
124
+ # Call the proc handler via SmartMessage::Base
125
+ SmartMessage::Base.call_proc_handler(message_processor, message_header, message_payload)
126
+ else
127
+ # Original method call logic
128
+ parts = message_processor.split('.')
129
+ target_klass = parts[0]
130
+ class_method = parts[1]
131
+ target_klass.constantize
132
+ .method(class_method)
133
+ .call(message_header, message_payload)
134
+ end
128
135
  rescue Exception => e
129
136
  # TODO: Add proper exception logging
130
137
  # Exception details: #{e.message}
@@ -135,6 +142,15 @@ module SmartMessage
135
142
  end
136
143
  end
137
144
 
145
+ private
146
+
147
+ # Check if a message processor is a proc handler
148
+ # @param message_processor [String] The message processor identifier
149
+ # @return [Boolean] True if this is a proc handler
150
+ def proc_handler?(message_processor)
151
+ SmartMessage::Base.proc_handler?(message_processor)
152
+ end
153
+
138
154
 
139
155
  #######################################################
140
156
  ## Class methods
@@ -0,0 +1,190 @@
1
+ # lib/smart_message/transport/redis_transport.rb
2
+ # encoding: utf-8
3
+ # frozen_string_literal: true
4
+
5
+ require 'redis'
6
+ require 'securerandom'
7
+ require 'set'
8
+
9
+ module SmartMessage
10
+ module Transport
11
+ # Redis pub/sub transport for SmartMessage
12
+ # Uses message class name as the Redis channel name
13
+ class RedisTransport < Base
14
+ attr_reader :redis_pub, :redis_sub, :subscriber_thread
15
+
16
+ def default_options
17
+ {
18
+ url: 'redis://localhost:6379',
19
+ db: 0,
20
+ auto_subscribe: true,
21
+ reconnect_attempts: 5,
22
+ reconnect_delay: 1
23
+ }
24
+ end
25
+
26
+ def configure
27
+ @redis_pub = Redis.new(url: @options[:url], db: @options[:db])
28
+ @redis_sub = Redis.new(url: @options[:url], db: @options[:db])
29
+ @subscribed_channels = Set.new
30
+ @subscriber_thread = nil
31
+ @running = false
32
+ @mutex = Mutex.new
33
+
34
+ start_subscriber if @options[:auto_subscribe]
35
+ end
36
+
37
+ # Publish message to Redis channel using message class name
38
+ def publish(message_header, message_payload)
39
+ channel = message_header.message_class
40
+
41
+ begin
42
+ @redis_pub.publish(channel, message_payload)
43
+ rescue Redis::ConnectionError
44
+ retry_with_reconnect('publish') { @redis_pub.publish(channel, message_payload) }
45
+ end
46
+ end
47
+
48
+ # Subscribe to a message class (Redis channel)
49
+ def subscribe(message_class, process_method)
50
+ super(message_class, process_method)
51
+
52
+ @mutex.synchronize do
53
+ @subscribed_channels.add(message_class)
54
+ restart_subscriber if @running
55
+ end
56
+ end
57
+
58
+ # Unsubscribe from a specific message class and process method
59
+ def unsubscribe(message_class, process_method)
60
+ super(message_class, process_method)
61
+
62
+ @mutex.synchronize do
63
+ # If no more subscribers for this message class, unsubscribe from channel
64
+ if @dispatcher.subscribers[message_class].nil? || @dispatcher.subscribers[message_class].empty?
65
+ @subscribed_channels.delete(message_class)
66
+ restart_subscriber if @running
67
+ end
68
+ end
69
+ end
70
+
71
+ # Unsubscribe from all process methods for a message class
72
+ def unsubscribe!(message_class)
73
+ super(message_class)
74
+
75
+ @mutex.synchronize do
76
+ @subscribed_channels.delete(message_class)
77
+ restart_subscriber if @running
78
+ end
79
+ end
80
+
81
+ def connected?
82
+ begin
83
+ @redis_pub.ping == 'PONG' && @redis_sub.ping == 'PONG'
84
+ rescue Redis::ConnectionError
85
+ false
86
+ end
87
+ end
88
+
89
+ def connect
90
+ @redis_pub.ping
91
+ @redis_sub.ping
92
+ start_subscriber unless @running
93
+ end
94
+
95
+ def disconnect
96
+ stop_subscriber
97
+ @redis_pub.quit if @redis_pub
98
+ @redis_sub.quit if @redis_sub
99
+ end
100
+
101
+ private
102
+
103
+ def start_subscriber
104
+ return if @running
105
+
106
+ @running = true
107
+ @subscriber_thread = Thread.new do
108
+ begin
109
+ subscribe_to_channels
110
+ rescue => e
111
+ # Log error but don't crash the thread
112
+ puts "Redis subscriber error: #{e.message}" if @options[:debug]
113
+ retry_subscriber
114
+ end
115
+ end
116
+ end
117
+
118
+ def stop_subscriber
119
+ @running = false
120
+
121
+ if @subscriber_thread
122
+ @subscriber_thread.kill
123
+ @subscriber_thread.join(5) # Wait up to 5 seconds
124
+ @subscriber_thread = nil
125
+ end
126
+ end
127
+
128
+ def restart_subscriber
129
+ stop_subscriber
130
+ start_subscriber if @subscribed_channels.any?
131
+ end
132
+
133
+ def subscribe_to_channels
134
+ return unless @subscribed_channels.any?
135
+
136
+ begin
137
+ @redis_sub.subscribe(*@subscribed_channels) do |on|
138
+ on.message do |channel, message_payload|
139
+ # Create a header with the channel as message_class
140
+ message_header = SmartMessage::Header.new(
141
+ message_class: channel,
142
+ uuid: SecureRandom.uuid,
143
+ published_at: Time.now,
144
+ publisher_pid: 'redis_subscriber'
145
+ )
146
+
147
+ receive(message_header, message_payload)
148
+ end
149
+
150
+ on.subscribe do |channel, subscriptions|
151
+ puts "Subscribed to Redis channel: #{channel} (#{subscriptions} total)" if @options[:debug]
152
+ end
153
+
154
+ on.unsubscribe do |channel, subscriptions|
155
+ puts "Unsubscribed from Redis channel: #{channel} (#{subscriptions} total)" if @options[:debug]
156
+ end
157
+ end
158
+ rescue => e
159
+ # Silently handle connection errors during subscription
160
+ puts "Redis subscription error: #{e.class.name}" if @options[:debug]
161
+ retry_subscriber if @running
162
+ end
163
+ end
164
+
165
+ def retry_subscriber
166
+ return unless @running
167
+
168
+ sleep(@options[:reconnect_delay])
169
+ subscribe_to_channels if @running
170
+ end
171
+
172
+ def retry_with_reconnect(operation)
173
+ attempts = 0
174
+ begin
175
+ yield
176
+ rescue Redis::ConnectionError => e
177
+ attempts += 1
178
+ if attempts <= @options[:reconnect_attempts]
179
+ sleep(@options[:reconnect_delay])
180
+ # Reconnect
181
+ @redis_pub = Redis.new(url: @options[:url], db: @options[:db])
182
+ retry
183
+ else
184
+ raise e
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
@@ -53,6 +53,7 @@ module SmartMessage
53
53
  # Register built-in transports
54
54
  register(:stdout, SmartMessage::Transport::StdoutTransport)
55
55
  register(:memory, SmartMessage::Transport::MemoryTransport)
56
+ register(:redis, SmartMessage::Transport::RedisTransport)
56
57
  end
57
58
  end
58
59
  end
@@ -6,6 +6,7 @@ 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
10
 
10
11
  module SmartMessage
11
12
  # Transport layer abstraction for SmartMessage
@@ -3,5 +3,5 @@
3
3
  # frozen_string_literal: true
4
4
 
5
5
  module SmartMessage
6
- VERSION = "0.0.2"
6
+ VERSION = "0.0.3"
7
7
  end
@@ -39,6 +39,7 @@ Gem::Specification.new do |spec|
39
39
  spec.add_dependency 'hashie'
40
40
  spec.add_dependency 'activesupport'
41
41
  spec.add_dependency 'concurrent-ruby'
42
+ spec.add_dependency 'redis'
42
43
 
43
44
  spec.add_development_dependency 'bundler'
44
45
  spec.add_development_dependency 'rake'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: smart_message
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dewayne VanHoozer
@@ -51,6 +51,20 @@ dependencies:
51
51
  - - ">="
52
52
  - !ruby/object:Gem::Version
53
53
  version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: redis
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
54
68
  - !ruby/object:Gem::Dependency
55
69
  name: bundler
56
70
  requirement: !ruby/object:Gem::Requirement
@@ -190,6 +204,8 @@ files:
190
204
  - docs/examples.md
191
205
  - docs/getting-started.md
192
206
  - docs/ideas_to_think_about.md
207
+ - docs/message_processing.md
208
+ - docs/proc_handlers_summary.md
193
209
  - docs/properties.md
194
210
  - docs/serializers.md
195
211
  - docs/transports.md
@@ -197,7 +213,10 @@ files:
197
213
  - examples/01_point_to_point_orders.rb
198
214
  - examples/02_publish_subscribe_events.rb
199
215
  - examples/03_many_to_many_chat.rb
216
+ - examples/04_redis_smart_home_iot.rb
217
+ - examples/05_proc_handlers.rb
200
218
  - examples/README.md
219
+ - examples/smart_home_iot_dataflow.md
201
220
  - examples/tmux_chat/README.md
202
221
  - examples/tmux_chat/bot_agent.rb
203
222
  - examples/tmux_chat/human_agent.rb
@@ -221,6 +240,7 @@ files:
221
240
  - lib/smart_message/transport.rb
222
241
  - lib/smart_message/transport/base.rb
223
242
  - lib/smart_message/transport/memory_transport.rb
243
+ - lib/smart_message/transport/redis_transport.rb
224
244
  - lib/smart_message/transport/registry.rb
225
245
  - lib/smart_message/transport/stdout_transport.rb
226
246
  - lib/smart_message/version.rb