jetstream_bridge 4.5.0 → 4.5.2

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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +338 -87
  3. data/README.md +3 -13
  4. data/docs/GETTING_STARTED.md +8 -12
  5. data/docs/PRODUCTION.md +13 -35
  6. data/docs/RESTRICTED_PERMISSIONS.md +525 -0
  7. data/docs/TESTING.md +33 -22
  8. data/lib/generators/jetstream_bridge/health_check/health_check_generator.rb +3 -3
  9. data/lib/generators/jetstream_bridge/initializer/templates/jetstream_bridge.rb +3 -0
  10. data/lib/jetstream_bridge/consumer/consumer.rb +100 -39
  11. data/lib/jetstream_bridge/consumer/message_processor.rb +1 -1
  12. data/lib/jetstream_bridge/consumer/subscription_manager.rb +97 -121
  13. data/lib/jetstream_bridge/core/bridge_helpers.rb +127 -0
  14. data/lib/jetstream_bridge/core/config.rb +32 -161
  15. data/lib/jetstream_bridge/core/connection.rb +508 -0
  16. data/lib/jetstream_bridge/core/connection_factory.rb +95 -0
  17. data/lib/jetstream_bridge/core/debug_helper.rb +2 -9
  18. data/lib/jetstream_bridge/core.rb +2 -0
  19. data/lib/jetstream_bridge/models/subject.rb +15 -23
  20. data/lib/jetstream_bridge/provisioner.rb +67 -0
  21. data/lib/jetstream_bridge/publisher/publisher.rb +121 -92
  22. data/lib/jetstream_bridge/rails/integration.rb +5 -8
  23. data/lib/jetstream_bridge/rails/railtie.rb +3 -4
  24. data/lib/jetstream_bridge/tasks/install.rake +59 -12
  25. data/lib/jetstream_bridge/topology/topology.rb +1 -6
  26. data/lib/jetstream_bridge/version.rb +1 -1
  27. data/lib/jetstream_bridge.rb +345 -202
  28. metadata +8 -8
  29. data/lib/jetstream_bridge/consumer/health_monitor.rb +0 -107
  30. data/lib/jetstream_bridge/core/connection_manager.rb +0 -513
  31. data/lib/jetstream_bridge/core/health_checker.rb +0 -184
  32. data/lib/jetstream_bridge/facade.rb +0 -212
  33. data/lib/jetstream_bridge/publisher/event_envelope_builder.rb +0 -110
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jetstream_bridge
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.5.0
4
+ version: 4.5.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Attara
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-01-22 00:00:00.000000000 Z
11
+ date: 2026-01-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -105,7 +105,7 @@ dependencies:
105
105
  - !ruby/object:Gem::Version
106
106
  version: '4.0'
107
107
  description: |-
108
- Production-ready publishers/consumers for NATS JetStream with environment-scoped
108
+ Production-ready publishers/consumers for NATS JetStream with app-scoped
109
109
  subjects, overlap guards, DLQ routing, retries/backoff, and optional inbox/outbox
110
110
  patterns. Includes health checks, auto-reconnection, graceful shutdown, topology
111
111
  setup helpers, and Rails generators.
@@ -123,6 +123,7 @@ files:
123
123
  - README.md
124
124
  - docs/GETTING_STARTED.md
125
125
  - docs/PRODUCTION.md
126
+ - docs/RESTRICTED_PERMISSIONS.md
126
127
  - docs/TESTING.md
127
128
  - lib/generators/jetstream_bridge/health_check/health_check_generator.rb
128
129
  - lib/generators/jetstream_bridge/health_check/templates/health_controller.rb
@@ -135,7 +136,6 @@ files:
135
136
  - lib/jetstream_bridge.rb
136
137
  - lib/jetstream_bridge/consumer/consumer.rb
137
138
  - lib/jetstream_bridge/consumer/dlq_publisher.rb
138
- - lib/jetstream_bridge/consumer/health_monitor.rb
139
139
  - lib/jetstream_bridge/consumer/inbox/inbox_message.rb
140
140
  - lib/jetstream_bridge/consumer/inbox/inbox_processor.rb
141
141
  - lib/jetstream_bridge/consumer/inbox/inbox_repository.rb
@@ -143,26 +143,26 @@ files:
143
143
  - lib/jetstream_bridge/consumer/middleware.rb
144
144
  - lib/jetstream_bridge/consumer/subscription_manager.rb
145
145
  - lib/jetstream_bridge/core.rb
146
+ - lib/jetstream_bridge/core/bridge_helpers.rb
146
147
  - lib/jetstream_bridge/core/config.rb
147
148
  - lib/jetstream_bridge/core/config_preset.rb
148
- - lib/jetstream_bridge/core/connection_manager.rb
149
+ - lib/jetstream_bridge/core/connection.rb
150
+ - lib/jetstream_bridge/core/connection_factory.rb
149
151
  - lib/jetstream_bridge/core/debug_helper.rb
150
152
  - lib/jetstream_bridge/core/duration.rb
151
- - lib/jetstream_bridge/core/health_checker.rb
152
153
  - lib/jetstream_bridge/core/logging.rb
153
154
  - lib/jetstream_bridge/core/model_codec_setup.rb
154
155
  - lib/jetstream_bridge/core/model_utils.rb
155
156
  - lib/jetstream_bridge/core/retry_strategy.rb
156
157
  - lib/jetstream_bridge/errors.rb
157
- - lib/jetstream_bridge/facade.rb
158
158
  - lib/jetstream_bridge/models/event.rb
159
159
  - lib/jetstream_bridge/models/event_envelope.rb
160
160
  - lib/jetstream_bridge/models/inbox_event.rb
161
161
  - lib/jetstream_bridge/models/outbox_event.rb
162
162
  - lib/jetstream_bridge/models/publish_result.rb
163
163
  - lib/jetstream_bridge/models/subject.rb
164
+ - lib/jetstream_bridge/provisioner.rb
164
165
  - lib/jetstream_bridge/publisher/batch_publisher.rb
165
- - lib/jetstream_bridge/publisher/event_envelope_builder.rb
166
166
  - lib/jetstream_bridge/publisher/outbox_repository.rb
167
167
  - lib/jetstream_bridge/publisher/publisher.rb
168
168
  - lib/jetstream_bridge/rails.rb
@@ -1,107 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative '../core/logging'
4
-
5
- module JetstreamBridge
6
- # Health monitoring for Consumer instances
7
- #
8
- # Responsible for:
9
- # - Periodic health checks
10
- # - Memory usage monitoring
11
- # - Heap object tracking
12
- # - GC recommendations
13
- class ConsumerHealthMonitor
14
- # Health check interval in seconds (10 minutes)
15
- HEALTH_CHECK_INTERVAL = 600
16
- # Memory warning threshold in MB
17
- MEMORY_WARNING_THRESHOLD_MB = 1000
18
- # Heap object count warning threshold
19
- HEAP_OBJECT_WARNING_THRESHOLD = 200_000
20
-
21
- def initialize(consumer_name)
22
- @consumer_name = consumer_name
23
- @start_time = Time.now
24
- @last_check = Time.now
25
- @iterations = 0
26
- @gc_warning_logged = false
27
- end
28
-
29
- # Increment iteration counter
30
- def increment_iterations
31
- @iterations += 1
32
- end
33
-
34
- # Check if health check is due and perform if needed
35
- #
36
- # @return [Boolean] true if check was performed
37
- def check_health_if_due
38
- now = Time.now
39
- time_since_check = now - @last_check
40
-
41
- return false unless time_since_check >= HEALTH_CHECK_INTERVAL
42
-
43
- perform_check(now)
44
- true
45
- end
46
-
47
- private
48
-
49
- def perform_check(now)
50
- @last_check = now
51
- uptime = now - @start_time
52
- memory_mb = memory_usage_mb
53
-
54
- Logging.info(
55
- "Consumer health: iterations=#{@iterations}, " \
56
- "memory=#{memory_mb}MB, uptime=#{uptime.round}s",
57
- tag: "JetstreamBridge::Consumer(#{@consumer_name})"
58
- )
59
-
60
- warn_if_high_memory(memory_mb)
61
- suggest_gc_if_needed
62
- rescue StandardError => e
63
- Logging.debug(
64
- "Health check failed: #{e.class} #{e.message}",
65
- tag: "JetstreamBridge::Consumer(#{@consumer_name})"
66
- )
67
- end
68
-
69
- def memory_usage_mb
70
- # Get memory usage from OS (works on Linux/macOS)
71
- rss_kb = `ps -o rss= -p #{Process.pid}`.to_i
72
- rss_kb / 1024.0
73
- rescue StandardError
74
- 0.0
75
- end
76
-
77
- def warn_if_high_memory(memory_mb)
78
- return unless memory_mb > MEMORY_WARNING_THRESHOLD_MB
79
-
80
- Logging.warn(
81
- "High memory usage detected: #{memory_mb}MB",
82
- tag: "JetstreamBridge::Consumer(#{@consumer_name})"
83
- )
84
- end
85
-
86
- def suggest_gc_if_needed
87
- return unless defined?(GC) && GC.respond_to?(:stat)
88
-
89
- stats = GC.stat
90
- heap_live_slots = stats[:heap_live_slots] || stats['heap_live_slots'] || 0
91
-
92
- return if heap_live_slots < HEAP_OBJECT_WARNING_THRESHOLD || @gc_warning_logged
93
-
94
- @gc_warning_logged = true
95
- Logging.warn(
96
- "High heap object count detected (#{heap_live_slots}); " \
97
- 'consider profiling or manual GC in the host app',
98
- tag: "JetstreamBridge::Consumer(#{@consumer_name})"
99
- )
100
- rescue StandardError => e
101
- Logging.debug(
102
- "GC check failed: #{e.class} #{e.message}",
103
- tag: "JetstreamBridge::Consumer(#{@consumer_name})"
104
- )
105
- end
106
- end
107
- end
@@ -1,513 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'nats/io/client'
4
- require 'uri'
5
- require_relative 'logging'
6
- require_relative 'config'
7
- require_relative '../topology/topology'
8
-
9
- module JetstreamBridge
10
- # Manages NATS connection lifecycle
11
- #
12
- # Responsible for:
13
- # - Establishing and closing connections
14
- # - Validating connection URLs
15
- # - Health checking
16
- # - Reconnection handling
17
- # - JetStream context management
18
- #
19
- # NOT a singleton - instances are created and injected
20
- class ConnectionManager
21
- # Connection states for observability
22
- module State
23
- DISCONNECTED = :disconnected
24
- CONNECTING = :connecting
25
- CONNECTED = :connected
26
- RECONNECTING = :reconnecting
27
- FAILED = :failed
28
- end
29
-
30
- # Valid NATS URL schemes
31
- VALID_NATS_SCHEMES = %w[nats nats+tls].freeze
32
-
33
- DEFAULT_CONN_OPTS = {
34
- reconnect: true,
35
- reconnect_time_wait: 2,
36
- max_reconnect_attempts: 10,
37
- connect_timeout: 5
38
- }.freeze
39
-
40
- # Health check cache TTL in seconds
41
- HEALTH_CHECK_CACHE_TTL = 30
42
-
43
- attr_reader :config, :connected_at, :last_reconnect_error, :last_reconnect_error_at, :state
44
-
45
- # @param config [Config] Configuration instance
46
- def initialize(config)
47
- @config = config
48
- @mutex = Mutex.new
49
- @state = State::DISCONNECTED
50
- @nc = nil
51
- @jts = nil
52
- @connected_at = nil
53
- @cached_health_status = nil
54
- @last_health_check = nil
55
- @reconnecting = false
56
- end
57
-
58
- # Establish connection to NATS and ensure topology
59
- #
60
- # Idempotent - safe to call multiple times
61
- #
62
- # @return [NATS::JetStream::JS] JetStream context
63
- # @raise [ConnectionError] If connection fails
64
- def connect!
65
- @mutex.synchronize do
66
- return @jts if connected_without_lock?
67
-
68
- servers = validate_and_parse_servers!(@config.nats_urls)
69
- @state = State::CONNECTING
70
-
71
- establish_connection_with_retry(servers)
72
-
73
- Logging.info(
74
- "Connected to NATS (#{servers.size} server#{'s' unless servers.size == 1}): " \
75
- "#{sanitize_urls(servers).join(', ')}",
76
- tag: 'JetstreamBridge::ConnectionManager'
77
- )
78
-
79
- ensure_topology_if_enabled
80
-
81
- @connected_at = Time.now.utc
82
- @state = State::CONNECTED
83
- @jts
84
- end
85
- rescue StandardError
86
- @state = State::FAILED
87
- cleanup_connection!
88
- raise
89
- end
90
-
91
- # Close the NATS connection
92
- #
93
- # @return [void]
94
- def disconnect!
95
- @mutex.synchronize do
96
- cleanup_connection!(close_nc: true)
97
- @state = State::DISCONNECTED
98
- Logging.info('Disconnected from NATS', tag: 'JetstreamBridge::ConnectionManager')
99
- end
100
- end
101
-
102
- # Reconnect to NATS (disconnect + connect)
103
- #
104
- # @return [void]
105
- def reconnect!
106
- Logging.info('Reconnecting to NATS...', tag: 'JetstreamBridge::ConnectionManager')
107
- disconnect!
108
- connect!
109
- end
110
-
111
- # Get JetStream context
112
- #
113
- # @return [NATS::JetStream::JS, nil] JetStream context if connected
114
- def jetstream
115
- @jts
116
- end
117
-
118
- # Get raw NATS client
119
- #
120
- # @return [NATS::IO::Client, nil] NATS client if connected
121
- def nats_client
122
- @nc
123
- end
124
-
125
- # Check if connected and healthy
126
- #
127
- # @param skip_cache [Boolean] Force fresh health check
128
- # @return [Boolean] true if connected and healthy
129
- def connected?(skip_cache: false)
130
- return false unless @nc&.connected?
131
- return false unless @jts
132
-
133
- # Use cached result if available and fresh
134
- now = Time.now.to_i
135
- if !skip_cache && @last_health_check && (now - @last_health_check) < HEALTH_CHECK_CACHE_TTL
136
- return @cached_health_status
137
- end
138
-
139
- # Thread-safe cache update
140
- @mutex.synchronize do
141
- # Double-check after acquiring lock
142
- now = Time.now.to_i
143
- if !skip_cache && @last_health_check && (now - @last_health_check) < HEALTH_CHECK_CACHE_TTL
144
- return @cached_health_status
145
- end
146
-
147
- # Perform actual health check
148
- @cached_health_status = jetstream_healthy?
149
- @last_health_check = now
150
- @cached_health_status
151
- end
152
- end
153
-
154
- # Get detailed health status
155
- #
156
- # @param skip_cache [Boolean] Force fresh check
157
- # @return [Hash] Health status details
158
- def health_check(skip_cache: false)
159
- {
160
- connected: connected?(skip_cache: skip_cache),
161
- state: @state,
162
- connected_at: @connected_at&.iso8601,
163
- last_error: @last_reconnect_error&.message,
164
- last_error_at: @last_reconnect_error_at&.iso8601
165
- }
166
- end
167
-
168
- private
169
-
170
- def connected_without_lock?
171
- @jts && @nc&.connected?
172
- end
173
-
174
- def jetstream_healthy?
175
- return true if @config.disable_js_api
176
-
177
- # Verify JetStream responds to simple API call
178
- @jts.account_info
179
- true
180
- rescue StandardError => e
181
- Logging.warn(
182
- "JetStream health check failed: #{e.class} #{e.message}",
183
- tag: 'JetstreamBridge::ConnectionManager'
184
- )
185
- false
186
- end
187
-
188
- def establish_connection_with_retry(servers)
189
- attempts = 0
190
- max_attempts = @config.connect_retry_attempts
191
- retry_delay = @config.connect_retry_delay
192
-
193
- begin
194
- attempts += 1
195
- establish_connection(servers)
196
- rescue ConnectionError => e
197
- if attempts < max_attempts
198
- delay = retry_delay * attempts
199
- Logging.warn(
200
- "Connection attempt #{attempts}/#{max_attempts} failed: #{e.message}. " \
201
- "Retrying in #{delay}s...",
202
- tag: 'JetstreamBridge::ConnectionManager'
203
- )
204
- sleep(delay)
205
- retry
206
- else
207
- Logging.error(
208
- "Failed to establish connection after #{attempts} attempts",
209
- tag: 'JetstreamBridge::ConnectionManager'
210
- )
211
- raise
212
- end
213
- end
214
- end
215
-
216
- def establish_connection(servers)
217
- @nc = create_nats_client
218
-
219
- setup_callbacks
220
-
221
- connect_opts = { servers: servers }.merge(DEFAULT_CONN_OPTS)
222
- inbox_prefix = @config.inbox_prefix.to_s.strip
223
- connect_opts[:inbox_prefix] = inbox_prefix unless inbox_prefix.empty?
224
-
225
- @nc.connect(connect_opts) unless skip_connect?
226
-
227
- verify_connection!
228
-
229
- # Create JetStream context
230
- @jts = @nc.jetstream
231
-
232
- verify_jetstream!
233
-
234
- # Ensure JetStream responds to #nc
235
- add_nc_accessor unless @jts.respond_to?(:nc)
236
- end
237
-
238
- def create_nats_client
239
- # Use mock NATS client if explicitly enabled for testing
240
- if test_mode?
241
- JetstreamBridge.instance_variable_get(:@mock_nats_client)
242
- else
243
- NATS::IO::Client.new
244
- end
245
- end
246
-
247
- def test_mode?
248
- defined?(JetstreamBridge::TestHelpers) &&
249
- JetstreamBridge::TestHelpers.respond_to?(:test_mode?) &&
250
- JetstreamBridge::TestHelpers.test_mode? &&
251
- JetstreamBridge.instance_variable_defined?(:@mock_nats_client)
252
- end
253
-
254
- def skip_connect?
255
- @nc.connected? && test_mode?
256
- end
257
-
258
- def setup_callbacks
259
- @nc.on_reconnect do
260
- @reconnecting = true
261
- Logging.info(
262
- 'NATS reconnected, refreshing JetStream context',
263
- tag: 'JetstreamBridge::ConnectionManager'
264
- )
265
- refresh_jetstream_context
266
- @reconnecting = false
267
- end
268
-
269
- @nc.on_disconnect do |reason|
270
- @state = State::DISCONNECTED
271
- Logging.warn(
272
- "NATS disconnected: #{reason}",
273
- tag: 'JetstreamBridge::ConnectionManager'
274
- )
275
- end
276
-
277
- @nc.on_error do |err|
278
- Logging.error(
279
- "NATS error: #{err}",
280
- tag: 'JetstreamBridge::ConnectionManager'
281
- )
282
- end
283
- end
284
-
285
- def add_nc_accessor
286
- nc_ref = @nc
287
- @jts.define_singleton_method(:nc) { nc_ref }
288
- end
289
-
290
- def verify_connection!
291
- Logging.debug(
292
- 'Verifying NATS connection...',
293
- tag: 'JetstreamBridge::ConnectionManager'
294
- )
295
-
296
- unless @nc.connected?
297
- Logging.error(
298
- 'NATS connection verification failed - client not connected',
299
- tag: 'JetstreamBridge::ConnectionManager'
300
- )
301
- raise ConnectionError, 'Failed to establish connection to NATS server(s)'
302
- end
303
-
304
- Logging.info(
305
- 'NATS connection verified successfully',
306
- tag: 'JetstreamBridge::ConnectionManager'
307
- )
308
- end
309
-
310
- def verify_jetstream!
311
- return true if @config.disable_js_api
312
-
313
- Logging.debug(
314
- 'Verifying JetStream availability...',
315
- tag: 'JetstreamBridge::ConnectionManager'
316
- )
317
-
318
- account_info = @jts.account_info
319
-
320
- # Handle both object-style and hash-style access for compatibility
321
- streams = account_info.respond_to?(:streams) ? account_info.streams : account_info[:streams]
322
- consumers = account_info.respond_to?(:consumers) ? account_info.consumers : account_info[:consumers]
323
- memory = account_info.respond_to?(:memory) ? account_info.memory : account_info[:memory]
324
- storage = account_info.respond_to?(:storage) ? account_info.storage : account_info[:storage]
325
-
326
- Logging.info(
327
- "JetStream verified - Streams: #{streams}, " \
328
- "Consumers: #{consumers}, " \
329
- "Memory: #{format_bytes(memory)}, " \
330
- "Storage: #{format_bytes(storage)}",
331
- tag: 'JetstreamBridge::ConnectionManager'
332
- )
333
- rescue NATS::IO::NoRespondersError
334
- Logging.error(
335
- 'JetStream not available - no responders (JetStream not enabled)',
336
- tag: 'JetstreamBridge::ConnectionManager'
337
- )
338
- raise ConnectionError, 'JetStream not enabled on NATS server. Please enable JetStream with -js flag'
339
- rescue StandardError => e
340
- Logging.error(
341
- "JetStream verification failed: #{e.class} - #{e.message}",
342
- tag: 'JetstreamBridge::ConnectionManager'
343
- )
344
- raise ConnectionError, "JetStream verification failed: #{e.message}"
345
- end
346
-
347
- def ensure_topology_if_enabled
348
- return if @config.disable_js_api
349
-
350
- Topology.ensure!(@jts, force: true)
351
- rescue StandardError => e
352
- Logging.warn(
353
- "Topology ensure skipped: #{e.class} #{e.message}",
354
- tag: 'JetstreamBridge::ConnectionManager'
355
- )
356
- end
357
-
358
- def refresh_jetstream_context
359
- @jts = @nc.jetstream
360
- add_nc_accessor unless @jts.respond_to?(:nc)
361
- ensure_topology_if_enabled
362
-
363
- # Invalidate health check cache on successful reconnect
364
- @cached_health_status = nil
365
- @last_health_check = nil
366
-
367
- # Clear error state on successful reconnect
368
- @last_reconnect_error = nil
369
- @last_reconnect_error_at = nil
370
- @state = State::CONNECTED
371
-
372
- Logging.info(
373
- 'JetStream context refreshed successfully after reconnect',
374
- tag: 'JetstreamBridge::ConnectionManager'
375
- )
376
- rescue StandardError => e
377
- # Store error state for diagnostics
378
- @last_reconnect_error = e
379
- @last_reconnect_error_at = Time.now
380
- @state = State::FAILED
381
- cleanup_connection!(close_nc: false)
382
- Logging.error(
383
- "Failed to refresh JetStream context: #{e.class} #{e.message}",
384
- tag: 'JetstreamBridge::ConnectionManager'
385
- )
386
-
387
- # Invalidate health check cache to force re-check
388
- @cached_health_status = false
389
- @last_health_check = Time.now.to_i
390
- end
391
-
392
- def cleanup_connection!(close_nc: true)
393
- begin
394
- # Avoid touching RSpec doubles used in unit tests
395
- is_rspec_double = defined?(RSpec::Mocks::Double) && @nc.is_a?(RSpec::Mocks::Double)
396
- @nc.close if !is_rspec_double && close_nc && @nc.respond_to?(:close) && @nc.connected?
397
- rescue StandardError
398
- # ignore cleanup errors
399
- end
400
- @nc = nil
401
- @jts = nil
402
- @cached_health_status = nil
403
- @last_health_check = nil
404
- @connected_at = nil
405
- end
406
-
407
- def format_bytes(bytes)
408
- return 'N/A' if bytes.nil? || bytes.zero?
409
-
410
- units = %w[B KB MB GB TB]
411
- exp = (Math.log(bytes) / Math.log(1024)).to_i
412
- exp = [exp, units.length - 1].min
413
- "#{(bytes / (1024.0**exp)).round(2)} #{units[exp]}"
414
- end
415
-
416
- def sanitize_urls(urls)
417
- urls.map { |u| Logging.sanitize_url(u) }
418
- end
419
-
420
- # Validate and parse NATS server URLs
421
- #
422
- # @param urls [String] Comma-separated NATS URLs
423
- # @return [Array<String>] Validated server URLs
424
- # @raise [ConnectionError] If validation fails
425
- def validate_and_parse_servers!(urls)
426
- servers = parse_urls(urls)
427
- validate_not_empty!(servers)
428
-
429
- servers.each { |url| validate_url!(url) }
430
-
431
- Logging.info(
432
- 'All NATS URLs validated successfully',
433
- tag: 'JetstreamBridge::ConnectionManager'
434
- )
435
-
436
- servers
437
- end
438
-
439
- def parse_urls(urls)
440
- urls.to_s
441
- .split(',')
442
- .map(&:strip)
443
- .reject(&:empty?)
444
- end
445
-
446
- def validate_not_empty!(servers)
447
- return unless servers.empty?
448
-
449
- raise ConnectionError, 'No NATS URLs configured'
450
- end
451
-
452
- def validate_url!(url)
453
- validate_url_format!(url)
454
-
455
- uri = URI.parse(url)
456
- validate_url_scheme!(uri, url)
457
- validate_url_host!(uri, url)
458
- validate_url_port!(uri, url)
459
-
460
- Logging.debug(
461
- "URL validated: #{Logging.sanitize_url(url)}",
462
- tag: 'JetstreamBridge::ConnectionManager'
463
- )
464
- rescue URI::InvalidURIError => e
465
- Logging.error(
466
- "Malformed URL: #{url} (#{e.message})",
467
- tag: 'JetstreamBridge::ConnectionManager'
468
- )
469
- raise ConnectionError, "Invalid NATS URL format: #{url} (#{e.message})"
470
- end
471
-
472
- def validate_url_format!(url)
473
- return if url.include?('://')
474
-
475
- Logging.error(
476
- "Invalid URL format (missing scheme): #{url}",
477
- tag: 'JetstreamBridge::ConnectionManager'
478
- )
479
- raise ConnectionError, "Invalid NATS URL format: #{url}. Expected format: nats://host:port"
480
- end
481
-
482
- def validate_url_scheme!(uri, url)
483
- scheme = uri.scheme&.downcase
484
- return if VALID_NATS_SCHEMES.include?(scheme)
485
-
486
- Logging.error(
487
- "Invalid URL scheme '#{uri.scheme}': #{Logging.sanitize_url(url)}",
488
- tag: 'JetstreamBridge::ConnectionManager'
489
- )
490
- raise ConnectionError, "Invalid NATS URL scheme '#{uri.scheme}' in: #{url}. Expected 'nats' or 'nats+tls'"
491
- end
492
-
493
- def validate_url_host!(uri, url)
494
- return unless uri.host.nil? || uri.host.empty?
495
-
496
- Logging.error(
497
- "Missing host in URL: #{Logging.sanitize_url(url)}",
498
- tag: 'JetstreamBridge::ConnectionManager'
499
- )
500
- raise ConnectionError, "Invalid NATS URL - missing host: #{url}"
501
- end
502
-
503
- def validate_url_port!(uri, url)
504
- return unless uri.port && (uri.port < 1 || uri.port > 65_535)
505
-
506
- Logging.error(
507
- "Invalid port #{uri.port} in URL: #{Logging.sanitize_url(url)}",
508
- tag: 'JetstreamBridge::ConnectionManager'
509
- )
510
- raise ConnectionError, "Invalid NATS URL - port must be 1-65535: #{url}"
511
- end
512
- end
513
- end