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.
@@ -2,48 +2,74 @@
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
10
- @middleware_stack = middleware_stack
9
+ @client = client
11
10
  @database_connector = DatabaseConnector.new(config)
12
11
  @model_mapper = ModelMapper.new(config)
13
12
  @message_transformer = MessageTransformer.new(config)
14
13
  @dead_letter_queue = DeadLetterQueue.new(config)
15
- @subscriptions = []
14
+
15
+ @registry_subscriptions = ModelRegistry.subscriptions
16
+ @nats_subscriptions = [] # NATS::Subscription objects
16
17
  @running = false
18
+ @shutdown = false
17
19
  end
18
20
 
19
- def start
20
- return if @running
21
+ def begin
22
+ return if @running || @shutdown
21
23
  return unless @config.subscription_enabled
22
24
 
23
25
  @running = true
26
+ @shutdown = false
27
+
24
28
  NatsWave.logger.info "Starting NATS subscriber for #{@config.service_name}"
25
29
 
26
- @config.subscriptions.each do |subscription|
27
- subscribe_to_subject(subscription[:subject],
28
- subscription[:handler])
30
+ # Use ModelRegistry subscriptions
31
+ @registry_subscriptions.each do |subscription|
32
+ subscription[:subjects].each do |subject|
33
+ subscribe_to_subject(subject, subscription[:handler])
34
+ end
29
35
  end
30
36
 
31
- NatsWave.logger.info "Subscribed to #{@config.subscriptions.size} subjects"
37
+ NatsWave.logger.info "Started #{@registry_subscriptions.size} subscriptions from Model Registry"
38
+
39
+ # Keep the subscriber alive
40
+ keep_alive
32
41
  end
33
42
 
34
- def subscribe(subjects:, model_mappings: {}, handler: nil)
43
+ def listen(subjects:, model_mappings: {}, handler: nil)
35
44
  subjects = Array(subjects)
36
45
 
37
46
  subjects.each do |subject|
38
- subscribe_to_subject(subject, handler,
39
- model_mappings)
47
+ subscribe_to_subject(subject, handler, model_mappings)
48
+ NatsWave.logger.info "Subscribed to #{subject}"
40
49
  end
41
50
  end
42
51
 
43
- def unsubscribe_all
44
- @subscriptions.each(&:unsubscribe)
45
- @subscriptions.clear
52
+ def reset
53
+ @shutdown = true
46
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"
47
73
  end
48
74
 
49
75
  def database_connected?
@@ -51,67 +77,121 @@ module NatsWave
51
77
  end
52
78
 
53
79
  def disconnect
54
- unsubscribe_all
80
+ reset
55
81
  end
56
82
 
57
83
  private
58
84
 
59
85
  def subscribe_to_subject(subject_pattern, custom_handler = nil, model_mappings = {})
60
- NatsWave.logger.info "Subscribing to: #{subject_pattern}"
86
+ NatsWave.logger.info "🔍 Attempting to subscribe to: #{subject_pattern}"
61
87
 
62
- subscription = @nats_client.subscribe(
88
+ # Create the NATS subscription
89
+ nats_subscription = @client.subscribe(
63
90
  subject_pattern,
64
91
  queue: @config.queue_group
65
92
  ) do |msg|
66
- process_message(msg,
67
- 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
68
101
  end
69
102
 
70
- @subscriptions << subscription
103
+ # Add to NATS subscriptions array
104
+ @nats_subscriptions << nats_subscription
105
+ NatsWave.logger.info "✅ Successfully subscribed to #{subject_pattern} (total: #{@nats_subscriptions.size})"
71
106
  end
72
107
 
73
108
  def process_message(raw_message, custom_handler, model_mappings)
74
109
  return unless should_process_message?(raw_message)
75
110
 
76
- Metrics.track_processing_time(raw_message) do
77
- message = parse_message(raw_message)
78
- processed_message = apply_middleware(message)
79
-
80
- if custom_handler
81
- custom_handler.call(processed_message)
82
- else
83
- handle_model_sync(
84
- processed_message, model_mappings
85
- )
86
- end
111
+ NatsWave.logger.debug "🔄 Processing message: #{raw_message[0..200]}..."
112
+
113
+ message = parse_message(raw_message)
114
+
115
+ if custom_handler
116
+ custom_handler.call(message)
117
+ else
118
+ handle_model_sync(message, model_mappings)
87
119
  end
88
120
 
89
- NatsWave.logger.debug('Successfully processed message')
121
+ NatsWave.logger.debug('Successfully processed message')
90
122
  rescue StandardError => e
91
123
  handle_error(e, raw_message, message)
124
+ # Don't re-raise - this would kill the subscription
92
125
  end
93
126
 
94
- def should_process_message?(raw_message)
95
- # Skip messages from same service instance
96
- message = JSON.parse(raw_message)
127
+ def should_process_message?(raw_message_data)
128
+ message = JSON.parse(raw_message_data)
97
129
  source = message['source'] || {}
98
130
 
99
- return false if source['service'] == @config.service_name &&
100
- source['instance_id'] == @config.instance_id
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
101
136
 
102
137
  true
103
- rescue JSON::ParserError
138
+ rescue JSON::ParserError => e
139
+ NatsWave.logger.error "Failed to parse message for filtering: #{e.message}"
104
140
  false
105
141
  end
106
142
 
107
- def parse_message(raw_message)
108
- @message_transformer.parse_message(raw_message)
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
109
166
  end
110
167
 
111
- def apply_middleware(message)
112
- @middleware_stack.reduce(message) do |msg, middleware|
113
- middleware.call(msg)
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
114
187
  end
188
+ rescue => e
189
+ NatsWave.logger.error "Failed to reconnect: #{e.message}"
190
+ end
191
+
192
+ # ... rest of your existing methods (parse_message, handle_model_sync, handle_error)
193
+ def parse_message(raw_message)
194
+ @message_transformer.parse_message(raw_message)
115
195
  end
116
196
 
117
197
  def handle_model_sync(message, model_mappings)
@@ -158,4 +238,4 @@ module NatsWave
158
238
  # Continue processing - don't raise to avoid breaking subscription
159
239
  end
160
240
  end
161
- end
241
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NatsWave
4
- VERSION = '1.1.4'
4
+ VERSION = '1.1.7'
5
5
  end
data/lib/nats_wave.rb CHANGED
@@ -82,8 +82,9 @@ module NatsWave
82
82
  client.subscribe(
83
83
  subjects: subjects,
84
84
  model_mappings: model_mappings,
85
- handler: block
85
+ &block
86
86
  )
87
+ NatsWave.logger.info "Completely subscribed to #{subjects.size} subjects"
87
88
  end
88
89
 
89
90
  def start_subscriber
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.4
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-17 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
@@ -318,9 +318,6 @@ files:
318
318
  - README.md
319
319
  - Rakefile
320
320
  - config/nats_wave.yml
321
- - examples/catalog_model.rb
322
- - examples/configuration_examples.rb
323
- - examples/user_model.rb
324
321
  - lib/generators/nats_wave/install_generator.rb
325
322
  - lib/generators/nats_wave/templates/README
326
323
  - lib/generators/nats_wave/templates/create_nats_wave_failed_messages.rb
@@ -1,36 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Catalog < ApplicationRecord
4
- include NatsWave::NatsPublishable
5
-
6
- nats_publishable(
7
- actions: %i[create update], # Don't publish deletes
8
- skip_attributes: %i[view_count search_vector internal_notes],
9
- include_associations: %i[category variants]
10
- )
11
-
12
- # Custom serializer support
13
- def self.nats_wave_attribute_transformations
14
- {
15
- hammer_estimate: ->(value, _action) { value.to_f.round(2) },
16
- name: :sanitize_name,
17
- description: lambda { |value, _action|
18
- ActionView::Base.full_sanitizer.sanitize(value)
19
- }
20
- }
21
- end
22
-
23
- private
24
-
25
- def sanitize_name(name, _action)
26
- name&.strip&.titleize
27
- end
28
-
29
- def nats_wave_metadata
30
- {
31
- organization: organization&.name,
32
- make:,
33
- catalog_tracked: track_catalog?
34
- }
35
- end
36
- end
@@ -1,68 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Basic configuration
4
- NatsWave.configure do |config|
5
- config.nats_url = 'nats://localhost:4222'
6
- config.service_name = 'ecommerce_api'
7
- config.default_subject_prefix = 'ecommerce.events'
8
- end
9
-
10
- # Advanced configuration with model mappings
11
- NatsWave.configure do |config|
12
- config.nats_url = ENV.fetch('NATS_URL', 'nats://localhost:4222')
13
- config.service_name = 'catalog_service'
14
- config.environment = Rails.env
15
-
16
- # Subscribe to other team's events
17
- config.add_subscription('warehouse.assets.*')
18
- config.add_subscription('external.catalogs.*')
19
- config.add_subscription('payments.transactions.*')
20
-
21
- # Map external models to local models
22
- config.add_model_mapping('WarehouseAsset', 'Catalog', {
23
- 'asset_name' => 'name',
24
- 'asset_description' => 'description',
25
- 'asset_value' => 'hammer_estimate',
26
- 'asset_id' => 'external_icn',
27
- 'model' => 'category'
28
- })
29
-
30
- config.add_model_mapping('ExternalCatalog', 'Catalog', {
31
- 'package_name' => 'name',
32
- 'package_cost' => 'hammer_estimate',
33
- 'external_vin' => 'vin'
34
- })
35
-
36
- # Custom transformation rules
37
- config.transformation_rules = {
38
- 'hammer_estimate' => ->(value) { value.to_f.round(2) },
39
- 'name' => ->(value) { value&.titleize },
40
- 'email' => ->(value) { value&.downcase }
41
- }
42
-
43
- # Custom subscription with handler
44
- config.add_subscription('payments.failed.*') do |message|
45
- PaymentFailureHandler.process(message)
46
- end
47
-
48
- # Middleware configuration
49
- config.middleware_authentication_enabled = Rails.env.production?
50
- config.auth_secret_key = ENV['NATS_AUTH_SECRET']
51
- config.middleware_validation_enabled = true
52
- config.middleware_logging_enabled = true
53
- config.log_level = Rails.env.production? ? 'info' : 'debug'
54
- end
55
-
56
- # Catalogion configuration
57
- if Rails.env.production?
58
- NatsWave.configure do |config|
59
- config.nats_url = ENV.fetch('NATS_CLUSTER_URL')
60
- config.connection_pool_size = 20
61
- config.async_publishing = true
62
- config.middleware_authentication_enabled = true
63
- config.auth_secret_key = ENV.fetch('NATS_AUTH_SECRET')
64
- config.schema_registry_url = ENV['SCHEMA_REGISTRY_URL']
65
- config.max_retries = 5
66
- config.retry_delay = 10
67
- end
68
- end
@@ -1,58 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class User < ApplicationRecord
4
- include NatsWave::NatsPublishable
5
-
6
- # Configure NATS publishing
7
- nats_publishable(
8
- actions: %i[create update destroy],
9
- skip_attributes: %i[password_digest remember_token password_reset_token],
10
- include_associations: %i[profile organization],
11
- subject_prefix: 'users',
12
- metadata: { tenant_id: 'main' },
13
- if: -> { active? },
14
- unless: -> { system_user? }
15
- )
16
-
17
- # Define unique attributes for syncing
18
- def self.nats_wave_unique_attributes
19
- %i[email external_icn username]
20
- end
21
-
22
- # Define attributes that shouldn't be synced from other instances
23
- def self.nats_wave_skip_sync_attributes
24
- %i[last_login_at login_count password_digest]
25
- end
26
-
27
- # Custom attribute transformations
28
- def self.nats_wave_attribute_transformations
29
- {
30
- email: ->(value, _action) { value&.downcase&.strip },
31
- phone: :format_phone_number
32
- }
33
- end
34
-
35
- private
36
-
37
- # Custom metadata for NATS messages
38
- def nats_wave_metadata
39
- {
40
- tenant_id: organization&.tenant_id,
41
- user_type: account_type,
42
- subscription_level: subscription&.level
43
- }
44
- end
45
-
46
- def format_phone_number(phone, _action)
47
- # Custom transformation logic
48
- phone&.gsub(/\D/, '')
49
- end
50
-
51
- def active?
52
- status == 'active'
53
- end
54
-
55
- def system_user?
56
- email&.ends_with?('@system.internal')
57
- end
58
- end