jetstream_bridge 3.0.1 → 4.0.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.
@@ -12,34 +12,178 @@ require_relative 'subscription_manager'
12
12
  require_relative 'inbox/inbox_processor'
13
13
 
14
14
  module JetstreamBridge
15
- # Subscribes to destination subject and processes messages via a pull durable.
15
+ # Subscribes to destination subject and processes messages via a pull durable consumer.
16
+ #
17
+ # The Consumer provides reliable message processing with features like:
18
+ # - Durable pull-based subscriptions with configurable batch sizes
19
+ # - Optional idempotent inbox pattern for exactly-once processing
20
+ # - Middleware support for cross-cutting concerns (logging, metrics, tracing)
21
+ # - Automatic reconnection and error recovery
22
+ # - Graceful shutdown with message draining
23
+ #
24
+ # @example Basic consumer
25
+ # consumer = JetstreamBridge::Consumer.new do |event|
26
+ # puts "Received: #{event.type} - #{event.payload.to_h}"
27
+ # # Process event...
28
+ # end
29
+ # consumer.run!
30
+ #
31
+ # @example Consumer with middleware
32
+ # consumer = JetstreamBridge::Consumer.new(handler)
33
+ # consumer.use(JetstreamBridge::Consumer::LoggingMiddleware.new)
34
+ # consumer.use(JetstreamBridge::Consumer::MetricsMiddleware.new)
35
+ # consumer.run!
36
+ #
37
+ # @example Using convenience method
38
+ # JetstreamBridge.subscribe do |event|
39
+ # ProcessEventJob.perform_later(event.to_h)
40
+ # end.run!
41
+ #
16
42
  class Consumer
43
+ # Default number of messages to fetch in each batch
17
44
  DEFAULT_BATCH_SIZE = 25
45
+ # Timeout for fetching messages from NATS (seconds)
18
46
  FETCH_TIMEOUT_SECS = 5
47
+ # Initial sleep duration when no messages available (seconds)
19
48
  IDLE_SLEEP_SECS = 0.05
49
+ # Maximum sleep duration during idle periods (seconds)
20
50
  MAX_IDLE_BACKOFF_SECS = 1.0
21
51
 
22
- def initialize(durable_name: nil, batch_size: nil, &block)
23
- raise ArgumentError, 'handler block required' unless block_given?
52
+ # Alias middleware classes for easier access
53
+ MiddlewareChain = ConsumerMiddleware::MiddlewareChain
54
+ LoggingMiddleware = ConsumerMiddleware::LoggingMiddleware
55
+ ErrorHandlingMiddleware = ConsumerMiddleware::ErrorHandlingMiddleware
56
+ MetricsMiddleware = ConsumerMiddleware::MetricsMiddleware
57
+ TracingMiddleware = ConsumerMiddleware::TracingMiddleware
58
+ TimeoutMiddleware = ConsumerMiddleware::TimeoutMiddleware
59
+
60
+ # @return [String] Durable consumer name
61
+ attr_reader :durable
62
+ # @return [Integer] Batch size for message fetching
63
+ attr_reader :batch_size
64
+ # @return [MiddlewareChain] Middleware chain for processing
65
+ attr_reader :middleware_chain
66
+
67
+ # Initialize a new Consumer instance.
68
+ #
69
+ # @param handler [Proc, #call, nil] Message handler that processes events.
70
+ # Must respond to #call(event) or #call(event, subject, deliveries).
71
+ # @param durable_name [String, nil] Optional durable consumer name override.
72
+ # Defaults to config.durable_name.
73
+ # @param batch_size [Integer, nil] Number of messages to fetch per batch.
74
+ # Defaults to DEFAULT_BATCH_SIZE (25).
75
+ # @yield [event] Optional block as handler. Receives Models::Event object.
76
+ #
77
+ # @raise [ArgumentError] If neither handler nor block provided
78
+ # @raise [ArgumentError] If destination_app not configured
79
+ # @raise [ConnectionError] If unable to connect to NATS
80
+ #
81
+ # @example With proc handler
82
+ # handler = ->(event) { puts "Received: #{event.type}" }
83
+ # consumer = JetstreamBridge::Consumer.new(handler)
84
+ #
85
+ # @example With block
86
+ # consumer = JetstreamBridge::Consumer.new do |event|
87
+ # UserEventHandler.process(event)
88
+ # end
89
+ #
90
+ # @example With custom configuration
91
+ # consumer = JetstreamBridge::Consumer.new(
92
+ # handler,
93
+ # durable_name: "my-consumer",
94
+ # batch_size: 10
95
+ # )
96
+ #
97
+ def initialize(handler = nil, durable_name: nil, batch_size: nil, &block)
98
+ @handler = handler || block
99
+ raise ArgumentError, 'handler or block required' unless @handler
24
100
 
25
- @handler = block
26
101
  @batch_size = Integer(batch_size || DEFAULT_BATCH_SIZE)
27
102
  @durable = durable_name || JetstreamBridge.config.durable_name
28
103
  @idle_backoff = IDLE_SLEEP_SECS
29
104
  @running = true
30
105
  @shutdown_requested = false
31
106
  @jts = Connection.connect!
107
+ @middleware_chain = MiddlewareChain.new
32
108
 
33
109
  ensure_destination!
34
110
 
35
111
  @sub_mgr = SubscriptionManager.new(@jts, @durable, JetstreamBridge.config)
36
- @processor = MessageProcessor.new(@jts, @handler)
112
+ @processor = MessageProcessor.new(@jts, @handler, middleware_chain: @middleware_chain)
37
113
  @inbox_proc = InboxProcessor.new(@processor) if JetstreamBridge.config.use_inbox
38
114
 
39
115
  ensure_subscription!
40
116
  setup_signal_handlers
41
117
  end
42
118
 
119
+ # Add middleware to the processing chain.
120
+ #
121
+ # Middleware is executed in the order it's added. Each middleware must respond
122
+ # to #call(event, &block) and yield to continue the chain.
123
+ #
124
+ # @param middleware [Object] Middleware that responds to #call(event, &block).
125
+ # Must yield to continue processing.
126
+ # @return [self] Returns self for method chaining
127
+ #
128
+ # @example Adding multiple middleware
129
+ # consumer = JetstreamBridge.subscribe { |event| process(event) }
130
+ # consumer.use(JetstreamBridge::Consumer::LoggingMiddleware.new)
131
+ # consumer.use(JetstreamBridge::Consumer::MetricsMiddleware.new)
132
+ # consumer.use(JetstreamBridge::Consumer::TimeoutMiddleware.new(timeout: 30))
133
+ # consumer.run!
134
+ #
135
+ # @example Custom middleware
136
+ # class MyMiddleware
137
+ # def call(event)
138
+ # puts "Before: #{event.type}"
139
+ # yield
140
+ # puts "After: #{event.type}"
141
+ # end
142
+ # end
143
+ # consumer.use(MyMiddleware.new)
144
+ #
145
+ def use(middleware)
146
+ @middleware_chain.use(middleware)
147
+ self
148
+ end
149
+
150
+ # Start the consumer and process messages in a blocking loop.
151
+ #
152
+ # This method blocks the current thread and continuously fetches and processes
153
+ # messages until stop! is called or a signal is received (INT/TERM).
154
+ #
155
+ # The consumer will:
156
+ # - Fetch messages in batches (configurable batch_size)
157
+ # - Process each message through the middleware chain
158
+ # - Handle errors and reconnection automatically
159
+ # - Implement exponential backoff during idle periods
160
+ # - Drain in-flight messages during graceful shutdown
161
+ #
162
+ # @return [void]
163
+ #
164
+ # @example Basic usage
165
+ # consumer = JetstreamBridge::Consumer.new { |event| process(event) }
166
+ # consumer.run! # Blocks here
167
+ #
168
+ # @example In a Rake task
169
+ # namespace :jetstream do
170
+ # task consume: :environment do
171
+ # consumer = JetstreamBridge.subscribe { |event| handle(event) }
172
+ # trap("TERM") { consumer.stop! }
173
+ # consumer.run!
174
+ # end
175
+ # end
176
+ #
177
+ # @example With error handling
178
+ # consumer = JetstreamBridge::Consumer.new do |event|
179
+ # process(event)
180
+ # rescue RecoverableError => e
181
+ # raise # Let NATS retry
182
+ # rescue UnrecoverableError => e
183
+ # logger.error(e) # Log but don't raise (moves to DLQ if configured)
184
+ # end
185
+ # consumer.run!
186
+ #
43
187
  def run!
44
188
  Logging.info(
45
189
  "Consumer #{@durable} started (batch=#{@batch_size}, dest=#{JetstreamBridge.config.destination_subject})…",
@@ -55,7 +199,31 @@ module JetstreamBridge
55
199
  Logging.info("Consumer #{@durable} stopped gracefully", tag: 'JetstreamBridge::Consumer')
56
200
  end
57
201
 
58
- # Allow external callers to stop a long-running loop gracefully.
202
+ # Stop the consumer gracefully and drain in-flight messages.
203
+ #
204
+ # This method signals the consumer to stop processing new messages and drain
205
+ # any messages that are currently being processed. It's safe to call from
206
+ # signal handlers or other threads.
207
+ #
208
+ # The consumer will:
209
+ # - Stop fetching new messages
210
+ # - Complete processing of in-flight messages
211
+ # - Drain pending messages (up to 5 batches)
212
+ # - Close the subscription cleanly
213
+ #
214
+ # @return [void]
215
+ #
216
+ # @example In signal handler
217
+ # consumer = JetstreamBridge::Consumer.new { |event| process(event) }
218
+ # trap("TERM") { consumer.stop! }
219
+ # consumer.run!
220
+ #
221
+ # @example Manual control
222
+ # consumer = JetstreamBridge::Consumer.new { |event| process(event) }
223
+ # Thread.new { consumer.run! }
224
+ # sleep 10
225
+ # consumer.stop! # Stop after 10 seconds
226
+ #
59
227
  def stop!
60
228
  @shutdown_requested = true
61
229
  @running = false
@@ -68,12 +68,12 @@ module JetstreamBridge
68
68
  .new(deliveries, seq, @consumer, stream)
69
69
  end
70
70
 
71
- def ack(*args, **kwargs)
72
- msg.ack(*args, **kwargs) if msg.respond_to?(:ack)
71
+ def ack(*, **)
72
+ msg.ack(*, **) if msg.respond_to?(:ack)
73
73
  end
74
74
 
75
- def nak(*args, **kwargs)
76
- msg.nak(*args, **kwargs) if msg.respond_to?(:nak)
75
+ def nak(*, **)
76
+ msg.nak(*, **) if msg.respond_to?(:nak)
77
77
  end
78
78
  end
79
79
  end
@@ -31,7 +31,7 @@ module JetstreamBridge
31
31
  repo.persist_post(record)
32
32
  true
33
33
  rescue StandardError => e
34
- repo.persist_failure(record, e) if defined?(repo) && defined?(record)
34
+ repo.persist_failure(record, e) if repo && record
35
35
  Logging.error("Inbox processing failed: #{e.class}: #{e.message}",
36
36
  tag: 'JetstreamBridge::Consumer')
37
37
  false
@@ -3,7 +3,9 @@
3
3
  require 'oj'
4
4
  require 'securerandom'
5
5
  require_relative '../core/logging'
6
+ require_relative '../models/event'
6
7
  require_relative 'dlq_publisher'
8
+ require_relative 'middleware'
7
9
 
8
10
  module JetstreamBridge
9
11
  # Immutable per-message metadata.
@@ -49,11 +51,14 @@ module JetstreamBridge
49
51
  class MessageProcessor
50
52
  UNRECOVERABLE_ERRORS = [ArgumentError, TypeError].freeze
51
53
 
52
- def initialize(jts, handler, dlq: nil, backoff: nil)
53
- @jts = jts
54
- @handler = handler
55
- @dlq = dlq || DlqPublisher.new(jts)
56
- @backoff = backoff || BackoffStrategy.new
54
+ attr_reader :middleware_chain
55
+
56
+ def initialize(jts, handler, dlq: nil, backoff: nil, middleware_chain: nil)
57
+ @jts = jts
58
+ @handler = handler
59
+ @dlq = dlq || DlqPublisher.new(jts)
60
+ @backoff = backoff || BackoffStrategy.new
61
+ @middleware_chain = middleware_chain || ConsumerMiddleware::MiddlewareChain.new
57
62
  end
58
63
 
59
64
  def handle_message(msg)
@@ -97,8 +102,17 @@ module JetstreamBridge
97
102
  nil
98
103
  end
99
104
 
100
- def process_event(msg, event, ctx)
101
- @handler.call(event, ctx.subject, ctx.deliveries)
105
+ def process_event(msg, event_hash, ctx)
106
+ # Convert hash to Event object
107
+ event = build_event_object(event_hash, ctx)
108
+
109
+ # Call handler through middleware chain
110
+ if @middleware_chain
111
+ @middleware_chain.call(event) { call_handler(event, event_hash, ctx) }
112
+ else
113
+ call_handler(event, event_hash, ctx)
114
+ end
115
+
102
116
  msg.ack
103
117
  Logging.info(
104
118
  "ACK event_id=#{ctx.event_id} subject=#{ctx.subject} seq=#{ctx.seq} deliveries=#{ctx.deliveries}",
@@ -175,5 +189,25 @@ module JetstreamBridge
175
189
  tag: 'JetstreamBridge::Consumer'
176
190
  )
177
191
  end
192
+
193
+ # Build Event object from hash and context
194
+ def build_event_object(event_hash, ctx)
195
+ Models::Event.new(
196
+ event_hash,
197
+ metadata: {
198
+ subject: ctx.subject,
199
+ deliveries: ctx.deliveries,
200
+ stream: ctx.stream,
201
+ sequence: ctx.seq,
202
+ consumer: ctx.consumer,
203
+ timestamp: Time.now
204
+ }
205
+ )
206
+ end
207
+
208
+ # Call handler with Event object
209
+ def call_handler(event, _event_hash, _ctx)
210
+ @handler.call(event)
211
+ end
178
212
  end
179
213
  end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JetstreamBridge
4
+ class Consumer; end
5
+
6
+ # Middleware classes for Consumer
7
+ module ConsumerMiddleware
8
+ # Middleware chain for consumer message processing
9
+ #
10
+ # Allows you to wrap message processing with cross-cutting concerns like
11
+ # logging, error handling, metrics, tracing, etc.
12
+ #
13
+ # @example Using middleware
14
+ # consumer = JetstreamBridge::Consumer::Consumer.new(handler)
15
+ # consumer.use(JetstreamBridge::Consumer::LoggingMiddleware.new)
16
+ # consumer.use(JetstreamBridge::Consumer::MetricsMiddleware.new)
17
+ # consumer.run!
18
+ #
19
+ class MiddlewareChain
20
+ def initialize
21
+ @middlewares = []
22
+ end
23
+
24
+ # Add a middleware to the chain
25
+ #
26
+ # @param middleware [Object] Middleware that responds to #call(event, &block)
27
+ # @return [self]
28
+ def use(middleware)
29
+ @middlewares << middleware
30
+ self
31
+ end
32
+
33
+ # Execute the middleware chain
34
+ #
35
+ # @param event [Models::Event] The event to process
36
+ # @yield Final handler to call after all middleware
37
+ # @return [void]
38
+ def call(event, &final_handler)
39
+ chain = @middlewares.reverse.reduce(final_handler) do |next_middleware, middleware|
40
+ lambda do |evt|
41
+ middleware.call(evt) { next_middleware.call(evt) }
42
+ end
43
+ end
44
+ chain.call(event)
45
+ end
46
+ end
47
+
48
+ # Logging middleware for consumer message processing
49
+ #
50
+ # @example
51
+ # consumer.use(JetstreamBridge::Consumer::LoggingMiddleware.new)
52
+ #
53
+ class LoggingMiddleware
54
+ def call(event)
55
+ start = Time.now
56
+ Logging.info("Processing event #{event.event_id} (#{event.type})", tag: 'Consumer')
57
+ yield
58
+ duration = Time.now - start
59
+ Logging.info("Completed event #{event.event_id} in #{duration.round(3)}s", tag: 'Consumer')
60
+ rescue StandardError => e
61
+ Logging.error("Failed event #{event.event_id}: #{e.message}", tag: 'Consumer')
62
+ raise
63
+ end
64
+ end
65
+
66
+ # Error handling middleware with configurable retry logic
67
+ #
68
+ # @example
69
+ # consumer.use(JetstreamBridge::Consumer::ErrorHandlingMiddleware.new(
70
+ # on_error: ->(event, error) { Sentry.capture_exception(error) }
71
+ # ))
72
+ #
73
+ class ErrorHandlingMiddleware
74
+ def initialize(on_error: nil)
75
+ @on_error = on_error
76
+ end
77
+
78
+ def call(event)
79
+ yield
80
+ rescue StandardError => e
81
+ @on_error&.call(event, e)
82
+ raise
83
+ end
84
+ end
85
+
86
+ # Metrics middleware for tracking event processing
87
+ #
88
+ # @example
89
+ # consumer.use(JetstreamBridge::Consumer::MetricsMiddleware.new(
90
+ # on_success: ->(event, duration) { StatsD.timing("event.process", duration) },
91
+ # on_failure: ->(event, error) { StatsD.increment("event.failed") }
92
+ # ))
93
+ #
94
+ class MetricsMiddleware
95
+ def initialize(on_success: nil, on_failure: nil)
96
+ @on_success = on_success
97
+ @on_failure = on_failure
98
+ end
99
+
100
+ def call(event)
101
+ start = Time.now
102
+ yield
103
+ duration = Time.now - start
104
+ @on_success&.call(event, duration)
105
+ rescue StandardError => e
106
+ @on_failure&.call(event, e)
107
+ raise
108
+ end
109
+ end
110
+
111
+ # Tracing middleware for distributed tracing
112
+ #
113
+ # @example
114
+ # consumer.use(JetstreamBridge::Consumer::TracingMiddleware.new)
115
+ #
116
+ class TracingMiddleware
117
+ def call(event)
118
+ trace_id = event.metadata.trace_id || event.trace_id
119
+
120
+ if defined?(ActiveSupport::CurrentAttributes)
121
+ # Set trace context if using Rails CurrentAttributes
122
+ previous_trace_id = Current.trace_id if defined?(Current)
123
+ Current.trace_id = trace_id if defined?(Current)
124
+ end
125
+
126
+ yield
127
+ ensure
128
+ Current.trace_id = previous_trace_id if defined?(Current) && defined?(previous_trace_id)
129
+ end
130
+ end
131
+
132
+ # Timeout middleware to prevent long-running handlers
133
+ #
134
+ # @example
135
+ # consumer.use(JetstreamBridge::Consumer::TimeoutMiddleware.new(timeout: 30))
136
+ #
137
+ class TimeoutMiddleware
138
+ def initialize(timeout: 30)
139
+ @timeout = timeout
140
+ end
141
+
142
+ def call(event, &)
143
+ require 'timeout'
144
+ Timeout.timeout(@timeout, &)
145
+ rescue Timeout::Error
146
+ raise ConsumerError.new(
147
+ "Event processing timeout after #{@timeout}s",
148
+ event_id: event.event_id,
149
+ deliveries: event.deliveries
150
+ )
151
+ end
152
+ end
153
+ end
154
+ end
@@ -3,11 +3,89 @@
3
3
  require_relative '../errors'
4
4
 
5
5
  module JetstreamBridge
6
+ # Configuration object for JetStream Bridge.
7
+ #
8
+ # Holds all configuration settings including NATS connection details,
9
+ # application identifiers, reliability features, and consumer tuning.
10
+ #
11
+ # @example Basic configuration
12
+ # JetstreamBridge.configure do |config|
13
+ # config.nats_urls = "nats://localhost:4222"
14
+ # config.env = "production"
15
+ # config.app_name = "api_service"
16
+ # config.destination_app = "worker_service"
17
+ # config.use_outbox = true
18
+ # config.use_inbox = true
19
+ # end
20
+ #
21
+ # @example Using a preset
22
+ # JetstreamBridge.configure_for(:production) do |config|
23
+ # config.nats_urls = ENV["NATS_URLS"]
24
+ # config.app_name = "api"
25
+ # config.destination_app = "worker"
26
+ # end
27
+ #
6
28
  class Config
7
- attr_accessor :destination_app, :nats_urls, :env, :app_name,
8
- :max_deliver, :ack_wait, :backoff,
9
- :use_outbox, :use_inbox, :inbox_model, :outbox_model,
10
- :use_dlq, :logger
29
+ # Status constants for event processing states.
30
+ module Status
31
+ # Event queued in outbox, not yet published
32
+ PENDING = 'pending'
33
+ # Event currently being published to NATS
34
+ PUBLISHING = 'publishing'
35
+ # Event successfully published to NATS
36
+ SENT = 'sent'
37
+ # Event failed to publish after retries
38
+ FAILED = 'failed'
39
+ # Event received by consumer
40
+ RECEIVED = 'received'
41
+ # Event currently being processed by consumer
42
+ PROCESSING = 'processing'
43
+ # Event successfully processed by consumer
44
+ PROCESSED = 'processed'
45
+ end
46
+
47
+ # NATS server URL(s), comma-separated for multiple servers
48
+ # @return [String]
49
+ attr_accessor :destination_app
50
+ # NATS server URL(s)
51
+ # @return [String]
52
+ attr_accessor :nats_urls
53
+ # Environment namespace (development, staging, production)
54
+ # @return [String]
55
+ attr_accessor :env
56
+ # Application name for subject routing
57
+ # @return [String]
58
+ attr_accessor :app_name
59
+ # Maximum delivery attempts before moving to DLQ
60
+ # @return [Integer]
61
+ attr_accessor :max_deliver
62
+ # Time to wait for acknowledgment before redelivery
63
+ # @return [String, Integer]
64
+ attr_accessor :ack_wait
65
+ # Backoff delays between retries
66
+ # @return [Array<String>]
67
+ attr_accessor :backoff
68
+ # Enable transactional outbox pattern
69
+ # @return [Boolean]
70
+ attr_accessor :use_outbox
71
+ # Enable idempotent inbox pattern
72
+ # @return [Boolean]
73
+ attr_accessor :use_inbox
74
+ # ActiveRecord model class name for inbox events
75
+ # @return [String]
76
+ attr_accessor :inbox_model
77
+ # ActiveRecord model class name for outbox events
78
+ # @return [String]
79
+ attr_accessor :outbox_model
80
+ # Enable dead letter queue
81
+ # @return [Boolean]
82
+ attr_accessor :use_dlq
83
+ # Logger instance
84
+ # @return [Logger, nil]
85
+ attr_accessor :logger
86
+ # Applied preset name
87
+ # @return [Symbol, nil]
88
+ attr_reader :preset_applied
11
89
 
12
90
  def initialize
13
91
  @nats_urls = ENV['NATS_URLS'] || ENV['NATS_URL'] || 'nats://localhost:4222'
@@ -25,16 +103,43 @@ module JetstreamBridge
25
103
  @outbox_model = 'JetstreamBridge::OutboxEvent'
26
104
  @inbox_model = 'JetstreamBridge::InboxEvent'
27
105
  @logger = nil
106
+ @preset_applied = nil
28
107
  end
29
108
 
30
- # Single stream name per env
109
+ # Apply a configuration preset
110
+ #
111
+ # @param preset_name [Symbol, String] Name of preset (e.g., :production, :development)
112
+ # @return [self]
113
+ def apply_preset(preset_name)
114
+ require_relative 'config_preset'
115
+ ConfigPreset.apply(self, preset_name)
116
+ @preset_applied = preset_name.to_sym
117
+ self
118
+ end
119
+
120
+ # Get the JetStream stream name for this environment.
121
+ #
122
+ # @return [String] Stream name in format "{env}-jetstream-bridge-stream"
123
+ # @example
124
+ # config.env = "production"
125
+ # config.stream_name # => "production-jetstream-bridge-stream"
31
126
  def stream_name
32
127
  "#{env}-jetstream-bridge-stream"
33
128
  end
34
129
 
35
- # Base subjects
130
+ # Get the NATS subject this application publishes to.
131
+ #
36
132
  # Producer publishes to: {env}.{app}.sync.{dest}
37
133
  # Consumer subscribes to: {env}.{dest}.sync.{app}
134
+ #
135
+ # @return [String] Source subject for publishing
136
+ # @raise [InvalidSubjectError] If components contain NATS wildcards
137
+ # @raise [MissingConfigurationError] If required components empty
138
+ # @example
139
+ # config.env = "production"
140
+ # config.app_name = "api"
141
+ # config.destination_app = "worker"
142
+ # config.source_subject # => "production.api.sync.worker"
38
143
  def source_subject
39
144
  validate_subject_component!(env, 'env')
40
145
  validate_subject_component!(app_name, 'app_name')
@@ -42,6 +147,16 @@ module JetstreamBridge
42
147
  "#{env}.#{app_name}.sync.#{destination_app}"
43
148
  end
44
149
 
150
+ # Get the NATS subject this application subscribes to.
151
+ #
152
+ # @return [String] Destination subject for consuming
153
+ # @raise [InvalidSubjectError] If components contain NATS wildcards
154
+ # @raise [MissingConfigurationError] If required components empty
155
+ # @example
156
+ # config.env = "production"
157
+ # config.app_name = "api"
158
+ # config.destination_app = "worker"
159
+ # config.destination_subject # => "production.worker.sync.api"
45
160
  def destination_subject
46
161
  validate_subject_component!(env, 'env')
47
162
  validate_subject_component!(app_name, 'app_name')
@@ -49,17 +164,43 @@ module JetstreamBridge
49
164
  "#{env}.#{destination_app}.sync.#{app_name}"
50
165
  end
51
166
 
52
- # DLQ
167
+ # Get the dead letter queue subject for this application.
168
+ #
169
+ # Each app has its own DLQ for better isolation and monitoring.
170
+ #
171
+ # @return [String] DLQ subject in format "{env}.{app_name}.sync.dlq"
172
+ # @raise [InvalidSubjectError] If components contain NATS wildcards
173
+ # @raise [MissingConfigurationError] If required components are empty
174
+ # @example
175
+ # config.env = "production"
176
+ # config.app_name = "api"
177
+ # config.dlq_subject # => "production.api.sync.dlq"
53
178
  def dlq_subject
54
179
  validate_subject_component!(env, 'env')
55
- "#{env}.sync.dlq"
180
+ validate_subject_component!(app_name, 'app_name')
181
+ "#{env}.#{app_name}.sync.dlq"
56
182
  end
57
183
 
184
+ # Get the durable consumer name for this application.
185
+ #
186
+ # @return [String] Durable name in format "{env}-{app_name}-workers"
187
+ # @example
188
+ # config.env = "production"
189
+ # config.app_name = "api"
190
+ # config.durable_name # => "production-api-workers"
58
191
  def durable_name
59
192
  "#{env}-#{app_name}-workers"
60
193
  end
61
194
 
62
- # Validate configuration settings
195
+ # Validate all configuration settings.
196
+ #
197
+ # Checks that required settings are present and valid. Raises errors
198
+ # for any invalid configuration.
199
+ #
200
+ # @return [true] If configuration is valid
201
+ # @raise [ConfigurationError] If any validation fails
202
+ # @example
203
+ # config.validate! # Raises if destination_app is missing
63
204
  def validate!
64
205
  errors = []
65
206
  errors << 'destination_app is required' if destination_app.to_s.strip.empty?