nats_wave 1.1.5 → 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.
@@ -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
- # Register this mapping globally
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
- # Register subscription
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,
@@ -18,7 +18,7 @@ module NatsWave
18
18
  def initialize(options = {})
19
19
  @nats_url = ENV['NATS_URL'] || "nats://localhost:4222"
20
20
  @service_name = ENV['NATS_SERVICE_NAME'] || "purplewave"
21
- @version = ENV['NATS_SERVICE_VERSION'] || "1.1.5"
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 = [:create, :update, :destroy])
74
+ def add_publication(model_class, actions = %i[create update destroy])
75
75
  @publications << {
76
76
  model: model_class,
77
77
  actions: Array(actions)
@@ -58,7 +58,7 @@ module NatsWave
58
58
  private
59
59
 
60
60
  def setup_storage
61
- # For production, you'd want to use Redis, database, or file storage
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 your retry strategy
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, you would:
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, you might move this to a different storage
141
+ # In a real implementation, we might move this to a different storage
142
142
  update(message)
143
143
  end
144
144
  end
@@ -122,4 +122,4 @@ module NatsWave
122
122
  end
123
123
  end
124
124
  end
125
- end
125
+ 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
- # Get all registered subscriptions
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
- # If only one mapping, use it
100
- return local_mappings.keys.first if local_mappings.size == 1
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,
@@ -1,52 +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
- 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
44
+ # frozen_string_literal: true
42
45
 
43
46
  module NatsWave
44
47
  class Publisher
45
- attr_reader :config, :nats_client
48
+ attr_reader :config, :client
46
49
 
47
- def initialize(config, nats_client, middleware_stack = [])
50
+ def initialize(config, client, middleware_stack = [])
48
51
  @config = config
49
- @nats_client = nats_client
52
+ @client = client
50
53
  @message_transformer = MessageTransformer.new(config)
51
54
  @dead_letter_queue = DeadLetterQueue.new(config) if config.dead_letter_queue
52
55
  end
@@ -80,14 +83,14 @@ module NatsWave
80
83
 
81
84
  subject = "#{@config.default_subject_prefix}.batch"
82
85
 
83
- @nats_client.publish(subject, batch_message.to_json)
86
+ @client.publish(subject, batch_message.to_json)
84
87
 
85
88
  # Metrics.increment_published_messages(subject)
86
89
  NatsWave.logger.info("Published batch with #{events.size} events")
87
90
  end
88
91
 
89
92
  def connected?
90
- @nats_client&.connected?
93
+ @client&.connected?
91
94
  end
92
95
 
93
96
  def disconnect
@@ -125,14 +128,14 @@ module NatsWave
125
128
  end
126
129
 
127
130
  def publish_sync(subject, message)
128
- @nats_client.publish(subject, message.to_json)
131
+ @client.publish(subject, message.to_json)
129
132
  NatsWave.logger.debug("Published sync message to #{subject}")
130
133
  end
131
134
 
132
135
  def publish_async(subject, message)
133
136
  if defined?(Concurrent)
134
137
  Concurrent::Future.execute do
135
- @nats_client.publish(subject, message.to_json)
138
+ @client.publish(subject, message.to_json)
136
139
  NatsWave.logger.debug("Published async message to #{subject}")
137
140
  end
138
141
  else
@@ -2,27 +2,29 @@
2
2
 
3
3
  module NatsWave
4
4
  class Subscriber
5
- attr_reader :config, :nats_client
5
+ attr_reader :config, :client
6
6
 
7
- def initialize(config, nats_client, middleware_stack = [])
7
+ def initialize(config, client, middleware_stack = [])
8
8
  @config = config
9
- @nats_client = nats_client
9
+ @client = client
10
10
  @database_connector = DatabaseConnector.new(config)
11
11
  @model_mapper = ModelMapper.new(config)
12
12
  @message_transformer = MessageTransformer.new(config)
13
13
  @dead_letter_queue = DeadLetterQueue.new(config)
14
14
 
15
- # Separate the two types of subscriptions
16
- @registry_subscriptions = ModelRegistry.subscriptions # Hash objects
17
- @nats_subscriptions = [] # NATS::Subscription objects
15
+ @registry_subscriptions = ModelRegistry.subscriptions
16
+ @nats_subscriptions = [] # NATS::Subscription objects
18
17
  @running = false
18
+ @shutdown = false
19
19
  end
20
20
 
21
- def start
22
- return if @running
21
+ def begin
22
+ return if @running || @shutdown
23
23
  return unless @config.subscription_enabled
24
24
 
25
25
  @running = true
26
+ @shutdown = false
27
+
26
28
  NatsWave.logger.info "Starting NATS subscriber for #{@config.service_name}"
27
29
 
28
30
  # Use ModelRegistry subscriptions
@@ -32,10 +34,13 @@ module NatsWave
32
34
  end
33
35
  end
34
36
 
35
- NatsWave.logger.info "Started #{@registry_subscriptions.size} subscriptions from ModelRegistry"
37
+ NatsWave.logger.info "Started #{@registry_subscriptions.size} subscriptions from Model Registry"
38
+
39
+ # Keep the subscriber alive
40
+ keep_alive
36
41
  end
37
42
 
38
- def subscribe(subjects:, model_mappings: {}, handler: nil)
43
+ def listen(subjects:, model_mappings: {}, handler: nil)
39
44
  subjects = Array(subjects)
40
45
 
41
46
  subjects.each do |subject|
@@ -44,11 +49,27 @@ module NatsWave
44
49
  end
45
50
  end
46
51
 
47
- def unsubscribe_all
48
- # Unsubscribe from NATS subscriptions (the actual NATS::Subscription objects)
49
- @nats_subscriptions.each(&:unsubscribe)
50
- @nats_subscriptions.clear
52
+ def reset
53
+ @shutdown = true
51
54
  @running = false
55
+
56
+ # Stop keep alive thread
57
+ if @keep_alive_thread&.alive?
58
+ @keep_alive_thread.kill
59
+ @keep_alive_thread = nil
60
+ end
61
+
62
+ # Unsubscribe from all subscriptions
63
+ @nats_subscriptions.each do |subscription|
64
+ begin
65
+ subscription.unsubscribe if subscription.respond_to?(:unsubscribe)
66
+ rescue => e
67
+ NatsWave.logger.error "Error unsubscribing: #{e.message}"
68
+ end
69
+ end
70
+ @nats_subscriptions.clear
71
+
72
+ NatsWave.logger.info "🛑 Subscriber shutdown complete"
52
73
  end
53
74
 
54
75
  def database_connected?
@@ -56,7 +77,7 @@ module NatsWave
56
77
  end
57
78
 
58
79
  def disconnect
59
- unsubscribe_all
80
+ reset
60
81
  end
61
82
 
62
83
  private
@@ -65,20 +86,30 @@ module NatsWave
65
86
  NatsWave.logger.info "🔍 Attempting to subscribe to: #{subject_pattern}"
66
87
 
67
88
  # Create the NATS subscription
68
- nats_subscription = @nats_client.subscribe(
89
+ nats_subscription = @client.subscribe(
69
90
  subject_pattern,
70
91
  queue: @config.queue_group
71
92
  ) do |msg|
72
- NatsWave.logger.info "📨 Received message on #{msg.subject}: #{msg.data}"
73
- process_message(msg.data, custom_handler, model_mappings)
93
+ begin
94
+ NatsWave.logger.debug "📨 Received message on #{msg.subject}"
95
+ process_message(msg.data, custom_handler, model_mappings)
96
+ rescue => e
97
+ NatsWave.logger.error "Error in subscription handler: #{e.message}"
98
+ NatsWave.logger.error e.backtrace.join("\n")
99
+ # Don't re-raise - this would kill the subscription
100
+ end
74
101
  end
75
102
 
76
- # Add to NATS subscriptions array (not the registry subscriptions)
103
+ # Add to NATS subscriptions array
77
104
  @nats_subscriptions << nats_subscription
78
105
  NatsWave.logger.info "✅ Successfully subscribed to #{subject_pattern} (total: #{@nats_subscriptions.size})"
79
106
  end
80
107
 
81
108
  def process_message(raw_message, custom_handler, model_mappings)
109
+ return unless should_process_message?(raw_message)
110
+
111
+ NatsWave.logger.debug "🔄 Processing message: #{raw_message[0..200]}..."
112
+
82
113
  message = parse_message(raw_message)
83
114
 
84
115
  if custom_handler
@@ -87,11 +118,78 @@ module NatsWave
87
118
  handle_model_sync(message, model_mappings)
88
119
  end
89
120
 
90
- NatsWave.logger.debug('Successfully processed message')
121
+ NatsWave.logger.debug('Successfully processed message')
91
122
  rescue StandardError => e
92
123
  handle_error(e, raw_message, message)
124
+ # Don't re-raise - this would kill the subscription
125
+ end
126
+
127
+ def should_process_message?(raw_message_data)
128
+ message = JSON.parse(raw_message_data)
129
+ source = message['source'] || {}
130
+
131
+ # Skip messages from same service instance
132
+ if source['service'] == @config.service_name && source['instance_id'] == @config.instance_id
133
+ NatsWave.logger.debug "🔄 Skipping message from same service instance"
134
+ return false
135
+ end
136
+
137
+ true
138
+ rescue JSON::ParserError => e
139
+ NatsWave.logger.error "Failed to parse message for filtering: #{e.message}"
140
+ false
141
+ end
142
+
143
+ def keep_alive
144
+ Rails.logger.info "🔄 Starting keep-alive thread for persistent JetStream connection"
145
+
146
+ @keep_alive_thread = Thread.new do
147
+ while @running && !@shutdown
148
+ begin
149
+ sleep 30 # Check every 30 seconds
150
+
151
+ if @client&.connected?
152
+ Rails.logger.debug "💓 Subscriber connection healthy - #{@nats_subscriptions.size} active subscriptions"
153
+ else
154
+ Rails.logger.error "❌ Subscriber connection lost! Attempting reconnection..."
155
+ attempt_reconnection
156
+ end
157
+
158
+ rescue => e
159
+ Rails.logger.error "Error in keep_alive thread: #{e.message}"
160
+ sleep 10
161
+ end
162
+ end
163
+
164
+ Rails.logger.info "🛑 Keep-alive thread shutting down"
165
+ end
166
+ end
167
+
168
+ def attempt_reconnection
169
+ return if @shutdown
170
+
171
+ NatsWave.logger.info "🔄 Attempting to reconnect to NATS..."
172
+
173
+ # Reset subscriptions
174
+ @nats_subscriptions.clear
175
+
176
+ # Try to reestablish subscriptions
177
+ sleep 5 # Wait before reconnecting
178
+
179
+ if @client&.connected?
180
+ NatsWave.logger.info "✅ NATS reconnected, reestablishing subscriptions"
181
+
182
+ @registry_subscriptions.each do |subscription|
183
+ subscription[:subjects].each do |subject|
184
+ subscribe_to_subject(subject, subscription[:handler])
185
+ end
186
+ end
187
+ end
188
+ rescue => e
189
+ NatsWave.logger.error "Failed to reconnect: #{e.message}"
93
190
  end
94
191
 
192
+ # ... rest of your existing methods (parse_message, handle_model_sync, handle_error)
95
193
  def parse_message(raw_message)
96
194
  @message_transformer.parse_message(raw_message)
97
195
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NatsWave
4
- VERSION = '1.1.5'
4
+ VERSION = '1.1.7'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nats_wave
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.5
4
+ version: 1.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeffrey Dabo
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-07-18 00:00:00.000000000 Z
11
+ date: 2025-07-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nats-pure