jetstream_bridge 3.0.0 → 3.0.2

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: d47bc85ddad1450a71960670d94969ec5f6416dc98c07c8409e4d7243ec92f3e
4
- data.tar.gz: b9130b48f7129d6c61c9f2cddc61a2e292c906c282925a3a550f79c24cc7eff4
3
+ metadata.gz: 01cdd0f6836d3c3ed272e69b801c9d583240774cb1dc76a7d4792fb3047c316d
4
+ data.tar.gz: a0e5feff3f131ab762607027138816ae7805aea7b4642e6247f86616803d8f8f
5
5
  SHA512:
6
- metadata.gz: 430fa83af6f8b1d9eb22fdf8edfbd2787c4a0609c6abf6626239a05132f1732bf9ecd8f0e6ad92cab7e53ffd234d0c3df5e626f06c332680a99b84849bdf7bc0
7
- data.tar.gz: b17ccb8f7bf525aa5f21917c87932115a11ccb4058d79c81af741ee985203c97d326b3c7cd8960819c4a93f5d088a8d0b39e1f9bbef108e2619e7114e621f74a
6
+ metadata.gz: 7c54289ab32d5798edff32bbfcb8a4e0758e8d251b05bf0ecfdc258c8a9fa4da77ea7c55608dd177507170a6434307b71b27c7b4cbcf0082667c431654669dfc
7
+ data.tar.gz: c8e60865e3b3082d5c199aa75d0411415cddbeae4a74d254e51be24bc223c8a9ebf9461e0d0e1ce8be7e73269783dadc3f16b23ecadb2fb571c8923ae8ace4c3
data/README.md CHANGED
@@ -12,6 +12,24 @@
12
12
  Includes durable consumers, backpressure, retries, <strong>DLQ</strong>, optional <strong>Inbox/Outbox</strong>, and <strong>overlap-safe stream provisioning</strong>
13
13
  </p>
14
14
 
15
+ <p align="center">
16
+ <a href="https://github.com/attaradev/jetstream_bridge/actions/workflows/ci.yml">
17
+ <img src="https://github.com/attaradev/jetstream_bridge/actions/workflows/ci.yml/badge.svg" alt="CI Status"/>
18
+ </a>
19
+ <a href="https://codecov.io/gh/attaradev/jetstream_bridge">
20
+ <img src="https://codecov.io/gh/attaradev/jetstream_bridge/branch/main/graph/badge.svg" alt="Coverage Status"/>
21
+ </a>
22
+ <a href="https://rubygems.org/gems/jetstream_bridge">
23
+ <img src="https://img.shields.io/gem/v/jetstream_bridge.svg" alt="Gem Version"/>
24
+ </a>
25
+ <a href="https://rubygems.org/gems/jetstream_bridge">
26
+ <img src="https://img.shields.io/gem/dt/jetstream_bridge.svg" alt="Downloads"/>
27
+ </a>
28
+ <a href="LICENSE">
29
+ <img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"/>
30
+ </a>
31
+ </p>
32
+
15
33
  <p align="center">
16
34
  <a href="#-features">Features</a> •
17
35
  <a href="#-install">Install</a> •
@@ -35,7 +53,7 @@
35
53
  * ⚡️ **Eager-loaded models** via Railtie (production)
36
54
  * 📊 Configurable logging with sensible defaults
37
55
 
38
- ### Production-Ready Features (v2.10+)
56
+ ### Production-Ready Features
39
57
 
40
58
  * 🏥 **Health checks** - Monitor NATS connection and stream status
41
59
  * 🔄 **Auto-reconnection** - Automatic recovery from connection failures
@@ -53,7 +71,7 @@
53
71
 
54
72
  ```ruby
55
73
  # Gemfile
56
- gem "jetstream_bridge", "~> 2.10"
74
+ gem "jetstream_bridge", "~> 3.0"
57
75
  ```
58
76
 
59
77
  ```bash
@@ -335,7 +353,7 @@ health = JetstreamBridge.health_check
335
353
  # connected_at: "2025-11-22T20:00:00Z",
336
354
  # stream: { exists: true, name: "...", ... },
337
355
  # config: { env: "production", ... },
338
- # version: "2.10.0"
356
+ # version: "3.0.0"
339
357
  # }
340
358
 
341
359
  # Force-connect & ensure topology at boot or in a check
@@ -13,8 +13,8 @@ module JetstreamBridge
13
13
  end
14
14
 
15
15
  def add_route
16
- route_content = " # JetStream Bridge health check endpoint\n" \
17
- " get '/health/jetstream', to: 'jetstream_health#show'"
16
+ route_content = " # JetStream Bridge health check endpoint\n " \
17
+ "get '/health/jetstream', to: 'jetstream_health#show'"
18
18
 
19
19
  if File.exist?('config/routes.rb')
20
20
  inject_into_file 'config/routes.rb', after: /Rails\.application\.routes\.draw do\n/ do
@@ -28,11 +28,11 @@ module JetstreamBridge
28
28
  end
29
29
 
30
30
  def show_usage
31
- say "\n" + '=' * 70, :green
31
+ say "\n#{'=' * 70}", :green
32
32
  say 'Health Check Endpoint Created!', :green
33
33
  say '=' * 70, :green
34
34
  say "\nThe health check endpoint is now available at:"
35
- say " GET /health/jetstream", :cyan
35
+ say ' GET /health/jetstream', :cyan
36
36
  say "\nExample response:"
37
37
  say <<~EXAMPLE, :white
38
38
  {
@@ -54,10 +54,10 @@ module JetstreamBridge
54
54
  }
55
55
  EXAMPLE
56
56
  say "\nUse this endpoint for:"
57
- say " • Kubernetes liveness/readiness probes", :white
58
- say " • Docker health checks", :white
59
- say " • Monitoring and alerting", :white
60
- say " • Load balancer health checks", :white
57
+ say ' • Kubernetes liveness/readiness probes', :white
58
+ say ' • Docker health checks', :white
59
+ say ' • Monitoring and alerting', :white
60
+ say ' • Load balancer health checks', :white
61
61
  say '=' * 70, :green
62
62
  end
63
63
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class CreateJetstreamInboxEvents < ActiveRecord::Migration[7.0]
3
+ class CreateJetstreamInboxEvents < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
4
4
  def change
5
5
  create_table :jetstream_inbox_events do |t|
6
6
  t.string :event_id # preferred dedupe key
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class CreateJetstreamOutboxEvents < ActiveRecord::Migration[7.0]
3
+ class CreateJetstreamOutboxEvents < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
4
4
  def change
5
5
  create_table :jetstream_outbox_events do |t|
6
6
  t.string :event_id, null: false
@@ -28,7 +28,7 @@ module JetstreamBridge
28
28
  @idle_backoff = IDLE_SLEEP_SECS
29
29
  @running = true
30
30
  @shutdown_requested = false
31
- @jts = Connection.connect!
31
+ @jts = Connection.connect!
32
32
 
33
33
  ensure_destination!
34
34
 
@@ -162,22 +162,20 @@ module JetstreamBridge
162
162
  def drain_inflight_messages
163
163
  return unless @psub
164
164
 
165
- Logging.info("Draining in-flight messages...", tag: 'JetstreamBridge::Consumer')
165
+ Logging.info('Draining in-flight messages...', tag: 'JetstreamBridge::Consumer')
166
166
  # Process any pending messages with a short timeout
167
167
  5.times do
168
- begin
169
- msgs = @psub.fetch(@batch_size, timeout: 1)
170
- break if msgs.nil? || msgs.empty?
171
-
172
- msgs.each { |m| process_one(m) }
173
- rescue NATS::Timeout, NATS::IO::Timeout
174
- break
175
- rescue StandardError => e
176
- Logging.warn("Error draining messages: #{e.class} #{e.message}", tag: 'JetstreamBridge::Consumer')
177
- break
178
- end
168
+ msgs = @psub.fetch(@batch_size, timeout: 1)
169
+ break if msgs.nil? || msgs.empty?
170
+
171
+ msgs.each { |m| process_one(m) }
172
+ rescue NATS::Timeout, NATS::IO::Timeout
173
+ break
174
+ rescue StandardError => e
175
+ Logging.warn("Error draining messages: #{e.class} #{e.message}", tag: 'JetstreamBridge::Consumer')
176
+ break
179
177
  end
180
- Logging.info("Drain complete", tag: 'JetstreamBridge::Consumer')
178
+ Logging.info('Drain complete', tag: 'JetstreamBridge::Consumer')
181
179
  rescue StandardError => e
182
180
  Logging.error("Drain failed: #{e.class} #{e.message}", tag: 'JetstreamBridge::Consumer')
183
181
  end
@@ -68,12 +68,12 @@ module JetstreamBridge
68
68
  .new(deliveries, seq, @consumer, stream)
69
69
  end
70
70
 
71
- def ack(*args, **kwargs)
72
- msg.ack(*args, **kwargs) if msg.respond_to?(:ack)
71
+ def ack(*, **)
72
+ msg.ack(*, **) if msg.respond_to?(:ack)
73
73
  end
74
74
 
75
- def nak(*args, **kwargs)
76
- msg.nak(*args, **kwargs) if msg.respond_to?(:nak)
75
+ def nak(*, **)
76
+ msg.nak(*, **) if msg.respond_to?(:nak)
77
77
  end
78
78
  end
79
79
  end
@@ -79,7 +79,7 @@ module JetstreamBridge
79
79
  Oj.load(data, mode: :strict)
80
80
  rescue Oj::ParseError => e
81
81
  dlq_success = @dlq.publish(msg, ctx,
82
- reason: 'malformed_json', error_class: e.class.name, error_message: e.message)
82
+ reason: 'malformed_json', error_class: e.class.name, error_message: e.message)
83
83
  if dlq_success
84
84
  msg.ack
85
85
  Logging.warn(
@@ -106,7 +106,7 @@ module JetstreamBridge
106
106
  )
107
107
  rescue *UNRECOVERABLE_ERRORS => e
108
108
  dlq_success = @dlq.publish(msg, ctx,
109
- reason: 'unrecoverable', error_class: e.class.name, error_message: e.message)
109
+ reason: 'unrecoverable', error_class: e.class.name, error_message: e.message)
110
110
  if dlq_success
111
111
  msg.ack
112
112
  Logging.warn(
@@ -130,9 +130,9 @@ module JetstreamBridge
130
130
  if ctx.deliveries >= max_deliver
131
131
  # Only ACK if DLQ publish succeeds
132
132
  dlq_success = @dlq.publish(msg, ctx,
133
- reason: 'max_deliver_exceeded',
134
- error_class: error.class.name,
135
- error_message: error.message)
133
+ reason: 'max_deliver_exceeded',
134
+ error_class: error.class.name,
135
+ error_message: error.message)
136
136
 
137
137
  if dlq_success
138
138
  msg.ack
@@ -86,9 +86,7 @@ module JetstreamBridge
86
86
 
87
87
  # Public API for getting connection timestamp
88
88
  # @return [Time, nil] timestamp when connection was established
89
- def connected_at
90
- @connected_at
91
- end
89
+ attr_reader :connected_at
92
90
 
93
91
  private
94
92
 
@@ -90,9 +90,7 @@ module JetstreamBridge
90
90
  jts = client.jetstream
91
91
 
92
92
  # Ensure JetStream responds to #nc for compatibility
93
- unless jts.respond_to?(:nc)
94
- jts.define_singleton_method(:nc) { client }
95
- end
93
+ jts.define_singleton_method(:nc) { client } unless jts.respond_to?(:nc)
96
94
 
97
95
  jts
98
96
  end
@@ -8,22 +8,22 @@ module JetstreamBridge
8
8
  class << self
9
9
  # Print comprehensive debug information about the current setup
10
10
  def debug_info
11
- info = {
12
- config: config_debug,
13
- connection: connection_debug,
14
- stream: stream_debug,
15
- health: JetstreamBridge.health_check
16
- }
11
+ info = {
12
+ config: config_debug,
13
+ connection: connection_debug,
14
+ stream: stream_debug,
15
+ health: JetstreamBridge.health_check
16
+ }
17
17
 
18
- Logging.info("=== JetStream Bridge Debug Info ===", tag: 'JetstreamBridge::Debug')
19
- info.each do |section, data|
20
- Logging.info("#{section.to_s.upcase}:", tag: 'JetstreamBridge::Debug')
21
- log_hash(data, indent: 2)
22
- end
23
- Logging.info("=== End Debug Info ===", tag: 'JetstreamBridge::Debug')
18
+ Logging.info('=== JetStream Bridge Debug Info ===', tag: 'JetstreamBridge::Debug')
19
+ info.each do |section, data|
20
+ Logging.info("#{section.to_s.upcase}:", tag: 'JetstreamBridge::Debug')
21
+ log_hash(data, indent: 2)
22
+ end
23
+ Logging.info('=== End Debug Info ===', tag: 'JetstreamBridge::Debug')
24
24
 
25
- info
26
- end
25
+ info
26
+ end
27
27
 
28
28
  private
29
29
 
@@ -34,9 +34,21 @@ module JetstreamBridge
34
34
  app_name: cfg.app_name,
35
35
  destination_app: cfg.destination_app,
36
36
  stream_name: cfg.stream_name,
37
- source_subject: (cfg.source_subject rescue 'ERROR'),
38
- destination_subject: (cfg.destination_subject rescue 'ERROR'),
39
- dlq_subject: (cfg.dlq_subject rescue 'ERROR'),
37
+ source_subject: begin
38
+ cfg.source_subject
39
+ rescue StandardError
40
+ 'ERROR'
41
+ end,
42
+ destination_subject: begin
43
+ cfg.destination_subject
44
+ rescue StandardError
45
+ 'ERROR'
46
+ end,
47
+ dlq_subject: begin
48
+ cfg.dlq_subject
49
+ rescue StandardError
50
+ 'ERROR'
51
+ end,
40
52
  durable_name: cfg.durable_name,
41
53
  nats_urls: cfg.nats_urls,
42
54
  max_deliver: cfg.max_deliver,
@@ -37,8 +37,8 @@ module JetstreamBridge
37
37
  'd' => 86_400_000 # days to ms
38
38
  }.freeze
39
39
 
40
- NUMBER_RE = /\A\d[\d_]*\z/.freeze
41
- TOKEN_RE = /\A(\d[\d_]*(?:\.\d+)?)\s*(ns|us|µs|ms|s|m|h|d)\z/i.freeze
40
+ NUMBER_RE = /\A\d[\d_]*\z/
41
+ TOKEN_RE = /\A(\d[\d_]*(?:\.\d+)?)\s*(ns|us|µs|ms|s|m|h|d)\z/i
42
42
 
43
43
  module_function
44
44
 
@@ -72,7 +72,7 @@ module JetstreamBridge
72
72
  case default_unit
73
73
  when :auto
74
74
  # Preserve existing heuristic for compatibility but log deprecation warning
75
- if defined?(Logging) && num > 0 && num < 1_000
75
+ if defined?(Logging) && num.positive? && num < 1_000
76
76
  Logging.debug(
77
77
  "Duration :auto heuristic treating #{num} as seconds. " \
78
78
  "Consider specifying default_unit: :s or :ms for clarity.",
@@ -40,8 +40,9 @@ module JetstreamBridge
40
40
  sleep delay
41
41
  end
42
42
  end
43
- rescue => e
43
+ rescue StandardError => e
44
44
  raise unless retryable?(e)
45
+
45
46
  raise RetryExhausted, "Failed after #{attempts} attempts: #{e.message}"
46
47
  end
47
48
 
@@ -99,7 +99,7 @@ module JetstreamBridge
99
99
  # Shim: loud failure if AR isn't present but someone calls the model.
100
100
  class InboxEvent
101
101
  class << self
102
- def method_missing(method_name, *_args, &_block)
102
+ def method_missing(method_name, *_args, &)
103
103
  raise_missing_ar!('Inbox', method_name)
104
104
  end
105
105
 
@@ -109,7 +109,10 @@ module JetstreamBridge
109
109
  def deep_freeze(obj)
110
110
  case obj
111
111
  when Hash
112
- obj.each { |k, v| deep_freeze(k); deep_freeze(v) }
112
+ obj.each do |k, v|
113
+ deep_freeze(k)
114
+ deep_freeze(v)
115
+ end
113
116
  obj.freeze
114
117
  when Array
115
118
  obj.each { |item| deep_freeze(item) }
@@ -7,7 +7,7 @@ module JetstreamBridge
7
7
  WILDCARD_SINGLE = '*'
8
8
  WILDCARD_MULTI = '>'
9
9
  SEPARATOR = '.'
10
- INVALID_CHARS = /[#{Regexp.escape(WILDCARD_SINGLE + WILDCARD_MULTI + SEPARATOR)}]/.freeze
10
+ INVALID_CHARS = /[#{Regexp.escape(WILDCARD_SINGLE + WILDCARD_MULTI + SEPARATOR)}]/
11
11
 
12
12
  attr_reader :value, :tokens
13
13
 
@@ -53,7 +53,7 @@ module JetstreamBridge
53
53
  end
54
54
 
55
55
  def ==(other)
56
- other.is_a?(Subject) ? @value == other.value : @value == other.to_s
56
+ @value == (other.is_a?(Subject) ? other.value : other.to_s)
57
57
  end
58
58
 
59
59
  alias eql? ==
@@ -66,8 +66,9 @@ module JetstreamBridge
66
66
  def self.validate_component!(value, name)
67
67
  str = value.to_s
68
68
  if str.match?(INVALID_CHARS)
69
+ wildcards = "#{SEPARATOR}, #{WILDCARD_SINGLE}, #{WILDCARD_MULTI}"
69
70
  raise ArgumentError,
70
- "#{name} cannot contain NATS wildcards (#{SEPARATOR}, #{WILDCARD_SINGLE}, #{WILDCARD_MULTI}): #{value.inspect}"
71
+ "#{name} cannot contain NATS wildcards (#{wildcards}): #{value.inspect}"
71
72
  end
72
73
  raise ArgumentError, "#{name} cannot be empty" if str.strip.empty?
73
74
 
@@ -109,7 +109,7 @@ module JetstreamBridge
109
109
  # Shim: loud failure if AR isn't present but someone calls the model.
110
110
  class OutboxEvent
111
111
  class << self
112
- def method_missing(method_name, *_args, &_block)
112
+ def method_missing(method_name, *_args, &)
113
113
  raise_missing_ar!('Outbox', method_name)
114
114
  end
115
115
 
@@ -96,8 +96,8 @@ module JetstreamBridge
96
96
  # ---- /Outbox path ----
97
97
 
98
98
  # Retry using strategy pattern
99
- def with_retries
100
- @retry_strategy.execute(context: 'Publisher') { yield }
99
+ def with_retries(&)
100
+ @retry_strategy.execute(context: 'Publisher', &)
101
101
  rescue RetryStrategy::RetryExhausted => e
102
102
  log_error(false, e)
103
103
  end
@@ -23,11 +23,9 @@ module JetstreamBridge
23
23
  initializer 'jetstream_bridge.validate_config', after: :load_config_initializers do |app|
24
24
  if Rails.env.development? || Rails.env.test?
25
25
  app.config.after_initialize do
26
- begin
27
- JetstreamBridge.config.validate! if JetstreamBridge.config.destination_app
28
- rescue JetstreamBridge::ConfigurationError => e
29
- Rails.logger.warn "[JetStream Bridge] Configuration warning: #{e.message}"
30
- end
26
+ JetstreamBridge.config.validate! if JetstreamBridge.config.destination_app
27
+ rescue JetstreamBridge::ConfigurationError => e
28
+ Rails.logger.warn "[JetStream Bridge] Configuration warning: #{e.message}"
31
29
  end
32
30
  end
33
31
  end
@@ -35,7 +33,7 @@ module JetstreamBridge
35
33
  # Add console helper methods
36
34
  console do
37
35
  Rails.logger.info "[JetStream Bridge] Loaded v#{JetstreamBridge::VERSION}"
38
- Rails.logger.info "[JetStream Bridge] Use JetstreamBridge.health_check to check status"
36
+ Rails.logger.info '[JetStream Bridge] Use JetstreamBridge.health_check to check status'
39
37
  end
40
38
 
41
39
  # Load rake tasks
@@ -87,8 +87,8 @@ namespace :jetstream_bridge do
87
87
  begin
88
88
  jts = JetstreamBridge.ensure_topology!
89
89
  puts '✓ Successfully connected to NATS'
90
- puts "✓ JetStream is available"
91
- puts "✓ Stream topology ensured"
90
+ puts '✓ JetStream is available'
91
+ puts '✓ Stream topology ensured'
92
92
 
93
93
  # Check if we can get account info
94
94
  info = jts.account_info
@@ -87,7 +87,7 @@ module JetstreamBridge
87
87
  end
88
88
 
89
89
  def conflict_message(target, conflicts)
90
- msg = +"Overlapping subjects for stream #{target}:\n"
90
+ msg = "Overlapping subjects for stream #{target}:\n"
91
91
  conflicts.each do |c|
92
92
  msg << "- Conflicts with '#{c[:name]}' on:\n"
93
93
  c[:pairs].each { |(a, b)| msg << " • #{a} × #{b}\n" }
@@ -4,5 +4,5 @@
4
4
  #
5
5
  # Version constant for the gem.
6
6
  module JetstreamBridge
7
- VERSION = '3.0.0'
7
+ VERSION = '3.0.2'
8
8
  end
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: 3.0.0
4
+ version: 3.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Attara
@@ -16,7 +16,7 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '6.0'
19
+ version: 7.1.5.2
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
22
  version: '8.0'
@@ -26,7 +26,7 @@ dependencies:
26
26
  requirements:
27
27
  - - ">="
28
28
  - !ruby/object:Gem::Version
29
- version: '6.0'
29
+ version: 7.1.5.2
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
32
  version: '8.0'
@@ -36,7 +36,7 @@ dependencies:
36
36
  requirements:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
- version: '6.0'
39
+ version: 7.1.5.2
40
40
  - - "<"
41
41
  - !ruby/object:Gem::Version
42
42
  version: '8.0'
@@ -46,10 +46,24 @@ dependencies:
46
46
  requirements:
47
47
  - - ">="
48
48
  - !ruby/object:Gem::Version
49
- version: '6.0'
49
+ version: 7.1.5.2
50
50
  - - "<"
51
51
  - !ruby/object:Gem::Version
52
52
  version: '8.0'
53
+ - !ruby/object:Gem::Dependency
54
+ name: mutex_m
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ type: :runtime
61
+ prerelease: false
62
+ version_requirements: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
53
67
  - !ruby/object:Gem::Dependency
54
68
  name: nats-pure
55
69
  requirement: !ruby/object:Gem::Requirement
@@ -84,20 +98,6 @@ dependencies:
84
98
  - - "<"
85
99
  - !ruby/object:Gem::Version
86
100
  version: '4.0'
87
- - !ruby/object:Gem::Dependency
88
- name: mutex_m
89
- requirement: !ruby/object:Gem::Requirement
90
- requirements:
91
- - - ">="
92
- - !ruby/object:Gem::Version
93
- version: '0'
94
- type: :runtime
95
- prerelease: false
96
- version_requirements: !ruby/object:Gem::Requirement
97
- requirements:
98
- - - ">="
99
- - !ruby/object:Gem::Version
100
- version: '0'
101
101
  description: |-
102
102
  Publisher/Consumer utilities for NATS JetStream with environment-scoped subjects,
103
103
  overlap guards, DLQ routing, retries/backoff, and optional Inbox/Outbox patterns.