jetstream_bridge 4.1.0 → 4.3.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.
@@ -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'
@@ -34,7 +31,7 @@ require_relative 'jetstream_bridge/models/outbox_event'
34
31
  # - Graceful startup/shutdown lifecycle management
35
32
  #
36
33
  # @example Quick start
37
- # # Configure (automatically starts connection)
34
+ # # Configure
38
35
  # JetstreamBridge.configure do |config|
39
36
  # config.nats_urls = "nats://localhost:4222"
40
37
  # config.env = "development"
@@ -44,6 +41,9 @@ require_relative 'jetstream_bridge/models/outbox_event'
44
41
  # config.use_inbox = true
45
42
  # end
46
43
  #
44
+ # # Explicitly start connection (or use Rails railtie for automatic startup)
45
+ # JetstreamBridge.startup!
46
+ #
47
47
  # # Publish events
48
48
  # JetstreamBridge.publish(
49
49
  # event_type: "user.created",
@@ -66,17 +66,17 @@ require_relative 'jetstream_bridge/models/outbox_event'
66
66
  #
67
67
  module JetstreamBridge
68
68
  class << self
69
+ include Core::BridgeHelpers
70
+
69
71
  def config
70
72
  @config ||= Config.new
71
73
  end
72
74
 
73
- # Configure JetStream Bridge settings and establish connection
74
- #
75
- # This method sets configuration and immediately establishes a connection
76
- # to NATS, providing fail-fast behavior during application startup.
77
- # If NATS is unavailable, the application will fail to start.
75
+ # Configure JetStream Bridge settings
78
76
  #
79
- # Set config.lazy_connect = true to defer connection until first use.
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
80
  #
81
81
  # @example Basic configuration
82
82
  # JetstreamBridge.configure do |config|
@@ -84,38 +84,29 @@ module JetstreamBridge
84
84
  # config.app_name = "my_app"
85
85
  # config.destination_app = "worker"
86
86
  # end
87
+ # JetstreamBridge.startup! # Explicitly start connection
87
88
  #
88
89
  # @example With hash overrides
89
90
  # JetstreamBridge.configure(env: 'production', app_name: 'my_app')
90
91
  #
91
- # @example Lazy connection (defer until first use)
92
- # JetstreamBridge.configure do |config|
93
- # config.nats_urls = "nats://localhost:4222"
94
- # config.lazy_connect = true
95
- # end
96
- #
97
92
  # @param overrides [Hash] Configuration key-value pairs to set
98
93
  # @yield [Config] Configuration object for block-based configuration
99
94
  # @return [Config] The configured instance
100
- # @raise [ConnectionError] If connection to NATS fails (unless lazy_connect is true)
101
95
  def configure(overrides = {}, **extra_overrides)
102
96
  # Merge extra keyword arguments into overrides hash
103
97
  all_overrides = overrides.nil? ? extra_overrides : overrides.merge(extra_overrides)
104
98
 
105
99
  cfg = config
106
- all_overrides.each { |k, v| assign!(cfg, k, v) } unless all_overrides.empty?
100
+ all_overrides.each { |k, v| assign_config_option!(cfg, k, v) } unless all_overrides.empty?
107
101
  yield(cfg) if block_given?
108
102
 
109
- # Establish connection immediately for fail-fast behavior (unless lazy_connect is true)
110
- startup! unless cfg.lazy_connect
111
-
112
103
  cfg
113
104
  end
114
105
 
115
- # Configure with a preset and establish connection
106
+ # Configure with a preset
116
107
  #
117
- # This method applies a configuration preset and immediately establishes
118
- # a connection to NATS, providing fail-fast behavior.
108
+ # This method applies a configuration preset. Connection must be
109
+ # established separately via startup! or via Rails railtie.
119
110
  #
120
111
  # @example
121
112
  # JetstreamBridge.configure_for(:production) do |config|
@@ -123,11 +114,11 @@ module JetstreamBridge
123
114
  # config.app_name = "my_app"
124
115
  # config.destination_app = "worker"
125
116
  # end
117
+ # JetstreamBridge.startup! # Explicitly start connection
126
118
  #
127
119
  # @param preset [Symbol] Preset name (:development, :test, :production, etc.)
128
120
  # @yield [Config] Configuration object
129
121
  # @return [Config] Configured instance
130
- # @raise [ConnectionError] If connection to NATS fails
131
122
  def configure_for(preset)
132
123
  configure do |cfg|
133
124
  cfg.apply_preset(preset)
@@ -142,18 +133,37 @@ module JetstreamBridge
142
133
 
143
134
  # Initialize the JetStream Bridge connection and topology
144
135
  #
145
- # This method is called automatically by `configure`, but can be called
146
- # explicitly if needed. It's idempotent and safe to call multiple times.
136
+ # This method can be called explicitly if needed. It's idempotent and safe to call multiple times.
147
137
  #
148
138
  # @return [void]
149
139
  def startup!
150
140
  return if @connection_initialized
151
141
 
152
- Connection.connect!
142
+ connect_and_ensure_stream!
153
143
  @connection_initialized = true
154
144
  Logging.info('JetStream Bridge started successfully', tag: 'JetstreamBridge')
155
145
  end
156
146
 
147
+ # Reconnect to NATS
148
+ #
149
+ # Closes existing connection and establishes a new one. Useful for:
150
+ # - Forking web servers (Puma, Unicorn) after worker boot
151
+ # - Recovering from connection issues
152
+ # - Configuration changes that require reconnection
153
+ #
154
+ # @example In Puma configuration (config/puma.rb)
155
+ # on_worker_boot do
156
+ # JetstreamBridge.reconnect! if defined?(JetstreamBridge)
157
+ # end
158
+ #
159
+ # @return [void]
160
+ # @raise [ConnectionError] If unable to reconnect to NATS
161
+ def reconnect!
162
+ Logging.info('Reconnecting to NATS...', tag: 'JetstreamBridge')
163
+ shutdown! if @connection_initialized
164
+ startup!
165
+ end
166
+
157
167
  # Gracefully shutdown the JetStream Bridge connection
158
168
  #
159
169
  # Closes the NATS connection and cleans up resources. Should be called
@@ -189,9 +199,16 @@ module JetstreamBridge
189
199
  # Establishes a connection and ensures stream topology.
190
200
  #
191
201
  # @return [Object] JetStream context
192
- def ensure_topology!
202
+ def connect_and_ensure_stream!
193
203
  Connection.connect!
194
- Connection.jetstream
204
+ jts = Connection.jetstream
205
+ Topology.ensure!(jts)
206
+ jts
207
+ end
208
+
209
+ # Backwards-compatible alias for the previous method name
210
+ def ensure_topology!
211
+ connect_and_ensure_stream!
195
212
  end
196
213
 
197
214
  # Active health check for monitoring and readiness probes
@@ -283,6 +300,8 @@ module JetstreamBridge
283
300
 
284
301
  # Convenience method to publish events
285
302
  #
303
+ # Automatically establishes connection on first use if not already connected.
304
+ #
286
305
  # Supports three usage patterns:
287
306
  #
288
307
  # 1. Structured parameters (recommended):
@@ -310,6 +329,7 @@ module JetstreamBridge
310
329
  # logger.error("Publish failed: #{result.error}")
311
330
  # end
312
331
  def publish(event_or_hash = nil, resource_type: nil, event_type: nil, payload: nil, subject: nil, **)
332
+ connect_if_needed!
313
333
  publisher = Publisher.new
314
334
  publisher.publish(event_or_hash, resource_type: resource_type, event_type: event_type, payload: payload,
315
335
  subject: subject, **)
@@ -354,6 +374,8 @@ module JetstreamBridge
354
374
 
355
375
  # Convenience method to start consuming messages
356
376
  #
377
+ # Automatically establishes connection on first use if not already connected.
378
+ #
357
379
  # Supports two usage patterns:
358
380
  #
359
381
  # 1. With a block (recommended):
@@ -380,6 +402,7 @@ module JetstreamBridge
380
402
  # @yield [event] Yields Models::Event object to block
381
403
  # @return [Consumer, Thread] Consumer instance or Thread if run: true
382
404
  def subscribe(handler = nil, run: false, durable_name: nil, batch_size: nil, &block)
405
+ connect_if_needed!
383
406
  handler ||= block
384
407
  raise ArgumentError, 'Handler or block required' unless handler
385
408
 
@@ -393,69 +416,5 @@ module JetstreamBridge
393
416
  consumer
394
417
  end
395
418
  end
396
-
397
- private
398
-
399
- # Enforce rate limit on uncached health checks to prevent abuse
400
- # Max 1 uncached request per 5 seconds per process
401
- def enforce_health_check_rate_limit!
402
- @health_check_mutex ||= Mutex.new
403
- @health_check_mutex.synchronize do
404
- now = Time.now
405
- if @last_uncached_health_check
406
- time_since = now - @last_uncached_health_check
407
- if time_since < 5
408
- raise HealthCheckFailedError,
409
- "Health check rate limit exceeded. Please wait #{(5 - time_since).ceil} second(s)"
410
- end
411
- end
412
- @last_uncached_health_check = now
413
- end
414
- end
415
-
416
- def fetch_stream_info
417
- jts = Connection.jetstream
418
- info = jts.stream_info(config.stream_name)
419
-
420
- # Handle both object-style and hash-style access for compatibility
421
- config_data = info.config
422
- state_data = info.state
423
- subjects = config_data.respond_to?(:subjects) ? config_data.subjects : config_data[:subjects]
424
- messages = state_data.respond_to?(:messages) ? state_data.messages : state_data[:messages]
425
-
426
- {
427
- exists: true,
428
- name: config.stream_name,
429
- subjects: subjects,
430
- messages: messages
431
- }
432
- rescue StandardError => e
433
- {
434
- exists: false,
435
- name: config.stream_name,
436
- error: "#{e.class}: #{e.message}"
437
- }
438
- end
439
-
440
- def measure_nats_rtt
441
- # Measure round-trip time using NATS RTT method
442
- nc = Connection.nc
443
- start = Time.now
444
- nc.rtt
445
- ((Time.now - start) * 1000).round(2)
446
- rescue StandardError => e
447
- Logging.warn(
448
- "Failed to measure NATS RTT: #{e.class} #{e.message}",
449
- tag: 'JetstreamBridge'
450
- )
451
- nil
452
- end
453
-
454
- def assign!(cfg, key, val)
455
- setter = :"#{key}="
456
- raise ArgumentError, "Unknown configuration option: #{key}" unless cfg.respond_to?(setter)
457
-
458
- cfg.public_send(setter, val)
459
- end
460
419
  end
461
420
  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.1.0
4
+ version: 4.3.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-23 00:00:00.000000000 Z
11
+ date: 2025-11-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -105,19 +105,25 @@ dependencies:
105
105
  - !ruby/object:Gem::Version
106
106
  version: '4.0'
107
107
  description: |-
108
- Publisher/Consumer utilities for NATS JetStream with environment-scoped subjects,
109
- overlap guards, DLQ routing, retries/backoff, and optional Inbox/Outbox patterns.
110
- Includes health checks, auto-reconnection, graceful shutdown, and topology setup
111
- helpers for production-safe operation.
108
+ Production-ready publishers/consumers for NATS JetStream with environment-scoped
109
+ subjects, overlap guards, DLQ routing, retries/backoff, and optional inbox/outbox
110
+ patterns. Includes health checks, auto-reconnection, graceful shutdown, topology
111
+ setup helpers, and Rails generators.
112
112
  email:
113
113
  - mpyebattara@gmail.com
114
114
  executables: []
115
115
  extensions: []
116
- extra_rdoc_files: []
116
+ extra_rdoc_files:
117
+ - README.md
118
+ - CHANGELOG.md
119
+ - docs/GETTING_STARTED.md
117
120
  files:
118
121
  - CHANGELOG.md
119
122
  - LICENSE
120
123
  - README.md
124
+ - docs/GETTING_STARTED.md
125
+ - docs/PRODUCTION.md
126
+ - docs/TESTING.md
121
127
  - lib/generators/jetstream_bridge/health_check/health_check_generator.rb
122
128
  - lib/generators/jetstream_bridge/health_check/templates/health_controller.rb
123
129
  - lib/generators/jetstream_bridge/initializer/initializer_generator.rb
@@ -135,6 +141,8 @@ files:
135
141
  - lib/jetstream_bridge/consumer/message_processor.rb
136
142
  - lib/jetstream_bridge/consumer/middleware.rb
137
143
  - lib/jetstream_bridge/consumer/subscription_manager.rb
144
+ - lib/jetstream_bridge/core.rb
145
+ - lib/jetstream_bridge/core/bridge_helpers.rb
138
146
  - lib/jetstream_bridge/core/config.rb
139
147
  - lib/jetstream_bridge/core/config_preset.rb
140
148
  - lib/jetstream_bridge/core/connection.rb
@@ -155,9 +163,14 @@ files:
155
163
  - lib/jetstream_bridge/publisher/batch_publisher.rb
156
164
  - lib/jetstream_bridge/publisher/outbox_repository.rb
157
165
  - lib/jetstream_bridge/publisher/publisher.rb
158
- - lib/jetstream_bridge/railtie.rb
166
+ - lib/jetstream_bridge/rails.rb
167
+ - lib/jetstream_bridge/rails/integration.rb
168
+ - lib/jetstream_bridge/rails/railtie.rb
159
169
  - lib/jetstream_bridge/tasks/install.rake
160
170
  - lib/jetstream_bridge/test_helpers.rb
171
+ - lib/jetstream_bridge/test_helpers/fixtures.rb
172
+ - lib/jetstream_bridge/test_helpers/integration_helpers.rb
173
+ - lib/jetstream_bridge/test_helpers/matchers.rb
161
174
  - lib/jetstream_bridge/test_helpers/mock_nats.rb
162
175
  - lib/jetstream_bridge/topology/overlap_guard.rb
163
176
  - lib/jetstream_bridge/topology/stream.rb
@@ -1,91 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'core/model_codec_setup'
4
- require_relative 'core/logging'
5
-
6
- module JetstreamBridge
7
- # Rails integration for JetStream Bridge.
8
- #
9
- # This Railtie integrates JetStream Bridge with the Rails application lifecycle:
10
- # - Startup: Connection is established when Rails initializers run (via configure)
11
- # - Shutdown: Connection is closed when Rails shuts down (at_exit hook)
12
- # - Restart: Puma/Unicorn workers get fresh connections on fork
13
- #
14
- class Railtie < ::Rails::Railtie
15
- # Set up logger to use Rails.logger by default
16
- # Note: configure() will call startup! which establishes the connection
17
- initializer 'jetstream_bridge.logger', before: :initialize_logger do
18
- JetstreamBridge.configure do |config|
19
- config.logger ||= Rails.logger if defined?(Rails.logger)
20
- end
21
- end
22
-
23
- # Load ActiveRecord model tweaks after ActiveRecord is loaded
24
- initializer 'jetstream_bridge.active_record', after: 'active_record.initialize_database' do
25
- ActiveSupport.on_load(:active_record) do
26
- ActiveSupport::Reloader.to_prepare { JetstreamBridge::ModelCodecSetup.apply! }
27
- end
28
- end
29
-
30
- # Validate configuration and setup environment-specific behavior
31
- initializer 'jetstream_bridge.setup', after: :load_config_initializers do |app|
32
- app.config.after_initialize do
33
- # Validate configuration in development/test
34
- if Rails.env.development? || Rails.env.test?
35
- begin
36
- JetstreamBridge.config.validate! if JetstreamBridge.config.destination_app
37
- rescue JetstreamBridge::ConfigurationError => e
38
- Rails.logger.warn "[JetStream Bridge] Configuration warning: #{e.message}"
39
- end
40
- end
41
-
42
- # Auto-enable test mode in test environment if NATS_URLS not set
43
- if Rails.env.test? && ENV['NATS_URLS'].blank? && !(defined?(JetstreamBridge::TestHelpers) &&
44
- JetstreamBridge::TestHelpers.respond_to?(:test_mode?) &&
45
- JetstreamBridge::TestHelpers.test_mode?)
46
- Rails.logger.info '[JetStream Bridge] Auto-enabling test mode (NATS_URLS not set)'
47
- require_relative 'test_helpers'
48
- JetstreamBridge::TestHelpers.enable_test_mode!
49
- end
50
-
51
- # Log helpful connection info in development
52
- if Rails.env.development? && JetstreamBridge.connected?
53
- conn_state = JetstreamBridge::Connection.instance.state
54
- Rails.logger.info "[JetStream Bridge] Connection state: #{conn_state}"
55
- Rails.logger.info "[JetStream Bridge] Connected to: #{JetstreamBridge.config.nats_urls}"
56
- Rails.logger.info "[JetStream Bridge] Stream: #{JetstreamBridge.config.stream_name}"
57
- Rails.logger.info "[JetStream Bridge] Publishing to: #{JetstreamBridge.config.source_subject}"
58
- Rails.logger.info "[JetStream Bridge] Consuming from: #{JetstreamBridge.config.destination_subject}"
59
- end
60
- rescue StandardError => e
61
- # Don't fail app initialization for logging errors
62
- Rails.logger.debug "[JetStream Bridge] Setup logging skipped: #{e.message}"
63
- end
64
- end
65
-
66
- # Register shutdown hook for graceful cleanup
67
- # This runs when Rails shuts down (Ctrl+C, SIGTERM, etc.)
68
- config.after_initialize do
69
- at_exit do
70
- JetstreamBridge.shutdown!
71
- end
72
- end
73
-
74
- # Add console helper methods
75
- console do
76
- Rails.logger.info "[JetStream Bridge] Loaded v#{JetstreamBridge::VERSION}"
77
- Rails.logger.info '[JetStream Bridge] Use JetstreamBridge.health_check to check status'
78
- Rails.logger.info '[JetStream Bridge] Use JetstreamBridge.shutdown! to gracefully disconnect'
79
- end
80
-
81
- # Load rake tasks
82
- rake_tasks do
83
- load File.expand_path('tasks/install.rake', __dir__)
84
- end
85
-
86
- # Add generators
87
- generators do
88
- require 'generators/jetstream_bridge/health_check/health_check_generator'
89
- end
90
- end
91
- end