jetstream_bridge 4.4.1 → 4.5.1
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 +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +3 -17
- data/docs/GETTING_STARTED.md +2 -2
- data/docs/PRODUCTION.md +28 -10
- data/docs/RESTRICTED_PERMISSIONS.md +399 -0
- data/docs/TESTING.md +5 -5
- data/lib/generators/jetstream_bridge/health_check/health_check_generator.rb +4 -4
- data/lib/generators/jetstream_bridge/initializer/initializer_generator.rb +1 -1
- data/lib/generators/jetstream_bridge/initializer/templates/jetstream_bridge.rb +2 -3
- data/lib/generators/jetstream_bridge/install/install_generator.rb +5 -5
- data/lib/generators/jetstream_bridge/migrations/migrations_generator.rb +2 -2
- data/lib/jetstream_bridge/consumer/consumer.rb +5 -6
- data/lib/jetstream_bridge/consumer/subscription_manager.rb +72 -79
- data/lib/jetstream_bridge/core/bridge_helpers.rb +37 -19
- data/lib/jetstream_bridge/core/config.rb +20 -42
- data/lib/jetstream_bridge/core/connection.rb +60 -16
- data/lib/jetstream_bridge/core/connection_factory.rb +2 -7
- data/lib/jetstream_bridge/core/debug_helper.rb +0 -1
- data/lib/jetstream_bridge/models/subject.rb +15 -23
- data/lib/jetstream_bridge/provisioner.rb +67 -0
- data/lib/jetstream_bridge/publisher/publisher.rb +8 -9
- data/lib/jetstream_bridge/tasks/install.rake +17 -2
- data/lib/jetstream_bridge/version.rb +1 -1
- data/lib/jetstream_bridge.rb +89 -37
- metadata +5 -3
|
@@ -4,7 +4,7 @@ require 'rails/generators'
|
|
|
4
4
|
|
|
5
5
|
module JetstreamBridge
|
|
6
6
|
module Generators
|
|
7
|
-
class HealthCheckGenerator < Rails::Generators::Base
|
|
7
|
+
class HealthCheckGenerator < ::Rails::Generators::Base
|
|
8
8
|
source_root File.expand_path('templates', __dir__)
|
|
9
9
|
desc 'Creates a health check endpoint for JetStream Bridge monitoring'
|
|
10
10
|
|
|
@@ -41,12 +41,12 @@ module JetstreamBridge
|
|
|
41
41
|
"connected_at": "2025-11-22T20:00:00Z",
|
|
42
42
|
"stream": {
|
|
43
43
|
"exists": true,
|
|
44
|
-
"name": "
|
|
45
|
-
"subjects": ["
|
|
44
|
+
"name": "jetstream-bridge-stream",
|
|
45
|
+
"subjects": ["app1.sync.app2"],
|
|
46
46
|
"messages": 42
|
|
47
47
|
},
|
|
48
48
|
"config": {
|
|
49
|
-
"
|
|
49
|
+
"stream_name": "jetstream-bridge-stream",
|
|
50
50
|
"app_name": "my_app",
|
|
51
51
|
"destination_app": "other_app"
|
|
52
52
|
},
|
|
@@ -4,7 +4,7 @@ require 'rails/generators'
|
|
|
4
4
|
|
|
5
5
|
module JetstreamBridge
|
|
6
6
|
module Generators
|
|
7
|
-
class InitializerGenerator < Rails::Generators::Base
|
|
7
|
+
class InitializerGenerator < ::Rails::Generators::Base
|
|
8
8
|
source_root File.expand_path('templates', __dir__)
|
|
9
9
|
desc 'Creates config/initializers/jetstream_bridge.rb'
|
|
10
10
|
|
|
@@ -13,9 +13,8 @@ JetstreamBridge.configure do |config|
|
|
|
13
13
|
# NATS server URLs (comma-separated for cluster)
|
|
14
14
|
config.nats_urls = ENV.fetch('NATS_URLS', 'nats://localhost:4222')
|
|
15
15
|
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
config.env = ENV.fetch('NATS_ENV', Rails.env)
|
|
16
|
+
# Stream name (required) - managed separately from runtime credentials
|
|
17
|
+
config.stream_name = ENV.fetch('JETSTREAM_STREAM_NAME', 'jetstream-bridge-stream')
|
|
19
18
|
|
|
20
19
|
# Application name (used in subject routing)
|
|
21
20
|
config.app_name = ENV.fetch('APP_NAME', Rails.application.class.module_parent_name.underscore)
|
|
@@ -5,16 +5,16 @@ require 'rails/generators'
|
|
|
5
5
|
module JetstreamBridge
|
|
6
6
|
module Generators
|
|
7
7
|
# Install generator.
|
|
8
|
-
class InstallGenerator < Rails::Generators::Base
|
|
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,
|
|
12
|
-
|
|
11
|
+
::Rails::Generators.invoke('jetstream_bridge:initializer', [], behavior: behavior,
|
|
12
|
+
destination_root: destination_root)
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def create_migrations
|
|
16
|
-
Rails::Generators.invoke('jetstream_bridge:migrations', [], behavior: behavior,
|
|
17
|
-
|
|
16
|
+
::Rails::Generators.invoke('jetstream_bridge:migrations', [], behavior: behavior,
|
|
17
|
+
destination_root: destination_root)
|
|
18
18
|
end
|
|
19
19
|
end
|
|
20
20
|
end
|
|
@@ -6,8 +6,8 @@ require 'rails/generators/active_record'
|
|
|
6
6
|
module JetstreamBridge
|
|
7
7
|
module Generators
|
|
8
8
|
# Migrations generator.
|
|
9
|
-
class MigrationsGenerator < Rails::Generators::Base
|
|
10
|
-
include Rails::Generators::Migration
|
|
9
|
+
class MigrationsGenerator < ::Rails::Generators::Base
|
|
10
|
+
include ::Rails::Generators::Migration
|
|
11
11
|
|
|
12
12
|
source_root File.expand_path('templates', __dir__)
|
|
13
13
|
desc 'Creates Inbox/Outbox migrations for JetstreamBridge'
|
|
@@ -244,17 +244,16 @@ module JetstreamBridge
|
|
|
244
244
|
|
|
245
245
|
private
|
|
246
246
|
|
|
247
|
-
def ensure_destination_app_configured!
|
|
248
|
-
return unless JetstreamBridge.config.destination_app.to_s.empty?
|
|
249
|
-
|
|
250
|
-
raise ArgumentError, 'destination_app must be configured'
|
|
251
|
-
end
|
|
252
|
-
|
|
253
247
|
def ensure_subscription!
|
|
254
248
|
@sub_mgr.ensure_consumer!
|
|
255
249
|
@psub = @sub_mgr.subscribe!
|
|
256
250
|
end
|
|
257
251
|
|
|
252
|
+
def ensure_destination_app_configured!
|
|
253
|
+
# Use subject builder to enforce required components and align with existing validation messages.
|
|
254
|
+
JetstreamBridge.config.destination_subject
|
|
255
|
+
end
|
|
256
|
+
|
|
258
257
|
# Returns number of messages processed; 0 on timeout/idle or after recovery.
|
|
259
258
|
def process_batch
|
|
260
259
|
msgs = fetch_messages
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative '../core/logging'
|
|
4
4
|
require_relative '../core/duration'
|
|
5
|
+
require_relative '../errors'
|
|
5
6
|
|
|
6
7
|
module JetstreamBridge
|
|
7
8
|
# Encapsulates durable ensure + subscribe for a pull consumer.
|
|
@@ -10,8 +11,7 @@ module JetstreamBridge
|
|
|
10
11
|
@jts = jts
|
|
11
12
|
@durable = durable
|
|
12
13
|
@cfg = cfg
|
|
13
|
-
@desired_cfg
|
|
14
|
-
@desired_cfg_norm = normalize_consumer_config(@desired_cfg)
|
|
14
|
+
@desired_cfg = build_consumer_config(@durable, filter_subject)
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def stream_name
|
|
@@ -26,53 +26,69 @@ module JetstreamBridge
|
|
|
26
26
|
@desired_cfg
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
def ensure_consumer!
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if have_norm == @desired_cfg_norm
|
|
35
|
-
log_consumer_ok
|
|
36
|
-
else
|
|
37
|
-
log_consumer_diff(have_norm)
|
|
38
|
-
recreate_consumer!
|
|
29
|
+
def ensure_consumer!(force: false)
|
|
30
|
+
# Runtime path: never hit JetStream management APIs to avoid admin permissions.
|
|
31
|
+
unless force || @cfg.auto_provision
|
|
32
|
+
log_runtime_skip
|
|
33
|
+
return
|
|
39
34
|
end
|
|
35
|
+
|
|
36
|
+
create_consumer!
|
|
40
37
|
end
|
|
41
38
|
|
|
42
39
|
# Bind a pull subscriber to the existing durable.
|
|
43
40
|
def subscribe!
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
41
|
+
# Always bypass consumer_info to avoid requiring JetStream API permissions at runtime.
|
|
42
|
+
subscribe_without_verification!
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def subscribe_without_verification!
|
|
46
|
+
# Manually create a pull subscription without calling consumer_info
|
|
47
|
+
# This bypasses the permission check in nats-pure's pull_subscribe
|
|
48
|
+
nc = resolve_nc
|
|
49
|
+
|
|
50
|
+
if nc.respond_to?(:new_inbox) && nc.respond_to?(:subscribe)
|
|
51
|
+
prefix = @jts.instance_variable_get(:@prefix) || '$JS.API'
|
|
52
|
+
deliver = nc.new_inbox
|
|
53
|
+
sub = nc.subscribe(deliver)
|
|
54
|
+
|
|
55
|
+
# Extend with PullSubscription module to add fetch methods
|
|
56
|
+
sub.extend(NATS::JetStream::PullSubscription)
|
|
57
|
+
|
|
58
|
+
# Set up the JSI (JetStream Info) struct that PullSubscription expects
|
|
59
|
+
# This matches what nats-pure does in pull_subscribe
|
|
60
|
+
subject = "#{prefix}.CONSUMER.MSG.NEXT.#{stream_name}.#{@durable}"
|
|
61
|
+
sub.jsi = NATS::JetStream::JS::Sub.new(
|
|
62
|
+
js: @jts,
|
|
63
|
+
stream: stream_name,
|
|
64
|
+
consumer: @durable,
|
|
65
|
+
nms: subject
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
Logging.info(
|
|
69
|
+
"Created pull subscription without verification for consumer #{@durable} " \
|
|
70
|
+
"(stream=#{stream_name}, filter=#{filter_subject})",
|
|
71
|
+
tag: 'JetstreamBridge::Consumer'
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
return sub
|
|
75
|
+
end
|
|
64
76
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
77
|
+
# Fallback for environments (mocks/tests) where low-level NATS client is unavailable.
|
|
78
|
+
if @jts.respond_to?(:pull_subscribe)
|
|
79
|
+
Logging.info(
|
|
80
|
+
"Using pull_subscribe fallback for consumer #{@durable} (stream=#{stream_name})",
|
|
81
|
+
tag: 'JetstreamBridge::Consumer'
|
|
82
|
+
)
|
|
83
|
+
return @jts.pull_subscribe(filter_subject, @durable, stream: stream_name)
|
|
68
84
|
end
|
|
69
85
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
tag: 'JetstreamBridge::Consumer'
|
|
73
|
-
)
|
|
86
|
+
raise JetstreamBridge::ConnectionError,
|
|
87
|
+
'Unable to create subscription without verification: NATS client not available'
|
|
74
88
|
end
|
|
75
89
|
|
|
90
|
+
private
|
|
91
|
+
|
|
76
92
|
def build_consumer_config(durable, filter_subject)
|
|
77
93
|
{
|
|
78
94
|
durable_name: durable,
|
|
@@ -86,30 +102,6 @@ module JetstreamBridge
|
|
|
86
102
|
}
|
|
87
103
|
end
|
|
88
104
|
|
|
89
|
-
# Normalize both server-returned config objects and our desired hash
|
|
90
|
-
# into a common hash with consistent units/types for accurate comparison.
|
|
91
|
-
def normalize_consumer_config(cfg)
|
|
92
|
-
{
|
|
93
|
-
filter_subject: sval(cfg, :filter_subject), # string
|
|
94
|
-
ack_policy: sval(cfg, :ack_policy), # string
|
|
95
|
-
deliver_policy: sval(cfg, :deliver_policy), # string
|
|
96
|
-
max_deliver: ival(cfg, :max_deliver), # integer
|
|
97
|
-
ack_wait_secs: d_secs(cfg, :ack_wait), # integer seconds
|
|
98
|
-
backoff_secs: darr_secs(cfg, :backoff) # array of integer seconds
|
|
99
|
-
}
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
# ---- lifecycle helpers ----
|
|
103
|
-
|
|
104
|
-
def recreate_consumer!
|
|
105
|
-
Logging.warn(
|
|
106
|
-
"Consumer #{@durable} exists with mismatched config; recreating (filter=#{filter_subject})",
|
|
107
|
-
tag: 'JetstreamBridge::Consumer'
|
|
108
|
-
)
|
|
109
|
-
safe_delete_consumer
|
|
110
|
-
create_consumer!
|
|
111
|
-
end
|
|
112
|
-
|
|
113
105
|
def create_consumer!
|
|
114
106
|
@jts.add_consumer(stream_name, **desired_consumer_cfg)
|
|
115
107
|
Logging.info(
|
|
@@ -118,22 +110,6 @@ module JetstreamBridge
|
|
|
118
110
|
)
|
|
119
111
|
end
|
|
120
112
|
|
|
121
|
-
def log_consumer_ok
|
|
122
|
-
Logging.info(
|
|
123
|
-
"Consumer #{@durable} exists with desired config.",
|
|
124
|
-
tag: 'JetstreamBridge::Consumer'
|
|
125
|
-
)
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def safe_delete_consumer
|
|
129
|
-
@jts.delete_consumer(stream_name, @durable)
|
|
130
|
-
rescue NATS::JetStream::Error => e
|
|
131
|
-
Logging.warn(
|
|
132
|
-
"Delete consumer #{@durable} ignored: #{e.class} #{e.message}",
|
|
133
|
-
tag: 'JetstreamBridge::Consumer'
|
|
134
|
-
)
|
|
135
|
-
end
|
|
136
|
-
|
|
137
113
|
# ---- cfg access/normalization (struct-like or hash-like) ----
|
|
138
114
|
|
|
139
115
|
def get(cfg, key)
|
|
@@ -200,5 +176,22 @@ module JetstreamBridge
|
|
|
200
176
|
# Always round up to avoid zero-second waits when sub-second durations are provided.
|
|
201
177
|
[(millis / 1000.0).ceil, 1].max
|
|
202
178
|
end
|
|
179
|
+
|
|
180
|
+
def log_runtime_skip
|
|
181
|
+
Logging.info(
|
|
182
|
+
"Skipping consumer provisioning/verification for #{@durable} at runtime to avoid JetStream API usage. " \
|
|
183
|
+
'Ensure it is pre-created via provisioning.',
|
|
184
|
+
tag: 'JetstreamBridge::Consumer'
|
|
185
|
+
)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def resolve_nc
|
|
189
|
+
return @jts.nc if @jts.respond_to?(:nc)
|
|
190
|
+
return @jts.instance_variable_get(:@nc) if @jts.instance_variable_defined?(:@nc)
|
|
191
|
+
|
|
192
|
+
return @cfg.mock_nats_client if @cfg.respond_to?(:mock_nats_client) && @cfg.mock_nats_client
|
|
193
|
+
|
|
194
|
+
nil
|
|
195
|
+
end
|
|
203
196
|
end
|
|
204
197
|
end
|
|
@@ -37,32 +37,17 @@ module JetstreamBridge
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def fetch_stream_info
|
|
40
|
+
return skipped_stream_info unless config.auto_provision
|
|
41
|
+
|
|
40
42
|
# Ensure we have an active connection before querying stream info
|
|
41
43
|
connect_if_needed!
|
|
42
44
|
|
|
43
45
|
jts = Connection.jetstream
|
|
44
46
|
raise ConnectionNotEstablishedError, 'NATS connection not established' unless jts
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
# Handle both object-style and hash-style access for compatibility
|
|
49
|
-
config_data = info.config
|
|
50
|
-
state_data = info.state
|
|
51
|
-
subjects = config_data.respond_to?(:subjects) ? config_data.subjects : config_data[:subjects]
|
|
52
|
-
messages = state_data.respond_to?(:messages) ? state_data.messages : state_data[:messages]
|
|
53
|
-
|
|
54
|
-
{
|
|
55
|
-
exists: true,
|
|
56
|
-
name: config.stream_name,
|
|
57
|
-
subjects: subjects,
|
|
58
|
-
messages: messages
|
|
59
|
-
}
|
|
48
|
+
stream_info_payload(jts.stream_info(config.stream_name))
|
|
60
49
|
rescue StandardError => e
|
|
61
|
-
|
|
62
|
-
exists: false,
|
|
63
|
-
name: config.stream_name,
|
|
64
|
-
error: "#{e.class}: #{e.message}"
|
|
65
|
-
}
|
|
50
|
+
stream_error_payload(e)
|
|
66
51
|
end
|
|
67
52
|
|
|
68
53
|
def measure_nats_rtt
|
|
@@ -104,6 +89,39 @@ module JetstreamBridge
|
|
|
104
89
|
|
|
105
90
|
cfg.public_send(setter, val)
|
|
106
91
|
end
|
|
92
|
+
|
|
93
|
+
def stream_info_payload(info)
|
|
94
|
+
config_data = info.config
|
|
95
|
+
state_data = info.state
|
|
96
|
+
|
|
97
|
+
{
|
|
98
|
+
exists: true,
|
|
99
|
+
name: config.stream_name,
|
|
100
|
+
subjects: extract_field(config_data, :subjects),
|
|
101
|
+
messages: extract_field(state_data, :messages)
|
|
102
|
+
}
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def extract_field(data, key)
|
|
106
|
+
data.respond_to?(key) ? data.public_send(key) : data[key]
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def stream_error_payload(error)
|
|
110
|
+
{
|
|
111
|
+
exists: false,
|
|
112
|
+
name: config.stream_name,
|
|
113
|
+
error: "#{error.class}: #{error.message}"
|
|
114
|
+
}
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def skipped_stream_info
|
|
118
|
+
{
|
|
119
|
+
exists: nil,
|
|
120
|
+
name: config.stream_name,
|
|
121
|
+
skipped: true,
|
|
122
|
+
reason: 'auto_provision=false (skip $JS.API.STREAM.INFO)'
|
|
123
|
+
}
|
|
124
|
+
end
|
|
107
125
|
end
|
|
108
126
|
end
|
|
109
127
|
end
|
|
@@ -11,9 +11,9 @@ module JetstreamBridge
|
|
|
11
11
|
# @example Basic configuration
|
|
12
12
|
# JetstreamBridge.configure do |config|
|
|
13
13
|
# config.nats_urls = "nats://localhost:4222"
|
|
14
|
-
# config.env = "production"
|
|
15
14
|
# config.app_name = "api_service"
|
|
16
15
|
# config.destination_app = "worker_service"
|
|
16
|
+
# config.stream_name = "jetstream-bridge-stream"
|
|
17
17
|
# config.use_outbox = true
|
|
18
18
|
# config.use_inbox = true
|
|
19
19
|
# end
|
|
@@ -23,6 +23,7 @@ module JetstreamBridge
|
|
|
23
23
|
# config.nats_urls = ENV["NATS_URLS"]
|
|
24
24
|
# config.app_name = "api"
|
|
25
25
|
# config.destination_app = "worker"
|
|
26
|
+
# config.stream_name = "jetstream-bridge-stream"
|
|
26
27
|
# end
|
|
27
28
|
#
|
|
28
29
|
class Config
|
|
@@ -50,9 +51,9 @@ module JetstreamBridge
|
|
|
50
51
|
# NATS server URL(s)
|
|
51
52
|
# @return [String]
|
|
52
53
|
attr_accessor :nats_urls
|
|
53
|
-
#
|
|
54
|
+
# JetStream stream name (required)
|
|
54
55
|
# @return [String]
|
|
55
|
-
attr_accessor :
|
|
56
|
+
attr_accessor :stream_name
|
|
56
57
|
# Application name for subject routing
|
|
57
58
|
# @return [String]
|
|
58
59
|
attr_accessor :app_name
|
|
@@ -95,11 +96,15 @@ module JetstreamBridge
|
|
|
95
96
|
# Enable lazy connection (connect on first use instead of during configure)
|
|
96
97
|
# @return [Boolean]
|
|
97
98
|
attr_accessor :lazy_connect
|
|
99
|
+
# Allow JetStream Bridge to create/update streams/consumers and call JetStream management APIs.
|
|
100
|
+
# Disable for locked-down environments and handle provisioning separately.
|
|
101
|
+
# @return [Boolean]
|
|
102
|
+
attr_accessor :auto_provision
|
|
98
103
|
|
|
99
104
|
def initialize
|
|
100
105
|
@nats_urls = ENV['NATS_URLS'] || ENV['NATS_URL'] || 'nats://localhost:4222'
|
|
101
|
-
@
|
|
102
|
-
@app_name = ENV['APP_NAME']
|
|
106
|
+
@stream_name = ENV['JETSTREAM_STREAM_NAME'] || 'jetstream-bridge-stream'
|
|
107
|
+
@app_name = ENV['APP_NAME'] || 'app'
|
|
103
108
|
@destination_app = ENV.fetch('DESTINATION_APP', nil)
|
|
104
109
|
|
|
105
110
|
@max_deliver = 5
|
|
@@ -118,6 +123,7 @@ module JetstreamBridge
|
|
|
118
123
|
@connect_retry_attempts = 3
|
|
119
124
|
@connect_retry_delay = 2
|
|
120
125
|
@lazy_connect = false
|
|
126
|
+
@auto_provision = true
|
|
121
127
|
end
|
|
122
128
|
|
|
123
129
|
# Apply a configuration preset
|
|
@@ -131,34 +137,22 @@ module JetstreamBridge
|
|
|
131
137
|
self
|
|
132
138
|
end
|
|
133
139
|
|
|
134
|
-
# Get the JetStream stream name for this environment.
|
|
135
|
-
#
|
|
136
|
-
# @return [String] Stream name in format "{env}-jetstream-bridge-stream"
|
|
137
|
-
# @example
|
|
138
|
-
# config.env = "production"
|
|
139
|
-
# config.stream_name # => "production-jetstream-bridge-stream"
|
|
140
|
-
def stream_name
|
|
141
|
-
"#{env}-jetstream-bridge-stream"
|
|
142
|
-
end
|
|
143
|
-
|
|
144
140
|
# Get the NATS subject this application publishes to.
|
|
145
141
|
#
|
|
146
|
-
# Producer publishes to: {
|
|
147
|
-
# Consumer subscribes to: {
|
|
142
|
+
# Producer publishes to: {app}.sync.{dest}
|
|
143
|
+
# Consumer subscribes to: {dest}.sync.{app}
|
|
148
144
|
#
|
|
149
145
|
# @return [String] Source subject for publishing
|
|
150
146
|
# @raise [InvalidSubjectError] If components contain NATS wildcards
|
|
151
147
|
# @raise [MissingConfigurationError] If required components empty
|
|
152
148
|
# @example
|
|
153
|
-
# config.env = "production"
|
|
154
149
|
# config.app_name = "api"
|
|
155
150
|
# config.destination_app = "worker"
|
|
156
|
-
# config.source_subject # => "
|
|
151
|
+
# config.source_subject # => "api.sync.worker"
|
|
157
152
|
def source_subject
|
|
158
|
-
validate_subject_component!(env, 'env')
|
|
159
153
|
validate_subject_component!(app_name, 'app_name')
|
|
160
154
|
validate_subject_component!(destination_app, 'destination_app')
|
|
161
|
-
"#{
|
|
155
|
+
"#{app_name}.sync.#{destination_app}"
|
|
162
156
|
end
|
|
163
157
|
|
|
164
158
|
# Get the NATS subject this application subscribes to.
|
|
@@ -166,44 +160,28 @@ module JetstreamBridge
|
|
|
166
160
|
# @return [String] Destination subject for consuming
|
|
167
161
|
# @raise [InvalidSubjectError] If components contain NATS wildcards
|
|
168
162
|
# @raise [MissingConfigurationError] If required components empty
|
|
169
|
-
# @example
|
|
170
|
-
# config.env = "production"
|
|
171
|
-
# config.app_name = "api"
|
|
172
|
-
# config.destination_app = "worker"
|
|
173
|
-
# config.destination_subject # => "production.worker.sync.api"
|
|
174
163
|
def destination_subject
|
|
175
|
-
validate_subject_component!(env, 'env')
|
|
176
164
|
validate_subject_component!(app_name, 'app_name')
|
|
177
165
|
validate_subject_component!(destination_app, 'destination_app')
|
|
178
|
-
"#{
|
|
166
|
+
"#{destination_app}.sync.#{app_name}"
|
|
179
167
|
end
|
|
180
168
|
|
|
181
169
|
# Get the dead letter queue subject for this application.
|
|
182
170
|
#
|
|
183
171
|
# Each app has its own DLQ for better isolation and monitoring.
|
|
184
172
|
#
|
|
185
|
-
# @return [String] DLQ subject in format "{
|
|
173
|
+
# @return [String] DLQ subject in format "{app_name}.sync.dlq"
|
|
186
174
|
# @raise [InvalidSubjectError] If components contain NATS wildcards
|
|
187
175
|
# @raise [MissingConfigurationError] If required components are empty
|
|
188
|
-
# @example
|
|
189
|
-
# config.env = "production"
|
|
190
|
-
# config.app_name = "api"
|
|
191
|
-
# config.dlq_subject # => "production.api.sync.dlq"
|
|
192
176
|
def dlq_subject
|
|
193
|
-
validate_subject_component!(env, 'env')
|
|
194
177
|
validate_subject_component!(app_name, 'app_name')
|
|
195
|
-
"#{
|
|
178
|
+
"#{app_name}.sync.dlq"
|
|
196
179
|
end
|
|
197
180
|
|
|
198
181
|
# Get the durable consumer name for this application.
|
|
199
182
|
#
|
|
200
|
-
# @return [String] Durable name in format "{env}-{app_name}-workers"
|
|
201
|
-
# @example
|
|
202
|
-
# config.env = "production"
|
|
203
|
-
# config.app_name = "api"
|
|
204
|
-
# config.durable_name # => "production-api-workers"
|
|
205
183
|
def durable_name
|
|
206
|
-
"#{
|
|
184
|
+
"#{app_name}-workers"
|
|
207
185
|
end
|
|
208
186
|
|
|
209
187
|
# Validate all configuration settings.
|
|
@@ -219,7 +197,7 @@ module JetstreamBridge
|
|
|
219
197
|
errors = []
|
|
220
198
|
errors << 'destination_app is required' if destination_app.to_s.strip.empty?
|
|
221
199
|
errors << 'nats_urls is required' if nats_urls.to_s.strip.empty?
|
|
222
|
-
errors << '
|
|
200
|
+
errors << 'stream_name is required' if stream_name.to_s.strip.empty?
|
|
223
201
|
errors << 'app_name is required' if app_name.to_s.strip.empty?
|
|
224
202
|
errors << 'max_deliver must be >= 1' if max_deliver.to_i < 1
|
|
225
203
|
errors << 'backoff must be an array' unless backoff.is_a?(Array)
|