nats_wave 1.1.8 → 1.1.9

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.
@@ -1,3 +1,394 @@
1
+ # # frozen_string_literal: true
2
+ #
3
+ # module NatsWave
4
+ # class Subscriber
5
+ # attr_reader :config, :client
6
+ #
7
+ # def initialize(config, client, middleware_stack = [])
8
+ # @config = config
9
+ # @client = client
10
+ # @original_client_factory = nil # Store how to recreate the client
11
+ # @database_connector = DatabaseConnector.new(config)
12
+ # @model_mapper = ModelMapper.new(config)
13
+ # @message_transformer = MessageTransformer.new(config)
14
+ # @dead_letter_queue = DeadLetterQueue.new(config)
15
+ #
16
+ # @registry_subscriptions = ModelRegistry.subscriptions
17
+ # @nats_subscriptions = [] # NATS::Subscription objects
18
+ # @running = false
19
+ # @shutdown = false
20
+ # @reconnecting = false
21
+ # @connection_stats = {
22
+ # reconnect_count: 0,
23
+ # last_successful_connection: Time.current,
24
+ # consecutive_failures: 0
25
+ # }
26
+ # end
27
+ #
28
+ # # Add method to store client factory for reconnection
29
+ # def set_client_factory(factory_proc)
30
+ # @original_client_factory = factory_proc
31
+ # end
32
+ #
33
+ # def begin
34
+ # return if @running || @shutdown
35
+ # return unless @config.subscription_enabled
36
+ #
37
+ # @running = true
38
+ # @shutdown = false
39
+ #
40
+ # NatsWave.logger.info "Starting NATS subscriber for #{@config.service_name}"
41
+ #
42
+ # setup_subscriptions
43
+ #
44
+ # NatsWave.logger.info "Started #{@registry_subscriptions.size} subscriptions from Model Registry"
45
+ #
46
+ # # Keep the subscriber alive
47
+ # keep_alive
48
+ # end
49
+ #
50
+ # def listen(subjects:, model_mappings: {}, handler: nil)
51
+ # subjects = Array(subjects)
52
+ #
53
+ # subjects.each do |subject|
54
+ # subscribe_to_subject(subject, handler, model_mappings)
55
+ # NatsWave.logger.info "Subscribed to #{subject}"
56
+ # end
57
+ # end
58
+ #
59
+ # def reset
60
+ # @shutdown = true
61
+ # @running = false
62
+ #
63
+ # # Stop keep alive thread
64
+ # if @keep_alive_thread&.alive?
65
+ # @keep_alive_thread.kill
66
+ # @keep_alive_thread.join(5) # Wait up to 5 seconds for graceful shutdown
67
+ # @keep_alive_thread = nil
68
+ # end
69
+ #
70
+ # # Unsubscribe from all subscriptions
71
+ # cleanup_subscriptions
72
+ #
73
+ # NatsWave.logger.info "🛑 Subscriber shutdown complete"
74
+ # end
75
+ #
76
+ # def database_connected?
77
+ # @database_connector.connected?
78
+ # end
79
+ #
80
+ # def disconnect
81
+ # reset
82
+ # end
83
+ #
84
+ # private
85
+ #
86
+ # def setup_subscriptions
87
+ # return unless @client&.connected?
88
+ #
89
+ # # Clear existing subscriptions first
90
+ # cleanup_subscriptions
91
+ #
92
+ # # Use ModelRegistry subscriptions
93
+ # @registry_subscriptions.each do |subscription|
94
+ # subscription[:subjects].each do |subject|
95
+ # subscribe_to_subject(subject, subscription[:handler])
96
+ # end
97
+ # end
98
+ # end
99
+ #
100
+ # def cleanup_subscriptions
101
+ # @nats_subscriptions.each do |subscription|
102
+ # begin
103
+ # subscription.unsubscribe if subscription.respond_to?(:unsubscribe)
104
+ # rescue => e
105
+ # NatsWave.logger.error "Error unsubscribing: #{e.message}"
106
+ # end
107
+ # end
108
+ # @nats_subscriptions.clear
109
+ # end
110
+ #
111
+ # def subscribe_to_subject(subject_pattern, custom_handler = nil, model_mappings = {})
112
+ # return unless @client&.connected?
113
+ #
114
+ # NatsWave.logger.info "🔍 Attempting to subscribe to: #{subject_pattern}"
115
+ #
116
+ # begin
117
+ # # Create the NATS subscription
118
+ # nats_subscription = @client.subscribe(
119
+ # subject_pattern,
120
+ # queue: @config.queue_group
121
+ # ) do |msg|
122
+ # begin
123
+ # NatsWave.logger.debug "📨 Received message on #{msg.subject}"
124
+ # process_message(msg.data, custom_handler, model_mappings)
125
+ # rescue => e
126
+ # NatsWave.logger.error "Error in subscription handler: #{e.message}"
127
+ # NatsWave.logger.error e.backtrace.join("\n")
128
+ # # Don't re-raise - this would kill the subscription
129
+ # end
130
+ # end
131
+ #
132
+ # # Add to NATS subscriptions array
133
+ # @nats_subscriptions << nats_subscription
134
+ # NatsWave.logger.info "✅ Successfully subscribed to #{subject_pattern} (total: #{@nats_subscriptions.size})"
135
+ #
136
+ # rescue => e
137
+ # NatsWave.logger.error "Failed to subscribe to #{subject_pattern}: #{e.message}"
138
+ # raise
139
+ # end
140
+ # end
141
+ #
142
+ # def process_message(raw_message, custom_handler, model_mappings)
143
+ # return unless should_process_message?(raw_message)
144
+ #
145
+ # NatsWave.logger.debug "🔄 Processing message: #{raw_message[0..200]}..."
146
+ #
147
+ # message = parse_message(raw_message)
148
+ #
149
+ # if custom_handler
150
+ # custom_handler.call(message)
151
+ # else
152
+ # handle_model_sync(message, model_mappings)
153
+ # end
154
+ #
155
+ # NatsWave.logger.debug('✅ Successfully processed message')
156
+ # rescue StandardError => e
157
+ # handle_error(e, raw_message, message)
158
+ # # Don't re-raise - this would kill the subscription
159
+ # end
160
+ #
161
+ # def should_process_message?(raw_message_data)
162
+ # message = JSON.parse(raw_message_data)
163
+ # source = message['source'] || {}
164
+ #
165
+ # # Skip messages from same service instance
166
+ # if source['service'] == @config.service_name && source['instance_id'] == @config.instance_id
167
+ # NatsWave.logger.debug "🔄 Skipping message from same service instance"
168
+ # return false
169
+ # end
170
+ #
171
+ # true
172
+ # rescue JSON::ParserError => e
173
+ # NatsWave.logger.error "Failed to parse message for filtering: #{e.message}"
174
+ # false
175
+ # end
176
+ #
177
+ # def keep_alive
178
+ # NatsWave.logger.info "🔄 Starting keep-alive thread for persistent JetStream connection"
179
+ #
180
+ # @keep_alive_thread = Thread.new do
181
+ # while @running && !@shutdown
182
+ # begin
183
+ # sleep 30 # Check every 30 seconds
184
+ #
185
+ # connection_healthy = check_connection_health
186
+ #
187
+ # if connection_healthy
188
+ # NatsWave.logger.debug "💓 Subscriber connection healthy - #{@nats_subscriptions.size} active subscriptions"
189
+ # @connection_stats[:consecutive_failures] = 0
190
+ # @connection_stats[:last_successful_connection] = Time.current
191
+ # else
192
+ # @connection_stats[:consecutive_failures] += 1
193
+ # time_since_last_good = Time.current - @connection_stats[:last_successful_connection]
194
+ #
195
+ # NatsWave.logger.error "❌ Subscriber connection lost! (failure ##{@connection_stats[:consecutive_failures]}, last good: #{time_since_last_good.to_i}s ago)"
196
+ #
197
+ # log_connection_diagnostics
198
+ # attempt_reconnection
199
+ # end
200
+ #
201
+ # # If too many consecutive failures, escalate
202
+ # if @connection_stats[:consecutive_failures] > 5
203
+ # NatsWave.logger.error "🚨 Too many consecutive failures. Performing full reset."
204
+ # perform_full_reset
205
+ # end
206
+ #
207
+ # rescue => e
208
+ # NatsWave.logger.error "Error in keep_alive thread: #{e.message}\n#{e.backtrace.first(3).join("\n")}"
209
+ # sleep 10
210
+ # end
211
+ # end
212
+ #
213
+ # NatsWave.logger.info "🛑 Keep-alive thread shutting down"
214
+ # end
215
+ # end
216
+ #
217
+ # def check_connection_health
218
+ # return false unless @client
219
+ #
220
+ # begin
221
+ # # First check basic connectivity
222
+ # return false unless @client.connected?
223
+ #
224
+ # # Try to perform a simple operation with timeout
225
+ # Timeout.timeout(5) do
226
+ # @client.flush
227
+ # return true
228
+ # end
229
+ # rescue Timeout::Error
230
+ # NatsWave.logger.warn "⏰ Connection health check timed out"
231
+ # return false
232
+ # rescue => e
233
+ # NatsWave.logger.warn "🔍 Health check failed: #{e.message}"
234
+ # return false
235
+ # end
236
+ # end
237
+ #
238
+ # def log_connection_diagnostics
239
+ # begin
240
+ # NatsWave.logger.info "🔍 Connection Diagnostics:"
241
+ # NatsWave.logger.info " - Client connected?: #{@client&.connected? rescue 'unknown'}"
242
+ # NatsWave.logger.info " - Active subscriptions: #{@nats_subscriptions.size}"
243
+ # NatsWave.logger.info " - Reconnect count: #{@connection_stats[:reconnect_count]}"
244
+ # NatsWave.logger.info " - Thread count: #{Thread.list.count}"
245
+ # NatsWave.logger.info " - Last successful: #{@connection_stats[:last_successful_connection]}"
246
+ #
247
+ # # Check if subscriptions are actually valid
248
+ # valid_subs = @nats_subscriptions.count { |sub| sub.respond_to?(:unsubscribe) }
249
+ # NatsWave.logger.info " - Valid subscriptions: #{valid_subs}/#{@nats_subscriptions.size}"
250
+ #
251
+ # rescue => e
252
+ # NatsWave.logger.error "Failed to gather diagnostics: #{e.message}"
253
+ # end
254
+ # end
255
+ #
256
+ # def attempt_reconnection
257
+ # return if @shutdown || @reconnecting
258
+ #
259
+ # @reconnecting = true
260
+ # @connection_stats[:reconnect_count] += 1
261
+ #
262
+ # begin
263
+ # NatsWave.logger.info "🔄 Attempting to reconnect to NATS... (attempt ##{@connection_stats[:reconnect_count]})"
264
+ #
265
+ # # First, clean up existing connection and subscriptions
266
+ # cleanup_subscriptions
267
+ #
268
+ # # Close the existing client connection
269
+ # begin
270
+ # @client&.close if @client&.connected?
271
+ # rescue => e
272
+ # NatsWave.logger.debug "Error closing existing connection: #{e.message}"
273
+ # end
274
+ #
275
+ # # Wait before attempting to reconnect
276
+ # sleep_time = [2 ** [@connection_stats[:consecutive_failures] - 1, 4].min, 30].min
277
+ # NatsWave.logger.debug "⏱️ Waiting #{sleep_time}s before reconnection attempt"
278
+ # sleep sleep_time
279
+ #
280
+ # # Attempt to recreate the client connection
281
+ # if @original_client_factory
282
+ # @client = @original_client_factory.call
283
+ # NatsWave.logger.info "🔄 Created new NATS client using factory"
284
+ # else
285
+ # NatsWave.logger.warn "⚠️ No client factory available, hoping existing client recovers"
286
+ # end
287
+ #
288
+ # # Check if we have a working connection
289
+ # if @client&.connected?
290
+ # NatsWave.logger.info "✅ NATS client reconnected successfully"
291
+ #
292
+ # # Re-establish all subscriptions
293
+ # setup_subscriptions
294
+ #
295
+ # NatsWave.logger.info "✅ Subscriptions restored (#{@nats_subscriptions.size} active)"
296
+ # @connection_stats[:consecutive_failures] = 0
297
+ # @connection_stats[:last_successful_connection] = Time.current
298
+ #
299
+ # return true
300
+ # else
301
+ # raise "Connection failed - client not connected"
302
+ # end
303
+ #
304
+ # rescue => e
305
+ # NatsWave.logger.error "Reconnection failed: #{e.message}"
306
+ # return false
307
+ # ensure
308
+ # @reconnecting = false
309
+ # end
310
+ # end
311
+ #
312
+ # def perform_full_reset
313
+ # NatsWave.logger.info "🔄 Performing full connection reset"
314
+ #
315
+ # begin
316
+ # # Force close everything
317
+ # cleanup_subscriptions
318
+ # @client&.close rescue nil
319
+ #
320
+ # # Clear connection stats
321
+ # @connection_stats[:consecutive_failures] = 0
322
+ #
323
+ # # Force garbage collection
324
+ # GC.start
325
+ #
326
+ # # Wait longer before attempting reset
327
+ # sleep 10
328
+ #
329
+ # # Try to recreate everything
330
+ # if @original_client_factory
331
+ # @client = @original_client_factory.call
332
+ # setup_subscriptions if @client&.connected?
333
+ # end
334
+ #
335
+ # rescue => e
336
+ # NatsWave.logger.error "Full reset failed: #{e.message}"
337
+ # end
338
+ # end
339
+ #
340
+ # # ... rest of your existing methods (parse_message, handle_model_sync, handle_error)
341
+ # def parse_message(raw_message)
342
+ # @message_transformer.parse_message(raw_message)
343
+ # end
344
+ #
345
+ # def handle_model_sync(message, model_mappings)
346
+ # source_model = message['model']
347
+ # mapping = model_mappings[source_model] || @config.model_mappings[source_model]
348
+ #
349
+ # return unless mapping
350
+ #
351
+ # target_model = mapping[:target_model]
352
+ # field_mappings = mapping[:field_mappings] || {}
353
+ # transformations = mapping[:transformations] || {}
354
+ #
355
+ # # Transform the data
356
+ # transformed_data = @model_mapper.transform_data(
357
+ # message['data'],
358
+ # field_mappings,
359
+ # transformations
360
+ # )
361
+ #
362
+ # # Apply to database
363
+ # @database_connector.apply_change(
364
+ # model: target_model,
365
+ # action: message['action'],
366
+ # data: transformed_data,
367
+ # metadata: message['metadata']
368
+ # )
369
+ #
370
+ # NatsWave.logger.info "Synced #{source_model} -> #{target_model}: #{message['action']}"
371
+ # end
372
+ #
373
+ # def handle_error(error, raw_message, parsed_message = nil)
374
+ # event_id = parsed_message&.dig('event_id') || 'unknown'
375
+ # subject = parsed_message&.dig('subject') || 'unknown'
376
+ #
377
+ # NatsWave.logger.error("Error processing message #{event_id} from #{subject}: #{error.message}")
378
+ #
379
+ # # Send to dead letter queue
380
+ # @dead_letter_queue.store_failed_message(
381
+ # parsed_message || raw_message,
382
+ # error,
383
+ # 0 # retry count
384
+ # )
385
+ #
386
+ # # Continue processing - don't raise to avoid breaking subscription
387
+ # end
388
+ # end
389
+ # end
390
+
391
+
1
392
  # frozen_string_literal: true
2
393
 
3
394
  module NatsWave
@@ -337,7 +728,6 @@ module NatsWave
337
728
  end
338
729
  end
339
730
 
340
- # ... rest of your existing methods (parse_message, handle_model_sync, handle_error)
341
731
  def parse_message(raw_message)
342
732
  @message_transformer.parse_message(raw_message)
343
733
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NatsWave
4
- VERSION = '1.1.8'
4
+ VERSION = '1.1.9'
5
5
  end
data/lib/nats_wave.rb CHANGED
@@ -1,3 +1,102 @@
1
+ # # frozen_string_literal: true
2
+ #
3
+ # require 'active_support/all'
4
+ # require 'logger'
5
+ # require 'socket'
6
+ # require 'json'
7
+ # require 'securerandom'
8
+ #
9
+ # require_relative "nats_wave/version"
10
+ # require_relative "nats_wave/errors"
11
+ # require_relative "nats_wave/configuration"
12
+ # require_relative "nats_wave/client"
13
+ # require_relative "nats_wave/publisher"
14
+ # require_relative "nats_wave/subscriber"
15
+ # require_relative "nats_wave/message_transformer"
16
+ # require_relative "nats_wave/model_mapper"
17
+ # require_relative "nats_wave/model_registry"
18
+ # require_relative "nats_wave/concerns/mappable"
19
+ # require_relative "nats_wave/adapters/datadog_metrics"
20
+ # require_relative "nats_wave/database_connector"
21
+ # require_relative "nats_wave/schema_registry"
22
+ # require_relative "nats_wave/dead_letter_queue"
23
+ # require_relative "nats_wave/metrics"
24
+ #
25
+ # # Middleware
26
+ # require_relative "nats_wave/middleware/base"
27
+ # require_relative "nats_wave/middleware/authentication"
28
+ # require_relative "nats_wave/middleware/validation"
29
+ # require_relative "nats_wave/middleware/logging"
30
+ #
31
+ # # Adapters
32
+ # require_relative "nats_wave/adapters/active_record"
33
+ #
34
+ # # Rails integration
35
+ # require_relative "nats_wave/railtie" if defined?(Rails)
36
+ #
37
+ # module NatsWave
38
+ # class << self
39
+ # attr_accessor :configuration
40
+ #
41
+ # def client
42
+ # @client ||= Client.new
43
+ # end
44
+ #
45
+ # def configure
46
+ # self.configuration ||= Configuration.new
47
+ # yield(configuration) if block_given?
48
+ # reset_client!
49
+ # configuration
50
+ # end
51
+ #
52
+ # def reset_client!
53
+ # @client = nil
54
+ # end
55
+ #
56
+ # def logger
57
+ # @logger ||= begin
58
+ # if defined?(Rails) && Rails.logger
59
+ # Rails.logger
60
+ # else
61
+ # Logger.new($stdout).tap { |l| l.level = Logger::INFO }
62
+ # end
63
+ # end
64
+ # end
65
+ #
66
+ # def logger=(logger)
67
+ # @logger = logger
68
+ # end
69
+ #
70
+ # # Convenience methods
71
+ # def publish(subject:, model:, action:, data:, metadata: {})
72
+ # client.publish(
73
+ # subject: subject,
74
+ # model: model,
75
+ # action: action,
76
+ # data: data,
77
+ # metadata: metadata
78
+ # )
79
+ # end
80
+ #
81
+ # def subscribe(subjects:, model_mappings: {}, &block)
82
+ # client.subscribe(
83
+ # subjects: subjects,
84
+ # model_mappings: model_mappings,
85
+ # &block
86
+ # )
87
+ # NatsWave.logger.info "Completely subscribed to #{subjects.size} subjects"
88
+ # end
89
+ #
90
+ # def start_subscriber
91
+ # client.start_subscriber
92
+ # end
93
+ #
94
+ # def health_check
95
+ # client.health_check
96
+ # end
97
+ # end
98
+ # end
99
+
1
100
  # frozen_string_literal: true
2
101
 
3
102
  require 'active_support/all'
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.8
4
+ version: 1.1.9
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-24 00:00:00.000000000 Z
11
+ date: 2025-07-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nats-pure
@@ -325,12 +325,12 @@ files:
325
325
  - lib/generators/nats_wave/templates/initializer.rb
326
326
  - lib/generators/nats_wave/templates/nats_wave.yml
327
327
  - lib/nats_wave.rb
328
+ - lib/nats_wave/active_record_extension.rb
328
329
  - lib/nats_wave/adapters/active_record.rb
329
330
  - lib/nats_wave/adapters/datadog_metrics.rb
330
331
  - lib/nats_wave/auto_registration.rb
331
332
  - lib/nats_wave/client.rb
332
333
  - lib/nats_wave/concerns/mappable.rb
333
- - lib/nats_wave/concerns/publishable.rb
334
334
  - lib/nats_wave/configuration.rb
335
335
  - lib/nats_wave/database_connector.rb
336
336
  - lib/nats_wave/dead_letter_queue.rb