nats_wave 1.1.20 → 1.2.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 56f43d30e884588fb8db2ecbf73b1c7e39aa7d63fbfbec563c156f6f13f9ee0c
4
- data.tar.gz: c07acd7e6f1e7acc05a72d13eee5640c7fa5cdda91adc135fe81f4eb51262ae3
3
+ metadata.gz: 8cfe71b3c054507c991ba39dfe44cfd275ceec15143053c92278b6dad000243a
4
+ data.tar.gz: 18fe41af6bfb86c573167e048fd9604e248fd9dec3a1e4d3f4ebf62ec3a9bf20
5
5
  SHA512:
6
- metadata.gz: 07dbf0eec97fbed551525feb102d4c101f7cfc6dd2f608ccb635e8a0b9248d64b8b66d610df63ae3f9b6a6d2cb9e66c2293485522230f5653ab9e0ae212b2451
7
- data.tar.gz: 855b54e2f045c8d0c12dbad5bb0a553a44e9ef8d0c7b6ba33997c25f25997bee9a397276a61ea8ca510f0500c5138048c233fc65169a6133d90133e8925636d2
6
+ metadata.gz: 54010d3177f0be905380f3b932fa0546cfd31665c6da161a2f25b34234948506750b62cc61e97aed931d5367c27cbfcee6b60e42bffc23296df2c49bec9556c8
7
+ data.tar.gz: 87ea4a45236016e1ceceb0a3386feaaff09707532b5a8937a83296da5b34617f4259dea7f1c2a08477f6cccbc6ba854d4d3ccee1c78c2264ac9d34682d2dd296
data/.idea/nats_wave.iml CHANGED
@@ -129,21 +129,21 @@
129
129
  <option name="myRootTask">
130
130
  <RakeTaskImpl id="rake">
131
131
  <subtasks>
132
- <RakeTaskImpl description="Build nats_wave-1.1.14.gem into the pkg directory" fullCommand="build" id="build" />
132
+ <RakeTaskImpl description="Build nats_wave-1.1.20.gem into the pkg directory" fullCommand="build" id="build" />
133
133
  <RakeTaskImpl id="build">
134
134
  <subtasks>
135
- <RakeTaskImpl description="Generate SHA512 checksum of nats_wave-1.1.14.gem into the checksums directory" fullCommand="build:checksum" id="checksum" />
135
+ <RakeTaskImpl description="Generate SHA512 checksum of nats_wave-1.1.20.gem into the checksums directory" fullCommand="build:checksum" id="checksum" />
136
136
  </subtasks>
137
137
  </RakeTaskImpl>
138
138
  <RakeTaskImpl description="Remove any temporary products" fullCommand="clean" id="clean" />
139
139
  <RakeTaskImpl description="Remove any generated files" fullCommand="clobber" id="clobber" />
140
- <RakeTaskImpl description="Build and install nats_wave-1.1.14.gem into system gems" fullCommand="install" id="install" />
140
+ <RakeTaskImpl description="Build and install nats_wave-1.1.20.gem into system gems" fullCommand="install" id="install" />
141
141
  <RakeTaskImpl id="install">
142
142
  <subtasks>
143
- <RakeTaskImpl description="Build and install nats_wave-1.1.14.gem into system gems without network access" fullCommand="install:local" id="local" />
143
+ <RakeTaskImpl description="Build and install nats_wave-1.1.20.gem into system gems without network access" fullCommand="install:local" id="local" />
144
144
  </subtasks>
145
145
  </RakeTaskImpl>
146
- <RakeTaskImpl description="Create tag v1.1.14 and build and push nats_wave-1.1.14.gem to https://rubygems.org" fullCommand="release[remote]" id="release[remote]" />
146
+ <RakeTaskImpl description="Create tag v1.1.20 and build and push nats_wave-1.1.20.gem to https://rubygems.org" fullCommand="release[remote]" id="release[remote]" />
147
147
  <RakeTaskImpl description="Run RuboCop" fullCommand="rubocop" id="rubocop" />
148
148
  <RakeTaskImpl id="rubocop">
149
149
  <subtasks>
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- nats_wave (1.1.20)
4
+ nats_wave (1.2.1)
5
5
  activerecord (>= 6.1, < 8.0)
6
6
  activesupport (>= 6.1, < 8.0)
7
7
  concurrent-ruby (~> 1.1)
@@ -403,10 +403,16 @@ module NatsWave
403
403
  end
404
404
 
405
405
  def with_nats_publishing_disabled(&block)
406
+ original_skip = Thread.current[:skip_nats_wave_publishing]
407
+ original_processing = Thread.current[:nats_wave_processing]
408
+
406
409
  Thread.current[:skip_nats_wave_publishing] = true
410
+ Thread.current[:nats_wave_processing] = true
411
+
407
412
  yield
408
413
  ensure
409
- Thread.current[:skip_nats_wave_publishing] = false
414
+ Thread.current[:skip_nats_wave_publishing] = original_skip
415
+ Thread.current[:nats_wave_processing] = original_processing
410
416
  end
411
417
  end
412
418
  end
@@ -77,7 +77,7 @@ module NatsWave
77
77
  {
78
78
  service: @service_name,
79
79
  environment: ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'unknown',
80
- version: ENV['APP_VERSION'] || '1.1.20'
80
+ version: ENV['APP_VERSION'] || '1.2.1'
81
81
  }
82
82
  end
83
83
 
@@ -201,9 +201,11 @@ module NatsWave
201
201
  def handle_auto_update(data, unique_fields, sync_strategy)
202
202
  existing = find_by_unique_fields(data, unique_fields)
203
203
  if existing
204
- existing.update!(data)
205
- NatsWave.logger.info "✅ Updated #{self.name}: #{existing.id}"
204
+ data_without_key = data.except('key', :key)
205
+ existing.update!(data_without_key)
206
+ NatsWave.logger.info "✅ Updated #{self.name}: #{existing.id} (preserved key: #{existing.key})"
206
207
  elsif sync_strategy == :upsert
208
+ data['key'] = SecureRandom.uuid unless data['key'].present?
207
209
  record = self.create!(data)
208
210
  NatsWave.logger.info "✅ Created new #{self.name} during update: #{record.id}"
209
211
  else
@@ -359,37 +361,50 @@ module NatsWave
359
361
 
360
362
  # Auto-publishing callback methods (only called once per action)
361
363
  def trigger_nats_wave_auto_publish_on_create
362
- return if @nats_wave_publishing_in_progress
363
- @nats_wave_publishing_in_progress = true
364
+ return if should_skip_nats_publishing?
364
365
 
366
+ @nats_wave_publishing_in_progress = true
365
367
  NatsWave.logger.debug "🚀 Auto-publishing on create"
366
368
  nats_wave_publish('create')
367
-
369
+ ensure
368
370
  @nats_wave_publishing_in_progress = false
369
371
  end
370
372
 
371
373
  def trigger_nats_wave_auto_publish_on_update
372
- return if @nats_wave_publishing_in_progress
373
- @nats_wave_publishing_in_progress = true
374
+ return if should_skip_nats_publishing?
374
375
 
376
+ @nats_wave_publishing_in_progress = true
375
377
  NatsWave.logger.debug "🚀 Auto-publishing on update"
376
378
  nats_wave_publish('update')
377
-
379
+ ensure
378
380
  @nats_wave_publishing_in_progress = false
379
381
  end
380
382
 
381
383
  def trigger_nats_wave_auto_publish_on_destroy
382
- return if @nats_wave_publishing_in_progress
383
- @nats_wave_publishing_in_progress = true
384
+ return if should_skip_nats_publishing?
384
385
 
386
+ @nats_wave_publishing_in_progress = true
385
387
  NatsWave.logger.debug "🚀 Auto-publishing on destroy"
386
388
  nats_wave_publish('destroy')
387
-
389
+ ensure
388
390
  @nats_wave_publishing_in_progress = false
389
391
  end
390
392
 
391
393
  private
392
394
 
395
+ def should_skip_nats_publishing?
396
+ # Skip if already publishing
397
+ return true if @nats_wave_publishing_in_progress
398
+
399
+ # Skip if processing a NATS message
400
+ return true if Thread.current[:nats_wave_processing]
401
+
402
+ # Skip if explicitly disabled
403
+ return true if Thread.current[:skip_nats_wave_publishing]
404
+
405
+ false
406
+ end
407
+
393
408
  def determine_action_from_context
394
409
  # Try to determine action from ActiveRecord context
395
410
  if defined?(ActiveRecord) && is_a?(ActiveRecord::Base)
@@ -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.20"
21
+ @version = ENV['NATS_SERVICE_VERSION'] || "1.2.1"
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
@@ -1,394 +1,3 @@
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
-
392
1
  # frozen_string_literal: true
393
2
 
394
3
  module NatsWave
@@ -398,14 +7,16 @@ module NatsWave
398
7
  def initialize(config, client, middleware_stack = [])
399
8
  @config = config
400
9
  @client = client
401
- @original_client_factory = nil # Store how to recreate the client
10
+ @original_client_factory = nil
402
11
  @database_connector = DatabaseConnector.new(config)
403
12
  @model_mapper = ModelMapper.new(config)
404
13
  @message_transformer = MessageTransformer.new(config)
405
14
  @dead_letter_queue = DeadLetterQueue.new(config)
406
-
15
+ @processed_messages = {}
16
+ @dedup_cleanup_interval = 300
17
+ @last_dedup_cleanup = Time.current
407
18
  @registry_subscriptions = ModelRegistry.subscriptions
408
- @nats_subscriptions = [] # NATS::Subscription objects
19
+ @nats_subscriptions = []
409
20
  @running = false
410
21
  @shutdown = false
411
22
  @reconnecting = false
@@ -474,6 +85,16 @@ module NatsWave
474
85
 
475
86
  private
476
87
 
88
+ def should_cleanup_dedup_cache?
89
+ Time.current - @last_dedup_cleanup > @dedup_cleanup_interval
90
+ end
91
+
92
+ def cleanup_old_message_ids
93
+ cutoff = Time.current - 300 # 5 minutes
94
+ @processed_messages.delete_if { |_, timestamp| timestamp < cutoff }
95
+ @last_dedup_cleanup = Time.current
96
+ end
97
+
477
98
  def setup_subscriptions
478
99
  return unless @client&.connected?
479
100
 
@@ -533,6 +154,8 @@ module NatsWave
533
154
  def process_message(raw_message, custom_handler, model_mappings)
534
155
  return unless should_process_message?(raw_message)
535
156
 
157
+ Thread.current[:nats_wave_processing] = true
158
+
536
159
  NatsWave.logger.debug "🔄 Processing message: #{raw_message[0..200]}..."
537
160
 
538
161
  message = parse_message(raw_message)
@@ -546,19 +169,38 @@ module NatsWave
546
169
  NatsWave.logger.debug('✅ Successfully processed message')
547
170
  rescue StandardError => e
548
171
  handle_error(e, raw_message, message)
549
- # Don't re-raise - this would kill the subscription
172
+ ensure
173
+ Thread.current[:nats_wave_processing] = false
550
174
  end
551
175
 
552
176
  def should_process_message?(raw_message_data)
553
177
  message = JSON.parse(raw_message_data)
554
178
  source = message['source'] || {}
555
179
 
556
- # Skip messages from same service instance
557
180
  if source['service'] == @config.service_name && source['instance_id'] == @config.instance_id
558
181
  NatsWave.logger.debug "🔄 Skipping message from same service instance"
559
182
  return false
560
183
  end
561
184
 
185
+ if Thread.current[:nats_wave_processing]
186
+ NatsWave.logger.debug "🔄 Skipping message - already processing NATS update"
187
+ return false
188
+ end
189
+
190
+ message_id = message['event_id'] || message['message_id']
191
+ if message_id && @processed_messages[message_id]
192
+ if @processed_messages[message_id] > Time.current - 60 # 1 minute window
193
+ NatsWave.logger.debug "🔄 Skipping duplicate message: #{message_id}"
194
+ return false
195
+ end
196
+ end
197
+
198
+ # Track this message
199
+ if message_id
200
+ @processed_messages[message_id] = Time.current
201
+ cleanup_old_message_ids if should_cleanup_dedup_cache?
202
+ end
203
+
562
204
  true
563
205
  rescue JSON::ParserError => e
564
206
  NatsWave.logger.error "Failed to parse message for filtering: #{e.message}"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NatsWave
4
- VERSION = '1.1.20'
4
+ VERSION = '1.2.1'
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.20
4
+ version: 1.2.1
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-29 00:00:00.000000000 Z
11
+ date: 2025-08-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nats-pure