jetstream_bridge 4.4.1 → 4.5.1

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.
@@ -58,8 +58,8 @@ module JetstreamBridge
58
58
  # Safe to call from multiple threads - uses class-level mutex for synchronization.
59
59
  #
60
60
  # @return [NATS::JetStream::JS] JetStream context
61
- def connect!
62
- @@connection_lock.synchronize { instance.connect! }
61
+ def connect!(verify_js: nil)
62
+ @@connection_lock.synchronize { instance.connect!(verify_js: verify_js) }
63
63
  end
64
64
 
65
65
  # Optional accessors if callers need raw handles
@@ -73,7 +73,8 @@ module JetstreamBridge
73
73
  end
74
74
 
75
75
  # Idempotent: returns an existing, healthy JetStream context or establishes one.
76
- def connect!
76
+ def connect!(verify_js: nil)
77
+ verify_js = config_auto_provision if verify_js.nil?
77
78
  # Check if already connected without acquiring mutex (for performance)
78
79
  return @jts if @jts && @nc&.connected?
79
80
 
@@ -81,7 +82,7 @@ module JetstreamBridge
81
82
  raise 'No NATS URLs configured' if servers.empty?
82
83
 
83
84
  @state = State::CONNECTING
84
- establish_connection_with_retry(servers)
85
+ establish_connection_with_retry(servers, verify_js: verify_js)
85
86
 
86
87
  Logging.info(
87
88
  "Connected to NATS (#{servers.size} server#{'s' unless servers.size == 1}): " \
@@ -89,9 +90,6 @@ module JetstreamBridge
89
90
  tag: 'JetstreamBridge::Connection'
90
91
  )
91
92
 
92
- # Ensure topology (streams, subjects, overlap guard, etc.)
93
- Topology.ensure!(@jts)
94
-
95
93
  @connected_at = Time.now.utc
96
94
  @state = State::CONNECTED
97
95
  @jts
@@ -124,8 +122,8 @@ module JetstreamBridge
124
122
  now = Time.now.to_i
125
123
  return @cached_health_status if !skip_cache && @last_health_check && (now - @last_health_check) < 30
126
124
 
127
- # Perform actual health check
128
- @cached_health_status = jetstream_healthy?
125
+ # Perform actual health check (management APIs optional)
126
+ @cached_health_status = jetstream_healthy?(verify_js: config_auto_provision)
129
127
  @last_health_check = now
130
128
  @cached_health_status
131
129
  end
@@ -151,7 +149,10 @@ module JetstreamBridge
151
149
 
152
150
  private
153
151
 
154
- def jetstream_healthy?
152
+ def jetstream_healthy?(verify_js:)
153
+ # Lightweight health when management APIs are disabled
154
+ return ping_only_health unless verify_js
155
+
155
156
  # Verify JetStream responds to simple API call
156
157
  @jts.account_info
157
158
  true
@@ -163,6 +164,20 @@ module JetstreamBridge
163
164
  false
164
165
  end
165
166
 
167
+ def ping_only_health
168
+ return false unless @nc&.connected?
169
+
170
+ # Flush acts as a ping/pong round-trip without hitting JetStream management subjects
171
+ @nc.flush(0.5)
172
+ true
173
+ rescue StandardError => e
174
+ Logging.warn(
175
+ "NATS connectivity check failed: #{e.class} #{e.message}",
176
+ tag: 'JetstreamBridge::Connection'
177
+ )
178
+ false
179
+ end
180
+
166
181
  def nats_servers
167
182
  servers = JetstreamBridge.config.nats_urls
168
183
  .to_s
@@ -174,14 +189,14 @@ module JetstreamBridge
174
189
  servers
175
190
  end
176
191
 
177
- def establish_connection_with_retry(servers)
192
+ def establish_connection_with_retry(servers, verify_js:)
178
193
  attempts = 0
179
194
  max_attempts = JetstreamBridge.config.connect_retry_attempts
180
195
  retry_delay = JetstreamBridge.config.connect_retry_delay
181
196
 
182
197
  begin
183
198
  attempts += 1
184
- establish_connection(servers)
199
+ establish_connection(servers, verify_js: verify_js)
185
200
  rescue ConnectionError => e
186
201
  if attempts < max_attempts
187
202
  delay = retry_delay * attempts
@@ -203,7 +218,7 @@ module JetstreamBridge
203
218
  end
204
219
  end
205
220
 
206
- def establish_connection(servers)
221
+ def establish_connection(servers, verify_js:)
207
222
  # Use mock NATS client if explicitly enabled for testing
208
223
  # This allows test helpers to inject a mock without affecting normal operation
209
224
  @nc = if defined?(JetstreamBridge::TestHelpers) &&
@@ -257,7 +272,22 @@ module JetstreamBridge
257
272
  @jts = @nc.jetstream
258
273
 
259
274
  # Verify JetStream is available
260
- verify_jetstream!
275
+ if verify_js
276
+ verify_jetstream!
277
+ if config_auto_provision
278
+ Topology.ensure!(@jts)
279
+ Logging.info(
280
+ 'Topology ensured after connection (auto_provision=true).',
281
+ tag: 'JetstreamBridge::Connection'
282
+ )
283
+ end
284
+ else
285
+ Logging.info(
286
+ 'Skipping JetStream account_info verification (auto_provision=false). ' \
287
+ 'Assuming JetStream is enabled.',
288
+ tag: 'JetstreamBridge::Connection'
289
+ )
290
+ end
261
291
 
262
292
  # Ensure JetStream responds to #nc
263
293
  return if @jts.respond_to?(:nc)
@@ -400,8 +430,15 @@ module JetstreamBridge
400
430
  nc_ref = @nc
401
431
  @jts.define_singleton_method(:nc) { nc_ref } unless @jts.respond_to?(:nc)
402
432
 
403
- # Re-ensure topology after reconnect
404
- Topology.ensure!(@jts)
433
+ # Re-ensure topology after reconnect when allowed
434
+ if config_auto_provision
435
+ Topology.ensure!(@jts)
436
+ else
437
+ Logging.info(
438
+ 'Skipping topology provisioning after reconnect (auto_provision=false).',
439
+ tag: 'JetstreamBridge::Connection'
440
+ )
441
+ end
405
442
 
406
443
  # Invalidate health check cache on successful reconnect
407
444
  @cached_health_status = nil
@@ -460,5 +497,12 @@ module JetstreamBridge
460
497
  @last_health_check = nil
461
498
  @connected_at = nil
462
499
  end
500
+
501
+ def config_auto_provision
502
+ cfg = JetstreamBridge.config
503
+ cfg.respond_to?(:auto_provision) ? cfg.auto_provision : true
504
+ rescue StandardError
505
+ true
506
+ end
463
507
  end
464
508
  end
@@ -9,12 +9,7 @@ module JetstreamBridge
9
9
  class ConnectionFactory
10
10
  # Connection options builder
11
11
  class ConnectionOptions
12
- DEFAULT_OPTS = {
13
- reconnect: true,
14
- reconnect_time_wait: 2,
15
- max_reconnect_attempts: 10,
16
- connect_timeout: 5
17
- }.freeze
12
+ DEFAULT_OPTS = JetstreamBridge::Connection::DEFAULT_CONN_OPTS
18
13
 
19
14
  attr_accessor :servers, :reconnect, :reconnect_time_wait,
20
15
  :max_reconnect_attempts, :connect_timeout,
@@ -73,7 +68,7 @@ module JetstreamBridge
73
68
 
74
69
  ConnectionOptions.new(
75
70
  servers: servers,
76
- name: "#{config.app_name}-#{config.env}"
71
+ name: config.app_name
77
72
  )
78
73
  end
79
74
 
@@ -30,7 +30,6 @@ module JetstreamBridge
30
30
  def config_debug
31
31
  cfg = JetstreamBridge.config
32
32
  {
33
- env: cfg.env,
34
33
  app_name: cfg.app_name,
35
34
  destination_app: cfg.destination_app,
36
35
  stream_name: cfg.stream_name,
@@ -5,12 +5,11 @@ module JetstreamBridge
5
5
  # Value object representing a NATS subject
6
6
  #
7
7
  # @example Creating a subject
8
- # subject = Subject.source(env: "production", app_name: "api", dest: "worker")
9
- # subject.to_s # => "production.api.sync.worker"
8
+ # subject = Subject.source(app_name: "api", dest: "worker")
9
+ # subject.to_s # => "api.sync.worker"
10
10
  #
11
11
  # @example Parsing a subject string
12
- # subject = Subject.parse("production.api.sync.worker")
13
- # subject.env # => "production"
12
+ # subject = Subject.parse("api.sync.worker")
14
13
  # subject.source_app # => "api"
15
14
  # subject.dest_app # => "worker"
16
15
  class Subject
@@ -31,16 +30,16 @@ module JetstreamBridge
31
30
  end
32
31
 
33
32
  # Factory methods
34
- def self.source(env:, app_name:, dest:)
35
- new("#{env}.#{app_name}.sync.#{dest}")
33
+ def self.source(app_name:, dest:)
34
+ new("#{app_name}.sync.#{dest}")
36
35
  end
37
36
 
38
- def self.destination(env:, source:, app_name:)
39
- new("#{env}.#{source}.sync.#{app_name}")
37
+ def self.destination(source:, app_name:)
38
+ new("#{source}.sync.#{app_name}")
40
39
  end
41
40
 
42
- def self.dlq(env:, app_name:)
43
- new("#{env}.#{app_name}.sync.dlq")
41
+ def self.dlq(app_name:)
42
+ new("#{app_name}.sync.dlq")
44
43
  end
45
44
 
46
45
  # Parse a subject string into a Subject object with metadata
@@ -51,37 +50,30 @@ module JetstreamBridge
51
50
  new(string)
52
51
  end
53
52
 
54
- # Get environment from subject (first token)
55
- #
56
- # @return [String, nil] Environment
57
- def env
58
- @tokens[0]
59
- end
60
-
61
53
  # Get source application from subject
62
54
  #
63
- # For regular subjects: {env}.{source_app}.sync.{dest}
64
- # For DLQ subjects: {env}.{app_name}.sync.dlq
55
+ # For regular subjects: {source_app}.sync.{dest}
56
+ # For DLQ subjects: {app_name}.sync.dlq
65
57
  #
66
58
  # @return [String, nil] Source application
67
59
  def source_app
68
- @tokens[1]
60
+ @tokens[0]
69
61
  end
70
62
 
71
63
  # Get destination application from subject
72
64
  #
73
65
  # @return [String, nil] Destination application
74
66
  def dest_app
75
- @tokens[3]
67
+ @tokens[2]
76
68
  end
77
69
 
78
70
  # Check if this is a DLQ subject
79
71
  #
80
- # DLQ subjects follow the pattern: {env}.{app}.sync.dlq
72
+ # DLQ subjects follow the pattern: {app}.sync.dlq
81
73
  #
82
74
  # @return [Boolean] True if this is a DLQ subject
83
75
  def dlq?
84
- @tokens.length == 4 && @tokens[2] == 'sync' && @tokens[3] == 'dlq'
76
+ @tokens.length == 3 && @tokens[1] == 'sync' && @tokens[2] == 'dlq'
85
77
  end
86
78
 
87
79
  # Check if this subject matches a pattern
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'topology/topology'
4
+ require_relative 'consumer/subscription_manager'
5
+ require_relative 'core/logging'
6
+ require_relative 'core/config'
7
+ require_relative 'core/connection'
8
+
9
+ module JetstreamBridge
10
+ # Dedicated provisioning orchestrator to keep connection concerns separate.
11
+ #
12
+ # Handles creating/updating stream topology and consumers. Can be used at
13
+ # deploy-time with admin credentials or during runtime when auto_provision
14
+ # is enabled.
15
+ class Provisioner
16
+ def initialize(config: JetstreamBridge.config)
17
+ @config = config
18
+ end
19
+
20
+ # Ensure stream (and optionally consumer) exist with desired config.
21
+ #
22
+ # @param jts [Object, nil] Existing JetStream context (optional)
23
+ # @param ensure_consumer [Boolean] Whether to create/align the consumer too
24
+ # @return [Object] JetStream context used for provisioning
25
+ def ensure!(jts: nil, ensure_consumer: true)
26
+ js = jts || Connection.connect!(verify_js: true)
27
+
28
+ ensure_stream!(js)
29
+ ensure_consumer!(js) if ensure_consumer
30
+
31
+ Logging.info(
32
+ "Provisioned stream=#{@config.stream_name} consumer=#{@config.durable_name if ensure_consumer}",
33
+ tag: 'JetstreamBridge::Provisioner'
34
+ )
35
+
36
+ js
37
+ end
38
+
39
+ # Ensure stream only.
40
+ #
41
+ # @param jts [Object, nil] Existing JetStream context (optional)
42
+ # @return [Object] JetStream context used
43
+ def ensure_stream!(jts: nil)
44
+ js = jts || Connection.connect!(verify_js: true)
45
+ Topology.ensure!(js)
46
+ Logging.info(
47
+ "Stream ensured: #{@config.stream_name}",
48
+ tag: 'JetstreamBridge::Provisioner'
49
+ )
50
+ js
51
+ end
52
+
53
+ # Ensure durable consumer only.
54
+ #
55
+ # @param jts [Object, nil] Existing JetStream context (optional)
56
+ # @return [Object] JetStream context used
57
+ def ensure_consumer!(jts: nil)
58
+ js = jts || Connection.connect!(verify_js: true)
59
+ SubscriptionManager.new(js, @config.durable_name, @config).ensure_consumer!(force: true)
60
+ Logging.info(
61
+ "Consumer ensured: #{@config.durable_name}",
62
+ tag: 'JetstreamBridge::Provisioner'
63
+ )
64
+ js
65
+ end
66
+ end
67
+ end
@@ -13,7 +13,7 @@ require_relative 'outbox_repository'
13
13
  module JetstreamBridge
14
14
  # Publishes events to NATS JetStream with reliability features.
15
15
  #
16
- # Publishes events to "{env}.{app}.sync.{dest}" subject pattern.
16
+ # Publishes events to "{app}.sync.{dest}" subject pattern.
17
17
  # Supports optional transactional outbox pattern for guaranteed delivery.
18
18
  #
19
19
  # @example Basic publishing
@@ -115,8 +115,8 @@ module JetstreamBridge
115
115
  envelope, resolved_subject = route_publish_params(params)
116
116
 
117
117
  do_publish(resolved_subject, envelope)
118
- rescue ArgumentError
119
- # Re-raise validation errors for invalid parameters
118
+ rescue ArgumentError, JetstreamBridge::ConfigurationError
119
+ # Re-raise validation/configuration errors (e.g., missing destination_app)
120
120
  raise
121
121
  rescue StandardError => e
122
122
  # Return failure result for publishing errors
@@ -191,12 +191,6 @@ module JetstreamBridge
191
191
  normalize_envelope({ 'event_type' => event_type, 'payload' => payload }, options)
192
192
  end
193
193
 
194
- def ensure_destination_app_configured!
195
- return unless JetstreamBridge.config.destination_app.to_s.empty?
196
-
197
- raise ArgumentError, 'destination_app must be configured'
198
- end
199
-
200
194
  def publish_to_nats(subject, envelope)
201
195
  headers = { 'nats-msg-id' => envelope['event_id'] }
202
196
 
@@ -347,5 +341,10 @@ module JetstreamBridge
347
341
  payload = payload.transform_keys(&:to_s) if payload.respond_to?(:transform_keys)
348
342
  (payload['id'] || payload[:id] || payload['resource_id'] || payload[:resource_id]).to_s
349
343
  end
344
+
345
+ def ensure_destination_app_configured!
346
+ # Leverage subject builder to enforce required components and surface consistent error messaging.
347
+ JetstreamBridge.config.source_subject
348
+ end
350
349
  end
351
350
  end
@@ -36,7 +36,7 @@ namespace :jetstream_bridge do
36
36
 
37
37
  if health[:config]
38
38
  puts "\nConfiguration:"
39
- puts " Environment: #{health[:config][:env]}"
39
+ puts " Stream: #{health[:config][:stream_name]}"
40
40
  puts " App Name: #{health[:config][:app_name]}"
41
41
  puts " Destination: #{health[:config][:destination_app] || 'NOT SET'}"
42
42
  puts " Outbox: #{health[:config][:use_outbox] ? 'Enabled' : 'Disabled'}"
@@ -62,7 +62,7 @@ namespace :jetstream_bridge do
62
62
  JetstreamBridge.config.validate!
63
63
  puts '✓ Configuration is valid'
64
64
  puts "\nCurrent settings:"
65
- puts " Environment: #{JetstreamBridge.config.env}"
65
+ puts " Stream: #{JetstreamBridge.config.stream_name}"
66
66
  puts " App Name: #{JetstreamBridge.config.app_name}"
67
67
  puts " Destination: #{JetstreamBridge.config.destination_app}"
68
68
  puts " Stream: #{JetstreamBridge.config.stream_name}"
@@ -80,6 +80,21 @@ namespace :jetstream_bridge do
80
80
  JetstreamBridge::DebugHelper.debug_info
81
81
  end
82
82
 
83
+ desc 'Provision stream and consumer (requires JetStream admin permissions)'
84
+ task provision: :environment do
85
+ puts '[jetstream_bridge] Provisioning JetStream stream and consumer...'
86
+
87
+ begin
88
+ provisioner = JetstreamBridge::Provisioner.new
89
+ provisioner.ensure!(ensure_consumer: true)
90
+ puts "✓ Provisioned stream=#{JetstreamBridge.config.stream_name} consumer=#{JetstreamBridge.config.durable_name}"
91
+ exit 0
92
+ rescue StandardError => e
93
+ puts "✗ Provisioning failed: #{e.message}"
94
+ exit 1
95
+ end
96
+ end
97
+
83
98
  desc 'Test connection to NATS'
84
99
  task test_connection: :environment do
85
100
  puts '[jetstream_bridge] Testing NATS connection...'
@@ -4,5 +4,5 @@
4
4
  #
5
5
  # Version constant for the gem.
6
6
  module JetstreamBridge
7
- VERSION = '4.4.1'
7
+ VERSION = '4.5.1'
8
8
  end
@@ -8,6 +8,7 @@ require_relative 'jetstream_bridge/consumer/consumer'
8
8
  require_relative 'jetstream_bridge/consumer/middleware'
9
9
  require_relative 'jetstream_bridge/models/publish_result'
10
10
  require_relative 'jetstream_bridge/models/event'
11
+ require_relative 'jetstream_bridge/provisioner'
11
12
 
12
13
  # Rails-specific entry point (lifecycle helpers + Railtie)
13
14
  require_relative 'jetstream_bridge/rails' if defined?(Rails::Railtie)
@@ -34,7 +35,6 @@ require_relative 'jetstream_bridge/models/outbox_event'
34
35
  # # Configure
35
36
  # JetstreamBridge.configure do |config|
36
37
  # config.nats_urls = "nats://localhost:4222"
37
- # config.env = "development"
38
38
  # config.app_name = "my_app"
39
39
  # config.destination_app = "other_app"
40
40
  # config.use_outbox = true
@@ -87,7 +87,7 @@ module JetstreamBridge
87
87
  # JetstreamBridge.startup! # Explicitly start connection
88
88
  #
89
89
  # @example With hash overrides
90
- # JetstreamBridge.configure(env: 'production', app_name: 'my_app')
90
+ # JetstreamBridge.configure(app_name: 'my_app')
91
91
  #
92
92
  # @param overrides [Hash] Configuration key-value pairs to set
93
93
  # @yield [Config] Configuration object for block-based configuration
@@ -139,6 +139,7 @@ module JetstreamBridge
139
139
  def startup!
140
140
  return if @connection_initialized
141
141
 
142
+ config.validate!
142
143
  connect_and_ensure_stream!
143
144
  @connection_initialized = true
144
145
  Logging.info('JetStream Bridge started successfully', tag: 'JetstreamBridge')
@@ -200,12 +201,34 @@ module JetstreamBridge
200
201
  #
201
202
  # @return [Object] JetStream context
202
203
  def connect_and_ensure_stream!
203
- Connection.connect!
204
+ config.validate!
205
+ provision = config.auto_provision
206
+ Connection.connect!(verify_js: provision)
204
207
  jts = Connection.jetstream
205
- Topology.ensure!(jts)
208
+ raise ConnectionNotEstablishedError, 'JetStream connection not available' unless jts
209
+
210
+ if provision
211
+ Provisioner.new(config: config).ensure_stream!(jts: jts)
212
+ else
213
+ Logging.info(
214
+ 'auto_provision=false: skipping stream provisioning and JetStream account_info. ' \
215
+ 'Run `bundle exec rake jetstream_bridge:provision` with admin credentials to create/update topology.',
216
+ tag: 'JetstreamBridge'
217
+ )
218
+ end
219
+
206
220
  jts
207
221
  end
208
222
 
223
+ # Provision stream/consumer using management credentials (out of band from runtime).
224
+ #
225
+ # @param ensure_consumer [Boolean] Whether to create/align the consumer along with the stream.
226
+ # @return [Object] JetStream context
227
+ def provision!(ensure_consumer: true)
228
+ config.validate!
229
+ Provisioner.new(config: config).ensure!(ensure_consumer: ensure_consumer)
230
+ end
231
+
209
232
  # Backwards-compatible alias for the previous method name
210
233
  def ensure_topology!
211
234
  connect_and_ensure_stream!
@@ -229,46 +252,20 @@ module JetstreamBridge
229
252
  enforce_health_check_rate_limit! if skip_cache
230
253
 
231
254
  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)
255
+ conn_status = connection_snapshot(Connection.instance, skip_cache: skip_cache)
256
+ stream_info = stream_status(conn_status[:connected])
257
+ rtt_ms = measure_nats_rtt if conn_status[:connected]
258
+ health_check_duration_ms = elapsed_ms(start_time)
249
259
 
250
260
  {
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
- },
261
+ healthy: health_flag(conn_status[:connected], stream_info),
262
+ connection: connection_payload(conn_status),
259
263
  stream: stream_info,
260
264
  performance: {
261
265
  nats_rtt_ms: rtt_ms,
262
266
  health_check_duration_ms: health_check_duration_ms
263
267
  },
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
- },
268
+ config: config_summary,
272
269
  version: JetstreamBridge::VERSION
273
270
  }
274
271
  rescue StandardError => e
@@ -416,5 +413,60 @@ module JetstreamBridge
416
413
  consumer
417
414
  end
418
415
  end
416
+
417
+ private
418
+
419
+ def connection_snapshot(conn_instance, skip_cache:)
420
+ {
421
+ connected: conn_instance.connected?(skip_cache: skip_cache),
422
+ connected_at: conn_instance.connected_at,
423
+ state: conn_instance.state,
424
+ last_error: conn_instance.last_reconnect_error,
425
+ last_error_at: conn_instance.last_reconnect_error_at
426
+ }
427
+ end
428
+
429
+ def stream_status(connected)
430
+ return stream_missing unless connected
431
+ return skipped_stream_info unless config.auto_provision
432
+
433
+ fetch_stream_info
434
+ end
435
+
436
+ def stream_missing
437
+ { exists: false, name: config.stream_name }
438
+ end
439
+
440
+ def health_flag(connected, stream_info)
441
+ return connected unless config.auto_provision
442
+
443
+ connected && stream_info&.fetch(:exists, false)
444
+ end
445
+
446
+ def connection_payload(status)
447
+ {
448
+ state: status[:state],
449
+ connected: status[:connected],
450
+ connected_at: status[:connected_at]&.iso8601,
451
+ last_error: status[:last_error]&.message,
452
+ last_error_at: status[:last_error_at]&.iso8601
453
+ }
454
+ end
455
+
456
+ def config_summary
457
+ {
458
+ app_name: config.app_name,
459
+ destination_app: config.destination_app,
460
+ stream_name: config.stream_name,
461
+ auto_provision: config.auto_provision,
462
+ use_outbox: config.use_outbox,
463
+ use_inbox: config.use_inbox,
464
+ use_dlq: config.use_dlq
465
+ }
466
+ end
467
+
468
+ def elapsed_ms(start_time)
469
+ ((Time.now - start_time) * 1000).round(2)
470
+ end
419
471
  end
420
472
  end
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.1
4
+ version: 4.5.1
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-16 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
@@ -160,6 +161,7 @@ files:
160
161
  - lib/jetstream_bridge/models/outbox_event.rb
161
162
  - lib/jetstream_bridge/models/publish_result.rb
162
163
  - lib/jetstream_bridge/models/subject.rb
164
+ - lib/jetstream_bridge/provisioner.rb
163
165
  - lib/jetstream_bridge/publisher/batch_publisher.rb
164
166
  - lib/jetstream_bridge/publisher/outbox_repository.rb
165
167
  - lib/jetstream_bridge/publisher/publisher.rb