jetstream_bridge 4.4.0 → 4.5.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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +92 -337
  3. data/README.md +1 -5
  4. data/docs/GETTING_STARTED.md +11 -7
  5. data/docs/PRODUCTION.md +51 -11
  6. data/docs/TESTING.md +24 -35
  7. data/lib/generators/jetstream_bridge/health_check/health_check_generator.rb +1 -1
  8. data/lib/generators/jetstream_bridge/initializer/initializer_generator.rb +1 -1
  9. data/lib/generators/jetstream_bridge/initializer/templates/jetstream_bridge.rb +0 -4
  10. data/lib/generators/jetstream_bridge/install/install_generator.rb +5 -5
  11. data/lib/generators/jetstream_bridge/migrations/migrations_generator.rb +2 -2
  12. data/lib/jetstream_bridge/consumer/consumer.rb +34 -96
  13. data/lib/jetstream_bridge/consumer/health_monitor.rb +107 -0
  14. data/lib/jetstream_bridge/consumer/message_processor.rb +1 -1
  15. data/lib/jetstream_bridge/consumer/subscription_manager.rb +51 -34
  16. data/lib/jetstream_bridge/core/config.rb +153 -46
  17. data/lib/jetstream_bridge/core/connection_manager.rb +513 -0
  18. data/lib/jetstream_bridge/core/debug_helper.rb +9 -3
  19. data/lib/jetstream_bridge/core/health_checker.rb +184 -0
  20. data/lib/jetstream_bridge/core.rb +0 -2
  21. data/lib/jetstream_bridge/facade.rb +212 -0
  22. data/lib/jetstream_bridge/publisher/event_envelope_builder.rb +110 -0
  23. data/lib/jetstream_bridge/publisher/publisher.rb +87 -117
  24. data/lib/jetstream_bridge/rails/integration.rb +8 -5
  25. data/lib/jetstream_bridge/rails/railtie.rb +4 -3
  26. data/lib/jetstream_bridge/tasks/install.rake +0 -1
  27. data/lib/jetstream_bridge/topology/topology.rb +6 -1
  28. data/lib/jetstream_bridge/version.rb +1 -1
  29. data/lib/jetstream_bridge.rb +206 -297
  30. metadata +7 -5
  31. data/lib/jetstream_bridge/core/bridge_helpers.rb +0 -109
  32. data/lib/jetstream_bridge/core/connection.rb +0 -464
  33. data/lib/jetstream_bridge/core/connection_factory.rb +0 -100
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.4.0
4
+ version: 4.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Attara
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-11-24 00:00:00.000000000 Z
11
+ date: 2026-01-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -135,6 +135,7 @@ files:
135
135
  - lib/jetstream_bridge.rb
136
136
  - lib/jetstream_bridge/consumer/consumer.rb
137
137
  - lib/jetstream_bridge/consumer/dlq_publisher.rb
138
+ - lib/jetstream_bridge/consumer/health_monitor.rb
138
139
  - lib/jetstream_bridge/consumer/inbox/inbox_message.rb
139
140
  - lib/jetstream_bridge/consumer/inbox/inbox_processor.rb
140
141
  - lib/jetstream_bridge/consumer/inbox/inbox_repository.rb
@@ -142,18 +143,18 @@ files:
142
143
  - lib/jetstream_bridge/consumer/middleware.rb
143
144
  - lib/jetstream_bridge/consumer/subscription_manager.rb
144
145
  - lib/jetstream_bridge/core.rb
145
- - lib/jetstream_bridge/core/bridge_helpers.rb
146
146
  - lib/jetstream_bridge/core/config.rb
147
147
  - lib/jetstream_bridge/core/config_preset.rb
148
- - lib/jetstream_bridge/core/connection.rb
149
- - lib/jetstream_bridge/core/connection_factory.rb
148
+ - lib/jetstream_bridge/core/connection_manager.rb
150
149
  - lib/jetstream_bridge/core/debug_helper.rb
151
150
  - lib/jetstream_bridge/core/duration.rb
151
+ - lib/jetstream_bridge/core/health_checker.rb
152
152
  - lib/jetstream_bridge/core/logging.rb
153
153
  - lib/jetstream_bridge/core/model_codec_setup.rb
154
154
  - lib/jetstream_bridge/core/model_utils.rb
155
155
  - lib/jetstream_bridge/core/retry_strategy.rb
156
156
  - lib/jetstream_bridge/errors.rb
157
+ - lib/jetstream_bridge/facade.rb
157
158
  - lib/jetstream_bridge/models/event.rb
158
159
  - lib/jetstream_bridge/models/event_envelope.rb
159
160
  - lib/jetstream_bridge/models/inbox_event.rb
@@ -161,6 +162,7 @@ files:
161
162
  - lib/jetstream_bridge/models/publish_result.rb
162
163
  - lib/jetstream_bridge/models/subject.rb
163
164
  - lib/jetstream_bridge/publisher/batch_publisher.rb
165
+ - lib/jetstream_bridge/publisher/event_envelope_builder.rb
164
166
  - lib/jetstream_bridge/publisher/outbox_repository.rb
165
167
  - lib/jetstream_bridge/publisher/publisher.rb
166
168
  - lib/jetstream_bridge/rails.rb
@@ -1,109 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'logging'
4
- require_relative 'connection'
5
-
6
- module JetstreamBridge
7
- module Core
8
- # Internal helper methods extracted from the main JetstreamBridge module
9
- # to keep the public API surface focused.
10
- module BridgeHelpers
11
- private
12
-
13
- # Ensure connection is established before use
14
- # Automatically connects on first use if not already connected
15
- # Thread-safe and idempotent
16
- def connect_if_needed!
17
- return if @connection_initialized
18
-
19
- startup!
20
- end
21
-
22
- # Enforce rate limit on uncached health checks to prevent abuse
23
- # Max 1 uncached request per 5 seconds per process
24
- def enforce_health_check_rate_limit!
25
- @health_check_mutex ||= Mutex.new
26
- @health_check_mutex.synchronize do
27
- now = Time.now
28
- if @last_uncached_health_check
29
- time_since = now - @last_uncached_health_check
30
- if time_since < 5
31
- raise HealthCheckFailedError,
32
- "Health check rate limit exceeded. Please wait #{(5 - time_since).ceil} second(s)"
33
- end
34
- end
35
- @last_uncached_health_check = now
36
- end
37
- end
38
-
39
- def fetch_stream_info
40
- # Ensure we have an active connection before querying stream info
41
- connect_if_needed!
42
-
43
- jts = Connection.jetstream
44
- raise ConnectionNotEstablishedError, 'NATS connection not established' unless jts
45
-
46
- info = jts.stream_info(config.stream_name)
47
-
48
- # Handle both object-style and hash-style access for compatibility
49
- config_data = info.config
50
- state_data = info.state
51
- subjects = config_data.respond_to?(:subjects) ? config_data.subjects : config_data[:subjects]
52
- messages = state_data.respond_to?(:messages) ? state_data.messages : state_data[:messages]
53
-
54
- {
55
- exists: true,
56
- name: config.stream_name,
57
- subjects: subjects,
58
- messages: messages
59
- }
60
- rescue StandardError => e
61
- {
62
- exists: false,
63
- name: config.stream_name,
64
- error: "#{e.class}: #{e.message}"
65
- }
66
- end
67
-
68
- def measure_nats_rtt
69
- nc = Connection.nc
70
- return nil unless nc
71
-
72
- # Prefer native RTT API when available (e.g., new NATS clients)
73
- if nc.respond_to?(:rtt)
74
- rtt_value = normalize_ms(nc.rtt)
75
- return rtt_value if rtt_value
76
- end
77
-
78
- # Fallback for clients without #rtt (nats-pure): measure ping/pong via flush
79
- start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
80
- nc.flush(1)
81
- duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
82
- normalize_ms(duration)
83
- rescue StandardError => e
84
- Logging.warn(
85
- "Failed to measure NATS RTT: #{e.class} #{e.message}",
86
- tag: 'JetstreamBridge'
87
- )
88
- nil
89
- end
90
-
91
- def normalize_ms(value)
92
- return nil if value.nil?
93
- return nil unless value.respond_to?(:to_f)
94
-
95
- numeric = value.to_f
96
- # Heuristic: sub-1 values are likely seconds; convert them to ms for reporting
97
- ms = numeric < 1 ? numeric * 1000 : numeric
98
- ms.round(2)
99
- end
100
-
101
- def assign_config_option!(cfg, key, val)
102
- setter = :"#{key}="
103
- raise ArgumentError, "Unknown configuration option: #{key}" unless cfg.respond_to?(setter)
104
-
105
- cfg.public_send(setter, val)
106
- end
107
- end
108
- end
109
- end
@@ -1,464 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'nats/io/client'
4
- require 'singleton'
5
- require 'oj'
6
- require_relative 'duration'
7
- require_relative 'logging'
8
- require_relative 'config'
9
- require_relative '../topology/topology'
10
-
11
- module JetstreamBridge
12
- # Singleton connection to NATS with thread-safe initialization.
13
- #
14
- # This class manages a single NATS connection for the entire application,
15
- # ensuring thread-safe access in multi-threaded environments like Rails
16
- # with Puma or Sidekiq.
17
- #
18
- # Thread Safety:
19
- # - Connection initialization is synchronized with a mutex
20
- # - The singleton pattern ensures only one connection instance exists
21
- # - Safe to call from multiple threads/workers simultaneously
22
- #
23
- # Example:
24
- # # Safe from any thread
25
- # jts = JetstreamBridge::Connection.connect!
26
- # jts.publish(...)
27
- class Connection
28
- include Singleton
29
-
30
- # Connection states for observability
31
- module State
32
- DISCONNECTED = :disconnected
33
- CONNECTING = :connecting
34
- CONNECTED = :connected
35
- RECONNECTING = :reconnecting
36
- FAILED = :failed
37
- end
38
-
39
- DEFAULT_CONN_OPTS = {
40
- reconnect: true,
41
- reconnect_time_wait: 2,
42
- max_reconnect_attempts: 10,
43
- connect_timeout: 5
44
- }.freeze
45
-
46
- VALID_NATS_SCHEMES = %w[nats nats+tls].freeze
47
-
48
- # Class-level mutex for thread-safe connection initialization
49
- # Using class variable to avoid race condition in mutex creation
50
- # rubocop:disable Style/ClassVars
51
- @@connection_lock = Mutex.new
52
- # rubocop:enable Style/ClassVars
53
-
54
- class << self
55
- # Thread-safe delegator to the singleton instance.
56
- # Returns a live JetStream context.
57
- #
58
- # Safe to call from multiple threads - uses class-level mutex for synchronization.
59
- #
60
- # @return [NATS::JetStream::JS] JetStream context
61
- def connect!
62
- @@connection_lock.synchronize { instance.connect! }
63
- end
64
-
65
- # Optional accessors if callers need raw handles
66
- def nc
67
- instance.__send__(:nc)
68
- end
69
-
70
- def jetstream
71
- instance.__send__(:jetstream)
72
- end
73
- end
74
-
75
- # Idempotent: returns an existing, healthy JetStream context or establishes one.
76
- def connect!
77
- # Check if already connected without acquiring mutex (for performance)
78
- return @jts if @jts && @nc&.connected?
79
-
80
- servers = nats_servers
81
- raise 'No NATS URLs configured' if servers.empty?
82
-
83
- @state = State::CONNECTING
84
- establish_connection_with_retry(servers)
85
-
86
- Logging.info(
87
- "Connected to NATS (#{servers.size} server#{'s' unless servers.size == 1}): " \
88
- "#{sanitize_urls(servers).join(', ')}",
89
- tag: 'JetstreamBridge::Connection'
90
- )
91
-
92
- # Ensure topology (streams, subjects, overlap guard, etc.)
93
- Topology.ensure!(@jts)
94
-
95
- @connected_at = Time.now.utc
96
- @state = State::CONNECTED
97
- @jts
98
- rescue StandardError
99
- @state = State::FAILED
100
- cleanup_connection!
101
- raise
102
- end
103
-
104
- # Public API for checking connection status
105
- #
106
- # Uses cached health check result to avoid excessive network calls.
107
- # Cache expires after 30 seconds.
108
- #
109
- # Thread-safe: Cache updates are synchronized to prevent race conditions.
110
- #
111
- # @param skip_cache [Boolean] Force fresh health check, bypass cache
112
- # @return [Boolean] true if NATS client is connected and JetStream is healthy
113
- def connected?(skip_cache: false)
114
- return false unless @nc&.connected?
115
- return false unless @jts
116
-
117
- # Use cached result if available and fresh
118
- now = Time.now.to_i
119
- return @cached_health_status if !skip_cache && @last_health_check && (now - @last_health_check) < 30
120
-
121
- # Thread-safe cache update to prevent race conditions
122
- @@connection_lock.synchronize do
123
- # Double-check after acquiring lock (another thread may have updated)
124
- now = Time.now.to_i
125
- return @cached_health_status if !skip_cache && @last_health_check && (now - @last_health_check) < 30
126
-
127
- # Perform actual health check
128
- @cached_health_status = jetstream_healthy?
129
- @last_health_check = now
130
- @cached_health_status
131
- end
132
- end
133
-
134
- # Public API for getting connection timestamp
135
- # @return [Time, nil] timestamp when connection was established
136
- attr_reader :connected_at
137
-
138
- # Last reconnection error metadata (exposed for health checks/diagnostics)
139
- attr_reader :last_reconnect_error, :last_reconnect_error_at
140
-
141
- # Get current connection state
142
- #
143
- # @return [Symbol] Current connection state (see State module)
144
- def state
145
- return State::DISCONNECTED unless @nc
146
- return State::FAILED if @last_reconnect_error && !@nc.connected?
147
- return State::RECONNECTING if @reconnecting
148
-
149
- @nc.connected? ? (@state || State::CONNECTED) : State::DISCONNECTED
150
- end
151
-
152
- private
153
-
154
- def jetstream_healthy?
155
- # Verify JetStream responds to simple API call
156
- @jts.account_info
157
- true
158
- rescue StandardError => e
159
- Logging.warn(
160
- "JetStream health check failed: #{e.class} #{e.message}",
161
- tag: 'JetstreamBridge::Connection'
162
- )
163
- false
164
- end
165
-
166
- def nats_servers
167
- servers = JetstreamBridge.config.nats_urls
168
- .to_s
169
- .split(',')
170
- .map(&:strip)
171
- .reject(&:empty?)
172
-
173
- validate_nats_urls!(servers)
174
- servers
175
- end
176
-
177
- def establish_connection_with_retry(servers)
178
- attempts = 0
179
- max_attempts = JetstreamBridge.config.connect_retry_attempts
180
- retry_delay = JetstreamBridge.config.connect_retry_delay
181
-
182
- begin
183
- attempts += 1
184
- establish_connection(servers)
185
- rescue ConnectionError => e
186
- if attempts < max_attempts
187
- delay = retry_delay * attempts
188
- Logging.warn(
189
- "Connection attempt #{attempts}/#{max_attempts} failed: #{e.message}. " \
190
- "Retrying in #{delay}s...",
191
- tag: 'JetstreamBridge::Connection'
192
- )
193
- sleep(delay)
194
- retry
195
- else
196
- Logging.error(
197
- "Failed to establish connection after #{attempts} attempts",
198
- tag: 'JetstreamBridge::Connection'
199
- )
200
- cleanup_connection!
201
- raise
202
- end
203
- end
204
- end
205
-
206
- def establish_connection(servers)
207
- # Use mock NATS client if explicitly enabled for testing
208
- # This allows test helpers to inject a mock without affecting normal operation
209
- @nc = if defined?(JetstreamBridge::TestHelpers) &&
210
- JetstreamBridge::TestHelpers.respond_to?(:test_mode?) &&
211
- JetstreamBridge::TestHelpers.test_mode? &&
212
- JetstreamBridge.instance_variable_defined?(:@mock_nats_client)
213
- JetstreamBridge.instance_variable_get(:@mock_nats_client)
214
- else
215
- NATS::IO::Client.new
216
- end
217
-
218
- # Setup reconnect handler to refresh JetStream context
219
- @nc.on_reconnect do
220
- @reconnecting = true
221
- Logging.info(
222
- 'NATS reconnected, refreshing JetStream context',
223
- tag: 'JetstreamBridge::Connection'
224
- )
225
- refresh_jetstream_context
226
- @reconnecting = false
227
- end
228
-
229
- @nc.on_disconnect do |reason|
230
- @state = State::DISCONNECTED
231
- Logging.warn(
232
- "NATS disconnected: #{reason}",
233
- tag: 'JetstreamBridge::Connection'
234
- )
235
- end
236
-
237
- @nc.on_error do |err|
238
- Logging.error(
239
- "NATS error: #{err}",
240
- tag: 'JetstreamBridge::Connection'
241
- )
242
- end
243
-
244
- # Only connect if not already connected (mock may be pre-connected)
245
- # Note: For test helpers mock, skip connect. For RSpec mocks, always call connect
246
- skip_connect = @nc.connected? &&
247
- defined?(JetstreamBridge::TestHelpers) &&
248
- JetstreamBridge::TestHelpers.respond_to?(:test_mode?) &&
249
- JetstreamBridge::TestHelpers.test_mode?
250
-
251
- @nc.connect({ servers: servers }.merge(DEFAULT_CONN_OPTS)) unless skip_connect
252
-
253
- # Verify connection is established
254
- verify_connection!
255
-
256
- # Create JetStream context
257
- @jts = @nc.jetstream
258
-
259
- # Verify JetStream is available
260
- verify_jetstream!
261
-
262
- # Ensure JetStream responds to #nc
263
- return if @jts.respond_to?(:nc)
264
-
265
- nc_ref = @nc
266
- @jts.define_singleton_method(:nc) { nc_ref }
267
- end
268
-
269
- def validate_nats_urls!(servers)
270
- Logging.debug(
271
- "Validating #{servers.size} NATS URL(s): #{sanitize_urls(servers).join(', ')}",
272
- tag: 'JetstreamBridge::Connection'
273
- )
274
-
275
- servers.each do |url|
276
- # Check for basic URL format (scheme://host)
277
- unless url.include?('://')
278
- Logging.error(
279
- "Invalid URL format (missing scheme): #{url}",
280
- tag: 'JetstreamBridge::Connection'
281
- )
282
- raise ConnectionError, "Invalid NATS URL format: #{url}. Expected format: nats://host:port"
283
- end
284
-
285
- uri = URI.parse(url)
286
-
287
- # Validate scheme
288
- scheme = uri.scheme&.downcase
289
- unless VALID_NATS_SCHEMES.include?(scheme)
290
- Logging.error(
291
- "Invalid URL scheme '#{uri.scheme}': #{Logging.sanitize_url(url)}",
292
- tag: 'JetstreamBridge::Connection'
293
- )
294
- raise ConnectionError, "Invalid NATS URL scheme '#{uri.scheme}' in: #{url}. Expected 'nats' or 'nats+tls'"
295
- end
296
-
297
- # Validate host is present
298
- if uri.host.nil? || uri.host.empty?
299
- Logging.error(
300
- "Missing host in URL: #{Logging.sanitize_url(url)}",
301
- tag: 'JetstreamBridge::Connection'
302
- )
303
- raise ConnectionError, "Invalid NATS URL - missing host: #{url}"
304
- end
305
-
306
- # Validate port if present
307
- if uri.port && (uri.port < 1 || uri.port > 65_535)
308
- Logging.error(
309
- "Invalid port #{uri.port} in URL: #{Logging.sanitize_url(url)}",
310
- tag: 'JetstreamBridge::Connection'
311
- )
312
- raise ConnectionError, "Invalid NATS URL - port must be 1-65535: #{url}"
313
- end
314
-
315
- Logging.debug(
316
- "URL validated: #{Logging.sanitize_url(url)}",
317
- tag: 'JetstreamBridge::Connection'
318
- )
319
- rescue URI::InvalidURIError => e
320
- Logging.error(
321
- "Malformed URL: #{url} (#{e.message})",
322
- tag: 'JetstreamBridge::Connection'
323
- )
324
- raise ConnectionError, "Invalid NATS URL format: #{url} (#{e.message})"
325
- end
326
-
327
- Logging.info(
328
- 'All NATS URLs validated successfully',
329
- tag: 'JetstreamBridge::Connection'
330
- )
331
- end
332
-
333
- def verify_connection!
334
- Logging.debug(
335
- 'Verifying NATS connection...',
336
- tag: 'JetstreamBridge::Connection'
337
- )
338
-
339
- unless @nc.connected?
340
- Logging.error(
341
- 'NATS connection verification failed - client not connected',
342
- tag: 'JetstreamBridge::Connection'
343
- )
344
- raise ConnectionError, 'Failed to establish connection to NATS server(s)'
345
- end
346
-
347
- Logging.info(
348
- 'NATS connection verified successfully',
349
- tag: 'JetstreamBridge::Connection'
350
- )
351
- end
352
-
353
- def verify_jetstream!
354
- Logging.debug(
355
- 'Verifying JetStream availability...',
356
- tag: 'JetstreamBridge::Connection'
357
- )
358
-
359
- # Verify JetStream is enabled by checking account info
360
- account_info = @jts.account_info
361
-
362
- # Handle both object-style and hash-style access for compatibility
363
- streams = account_info.respond_to?(:streams) ? account_info.streams : account_info[:streams]
364
- consumers = account_info.respond_to?(:consumers) ? account_info.consumers : account_info[:consumers]
365
- memory = account_info.respond_to?(:memory) ? account_info.memory : account_info[:memory]
366
- storage = account_info.respond_to?(:storage) ? account_info.storage : account_info[:storage]
367
-
368
- Logging.info(
369
- "JetStream verified - Streams: #{streams}, " \
370
- "Consumers: #{consumers}, " \
371
- "Memory: #{format_bytes(memory)}, " \
372
- "Storage: #{format_bytes(storage)}",
373
- tag: 'JetstreamBridge::Connection'
374
- )
375
- rescue NATS::IO::NoRespondersError
376
- Logging.error(
377
- 'JetStream not available - no responders (JetStream not enabled)',
378
- tag: 'JetstreamBridge::Connection'
379
- )
380
- raise ConnectionError, 'JetStream not enabled on NATS server. Please enable JetStream with -js flag'
381
- rescue StandardError => e
382
- Logging.error(
383
- "JetStream verification failed: #{e.class} - #{e.message}",
384
- tag: 'JetstreamBridge::Connection'
385
- )
386
- raise ConnectionError, "JetStream verification failed: #{e.message}"
387
- end
388
-
389
- def format_bytes(bytes)
390
- return 'N/A' if bytes.nil? || bytes.zero?
391
-
392
- units = %w[B KB MB GB TB]
393
- exp = (Math.log(bytes) / Math.log(1024)).to_i
394
- exp = [exp, units.length - 1].min
395
- "#{(bytes / (1024.0**exp)).round(2)} #{units[exp]}"
396
- end
397
-
398
- def refresh_jetstream_context
399
- @jts = @nc.jetstream
400
- nc_ref = @nc
401
- @jts.define_singleton_method(:nc) { nc_ref } unless @jts.respond_to?(:nc)
402
-
403
- # Re-ensure topology after reconnect
404
- Topology.ensure!(@jts)
405
-
406
- # Invalidate health check cache on successful reconnect
407
- @cached_health_status = nil
408
- @last_health_check = nil
409
-
410
- # Clear error state on successful reconnect
411
- @last_reconnect_error = nil
412
- @last_reconnect_error_at = nil
413
- @state = State::CONNECTED
414
-
415
- Logging.info(
416
- 'JetStream context refreshed successfully after reconnect',
417
- tag: 'JetstreamBridge::Connection'
418
- )
419
- rescue StandardError => e
420
- # Store error state for diagnostics
421
- @last_reconnect_error = e
422
- @last_reconnect_error_at = Time.now
423
- @state = State::FAILED
424
- cleanup_connection!(close_nc: false)
425
- Logging.error(
426
- "Failed to refresh JetStream context: #{e.class} #{e.message}",
427
- tag: 'JetstreamBridge::Connection'
428
- )
429
-
430
- # Invalidate health check cache to force re-check
431
- @cached_health_status = false
432
- @last_health_check = Time.now.to_i
433
- end
434
-
435
- # Expose for class-level helpers (not part of public API)
436
- attr_reader :nc
437
-
438
- def jetstream
439
- @jts
440
- end
441
-
442
- # Mask credentials in NATS URLs:
443
- # - "nats://user:pass@host:4222" -> "nats://user:***@host:4222"
444
- # - "nats://token@host:4222" -> "nats://***@host:4222"
445
- def sanitize_urls(urls)
446
- urls.map { |u| Logging.sanitize_url(u) }
447
- end
448
-
449
- def cleanup_connection!(close_nc: true)
450
- begin
451
- # Avoid touching RSpec doubles used in unit tests
452
- is_rspec_double = defined?(RSpec::Mocks::Double) && @nc.is_a?(RSpec::Mocks::Double)
453
- @nc.close if !is_rspec_double && close_nc && @nc.respond_to?(:close) && @nc.connected?
454
- rescue StandardError
455
- # ignore cleanup errors
456
- end
457
- @nc = nil
458
- @jts = nil
459
- @cached_health_status = nil
460
- @last_health_check = nil
461
- @connected_at = nil
462
- end
463
- end
464
- end