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
@@ -2,12 +2,15 @@
2
2
 
3
3
  require_relative 'jetstream_bridge/version'
4
4
  require_relative 'jetstream_bridge/core'
5
+ require_relative 'jetstream_bridge/core/connection_manager'
6
+ require_relative 'jetstream_bridge/publisher/event_envelope_builder'
5
7
  require_relative 'jetstream_bridge/publisher/publisher'
6
8
  require_relative 'jetstream_bridge/publisher/batch_publisher'
7
9
  require_relative 'jetstream_bridge/consumer/consumer'
8
10
  require_relative 'jetstream_bridge/consumer/middleware'
9
11
  require_relative 'jetstream_bridge/models/publish_result'
10
12
  require_relative 'jetstream_bridge/models/event'
13
+ require_relative 'jetstream_bridge/facade'
11
14
 
12
15
  # Rails-specific entry point (lifecycle helpers + Railtie)
13
16
  require_relative 'jetstream_bridge/rails' if defined?(Rails::Railtie)
@@ -18,31 +21,19 @@ require_relative 'jetstream_bridge/models/outbox_event'
18
21
 
19
22
  # JetStream Bridge - Production-safe realtime data bridge using NATS JetStream.
20
23
  #
21
- # JetStream Bridge provides a reliable, production-ready way to publish and consume
22
- # events using NATS JetStream with features like:
23
- #
24
- # - Transactional Outbox pattern for guaranteed event publishing
25
- # - Idempotent Inbox pattern for exactly-once message processing
26
- # - Dead Letter Queue (DLQ) for poison message handling
27
- # - Automatic stream provisioning and overlap detection
28
- # - Built-in health checks and monitoring
29
- # - Middleware support for cross-cutting concerns
30
- # - Rails integration with generators and migrations
31
- # - Graceful startup/shutdown lifecycle management
32
- #
33
24
  # @example Quick start
34
25
  # # Configure
35
26
  # JetstreamBridge.configure do |config|
36
27
  # config.nats_urls = "nats://localhost:4222"
37
- # config.env = "development"
38
28
  # config.app_name = "my_app"
39
29
  # config.destination_app = "other_app"
30
+ # config.stream_name = "MY_STREAM"
40
31
  # config.use_outbox = true
41
32
  # config.use_inbox = true
42
33
  # end
43
34
  #
44
- # # Explicitly start connection (or use Rails railtie for automatic startup)
45
- # JetstreamBridge.startup!
35
+ # # Connect (validates config and establishes connection)
36
+ # JetstreamBridge.connect!
46
37
  #
47
38
  # # Publish events
48
39
  # JetstreamBridge.publish(
@@ -57,364 +48,282 @@ require_relative 'jetstream_bridge/models/outbox_event'
57
48
  # consumer.run!
58
49
  #
59
50
  # # Graceful shutdown
60
- # at_exit { JetstreamBridge.shutdown! }
61
- #
62
- # @see Publisher For publishing events
63
- # @see Consumer For consuming events
64
- # @see Config For configuration options
65
- # @see TestHelpers For testing utilities
51
+ # at_exit { JetstreamBridge.disconnect! }
66
52
  #
67
53
  module JetstreamBridge
68
54
  class << self
69
- include Core::BridgeHelpers
70
-
55
+ # Get configuration instance
56
+ #
57
+ # @return [Config] Configuration object
71
58
  def config
72
- @config ||= Config.new
59
+ facade.config
73
60
  end
74
61
 
75
62
  # Configure JetStream Bridge settings
76
63
  #
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.
64
+ # @yield [Config] Configuration object for block-based configuration
65
+ # @return [Config] The configured instance
80
66
  #
81
- # @example Basic configuration
67
+ # @example
82
68
  # JetstreamBridge.configure do |config|
83
69
  # config.nats_urls = "nats://localhost:4222"
84
70
  # config.app_name = "my_app"
85
71
  # config.destination_app = "worker"
72
+ # config.stream_name = "MY_STREAM"
86
73
  # 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
-
99
- cfg = config
100
- all_overrides.each { |k, v| assign_config_option!(cfg, k, v) } unless all_overrides.empty?
101
- yield(cfg) if block_given?
102
-
103
- cfg
74
+ def configure(&)
75
+ facade.configure(&)
104
76
  end
105
77
 
106
- # Configure with a preset
78
+ # Connect to NATS and ensure stream topology
107
79
  #
108
- # This method applies a configuration preset. Connection must be
109
- # established separately via startup! or via Rails railtie.
80
+ # Validates configuration and establishes connection.
81
+ # Idempotent - safe to call multiple times.
82
+ #
83
+ # @return [void]
84
+ # @raise [ConfigurationError] If configuration is invalid
85
+ # @raise [ConnectionError] If unable to connect
110
86
  #
111
87
  # @example
112
- # JetstreamBridge.configure_for(:production) do |config|
113
- # config.nats_urls = ENV["NATS_URLS"]
114
- # config.app_name = "my_app"
115
- # config.destination_app = "worker"
116
- # end
117
- # JetstreamBridge.startup! # Explicitly start connection
118
- #
119
- # @param preset [Symbol] Preset name (:development, :test, :production, etc.)
120
- # @yield [Config] Configuration object
121
- # @return [Config] Configured instance
122
- def configure_for(preset)
123
- configure do |cfg|
124
- cfg.apply_preset(preset)
125
- yield(cfg) if block_given?
126
- end
127
- end
128
-
129
- def reset!
130
- @config = nil
131
- @connection_initialized = false
88
+ # JetstreamBridge.connect!
89
+ def connect!
90
+ facade.connect!
132
91
  end
133
92
 
134
- # Initialize the JetStream Bridge connection and topology
93
+ # Disconnect from NATS
135
94
  #
136
- # This method can be called explicitly if needed. It's idempotent and safe to call multiple times.
95
+ # Closes the NATS connection and cleans up resources.
137
96
  #
138
97
  # @return [void]
139
- def startup!
140
- return if @connection_initialized
141
-
142
- connect_and_ensure_stream!
143
- @connection_initialized = true
144
- Logging.info('JetStream Bridge started successfully', tag: 'JetstreamBridge')
98
+ #
99
+ # @example
100
+ # at_exit { JetstreamBridge.disconnect! }
101
+ def disconnect!
102
+ facade.disconnect!
145
103
  end
146
104
 
147
- # Reconnect to NATS
105
+ # Reconnect to NATS (disconnect + connect)
148
106
  #
149
- # Closes existing connection and establishes a new one. Useful for:
107
+ # Useful for:
150
108
  # - Forking web servers (Puma, Unicorn) after worker boot
151
109
  # - Recovering from connection issues
152
- # - Configuration changes that require reconnection
153
110
  #
154
- # @example In Puma configuration (config/puma.rb)
111
+ # @return [void]
112
+ #
113
+ # @example In Puma configuration
155
114
  # on_worker_boot do
156
115
  # JetstreamBridge.reconnect! if defined?(JetstreamBridge)
157
116
  # end
158
- #
159
- # @return [void]
160
- # @raise [ConnectionError] If unable to reconnect to NATS
161
117
  def reconnect!
162
- Logging.info('Reconnecting to NATS...', tag: 'JetstreamBridge')
163
- shutdown! if @connection_initialized
164
- startup!
118
+ facade.reconnect!
165
119
  end
166
120
 
167
- # Gracefully shutdown the JetStream Bridge connection
121
+ # Publish an event
168
122
  #
169
- # Closes the NATS connection and cleans up resources. Should be called
170
- # during application shutdown (e.g., in at_exit or signal handlers).
123
+ # Simplified API with single pattern:
124
+ # - event_type: required (e.g., "user.created")
125
+ # - payload: required event data
126
+ # - resource_type: optional (inferred from event_type if dotted notation)
127
+ # - All other fields are optional
171
128
  #
172
- # @return [void]
173
- def shutdown!
174
- return unless @connection_initialized
175
-
176
- begin
177
- nc = Connection.nc
178
- nc&.close if nc&.connected?
179
- Logging.info('JetStream Bridge shut down gracefully', tag: 'JetstreamBridge')
180
- rescue StandardError => e
181
- Logging.error("Error during shutdown: #{e.message}", tag: 'JetstreamBridge')
182
- ensure
183
- @connection_initialized = false
184
- end
185
- end
186
-
187
- def use_outbox?
188
- config.use_outbox
189
- end
190
-
191
- def use_inbox?
192
- config.use_inbox
193
- end
194
-
195
- def use_dlq?
196
- config.use_dlq
129
+ # @param event_type [String] Event type (required)
130
+ # @param payload [Hash] Event payload data (required)
131
+ # @param resource_type [String, nil] Resource type (optional, inferred if nil)
132
+ # @param subject [String, nil] Optional NATS subject override
133
+ # @param options [Hash] Additional options (event_id, occurred_at, trace_id, etc.)
134
+ # @return [Models::PublishResult] Result object with success status and metadata
135
+ #
136
+ # @example Basic publishing
137
+ # result = JetstreamBridge.publish(
138
+ # event_type: "user.created",
139
+ # payload: { id: 1, email: "ada@example.com" }
140
+ # )
141
+ # puts "Success!" if result.success?
142
+ #
143
+ # @example With options
144
+ # result = JetstreamBridge.publish(
145
+ # event_type: "order.updated",
146
+ # payload: { id: 123, status: "shipped" },
147
+ # resource_type: "order",
148
+ # trace_id: request_id
149
+ # )
150
+ def publish(event_type:, payload:, **)
151
+ facade.publish(event_type: event_type, payload: payload, **)
197
152
  end
198
153
 
199
- # Establishes a connection and ensures stream topology.
200
- #
201
- # @return [Object] JetStream context
202
- def connect_and_ensure_stream!
203
- Connection.connect!
204
- jts = Connection.jetstream
205
- Topology.ensure!(jts)
206
- jts
207
- end
154
+ # Publish variant that raises on error
155
+ #
156
+ # @param (see #publish)
157
+ # @return [Models::PublishResult] Result object
158
+ # @raise [PublishError] If publishing fails
159
+ #
160
+ # @example
161
+ # JetstreamBridge.publish!(event_type: "user.created", payload: { id: 1 })
162
+ def publish!(event_type:, payload:, **)
163
+ result = publish(event_type: event_type, payload: payload, **)
164
+ if result.failure?
165
+ raise PublishError.new(result.error&.message, event_id: result.event_id, subject: result.subject)
166
+ end
208
167
 
209
- # Backwards-compatible alias for the previous method name
210
- def ensure_topology!
211
- connect_and_ensure_stream!
168
+ result
212
169
  end
213
170
 
214
- # Active health check for monitoring and readiness probes
171
+ # Publish a complete event envelope (advanced usage)
215
172
  #
216
- # Performs actual operations to verify system health:
217
- # - Checks NATS connection (active: calls account_info API)
218
- # - Verifies stream exists and is accessible (active: queries stream info)
219
- # - Tests NATS round-trip communication (active: RTT measurement)
173
+ # Provides full control over the envelope structure. Use this when you need to
174
+ # manually construct the envelope or preserve all fields from an external source.
220
175
  #
221
- # Rate Limiting: To prevent abuse, uncached health checks are limited to once every 5 seconds.
222
- # Cached results (within 30s TTL) bypass this limit via Connection.instance.connected?.
176
+ # @param envelope [Hash] Complete event envelope with all required fields
177
+ # @param subject [String, nil] Optional NATS subject override
178
+ # @return [Models::PublishResult] Result object
223
179
  #
224
- # @param skip_cache [Boolean] Force fresh health check, bypass connection cache (rate limited)
225
- # @return [Hash] Health status including NATS connection, stream, and version
226
- # @raise [HealthCheckFailedError] If skip_cache requested too frequently
227
- def health_check(skip_cache: false)
228
- # Rate limit uncached requests to prevent abuse (max 1 per 5 seconds)
229
- enforce_health_check_rate_limit! if skip_cache
230
-
231
- start_time = Time.now
232
- conn_instance = Connection.instance
233
-
234
- # Active check: calls @jts.account_info internally
235
- # Pass skip_cache to force fresh check if requested
236
- connected = conn_instance.connected?(skip_cache: skip_cache)
237
- connected_at = conn_instance.connected_at
238
- connection_state = conn_instance.state
239
- last_error = conn_instance.last_reconnect_error
240
- last_error_at = conn_instance.last_reconnect_error_at
241
-
242
- # Active check: queries actual stream from NATS server
243
- stream_info = connected ? fetch_stream_info : { exists: false, name: config.stream_name }
244
-
245
- # Active check: measure NATS round-trip time
246
- rtt_ms = measure_nats_rtt if connected
247
-
248
- health_check_duration_ms = ((Time.now - start_time) * 1000).round(2)
180
+ # @raise [ArgumentError] If required envelope fields are missing
181
+ #
182
+ # @example Publishing a complete envelope
183
+ # envelope = {
184
+ # 'event_id' => SecureRandom.uuid,
185
+ # 'schema_version' => 1,
186
+ # 'event_type' => 'user.created',
187
+ # 'producer' => 'custom-producer',
188
+ # 'resource_type' => 'user',
189
+ # 'resource_id' => '123',
190
+ # 'occurred_at' => Time.now.utc.iso8601,
191
+ # 'trace_id' => 'trace-123',
192
+ # 'payload' => { id: 123, name: 'Alice' }
193
+ # }
194
+ #
195
+ # result = JetstreamBridge.publish_envelope(envelope)
196
+ # puts "Published: #{result.event_id}"
197
+ #
198
+ # @example Forwarding events from another system
199
+ # # Receive event from external system
200
+ # external_event = external_api.get_event
201
+ #
202
+ # # Publish as-is, preserving all metadata
203
+ # JetstreamBridge.publish_envelope(external_event)
204
+ def publish_envelope(envelope, subject: nil)
205
+ facade.publish_envelope(envelope, subject: subject)
206
+ end
249
207
 
250
- {
251
- healthy: connected && stream_info&.fetch(:exists, false),
252
- connection: {
253
- state: connection_state,
254
- connected: connected,
255
- connected_at: connected_at&.iso8601,
256
- last_error: last_error&.message,
257
- last_error_at: last_error_at&.iso8601
258
- },
259
- stream: stream_info,
260
- performance: {
261
- nats_rtt_ms: rtt_ms,
262
- health_check_duration_ms: health_check_duration_ms
263
- },
264
- config: {
265
- env: config.env,
266
- app_name: config.app_name,
267
- destination_app: config.destination_app,
268
- use_outbox: config.use_outbox,
269
- use_inbox: config.use_inbox,
270
- use_dlq: config.use_dlq
271
- },
272
- version: JetstreamBridge::VERSION
273
- }
274
- rescue StandardError => e
275
- {
276
- healthy: false,
277
- connection: {
278
- state: :failed,
279
- connected: false
280
- },
281
- error: "#{e.class}: #{e.message}"
282
- }
208
+ # Subscribe to events
209
+ #
210
+ # @param handler [Proc, #call, nil] Message handler (optional if block given)
211
+ # @param durable_name [String, nil] Optional durable consumer name override
212
+ # @param batch_size [Integer, nil] Optional batch size override
213
+ # @yield [event] Yields Models::Event object to block
214
+ # @return [Consumer] Consumer instance (call run! to start processing)
215
+ #
216
+ # @example With block
217
+ # consumer = JetstreamBridge.subscribe do |event|
218
+ # puts "Received: #{event.type} - #{event.payload.to_h}"
219
+ # end
220
+ # consumer.run! # Blocking
221
+ #
222
+ # @example With handler
223
+ # handler = ->(event) { EventProcessor.process(event) }
224
+ # consumer = JetstreamBridge.subscribe(handler)
225
+ # consumer.run!
226
+ def subscribe(handler = nil, durable_name: nil, batch_size: nil, &block)
227
+ raise ArgumentError, 'Handler or block required' unless handler || block
228
+
229
+ facade.subscribe(
230
+ handler || block,
231
+ durable_name: durable_name,
232
+ batch_size: batch_size
233
+ )
283
234
  end
284
235
 
285
236
  # Check if connected to NATS
286
237
  #
238
+ # @param skip_cache [Boolean] Force fresh health check
287
239
  # @return [Boolean] true if connected and healthy
288
- def connected?
289
- Connection.instance.connected?
290
- rescue StandardError
291
- false
292
- end
293
-
294
- # Get stream information for the configured stream
295
240
  #
296
- # @return [Hash] Stream information including subjects and message count
297
- def stream_info
298
- fetch_stream_info
241
+ # @example
242
+ # if JetstreamBridge.connected?
243
+ # puts "Ready to publish"
244
+ # end
245
+ def connected?(skip_cache: false)
246
+ facade.connected?(skip_cache: skip_cache)
299
247
  end
300
248
 
301
- # Convenience method to publish events
302
- #
303
- # Automatically establishes connection on first use if not already connected.
304
- #
305
- # Supports three usage patterns:
249
+ # Get comprehensive health status (primary method)
306
250
  #
307
- # 1. Structured parameters (recommended):
308
- # JetstreamBridge.publish(resource_type: 'user', event_type: 'created', payload: { id: 1, name: 'Ada' })
251
+ # Provides complete system health including connection state, stream info,
252
+ # performance metrics, and configuration. Use this for monitoring and
253
+ # readiness probes.
309
254
  #
310
- # 2. Simplified hash (infers resource_type from event_type):
311
- # JetstreamBridge.publish(event_type: 'user.created', payload: { id: 1, name: 'Ada' })
255
+ # Rate limited to once every 5 seconds for uncached checks.
312
256
  #
313
- # 3. Complete envelope (advanced):
314
- # JetstreamBridge.publish({ event_type: 'created', resource_type: 'user', payload: {...}, event_id: '...' })
257
+ # @param skip_cache [Boolean] Force fresh health check (rate limited)
258
+ # @return [Hash] Health status including connection, stream, performance, config, and version
315
259
  #
316
- # @param event_or_hash [Hash, nil] Event hash or first positional argument
317
- # @param resource_type [String, nil] Resource type (e.g., 'user', 'order')
318
- # @param event_type [String, nil] Event type (e.g., 'created', 'updated', 'user.created')
319
- # @param payload [Hash, nil] Event payload data
320
- # @param subject [String, nil] Optional subject override
321
- # @param options [Hash] Additional options (event_id, occurred_at, trace_id)
322
- # @return [Models::PublishResult] Result object with success status and metadata
260
+ # @example Basic health check
261
+ # health = JetstreamBridge.health
262
+ # puts "Healthy: #{health[:healthy]}"
263
+ # puts "State: #{health[:connection][:state]}"
264
+ # puts "RTT: #{health[:performance][:nats_rtt_ms]}ms"
323
265
  #
324
- # @example Check result status
325
- # result = JetstreamBridge.publish(event_type: "user.created", payload: { id: 1 })
326
- # if result.success?
327
- # puts "Published event #{result.event_id}"
328
- # else
329
- # logger.error("Publish failed: #{result.error}")
266
+ # @example Kubernetes readiness probe
267
+ # def ready
268
+ # health = JetstreamBridge.health
269
+ # if health[:healthy]
270
+ # render json: health, status: :ok
271
+ # else
272
+ # render json: health, status: :service_unavailable
273
+ # end
330
274
  # end
331
- def publish(event_or_hash = nil, resource_type: nil, event_type: nil, payload: nil, subject: nil, **)
332
- connect_if_needed!
333
- publisher = Publisher.new
334
- publisher.publish(event_or_hash, resource_type: resource_type, event_type: event_type, payload: payload,
335
- subject: subject, **)
275
+ def health(skip_cache: false)
276
+ facade.health(skip_cache: skip_cache)
336
277
  end
337
278
 
338
- # Publish variant that raises on error
339
- #
340
- # @example
341
- # JetstreamBridge.publish!(event_type: "user.created", payload: { id: 1 })
342
- # # Raises PublishError if publishing fails
279
+ # Backward compatibility alias for health
343
280
  #
344
- # @param (see #publish)
345
- # @return [Models::PublishResult] Result object
346
- # @raise [PublishError] If publishing fails
347
- def publish!(...)
348
- result = publish(...)
349
- if result.failure?
350
- raise PublishError.new(result.error&.message, event_id: result.event_id,
351
- subject: result.subject)
352
- end
353
-
354
- result
281
+ # @deprecated Use {#health} instead
282
+ # @param skip_cache [Boolean] Force fresh health check
283
+ # @return [Hash] Health status
284
+ def health_check(skip_cache: false)
285
+ health(skip_cache: skip_cache)
355
286
  end
356
287
 
357
- # Batch publish multiple events efficiently
288
+ # Check if system is healthy (convenience method)
289
+ #
290
+ # Returns a simple boolean indicating overall health.
291
+ # Equivalent to `health[:healthy]` but more convenient.
292
+ #
293
+ # @return [Boolean] true if connected and stream exists
358
294
  #
359
295
  # @example
360
- # results = JetstreamBridge.publish_batch do |batch|
361
- # users.each do |user|
362
- # batch.add(event_type: "user.created", payload: { id: user.id })
363
- # end
296
+ # if JetstreamBridge.healthy?
297
+ # JetstreamBridge.publish(...)
298
+ # else
299
+ # logger.warn "JetStream not healthy, skipping publish"
364
300
  # end
365
- # puts "Success: #{results.successful_count}, Failed: #{results.failed_count}"
366
- #
367
- # @yield [BatchPublisher] Batch publisher instance
368
- # @return [BatchPublisher::BatchResult] Result with success/failure counts
369
- def publish_batch
370
- batch = BatchPublisher.new
371
- yield(batch) if block_given?
372
- batch.publish
301
+ def healthy?
302
+ facade.healthy?
373
303
  end
374
304
 
375
- # Convenience method to start consuming messages
376
- #
377
- # Automatically establishes connection on first use if not already connected.
378
- #
379
- # Supports two usage patterns:
305
+ # Get stream information
380
306
  #
381
- # 1. With a block (recommended):
382
- # consumer = JetstreamBridge.subscribe do |event|
383
- # puts "Received: #{event.type} on #{event.subject} (attempt #{event.deliveries})"
384
- # end
385
- # consumer.run!
386
- #
387
- # 2. With auto-run (returns Thread):
388
- # thread = JetstreamBridge.subscribe(run: true) do |event|
389
- # puts "Received: #{event.type}"
390
- # end
391
- # thread.join # Wait for consumer to finish
307
+ # @return [Hash] Stream information including subjects and message count
392
308
  #
393
- # 3. With a handler object:
394
- # handler = ->(event) { puts event.type }
395
- # consumer = JetstreamBridge.subscribe(handler)
396
- # consumer.run!
309
+ # @example
310
+ # info = JetstreamBridge.stream_info
311
+ # puts "Messages: #{info[:messages]}"
312
+ def stream_info
313
+ facade.stream_info
314
+ end
315
+
316
+ # Reset facade (for testing)
397
317
  #
398
- # @param handler [Proc, #call, nil] Message handler (optional if block given)
399
- # @param run [Boolean] If true, automatically runs consumer in a background thread
400
- # @param durable_name [String, nil] Optional durable consumer name override
401
- # @param batch_size [Integer, nil] Optional batch size override
402
- # @yield [event] Yields Models::Event object to block
403
- # @return [Consumer, Thread] Consumer instance or Thread if run: true
404
- def subscribe(handler = nil, run: false, durable_name: nil, batch_size: nil, &block)
405
- connect_if_needed!
406
- handler ||= block
407
- raise ArgumentError, 'Handler or block required' unless handler
318
+ # @api private
319
+ def reset!
320
+ @facade = nil
321
+ end
408
322
 
409
- consumer = Consumer.new(handler, durable_name: durable_name, batch_size: batch_size)
323
+ private
410
324
 
411
- if run
412
- thread = Thread.new { consumer.run! }
413
- thread.abort_on_exception = true
414
- thread
415
- else
416
- consumer
417
- end
325
+ def facade
326
+ @facade ||= Facade.new
418
327
  end
419
328
  end
420
329
  end