jetstream_bridge 2.9.0 → 2.10.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0fd7228628d4d5308efceec8d1bf60d9dcff0caf311ab6d459c6c9fc32fbea11
4
- data.tar.gz: 4fe44a58012328837b727807d5d07aa3f265ff8c23dc852a906d84714eaebbf8
3
+ metadata.gz: 15fe547a615edbbd3bbeafb5bf5193bfdc4f1bd9911d7497803cf5d331ced36b
4
+ data.tar.gz: 5c0b98f2cbbff19762e98abcb39fa7c5f006cbfcd951295020adbcc08da80d49
5
5
  SHA512:
6
- metadata.gz: d9343bc42829b93c1279e4bd2a22fafdd77f69a1e18330b9771f37caeb04fac83fab7687b099484e670a8027d139ee5354601485a06d83ac79b1e3bd265b3a29
7
- data.tar.gz: aab3b5ae4b601ece51560a46fca18f459f01de29b3731fd545ad08e1a00607e54b03167b484a5aa59e414be448c877d8f909c17640fa00ab2c4301618953c2fe
6
+ metadata.gz: 4135f2c9287c5ddc0716f9eb7f3bcfeab104072c0ed59bdf41ff7a3df07dd5254240147e6a176d80f3a93198240ee633ff24ed742caf6142c859d6f259110c56
7
+ data.tar.gz: d341432852e94c1983128f5d0b0094e812c867f7133c609f609fb4606a4945ee2e5129cc20a24ef166e9e36dbd73a06e6bcb3b3a6d8c19752906a26756883345
@@ -6,7 +6,7 @@ JetstreamBridge.configure do |config|
6
6
  config.nats_urls = ENV.fetch('NATS_URLS', 'nats://localhost:4222')
7
7
  config.env = ENV.fetch('NATS_ENV', Rails.env)
8
8
  config.app_name = ENV.fetch('APP_NAME', Rails.application.class.module_parent_name.underscore)
9
- config.destination_app = ENV['DESTINATION_APP'] # required for cross-app data sync
9
+ config.destination_app = ENV.fetch('DESTINATION_APP', nil) # required for cross-app data sync
10
10
 
11
11
  # Consumer Tuning
12
12
  config.max_deliver = 5
@@ -21,4 +21,7 @@ JetstreamBridge.configure do |config|
21
21
  # Models (override if you keep custom AR classes)
22
22
  config.outbox_model = 'JetstreamBridge::OutboxEvent'
23
23
  config.inbox_model = 'JetstreamBridge::InboxEvent'
24
+
25
+ # Logging
26
+ # config.logger = Rails.logger
24
27
  end
@@ -8,11 +8,13 @@ module JetstreamBridge
8
8
  class InstallGenerator < Rails::Generators::Base
9
9
  desc 'Creates JetstreamBridge initializer and migrations'
10
10
  def create_initializer
11
- Rails::Generators.invoke('jetstream_bridge:initializer', [], behavior: behavior, destination_root: destination_root)
11
+ Rails::Generators.invoke('jetstream_bridge:initializer', [], behavior: behavior,
12
+ destination_root: destination_root)
12
13
  end
13
14
 
14
15
  def create_migrations
15
- Rails::Generators.invoke('jetstream_bridge:migrations', [], behavior: behavior, destination_root: destination_root)
16
+ Rails::Generators.invoke('jetstream_bridge:migrations', [], behavior: behavior,
17
+ destination_root: destination_root)
16
18
  end
17
19
  end
18
20
  end
@@ -8,6 +8,7 @@ module JetstreamBridge
8
8
  # Migrations generator.
9
9
  class MigrationsGenerator < Rails::Generators::Base
10
10
  include Rails::Generators::Migration
11
+
11
12
  source_root File.expand_path('templates', __dir__)
12
13
  desc 'Creates Inbox/Outbox migrations for JetstreamBridge'
13
14
 
@@ -7,7 +7,6 @@ require_relative '../core/duration'
7
7
  require_relative '../core/logging'
8
8
  require_relative '../core/config'
9
9
  require_relative '../core/model_utils'
10
- require_relative 'consumer_config'
11
10
  require_relative 'message_processor'
12
11
  require_relative 'subscription_manager'
13
12
  require_relative 'inbox/inbox_processor'
@@ -20,13 +19,12 @@ module JetstreamBridge
20
19
  IDLE_SLEEP_SECS = 0.05
21
20
  MAX_IDLE_BACKOFF_SECS = 1.0
22
21
 
23
- def initialize(durable_name: JetstreamBridge.config.durable_name,
24
- batch_size: DEFAULT_BATCH_SIZE, &block)
22
+ def initialize(durable_name: nil, batch_size: nil, &block)
25
23
  raise ArgumentError, 'handler block required' unless block_given?
26
24
 
27
25
  @handler = block
28
- @batch_size = Integer(batch_size)
29
- @durable = durable_name
26
+ @batch_size = Integer(batch_size || DEFAULT_BATCH_SIZE)
27
+ @durable = durable_name || JetstreamBridge.config.durable_name
30
28
  @idle_backoff = IDLE_SLEEP_SECS
31
29
  @running = true
32
30
  @jts = Connection.connect!
@@ -103,15 +101,15 @@ module JetstreamBridge
103
101
  0
104
102
  end
105
103
 
106
- def handle_js_error(e)
107
- if recoverable_consumer_error?(e)
104
+ def handle_js_error(error)
105
+ if recoverable_consumer_error?(error)
108
106
  Logging.warn(
109
- "Recovering subscription after error: #{e.class} #{e.message}",
107
+ "Recovering subscription after error: #{error.class} #{error.message}",
110
108
  tag: 'JetstreamBridge::Consumer'
111
109
  )
112
110
  ensure_subscription!
113
111
  else
114
- Logging.error("Fetch failed (non-recoverable): #{e.class} #{e.message}", tag: 'JetstreamBridge::Consumer')
112
+ Logging.error("Fetch failed (non-recoverable): #{error.class} #{error.message}", tag: 'JetstreamBridge::Consumer')
115
113
  end
116
114
  0
117
115
  end
@@ -7,6 +7,7 @@ module JetstreamBridge
7
7
  class InboxMessage
8
8
  attr_reader :msg, :seq, :deliveries, :stream, :subject, :headers, :body, :raw, :event_id, :now
9
9
 
10
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
10
11
  def self.from_nats(msg)
11
12
  meta = (msg.respond_to?(:metadata) && msg.metadata) || nil
12
13
  seq = meta.respond_to?(:stream_sequence) ? meta.stream_sequence : nil
@@ -30,7 +31,9 @@ module JetstreamBridge
30
31
 
31
32
  new(msg, seq, deliveries, stream, subject, headers, body, raw, id, Time.now.utc, consumer)
32
33
  end
34
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
33
35
 
36
+ # rubocop:disable Metrics/ParameterLists
34
37
  def initialize(msg, seq, deliveries, stream, subject, headers, body, raw, event_id, now, consumer = nil)
35
38
  @msg = msg
36
39
  @seq = seq
@@ -44,6 +47,7 @@ module JetstreamBridge
44
47
  @now = now
45
48
  @consumer = consumer
46
49
  end
50
+ # rubocop:enable Metrics/ParameterLists
47
51
 
48
52
  def body_for_store
49
53
  body.empty? ? raw : body
@@ -59,7 +63,7 @@ module JetstreamBridge
59
63
 
60
64
  def metadata
61
65
  @metadata ||= Struct.new(:num_delivered, :sequence, :consumer, :stream)
62
- .new(deliveries, seq, @consumer, stream)
66
+ .new(deliveries, seq, @consumer, stream)
63
67
  end
64
68
 
65
69
  def ack(*args, **kwargs)
@@ -25,53 +25,31 @@ module JetstreamBridge
25
25
  end
26
26
 
27
27
  def persist_pre(record, msg)
28
- ModelUtils.assign_known_attrs(record, {
29
- event_id: (if ModelUtils.has_columns?(@klass,
30
- :event_id)
31
- msg.event_id
32
- end),
33
- subject: msg.subject,
34
- payload: ModelUtils.json_dump(msg.body_for_store),
35
- headers: ModelUtils.json_dump(msg.headers),
36
- stream: (if ModelUtils.has_columns?(@klass,
37
- :stream)
38
- msg.stream
39
- end),
40
- stream_seq: (if ModelUtils.has_columns?(@klass,
41
- :stream_seq)
42
- msg.seq
43
- end),
44
- deliveries: (if ModelUtils.has_columns?(@klass,
45
- :deliveries)
46
- msg.deliveries
47
- end),
48
- status: 'processing',
49
- last_error: nil,
50
- received_at: (if ModelUtils.has_columns?(@klass,
51
- :received_at)
52
- record.received_at || msg.now
53
- end),
54
- updated_at: (if ModelUtils.has_columns?(@klass,
55
- :updated_at)
56
- msg.now
57
- end)
58
- })
28
+ attrs = {
29
+ event_id: msg.event_id,
30
+ subject: msg.subject,
31
+ payload: ModelUtils.json_dump(msg.body_for_store),
32
+ headers: ModelUtils.json_dump(msg.headers),
33
+ stream: msg.stream,
34
+ stream_seq: msg.seq,
35
+ deliveries: msg.deliveries,
36
+ status: 'processing',
37
+ last_error: nil,
38
+ received_at: record.respond_to?(:received_at) ? (record.received_at || msg.now) : nil,
39
+ updated_at: record.respond_to?(:updated_at) ? msg.now : nil
40
+ }
41
+ ModelUtils.assign_known_attrs(record, attrs)
59
42
  record.save!
60
43
  end
61
44
 
62
45
  def persist_post(record)
63
46
  now = Time.now.utc
64
- ModelUtils.assign_known_attrs(record, {
65
- status: 'processed',
66
- processed_at: (if ModelUtils.has_columns?(@klass,
67
- :processed_at)
68
- now
69
- end),
70
- updated_at: (if ModelUtils.has_columns?(@klass,
71
- :updated_at)
72
- now
73
- end)
74
- })
47
+ attrs = {
48
+ status: 'processed',
49
+ processed_at: record.respond_to?(:processed_at) ? now : nil,
50
+ updated_at: record.respond_to?(:updated_at) ? now : nil
51
+ }
52
+ ModelUtils.assign_known_attrs(record, attrs)
75
53
  record.save!
76
54
  end
77
55
 
@@ -79,20 +57,12 @@ module JetstreamBridge
79
57
  return unless record
80
58
 
81
59
  now = Time.now.utc
82
- ModelUtils.assign_known_attrs(record, {
83
- status: (if ModelUtils.has_columns?(@klass,
84
- :status)
85
- 'failed'
86
- end),
87
- last_error: (if ModelUtils.has_columns?(@klass,
88
- :last_error)
89
- "#{error.class}: #{error.message}"
90
- end),
91
- updated_at: (if ModelUtils.has_columns?(@klass,
92
- :updated_at)
93
- now
94
- end)
95
- })
60
+ attrs = {
61
+ status: 'failed',
62
+ last_error: "#{error.class}: #{error.message}",
63
+ updated_at: record.respond_to?(:updated_at) ? now : nil
64
+ }
65
+ ModelUtils.assign_known_attrs(record, attrs)
96
66
  record.save!
97
67
  rescue StandardError => e
98
68
  Logging.warn("Failed to persist inbox failure: #{e.class}: #{e.message}",
@@ -1,12 +1,50 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'oj'
4
+ require 'securerandom'
4
5
  require_relative '../core/logging'
5
- require_relative 'message_context'
6
6
  require_relative 'dlq_publisher'
7
- require_relative 'backoff_strategy'
8
7
 
9
8
  module JetstreamBridge
9
+ # Immutable per-message metadata.
10
+ MessageContext = Struct.new(
11
+ :event_id, :deliveries, :subject, :seq, :consumer, :stream,
12
+ keyword_init: true
13
+ ) do
14
+ def self.build(msg)
15
+ new(
16
+ event_id: msg.header&.[]('nats-msg-id') || SecureRandom.uuid,
17
+ deliveries: msg.metadata&.num_delivered.to_i,
18
+ subject: msg.subject,
19
+ seq: msg.metadata&.sequence,
20
+ consumer: msg.metadata&.consumer,
21
+ stream: msg.metadata&.stream
22
+ )
23
+ end
24
+ end
25
+
26
+ # Simple exponential backoff strategy for transient failures.
27
+ class BackoffStrategy
28
+ TRANSIENT_ERRORS = [Timeout::Error, IOError].freeze
29
+ MAX_EXPONENT = 6
30
+ MAX_DELAY = 60
31
+ MIN_DELAY = 1
32
+
33
+ # Returns a bounded delay in seconds
34
+ def delay(deliveries, error)
35
+ base = transient?(error) ? 0.5 : 2.0
36
+ power = [deliveries - 1, MAX_EXPONENT].min
37
+ raw = (base * (2**power)).to_i
38
+ raw.clamp(MIN_DELAY, MAX_DELAY)
39
+ end
40
+
41
+ private
42
+
43
+ def transient?(error)
44
+ TRANSIENT_ERRORS.any? { |k| error.is_a?(k) }
45
+ end
46
+ end
47
+
10
48
  # Orchestrates parse → handler → ack/nak → DLQ
11
49
  class MessageProcessor
12
50
  UNRECOVERABLE_ERRORS = [ArgumentError, TypeError].freeze
@@ -2,7 +2,6 @@
2
2
 
3
3
  require_relative '../core/logging'
4
4
  require_relative '../core/duration'
5
- require_relative '../consumer/consumer_config'
6
5
 
7
6
  module JetstreamBridge
8
7
  # Encapsulates durable ensure + subscribe for a pull consumer.
@@ -11,7 +10,7 @@ module JetstreamBridge
11
10
  @jts = jts
12
11
  @durable = durable
13
12
  @cfg = cfg
14
- @desired_cfg = ConsumerConfig.consumer_config(@durable, filter_subject)
13
+ @desired_cfg = build_consumer_config(@durable, filter_subject)
15
14
  @desired_cfg_norm = normalize_consumer_config(@desired_cfg)
16
15
  end
17
16
 
@@ -74,6 +73,18 @@ module JetstreamBridge
74
73
  )
75
74
  end
76
75
 
76
+ def build_consumer_config(durable, filter_subject)
77
+ {
78
+ durable_name: durable,
79
+ filter_subject: filter_subject,
80
+ ack_policy: 'explicit',
81
+ deliver_policy: 'all',
82
+ max_deliver: JetstreamBridge.config.max_deliver,
83
+ ack_wait: Duration.to_millis(JetstreamBridge.config.ack_wait),
84
+ backoff: Array(JetstreamBridge.config.backoff).map { |d| Duration.to_millis(d) }
85
+ }
86
+ end
87
+
77
88
  # Normalize both server-returned config objects and our desired hash
78
89
  # into a common hash with consistent units/types for accurate comparison.
79
90
  def normalize_consumer_config(cfg)
@@ -5,7 +5,7 @@ module JetstreamBridge
5
5
  attr_accessor :destination_app, :nats_urls, :env, :app_name,
6
6
  :max_deliver, :ack_wait, :backoff,
7
7
  :use_outbox, :use_inbox, :inbox_model, :outbox_model,
8
- :use_dlq
8
+ :use_dlq, :logger
9
9
 
10
10
  def initialize
11
11
  @nats_urls = ENV['NATS_URLS'] || ENV['NATS_URL'] || 'nats://localhost:4222'
@@ -22,6 +22,7 @@ module JetstreamBridge
22
22
  @use_dlq = true
23
23
  @outbox_model = 'JetstreamBridge::OutboxEvent'
24
24
  @inbox_model = 'JetstreamBridge::InboxEvent'
25
+ @logger = nil
25
26
  end
26
27
 
27
28
  # Single stream name per env
@@ -1,20 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'uri'
4
+ require 'logger'
4
5
 
5
6
  module JetstreamBridge
6
- # Logging helpers that route to Rails.logger when available,
7
- # falling back to STDOUT.
7
+ # Logging helpers that route to the configured logger when available,
8
+ # falling back to Rails.logger or STDOUT.
8
9
  module Logging
9
10
  module_function
10
11
 
12
+ def logger
13
+ JetstreamBridge.config.logger ||
14
+ (defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger) ||
15
+ default_logger
16
+ end
17
+
18
+ def default_logger
19
+ @default_logger ||= Logger.new($stdout)
20
+ end
21
+
11
22
  def log(level, msg, tag: nil)
12
23
  message = tag ? "[#{tag}] #{msg}" : msg
13
- if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
14
- Rails.logger.public_send(level, message)
15
- else
16
- puts "[#{level.to_s.upcase}] #{message}"
17
- end
24
+ logger.public_send(level, message)
25
+ end
26
+
27
+ def debug(msg, tag: nil)
28
+ log(:debug, msg, tag: tag)
18
29
  end
19
30
 
20
31
  def info(msg, tag: nil)
@@ -29,6 +40,7 @@ module JetstreamBridge
29
40
  log(:error, msg, tag: tag)
30
41
  end
31
42
 
43
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
32
44
  def sanitize_url(url)
33
45
  uri = URI.parse(url)
34
46
  return url unless uri.user || uri.password
@@ -55,5 +67,6 @@ module JetstreamBridge
55
67
  "#{scheme}://#{masked}@"
56
68
  end
57
69
  end
70
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
58
71
  end
59
72
  end
@@ -14,10 +14,13 @@ module JetstreamBridge
14
14
  defined?(ActiveRecord::Base) && klass <= ActiveRecord::Base
15
15
  end
16
16
 
17
+ # rubocop:disable Naming/PredicatePrefix
17
18
  def has_columns?(klass, *cols)
18
19
  return false unless ar_class?(klass)
20
+
19
21
  cols.flatten.all? { |c| klass.column_names.include?(c.to_s) }
20
22
  end
23
+ # rubocop:enable Naming/PredicatePrefix
21
24
 
22
25
  def assign_known_attrs(record, attrs)
23
26
  attrs.each do |k, v|
@@ -30,9 +33,7 @@ module JetstreamBridge
30
33
  def find_or_init_by_best(klass, *keysets)
31
34
  keysets.each do |keys|
32
35
  next if keys.nil? || keys.empty?
33
- if has_columns?(klass, keys.keys)
34
- return klass.find_or_initialize_by(keys)
35
- end
36
+ return klass.find_or_initialize_by(keys) if has_columns?(klass, keys.keys)
36
37
  end
37
38
  klass.new
38
39
  end
@@ -15,6 +15,7 @@ module JetstreamBridge
15
15
 
16
16
  class << self
17
17
  # Safe column presence check that never boots a connection during class load.
18
+ # rubocop:disable Naming/PredicatePrefix
18
19
  def has_column?(name)
19
20
  return false unless ar_connected?
20
21
 
@@ -22,6 +23,7 @@ module JetstreamBridge
22
23
  rescue ActiveRecord::ConnectionNotEstablished, ActiveRecord::NoDatabaseError
23
24
  false
24
25
  end
26
+ # rubocop:enable Naming/PredicatePrefix
25
27
 
26
28
  def ar_connected?
27
29
  ActiveRecord::Base.connected? && connection_pool.active_connection?
@@ -66,9 +68,7 @@ module JetstreamBridge
66
68
  # ---- Defaults that do not require schema at load time ----
67
69
  before_validation do
68
70
  self.status ||= 'received' if self.class.has_column?(:status) && status.blank?
69
- if self.class.has_column?(:received_at) && received_at.blank?
70
- self.received_at ||= Time.now.utc
71
- end
71
+ self.received_at ||= Time.now.utc if self.class.has_column?(:received_at) && received_at.blank?
72
72
  end
73
73
 
74
74
  # ---- Helpers ----
@@ -103,7 +103,7 @@ module JetstreamBridge
103
103
  raise_missing_ar!('Inbox', method_name)
104
104
  end
105
105
 
106
- def respond_to_missing?(_m, _p = false)
106
+ def respond_to_missing?(_method_name, _include_private = false)
107
107
  false
108
108
  end
109
109
 
@@ -15,6 +15,7 @@ module JetstreamBridge
15
15
 
16
16
  class << self
17
17
  # Safe column presence check that never boots a connection during class load.
18
+ # rubocop:disable Naming/PredicatePrefix
18
19
  def has_column?(name)
19
20
  return false unless ar_connected?
20
21
 
@@ -22,6 +23,7 @@ module JetstreamBridge
22
23
  rescue ActiveRecord::ConnectionNotEstablished, ActiveRecord::NoDatabaseError
23
24
  false
24
25
  end
26
+ # rubocop:enable Naming/PredicatePrefix
25
27
 
26
28
  def ar_connected?
27
29
  # Avoid creating a connection; rescue if pool isn't set yet.
@@ -111,7 +113,7 @@ module JetstreamBridge
111
113
  raise_missing_ar!('Outbox', method_name)
112
114
  end
113
115
 
114
- def respond_to_missing?(_m, _p = false)
116
+ def respond_to_missing?(_method_name, _include_private = false)
115
117
  false
116
118
  end
117
119
 
@@ -123,9 +123,7 @@ module JetstreamBridge
123
123
 
124
124
  # Retention is immutable; warn if different and do not include on update.
125
125
  have_ret = info.config.retention.to_s.downcase
126
- if have_ret != RETENTION
127
- StreamSupport.log_retention_mismatch(name, have: have_ret, want: RETENTION)
128
- end
126
+ StreamSupport.log_retention_mismatch(name, have: have_ret, want: RETENTION) if have_ret != RETENTION
129
127
 
130
128
  # Storage can be updated; do it without passing retention.
131
129
  have_storage = info.config.storage.to_s.downcase
@@ -48,20 +48,30 @@ module JetstreamBridge
48
48
  while ai < a_parts.length && bi < b_parts.length
49
49
  at = a_parts[ai]
50
50
  bt = b_parts[bi]
51
- return true if at == '>' || bt == '>'
52
- return false unless at == bt || at == '*' || bt == '*'
51
+ return true if tail?(at, bt)
52
+ return false unless token_match?(at, bt)
53
53
 
54
54
  ai += 1
55
55
  bi += 1
56
56
  end
57
57
 
58
- # If any side still has a '>' remaining, it can absorb the other's remainder
59
- a_tail = a_parts[ai..] || []
60
- b_tail = b_parts[bi..] || []
58
+ tail_overlap?(a_parts[ai..], b_parts[bi..])
59
+ end
60
+
61
+ def tail?(a_token, b_token)
62
+ a_token == '>' || b_token == '>'
63
+ end
64
+
65
+ def token_match?(a_token, b_token)
66
+ a_token == b_token || a_token == '*' || b_token == '*'
67
+ end
68
+
69
+ def tail_overlap?(a_tail, b_tail)
70
+ a_tail ||= []
71
+ b_tail ||= []
61
72
  return true if a_tail.include?('>') || b_tail.include?('>')
62
73
 
63
- # Otherwise they overlap only if both consumed exactly
64
- ai == a_parts.length && bi == b_parts.length
74
+ a_tail.empty? && b_tail.empty?
65
75
  end
66
76
  end
67
77
  end
@@ -14,7 +14,7 @@ module JetstreamBridge
14
14
 
15
15
  Logging.info(
16
16
  "Subjects ready: producer=#{cfg.source_subject}, consumer=#{cfg.destination_subject}. " \
17
- "Counterpart publishes on #{cfg.destination_subject} and subscribes on #{cfg.source_subject}.",
17
+ "Counterpart publishes on #{cfg.destination_subject} and subscribes on #{cfg.source_subject}.",
18
18
  tag: 'JetstreamBridge::Topology'
19
19
  )
20
20
  end
@@ -4,5 +4,5 @@
4
4
  #
5
5
  # Version constant for the gem.
6
6
  module JetstreamBridge
7
- VERSION = '2.9.0'
7
+ VERSION = '2.10.0'
8
8
  end
@@ -15,7 +15,6 @@ require_relative 'jetstream_bridge/railtie' if defined?(Rails::Railtie)
15
15
  require_relative 'jetstream_bridge/inbox_event'
16
16
  require_relative 'jetstream_bridge/outbox_event'
17
17
 
18
-
19
18
  # JetstreamBridge main module.
20
19
  module JetstreamBridge
21
20
  class << self
@@ -54,13 +53,6 @@ module JetstreamBridge
54
53
  Connection.jetstream
55
54
  end
56
55
 
57
- # @deprecated Use {ensure_topology!} instead. This method will be removed
58
- # in a future version.
59
- def ensure_topology?
60
- Logging.warn('ensure_topology? is deprecated; use ensure_topology! instead', tag: 'JetstreamBridge')
61
- !!ensure_topology!
62
- end
63
-
64
56
  private
65
57
 
66
58
  def assign!(cfg, key, val)
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: 2.9.0
4
+ version: 2.10.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-08-25 00:00:00.000000000 Z
11
+ date: 2025-08-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -83,14 +83,11 @@ files:
83
83
  - lib/generators/jetstream_bridge/migrations/templates/create_jetstream_inbox_events.rb.erb
84
84
  - lib/generators/jetstream_bridge/migrations/templates/create_jetstream_outbox_events.rb.erb
85
85
  - lib/jetstream_bridge.rb
86
- - lib/jetstream_bridge/consumer/backoff_strategy.rb
87
86
  - lib/jetstream_bridge/consumer/consumer.rb
88
- - lib/jetstream_bridge/consumer/consumer_config.rb
89
87
  - lib/jetstream_bridge/consumer/dlq_publisher.rb
90
88
  - lib/jetstream_bridge/consumer/inbox/inbox_message.rb
91
89
  - lib/jetstream_bridge/consumer/inbox/inbox_processor.rb
92
90
  - lib/jetstream_bridge/consumer/inbox/inbox_repository.rb
93
- - lib/jetstream_bridge/consumer/message_context.rb
94
91
  - lib/jetstream_bridge/consumer/message_processor.rb
95
92
  - lib/jetstream_bridge/consumer/subscription_manager.rb
96
93
  - lib/jetstream_bridge/core/config.rb
@@ -1,24 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module JetstreamBridge
4
- class BackoffStrategy
5
- TRANSIENT_ERRORS = [Timeout::Error, IOError].freeze
6
- MAX_EXPONENT = 6
7
- MAX_DELAY = 60
8
- MIN_DELAY = 1
9
-
10
- # Returns a bounded delay in seconds
11
- def delay(deliveries, error)
12
- base = transient?(error) ? 0.5 : 2.0
13
- power = [deliveries - 1, MAX_EXPONENT].min
14
- raw = (base * (2**power)).to_i
15
- raw.clamp(MIN_DELAY, MAX_DELAY)
16
- end
17
-
18
- private
19
-
20
- def transient?(error)
21
- TRANSIENT_ERRORS.any? { |k| error.is_a?(k) }
22
- end
23
- end
24
- end
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative '../core/duration'
4
- require_relative '../core/config'
5
- require_relative '../core/logging'
6
-
7
- module JetstreamBridge
8
- # Consumer configuration helpers.
9
- module ConsumerConfig
10
- module_function
11
-
12
- # Complete consumer config (pre-provisioned durable, pull mode).
13
- def consumer_config(durable, filter_subject)
14
- {
15
- durable_name: durable,
16
- filter_subject: filter_subject,
17
- ack_policy: 'explicit',
18
- deliver_policy: 'all',
19
- max_deliver: JetstreamBridge.config.max_deliver,
20
- ack_wait: Duration.to_millis(JetstreamBridge.config.ack_wait),
21
- backoff: Array(JetstreamBridge.config.backoff)
22
- .map { |d| Duration.to_millis(d) }
23
- }
24
- end
25
- end
26
- end
@@ -1,22 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'securerandom'
4
-
5
- module JetstreamBridge
6
- # Immutable per-message metadata
7
- MessageContext = Struct.new(
8
- :event_id, :deliveries, :subject, :seq, :consumer, :stream,
9
- keyword_init: true
10
- ) do
11
- def self.build(msg)
12
- new(
13
- event_id: msg.header&.[]('nats-msg-id') || SecureRandom.uuid,
14
- deliveries: msg.metadata&.num_delivered.to_i,
15
- subject: msg.subject,
16
- seq: msg.metadata&.sequence,
17
- consumer: msg.metadata&.consumer,
18
- stream: msg.metadata&.stream
19
- )
20
- end
21
- end
22
- end