jetstream_bridge 5.0.2 → 7.0.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.
@@ -417,7 +417,7 @@ If you're working on the jetstream_bridge gem itself:
417
417
  gem build jetstream_bridge.gemspec
418
418
 
419
419
  # Install locally for testing
420
- gem install ./jetstream_bridge-4.5.0.gem
420
+ gem install ./jetstream_bridge-7.0.0.gem
421
421
 
422
422
  # Or update in your application's Gemfile.lock
423
423
  bundle update jetstream_bridge
data/docs/TESTING.md CHANGED
@@ -47,7 +47,7 @@ RSpec.describe MyService do
47
47
  name: 'test-jetstream-bridge-stream',
48
48
  subjects: ['test.>']
49
49
  )
50
- allow(JetstreamBridge::Topology).to receive(:ensure!)
50
+ allow(JetstreamBridge::Topology).to receive(:provision!)
51
51
  end
52
52
 
53
53
  after do
@@ -280,7 +280,7 @@ before do
280
280
  )
281
281
 
282
282
  # Allow topology check to succeed
283
- allow(JetstreamBridge::Topology).to receive(:ensure!)
283
+ allow(JetstreamBridge::Topology).to receive(:provision!)
284
284
 
285
285
  JetstreamBridge.configure do |config|
286
286
  config.stream_name = 'jetstream-bridge-stream'
@@ -391,7 +391,7 @@ storage.reset!
391
391
  3. **Test both success and failure paths**: Use the mock to simulate errors
392
392
  4. **Verify message content**: Check that envelopes are correctly formatted
393
393
  5. **Test idempotency**: Verify duplicate detection and redelivery behavior
394
- 6. **Mock topology setup**: Remember to stub `JetstreamBridge::Topology.ensure!`
394
+ 6. **Mock topology setup**: Remember to stub `JetstreamBridge::Topology.provision!`
395
395
 
396
396
  ## Examples
397
397
 
@@ -3,22 +3,38 @@
3
3
  class CreateJetstreamInboxEvents < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
4
4
  def change
5
5
  create_table :jetstream_inbox_events do |t|
6
- t.string :event_id # preferred dedupe key
7
- t.string :subject, null: false
8
- t.jsonb :payload, null: false, default: {}
9
- t.jsonb :headers, null: false, default: {}
10
- t.string :stream
11
- t.bigint :stream_seq
12
- t.integer :deliveries
13
- t.string :status, null: false, default: 'received' # received|processing|processed|failed
14
- t.text :last_error
15
- t.datetime :received_at
16
- t.datetime :processed_at
6
+ # Event identification and deduplication
7
+ t.string :event_id, null: false # unique event identifier for deduplication
8
+ t.string :event_type, null: false # type of event (e.g., 'created', 'updated')
9
+ t.string :resource_type # type of resource (e.g., 'organization', 'user')
10
+ t.string :resource_id # ID of the resource
11
+
12
+ # Event payload and metadata
13
+ t.text :payload, null: false # full event payload as JSON
14
+ t.string :subject # NATS subject (optional)
15
+ t.jsonb :headers, default: {} # NATS message headers (optional)
16
+
17
+ # NATS JetStream metadata (optional - useful for debugging)
18
+ t.string :stream # JetStream stream name
19
+ t.bigint :stream_seq # stream sequence number
20
+ t.integer :deliveries # number of delivery attempts
21
+
22
+ # Processing status and error tracking
23
+ t.string :status, null: false, default: 'received' # received|processing|processed|failed
24
+ t.text :error_message # error message if processing failed
25
+ t.integer :processing_attempts, null: false, default: 0 # number of processing attempts
26
+
27
+ # Timestamps
28
+ t.datetime :received_at # when the event was first received
29
+ t.datetime :processed_at # when the event was successfully processed
30
+ t.datetime :failed_at # when the event failed processing
17
31
  t.timestamps
18
32
  end
19
33
 
20
- add_index :jetstream_inbox_events, :event_id, unique: true, where: 'event_id IS NOT NULL'
21
- add_index :jetstream_inbox_events, [:stream, :stream_seq], unique: true, where: 'stream IS NOT NULL AND stream_seq IS NOT NULL'
34
+ # Indexes for efficient querying and deduplication
35
+ add_index :jetstream_inbox_events, :event_id, unique: true
22
36
  add_index :jetstream_inbox_events, :status
37
+ add_index :jetstream_inbox_events, :created_at
38
+ add_index :jetstream_inbox_events, [:stream, :stream_seq], unique: true, where: 'stream IS NOT NULL AND stream_seq IS NOT NULL'
23
39
  end
24
40
  end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'core/logging'
4
+ require_relative 'core/config'
5
+
6
+ module JetstreamBridge
7
+ # Convenience helpers to keep example configuration lean and consistent.
8
+ module ConfigHelpers
9
+ DEFAULT_STREAM = 'sync-stream'
10
+ DEFAULT_BACKOFF = %w[1s 5s 15s 30s 60s].freeze
11
+ DEFAULT_ACK_WAIT = '30s'
12
+ DEFAULT_MAX_DELIVER = 5
13
+
14
+ module_function
15
+
16
+ # Configure a bidirectional bridge with sensible defaults.
17
+ #
18
+ # @param app_name [String] Name of the local app (publisher + consumer)
19
+ # @param destination_app [String] Remote app to sync with
20
+ # @param mode [Symbol] :non_restrictive (auto provision) or :restrictive
21
+ # @param stream_name [String] JetStream stream name
22
+ # @param nats_url [String] NATS connection URL(s)
23
+ # @param use_outbox [Boolean] Enable transactional outbox pattern
24
+ # @param use_inbox [Boolean] Enable idempotent inbox pattern
25
+ # @param logger [Logger,nil] Logger to attach to configuration
26
+ # @param overrides [Hash] Additional config overrides applied verbatim
27
+ #
28
+ # @yield [config] Optional block for further customization
29
+ #
30
+ # @return [JetstreamBridge::Config]
31
+ def configure_bidirectional(
32
+ app_name:,
33
+ destination_app:,
34
+ mode: :non_restrictive,
35
+ stream_name: DEFAULT_STREAM,
36
+ nats_url: ENV.fetch('NATS_URL', 'nats://nats:4222'),
37
+ use_outbox: true,
38
+ use_inbox: true,
39
+ logger: nil,
40
+ **overrides
41
+ )
42
+ JetstreamBridge.configure do |config|
43
+ apply_base_settings(config, app_name, destination_app, stream_name, nats_url, use_outbox, use_inbox, mode,
44
+ overrides)
45
+ apply_reliability_defaults(config, overrides)
46
+ config.logger = logger if logger
47
+ apply_overrides(config, overrides)
48
+ yield(config) if block_given?
49
+
50
+ config
51
+ end
52
+ end
53
+
54
+ # Wire JetstreamBridge lifecycle into Rails boot/shutdown.
55
+ #
56
+ # Safe to call multiple times; startup! is idempotent.
57
+ #
58
+ # @param logger [Logger,nil] Logger to use for lifecycle messages
59
+ # @return [void]
60
+ def setup_rails_lifecycle(logger: nil, rails_app: nil)
61
+ app = rails_app
62
+ app ||= Rails.application if defined?(Rails) && Rails.respond_to?(:application)
63
+
64
+ # Gracefully no-op when Rails isn't available (e.g., non-Rails runtimes or early boot)
65
+ return unless app
66
+
67
+ effective_logger = logger || default_rails_logger(app)
68
+
69
+ app.config.after_initialize do
70
+ JetstreamBridge.startup!
71
+ effective_logger&.info('JetStream Bridge connected successfully')
72
+ rescue StandardError => e
73
+ effective_logger&.error("Failed to connect to JetStream: #{e.message}")
74
+ end
75
+
76
+ Kernel.at_exit { JetstreamBridge.shutdown! }
77
+ end
78
+
79
+ def restrictive?(mode)
80
+ mode.to_sym == :restrictive
81
+ end
82
+ private_class_method :restrictive?
83
+
84
+ def apply_base_settings(config, app_name, destination_app, stream_name, nats_url, use_outbox, use_inbox, mode,
85
+ overrides)
86
+ config.nats_urls = nats_url
87
+ config.app_name = app_name
88
+ config.destination_app = destination_app
89
+ config.stream_name = stream_name
90
+ config.auto_provision = !restrictive?(mode)
91
+ config.use_outbox = use_outbox
92
+ config.use_inbox = use_inbox
93
+ config.consumer_mode = overrides.fetch(:consumer_mode, config.consumer_mode || :pull)
94
+ end
95
+ private_class_method :apply_base_settings
96
+
97
+ def apply_reliability_defaults(config, overrides)
98
+ config.max_deliver = overrides.fetch(:max_deliver, DEFAULT_MAX_DELIVER)
99
+ config.ack_wait = overrides.fetch(:ack_wait, DEFAULT_ACK_WAIT)
100
+ config.backoff = overrides.fetch(:backoff, DEFAULT_BACKOFF)
101
+ end
102
+ private_class_method :apply_reliability_defaults
103
+
104
+ def apply_overrides(config, overrides)
105
+ ignored = [:max_deliver, :ack_wait, :backoff, :consumer_mode]
106
+ overrides.each do |key, value|
107
+ next if ignored.include?(key)
108
+
109
+ setter = "#{key}="
110
+ config.public_send(setter, value) if config.respond_to?(setter)
111
+ end
112
+ end
113
+ private_class_method :apply_overrides
114
+
115
+ def default_rails_logger(app = nil)
116
+ return app.logger if app.respond_to?(:logger)
117
+
118
+ defined?(Rails) && Rails.respond_to?(:logger) ? Rails.logger : nil
119
+ end
120
+ private_class_method :default_rails_logger
121
+ end
122
+ end
@@ -9,6 +9,7 @@ require_relative '../core/config'
9
9
  require_relative '../core/model_utils'
10
10
  require_relative 'message_processor'
11
11
  require_relative 'subscription_manager'
12
+ require_relative 'consumer_state'
12
13
  require_relative 'inbox/inbox_processor'
13
14
 
14
15
  module JetstreamBridge
@@ -63,6 +64,8 @@ module JetstreamBridge
63
64
  attr_reader :batch_size
64
65
  # @return [MiddlewareChain] Middleware chain for processing
65
66
  attr_reader :middleware_chain
67
+ # Expose grouped state objects for observability/testing
68
+ attr_reader :processing_state, :lifecycle_state, :connection_state
66
69
 
67
70
  # Initialize a new Consumer instance.
68
71
  #
@@ -100,13 +103,9 @@ module JetstreamBridge
100
103
 
101
104
  @batch_size = Integer(batch_size || DEFAULT_BATCH_SIZE)
102
105
  @durable = durable_name || JetstreamBridge.config.durable_name
103
- @idle_backoff = IDLE_SLEEP_SECS
104
- @reconnect_attempts = 0
105
- @running = true
106
- @shutdown_requested = false
107
- @start_time = Time.now
108
- @iterations = 0
109
- @last_health_check = Time.now
106
+ @processing_state = ProcessingState.new(idle_backoff: IDLE_SLEEP_SECS)
107
+ @lifecycle_state = LifecycleState.new
108
+ @connection_state = ConnectionState.new
110
109
  # Use existing connection (should already be established)
111
110
  @jts = Connection.jetstream
112
111
  raise ConnectionError, 'JetStream connection not available. Call JetstreamBridge.startup! first.' unless @jts
@@ -196,18 +195,33 @@ module JetstreamBridge
196
195
  "Consumer #{@durable} started (batch=#{@batch_size}, dest=#{JetstreamBridge.config.destination_subject})…",
197
196
  tag: 'JetstreamBridge::Consumer'
198
197
  )
199
- while @running
198
+ while @lifecycle_state.running
199
+ # Check if signal was received and log it (safe from main loop)
200
+ if @lifecycle_state.signal_received && !@lifecycle_state.signal_logged
201
+ Logging.info("Received #{@lifecycle_state.signal_received}, stopping consumer...",
202
+ tag: 'JetstreamBridge::Consumer')
203
+ @lifecycle_state.signal_logged = true
204
+ end
205
+
206
+ Logging.debug(
207
+ "Fetching messages (iteration=#{@processing_state.iterations}, batch_size=#{@batch_size})...",
208
+ tag: 'JetstreamBridge::Consumer'
209
+ )
200
210
  processed = process_batch
211
+ Logging.debug(
212
+ "Processed #{processed} messages",
213
+ tag: 'JetstreamBridge::Consumer'
214
+ )
201
215
  idle_sleep(processed)
202
216
 
203
- @iterations += 1
217
+ @processing_state.iterations += 1
204
218
 
205
219
  # Periodic health checks every 10 minutes (600 seconds)
206
220
  perform_health_check_if_due
207
221
  end
208
222
 
209
223
  # Drain in-flight messages before exiting
210
- drain_inflight_messages if @shutdown_requested
224
+ drain_inflight_messages if @lifecycle_state.shutdown_requested
211
225
  Logging.info("Consumer #{@durable} stopped gracefully", tag: 'JetstreamBridge::Consumer')
212
226
  end
213
227
 
@@ -237,8 +251,7 @@ module JetstreamBridge
237
251
  # consumer.stop! # Stop after 10 seconds
238
252
  #
239
253
  def stop!
240
- @shutdown_requested = true
241
- @running = false
254
+ @lifecycle_state.stop!
242
255
  Logging.info("Consumer #{@durable} shutdown requested", tag: 'JetstreamBridge::Consumer')
243
256
  end
244
257
 
@@ -256,16 +269,21 @@ module JetstreamBridge
256
269
 
257
270
  # Returns number of messages processed; 0 on timeout/idle or after recovery.
258
271
  def process_batch
272
+ Logging.debug('Calling fetch_messages...', tag: 'JetstreamBridge::Consumer')
259
273
  msgs = fetch_messages
274
+ Logging.debug("Fetched #{msgs&.size || 0} messages", tag: 'JetstreamBridge::Consumer')
260
275
  return 0 if msgs.nil? || msgs.empty?
261
276
 
262
277
  msgs.sum { |m| process_one(m) }
263
- rescue NATS::Timeout, NATS::IO::Timeout
278
+ rescue NATS::Timeout, NATS::IO::Timeout => e
279
+ Logging.debug("Fetch timeout: #{e.class}", tag: 'JetstreamBridge::Consumer')
264
280
  0
265
281
  rescue NATS::JetStream::Error => e
282
+ Logging.error("JetStream error: #{e.class} #{e.message}", tag: 'JetstreamBridge::Consumer')
266
283
  handle_js_error(e)
267
284
  rescue StandardError => e
268
285
  Logging.error("Unexpected process_batch error: #{e.class} #{e.message}", tag: 'JetstreamBridge::Consumer')
286
+ Logging.error("Backtrace: #{e.backtrace.first(5).join("\n")}", tag: 'JetstreamBridge::Consumer')
269
287
  0
270
288
  end
271
289
 
@@ -280,7 +298,17 @@ module JetstreamBridge
280
298
  end
281
299
 
282
300
  def fetch_messages_pull
283
- @psub.fetch(@batch_size, timeout: FETCH_TIMEOUT_SECS)
301
+ Logging.debug(
302
+ "fetch_messages_pull called (@psub=#{@psub.class}, batch=#{@batch_size}, timeout=#{FETCH_TIMEOUT_SECS})",
303
+ tag: 'JetstreamBridge::Consumer'
304
+ )
305
+ result = @psub.fetch(@batch_size, timeout: FETCH_TIMEOUT_SECS)
306
+ Logging.debug("fetch returned #{result&.size || 0} messages", tag: 'JetstreamBridge::Consumer')
307
+ result
308
+ rescue StandardError => e
309
+ Logging.error("fetch_messages_pull error: #{e.class} #{e.message}", tag: 'JetstreamBridge::Consumer')
310
+ Logging.error("Backtrace: #{e.backtrace.first(5).join("\n")}", tag: 'JetstreamBridge::Consumer')
311
+ raise
284
312
  end
285
313
 
286
314
  def fetch_messages_push
@@ -313,11 +341,11 @@ module JetstreamBridge
313
341
  def handle_js_error(error)
314
342
  if recoverable_consumer_error?(error)
315
343
  # Increment reconnect attempts and calculate exponential backoff
316
- @reconnect_attempts += 1
317
- backoff_secs = calculate_reconnect_backoff(@reconnect_attempts)
344
+ @connection_state.reconnect_attempts += 1
345
+ backoff_secs = calculate_reconnect_backoff(@connection_state.reconnect_attempts)
318
346
 
319
347
  Logging.warn(
320
- "Recovering subscription after error (attempt #{@reconnect_attempts}): " \
348
+ "Recovering subscription after error (attempt #{@connection_state.reconnect_attempts}): " \
321
349
  "#{error.class} #{error.message}, waiting #{backoff_secs}s",
322
350
  tag: 'JetstreamBridge::Consumer'
323
351
  )
@@ -326,7 +354,7 @@ module JetstreamBridge
326
354
  ensure_subscription!
327
355
 
328
356
  # Reset counter on successful reconnection
329
- @reconnect_attempts = 0
357
+ @connection_state.reconnect_attempts = 0
330
358
  else
331
359
  Logging.error("Fetch failed (non-recoverable): #{error.class} #{error.message}", tag: 'JetstreamBridge::Consumer')
332
360
  end
@@ -358,18 +386,19 @@ module JetstreamBridge
358
386
  def idle_sleep(processed)
359
387
  if processed.zero?
360
388
  # exponential-ish backoff with a tiny jitter to avoid sync across workers
361
- @idle_backoff = [@idle_backoff * 1.5, MAX_IDLE_BACKOFF_SECS].min
362
- sleep(@idle_backoff + (rand * 0.01))
389
+ @processing_state.idle_backoff = [@processing_state.idle_backoff * 1.5, MAX_IDLE_BACKOFF_SECS].min
390
+ sleep(@processing_state.idle_backoff + (rand * 0.01))
363
391
  else
364
- @idle_backoff = IDLE_SLEEP_SECS
392
+ @processing_state.idle_backoff = IDLE_SLEEP_SECS
365
393
  end
366
394
  end
367
395
 
368
396
  def setup_signal_handlers
369
397
  %w[INT TERM].each do |sig|
370
398
  Signal.trap(sig) do
371
- Logging.info("Received #{sig}, stopping consumer...", tag: 'JetstreamBridge::Consumer')
372
- stop!
399
+ # CRITICAL: Only set flags in trap context, no I/O or mutex operations
400
+ # Logging and other operations are unsafe from signal handlers
401
+ @lifecycle_state.signal!(sig)
373
402
  end
374
403
  end
375
404
  rescue ArgumentError => e
@@ -379,16 +408,16 @@ module JetstreamBridge
379
408
 
380
409
  def perform_health_check_if_due
381
410
  now = Time.now
382
- time_since_check = now - @last_health_check
411
+ time_since_check = now - @connection_state.last_health_check
383
412
 
384
413
  return unless time_since_check >= 600 # 10 minutes
385
414
 
386
- @last_health_check = now
387
- uptime = now - @start_time
415
+ @connection_state.mark_health_check(now)
416
+ uptime = @lifecycle_state.uptime(now)
388
417
  memory_mb = memory_usage_mb
389
418
 
390
419
  Logging.info(
391
- "Consumer health: iterations=#{@iterations}, " \
420
+ "Consumer health: iterations=#{@processing_state.iterations}, " \
392
421
  "memory=#{memory_mb}MB, uptime=#{uptime.round}s",
393
422
  tag: 'JetstreamBridge::Consumer'
394
423
  )
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JetstreamBridge
4
+ class Consumer
5
+ # Tracks processing counters and backoff state.
6
+ class ProcessingState
7
+ attr_accessor :idle_backoff, :iterations
8
+
9
+ def initialize(idle_backoff:, iterations: 0)
10
+ @idle_backoff = idle_backoff
11
+ @iterations = iterations
12
+ end
13
+ end
14
+
15
+ # Tracks lifecycle flags and timing for the consumer.
16
+ class LifecycleState
17
+ attr_accessor :running, :shutdown_requested, :signal_received, :signal_logged
18
+ attr_reader :start_time
19
+
20
+ def initialize(start_time: Time.now)
21
+ @running = true
22
+ @shutdown_requested = false
23
+ @signal_received = nil
24
+ @signal_logged = false
25
+ @start_time = start_time
26
+ end
27
+
28
+ def stop!
29
+ @shutdown_requested = true
30
+ @running = false
31
+ end
32
+
33
+ def signal!(sig)
34
+ @signal_received = sig
35
+ stop!
36
+ end
37
+
38
+ def uptime(now = Time.now)
39
+ now - @start_time
40
+ end
41
+ end
42
+
43
+ # Tracks reconnection attempts and health check timing.
44
+ class ConnectionState
45
+ attr_accessor :reconnect_attempts
46
+ attr_reader :last_health_check
47
+
48
+ def initialize(now: Time.now)
49
+ @reconnect_attempts = 0
50
+ @last_health_check = now
51
+ end
52
+
53
+ def mark_health_check(now = Time.now)
54
+ @last_health_check = now
55
+ end
56
+ end
57
+ end
58
+ end
@@ -30,6 +30,9 @@ module JetstreamBridge
30
30
  ActiveRecord::Base.transaction do
31
31
  attrs = {
32
32
  event_id: msg.event_id,
33
+ event_type: msg.body['type'] || msg.body['event_type'],
34
+ resource_type: msg.body['resource_type'],
35
+ resource_id: msg.body['resource_id'],
33
36
  subject: msg.subject,
34
37
  payload: ModelUtils.json_dump(msg.body_for_store),
35
38
  headers: ModelUtils.json_dump(msg.headers),
@@ -37,10 +40,14 @@ module JetstreamBridge
37
40
  stream_seq: msg.seq,
38
41
  deliveries: msg.deliveries,
39
42
  status: 'processing',
40
- last_error: nil,
43
+ error_message: nil, # Clear any previous error
44
+ last_error: nil, # Legacy field (for backwards compatibility)
45
+ processing_attempts: (record.respond_to?(:processing_attempts) ? (record.processing_attempts || 0) + 1 : nil),
41
46
  received_at: record.respond_to?(:received_at) ? (record.received_at || msg.now) : nil,
42
47
  updated_at: record.respond_to?(:updated_at) ? msg.now : nil
43
48
  }
49
+ # Some schemas capture the producing app
50
+ attrs[:source_app] = msg.body['producer'] || msg.headers['producer'] if record.respond_to?(:source_app=)
44
51
  ModelUtils.assign_known_attrs(record, attrs)
45
52
  record.save!
46
53
  end
@@ -64,9 +71,12 @@ module JetstreamBridge
64
71
 
65
72
  ActiveRecord::Base.transaction do
66
73
  now = Time.now.utc
74
+ error_msg = "#{error.class}: #{error.message}"
67
75
  attrs = {
68
76
  status: 'failed',
69
- last_error: "#{error.class}: #{error.message}",
77
+ error_message: error_msg, # Standard field name
78
+ last_error: error_msg, # Legacy field (for backwards compatibility)
79
+ failed_at: record.respond_to?(:failed_at) ? now : nil,
70
80
  updated_at: record.respond_to?(:updated_at) ? now : nil
71
81
  }
72
82
  ModelUtils.assign_known_attrs(record, attrs)
@@ -22,8 +22,8 @@ module JetstreamBridge
22
22
  sub.instance_variable_set(:@_jsb_deliver, deliver)
23
23
  sub.instance_variable_set(:@_jsb_next_subject, "#{prefix}.CONSUMER.MSG.NEXT.#{@stream_name}.#{@durable}")
24
24
 
25
- extend_pull_subscription(sub)
26
- attach_jsi(sub)
25
+ apply_pull_subscription_extensions(sub)
26
+ attach_js_subscription_metadata(sub)
27
27
 
28
28
  Logging.info(
29
29
  "Created pull subscription without verification for consumer #{@durable} " \
@@ -36,7 +36,7 @@ module JetstreamBridge
36
36
 
37
37
  private
38
38
 
39
- def extend_pull_subscription(sub)
39
+ def apply_pull_subscription_extensions(sub)
40
40
  pull_mod = begin
41
41
  NATS::JetStream.const_get(:PullSubscription)
42
42
  rescue NameError
@@ -44,10 +44,10 @@ module JetstreamBridge
44
44
  end
45
45
 
46
46
  sub.extend(pull_mod) if pull_mod
47
- shim_fetch(sub) unless pull_mod
47
+ define_fetch_fallback(sub) unless pull_mod
48
48
  end
49
49
 
50
- def shim_fetch(sub)
50
+ def define_fetch_fallback(sub)
51
51
  Logging.warn(
52
52
  'PullSubscription mixin unavailable; using shim fetch implementation',
53
53
  tag: 'JetstreamBridge::Consumer'
@@ -78,7 +78,7 @@ module JetstreamBridge
78
78
  end
79
79
  end
80
80
 
81
- def attach_jsi(sub)
81
+ def attach_js_subscription_metadata(sub)
82
82
  js_sub_class = begin
83
83
  NATS::JetStream.const_get(:JS).const_get(:Sub)
84
84
  rescue NameError