smart_message 0.0.8 → 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.irbrc +24 -0
- data/CHANGELOG.md +119 -0
- data/Gemfile.lock +6 -1
- data/README.md +389 -17
- data/docs/README.md +3 -1
- data/docs/addressing.md +119 -13
- data/docs/architecture.md +184 -46
- data/docs/dead_letter_queue.md +673 -0
- data/docs/dispatcher.md +87 -0
- data/docs/examples.md +59 -1
- data/docs/getting-started.md +8 -1
- data/docs/logging.md +382 -326
- data/docs/message_deduplication.md +488 -0
- data/docs/message_filtering.md +451 -0
- data/examples/01_point_to_point_orders.rb +54 -53
- data/examples/02_publish_subscribe_events.rb +14 -10
- data/examples/03_many_to_many_chat.rb +16 -8
- data/examples/04_redis_smart_home_iot.rb +20 -10
- data/examples/05_proc_handlers.rb +12 -11
- data/examples/06_custom_logger_example.rb +95 -100
- data/examples/07_error_handling_scenarios.rb +4 -2
- data/examples/08_entity_addressing_basic.rb +18 -6
- data/examples/08_entity_addressing_with_filtering.rb +27 -9
- data/examples/09_dead_letter_queue_demo.rb +559 -0
- data/examples/09_regex_filtering_microservices.rb +407 -0
- data/examples/10_header_block_configuration.rb +263 -0
- data/examples/10_message_deduplication.rb +209 -0
- data/examples/11_global_configuration_example.rb +219 -0
- data/examples/README.md +102 -0
- data/examples/dead_letters.jsonl +12 -0
- data/examples/performance_metrics/benchmark_results_ractor_20250818_205603.json +135 -0
- data/examples/performance_metrics/benchmark_results_ractor_20250818_205831.json +135 -0
- data/examples/performance_metrics/benchmark_results_test_20250818_204942.json +130 -0
- data/examples/performance_metrics/benchmark_results_threadpool_20250818_204942.json +130 -0
- data/examples/performance_metrics/benchmark_results_threadpool_20250818_204959.json +130 -0
- data/examples/performance_metrics/benchmark_results_threadpool_20250818_205044.json +130 -0
- data/examples/performance_metrics/benchmark_results_threadpool_20250818_205109.json +130 -0
- data/examples/performance_metrics/benchmark_results_threadpool_20250818_205252.json +130 -0
- data/examples/performance_metrics/benchmark_results_unknown_20250819_172852.json +130 -0
- data/examples/performance_metrics/compare_benchmarks.rb +519 -0
- data/examples/performance_metrics/dead_letters.jsonl +3100 -0
- data/examples/performance_metrics/performance_benchmark.rb +344 -0
- data/examples/show_logger.rb +367 -0
- data/examples/show_me.rb +145 -0
- data/examples/temp.txt +94 -0
- data/examples/tmux_chat/bot_agent.rb +4 -2
- data/examples/tmux_chat/human_agent.rb +4 -2
- data/examples/tmux_chat/room_monitor.rb +4 -2
- data/examples/tmux_chat/shared_chat_system.rb +6 -3
- data/lib/smart_message/addressing.rb +259 -0
- data/lib/smart_message/base.rb +123 -599
- data/lib/smart_message/circuit_breaker.rb +2 -1
- data/lib/smart_message/configuration.rb +199 -0
- data/lib/smart_message/ddq/base.rb +71 -0
- data/lib/smart_message/ddq/memory.rb +109 -0
- data/lib/smart_message/ddq/redis.rb +168 -0
- data/lib/smart_message/ddq.rb +31 -0
- data/lib/smart_message/dead_letter_queue.rb +27 -10
- data/lib/smart_message/deduplication.rb +174 -0
- data/lib/smart_message/dispatcher.rb +259 -61
- data/lib/smart_message/header.rb +5 -0
- data/lib/smart_message/logger/base.rb +21 -1
- data/lib/smart_message/logger/default.rb +88 -138
- data/lib/smart_message/logger/lumberjack.rb +324 -0
- data/lib/smart_message/logger/null.rb +81 -0
- data/lib/smart_message/logger.rb +17 -9
- data/lib/smart_message/messaging.rb +100 -0
- data/lib/smart_message/plugins.rb +132 -0
- data/lib/smart_message/serializer/base.rb +25 -8
- data/lib/smart_message/serializer/json.rb +5 -4
- data/lib/smart_message/subscription.rb +196 -0
- data/lib/smart_message/transport/base.rb +72 -41
- data/lib/smart_message/transport/memory_transport.rb +7 -5
- data/lib/smart_message/transport/redis_transport.rb +15 -45
- data/lib/smart_message/transport/stdout_transport.rb +18 -8
- data/lib/smart_message/transport.rb +1 -34
- data/lib/smart_message/utilities.rb +142 -0
- data/lib/smart_message/version.rb +1 -1
- data/lib/smart_message/versioning.rb +85 -0
- data/lib/smart_message/wrapper.rb.bak +132 -0
- data/lib/smart_message.rb +74 -28
- data/smart_message.gemspec +3 -0
- metadata +83 -3
- data/lib/smart_message/serializer.rb +0 -10
- data/lib/smart_message/wrapper.rb +0 -43
data/lib/smart_message/logger.rb
CHANGED
@@ -2,14 +2,22 @@
|
|
2
2
|
# encoding: utf-8
|
3
3
|
# frozen_string_literal: true
|
4
4
|
|
5
|
-
module SmartMessage::Logger
|
6
|
-
# Logger module provides logging capabilities for SmartMessage
|
7
|
-
# The Default logger automatically uses Rails.logger if available,
|
8
|
-
# otherwise falls back to a standard Ruby Logger
|
9
|
-
end # module SmartMessage::Logger
|
10
|
-
|
11
|
-
# Load the base class first
|
12
5
|
require_relative 'logger/base'
|
13
|
-
|
14
|
-
# Load the default logger implementation
|
15
6
|
require_relative 'logger/default'
|
7
|
+
require_relative 'logger/lumberjack'
|
8
|
+
|
9
|
+
module SmartMessage
|
10
|
+
module Logger
|
11
|
+
class << self
|
12
|
+
# Global default logger instance
|
13
|
+
def default
|
14
|
+
@default ||= Lumberjack.new
|
15
|
+
end
|
16
|
+
|
17
|
+
# Set the default logger
|
18
|
+
def default=(logger)
|
19
|
+
@default = logger
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# lib/smart_message/messaging.rb
|
2
|
+
# encoding: utf-8
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
module SmartMessage
|
6
|
+
# Messaging module for SmartMessage::Base
|
7
|
+
# Handles message encoding and publishing operations
|
8
|
+
module Messaging
|
9
|
+
# SMELL: How does the transport know how to decode a message before
|
10
|
+
# it knows the message class? We need a wrapper around
|
11
|
+
# the entire message in a known serialization. That
|
12
|
+
# wrapper would contain two properties: _sm_header and
|
13
|
+
# _sm_payload
|
14
|
+
|
15
|
+
# NOTE: to publish a message it must first be encoded using a
|
16
|
+
# serializer. The receive a subscribed to message it must
|
17
|
+
# be decoded via a serializer from the transport to be processed.
|
18
|
+
def encode
|
19
|
+
raise Errors::SerializerNotConfigured if serializer_missing?
|
20
|
+
|
21
|
+
serializer.encode(self)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Convert message to hash with _sm_header and _sm_payload structure
|
25
|
+
# This is the foundation for wrapper architecture
|
26
|
+
def to_h
|
27
|
+
# Update header with serializer info before converting
|
28
|
+
_sm_header.serializer = serializer.class.to_s if serializer_configured?
|
29
|
+
|
30
|
+
{
|
31
|
+
:_sm_header => header_hash_with_symbols,
|
32
|
+
:_sm_payload => payload_hash_with_symbols
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
# NOTE: you publish instances; but, you subscribe/unsubscribe at
|
38
|
+
# the class-level
|
39
|
+
def publish
|
40
|
+
begin
|
41
|
+
# Validate the complete message before publishing (now uses overridden validate!)
|
42
|
+
validate!
|
43
|
+
|
44
|
+
# Update header with current publication info
|
45
|
+
_sm_header.published_at = Time.now
|
46
|
+
_sm_header.publisher_pid = Process.pid
|
47
|
+
_sm_header.serializer = serializer.class.to_s if serializer_configured?
|
48
|
+
|
49
|
+
# Single-tier serialization: serialize entire message with designated serializer
|
50
|
+
serialized_message = encode
|
51
|
+
|
52
|
+
raise Errors::TransportNotConfigured if transport_missing?
|
53
|
+
|
54
|
+
# Transport receives the message class name (for channel routing) and serialized message
|
55
|
+
(self.class.logger || SmartMessage::Logger.default).debug { "[SmartMessage::Messaging] About to call transport.publish" }
|
56
|
+
transport.publish(_sm_header.message_class, serialized_message)
|
57
|
+
(self.class.logger || SmartMessage::Logger.default).debug { "[SmartMessage::Messaging] transport.publish completed" }
|
58
|
+
|
59
|
+
# Log the message publish
|
60
|
+
(self.class.logger || SmartMessage::Logger.default).info { "[SmartMessage] Published: #{self.class.name} via #{transport.class.name.split('::').last}" }
|
61
|
+
|
62
|
+
SS.add(_sm_header.message_class, 'publish')
|
63
|
+
SS.get(_sm_header.message_class, 'publish')
|
64
|
+
rescue => e
|
65
|
+
(self.class.logger || SmartMessage::Logger.default).error { "[SmartMessage] Error in message publishing: #{e.class.name} - #{e.message}" }
|
66
|
+
raise
|
67
|
+
end
|
68
|
+
end # def publish
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
# Convert header to hash with symbol keys
|
73
|
+
def header_hash_with_symbols
|
74
|
+
_sm_header.to_hash.transform_keys(&:to_sym)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Extract all non-header properties into a hash with symbol keys
|
78
|
+
# Performs deep symbolization on nested structures
|
79
|
+
def payload_hash_with_symbols
|
80
|
+
self.class.properties.each_with_object({}) do |prop, hash|
|
81
|
+
next if prop == :_sm_header
|
82
|
+
hash[prop.to_sym] = deep_symbolize_keys(self[prop])
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Recursively convert all string keys to symbols in nested hashes and arrays
|
87
|
+
def deep_symbolize_keys(obj)
|
88
|
+
case obj
|
89
|
+
when Hash
|
90
|
+
obj.each_with_object({}) do |(key, value), result|
|
91
|
+
result[key.to_sym] = deep_symbolize_keys(value)
|
92
|
+
end
|
93
|
+
when Array
|
94
|
+
obj.map { |item| deep_symbolize_keys(item) }
|
95
|
+
else
|
96
|
+
obj
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
# lib/smart_message/plugins.rb
|
2
|
+
# encoding: utf-8
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
module SmartMessage
|
6
|
+
# Plugin configuration module for SmartMessage::Base
|
7
|
+
# Handles transport, serializer, and logger configuration at both
|
8
|
+
# class and instance levels
|
9
|
+
module Plugins
|
10
|
+
def self.included(base)
|
11
|
+
base.extend(ClassMethods)
|
12
|
+
base.class_eval do
|
13
|
+
# Class-level plugin storage
|
14
|
+
class_variable_set(:@@transport, nil) unless class_variable_defined?(:@@transport)
|
15
|
+
class_variable_set(:@@serializer, nil) unless class_variable_defined?(:@@serializer)
|
16
|
+
class_variable_set(:@@logger, nil) unless class_variable_defined?(:@@logger)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
#########################################################
|
21
|
+
## instance-level configuration
|
22
|
+
|
23
|
+
# Configure the plugins for transport, serializer and logger
|
24
|
+
def config(&block)
|
25
|
+
instance_eval(&block) if block_given?
|
26
|
+
end
|
27
|
+
|
28
|
+
#########################################################
|
29
|
+
## instance-level transport configuration
|
30
|
+
|
31
|
+
def transport(klass_or_instance = nil)
|
32
|
+
if klass_or_instance.nil?
|
33
|
+
# Return instance transport, class transport, or global configuration
|
34
|
+
@transport || self.class.class_variable_get(:@@transport) || SmartMessage::Transport.default
|
35
|
+
else
|
36
|
+
@transport = klass_or_instance
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def transport_configured?; !transport_missing?; end
|
41
|
+
def transport_missing?
|
42
|
+
# Check if transport is explicitly configured (without fallback to defaults)
|
43
|
+
@transport.nil? &&
|
44
|
+
(self.class.class_variable_get(:@@transport) rescue nil).nil?
|
45
|
+
end
|
46
|
+
def reset_transport; @transport = nil; end
|
47
|
+
|
48
|
+
|
49
|
+
#########################################################
|
50
|
+
## instance-level serializer configuration
|
51
|
+
|
52
|
+
def serializer(klass_or_instance = nil)
|
53
|
+
if klass_or_instance.nil?
|
54
|
+
# Return instance serializer, class serializer, or global configuration
|
55
|
+
@serializer || self.class.class_variable_get(:@@serializer) || SmartMessage::Serializer.default
|
56
|
+
else
|
57
|
+
@serializer = klass_or_instance
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def serializer_configured?; !serializer_missing?; end
|
62
|
+
def serializer_missing?
|
63
|
+
# Check if serializer is explicitly configured (without fallback to defaults)
|
64
|
+
@serializer.nil? &&
|
65
|
+
(self.class.class_variable_get(:@@serializer) rescue nil).nil?
|
66
|
+
end
|
67
|
+
def reset_serializer; @serializer = nil; end
|
68
|
+
|
69
|
+
module ClassMethods
|
70
|
+
#########################################################
|
71
|
+
## class-level configuration
|
72
|
+
|
73
|
+
def config(&block)
|
74
|
+
class_eval(&block) if block_given?
|
75
|
+
end
|
76
|
+
|
77
|
+
#########################################################
|
78
|
+
## class-level transport configuration
|
79
|
+
|
80
|
+
def transport(klass_or_instance = nil)
|
81
|
+
if klass_or_instance.nil?
|
82
|
+
# Return class-level transport or fall back to global configuration
|
83
|
+
class_variable_get(:@@transport) || SmartMessage::Transport.default
|
84
|
+
else
|
85
|
+
class_variable_set(:@@transport, klass_or_instance)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def transport_configured?; !transport_missing?; end
|
90
|
+
def transport_missing?
|
91
|
+
# Check if class-level transport is explicitly configured (without fallback to defaults)
|
92
|
+
(class_variable_get(:@@transport) rescue nil).nil?
|
93
|
+
end
|
94
|
+
def reset_transport; class_variable_set(:@@transport, nil); end
|
95
|
+
|
96
|
+
#########################################################
|
97
|
+
## class-level logger configuration
|
98
|
+
|
99
|
+
def logger(klass_or_instance = nil)
|
100
|
+
if klass_or_instance.nil?
|
101
|
+
# Return class-level logger or fall back to global configuration
|
102
|
+
class_variable_get(:@@logger) || SmartMessage::Logger.default
|
103
|
+
else
|
104
|
+
class_variable_set(:@@logger, klass_or_instance)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def logger_configured?; !logger.nil?; end
|
109
|
+
def logger_missing?; logger.nil?; end
|
110
|
+
def reset_logger; class_variable_set(:@@logger, nil); end
|
111
|
+
|
112
|
+
#########################################################
|
113
|
+
## class-level serializer configuration
|
114
|
+
|
115
|
+
def serializer(klass_or_instance = nil)
|
116
|
+
if klass_or_instance.nil?
|
117
|
+
# Return class-level serializer or fall back to global configuration
|
118
|
+
class_variable_get(:@@serializer) || SmartMessage::Serializer.default
|
119
|
+
else
|
120
|
+
class_variable_set(:@@serializer, klass_or_instance)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def serializer_configured?; !serializer_missing?; end
|
125
|
+
def serializer_missing?
|
126
|
+
# Check if class-level serializer is explicitly configured (without fallback to defaults)
|
127
|
+
(class_variable_get(:@@serializer) rescue nil).nil?
|
128
|
+
end
|
129
|
+
def reset_serializer; class_variable_set(:@@serializer, nil); end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -2,6 +2,7 @@
|
|
2
2
|
# encoding: utf-8
|
3
3
|
# frozen_string_literal: true
|
4
4
|
|
5
|
+
require 'json' # STDLIB
|
5
6
|
require_relative '../circuit_breaker'
|
6
7
|
|
7
8
|
module SmartMessage::Serializer
|
@@ -12,7 +13,20 @@ module SmartMessage::Serializer
|
|
12
13
|
# provide basic configuration
|
13
14
|
def initialize
|
14
15
|
configure_serializer_circuit_breakers
|
16
|
+
|
17
|
+
logger.debug { "[SmartMessage::Serializer::#{self.class.name.split('::').last}] Initialized" }
|
18
|
+
rescue => e
|
19
|
+
logger&.error { "[SmartMessage] Error in serializer initialization: #{e.class.name} - #{e.message}" }
|
20
|
+
raise
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def logger
|
26
|
+
@logger ||= SmartMessage::Logger.default
|
15
27
|
end
|
28
|
+
|
29
|
+
public
|
16
30
|
|
17
31
|
def encode(message_instance)
|
18
32
|
circuit(:serializer).wrap do
|
@@ -39,10 +53,16 @@ module SmartMessage::Serializer
|
|
39
53
|
raise
|
40
54
|
end
|
41
55
|
end
|
56
|
+
|
57
|
+
private
|
42
58
|
|
43
59
|
# Template methods for actual serialization (implement in subclasses)
|
44
60
|
def do_encode(message_instance)
|
45
|
-
|
61
|
+
# Default implementation: serialize only the payload portion for wrapper architecture
|
62
|
+
# Subclasses can override this for specific serialization formats
|
63
|
+
message_hash = message_instance.to_h
|
64
|
+
payload_portion = message_hash[:_sm_payload]
|
65
|
+
::JSON.generate(payload_portion)
|
46
66
|
end
|
47
67
|
|
48
68
|
def do_decode(payload)
|
@@ -81,13 +101,10 @@ module SmartMessage::Serializer
|
|
81
101
|
|
82
102
|
# Handle serializer circuit breaker fallback
|
83
103
|
def handle_serializer_fallback(fallback_result, operation, data)
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
end
|
89
|
-
|
90
|
-
# TODO: Integrate with structured logging when implemented
|
104
|
+
# Integrate with structured logging
|
105
|
+
logger.error { "[SmartMessage::Serializer] Circuit breaker activated: #{self.class.name}" }
|
106
|
+
logger.error { "[SmartMessage::Serializer] Operation: #{operation}" }
|
107
|
+
logger.error { "[SmartMessage::Serializer] Error: #{fallback_result[:circuit_breaker][:error]}" }
|
91
108
|
|
92
109
|
# Return the fallback result
|
93
110
|
fallback_result
|
@@ -5,11 +5,12 @@
|
|
5
5
|
require 'json' # STDLIB
|
6
6
|
|
7
7
|
module SmartMessage::Serializer
|
8
|
-
class
|
8
|
+
class Json < Base
|
9
9
|
def do_encode(message_instance)
|
10
|
-
#
|
11
|
-
#
|
12
|
-
message_instance.
|
10
|
+
# Single-tier serialization: serialize the complete message structure
|
11
|
+
# This includes both header and payload for full message reconstruction
|
12
|
+
message_hash = message_instance.to_h
|
13
|
+
::JSON.generate(message_hash)
|
13
14
|
end
|
14
15
|
|
15
16
|
def do_decode(payload)
|
@@ -0,0 +1,196 @@
|
|
1
|
+
# lib/smart_message/subscription.rb
|
2
|
+
# encoding: utf-8
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
require 'securerandom' # STDLIB
|
6
|
+
|
7
|
+
module SmartMessage
|
8
|
+
# Subscription management module for SmartMessage::Base
|
9
|
+
# Handles subscribe/unsubscribe operations and proc handler management
|
10
|
+
module Subscription
|
11
|
+
def self.included(base)
|
12
|
+
base.extend(ClassMethods)
|
13
|
+
base.class_eval do
|
14
|
+
# Registry for proc-based message handlers
|
15
|
+
class_variable_set(:@@proc_handlers, {}) unless class_variable_defined?(:@@proc_handlers)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
module ClassMethods
|
20
|
+
#########################################################
|
21
|
+
## proc handler management
|
22
|
+
|
23
|
+
# Register a proc handler and return a unique identifier for it
|
24
|
+
# @param message_class [String] The message class name
|
25
|
+
# @param handler_proc [Proc] The proc to register
|
26
|
+
# @return [String] Unique identifier for this handler
|
27
|
+
def register_proc_handler(message_class, handler_proc)
|
28
|
+
handler_id = "#{message_class}.proc_#{SecureRandom.hex(8)}"
|
29
|
+
class_variable_get(:@@proc_handlers)[handler_id] = handler_proc
|
30
|
+
handler_id
|
31
|
+
end
|
32
|
+
|
33
|
+
# Call a registered proc handler
|
34
|
+
# @param handler_id [String] The handler identifier
|
35
|
+
# @param wrapper [SmartMessage::Wrapper::Base] The message wrapper
|
36
|
+
def call_proc_handler(handler_id, wrapper)
|
37
|
+
handler_proc = class_variable_get(:@@proc_handlers)[handler_id]
|
38
|
+
return unless handler_proc
|
39
|
+
|
40
|
+
handler_proc.call(wrapper)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Remove a proc handler from the registry
|
44
|
+
# @param handler_id [String] The handler identifier to remove
|
45
|
+
def unregister_proc_handler(handler_id)
|
46
|
+
class_variable_get(:@@proc_handlers).delete(handler_id)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Check if a handler ID refers to a proc handler
|
50
|
+
# @param handler_id [String] The handler identifier
|
51
|
+
# @return [Boolean] True if this is a proc handler
|
52
|
+
def proc_handler?(handler_id)
|
53
|
+
class_variable_get(:@@proc_handlers).key?(handler_id)
|
54
|
+
end
|
55
|
+
|
56
|
+
#########################################################
|
57
|
+
## class-level subscription management via the transport
|
58
|
+
|
59
|
+
# Add this message class to the transport's catalog of
|
60
|
+
# subscribed messages. If the transport is missing, raise
|
61
|
+
# an exception.
|
62
|
+
#
|
63
|
+
# @param process_method [String, Proc, nil] The processing method:
|
64
|
+
# - String: Method name like "MyService.handle_message"
|
65
|
+
# - Proc: A proc/lambda that accepts (message_header, message_payload)
|
66
|
+
# - nil: Uses default "MessageClass.process" method
|
67
|
+
# @param broadcast [Boolean, nil] Filter for broadcast messages (to: nil)
|
68
|
+
# @param to [String, Array, nil] Filter for messages directed to specific entities
|
69
|
+
# @param from [String, Array, nil] Filter for messages from specific entities
|
70
|
+
# @param block [Proc] Alternative way to pass a processing block
|
71
|
+
# @return [String] The identifier used for this subscription
|
72
|
+
#
|
73
|
+
# @example Using default handler
|
74
|
+
# MyMessage.subscribe
|
75
|
+
#
|
76
|
+
# @example Using custom method name with filtering
|
77
|
+
# MyMessage.subscribe("MyService.handle_message", from: ['order-service'])
|
78
|
+
#
|
79
|
+
# @example Using a block with broadcast filtering
|
80
|
+
# MyMessage.subscribe(broadcast: true) do |header, payload|
|
81
|
+
# data = JSON.parse(payload)
|
82
|
+
# puts "Received broadcast: #{data}"
|
83
|
+
# end
|
84
|
+
#
|
85
|
+
# @example Entity-specific filtering (receives only messages from payment service)
|
86
|
+
# MyMessage.subscribe("OrderService.process", from: ['payment'])
|
87
|
+
#
|
88
|
+
# @example Explicit to filter
|
89
|
+
# MyMessage.subscribe("AdminService.handle", to: 'admin', broadcast: false)
|
90
|
+
def subscribe(process_method = nil, broadcast: nil, to: nil, from: nil, &block)
|
91
|
+
message_class = whoami
|
92
|
+
|
93
|
+
# Handle different parameter types
|
94
|
+
if block_given?
|
95
|
+
# Block was passed - use it as the handler
|
96
|
+
handler_proc = block
|
97
|
+
process_method = register_proc_handler(message_class, handler_proc)
|
98
|
+
elsif process_method.respond_to?(:call)
|
99
|
+
# Proc/lambda was passed as first parameter
|
100
|
+
handler_proc = process_method
|
101
|
+
process_method = register_proc_handler(message_class, handler_proc)
|
102
|
+
elsif process_method.nil?
|
103
|
+
# Use default handler
|
104
|
+
process_method = message_class + '.process'
|
105
|
+
end
|
106
|
+
# If process_method is a String, use it as-is
|
107
|
+
|
108
|
+
# Subscriber identity is derived from the process method (handler)
|
109
|
+
# This ensures each handler gets its own DDQ scope per message class
|
110
|
+
|
111
|
+
# Normalize string filters to arrays
|
112
|
+
to_filter = normalize_filter_value(to)
|
113
|
+
from_filter = normalize_filter_value(from)
|
114
|
+
|
115
|
+
# Create filter options (no explicit subscriber identity needed)
|
116
|
+
filter_options = {
|
117
|
+
broadcast: broadcast,
|
118
|
+
to: to_filter,
|
119
|
+
from: from_filter
|
120
|
+
}
|
121
|
+
|
122
|
+
# Add proper logging
|
123
|
+
logger = SmartMessage::Logger.default
|
124
|
+
|
125
|
+
begin
|
126
|
+
raise Errors::TransportNotConfigured if transport_missing?
|
127
|
+
transport.subscribe(message_class, process_method, filter_options)
|
128
|
+
|
129
|
+
# Log successful subscription
|
130
|
+
handler_desc = block_given? || process_method.respond_to?(:call) ? " with block/proc handler" : ""
|
131
|
+
logger.info { "[SmartMessage] Subscribed: #{self.name}#{handler_desc}" }
|
132
|
+
logger.debug { "[SmartMessage::Subscription] Subscribed #{message_class} with filters: #{filter_options}" }
|
133
|
+
|
134
|
+
process_method
|
135
|
+
rescue => e
|
136
|
+
logger.error { "[SmartMessage] Error in message subscription: #{e.class.name} - #{e.message}" }
|
137
|
+
raise
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Remove this process_method for this message class from the
|
142
|
+
# subscribers list.
|
143
|
+
# @param process_method [String, nil] The processing method identifier to remove
|
144
|
+
# - String: Method name like "MyService.handle_message" or proc handler ID
|
145
|
+
# - nil: Uses default "MessageClass.process" method
|
146
|
+
def unsubscribe(process_method = nil)
|
147
|
+
message_class = whoami
|
148
|
+
process_method = message_class + '.process' if process_method.nil?
|
149
|
+
# Add proper logging
|
150
|
+
logger = SmartMessage::Logger.default
|
151
|
+
|
152
|
+
begin
|
153
|
+
if transport_configured?
|
154
|
+
transport.unsubscribe(message_class, process_method)
|
155
|
+
|
156
|
+
# If this was a proc handler, clean it up from the registry
|
157
|
+
if proc_handler?(process_method)
|
158
|
+
unregister_proc_handler(process_method)
|
159
|
+
end
|
160
|
+
|
161
|
+
# Log successful unsubscription
|
162
|
+
logger.info { "[SmartMessage] Unsubscribed: #{self.name}" }
|
163
|
+
logger.debug { "[SmartMessage::Subscription] Unsubscribed #{message_class} from #{process_method}" }
|
164
|
+
end
|
165
|
+
rescue => e
|
166
|
+
logger.error { "[SmartMessage] Error in message unsubscription: #{e.class.name} - #{e.message}" }
|
167
|
+
raise
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
# Remove this message class and all of its processing methods
|
172
|
+
# from the subscribers list.
|
173
|
+
def unsubscribe!
|
174
|
+
message_class = whoami
|
175
|
+
|
176
|
+
# TODO: Add proper logging here
|
177
|
+
|
178
|
+
transport.unsubscribe!(message_class) if transport_configured?
|
179
|
+
end
|
180
|
+
|
181
|
+
###################################################
|
182
|
+
## Business Logic resides in the #process method.
|
183
|
+
|
184
|
+
# When a transport receives a subscribed to message it
|
185
|
+
# creates an instance of the message and then calls
|
186
|
+
# the process method on that instance.
|
187
|
+
#
|
188
|
+
# It is expected that SmartMessage classes over ride
|
189
|
+
# the SmartMessage::Base#process method with appropriate
|
190
|
+
# business logic to handle the received message content.
|
191
|
+
def process(message_instance)
|
192
|
+
raise Errors::NotImplemented
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|