jetstream_bridge 7.1.3 → 7.1.4

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 960c62e2e115e43f9e9f850adda06a2fd9337a72ad0eb09f1b0e99be53b4b0c3
4
- data.tar.gz: 011f110e84e31b9fbc69c388c34d4c1be47bba2bb44ba4c5a1f5ce8318aa7e8a
3
+ metadata.gz: c558d8852741ad943ccc0028d906690da3495114a144f14319e61ee00bf5fb1e
4
+ data.tar.gz: 86aafa9188755f32a71de67b356f99911585c166bbc93450454677330c46f1ae
5
5
  SHA512:
6
- metadata.gz: 89233b76d7cf91b0977e78ec9b08fc12ddec28361da16db0fe84ea573e36fd49491515809ecd8ae1de583869974e5a123808dee98366bf0ce0ec6acd52e64402
7
- data.tar.gz: 3e15d5f11c6407af7bd76cd11ac1978bd6fe663d9f72fb08e3f284f7138f8bc12454b3a46f0e3341d8f3c4132e4849e040a09b296708cef1ea16b63659aacf68
6
+ metadata.gz: 41f0d7e69e12f60d901e1fef56752bf26d2809e317b63ddbc45eeac1126855ea11b601347f5faed749e1ea04fa612746e84f3ac0173361d5bc22874c5eb186ee
7
+ data.tar.gz: 69e8caf822d7591a4e34a94c9bbdf000509d0261e09ac19e49ea402061735d433709e0d2e2328eee8979c6300f6bbe3f7a8055de0097f79d97ffae4d1bb5af72
data/CHANGELOG.md CHANGED
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [7.1.4] - 2026-02-13
9
+
10
+ ### Fixed
11
+
12
+ - **Documentation accuracy** - Fixed ~20 inaccuracies in `docs/` where method names, parameters, and examples had drifted from the source.
13
+ - **YARD inline docs** - Added `@param`, `@return`, `@raise` tags across all public API surfaces.
14
+
8
15
  ## [7.1.3] - 2026-02-13
9
16
 
10
17
  ### Fixed
data/docs/API.md CHANGED
@@ -34,7 +34,6 @@ JetstreamBridge.configure do |config|
34
34
  config.use_dlq = true # Dead letter queue for poison messages
35
35
 
36
36
  # Consumer settings
37
- config.durable_name = "#{app_name}-workers"
38
37
  config.max_deliver = 5 # Max delivery attempts
39
38
  config.ack_wait = "30s" # Time to wait for ACK
40
39
  config.backoff = ["1s", "5s", "15s", "30s", "60s"]
@@ -51,7 +50,7 @@ end
51
50
 
52
51
  ### `JetstreamBridge.config`
53
52
 
54
- Returns the current configuration object (read-only).
53
+ Returns the current configuration object, creating a default instance if needed.
55
54
 
56
55
  ```ruby
57
56
  stream_name = JetstreamBridge.config.stream_name
@@ -67,6 +66,8 @@ Explicitly start the connection and provision topology (if `auto_provision=true`
67
66
  JetstreamBridge.startup!
68
67
  ```
69
68
 
69
+ **Raises:** `ConfigurationError`, `ConnectionError`
70
+
70
71
  **Note:** Rails applications auto-start after initialization. Non-Rails apps should call this manually or rely on auto-connect on first publish/subscribe.
71
72
 
72
73
  ### `JetstreamBridge.shutdown!`
@@ -79,7 +80,7 @@ JetstreamBridge.shutdown!
79
80
 
80
81
  ### `JetstreamBridge.reset!`
81
82
 
82
- Reset all internal state (for testing).
83
+ Reset all internal state (for testing). Also resets consumer signal handlers.
83
84
 
84
85
  ```ruby
85
86
  JetstreamBridge.reset!
@@ -95,14 +96,12 @@ Publish an event to the destination app.
95
96
  JetstreamBridge.publish(
96
97
  event_type: "user.created", # Required
97
98
  resource_type: "user", # Required
98
- resource_id: user.id, # Optional
99
99
  payload: { id: user.id, email: user.email },
100
- headers: { correlation_id: "..." }, # Optional
101
100
  event_id: "custom-uuid" # Optional (auto-generated)
102
101
  )
103
102
  ```
104
103
 
105
- **Returns:** `JetstreamBridge::PublishResult`
104
+ **Returns:** `Models::PublishResult`
106
105
 
107
106
  **With Outbox:**
108
107
 
@@ -131,11 +130,11 @@ Publish multiple events efficiently.
131
130
  ```ruby
132
131
  results = JetstreamBridge.publish_batch do |batch|
133
132
  users.each do |user|
134
- batch.publish(event_type: "user.created", resource_type: "user", payload: user)
133
+ batch.add(event_type: "user.created", resource_type: "user", payload: user)
135
134
  end
136
135
  end
137
136
 
138
- puts "Published: #{results.success_count}, Failed: #{results.failure_count}"
137
+ puts "Published: #{results.successful_count}, Failed: #{results.failed_count}"
139
138
  ```
140
139
 
141
140
  ## Consuming
@@ -155,8 +154,8 @@ end
155
154
 
156
155
  ```ruby
157
156
  consumer = JetstreamBridge::Consumer.new(
158
- batch_size: 10, # Process up to 10 messages at once
159
- error_handler: ->(error, event) { logger.error(error) }
157
+ durable_name: "my-consumer", # Override default durable name
158
+ batch_size: 10 # Process up to 10 messages at once
160
159
  ) do |event|
161
160
  # ...
162
161
  end
@@ -184,14 +183,13 @@ The event object passed to your handler:
184
183
 
185
184
  ```ruby
186
185
  event.event_id # => "evt_123"
187
- event.event_type # => "user.created"
186
+ event.type # => "user.created"
188
187
  event.resource_type # => "user"
189
188
  event.resource_id # => "456"
190
- event.payload # => { "id" => 456, "email" => "..." }
191
- event.headers # => { "correlation_id" => "..." }
189
+ event.payload # => PayloadAccessor (supports method-style: event.payload.email)
192
190
  event.subject # => "source_app.sync.my_app"
193
191
  event.stream # => "jetstream-bridge-stream"
194
- event.seq # => 123
192
+ event.sequence # => 123
195
193
  event.deliveries # => 1
196
194
  ```
197
195
 
@@ -255,13 +253,18 @@ Get comprehensive health status.
255
253
  ```ruby
256
254
  health = JetstreamBridge.health_check
257
255
 
258
- health[:status] # => "healthy" | "unhealthy"
259
- health[:connected] # => true/false
260
- health[:stream_exists] # => true/false
261
- health[:messages] # => 123
262
- health[:consumers] # => 2
263
- health[:nats_rtt_ms] # => 1.2
264
- health[:version] # => "7.0.0"
256
+ health[:healthy] # => true/false
257
+ health[:connection][:state] # => :connected
258
+ health[:connection][:connected] # => true/false
259
+ health[:connection][:connected_at] # => "2025-01-01T00:00:00Z"
260
+ health[:stream][:exists] # => true/false
261
+ health[:stream][:name] # => "jetstream-bridge-stream"
262
+ health[:stream][:subjects] # => ["app.sync.worker"]
263
+ health[:stream][:messages] # => 123
264
+ health[:performance][:nats_rtt_ms] # => 1.2
265
+ health[:performance][:health_check_duration_ms] # => 45.2
266
+ health[:config] # => { app_name:, stream_name:, ... }
267
+ health[:version] # => "7.0.0"
265
268
  ```
266
269
 
267
270
  ### `JetstreamBridge.stream_info`
@@ -280,18 +283,6 @@ info[:last_seq] # => 1000
280
283
  info[:consumer_count] # => 2
281
284
  ```
282
285
 
283
- ### `JetstreamBridge.connection_info`
284
-
285
- Get NATS connection details.
286
-
287
- ```ruby
288
- info = JetstreamBridge.connection_info
289
-
290
- info[:connected] # => true
291
- info[:servers] # => ["nats://localhost:4222"]
292
- info[:connected_at] # => 2024-01-29 12:00:00 UTC
293
- ```
294
-
295
286
  ## Models
296
287
 
297
288
  ### `JetstreamBridge::OutboxEvent`
@@ -383,17 +374,23 @@ rescue JetstreamBridge::StreamNotFoundError => e
383
374
  end
384
375
  ```
385
376
 
386
- ### Custom Error Handler
377
+ ### Custom Error Handling (Middleware)
387
378
 
388
379
  ```ruby
389
- consumer = JetstreamBridge::Consumer.new(
390
- error_handler: lambda { |error, event|
391
- logger.error("Failed to process event #{event.event_id}: #{error.message}")
392
- Sentry.capture_exception(error, extra: { event_id: event.event_id })
393
- }
394
- ) do |event|
380
+ class SentryErrorMiddleware
381
+ def call(event)
382
+ yield
383
+ rescue StandardError => e
384
+ logger.error("Failed to process event #{event.event_id}: #{e.message}")
385
+ Sentry.capture_exception(e, extra: { event_id: event.event_id })
386
+ raise # Re-raise so the consumer can NAK/DLQ as appropriate
387
+ end
388
+ end
389
+
390
+ consumer = JetstreamBridge::Consumer.new do |event|
395
391
  # Process event
396
392
  end
393
+ consumer.use(SentryErrorMiddleware.new)
397
394
  ```
398
395
 
399
396
  ## Testing
data/docs/ARCHITECTURE.md CHANGED
@@ -68,12 +68,11 @@ Thread-safe singleton managing NATS connections:
68
68
 
69
69
  **Key Methods:**
70
70
 
71
- - `Connection.instance` - Get singleton connection
72
- - `connection.connect!` - Establish NATS connection
73
- - `connection.nats` - Access raw NATS client
74
- - `connection.jetstream` - Access JetStream context
75
- - `connection.healthy?` - Check connection health
76
- - `connection.reconnect!` - Force reconnection
71
+ - `Connection.connect!` - Thread-safe connection establishment (class-level)
72
+ - `Connection.nc` - Access raw NATS client (class-level)
73
+ - `Connection.jetstream` - Access JetStream context (class-level)
74
+ - `connection.connected?` - Check connection health (cached, 30s TTL)
75
+ - `connection.state` - Get current state (:disconnected, :connecting, :connected, :reconnecting, :failed)
77
76
 
78
77
  ### Publisher (`lib/jetstream_bridge/publisher/publisher.rb`)
79
78
 
@@ -104,9 +103,9 @@ JetstreamBridge.publish(
104
103
  "resource_type": "user",
105
104
  "resource_id": "1",
106
105
  "payload": { "id": 1, "email": "user@example.com" },
107
- "produced_at": "2024-01-01T00:00:00Z",
106
+ "occurred_at": "2024-01-01T00:00:00Z",
108
107
  "producer": "api",
109
- "schema_version": "1.0",
108
+ "schema_version": 1,
110
109
  "trace_id": "trace-uuid"
111
110
  }
112
111
  ```
@@ -836,16 +835,16 @@ end
836
835
 
837
836
  ```ruby
838
837
  class CustomErrorHandler
839
- def call(event, next_middleware)
840
- next_middleware.call(event)
838
+ def call(event)
839
+ yield
841
840
  rescue CustomRetryableError => e
842
- # Return ActionResult with custom delay
843
- JetstreamBridge::Consumer::ActionResult.new(:nak, delay: 10)
841
+ # Re-raise to let the consumer NAK with backoff
842
+ raise
844
843
  rescue CustomPermanentError => e
845
844
  # Log and move to DLQ
846
845
  logger.error("Permanent error: #{e.message}")
847
846
  publish_to_custom_dlq(event, e)
848
- JetstreamBridge::Consumer::ActionResult.new(:ack)
847
+ # Don't re-raise — consumer will ACK
849
848
  end
850
849
  end
851
850
 
@@ -858,19 +857,27 @@ consumer.use(CustomErrorHandler.new)
858
857
 
859
858
  ### Connection Singleton
860
859
 
861
- **Thread-safe initialization:**
860
+ **Thread-safe initialization (Singleton + class-level mutex):**
862
861
 
863
862
  ```ruby
864
- @@connection_lock = Mutex.new
863
+ class Connection
864
+ include Singleton
865
865
 
866
- def self.instance
867
- return @@connection if @@connection
866
+ @@connection_lock = Mutex.new
868
867
 
869
- @@connection_lock.synchronize do
870
- @@connection ||= new
871
- end
868
+ class << self
869
+ def connect!(verify_js: nil)
870
+ @@connection_lock.synchronize { instance.connect!(verify_js: verify_js) }
871
+ end
872
+
873
+ def nc
874
+ instance.__send__(:nc)
875
+ end
872
876
 
873
- @@connection
877
+ def jetstream
878
+ instance.__send__(:jetstream)
879
+ end
880
+ end
874
881
  end
875
882
  ```
876
883
 
@@ -991,28 +998,30 @@ health = JetstreamBridge.health_check(skip_cache: false)
991
998
  {
992
999
  healthy: true,
993
1000
  connection: {
994
- status: "connected",
995
- servers: ["nats://localhost:4222"],
996
- connected_at: "2024-01-01T00:00:00Z"
1001
+ state: :connected,
1002
+ connected: true,
1003
+ connected_at: "2025-01-01T00:00:00Z"
1004
+ },
1005
+ stream: {
1006
+ exists: true,
1007
+ name: "jetstream-bridge-stream",
1008
+ subjects: ["app.sync.worker"],
1009
+ messages: 1523
997
1010
  },
998
- jetstream: {
999
- streams: 1,
1000
- consumers: 2,
1001
- memory_bytes: 104857600,
1002
- storage_bytes: 1073741824
1011
+ performance: {
1012
+ nats_rtt_ms: 2.5,
1013
+ health_check_duration_ms: 45.2
1003
1014
  },
1004
1015
  config: {
1005
- stream_name: "jetstream-bridge-stream",
1006
1016
  app_name: "api",
1007
1017
  destination_app: "worker",
1018
+ stream_name: "jetstream-bridge-stream",
1019
+ auto_provision: true,
1008
1020
  use_outbox: true,
1009
1021
  use_inbox: true,
1010
1022
  use_dlq: true
1011
1023
  },
1012
- performance: {
1013
- message_processing_time_ms: 45.2,
1014
- last_health_check_ms: 12.5
1015
- }
1024
+ version: "7.0.0"
1016
1025
  }
1017
1026
  ```
1018
1027
 
@@ -1046,15 +1055,13 @@ ERROR [JetstreamBridge::Consumer] Unrecoverable error: ArgumentError
1046
1055
 
1047
1056
  ```ruby
1048
1057
  class MetricsMiddleware
1049
- def call(event, next_middleware)
1058
+ def call(event)
1050
1059
  start = Time.now
1051
- result = next_middleware.call(event)
1060
+ yield
1052
1061
  duration = Time.now - start
1053
1062
 
1054
1063
  StatsD.increment('jetstream.messages.processed')
1055
1064
  StatsD.histogram('jetstream.processing_time', duration)
1056
-
1057
- result
1058
1065
  rescue => e
1059
1066
  StatsD.increment('jetstream.messages.failed', tags: ["error:#{e.class}"])
1060
1067
  raise
data/docs/PRODUCTION.md CHANGED
@@ -117,11 +117,6 @@ Optimize consumer configuration based on your workload:
117
117
 
118
118
  ```ruby
119
119
  JetstreamBridge.configure do |config|
120
- # Adjust batch size based on message processing time
121
- # Larger = better throughput, smaller = lower latency
122
- # Default: 25
123
- config.batch_size = 50
124
-
125
120
  # Increase max_deliver for critical messages
126
121
  # Default: 5
127
122
  config.max_deliver = 10
@@ -134,6 +129,9 @@ JetstreamBridge.configure do |config|
134
129
  # Default: [1s, 5s, 15s, 30s, 60s]
135
130
  config.backoff = %w[2s 10s 30s 60s 120s]
136
131
  end
132
+
133
+ # batch_size is a Consumer.new parameter, not a config attribute:
134
+ consumer = JetstreamBridge::Consumer.new(handler, batch_size: 50)
137
135
  ```
138
136
 
139
137
  ### Consumer Best Practices
@@ -59,6 +59,10 @@ module JetstreamBridge
59
59
  TimeoutMiddleware = ConsumerMiddleware::TimeoutMiddleware
60
60
 
61
61
  class << self
62
+ # Register a consumer instance to receive OS signal notifications (INT, TERM).
63
+ #
64
+ # @param consumer [Consumer] Consumer to register
65
+ # @return [void]
62
66
  def register_consumer_for_signals(consumer)
63
67
  signal_registry_mutex.synchronize do
64
68
  signal_consumers << consumer
@@ -66,10 +70,17 @@ module JetstreamBridge
66
70
  end
67
71
  end
68
72
 
73
+ # Remove a consumer from the signal registry.
74
+ #
75
+ # @param consumer [Consumer] Consumer to unregister
76
+ # @return [void]
69
77
  def unregister_consumer_for_signals(consumer)
70
78
  signal_registry_mutex.synchronize { signal_consumers.delete(consumer) }
71
79
  end
72
80
 
81
+ # Clear all registered consumers and reset signal handler state.
82
+ #
83
+ # @return [void]
73
84
  def reset_signal_handlers!
74
85
  signal_registry_mutex.synchronize { signal_consumers.clear }
75
86
  @signal_handlers_installed = false
@@ -141,13 +152,17 @@ module JetstreamBridge
141
152
  attr_reader :batch_size
142
153
  # @return [MiddlewareChain] Middleware chain for processing
143
154
  attr_reader :middleware_chain
144
- # Expose grouped state objects for observability/testing
145
- attr_reader :processing_state, :lifecycle_state, :connection_state
155
+ # @return [ProcessingState] Processing counters and backoff state
156
+ attr_reader :processing_state
157
+ # @return [LifecycleState] Lifecycle flags and timing
158
+ attr_reader :lifecycle_state
159
+ # @return [ConnectionState] Reconnection attempts and health check timing
160
+ attr_reader :connection_state
146
161
 
147
162
  # Initialize a new Consumer instance.
148
163
  #
149
164
  # @param handler [Proc, #call, nil] Message handler that processes events.
150
- # Must respond to #call(event) or #call(event, subject, deliveries).
165
+ # Must respond to #call(event).
151
166
  # @param durable_name [String, nil] Optional durable consumer name override.
152
167
  # Defaults to config.durable_name.
153
168
  # @param batch_size [Integer, nil] Number of messages to fetch per batch.
@@ -2,10 +2,15 @@
2
2
 
3
3
  module JetstreamBridge
4
4
  class Consumer
5
- # Tracks processing counters and backoff state.
5
+ # Tracks processing counters and idle-backoff state.
6
6
  class ProcessingState
7
- attr_accessor :idle_backoff, :iterations
7
+ # @return [Float] Current idle backoff duration in seconds
8
+ attr_accessor :idle_backoff
9
+ # @return [Integer] Total number of processing loop iterations
10
+ attr_accessor :iterations
8
11
 
12
+ # @param idle_backoff [Float] Initial idle backoff duration (seconds)
13
+ # @param iterations [Integer] Starting iteration count
9
14
  def initialize(idle_backoff:, iterations: 0)
10
15
  @idle_backoff = idle_backoff
11
16
  @iterations = iterations
@@ -14,9 +19,18 @@ module JetstreamBridge
14
19
 
15
20
  # Tracks lifecycle flags and timing for the consumer.
16
21
  class LifecycleState
17
- attr_accessor :running, :shutdown_requested, :signal_received, :signal_logged
22
+ # @return [Boolean] Whether the consumer loop should keep running
23
+ attr_accessor :running
24
+ # @return [Boolean] Whether a graceful shutdown has been requested
25
+ attr_accessor :shutdown_requested
26
+ # @return [String, nil] Signal name that triggered shutdown (e.g. "INT")
27
+ attr_accessor :signal_received
28
+ # @return [Boolean] Whether the signal receipt has been logged
29
+ attr_accessor :signal_logged
30
+ # @return [Time] When the consumer started
18
31
  attr_reader :start_time
19
32
 
33
+ # @param start_time [Time] Consumer start time (defaults to now)
20
34
  def initialize(start_time: Time.now)
21
35
  @running = true
22
36
  @shutdown_requested = false
@@ -25,16 +39,27 @@ module JetstreamBridge
25
39
  @start_time = start_time
26
40
  end
27
41
 
42
+ # Request a graceful shutdown.
43
+ #
44
+ # @return [void]
28
45
  def stop!
29
46
  @shutdown_requested = true
30
47
  @running = false
31
48
  end
32
49
 
50
+ # Record an OS signal and trigger shutdown.
51
+ #
52
+ # @param sig [String] Signal name (e.g. "INT", "TERM")
53
+ # @return [void]
33
54
  def signal!(sig)
34
55
  @signal_received = sig
35
56
  stop!
36
57
  end
37
58
 
59
+ # Calculate consumer uptime in seconds.
60
+ #
61
+ # @param now [Time] Current time
62
+ # @return [Float] Uptime in seconds
38
63
  def uptime(now = Time.now)
39
64
  now - @start_time
40
65
  end
@@ -42,14 +67,21 @@ module JetstreamBridge
42
67
 
43
68
  # Tracks reconnection attempts and health check timing.
44
69
  class ConnectionState
70
+ # @return [Integer] Number of consecutive reconnection attempts
45
71
  attr_accessor :reconnect_attempts
72
+ # @return [Time] When the last health check was performed
46
73
  attr_reader :last_health_check
47
74
 
75
+ # @param now [Time] Initial health check timestamp
48
76
  def initialize(now: Time.now)
49
77
  @reconnect_attempts = 0
50
78
  @last_health_check = now
51
79
  end
52
80
 
81
+ # Record that a health check was performed.
82
+ #
83
+ # @param now [Time] Timestamp of the health check
84
+ # @return [void]
53
85
  def mark_health_check(now = Time.now)
54
86
  @last_health_check = now
55
87
  end
@@ -8,11 +8,28 @@ require_relative 'dlq_publisher'
8
8
  require_relative 'middleware'
9
9
 
10
10
  module JetstreamBridge
11
- # Immutable per-message metadata.
11
+ # Immutable per-message metadata extracted from a NATS message.
12
+ #
13
+ # @!attribute [r] event_id
14
+ # @return [String] Event identifier (from nats-msg-id header or generated UUID)
15
+ # @!attribute [r] deliveries
16
+ # @return [Integer] Number of delivery attempts
17
+ # @!attribute [r] subject
18
+ # @return [String] NATS subject the message arrived on
19
+ # @!attribute [r] seq
20
+ # @return [Integer, nil] Stream sequence number
21
+ # @!attribute [r] consumer
22
+ # @return [String, nil] Consumer name
23
+ # @!attribute [r] stream
24
+ # @return [String, nil] Stream name
12
25
  MessageContext = Struct.new(
13
26
  :event_id, :deliveries, :subject, :seq, :consumer, :stream,
14
27
  keyword_init: true
15
28
  ) do
29
+ # Build a MessageContext from a raw NATS message.
30
+ #
31
+ # @param msg [NATS::Msg] Raw NATS message
32
+ # @return [MessageContext]
16
33
  def self.build(msg)
17
34
  new(
18
35
  event_id: msg.header&.[]('nats-msg-id') || SecureRandom.uuid,
@@ -26,13 +43,21 @@ module JetstreamBridge
26
43
  end
27
44
 
28
45
  # Simple exponential backoff strategy for transient failures.
46
+ #
47
+ # Produces a bounded delay in seconds based on delivery count and error type.
48
+ # Transient errors (Timeout, IO) use a lower base for faster retries.
29
49
  class BackoffStrategy
50
+ # Error types considered transient (faster backoff)
30
51
  TRANSIENT_ERRORS = [Timeout::Error, IOError].freeze
31
52
  MAX_EXPONENT = 6
32
53
  MAX_DELAY = 60
33
54
  MIN_DELAY = 1
34
55
 
35
- # Returns a bounded delay in seconds
56
+ # Calculate delay for the next retry attempt.
57
+ #
58
+ # @param deliveries [Integer] Current delivery attempt number
59
+ # @param error [Exception] The error that triggered the retry
60
+ # @return [Integer] Delay in seconds, clamped between MIN_DELAY and MAX_DELAY
36
61
  def delay(deliveries, error)
37
62
  base = transient?(error) ? 0.5 : 2.0
38
63
  power = [deliveries - 1, MAX_EXPONENT].min
@@ -47,13 +72,35 @@ module JetstreamBridge
47
72
  end
48
73
  end
49
74
 
50
- # Orchestrates parse handler ack/nak DLQ
75
+ # Orchestrates the parse -> handler -> ack/nak -> DLQ pipeline for each message.
76
+ #
77
+ # Responsible for deserializing incoming NATS messages, running them through
78
+ # the middleware chain and handler, and deciding whether to ACK, NAK, or
79
+ # route to the dead letter queue.
51
80
  class MessageProcessor
81
+ # Error types that skip retry and go straight to DLQ
52
82
  UNRECOVERABLE_ERRORS = [ArgumentError, TypeError].freeze
83
+
84
+ # Result of processing a single message.
85
+ #
86
+ # @!attribute [r] action
87
+ # @return [Symbol] :ack or :nak
88
+ # @!attribute [r] ctx
89
+ # @return [MessageContext] Per-message metadata
90
+ # @!attribute [r] error
91
+ # @return [Exception, nil] Error if processing failed
92
+ # @!attribute [r] delay
93
+ # @return [Integer, nil] NAK delay in seconds
53
94
  ActionResult = Struct.new(:action, :ctx, :error, :delay, keyword_init: true)
54
95
 
96
+ # @return [ConsumerMiddleware::MiddlewareChain] Middleware chain
55
97
  attr_reader :middleware_chain
56
98
 
99
+ # @param jts [NATS::JetStream] JetStream context
100
+ # @param handler [#call] User-provided event handler
101
+ # @param dlq [DlqPublisher, nil] DLQ publisher (auto-created if nil)
102
+ # @param backoff [BackoffStrategy, nil] Backoff strategy (auto-created if nil)
103
+ # @param middleware_chain [ConsumerMiddleware::MiddlewareChain, nil] Middleware chain
57
104
  def initialize(jts, handler, dlq: nil, backoff: nil, middleware_chain: nil)
58
105
  @jts = jts
59
106
  @handler = handler
@@ -62,6 +109,11 @@ module JetstreamBridge
62
109
  @middleware_chain = middleware_chain || ConsumerMiddleware::MiddlewareChain.new
63
110
  end
64
111
 
112
+ # Process a single NATS message through the full pipeline.
113
+ #
114
+ # @param msg [NATS::Msg] Raw NATS message
115
+ # @param auto_ack [Boolean] Whether to automatically ACK/NAK the message
116
+ # @return [ActionResult] Result indicating the action taken
65
117
  def handle_message(msg, auto_ack: true)
66
118
  ctx = MessageContext.build(msg)
67
119
  event, early_action = parse_message(msg, ctx)
@@ -7,8 +7,14 @@ require_relative '../errors'
7
7
  require_relative 'pull_subscription_builder'
8
8
 
9
9
  module JetstreamBridge
10
- # Encapsulates durable ensure + subscribe for a pull consumer.
10
+ # Manages durable consumer provisioning and subscription lifecycle.
11
+ #
12
+ # Encapsulates the ensure-consumer + subscribe flow for both pull and push
13
+ # consumer modes, with automatic fallback between modes.
11
14
  class SubscriptionManager
15
+ # @param jts [NATS::JetStream] JetStream context
16
+ # @param durable [String] Durable consumer name
17
+ # @param cfg [Config] Configuration instance
12
18
  def initialize(jts, durable, cfg = JetstreamBridge.config)
13
19
  @jts = jts
14
20
  @durable = durable
@@ -28,7 +34,14 @@ module JetstreamBridge
28
34
  @desired_cfg
29
35
  end
30
36
 
31
- # Ensure consumer exists; skips all JS.API calls when auto_provision is false.
37
+ # Ensure the durable consumer exists on the server.
38
+ #
39
+ # Skips all JetStream API calls when +auto_provision+ is false, assuming
40
+ # the consumer was pre-provisioned externally.
41
+ #
42
+ # @return [void]
43
+ # @raise [StreamNotFoundError] If the stream does not exist
44
+ # @raise [ConsumerProvisioningError] If consumer creation fails due to permissions
32
45
  def ensure_consumer!(**_options)
33
46
  unless @cfg.auto_provision
34
47
  Logging.info("Skipping consumer validation (auto_provision=false); assuming '#{@durable}' exists.",
@@ -126,6 +139,12 @@ module JetstreamBridge
126
139
  end
127
140
 
128
141
  # Bind a subscriber to the existing durable consumer.
142
+ #
143
+ # Automatically selects pull or push mode based on configuration, with
144
+ # fallback to the opposite mode if the primary subscription fails.
145
+ #
146
+ # @return [Object] NATS subscription handle (pull or push)
147
+ # @raise [ConnectionError] If neither subscription mode succeeds
129
148
  def subscribe!
130
149
  if @cfg.push_consumer?
131
150
  subscribe_push_with_fallback
@@ -49,10 +49,10 @@ module JetstreamBridge
49
49
  PROCESSED = 'processed'
50
50
  end
51
51
 
52
- # NATS server URL(s), comma-separated for multiple servers
52
+ # Destination application name for subject routing
53
53
  # @return [String]
54
54
  attr_accessor :destination_app
55
- # NATS server URL(s)
55
+ # NATS server URL(s), comma-separated for multiple servers
56
56
  # @return [String]
57
57
  attr_accessor :nats_urls
58
58
  # JetStream stream name (required)
@@ -119,6 +119,12 @@ module JetstreamBridge
119
119
  # @return [String, nil]
120
120
  attr_accessor :push_consumer_group
121
121
 
122
+ # Initialize a new Config with sensible defaults.
123
+ #
124
+ # Reads initial values from environment variables when available
125
+ # (NATS_URLS, NATS_URL, JETSTREAM_STREAM_NAME, APP_NAME, DESTINATION_APP).
126
+ #
127
+ # @return [Config]
122
128
  def initialize
123
129
  @nats_urls = ENV['NATS_URLS'] || ENV['NATS_URL'] || 'nats://localhost:4222'
124
130
  @stream_name = ENV['JETSTREAM_STREAM_NAME'] || 'jetstream-bridge-stream'
@@ -60,22 +60,33 @@ module JetstreamBridge
60
60
  #
61
61
  # Safe to call from multiple threads - uses class-level mutex for synchronization.
62
62
  #
63
- # @return [NATS::JetStream::JS] JetStream context
63
+ # @return [NATS::JetStream] JetStream context
64
64
  def connect!(verify_js: nil)
65
65
  @@connection_lock.synchronize { instance.connect!(verify_js: verify_js) }
66
66
  end
67
67
 
68
- # Optional accessors if callers need raw handles
68
+ # Returns the raw NATS client from the singleton instance.
69
+ #
70
+ # @return [NATS::IO::Client, nil] The underlying NATS connection
69
71
  def nc
70
72
  instance.__send__(:nc)
71
73
  end
72
74
 
75
+ # Returns the JetStream context from the singleton instance.
76
+ #
77
+ # @return [NATS::JetStream, nil] JetStream context
78
+ # @raise [ConnectionNotEstablishedError] If JetStream context is unavailable
73
79
  def jetstream
74
80
  instance.__send__(:jetstream)
75
81
  end
76
82
  end
77
83
 
78
84
  # Idempotent: returns an existing, healthy JetStream context or establishes one.
85
+ #
86
+ # @param verify_js [Boolean, nil] Whether to verify JetStream availability via account_info.
87
+ # Defaults to the value of {Config#auto_provision}.
88
+ # @return [NATS::JetStream] JetStream context
89
+ # @raise [ConnectionError] If unable to connect to NATS or JetStream
79
90
  def connect!(verify_js: nil)
80
91
  verify_js = config_auto_provision if verify_js.nil?
81
92
  # Check if already connected without acquiring mutex (for performance)
@@ -9,17 +9,20 @@ module JetstreamBridge
9
9
  # seconds, >=1000 as milliseconds. Strings with unit suffixes are supported
10
10
  # (e.g., "30s", "500ms", "1h").
11
11
  #
12
- # Examples:
12
+ # @example Integer with auto-detection
13
13
  # Duration.to_millis(2) #=> 2000 (auto: seconds)
14
14
  # Duration.to_millis(1500) #=> 1500 (auto: milliseconds)
15
+ #
16
+ # @example Explicit units
15
17
  # Duration.to_millis(30, default_unit: :s) #=> 30000
18
+ # Duration.to_millis(1_500_000_000, default_unit: :ns) #=> 1500
19
+ #
20
+ # @example String with suffix
16
21
  # Duration.to_millis("30s") #=> 30000
17
22
  # Duration.to_millis("500ms") #=> 500
18
- # Duration.to_millis("250us") #=> 0
19
23
  # Duration.to_millis("1h") #=> 3_600_000
20
- # Duration.to_millis(1_500_000_000, default_unit: :ns) #=> 1500
21
24
  #
22
- # Also:
25
+ # @example Normalizing a list
23
26
  # Duration.normalize_list_to_millis(%w[1s 5s 15s]) #=> [1000, 5000, 15000]
24
27
  module Duration
25
28
  # multipliers to convert 1 unit into milliseconds
@@ -39,10 +42,14 @@ module JetstreamBridge
39
42
 
40
43
  module_function
41
44
 
42
- # default_unit:
43
- # :auto (default: heuristic - <1000 => seconds, >=1000 => milliseconds)
44
- # :ms (explicit milliseconds)
45
- # :ns, :us, :s, :m, :h, :d (explicit units)
45
+ # Convert a duration value to milliseconds.
46
+ #
47
+ # @param val [Integer, Float, String] Duration value to convert
48
+ # @param default_unit [Symbol] Unit for bare numbers.
49
+ # :auto (heuristic: <1000 => seconds, >=1000 => ms),
50
+ # :ms, :ns, :us, :s, :m, :h, :d
51
+ # @return [Integer] Duration in milliseconds
52
+ # @raise [ArgumentError] If the value cannot be parsed
46
53
  def to_millis(val, default_unit: :auto)
47
54
  case val
48
55
  when Integer then int_to_ms(val, default_unit: default_unit)
@@ -57,6 +64,10 @@ module JetstreamBridge
57
64
  end
58
65
 
59
66
  # Normalize an array of durations into integer milliseconds.
67
+ #
68
+ # @param values [Array<Integer, Float, String>] Duration values
69
+ # @param default_unit [Symbol] Default unit for bare numbers
70
+ # @return [Array<Integer>] Durations in milliseconds
60
71
  def normalize_list_to_millis(values, default_unit: :auto)
61
72
  vals = Array(values)
62
73
  return [] if vals.empty?
@@ -69,6 +80,10 @@ module JetstreamBridge
69
80
  # Retains the nanosecond heuristic used in SubscriptionManager:
70
81
  # extremely large integers (>= 1_000_000_000) are treated as nanoseconds
71
82
  # when default_unit is :auto.
83
+ #
84
+ # @param val [Integer, Float, String, nil] Duration value (returns nil if nil)
85
+ # @param default_unit [Symbol] Default unit for bare numbers
86
+ # @return [Integer, nil] Duration in seconds (minimum 1), or nil if val is nil
72
87
  def to_seconds(val, default_unit: :auto)
73
88
  return nil if val.nil?
74
89
 
@@ -82,6 +97,10 @@ module JetstreamBridge
82
97
  end
83
98
 
84
99
  # Normalize an array of durations into integer seconds.
100
+ #
101
+ # @param values [Array<Integer, Float, String>] Duration values
102
+ # @param default_unit [Symbol] Default unit for bare numbers
103
+ # @return [Array<Integer>] Durations in seconds
85
104
  def normalize_list_to_seconds(values, default_unit: :auto)
86
105
  vals = Array(values)
87
106
  return [] if vals.empty?
@@ -9,33 +9,59 @@ module JetstreamBridge
9
9
  module Logging
10
10
  module_function
11
11
 
12
+ # Returns the active logger instance.
13
+ #
14
+ # Resolution order: configured logger, Rails.logger, STDOUT fallback.
15
+ #
16
+ # @return [Logger] Active logger
12
17
  def logger
13
18
  JetstreamBridge.config.logger ||
14
19
  (defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger) ||
15
20
  default_logger
16
21
  end
17
22
 
23
+ # Returns a default STDOUT logger, memoized for reuse.
24
+ #
25
+ # @return [Logger]
18
26
  def default_logger
19
27
  @default_logger ||= Logger.new($stdout)
20
28
  end
21
29
 
30
+ # Log a message at the given level with an optional tag prefix.
31
+ #
32
+ # @param level [Symbol] Log level (:debug, :info, :warn, :error)
33
+ # @param msg [String] Message to log
34
+ # @param tag [String, nil] Optional prefix (e.g. "JetstreamBridge::Consumer")
35
+ # @return [void]
22
36
  def log(level, msg, tag: nil)
23
37
  message = tag ? "[#{tag}] #{msg}" : msg
24
38
  logger.public_send(level, message)
25
39
  end
26
40
 
41
+ # @param msg [String] Message to log
42
+ # @param tag [String, nil] Optional prefix
43
+ # @return [void]
27
44
  def debug(msg, tag: nil)
28
45
  log(:debug, msg, tag: tag)
29
46
  end
30
47
 
48
+ # @param msg [String] Message to log
49
+ # @param tag [String, nil] Optional prefix
50
+ # @return [void]
31
51
  def info(msg, tag: nil)
32
52
  log(:info, msg, tag: tag)
33
53
  end
34
54
 
55
+ # @param msg [String] Message to log
56
+ # @param tag [String, nil] Optional prefix
57
+ # @return [void]
35
58
  def warn(msg, tag: nil)
36
59
  log(:warn, msg, tag: tag)
37
60
  end
38
61
 
62
+ # @param msg [String] Message to log
63
+ # @param tag [String, nil] Optional prefix
64
+ # @return [void]
39
65
  def error(msg, tag: nil)
40
66
  log(:error, msg, tag: tag)
41
67
  end
@@ -1,30 +1,50 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JetstreamBridge
4
- # Base error for all JetStream Bridge errors with context support
4
+ # Base error for all JetStream Bridge errors.
5
+ #
6
+ # Every error carries an optional +context+ hash for structured diagnostics.
5
7
  class Error < StandardError
8
+ # @return [Hash] Structured context for diagnostics
6
9
  attr_reader :context
7
10
 
11
+ # @param message [String, nil] Human-readable error message
12
+ # @param context [Hash] Arbitrary diagnostic context (frozen on assignment)
8
13
  def initialize(message = nil, context: {})
9
14
  super(message)
10
15
  @context = context.freeze
11
16
  end
12
17
  end
13
18
 
14
- # Configuration errors
19
+ # Raised when configuration values are invalid or inconsistent.
15
20
  class ConfigurationError < Error; end
21
+
22
+ # Raised when a NATS subject contains invalid characters.
16
23
  class InvalidSubjectError < ConfigurationError; end
24
+
25
+ # Raised when a required configuration value is missing.
17
26
  class MissingConfigurationError < ConfigurationError; end
18
27
 
19
- # Connection errors
28
+ # Raised when a NATS connection cannot be established or is lost.
20
29
  class ConnectionError < Error; end
30
+
31
+ # Raised when an operation requires a connection that has not been established.
21
32
  class ConnectionNotEstablishedError < ConnectionError; end
33
+
34
+ # Raised when a health check fails or is rate-limited.
22
35
  class HealthCheckFailedError < ConnectionError; end
23
36
 
24
- # Publisher errors with enriched context
37
+ # Raised when an event fails to publish.
25
38
  class PublishError < Error
26
- attr_reader :event_id, :subject
27
-
39
+ # @return [String, nil] Event ID that failed to publish
40
+ attr_reader :event_id
41
+ # @return [String, nil] NATS subject the publish was attempted on
42
+ attr_reader :subject
43
+
44
+ # @param message [String, nil] Human-readable error message
45
+ # @param event_id [String, nil] The event ID
46
+ # @param subject [String, nil] The NATS subject
47
+ # @param context [Hash] Additional diagnostic context
28
48
  def initialize(message = nil, event_id: nil, subject: nil, context: {})
29
49
  @event_id = event_id
30
50
  @subject = subject
@@ -32,12 +52,23 @@ module JetstreamBridge
32
52
  end
33
53
  end
34
54
 
55
+ # Raised when a publish operation fails after retries.
35
56
  class PublishFailedError < PublishError; end
57
+
58
+ # Raised when the outbox persistence layer encounters an error.
36
59
  class OutboxError < PublishError; end
37
60
 
61
+ # Raised when a batch publish has one or more failures.
38
62
  class BatchPublishError < PublishError
39
- attr_reader :failed_events, :successful_count
40
-
63
+ # @return [Array<Hash>] Details of each failed event
64
+ attr_reader :failed_events
65
+ # @return [Integer] Number of events that published successfully
66
+ attr_reader :successful_count
67
+
68
+ # @param message [String, nil] Human-readable error message
69
+ # @param failed_events [Array<Hash>] Failed event details
70
+ # @param successful_count [Integer] Count of successful publishes
71
+ # @param context [Hash] Additional diagnostic context
41
72
  def initialize(message = nil, failed_events: [], successful_count: 0, context: {})
42
73
  @failed_events = failed_events
43
74
  @successful_count = successful_count
@@ -51,10 +82,17 @@ module JetstreamBridge
51
82
  end
52
83
  end
53
84
 
54
- # Consumer errors with delivery context
85
+ # Raised when message consumption encounters an error.
55
86
  class ConsumerError < Error
56
- attr_reader :event_id, :deliveries
57
-
87
+ # @return [String, nil] Event ID being processed
88
+ attr_reader :event_id
89
+ # @return [Integer, nil] Number of delivery attempts so far
90
+ attr_reader :deliveries
91
+
92
+ # @param message [String, nil] Human-readable error message
93
+ # @param event_id [String, nil] The event ID being consumed
94
+ # @param deliveries [Integer, nil] Delivery attempt count
95
+ # @param context [Hash] Additional diagnostic context
58
96
  def initialize(message = nil, event_id: nil, deliveries: nil, context: {})
59
97
  @event_id = event_id
60
98
  @deliveries = deliveries
@@ -62,24 +100,44 @@ module JetstreamBridge
62
100
  end
63
101
  end
64
102
 
103
+ # Raised when a user-provided handler raises during event processing.
65
104
  class HandlerError < ConsumerError; end
105
+
106
+ # Raised when the inbox deduplication layer encounters an error.
66
107
  class InboxError < ConsumerError; end
67
108
 
68
- # Topology errors
109
+ # Raised when stream or consumer topology operations fail.
69
110
  class TopologyError < Error; end
111
+
112
+ # Raised when a JetStream consumer cannot be provisioned.
70
113
  class ConsumerProvisioningError < TopologyError; end
114
+
115
+ # Raised when the configured stream does not exist.
71
116
  class StreamNotFoundError < TopologyError; end
117
+
118
+ # Raised when subject filter patterns overlap between consumers.
72
119
  class SubjectOverlapError < TopologyError; end
120
+
121
+ # Raised when stream creation fails.
73
122
  class StreamCreationFailedError < TopologyError; end
74
123
 
75
- # DLQ errors
124
+ # Raised when a DLQ operation fails.
76
125
  class DlqError < Error; end
126
+
127
+ # Raised when publishing a message to the dead letter queue fails.
77
128
  class DlqPublishFailedError < DlqError; end
78
129
 
79
- # Retry errors
130
+ # Raised when all retry attempts have been exhausted.
80
131
  class RetryExhausted < Error
81
- attr_reader :attempts, :original_error
82
-
132
+ # @return [Integer] Total number of attempts made
133
+ attr_reader :attempts
134
+ # @return [Exception, nil] The last error that triggered the retry
135
+ attr_reader :original_error
136
+
137
+ # @param message [String, nil] Human-readable error message (auto-generated if nil)
138
+ # @param attempts [Integer] Number of attempts made
139
+ # @param original_error [Exception, nil] The underlying error
140
+ # @param context [Hash] Additional diagnostic context
83
141
  def initialize(message = nil, attempts: 0, original_error: nil, context: {})
84
142
  @attempts = attempts
85
143
  @original_error = original_error
@@ -15,7 +15,23 @@ module JetstreamBridge
15
15
  # puts event.metadata.trace_id # "abc123"
16
16
  # end
17
17
  class Event
18
- # Metadata associated with message delivery
18
+ # Metadata associated with message delivery.
19
+ #
20
+ # Contains NATS-level delivery information such as the subject,
21
+ # delivery count, stream name, and sequence number.
22
+ #
23
+ # @!attribute [r] subject
24
+ # @return [String] NATS subject the message was received on
25
+ # @!attribute [r] deliveries
26
+ # @return [Integer] Number of delivery attempts
27
+ # @!attribute [r] stream
28
+ # @return [String, nil] Stream name
29
+ # @!attribute [r] sequence
30
+ # @return [Integer, nil] Message sequence number in the stream
31
+ # @!attribute [r] consumer
32
+ # @return [String, nil] Consumer name
33
+ # @!attribute [r] timestamp
34
+ # @return [Time] When the metadata was captured
19
35
  Metadata = Struct.new(
20
36
  :subject,
21
37
  :deliveries,
@@ -37,8 +53,15 @@ module JetstreamBridge
37
53
  end
38
54
  end
39
55
 
40
- # Payload accessor with method-style access
56
+ # Wraps a Hash payload to allow method-style access to its keys.
57
+ #
58
+ # @example
59
+ # accessor = PayloadAccessor.new("user_id" => 42)
60
+ # accessor.user_id #=> 42
61
+ # accessor["user_id"] #=> 42
62
+ # accessor.to_h #=> {"user_id" => 42}
41
63
  class PayloadAccessor
64
+ # @param payload [Hash] Raw payload hash
42
65
  def initialize(payload)
43
66
  @payload = payload.is_a?(Hash) ? payload.transform_keys(&:to_s) : {}
44
67
  end
@@ -68,6 +91,15 @@ module JetstreamBridge
68
91
  alias to_hash to_h
69
92
  end
70
93
 
94
+ # @return [String] Unique event identifier
95
+ # @return [String] Event type (e.g. "user.created")
96
+ # @return [String] Resource type (e.g. "user")
97
+ # @return [String] Resource identifier
98
+ # @return [String] Name of the producing application
99
+ # @return [Time, nil] When the event occurred
100
+ # @return [String] Distributed trace identifier
101
+ # @return [Integer] Envelope schema version
102
+ # @return [Metadata] Message delivery metadata
71
103
  attr_reader :event_id, :type, :resource_type, :resource_id,
72
104
  :producer, :occurred_at, :trace_id, :schema_version,
73
105
  :metadata
@@ -5,13 +5,37 @@ require 'time'
5
5
 
6
6
  module JetstreamBridge
7
7
  module Models
8
- # Value object representing an event envelope
8
+ # Immutable value object representing an event envelope.
9
+ #
10
+ # Encapsulates all fields of a JetStream Bridge event and freezes itself
11
+ # (including the payload) after construction for thread-safety.
9
12
  class EventEnvelope
13
+ # Current envelope schema version
10
14
  SCHEMA_VERSION = 1
11
15
 
16
+ # @return [String] Unique event identifier (UUID)
17
+ # @return [Integer] Envelope schema version
18
+ # @return [String] Event type (e.g. "created", "updated")
19
+ # @return [String] Name of the producing application
20
+ # @return [String] Resource type (e.g. "user", "order")
21
+ # @return [String] Resource identifier extracted from payload
22
+ # @return [Time] When the event occurred
23
+ # @return [String] Distributed trace identifier
24
+ # @return [Hash] Frozen event payload
12
25
  attr_reader :event_id, :schema_version, :event_type, :producer,
13
26
  :resource_type, :resource_id, :occurred_at, :trace_id, :payload
14
27
 
28
+ # Build a new EventEnvelope.
29
+ #
30
+ # @param resource_type [String] Resource type (e.g. "user")
31
+ # @param event_type [String] Event type (e.g. "created")
32
+ # @param payload [Hash] Event payload data
33
+ # @param event_id [String, nil] Custom event ID (auto-generated UUID if nil)
34
+ # @param occurred_at [Time, String, nil] Event timestamp (defaults to now)
35
+ # @param trace_id [String, nil] Distributed trace ID (auto-generated if nil)
36
+ # @param producer [String, nil] Producer app name (defaults to config.app_name)
37
+ # @param resource_id [String, nil] Resource ID (extracted from payload if nil)
38
+ # @raise [ArgumentError] If required fields are blank
15
39
  def initialize(
16
40
  resource_type:,
17
41
  event_type:,
@@ -36,7 +60,9 @@ module JetstreamBridge
36
60
  freeze
37
61
  end
38
62
 
39
- # Convert to hash for serialization
63
+ # Convert to hash for serialization.
64
+ #
65
+ # @return [Hash] Envelope fields as a symbol-keyed hash
40
66
  def to_h
41
67
  hash = {
42
68
  event_id: @event_id,
@@ -55,7 +81,10 @@ module JetstreamBridge
55
81
  hash
56
82
  end
57
83
 
58
- # Create from hash
84
+ # Reconstruct an EventEnvelope from a hash (e.g. deserialized JSON).
85
+ #
86
+ # @param hash [Hash] String- or symbol-keyed envelope data
87
+ # @return [EventEnvelope]
59
88
  def self.from_h(hash)
60
89
  new(
61
90
  event_id: hash['event_id'] || hash[:event_id],
@@ -96,6 +96,8 @@ module JetstreamBridge
96
96
  alias to_hash to_h
97
97
  end
98
98
 
99
+ # @param publisher [Publisher, nil] Publisher instance to use for each event.
100
+ # Defaults to a new {Publisher} instance.
99
101
  def initialize(publisher = nil)
100
102
  @publisher = publisher || Publisher.new
101
103
  @events = []
@@ -156,6 +158,9 @@ module JetstreamBridge
156
158
  alias count size
157
159
  alias length size
158
160
 
161
+ # Whether the batch has no queued events.
162
+ #
163
+ # @return [Boolean]
159
164
  def empty?
160
165
  @events.empty?
161
166
  end
@@ -76,11 +76,11 @@ module JetstreamBridge
76
76
  # - occurred_at [Time, String] Event timestamp (defaults to current time)
77
77
  #
78
78
  # @return [Models::PublishResult] Result object containing:
79
- # - success [Boolean] Whether publish succeeded
79
+ # - success? [Boolean] Whether publish succeeded
80
80
  # - event_id [String] The published event ID
81
81
  # - subject [String] NATS subject used
82
82
  # - error [Exception, nil] Error if publish failed
83
- # - duplicate [Boolean] Whether NATS detected as duplicate
83
+ # - duplicate? [Boolean] Whether NATS detected as duplicate
84
84
  #
85
85
  # @raise [ArgumentError] If required parameters are missing or invalid
86
86
  #
@@ -4,5 +4,5 @@
4
4
  #
5
5
  # Version constant for the gem.
6
6
  module JetstreamBridge
7
- VERSION = '7.1.3'
7
+ VERSION = '7.1.4'
8
8
  end
@@ -68,6 +68,9 @@ module JetstreamBridge
68
68
  class << self
69
69
  include Core::BridgeHelpers
70
70
 
71
+ # Returns the current configuration, creating a default instance if needed.
72
+ #
73
+ # @return [Config] The current configuration object
71
74
  def config
72
75
  @config ||= Config.new
73
76
  end
@@ -90,6 +93,7 @@ module JetstreamBridge
90
93
  # JetstreamBridge.configure(app_name: 'my_app')
91
94
  #
92
95
  # @param overrides [Hash] Configuration key-value pairs to set
96
+ # @param extra_overrides [Hash] Additional keyword arguments merged into overrides
93
97
  # @yield [Config] Configuration object for block-based configuration
94
98
  # @return [Config] The configured instance
95
99
  def configure(overrides = {}, **extra_overrides)
@@ -126,6 +130,12 @@ module JetstreamBridge
126
130
  end
127
131
  end
128
132
 
133
+ # Reset all configuration and connection state.
134
+ #
135
+ # Clears the config singleton and marks the connection as uninitialized.
136
+ # Primarily used in tests to restore a clean slate between examples.
137
+ #
138
+ # @return [void]
129
139
  def reset!
130
140
  @config = nil
131
141
  @connection_initialized = false
@@ -137,6 +147,8 @@ module JetstreamBridge
137
147
  # This method can be called explicitly if needed. It's idempotent and safe to call multiple times.
138
148
  #
139
149
  # @return [void]
150
+ # @raise [ConfigurationError] If configuration validation fails
151
+ # @raise [ConnectionError] If unable to connect to NATS
140
152
  def startup!
141
153
  return if @connection_initialized
142
154
 
@@ -186,14 +198,23 @@ module JetstreamBridge
186
198
  end
187
199
  end
188
200
 
201
+ # Whether the transactional outbox pattern is enabled.
202
+ #
203
+ # @return [Boolean]
189
204
  def use_outbox?
190
205
  config.use_outbox
191
206
  end
192
207
 
208
+ # Whether the idempotent inbox pattern is enabled.
209
+ #
210
+ # @return [Boolean]
193
211
  def use_inbox?
194
212
  config.use_inbox
195
213
  end
196
214
 
215
+ # Whether the dead letter queue is enabled.
216
+ #
217
+ # @return [Boolean]
197
218
  def use_dlq?
198
219
  config.use_dlq
199
220
  end
@@ -311,7 +332,8 @@ module JetstreamBridge
311
332
  # @param event_type [String, nil] Event type (e.g., 'created', 'updated', 'user.created')
312
333
  # @param payload [Hash, nil] Event payload data
313
334
  # @param subject [String, nil] Optional subject override
314
- # @param options [Hash] Additional options (event_id, occurred_at, trace_id)
335
+ # @param kwargs [Hash] Additional keyword arguments forwarded to Publisher#publish
336
+ # (event_id, occurred_at, trace_id)
315
337
  # @return [Models::PublishResult] Result object with success status and metadata
316
338
  #
317
339
  # @example Check result status
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jetstream_bridge
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.1.3
4
+ version: 7.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Attara