jetstream_bridge 4.1.0 → 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.
@@ -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)
@@ -154,6 +145,26 @@ module JetstreamBridge
154
145
  Logging.info('JetStream Bridge started successfully', tag: 'JetstreamBridge')
155
146
  end
156
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
+
157
168
  # Gracefully shutdown the JetStream Bridge connection
158
169
  #
159
170
  # Closes the NATS connection and cleans up resources. Should be called
@@ -189,11 +200,16 @@ module JetstreamBridge
189
200
  # Establishes a connection and ensures stream topology.
190
201
  #
191
202
  # @return [Object] JetStream context
192
- def ensure_topology!
203
+ def connect_and_ensure_stream!
193
204
  Connection.connect!
194
205
  Connection.jetstream
195
206
  end
196
207
 
208
+ # Backwards-compatible alias for the previous method name
209
+ def ensure_topology!
210
+ connect_and_ensure_stream!
211
+ end
212
+
197
213
  # Active health check for monitoring and readiness probes
198
214
  #
199
215
  # Performs actual operations to verify system health:
@@ -283,6 +299,8 @@ module JetstreamBridge
283
299
 
284
300
  # Convenience method to publish events
285
301
  #
302
+ # Automatically establishes connection on first use if not already connected.
303
+ #
286
304
  # Supports three usage patterns:
287
305
  #
288
306
  # 1. Structured parameters (recommended):
@@ -310,6 +328,7 @@ module JetstreamBridge
310
328
  # logger.error("Publish failed: #{result.error}")
311
329
  # end
312
330
  def publish(event_or_hash = nil, resource_type: nil, event_type: nil, payload: nil, subject: nil, **)
331
+ connect_if_needed!
313
332
  publisher = Publisher.new
314
333
  publisher.publish(event_or_hash, resource_type: resource_type, event_type: event_type, payload: payload,
315
334
  subject: subject, **)
@@ -354,6 +373,8 @@ module JetstreamBridge
354
373
 
355
374
  # Convenience method to start consuming messages
356
375
  #
376
+ # Automatically establishes connection on first use if not already connected.
377
+ #
357
378
  # Supports two usage patterns:
358
379
  #
359
380
  # 1. With a block (recommended):
@@ -380,6 +401,7 @@ module JetstreamBridge
380
401
  # @yield [event] Yields Models::Event object to block
381
402
  # @return [Consumer, Thread] Consumer instance or Thread if run: true
382
403
  def subscribe(handler = nil, run: false, durable_name: nil, batch_size: nil, &block)
404
+ connect_if_needed!
383
405
  handler ||= block
384
406
  raise ArgumentError, 'Handler or block required' unless handler
385
407
 
@@ -393,69 +415,5 @@ module JetstreamBridge
393
415
  consumer
394
416
  end
395
417
  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
418
  end
461
419
  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.2.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