jetstream_bridge 4.0.4 → 4.2.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 +106 -0
  3. data/README.md +22 -1402
  4. data/docs/GETTING_STARTED.md +92 -0
  5. data/docs/PRODUCTION.md +503 -0
  6. data/docs/TESTING.md +414 -0
  7. data/lib/jetstream_bridge/consumer/consumer.rb +101 -5
  8. data/lib/jetstream_bridge/consumer/inbox/inbox_processor.rb +17 -3
  9. data/lib/jetstream_bridge/consumer/inbox/inbox_repository.rb +19 -7
  10. data/lib/jetstream_bridge/consumer/message_processor.rb +88 -52
  11. data/lib/jetstream_bridge/consumer/subscription_manager.rb +24 -15
  12. data/lib/jetstream_bridge/core/bridge_helpers.rb +85 -0
  13. data/lib/jetstream_bridge/core/config.rb +27 -4
  14. data/lib/jetstream_bridge/core/connection.rb +162 -13
  15. data/lib/jetstream_bridge/core.rb +8 -0
  16. data/lib/jetstream_bridge/models/inbox_event.rb +13 -7
  17. data/lib/jetstream_bridge/models/outbox_event.rb +2 -2
  18. data/lib/jetstream_bridge/publisher/publisher.rb +10 -5
  19. data/lib/jetstream_bridge/rails/integration.rb +153 -0
  20. data/lib/jetstream_bridge/rails/railtie.rb +53 -0
  21. data/lib/jetstream_bridge/rails.rb +5 -0
  22. data/lib/jetstream_bridge/tasks/install.rake +1 -1
  23. data/lib/jetstream_bridge/test_helpers/fixtures.rb +41 -0
  24. data/lib/jetstream_bridge/test_helpers/integration_helpers.rb +77 -0
  25. data/lib/jetstream_bridge/test_helpers/matchers.rb +98 -0
  26. data/lib/jetstream_bridge/test_helpers/mock_nats.rb +524 -0
  27. data/lib/jetstream_bridge/test_helpers.rb +85 -121
  28. data/lib/jetstream_bridge/topology/overlap_guard.rb +46 -0
  29. data/lib/jetstream_bridge/topology/stream.rb +7 -4
  30. data/lib/jetstream_bridge/version.rb +1 -1
  31. data/lib/jetstream_bridge.rb +138 -63
  32. metadata +32 -12
  33. data/lib/jetstream_bridge/railtie.rb +0 -49
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'securerandom'
4
+ require_relative 'test_helpers/mock_nats'
5
+ require_relative 'test_helpers/matchers'
6
+ require_relative 'test_helpers/fixtures'
7
+ require_relative 'test_helpers/integration_helpers'
4
8
 
5
9
  module JetstreamBridge
6
10
  # Test helpers for easier testing of JetStream Bridge integrations
@@ -34,13 +38,50 @@ module JetstreamBridge
34
38
  #
35
39
  module TestHelpers
36
40
  class << self
37
- # Enable test mode with in-memory event capture
41
+ # Auto-configure test helpers when RSpec is detected
38
42
  #
43
+ # This method is called automatically when test_helpers.rb is required.
44
+ # It sets up RSpec configuration to enable test mode for tests tagged with :jetstream.
45
+ #
46
+ # @return [void]
47
+ def auto_configure!
48
+ return unless defined?(RSpec)
49
+ return if @configured
50
+
51
+ RSpec.configure do |config|
52
+ config.include JetstreamBridge::TestHelpers
53
+ config.include JetstreamBridge::TestHelpers::Matchers
54
+
55
+ config.before(:each, :jetstream) do
56
+ JetstreamBridge::TestHelpers.enable_test_mode!
57
+ end
58
+
59
+ config.after(:each, :jetstream) do
60
+ JetstreamBridge::TestHelpers.reset_test_mode!
61
+ end
62
+ end
63
+
64
+ @configured = true
65
+ end
66
+
67
+ # Check if auto-configuration has been applied
68
+ #
69
+ # @return [Boolean]
70
+ def configured?
71
+ @configured ||= false
72
+ end
73
+
74
+ # Enable test mode with in-memory event capture and mock NATS connection
75
+ #
76
+ # @param use_mock_nats [Boolean] Whether to use mock NATS connection (default: true)
39
77
  # @return [void]
40
- def enable_test_mode!
78
+ def enable_test_mode!(use_mock_nats: true)
41
79
  @test_mode = true
42
80
  @published_events = []
43
81
  @consumed_events = []
82
+ @mock_nats_enabled = use_mock_nats
83
+
84
+ setup_mock_nats if use_mock_nats
44
85
  end
45
86
 
46
87
  # Reset test mode and clear captured events
@@ -50,6 +91,44 @@ module JetstreamBridge
50
91
  @test_mode = false
51
92
  @published_events = []
52
93
  @consumed_events = []
94
+
95
+ teardown_mock_nats if @mock_nats_enabled
96
+ @mock_nats_enabled = false
97
+ end
98
+
99
+ # Setup mock NATS connection
100
+ #
101
+ # @return [void]
102
+ def setup_mock_nats
103
+ MockNats.reset!
104
+ @mock_connection = MockNats.create_mock_connection
105
+ @mock_connection.connect
106
+
107
+ # Store the mock for Connection to use
108
+ JetstreamBridge.instance_variable_set(:@mock_nats_client, @mock_connection)
109
+ end
110
+
111
+ # Teardown mock NATS connection
112
+ #
113
+ # @return [void]
114
+ def teardown_mock_nats
115
+ MockNats.reset!
116
+ @mock_connection = nil
117
+ return unless JetstreamBridge.instance_variable_defined?(:@mock_nats_client)
118
+
119
+ JetstreamBridge.remove_instance_variable(:@mock_nats_client)
120
+ end
121
+
122
+ # Get the current mock connection
123
+ #
124
+ # @return [MockNats::MockConnection, nil]
125
+ attr_reader :mock_connection
126
+
127
+ # Get the mock storage for direct access in tests
128
+ #
129
+ # @return [MockNats::InMemoryStorage]
130
+ def mock_storage
131
+ MockNats.storage
53
132
  end
54
133
 
55
134
  # Check if test mode is enabled
@@ -134,6 +213,7 @@ module JetstreamBridge
134
213
  }
135
214
  )
136
215
  end
216
+ module_function :build_jetstream_event
137
217
 
138
218
  # Simulate triggering an event to a consumer
139
219
  #
@@ -152,124 +232,8 @@ module JetstreamBridge
152
232
  TestHelpers.record_consumed_event(event.to_h) if TestHelpers.test_mode?
153
233
  handler.call(event)
154
234
  end
155
-
156
- # RSpec matchers module
157
- #
158
- # @example Include in RSpec
159
- # RSpec.configure do |config|
160
- # config.include JetstreamBridge::TestHelpers
161
- # config.include JetstreamBridge::TestHelpers::Matchers
162
- # end
163
- #
164
- module Matchers
165
- # Matcher for checking if an event was published
166
- #
167
- # @param event_type [String] Event type to match
168
- # @param payload [Hash] Optional payload attributes to match
169
- # @return [HavePublished] Matcher instance
170
- #
171
- # @example
172
- # expect(JetstreamBridge).to have_published(
173
- # event_type: "user.created",
174
- # payload: { id: 1 }
175
- # )
176
- #
177
- def have_published(event_type:, payload: {})
178
- HavePublished.new(event_type, payload)
179
- end
180
-
181
- # Matcher implementation for have_published
182
- class HavePublished
183
- def initialize(event_type, payload_attributes)
184
- @event_type = event_type
185
- @payload_attributes = payload_attributes
186
- end
187
-
188
- def matches?(_actual)
189
- TestHelpers.published_events.any? do |event|
190
- matches_event_type?(event) && matches_payload?(event)
191
- end
192
- end
193
-
194
- def failure_message
195
- "expected to have published event_type: #{@event_type.inspect} " \
196
- "with payload: #{@payload_attributes.inspect}\n" \
197
- "but found events: #{TestHelpers.published_events.map { |e| e['event_type'] }.inspect}"
198
- end
199
-
200
- def failure_message_when_negated
201
- "expected not to have published event_type: #{@event_type.inspect} " \
202
- "with payload: #{@payload_attributes.inspect}"
203
- end
204
-
205
- private
206
-
207
- def matches_event_type?(event)
208
- event['event_type'] == @event_type || event[:event_type] == @event_type
209
- end
210
-
211
- def matches_payload?(event)
212
- payload = event['payload'] || event[:payload] || {}
213
- @payload_attributes.all? do |key, value|
214
- payload_value = payload[key.to_s] || payload[key.to_sym]
215
- if value.is_a?(RSpec::Matchers::BuiltIn::BaseMatcher)
216
- value.matches?(payload)
217
- else
218
- payload_value == value
219
- end
220
- end
221
- end
222
- end
223
-
224
- # Matcher for checking publish result
225
- #
226
- # @example
227
- # result = JetstreamBridge.publish(...)
228
- # expect(result).to be_publish_success
229
- #
230
- def be_publish_success
231
- BePublishSuccess.new
232
- end
233
-
234
- # Matcher implementation for be_publish_success
235
- class BePublishSuccess
236
- def matches?(actual)
237
- actual.respond_to?(:success?) && actual.success?
238
- end
239
-
240
- def failure_message
241
- 'expected PublishResult to be successful but it failed'
242
- end
243
-
244
- def failure_message_when_negated
245
- 'expected PublishResult to not be successful but it was'
246
- end
247
- end
248
-
249
- # Matcher for checking publish failure
250
- #
251
- # @example
252
- # result = JetstreamBridge.publish(...)
253
- # expect(result).to be_publish_failure
254
- #
255
- def be_publish_failure
256
- BePublishFailure.new
257
- end
258
-
259
- # Matcher implementation for be_publish_failure
260
- class BePublishFailure
261
- def matches?(actual)
262
- actual.respond_to?(:failure?) && actual.failure?
263
- end
264
-
265
- def failure_message
266
- 'expected PublishResult to be a failure but it succeeded'
267
- end
268
-
269
- def failure_message_when_negated
270
- 'expected PublishResult to not be a failure but it was'
271
- end
272
- end
273
- end
274
235
  end
275
236
  end
237
+
238
+ # Auto-configure when loaded (only if RSpec is available)
239
+ JetstreamBridge::TestHelpers.auto_configure! if defined?(RSpec)
@@ -7,6 +7,12 @@ require_relative '../core/logging'
7
7
  module JetstreamBridge
8
8
  # Checks for overlapping subjects.
9
9
  class OverlapGuard
10
+ # Cache for stream metadata to reduce N+1 API calls
11
+ @cache_mutex = Mutex.new
12
+ @stream_cache = {}
13
+ @cache_expires_at = Time.at(0)
14
+ CACHE_TTL = 60 # seconds
15
+
10
16
  class << self
11
17
  # Raise if any desired subjects conflict with other streams.
12
18
  def check!(jts, target_name, new_subjects)
@@ -46,6 +52,46 @@ module JetstreamBridge
46
52
  end
47
53
 
48
54
  def list_streams_with_subjects(jts)
55
+ # Use cached data if available and fresh
56
+ @cache_mutex.synchronize do
57
+ now = Time.now
58
+ if now < @cache_expires_at && @stream_cache.key?(:data)
59
+ Logging.debug(
60
+ 'Using cached stream metadata',
61
+ tag: 'JetstreamBridge::OverlapGuard'
62
+ )
63
+ return @stream_cache[:data]
64
+ end
65
+
66
+ # Fetch fresh data
67
+ Logging.debug(
68
+ 'Fetching fresh stream metadata from NATS',
69
+ tag: 'JetstreamBridge::OverlapGuard'
70
+ )
71
+ result = fetch_streams_uncached(jts)
72
+ @stream_cache = { data: result }
73
+ @cache_expires_at = now + CACHE_TTL
74
+ result
75
+ end
76
+ rescue StandardError => e
77
+ Logging.warn(
78
+ "Failed to fetch stream metadata: #{e.class} #{e.message}",
79
+ tag: 'JetstreamBridge::OverlapGuard'
80
+ )
81
+ # Return cached data on error if available, otherwise empty array
82
+ @cache_mutex.synchronize { @stream_cache[:data] || [] }
83
+ end
84
+
85
+ # Clear the cache (useful for testing)
86
+ def clear_cache!
87
+ @cache_mutex.synchronize do
88
+ @stream_cache = {}
89
+ @cache_expires_at = Time.at(0)
90
+ end
91
+ end
92
+
93
+ # Fetch stream metadata without caching (for internal use)
94
+ def fetch_streams_uncached(jts)
49
95
  list_stream_names(jts).map do |name|
50
96
  info = jts.stream_info(name)
51
97
  # Handle both object-style and hash-style access for compatibility
@@ -79,7 +79,7 @@ module JetstreamBridge
79
79
  def log_retention_mismatch(name, have:, want:)
80
80
  Logging.warn(
81
81
  "Stream #{name} retention mismatch (have=#{have.inspect}, want=#{want.inspect}). " \
82
- "Retention is immutable; skipping retention change.",
82
+ 'Retention is immutable; skipping retention change.',
83
83
  tag: 'JetstreamBridge::Stream'
84
84
  )
85
85
  end
@@ -141,7 +141,11 @@ module JetstreamBridge
141
141
  return
142
142
  end
143
143
 
144
- add_subjects(jts, name, existing, to_add) if to_add.any?
144
+ # Add subjects if needed and return (logging is handled in add_subjects)
145
+ if to_add.any?
146
+ add_subjects(jts, name, existing, to_add)
147
+ return
148
+ end
145
149
 
146
150
  # Storage can be updated; do it without passing retention.
147
151
  storage = config_data.respond_to?(:storage) ? config_data.storage : config_data[:storage]
@@ -152,8 +156,7 @@ module JetstreamBridge
152
156
  return
153
157
  end
154
158
 
155
- return if to_add.any?
156
-
159
+ # If we reach here, nothing was updated
157
160
  StreamSupport.log_already_covered(name)
158
161
  end
159
162
 
@@ -4,5 +4,5 @@
4
4
  #
5
5
  # Version constant for the gem.
6
6
  module JetstreamBridge
7
- VERSION = '4.0.4'
7
+ VERSION = '4.2.0'
8
8
  end
@@ -1,10 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'jetstream_bridge/version'
4
- require_relative 'jetstream_bridge/core/config'
5
- require_relative 'jetstream_bridge/core/duration'
6
- require_relative 'jetstream_bridge/core/logging'
7
- require_relative 'jetstream_bridge/core/connection'
4
+ require_relative 'jetstream_bridge/core'
8
5
  require_relative 'jetstream_bridge/publisher/publisher'
9
6
  require_relative 'jetstream_bridge/publisher/batch_publisher'
10
7
  require_relative 'jetstream_bridge/consumer/consumer'
@@ -12,8 +9,8 @@ require_relative 'jetstream_bridge/consumer/middleware'
12
9
  require_relative 'jetstream_bridge/models/publish_result'
13
10
  require_relative 'jetstream_bridge/models/event'
14
11
 
15
- # If you have a Railtie for tasks/eager-loading
16
- require_relative 'jetstream_bridge/railtie' if defined?(Rails::Railtie)
12
+ # Rails-specific entry point (lifecycle helpers + Railtie)
13
+ require_relative 'jetstream_bridge/rails' if defined?(Rails::Railtie)
17
14
 
18
15
  # Load gem-provided models from lib/
19
16
  require_relative 'jetstream_bridge/models/inbox_event'
@@ -31,6 +28,7 @@ require_relative 'jetstream_bridge/models/outbox_event'
31
28
  # - Built-in health checks and monitoring
32
29
  # - Middleware support for cross-cutting concerns
33
30
  # - Rails integration with generators and migrations
31
+ # - Graceful startup/shutdown lifecycle management
34
32
  #
35
33
  # @example Quick start
36
34
  # # Configure
@@ -43,6 +41,9 @@ require_relative 'jetstream_bridge/models/outbox_event'
43
41
  # config.use_inbox = true
44
42
  # end
45
43
  #
44
+ # # Explicitly start connection (or use Rails railtie for automatic startup)
45
+ # JetstreamBridge.startup!
46
+ #
46
47
  # # Publish events
47
48
  # JetstreamBridge.publish(
48
49
  # event_type: "user.created",
@@ -50,9 +51,13 @@ require_relative 'jetstream_bridge/models/outbox_event'
50
51
  # )
51
52
  #
52
53
  # # Consume events
53
- # JetstreamBridge.subscribe do |event|
54
+ # consumer = JetstreamBridge.subscribe do |event|
54
55
  # puts "Received: #{event.type} - #{event.payload.to_h}"
55
- # end.run!
56
+ # end
57
+ # consumer.run!
58
+ #
59
+ # # Graceful shutdown
60
+ # at_exit { JetstreamBridge.shutdown! }
56
61
  #
57
62
  # @see Publisher For publishing events
58
63
  # @see Consumer For consuming events
@@ -61,25 +66,55 @@ require_relative 'jetstream_bridge/models/outbox_event'
61
66
  #
62
67
  module JetstreamBridge
63
68
  class << self
69
+ include Core::BridgeHelpers
70
+
64
71
  def config
65
72
  @config ||= Config.new
66
73
  end
67
74
 
68
- def configure(overrides = {})
75
+ # Configure JetStream Bridge settings
76
+ #
77
+ # This method sets configuration WITHOUT automatically establishing a connection.
78
+ # Connection must be established explicitly via startup! or will be established
79
+ # automatically on first use (publish/subscribe) or via Rails railtie initialization.
80
+ #
81
+ # @example Basic configuration
82
+ # JetstreamBridge.configure do |config|
83
+ # config.nats_urls = "nats://localhost:4222"
84
+ # config.app_name = "my_app"
85
+ # config.destination_app = "worker"
86
+ # end
87
+ # JetstreamBridge.startup! # Explicitly start connection
88
+ #
89
+ # @example With hash overrides
90
+ # JetstreamBridge.configure(env: 'production', app_name: 'my_app')
91
+ #
92
+ # @param overrides [Hash] Configuration key-value pairs to set
93
+ # @yield [Config] Configuration object for block-based configuration
94
+ # @return [Config] The configured instance
95
+ def configure(overrides = {}, **extra_overrides)
96
+ # Merge extra keyword arguments into overrides hash
97
+ all_overrides = overrides.nil? ? extra_overrides : overrides.merge(extra_overrides)
98
+
69
99
  cfg = config
70
- overrides.each { |k, v| assign!(cfg, k, v) } unless overrides.nil? || overrides.empty?
100
+ all_overrides.each { |k, v| assign_config_option!(cfg, k, v) } unless all_overrides.empty?
71
101
  yield(cfg) if block_given?
102
+
72
103
  cfg
73
104
  end
74
105
 
75
106
  # Configure with a preset
76
107
  #
108
+ # This method applies a configuration preset. Connection must be
109
+ # established separately via startup! or via Rails railtie.
110
+ #
77
111
  # @example
78
112
  # JetstreamBridge.configure_for(:production) do |config|
79
113
  # config.nats_urls = ENV["NATS_URLS"]
80
114
  # config.app_name = "my_app"
81
115
  # config.destination_app = "worker"
82
116
  # end
117
+ # JetstreamBridge.startup! # Explicitly start connection
83
118
  #
84
119
  # @param preset [Symbol] Preset name (:development, :test, :production, etc.)
85
120
  # @yield [Config] Configuration object
@@ -93,6 +128,61 @@ module JetstreamBridge
93
128
 
94
129
  def reset!
95
130
  @config = nil
131
+ @connection_initialized = false
132
+ end
133
+
134
+ # Initialize the JetStream Bridge connection and topology
135
+ #
136
+ # This method is called automatically by `configure`, but can be called
137
+ # explicitly if needed. It's idempotent and safe to call multiple times.
138
+ #
139
+ # @return [void]
140
+ def startup!
141
+ return if @connection_initialized
142
+
143
+ Connection.connect!
144
+ @connection_initialized = true
145
+ Logging.info('JetStream Bridge started successfully', tag: 'JetstreamBridge')
146
+ end
147
+
148
+ # Reconnect to NATS
149
+ #
150
+ # Closes existing connection and establishes a new one. Useful for:
151
+ # - Forking web servers (Puma, Unicorn) after worker boot
152
+ # - Recovering from connection issues
153
+ # - Configuration changes that require reconnection
154
+ #
155
+ # @example In Puma configuration (config/puma.rb)
156
+ # on_worker_boot do
157
+ # JetstreamBridge.reconnect! if defined?(JetstreamBridge)
158
+ # end
159
+ #
160
+ # @return [void]
161
+ # @raise [ConnectionError] If unable to reconnect to NATS
162
+ def reconnect!
163
+ Logging.info('Reconnecting to NATS...', tag: 'JetstreamBridge')
164
+ shutdown! if @connection_initialized
165
+ startup!
166
+ end
167
+
168
+ # Gracefully shutdown the JetStream Bridge connection
169
+ #
170
+ # Closes the NATS connection and cleans up resources. Should be called
171
+ # during application shutdown (e.g., in at_exit or signal handlers).
172
+ #
173
+ # @return [void]
174
+ def shutdown!
175
+ return unless @connection_initialized
176
+
177
+ begin
178
+ nc = Connection.nc
179
+ nc&.close if nc&.connected?
180
+ Logging.info('JetStream Bridge shut down gracefully', tag: 'JetstreamBridge')
181
+ rescue StandardError => e
182
+ Logging.error("Error during shutdown: #{e.message}", tag: 'JetstreamBridge')
183
+ ensure
184
+ @connection_initialized = false
185
+ end
96
186
  end
97
187
 
98
188
  def use_outbox?
@@ -110,11 +200,16 @@ module JetstreamBridge
110
200
  # Establishes a connection and ensures stream topology.
111
201
  #
112
202
  # @return [Object] JetStream context
113
- def ensure_topology!
203
+ def connect_and_ensure_stream!
114
204
  Connection.connect!
115
205
  Connection.jetstream
116
206
  end
117
207
 
208
+ # Backwards-compatible alias for the previous method name
209
+ def ensure_topology!
210
+ connect_and_ensure_stream!
211
+ end
212
+
118
213
  # Active health check for monitoring and readiness probes
119
214
  #
120
215
  # Performs actual operations to verify system health:
@@ -122,17 +217,29 @@ module JetstreamBridge
122
217
  # - Verifies stream exists and is accessible (active: queries stream info)
123
218
  # - Tests NATS round-trip communication (active: RTT measurement)
124
219
  #
220
+ # Rate Limiting: To prevent abuse, uncached health checks are limited to once every 5 seconds.
221
+ # Cached results (within 30s TTL) bypass this limit via Connection.instance.connected?.
222
+ #
223
+ # @param skip_cache [Boolean] Force fresh health check, bypass connection cache (rate limited)
125
224
  # @return [Hash] Health status including NATS connection, stream, and version
126
- def health_check
225
+ # @raise [HealthCheckFailedError] If skip_cache requested too frequently
226
+ def health_check(skip_cache: false)
227
+ # Rate limit uncached requests to prevent abuse (max 1 per 5 seconds)
228
+ enforce_health_check_rate_limit! if skip_cache
229
+
127
230
  start_time = Time.now
128
231
  conn_instance = Connection.instance
129
232
 
130
233
  # Active check: calls @jts.account_info internally
131
- connected = conn_instance.connected?
234
+ # Pass skip_cache to force fresh check if requested
235
+ connected = conn_instance.connected?(skip_cache: skip_cache)
132
236
  connected_at = conn_instance.connected_at
237
+ connection_state = conn_instance.state
238
+ last_error = conn_instance.last_reconnect_error
239
+ last_error_at = conn_instance.last_reconnect_error_at
133
240
 
134
241
  # Active check: queries actual stream from NATS server
135
- stream_info = fetch_stream_info if connected
242
+ stream_info = connected ? fetch_stream_info : { exists: false, name: config.stream_name }
136
243
 
137
244
  # Active check: measure NATS round-trip time
138
245
  rtt_ms = measure_nats_rtt if connected
@@ -141,8 +248,13 @@ module JetstreamBridge
141
248
 
142
249
  {
143
250
  healthy: connected && stream_info&.fetch(:exists, false),
144
- nats_connected: connected,
145
- connected_at: connected_at&.iso8601,
251
+ connection: {
252
+ state: connection_state,
253
+ connected: connected,
254
+ connected_at: connected_at&.iso8601,
255
+ last_error: last_error&.message,
256
+ last_error_at: last_error_at&.iso8601
257
+ },
146
258
  stream: stream_info,
147
259
  performance: {
148
260
  nats_rtt_ms: rtt_ms,
@@ -161,6 +273,10 @@ module JetstreamBridge
161
273
  rescue StandardError => e
162
274
  {
163
275
  healthy: false,
276
+ connection: {
277
+ state: :failed,
278
+ connected: false
279
+ },
164
280
  error: "#{e.class}: #{e.message}"
165
281
  }
166
282
  end
@@ -183,6 +299,8 @@ module JetstreamBridge
183
299
 
184
300
  # Convenience method to publish events
185
301
  #
302
+ # Automatically establishes connection on first use if not already connected.
303
+ #
186
304
  # Supports three usage patterns:
187
305
  #
188
306
  # 1. Structured parameters (recommended):
@@ -210,6 +328,7 @@ module JetstreamBridge
210
328
  # logger.error("Publish failed: #{result.error}")
211
329
  # end
212
330
  def publish(event_or_hash = nil, resource_type: nil, event_type: nil, payload: nil, subject: nil, **)
331
+ connect_if_needed!
213
332
  publisher = Publisher.new
214
333
  publisher.publish(event_or_hash, resource_type: resource_type, event_type: event_type, payload: payload,
215
334
  subject: subject, **)
@@ -254,6 +373,8 @@ module JetstreamBridge
254
373
 
255
374
  # Convenience method to start consuming messages
256
375
  #
376
+ # Automatically establishes connection on first use if not already connected.
377
+ #
257
378
  # Supports two usage patterns:
258
379
  #
259
380
  # 1. With a block (recommended):
@@ -280,6 +401,7 @@ module JetstreamBridge
280
401
  # @yield [event] Yields Models::Event object to block
281
402
  # @return [Consumer, Thread] Consumer instance or Thread if run: true
282
403
  def subscribe(handler = nil, run: false, durable_name: nil, batch_size: nil, &block)
404
+ connect_if_needed!
283
405
  handler ||= block
284
406
  raise ArgumentError, 'Handler or block required' unless handler
285
407
 
@@ -293,52 +415,5 @@ module JetstreamBridge
293
415
  consumer
294
416
  end
295
417
  end
296
-
297
- private
298
-
299
- def fetch_stream_info
300
- jts = Connection.jetstream
301
- info = jts.stream_info(config.stream_name)
302
-
303
- # Handle both object-style and hash-style access for compatibility
304
- config_data = info.config
305
- state_data = info.state
306
- subjects = config_data.respond_to?(:subjects) ? config_data.subjects : config_data[:subjects]
307
- messages = state_data.respond_to?(:messages) ? state_data.messages : state_data[:messages]
308
-
309
- {
310
- exists: true,
311
- name: config.stream_name,
312
- subjects: subjects,
313
- messages: messages
314
- }
315
- rescue StandardError => e
316
- {
317
- exists: false,
318
- name: config.stream_name,
319
- error: "#{e.class}: #{e.message}"
320
- }
321
- end
322
-
323
- def measure_nats_rtt
324
- # Measure round-trip time using NATS RTT method
325
- nc = Connection.nc
326
- start = Time.now
327
- nc.rtt
328
- ((Time.now - start) * 1000).round(2)
329
- rescue StandardError => e
330
- Logging.warn(
331
- "Failed to measure NATS RTT: #{e.class} #{e.message}",
332
- tag: 'JetstreamBridge'
333
- )
334
- nil
335
- end
336
-
337
- def assign!(cfg, key, val)
338
- setter = :"#{key}="
339
- raise ArgumentError, "Unknown configuration option: #{key}" unless cfg.respond_to?(setter)
340
-
341
- cfg.public_send(setter, val)
342
- end
343
418
  end
344
419
  end