nats_wave 1.1.0

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.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.idea/.gitignore +8 -0
  3. data/.idea/misc.xml +4 -0
  4. data/.idea/modules.xml +8 -0
  5. data/.idea/nats_wave.iml +169 -0
  6. data/.idea/vcs.xml +6 -0
  7. data/.rspec +3 -0
  8. data/.rubocop.yml +16 -0
  9. data/CHANGELOG.md +5 -0
  10. data/CODE_OF_CONDUCT.md +136 -0
  11. data/Gemfile +15 -0
  12. data/Gemfile.lock +332 -0
  13. data/LICENSE.txt +21 -0
  14. data/README.md +985 -0
  15. data/Rakefile +12 -0
  16. data/config/nats_wave.yml +65 -0
  17. data/examples/catalog_model.rb +36 -0
  18. data/examples/configuration_examples.rb +68 -0
  19. data/examples/user_model.rb +58 -0
  20. data/lib/generators/nats_wave/install_generator.rb +40 -0
  21. data/lib/generators/nats_wave/templates/README +31 -0
  22. data/lib/generators/nats_wave/templates/create_nats_wave_failed_messages.rb +20 -0
  23. data/lib/generators/nats_wave/templates/create_nats_wave_failed_subscriptions.rb +20 -0
  24. data/lib/generators/nats_wave/templates/initializer.rb +64 -0
  25. data/lib/generators/nats_wave/templates/nats_wave.yml +65 -0
  26. data/lib/nats_wave/adapters/active_record.rb +206 -0
  27. data/lib/nats_wave/adapters/datadog_metrics.rb +93 -0
  28. data/lib/nats_wave/auto_registration.rb +109 -0
  29. data/lib/nats_wave/client.rb +142 -0
  30. data/lib/nats_wave/concerns/mappable.rb +172 -0
  31. data/lib/nats_wave/concerns/publishable.rb +216 -0
  32. data/lib/nats_wave/configuration.rb +105 -0
  33. data/lib/nats_wave/database_connector.rb +50 -0
  34. data/lib/nats_wave/dead_letter_queue.rb +146 -0
  35. data/lib/nats_wave/errors.rb +27 -0
  36. data/lib/nats_wave/message_transformer.rb +95 -0
  37. data/lib/nats_wave/metrics.rb +220 -0
  38. data/lib/nats_wave/middleware/authentication.rb +65 -0
  39. data/lib/nats_wave/middleware/base.rb +19 -0
  40. data/lib/nats_wave/middleware/logging.rb +58 -0
  41. data/lib/nats_wave/middleware/validation.rb +74 -0
  42. data/lib/nats_wave/model_mapper.rb +125 -0
  43. data/lib/nats_wave/model_registry.rb +150 -0
  44. data/lib/nats_wave/publisher.rb +151 -0
  45. data/lib/nats_wave/railtie.rb +111 -0
  46. data/lib/nats_wave/schema_registry.rb +77 -0
  47. data/lib/nats_wave/subscriber.rb +161 -0
  48. data/lib/nats_wave/version.rb +5 -0
  49. data/lib/nats_wave.rb +97 -0
  50. data/lib/tasks/nats_wave.rake +360 -0
  51. data/sig/nats_wave.rbs +5 -0
  52. metadata +385 -0
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NatsWave
4
+ class ModelRegistry
5
+ @mappings = {}
6
+ @reverse_mappings = {}
7
+ @subscriptions = []
8
+
9
+ class << self
10
+ # Register a mapping from local model to external models
11
+ def register_mapping(local_model, external_mappings)
12
+ @mappings[local_model] = external_mappings
13
+ end
14
+
15
+ # Register a reverse mapping from external model to local model
16
+ def register_reverse_mapping(external_model, local_model, mapping_config)
17
+ @reverse_mappings[external_model] ||= {}
18
+ @reverse_mappings[external_model][local_model] = mapping_config
19
+ end
20
+
21
+ # Register a subscription
22
+ def register_subscription(subjects:, model:, external_model: nil, handler: nil, queue_group: nil)
23
+ subscription = {
24
+ subjects: Array(subjects),
25
+ model: model,
26
+ external_model: external_model,
27
+ handler: handler,
28
+ queue_group: queue_group,
29
+ registered_at: Time.current
30
+ }
31
+
32
+ @subscriptions << subscription
33
+
34
+ Rails.logger.debug "📡 Registered subscription: #{model} -> #{subjects.join(', ')}" if defined?(Rails)
35
+ end
36
+
37
+ # Get all registered subscriptions
38
+ def subscriptions
39
+ @subscriptions
40
+ end
41
+
42
+ # Get subscriptions for a specific model
43
+ def subscriptions_for_model(model_name)
44
+ @subscriptions.select { |sub| sub[:model] == model_name }
45
+ end
46
+
47
+ # Get all unique subjects to subscribe to
48
+ def all_subscription_subjects
49
+ @subscriptions.flat_map { |sub| sub[:subjects] }.uniq
50
+ end
51
+
52
+ # Get subscription handlers grouped by subject pattern
53
+ def subscription_handlers
54
+ handlers = {}
55
+
56
+ @subscriptions.each do |subscription|
57
+ subscription[:subjects].each do |subject|
58
+ handlers[subject] ||= []
59
+ handlers[subject] << {
60
+ model: subscription[:model],
61
+ external_model: subscription[:external_model],
62
+ handler: subscription[:handler],
63
+ queue_group: subscription[:queue_group]
64
+ }
65
+ end
66
+ end
67
+
68
+ handlers
69
+ end
70
+
71
+ # Get local model mappings for an external model
72
+ def local_models_for(external_model)
73
+ @reverse_mappings[external_model] || {}
74
+ end
75
+
76
+ # Get external model mappings for a local model
77
+ def external_models_for(local_model)
78
+ @mappings[local_model] || {}
79
+ end
80
+
81
+ # Check if an external model can be synced
82
+ def can_sync_external_model?(external_model)
83
+ @reverse_mappings.key?(external_model)
84
+ end
85
+
86
+ # Get all registered mappings
87
+ def all_mappings
88
+ {
89
+ local_to_external: @mappings,
90
+ external_to_local: @reverse_mappings
91
+ }
92
+ end
93
+
94
+ # Find the best local model for an external model
95
+ def best_local_model_for(external_model, data = {})
96
+ local_mappings = local_models_for(external_model)
97
+ return nil if local_mappings.empty?
98
+
99
+ # If only one mapping, use it
100
+ return local_mappings.keys.first if local_mappings.size == 1
101
+
102
+ # If multiple mappings, try to find the best match based on conditions
103
+ local_mappings.each do |local_model, config|
104
+ conditions = config[:conditions] || {}
105
+
106
+ if conditions.empty? || evaluate_conditions(conditions, data)
107
+ return local_model
108
+ end
109
+ end
110
+
111
+ # Fallback to first available
112
+ local_mappings.keys.first
113
+ end
114
+
115
+ # Clear all mappings and subscriptions (useful for testing)
116
+ def clear!
117
+ @mappings.clear
118
+ @reverse_mappings.clear
119
+ @subscriptions.clear
120
+ end
121
+
122
+ # Get subscription statistics
123
+ def subscription_stats
124
+ {
125
+ total_subscriptions: @subscriptions.size,
126
+ unique_subjects: all_subscription_subjects.size,
127
+ models_with_subscriptions: @subscriptions.map { |s| s[:model] }.uniq.size,
128
+ subscription_breakdown: @subscriptions.group_by { |s| s[:model] }.transform_values(&:size)
129
+ }
130
+ end
131
+
132
+ private
133
+
134
+ def evaluate_conditions(conditions, data)
135
+ conditions.all? do |field, expected_value|
136
+ case expected_value
137
+ when Proc
138
+ expected_value.call(data[field])
139
+ when Regexp
140
+ expected_value.match?(data[field].to_s)
141
+ when Array
142
+ expected_value.include?(data[field])
143
+ else
144
+ data[field] == expected_value
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,151 @@
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
+
43
+ module NatsWave
44
+ class Publisher
45
+ attr_reader :config, :nats_client
46
+
47
+ def initialize(config, nats_client, middleware_stack)
48
+ @config = config
49
+ @nats_client = nats_client
50
+ @middleware_stack = middleware_stack
51
+ @message_transformer = MessageTransformer.new(config)
52
+ @dead_letter_queue = DeadLetterQueue.new(config) if config.dead_letter_queue
53
+ end
54
+
55
+ def publish(subject:, model:, action:, data:, metadata: {})
56
+ return unless @config.publishing_enabled
57
+
58
+ message = build_message(subject, model, action, data, metadata)
59
+ processed_message = apply_middleware(message)
60
+ full_subject = build_full_subject(subject)
61
+
62
+ if @config.async_publishing && defined?(Concurrent)
63
+ publish_async(full_subject, processed_message)
64
+ else
65
+ publish_sync(full_subject, processed_message)
66
+ end
67
+
68
+ Metrics.increment_published_messages(full_subject)
69
+ NatsWave.logger.debug("Published message to #{full_subject}")
70
+ rescue => e
71
+ NatsWave.logger.error("Failed to publish message: #{e.message}")
72
+ @dead_letter_queue&.store_failed_message(message, e, 0)
73
+ raise PublishError, "Failed to publish message: #{e.message}"
74
+ end
75
+
76
+ def publish_batch(events)
77
+ batch_message = {
78
+ batch_id: SecureRandom.uuid,
79
+ events: events,
80
+ timestamp: Time.current.iso8601,
81
+ source: build_source_info
82
+ }
83
+
84
+ processed_message = apply_middleware(batch_message)
85
+ subject = "#{@config.default_subject_prefix}.batch"
86
+
87
+ @nats_client.publish(subject, processed_message.to_json)
88
+
89
+ Metrics.increment_published_messages(subject)
90
+ NatsWave.logger.info("Published batch with #{events.size} events")
91
+ end
92
+
93
+ def connected?
94
+ @nats_client&.connected?
95
+ end
96
+
97
+ def disconnect
98
+ # Publisher cleanup if needed
99
+ end
100
+
101
+ private
102
+
103
+ def build_message(subject, model, action, data, metadata)
104
+ @message_transformer.build_standard_message(
105
+ subject: subject,
106
+ model: model,
107
+ action: action,
108
+ data: data,
109
+ metadata: metadata,
110
+ source: build_source_info
111
+ )
112
+ end
113
+
114
+ def build_source_info
115
+ {
116
+ service: @config.service_name,
117
+ version: @config.version,
118
+ instance_id: @config.instance_id,
119
+ environment: defined?(Rails) ? Rails.env : 'test'
120
+ }
121
+ end
122
+
123
+ def build_full_subject(subject)
124
+ if @config.default_subject_prefix && !subject.start_with?(@config.default_subject_prefix)
125
+ "#{@config.default_subject_prefix}.#{subject}"
126
+ else
127
+ subject
128
+ end
129
+ end
130
+
131
+ def apply_middleware(message)
132
+ @middleware_stack.reduce(message) do |msg, middleware|
133
+ middleware.call(msg)
134
+ end
135
+ end
136
+
137
+ def publish_sync(subject, message)
138
+ @nats_client.publish(subject, message.to_json)
139
+ end
140
+
141
+ def publish_async(subject, message)
142
+ if defined?(Concurrent)
143
+ Concurrent::Future.execute do
144
+ @nats_client.publish(subject, message.to_json)
145
+ end
146
+ else
147
+ publish_sync(subject, message)
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/railtie'
4
+
5
+ module NatsWave
6
+ class Railtie < Rails::Railtie
7
+ config.nats_wave = ActiveSupport::OrderedOptions.new
8
+
9
+ initializer "nats_wave.configure" do |app|
10
+ # Load configuration from Rails config
11
+ if app.config.respond_to?(:nats_wave)
12
+ NatsWave.configure do |config|
13
+ app.config.nats_wave.each do |key, value|
14
+ config.send("#{key}=", value) if config.respond_to?("#{key}=")
15
+ end
16
+ end
17
+ end
18
+
19
+ # Load YAML configuration if it exists
20
+ config_file = Rails.root.join('config', 'nats_wave.yml')
21
+ if File.exist?(config_file)
22
+ begin
23
+ yaml_config = YAML.load_file(config_file)[Rails.env]
24
+
25
+ NatsWave.configure do |config|
26
+ # NATS Configuration
27
+ config.nats_url = yaml_config.dig('nats', 'url') if yaml_config.dig('nats', 'url')
28
+ config.connection_pool_size = yaml_config.dig('nats', 'connection_pool_size') if yaml_config.dig('nats', 'connection_pool_size')
29
+ config.timeout = yaml_config.dig('nats', 'timeout') if yaml_config.dig('nats', 'timeout')
30
+ config.reconnect_attempts = yaml_config.dig('nats', 'reconnect_attempts') if yaml_config.dig('nats', 'reconnect_attempts')
31
+
32
+ # Service Configuration
33
+ config.service_name = yaml_config.dig('publishing', 'default_subject_prefix') if yaml_config.dig('publishing', 'default_subject_prefix')
34
+ config.version = Rails.application.class.parent_name.underscore if defined?(Rails.application.class.parent_name)
35
+
36
+ # Publishing Configuration
37
+ config.publishing_enabled = yaml_config.dig('publishing', 'enabled') unless yaml_config.dig('publishing', 'enabled').nil?
38
+ config.default_subject_prefix = yaml_config.dig('publishing', 'default_subject_prefix') if yaml_config.dig('publishing', 'default_subject_prefix')
39
+ config.batch_size = yaml_config.dig('publishing', 'batch_size') if yaml_config.dig('publishing', 'batch_size')
40
+ config.async_publishing = yaml_config.dig('publishing', 'async') unless yaml_config.dig('publishing', 'async').nil?
41
+
42
+ # Subscription Configuration
43
+ config.subscription_enabled = yaml_config.dig('subscription', 'enabled') unless yaml_config.dig('subscription', 'enabled').nil?
44
+ config.queue_group = yaml_config.dig('subscription', 'queue_group') if yaml_config.dig('subscription', 'queue_group')
45
+
46
+ # Middleware Configuration
47
+ config.middleware_authentication_enabled = yaml_config.dig('middleware', 'authentication', 'enabled') unless yaml_config.dig('middleware', 'authentication', 'enabled').nil?
48
+ config.middleware_validation_enabled = yaml_config.dig('middleware', 'validation', 'enabled') unless yaml_config.dig('middleware', 'validation', 'enabled').nil?
49
+ config.middleware_logging_enabled = yaml_config.dig('middleware', 'logging', 'enabled') unless yaml_config.dig('middleware', 'logging', 'enabled').nil?
50
+ config.auth_secret_key = yaml_config.dig('middleware', 'authentication', 'secret_key') if yaml_config.dig('middleware', 'authentication', 'secret_key')
51
+ config.schema_registry_url = yaml_config.dig('middleware', 'validation', 'schema_registry') if yaml_config.dig('middleware', 'validation', 'schema_registry')
52
+ config.log_level = yaml_config.dig('middleware', 'logging', 'level') if yaml_config.dig('middleware', 'logging', 'level')
53
+
54
+ # Error Handling Configuration
55
+ config.max_retries = yaml_config.dig('error_handling', 'max_retries') if yaml_config.dig('error_handling', 'max_retries')
56
+ config.retry_delay = yaml_config.dig('error_handling', 'retry_delay') if yaml_config.dig('error_handling', 'retry_delay')
57
+ config.dead_letter_queue = yaml_config.dig('error_handling', 'dead_letter_queue') if yaml_config.dig('error_handling', 'dead_letter_queue')
58
+ end
59
+ rescue => e
60
+ Rails.logger.warn "Failed to load NATS Wave YAML configuration: #{e.message}"
61
+ end
62
+
63
+ # Environment-specific overrides
64
+ if Rails.env.development?
65
+ NatsWave.configure do |config|
66
+ config.log_level = "debug"
67
+ config.middleware_authentication_enabled = false
68
+ end
69
+ elsif Rails.env.test?
70
+ NatsWave.configure do |config|
71
+ config.publishing_enabled = false
72
+ config.subscription_enabled = false
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ initializer "nats_wave.active_record" do
79
+ ActiveSupport.on_load(:active_record) do
80
+ require 'nats_wave/concerns/publishable'
81
+ include NatsWave::ActiveRecordExtension
82
+ end
83
+ end
84
+
85
+ rake_tasks do
86
+ load "tasks/nats_wave.rake"
87
+ end
88
+
89
+ generators do
90
+ require 'generators/nats_wave/install_generator'
91
+ end
92
+
93
+ # Initialize client on Rails boot
94
+ initializer "nats_wave.initialize_client", after: :load_config_initializers do
95
+ Rails.application.config.after_initialize do
96
+ if NatsWave.configuration&.publishing_enabled || NatsWave.configuration&.subscription_enabled
97
+ Thread.new do
98
+ sleep 1 # Give Rails time to fully boot
99
+ begin
100
+ NatsWave.client
101
+ NatsWave.logger.info "NatsWave client initialized"
102
+ NatsWave.logger.info "NATS URL #{@nats_url}"
103
+ rescue => e
104
+ NatsWave.logger.error "Failed to initialize NatsWave client: #{e.message}"
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'uri'
5
+ require 'json'
6
+
7
+ module NatsWave
8
+ class SchemaRegistry
9
+ def initialize(registry_url)
10
+ @registry_url = registry_url
11
+ @schemas = {}
12
+ @uri = URI.parse(registry_url) if registry_url
13
+ end
14
+
15
+ def validate_message(message, schema_version)
16
+ return true unless @uri
17
+
18
+ schema = get_schema(schema_version)
19
+ return true unless schema
20
+
21
+ JSON::Validator.validate(schema, message)
22
+ end
23
+
24
+ def register_schema(model, version, schema)
25
+ return false unless @uri
26
+
27
+ endpoint = "/schemas/#{model}/#{version}"
28
+ response = make_request(:post, endpoint, schema.to_json)
29
+
30
+ if response.code == '200'
31
+ NatsWave.logger.info("Registered schema for #{model} v#{version}")
32
+ true
33
+ else
34
+ NatsWave.logger.error("Failed to register schema: #{response.body}")
35
+ false
36
+ end
37
+ end
38
+
39
+ def get_schema(schema_version)
40
+ return @schemas[schema_version] if @schemas[schema_version]
41
+
42
+ endpoint = "/schemas/#{schema_version}"
43
+ response = make_request(:get, endpoint)
44
+
45
+ if response.code == '200'
46
+ @schemas[schema_version] = JSON.parse(response.body)
47
+ else
48
+ NatsWave.logger.warn("Schema not found: #{schema_version}")
49
+ nil
50
+ end
51
+ rescue StandardError => e
52
+ NatsWave.logger.error("Failed to fetch schema #{schema_version}: #{e.message}")
53
+ nil
54
+ end
55
+
56
+ private
57
+
58
+ def make_request(method, endpoint, body = nil)
59
+ return nil unless @uri
60
+
61
+ http = Net::HTTP.new(@uri.host, @uri.port)
62
+ http.use_ssl = @uri.scheme == 'https'
63
+
64
+ request = case method
65
+ when :get
66
+ Net::HTTP::Get.new(endpoint)
67
+ when :post
68
+ Net::HTTP::Post.new(endpoint)
69
+ end
70
+
71
+ request['Content-Type'] = 'application/json'
72
+ request.body = body if body
73
+
74
+ http.request(request)
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NatsWave
4
+ class Subscriber
5
+ attr_reader :config, :nats_client
6
+
7
+ def initialize(config, nats_client, middleware_stack)
8
+ @config = config
9
+ @nats_client = nats_client
10
+ @middleware_stack = middleware_stack
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
+ @subscriptions = []
16
+ @running = false
17
+ end
18
+
19
+ def start
20
+ return if @running
21
+ return unless @config.subscription_enabled
22
+
23
+ @running = true
24
+ NatsWave.logger.info "Starting NATS subscriber for #{@config.service_name}"
25
+
26
+ @config.subscriptions.each do |subscription|
27
+ subscribe_to_subject(subscription[:subject],
28
+ subscription[:handler])
29
+ end
30
+
31
+ NatsWave.logger.info "Subscribed to #{@config.subscriptions.size} subjects"
32
+ end
33
+
34
+ def subscribe(subjects:, model_mappings: {}, handler: nil)
35
+ subjects = Array(subjects)
36
+
37
+ subjects.each do |subject|
38
+ subscribe_to_subject(subject, handler,
39
+ model_mappings)
40
+ end
41
+ end
42
+
43
+ def unsubscribe_all
44
+ @subscriptions.each(&:unsubscribe)
45
+ @subscriptions.clear
46
+ @running = false
47
+ end
48
+
49
+ def database_connected?
50
+ @database_connector.connected?
51
+ end
52
+
53
+ def disconnect
54
+ unsubscribe_all
55
+ end
56
+
57
+ private
58
+
59
+ def subscribe_to_subject(subject_pattern, custom_handler = nil, model_mappings = {})
60
+ NatsWave.logger.info "Subscribing to: #{subject_pattern}"
61
+
62
+ subscription = @nats_client.subscribe(
63
+ subject_pattern,
64
+ queue: @config.queue_group
65
+ ) do |msg|
66
+ process_message(msg,
67
+ custom_handler, model_mappings)
68
+ end
69
+
70
+ @subscriptions << subscription
71
+ end
72
+
73
+ def process_message(raw_message, custom_handler, model_mappings)
74
+ return unless should_process_message?(raw_message)
75
+
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
87
+ end
88
+
89
+ NatsWave.logger.debug('Successfully processed message')
90
+ rescue StandardError => e
91
+ handle_error(e, raw_message, message)
92
+ end
93
+
94
+ def should_process_message?(raw_message)
95
+ # Skip messages from same service instance
96
+ message = JSON.parse(raw_message)
97
+ source = message['source'] || {}
98
+
99
+ return false if source['service'] == @config.service_name &&
100
+ source['instance_id'] == @config.instance_id
101
+
102
+ true
103
+ rescue JSON::ParserError
104
+ false
105
+ end
106
+
107
+ def parse_message(raw_message)
108
+ @message_transformer.parse_message(raw_message)
109
+ end
110
+
111
+ def apply_middleware(message)
112
+ @middleware_stack.reduce(message) do |msg, middleware|
113
+ middleware.call(msg)
114
+ end
115
+ end
116
+
117
+ def handle_model_sync(message, model_mappings)
118
+ source_model = message['model']
119
+ mapping = model_mappings[source_model] || @config.model_mappings[source_model]
120
+
121
+ return unless mapping
122
+
123
+ target_model = mapping[:target_model]
124
+ field_mappings = mapping[:field_mappings] || {}
125
+ transformations = mapping[:transformations] || {}
126
+
127
+ # Transform the data
128
+ transformed_data = @model_mapper.transform_data(
129
+ message['data'],
130
+ field_mappings,
131
+ transformations
132
+ )
133
+
134
+ # Apply to database
135
+ @database_connector.apply_change(
136
+ model: target_model,
137
+ action: message['action'],
138
+ data: transformed_data,
139
+ metadata: message['metadata']
140
+ )
141
+
142
+ NatsWave.logger.info "Synced #{source_model} -> #{target_model}: #{message['action']}"
143
+ end
144
+
145
+ def handle_error(error, raw_message, parsed_message = nil)
146
+ event_id = parsed_message&.dig('event_id') || 'unknown'
147
+ subject = parsed_message&.dig('subject') || 'unknown'
148
+
149
+ NatsWave.logger.error("Error processing message #{event_id} from #{subject}: #{error.message}")
150
+
151
+ # Send to dead letter queue
152
+ @dead_letter_queue.store_failed_message(
153
+ parsed_message || raw_message,
154
+ error,
155
+ 0 # retry count
156
+ )
157
+
158
+ # Continue processing - don't raise to avoid breaking subscription
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NatsWave
4
+ VERSION = '1.1.0'
5
+ end