nats_wave 1.1.4 → 1.1.7
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/.idea/nats_wave.iml +5 -5
- data/Gemfile.lock +1 -1
- data/README.md +1153 -401
- data/lib/generators/nats_wave/templates/initializer.rb +103 -54
- data/lib/nats_wave/adapters/datadog_metrics.rb +1 -1
- data/lib/nats_wave/client.rb +638 -29
- data/lib/nats_wave/concerns/mappable.rb +9 -3
- data/lib/nats_wave/configuration.rb +5 -4
- data/lib/nats_wave/dead_letter_queue.rb +4 -4
- data/lib/nats_wave/model_mapper.rb +1 -1
- data/lib/nats_wave/model_registry.rb +39 -15
- data/lib/nats_wave/publisher.rb +56 -61
- data/lib/nats_wave/railtie.rb +4 -3
- data/lib/nats_wave/subscriber.rb +127 -47
- data/lib/nats_wave/version.rb +1 -1
- data/lib/nats_wave.rb +2 -1
- metadata +2 -5
- data/examples/catalog_model.rb +0 -36
- data/examples/configuration_examples.rb +0 -68
- data/examples/user_model.rb +0 -58
@@ -22,6 +22,8 @@ module NatsWave
|
|
22
22
|
|
23
23
|
# Configure how external models map to this model AND what subjects to subscribe to
|
24
24
|
def nats_wave_maps_from(external_model, options = {})
|
25
|
+
Rails.logger.debug "🔄 #{self.name}: Setting up mapping from #{external_model} with options: #{options.inspect}" if defined?(Rails)
|
26
|
+
|
25
27
|
mapping = {
|
26
28
|
field_mappings: options[:field_mappings] || {},
|
27
29
|
transformations: options[:transformations] || {},
|
@@ -29,7 +31,6 @@ module NatsWave
|
|
29
31
|
sync_strategy: options[:sync_strategy] || :upsert,
|
30
32
|
unique_fields: options[:unique_fields] || [:id],
|
31
33
|
skip_fields: options[:skip_fields] || [],
|
32
|
-
# NEW: Subscription configuration
|
33
34
|
subjects: options[:subjects] || [],
|
34
35
|
handler: options[:handler],
|
35
36
|
queue_group: options[:queue_group]
|
@@ -37,11 +38,12 @@ module NatsWave
|
|
37
38
|
|
38
39
|
self.nats_wave_mapping_config[external_model] = mapping
|
39
40
|
|
40
|
-
#
|
41
|
+
Rails.logger.debug "🔄 #{self.name}: Registering reverse mapping for #{external_model}" if defined?(Rails)
|
41
42
|
NatsWave::ModelRegistry.register_reverse_mapping(external_model, self.name, mapping)
|
42
43
|
|
43
44
|
# Register subscription if subjects are provided
|
44
45
|
if mapping[:subjects].any?
|
46
|
+
Rails.logger.debug "🔄 #{self.name}: Registering subscription for subjects: #{mapping[:subjects]}" if defined?(Rails)
|
45
47
|
NatsWave::ModelRegistry.register_subscription(
|
46
48
|
subjects: mapping[:subjects],
|
47
49
|
model: self.name,
|
@@ -49,6 +51,8 @@ module NatsWave
|
|
49
51
|
handler: mapping[:handler],
|
50
52
|
queue_group: mapping[:queue_group]
|
51
53
|
)
|
54
|
+
else
|
55
|
+
Rails.logger.debug "🔄 #{self.name}: No subjects provided, skipping subscription registration" if defined?(Rails)
|
52
56
|
end
|
53
57
|
end
|
54
58
|
|
@@ -56,6 +60,8 @@ module NatsWave
|
|
56
60
|
def nats_wave_subscribes_to(*subjects, handler: nil, queue_group: nil, &block)
|
57
61
|
handler ||= block
|
58
62
|
|
63
|
+
Rails.logger.debug "📡 #{self.name}: Setting up subscription to subjects: #{subjects.inspect}" if defined?(Rails)
|
64
|
+
|
59
65
|
subscription_config = {
|
60
66
|
subjects: subjects.flatten,
|
61
67
|
handler: handler,
|
@@ -65,7 +71,7 @@ module NatsWave
|
|
65
71
|
|
66
72
|
self.nats_wave_subscription_config[:custom] = subscription_config
|
67
73
|
|
68
|
-
#
|
74
|
+
Rails.logger.debug "📡 #{self.name}: Registering subscription in ModelRegistry" if defined?(Rails)
|
69
75
|
NatsWave::ModelRegistry.register_subscription(
|
70
76
|
subjects: subjects.flatten,
|
71
77
|
model: self.name,
|
@@ -16,9 +16,9 @@ module NatsWave
|
|
16
16
|
:subscriptions, :publications
|
17
17
|
|
18
18
|
def initialize(options = {})
|
19
|
-
@nats_url = ENV['NATS_URL']
|
20
|
-
@service_name = ENV['
|
21
|
-
@version = ENV['NATS_SERVICE_VERSION'] || "1.1.
|
19
|
+
@nats_url = ENV['NATS_URL'] || "nats://localhost:4222"
|
20
|
+
@service_name = ENV['NATS_SERVICE_NAME'] || "purplewave"
|
21
|
+
@version = ENV['NATS_SERVICE_VERSION'] || "1.1.7"
|
22
22
|
@instance_id = ENV['NATS_INSTANCE_ID'] || Socket.gethostname
|
23
23
|
@database_url = ENV['NATS_DATABASE_URL'] || nil
|
24
24
|
@connection_pool_size = (ENV['NATS_CONNECTION_POOL_SIZE'] || 10).to_i
|
@@ -71,7 +71,7 @@ module NatsWave
|
|
71
71
|
}
|
72
72
|
end
|
73
73
|
|
74
|
-
def add_publication(model_class, actions = [
|
74
|
+
def add_publication(model_class, actions = %i[create update destroy])
|
75
75
|
@publications << {
|
76
76
|
model: model_class,
|
77
77
|
actions: Array(actions)
|
@@ -80,6 +80,7 @@ module NatsWave
|
|
80
80
|
|
81
81
|
def nats_server_url
|
82
82
|
# This is the URL other teams can use to subscribe to your events
|
83
|
+
return nil if @nats_url.nil?
|
83
84
|
uri = URI.parse(@nats_url)
|
84
85
|
"#{uri.scheme}://#{uri.host}:#{uri.port}"
|
85
86
|
end
|
@@ -58,7 +58,7 @@ module NatsWave
|
|
58
58
|
private
|
59
59
|
|
60
60
|
def setup_storage
|
61
|
-
# For production,
|
61
|
+
# For production, we'd want to use Redis, database, or file storage
|
62
62
|
# For now, use in-memory storage
|
63
63
|
InMemoryStorage.new
|
64
64
|
end
|
@@ -70,11 +70,11 @@ module NatsWave
|
|
70
70
|
end
|
71
71
|
|
72
72
|
def retry_message(failed_message)
|
73
|
-
# This would need to be implemented based on
|
73
|
+
# This would need to be implemented based on wer retry strategy
|
74
74
|
# For now, just log that we would retry
|
75
75
|
NatsWave.logger.info("Retrying message: #{failed_message[:id]}")
|
76
76
|
|
77
|
-
# In a real implementation,
|
77
|
+
# In a real implementation, we would:
|
78
78
|
# 1. Re-parse the message
|
79
79
|
# 2. Re-apply middleware
|
80
80
|
# 3. Re-process the message
|
@@ -138,7 +138,7 @@ module NatsWave
|
|
138
138
|
end
|
139
139
|
|
140
140
|
def mark_permanent_failure(message)
|
141
|
-
# In a real implementation,
|
141
|
+
# In a real implementation, we might move this to a different storage
|
142
142
|
update(message)
|
143
143
|
end
|
144
144
|
end
|
@@ -9,11 +9,13 @@ module NatsWave
|
|
9
9
|
class << self
|
10
10
|
# Register a mapping from local model to external models
|
11
11
|
def register_mapping(local_model, external_mappings)
|
12
|
+
Rails.logger.debug "🗂️ Registering mapping: #{local_model} -> #{external_mappings.inspect}" if defined?(Rails)
|
12
13
|
@mappings[local_model] = external_mappings
|
13
14
|
end
|
14
15
|
|
15
16
|
# Register a reverse mapping from external model to local model
|
16
17
|
def register_reverse_mapping(external_model, local_model, mapping_config)
|
18
|
+
Rails.logger.debug "🔄 Registering reverse mapping: #{external_model} -> #{local_model} with config: #{mapping_config.inspect}" if defined?(Rails)
|
17
19
|
@reverse_mappings[external_model] ||= {}
|
18
20
|
@reverse_mappings[external_model][local_model] = mapping_config
|
19
21
|
end
|
@@ -34,22 +36,52 @@ module NatsWave
|
|
34
36
|
Rails.logger.debug "📡 Registered subscription: #{model} -> #{subjects.join(', ')}" if defined?(Rails)
|
35
37
|
end
|
36
38
|
|
37
|
-
#
|
39
|
+
# Debug method to show all registrations
|
40
|
+
def debug_registrations
|
41
|
+
puts "\n" + "="*80
|
42
|
+
puts "🔍 NATS WAVE DEBUG - ALL REGISTRATIONS"
|
43
|
+
puts "="*80
|
44
|
+
|
45
|
+
puts "\n📡 SUBSCRIPTIONS (#{@subscriptions.size}):"
|
46
|
+
@subscriptions.each_with_index do |sub, i|
|
47
|
+
puts " #{i+1}. Model: #{sub[:model]}"
|
48
|
+
puts " Subjects: #{sub[:subjects].join(', ')}"
|
49
|
+
puts " External Model: #{sub[:external_model] || 'N/A'}"
|
50
|
+
puts " Handler: #{sub[:handler] ? 'Yes' : 'No'}"
|
51
|
+
puts " Queue Group: #{sub[:queue_group] || 'Default'}"
|
52
|
+
puts " Registered: #{sub[:registered_at]}"
|
53
|
+
puts ""
|
54
|
+
end
|
55
|
+
|
56
|
+
puts "\n🗂️ MAPPINGS (#{@mappings.size}):"
|
57
|
+
@mappings.each do |local_model, external_mappings|
|
58
|
+
puts " #{local_model} -> #{external_mappings.inspect}"
|
59
|
+
end
|
60
|
+
|
61
|
+
puts "\n🔄 REVERSE MAPPINGS (#{@reverse_mappings.size}):"
|
62
|
+
@reverse_mappings.each do |external_model, local_mappings|
|
63
|
+
puts " #{external_model}:"
|
64
|
+
local_mappings.each do |local_model, config|
|
65
|
+
puts " -> #{local_model}: #{config.keys.join(', ')}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
puts "="*80 + "\n"
|
70
|
+
end
|
71
|
+
|
72
|
+
# Rest of your existing methods...
|
38
73
|
def subscriptions
|
39
74
|
@subscriptions
|
40
75
|
end
|
41
76
|
|
42
|
-
# Get subscriptions for a specific model
|
43
77
|
def subscriptions_for_model(model_name)
|
44
78
|
@subscriptions.select { |sub| sub[:model] == model_name }
|
45
79
|
end
|
46
80
|
|
47
|
-
# Get all unique subjects to subscribe to
|
48
81
|
def all_subscription_subjects
|
49
82
|
@subscriptions.flat_map { |sub| sub[:subjects] }.uniq
|
50
83
|
end
|
51
84
|
|
52
|
-
# Get subscription handlers grouped by subject pattern
|
53
85
|
def subscription_handlers
|
54
86
|
handlers = {}
|
55
87
|
|
@@ -68,22 +100,18 @@ module NatsWave
|
|
68
100
|
handlers
|
69
101
|
end
|
70
102
|
|
71
|
-
# Get local model mappings for an external model
|
72
103
|
def local_models_for(external_model)
|
73
104
|
@reverse_mappings[external_model] || {}
|
74
105
|
end
|
75
106
|
|
76
|
-
# Get external model mappings for a local model
|
77
107
|
def external_models_for(local_model)
|
78
108
|
@mappings[local_model] || {}
|
79
109
|
end
|
80
110
|
|
81
|
-
# Check if an external model can be synced
|
82
111
|
def can_sync_external_model?(external_model)
|
83
112
|
@reverse_mappings.key?(external_model)
|
84
113
|
end
|
85
114
|
|
86
|
-
# Get all registered mappings
|
87
115
|
def all_mappings
|
88
116
|
{
|
89
117
|
local_to_external: @mappings,
|
@@ -91,15 +119,14 @@ module NatsWave
|
|
91
119
|
}
|
92
120
|
end
|
93
121
|
|
94
|
-
# Find the best local model for an external model
|
95
122
|
def best_local_model_for(external_model, data = {})
|
96
123
|
local_mappings = local_models_for(external_model)
|
97
124
|
return nil if local_mappings.empty?
|
98
125
|
|
99
|
-
|
100
|
-
|
126
|
+
if local_mappings.size == 1
|
127
|
+
return local_mappings.keys.first
|
128
|
+
end
|
101
129
|
|
102
|
-
# If multiple mappings, try to find the best match based on conditions
|
103
130
|
local_mappings.each do |local_model, config|
|
104
131
|
conditions = config[:conditions] || {}
|
105
132
|
|
@@ -108,18 +135,15 @@ module NatsWave
|
|
108
135
|
end
|
109
136
|
end
|
110
137
|
|
111
|
-
# Fallback to first available
|
112
138
|
local_mappings.keys.first
|
113
139
|
end
|
114
140
|
|
115
|
-
# Clear all mappings and subscriptions (useful for testing)
|
116
141
|
def clear!
|
117
142
|
@mappings.clear
|
118
143
|
@reverse_mappings.clear
|
119
144
|
@subscriptions.clear
|
120
145
|
end
|
121
146
|
|
122
|
-
# Get subscription statistics
|
123
147
|
def subscription_stats
|
124
148
|
{
|
125
149
|
total_subscriptions: @subscriptions.size,
|
data/lib/nats_wave/publisher.rb
CHANGED
@@ -1,53 +1,55 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'securerandom'
|
4
|
-
require 'json'
|
1
|
+
# # frozen_string_literal: true
|
2
|
+
#
|
3
|
+
# require 'securerandom'
|
4
|
+
# require 'json'
|
5
|
+
#
|
6
|
+
# # begin
|
7
|
+
# # require 'nats/client'
|
8
|
+
# # rescue LoadError
|
9
|
+
# # # NATS not available - define a mock for testing
|
10
|
+
# # module NATS
|
11
|
+
# # def self.connect(url, options = {})
|
12
|
+
# # NatsClient.new
|
13
|
+
# # end
|
14
|
+
# #
|
15
|
+
# # class NatsClient
|
16
|
+
# # def connected?
|
17
|
+
# # false
|
18
|
+
# # end
|
19
|
+
# #
|
20
|
+
# # def publish(subject, message)
|
21
|
+
# # puts "NATS: Publishing to #{subject}: #{message}"
|
22
|
+
# # end
|
23
|
+
# #
|
24
|
+
# # def subscribe(subject, options = {})
|
25
|
+
# # puts "NATS: Subscribing to #{subject}"
|
26
|
+
# # yield('{"mock": "message"}') if block_given?
|
27
|
+
# # Subscription.new
|
28
|
+
# # end
|
29
|
+
# #
|
30
|
+
# # def close
|
31
|
+
# # true
|
32
|
+
# # end
|
33
|
+
# # end
|
34
|
+
# #
|
35
|
+
# # class Subscription
|
36
|
+
# # def unsubscribe
|
37
|
+
# # true
|
38
|
+
# # end
|
39
|
+
# # end
|
40
|
+
# # end
|
41
|
+
# # end
|
42
|
+
#
|
5
43
|
|
6
|
-
|
7
|
-
require 'nats/client'
|
8
|
-
rescue LoadError
|
9
|
-
# NATS not available - define a mock for testing
|
10
|
-
module NATS
|
11
|
-
def self.connect(url, options = {})
|
12
|
-
NatsClient.new
|
13
|
-
end
|
14
|
-
|
15
|
-
class NatsClient
|
16
|
-
def connected?
|
17
|
-
false
|
18
|
-
end
|
19
|
-
|
20
|
-
def publish(subject, message)
|
21
|
-
puts "NATS: Publishing to #{subject}: #{message}"
|
22
|
-
end
|
23
|
-
|
24
|
-
def subscribe(subject, options = {})
|
25
|
-
puts "NATS: Subscribing to #{subject}"
|
26
|
-
yield('{"mock": "message"}') if block_given?
|
27
|
-
Subscription.new
|
28
|
-
end
|
29
|
-
|
30
|
-
def close
|
31
|
-
true
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
class Subscription
|
36
|
-
def unsubscribe
|
37
|
-
true
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
44
|
+
# frozen_string_literal: true
|
42
45
|
|
43
46
|
module NatsWave
|
44
47
|
class Publisher
|
45
|
-
attr_reader :config, :
|
48
|
+
attr_reader :config, :client
|
46
49
|
|
47
|
-
def initialize(config,
|
50
|
+
def initialize(config, client, middleware_stack = [])
|
48
51
|
@config = config
|
49
|
-
@
|
50
|
-
@middleware_stack = middleware_stack
|
52
|
+
@client = client
|
51
53
|
@message_transformer = MessageTransformer.new(config)
|
52
54
|
@dead_letter_queue = DeadLetterQueue.new(config) if config.dead_letter_queue
|
53
55
|
end
|
@@ -56,17 +58,15 @@ module NatsWave
|
|
56
58
|
return unless @config.publishing_enabled
|
57
59
|
|
58
60
|
message = build_message(subject, model, action, data, metadata)
|
59
|
-
processed_message = apply_middleware(message)
|
60
61
|
full_subject = build_full_subject(subject)
|
61
62
|
|
62
63
|
if @config.async_publishing && defined?(Concurrent)
|
63
|
-
publish_async(full_subject,
|
64
|
+
publish_async(full_subject, message)
|
64
65
|
else
|
65
|
-
publish_sync(full_subject,
|
66
|
+
publish_sync(full_subject, message)
|
66
67
|
end
|
67
68
|
|
68
|
-
Metrics.increment_published_messages(full_subject)
|
69
|
-
NatsWave.logger.debug("Published message to #{full_subject}")
|
69
|
+
# Metrics.increment_published_messages(full_subject)
|
70
70
|
rescue => e
|
71
71
|
NatsWave.logger.error("Failed to publish message: #{e.message}")
|
72
72
|
@dead_letter_queue&.store_failed_message(message, e, 0)
|
@@ -81,17 +81,16 @@ module NatsWave
|
|
81
81
|
source: build_source_info
|
82
82
|
}
|
83
83
|
|
84
|
-
processed_message = apply_middleware(batch_message)
|
85
84
|
subject = "#{@config.default_subject_prefix}.batch"
|
86
85
|
|
87
|
-
@
|
86
|
+
@client.publish(subject, batch_message.to_json)
|
88
87
|
|
89
|
-
Metrics.increment_published_messages(subject)
|
88
|
+
# Metrics.increment_published_messages(subject)
|
90
89
|
NatsWave.logger.info("Published batch with #{events.size} events")
|
91
90
|
end
|
92
91
|
|
93
92
|
def connected?
|
94
|
-
@
|
93
|
+
@client&.connected?
|
95
94
|
end
|
96
95
|
|
97
96
|
def disconnect
|
@@ -128,20 +127,16 @@ module NatsWave
|
|
128
127
|
end
|
129
128
|
end
|
130
129
|
|
131
|
-
def apply_middleware(message)
|
132
|
-
@middleware_stack.reduce(message) do |msg, middleware|
|
133
|
-
middleware.call(msg)
|
134
|
-
end
|
135
|
-
end
|
136
|
-
|
137
130
|
def publish_sync(subject, message)
|
138
|
-
@
|
131
|
+
@client.publish(subject, message.to_json)
|
132
|
+
NatsWave.logger.debug("Published sync message to #{subject}")
|
139
133
|
end
|
140
134
|
|
141
135
|
def publish_async(subject, message)
|
142
136
|
if defined?(Concurrent)
|
143
137
|
Concurrent::Future.execute do
|
144
|
-
@
|
138
|
+
@client.publish(subject, message.to_json)
|
139
|
+
NatsWave.logger.debug("Published async message to #{subject}")
|
145
140
|
end
|
146
141
|
else
|
147
142
|
publish_sync(subject, message)
|
data/lib/nats_wave/railtie.rb
CHANGED
@@ -20,7 +20,9 @@ module NatsWave
|
|
20
20
|
config_file = Rails.root.join('config', 'nats_wave.yml')
|
21
21
|
if File.exist?(config_file)
|
22
22
|
begin
|
23
|
-
|
23
|
+
erb_content = ERB.new(File.read(config_file)).result
|
24
|
+
yaml_data = YAML.safe_load(erb_content)
|
25
|
+
yaml_config = yaml_data&.dig(Rails.env.to_s) || yaml_data&.dig(Rails.env) || {}
|
24
26
|
|
25
27
|
NatsWave.configure do |config|
|
26
28
|
# NATS Configuration
|
@@ -95,11 +97,10 @@ module NatsWave
|
|
95
97
|
Rails.application.config.after_initialize do
|
96
98
|
if NatsWave.configuration&.publishing_enabled || NatsWave.configuration&.subscription_enabled
|
97
99
|
Thread.new do
|
98
|
-
sleep
|
100
|
+
sleep 2 # Give Rails time to fully boot
|
99
101
|
begin
|
100
102
|
NatsWave.client
|
101
103
|
NatsWave.logger.info "NatsWave client initialized"
|
102
|
-
NatsWave.logger.info "NATS URL #{@nats_url}"
|
103
104
|
rescue => e
|
104
105
|
NatsWave.logger.error "Failed to initialize NatsWave client: #{e.message}"
|
105
106
|
end
|