jetstream_bridge 4.5.0 → 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 +338 -87
- data/README.md +3 -13
- data/docs/GETTING_STARTED.md +8 -12
- data/docs/PRODUCTION.md +13 -35
- data/docs/RESTRICTED_PERMISSIONS.md +399 -0
- data/docs/TESTING.md +33 -22
- data/lib/generators/jetstream_bridge/health_check/health_check_generator.rb +3 -3
- data/lib/generators/jetstream_bridge/initializer/templates/jetstream_bridge.rb +3 -0
- data/lib/jetstream_bridge/consumer/consumer.rb +100 -39
- data/lib/jetstream_bridge/consumer/message_processor.rb +1 -1
- data/lib/jetstream_bridge/consumer/subscription_manager.rb +97 -121
- data/lib/jetstream_bridge/core/bridge_helpers.rb +127 -0
- data/lib/jetstream_bridge/core/config.rb +32 -161
- data/lib/jetstream_bridge/core/connection.rb +508 -0
- data/lib/jetstream_bridge/core/connection_factory.rb +95 -0
- data/lib/jetstream_bridge/core/debug_helper.rb +2 -9
- data/lib/jetstream_bridge/core.rb +2 -0
- data/lib/jetstream_bridge/models/subject.rb +15 -23
- data/lib/jetstream_bridge/provisioner.rb +67 -0
- data/lib/jetstream_bridge/publisher/publisher.rb +121 -92
- data/lib/jetstream_bridge/rails/integration.rb +5 -8
- data/lib/jetstream_bridge/rails/railtie.rb +3 -4
- data/lib/jetstream_bridge/tasks/install.rake +17 -1
- data/lib/jetstream_bridge/topology/topology.rb +1 -6
- data/lib/jetstream_bridge/version.rb +1 -1
- data/lib/jetstream_bridge.rb +345 -202
- metadata +8 -8
- data/lib/jetstream_bridge/consumer/health_monitor.rb +0 -107
- data/lib/jetstream_bridge/core/connection_manager.rb +0 -513
- data/lib/jetstream_bridge/core/health_checker.rb +0 -184
- data/lib/jetstream_bridge/facade.rb +0 -212
- data/lib/jetstream_bridge/publisher/event_envelope_builder.rb +0 -110
data/lib/jetstream_bridge.rb
CHANGED
|
@@ -2,15 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'jetstream_bridge/version'
|
|
4
4
|
require_relative 'jetstream_bridge/core'
|
|
5
|
-
require_relative 'jetstream_bridge/core/connection_manager'
|
|
6
|
-
require_relative 'jetstream_bridge/publisher/event_envelope_builder'
|
|
7
5
|
require_relative 'jetstream_bridge/publisher/publisher'
|
|
8
6
|
require_relative 'jetstream_bridge/publisher/batch_publisher'
|
|
9
7
|
require_relative 'jetstream_bridge/consumer/consumer'
|
|
10
8
|
require_relative 'jetstream_bridge/consumer/middleware'
|
|
11
9
|
require_relative 'jetstream_bridge/models/publish_result'
|
|
12
10
|
require_relative 'jetstream_bridge/models/event'
|
|
13
|
-
require_relative 'jetstream_bridge/
|
|
11
|
+
require_relative 'jetstream_bridge/provisioner'
|
|
14
12
|
|
|
15
13
|
# Rails-specific entry point (lifecycle helpers + Railtie)
|
|
16
14
|
require_relative 'jetstream_bridge/rails' if defined?(Rails::Railtie)
|
|
@@ -21,19 +19,30 @@ require_relative 'jetstream_bridge/models/outbox_event'
|
|
|
21
19
|
|
|
22
20
|
# JetStream Bridge - Production-safe realtime data bridge using NATS JetStream.
|
|
23
21
|
#
|
|
22
|
+
# JetStream Bridge provides a reliable, production-ready way to publish and consume
|
|
23
|
+
# events using NATS JetStream with features like:
|
|
24
|
+
#
|
|
25
|
+
# - Transactional Outbox pattern for guaranteed event publishing
|
|
26
|
+
# - Idempotent Inbox pattern for exactly-once message processing
|
|
27
|
+
# - Dead Letter Queue (DLQ) for poison message handling
|
|
28
|
+
# - Automatic stream provisioning and overlap detection
|
|
29
|
+
# - Built-in health checks and monitoring
|
|
30
|
+
# - Middleware support for cross-cutting concerns
|
|
31
|
+
# - Rails integration with generators and migrations
|
|
32
|
+
# - Graceful startup/shutdown lifecycle management
|
|
33
|
+
#
|
|
24
34
|
# @example Quick start
|
|
25
35
|
# # Configure
|
|
26
36
|
# JetstreamBridge.configure do |config|
|
|
27
37
|
# config.nats_urls = "nats://localhost:4222"
|
|
28
38
|
# config.app_name = "my_app"
|
|
29
39
|
# config.destination_app = "other_app"
|
|
30
|
-
# config.stream_name = "MY_STREAM"
|
|
31
40
|
# config.use_outbox = true
|
|
32
41
|
# config.use_inbox = true
|
|
33
42
|
# end
|
|
34
43
|
#
|
|
35
|
-
# #
|
|
36
|
-
# JetstreamBridge.
|
|
44
|
+
# # Explicitly start connection (or use Rails railtie for automatic startup)
|
|
45
|
+
# JetstreamBridge.startup!
|
|
37
46
|
#
|
|
38
47
|
# # Publish events
|
|
39
48
|
# JetstreamBridge.publish(
|
|
@@ -48,282 +57,416 @@ require_relative 'jetstream_bridge/models/outbox_event'
|
|
|
48
57
|
# consumer.run!
|
|
49
58
|
#
|
|
50
59
|
# # Graceful shutdown
|
|
51
|
-
# at_exit { JetstreamBridge.
|
|
60
|
+
# at_exit { JetstreamBridge.shutdown! }
|
|
61
|
+
#
|
|
62
|
+
# @see Publisher For publishing events
|
|
63
|
+
# @see Consumer For consuming events
|
|
64
|
+
# @see Config For configuration options
|
|
65
|
+
# @see TestHelpers For testing utilities
|
|
52
66
|
#
|
|
53
67
|
module JetstreamBridge
|
|
54
68
|
class << self
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
# @return [Config] Configuration object
|
|
69
|
+
include Core::BridgeHelpers
|
|
70
|
+
|
|
58
71
|
def config
|
|
59
|
-
|
|
72
|
+
@config ||= Config.new
|
|
60
73
|
end
|
|
61
74
|
|
|
62
75
|
# Configure JetStream Bridge settings
|
|
63
76
|
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
77
|
+
# This method sets configuration WITHOUT automatically establishing a connection.
|
|
78
|
+
# Connection must be established explicitly via startup! or will be established
|
|
79
|
+
# automatically on first use (publish/subscribe) or via Rails railtie initialization.
|
|
66
80
|
#
|
|
67
|
-
# @example
|
|
81
|
+
# @example Basic configuration
|
|
68
82
|
# JetstreamBridge.configure do |config|
|
|
69
83
|
# config.nats_urls = "nats://localhost:4222"
|
|
70
84
|
# config.app_name = "my_app"
|
|
71
85
|
# config.destination_app = "worker"
|
|
72
|
-
# config.stream_name = "MY_STREAM"
|
|
73
86
|
# end
|
|
74
|
-
|
|
75
|
-
|
|
87
|
+
# JetstreamBridge.startup! # Explicitly start connection
|
|
88
|
+
#
|
|
89
|
+
# @example With hash overrides
|
|
90
|
+
# JetstreamBridge.configure(app_name: 'my_app')
|
|
91
|
+
#
|
|
92
|
+
# @param overrides [Hash] Configuration key-value pairs to set
|
|
93
|
+
# @yield [Config] Configuration object for block-based configuration
|
|
94
|
+
# @return [Config] The configured instance
|
|
95
|
+
def configure(overrides = {}, **extra_overrides)
|
|
96
|
+
# Merge extra keyword arguments into overrides hash
|
|
97
|
+
all_overrides = overrides.nil? ? extra_overrides : overrides.merge(extra_overrides)
|
|
98
|
+
|
|
99
|
+
cfg = config
|
|
100
|
+
all_overrides.each { |k, v| assign_config_option!(cfg, k, v) } unless all_overrides.empty?
|
|
101
|
+
yield(cfg) if block_given?
|
|
102
|
+
|
|
103
|
+
cfg
|
|
76
104
|
end
|
|
77
105
|
|
|
78
|
-
#
|
|
106
|
+
# Configure with a preset
|
|
79
107
|
#
|
|
80
|
-
#
|
|
81
|
-
#
|
|
82
|
-
#
|
|
83
|
-
# @return [void]
|
|
84
|
-
# @raise [ConfigurationError] If configuration is invalid
|
|
85
|
-
# @raise [ConnectionError] If unable to connect
|
|
108
|
+
# This method applies a configuration preset. Connection must be
|
|
109
|
+
# established separately via startup! or via Rails railtie.
|
|
86
110
|
#
|
|
87
111
|
# @example
|
|
88
|
-
# JetstreamBridge.
|
|
89
|
-
|
|
90
|
-
|
|
112
|
+
# JetstreamBridge.configure_for(:production) do |config|
|
|
113
|
+
# config.nats_urls = ENV["NATS_URLS"]
|
|
114
|
+
# config.app_name = "my_app"
|
|
115
|
+
# config.destination_app = "worker"
|
|
116
|
+
# end
|
|
117
|
+
# JetstreamBridge.startup! # Explicitly start connection
|
|
118
|
+
#
|
|
119
|
+
# @param preset [Symbol] Preset name (:development, :test, :production, etc.)
|
|
120
|
+
# @yield [Config] Configuration object
|
|
121
|
+
# @return [Config] Configured instance
|
|
122
|
+
def configure_for(preset)
|
|
123
|
+
configure do |cfg|
|
|
124
|
+
cfg.apply_preset(preset)
|
|
125
|
+
yield(cfg) if block_given?
|
|
126
|
+
end
|
|
91
127
|
end
|
|
92
128
|
|
|
93
|
-
|
|
129
|
+
def reset!
|
|
130
|
+
@config = nil
|
|
131
|
+
@connection_initialized = false
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Initialize the JetStream Bridge connection and topology
|
|
94
135
|
#
|
|
95
|
-
#
|
|
136
|
+
# This method can be called explicitly if needed. It's idempotent and safe to call multiple times.
|
|
96
137
|
#
|
|
97
138
|
# @return [void]
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
139
|
+
def startup!
|
|
140
|
+
return if @connection_initialized
|
|
141
|
+
|
|
142
|
+
config.validate!
|
|
143
|
+
connect_and_ensure_stream!
|
|
144
|
+
@connection_initialized = true
|
|
145
|
+
Logging.info('JetStream Bridge started successfully', tag: 'JetstreamBridge')
|
|
103
146
|
end
|
|
104
147
|
|
|
105
|
-
# Reconnect to NATS
|
|
148
|
+
# Reconnect to NATS
|
|
106
149
|
#
|
|
107
|
-
# Useful for:
|
|
150
|
+
# Closes existing connection and establishes a new one. Useful for:
|
|
108
151
|
# - Forking web servers (Puma, Unicorn) after worker boot
|
|
109
152
|
# - Recovering from connection issues
|
|
153
|
+
# - Configuration changes that require reconnection
|
|
110
154
|
#
|
|
111
|
-
# @
|
|
112
|
-
#
|
|
113
|
-
# @example In Puma configuration
|
|
155
|
+
# @example In Puma configuration (config/puma.rb)
|
|
114
156
|
# on_worker_boot do
|
|
115
157
|
# JetstreamBridge.reconnect! if defined?(JetstreamBridge)
|
|
116
158
|
# end
|
|
159
|
+
#
|
|
160
|
+
# @return [void]
|
|
161
|
+
# @raise [ConnectionError] If unable to reconnect to NATS
|
|
117
162
|
def reconnect!
|
|
118
|
-
|
|
163
|
+
Logging.info('Reconnecting to NATS...', tag: 'JetstreamBridge')
|
|
164
|
+
shutdown! if @connection_initialized
|
|
165
|
+
startup!
|
|
119
166
|
end
|
|
120
167
|
|
|
121
|
-
#
|
|
122
|
-
#
|
|
123
|
-
# Simplified API with single pattern:
|
|
124
|
-
# - event_type: required (e.g., "user.created")
|
|
125
|
-
# - payload: required event data
|
|
126
|
-
# - resource_type: optional (inferred from event_type if dotted notation)
|
|
127
|
-
# - All other fields are optional
|
|
168
|
+
# Gracefully shutdown the JetStream Bridge connection
|
|
128
169
|
#
|
|
129
|
-
#
|
|
130
|
-
#
|
|
131
|
-
# @param resource_type [String, nil] Resource type (optional, inferred if nil)
|
|
132
|
-
# @param subject [String, nil] Optional NATS subject override
|
|
133
|
-
# @param options [Hash] Additional options (event_id, occurred_at, trace_id, etc.)
|
|
134
|
-
# @return [Models::PublishResult] Result object with success status and metadata
|
|
170
|
+
# Closes the NATS connection and cleans up resources. Should be called
|
|
171
|
+
# during application shutdown (e.g., in at_exit or signal handlers).
|
|
135
172
|
#
|
|
136
|
-
# @
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
# )
|
|
150
|
-
def publish(event_type:, payload:, **)
|
|
151
|
-
facade.publish(event_type: event_type, payload: payload, **)
|
|
173
|
+
# @return [void]
|
|
174
|
+
def shutdown!
|
|
175
|
+
return unless @connection_initialized
|
|
176
|
+
|
|
177
|
+
begin
|
|
178
|
+
nc = Connection.nc
|
|
179
|
+
nc&.close if nc&.connected?
|
|
180
|
+
Logging.info('JetStream Bridge shut down gracefully', tag: 'JetstreamBridge')
|
|
181
|
+
rescue StandardError => e
|
|
182
|
+
Logging.error("Error during shutdown: #{e.message}", tag: 'JetstreamBridge')
|
|
183
|
+
ensure
|
|
184
|
+
@connection_initialized = false
|
|
185
|
+
end
|
|
152
186
|
end
|
|
153
187
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
def
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
188
|
+
def use_outbox?
|
|
189
|
+
config.use_outbox
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def use_inbox?
|
|
193
|
+
config.use_inbox
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def use_dlq?
|
|
197
|
+
config.use_dlq
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Establishes a connection and ensures stream topology.
|
|
201
|
+
#
|
|
202
|
+
# @return [Object] JetStream context
|
|
203
|
+
def connect_and_ensure_stream!
|
|
204
|
+
config.validate!
|
|
205
|
+
provision = config.auto_provision
|
|
206
|
+
Connection.connect!(verify_js: provision)
|
|
207
|
+
jts = Connection.jetstream
|
|
208
|
+
raise ConnectionNotEstablishedError, 'JetStream connection not available' unless jts
|
|
209
|
+
|
|
210
|
+
if provision
|
|
211
|
+
Provisioner.new(config: config).ensure_stream!(jts: jts)
|
|
212
|
+
else
|
|
213
|
+
Logging.info(
|
|
214
|
+
'auto_provision=false: skipping stream provisioning and JetStream account_info. ' \
|
|
215
|
+
'Run `bundle exec rake jetstream_bridge:provision` with admin credentials to create/update topology.',
|
|
216
|
+
tag: 'JetstreamBridge'
|
|
217
|
+
)
|
|
166
218
|
end
|
|
167
219
|
|
|
168
|
-
|
|
220
|
+
jts
|
|
169
221
|
end
|
|
170
222
|
|
|
171
|
-
#
|
|
172
|
-
#
|
|
173
|
-
# Provides full control over the envelope structure. Use this when you need to
|
|
174
|
-
# manually construct the envelope or preserve all fields from an external source.
|
|
175
|
-
#
|
|
176
|
-
# @param envelope [Hash] Complete event envelope with all required fields
|
|
177
|
-
# @param subject [String, nil] Optional NATS subject override
|
|
178
|
-
# @return [Models::PublishResult] Result object
|
|
223
|
+
# Provision stream/consumer using management credentials (out of band from runtime).
|
|
179
224
|
#
|
|
180
|
-
# @
|
|
181
|
-
#
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
# 'schema_version' => 1,
|
|
186
|
-
# 'event_type' => 'user.created',
|
|
187
|
-
# 'producer' => 'custom-producer',
|
|
188
|
-
# 'resource_type' => 'user',
|
|
189
|
-
# 'resource_id' => '123',
|
|
190
|
-
# 'occurred_at' => Time.now.utc.iso8601,
|
|
191
|
-
# 'trace_id' => 'trace-123',
|
|
192
|
-
# 'payload' => { id: 123, name: 'Alice' }
|
|
193
|
-
# }
|
|
194
|
-
#
|
|
195
|
-
# result = JetstreamBridge.publish_envelope(envelope)
|
|
196
|
-
# puts "Published: #{result.event_id}"
|
|
197
|
-
#
|
|
198
|
-
# @example Forwarding events from another system
|
|
199
|
-
# # Receive event from external system
|
|
200
|
-
# external_event = external_api.get_event
|
|
201
|
-
#
|
|
202
|
-
# # Publish as-is, preserving all metadata
|
|
203
|
-
# JetstreamBridge.publish_envelope(external_event)
|
|
204
|
-
def publish_envelope(envelope, subject: nil)
|
|
205
|
-
facade.publish_envelope(envelope, subject: subject)
|
|
225
|
+
# @param ensure_consumer [Boolean] Whether to create/align the consumer along with the stream.
|
|
226
|
+
# @return [Object] JetStream context
|
|
227
|
+
def provision!(ensure_consumer: true)
|
|
228
|
+
config.validate!
|
|
229
|
+
Provisioner.new(config: config).ensure!(ensure_consumer: ensure_consumer)
|
|
206
230
|
end
|
|
207
231
|
|
|
208
|
-
#
|
|
232
|
+
# Backwards-compatible alias for the previous method name
|
|
233
|
+
def ensure_topology!
|
|
234
|
+
connect_and_ensure_stream!
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Active health check for monitoring and readiness probes
|
|
209
238
|
#
|
|
210
|
-
#
|
|
211
|
-
#
|
|
212
|
-
#
|
|
213
|
-
#
|
|
214
|
-
# @return [Consumer] Consumer instance (call run! to start processing)
|
|
239
|
+
# Performs actual operations to verify system health:
|
|
240
|
+
# - Checks NATS connection (active: calls account_info API)
|
|
241
|
+
# - Verifies stream exists and is accessible (active: queries stream info)
|
|
242
|
+
# - Tests NATS round-trip communication (active: RTT measurement)
|
|
215
243
|
#
|
|
216
|
-
#
|
|
217
|
-
#
|
|
218
|
-
#
|
|
219
|
-
#
|
|
220
|
-
#
|
|
221
|
-
#
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
244
|
+
# Rate Limiting: To prevent abuse, uncached health checks are limited to once every 5 seconds.
|
|
245
|
+
# Cached results (within 30s TTL) bypass this limit via Connection.instance.connected?.
|
|
246
|
+
#
|
|
247
|
+
# @param skip_cache [Boolean] Force fresh health check, bypass connection cache (rate limited)
|
|
248
|
+
# @return [Hash] Health status including NATS connection, stream, and version
|
|
249
|
+
# @raise [HealthCheckFailedError] If skip_cache requested too frequently
|
|
250
|
+
def health_check(skip_cache: false)
|
|
251
|
+
# Rate limit uncached requests to prevent abuse (max 1 per 5 seconds)
|
|
252
|
+
enforce_health_check_rate_limit! if skip_cache
|
|
253
|
+
|
|
254
|
+
start_time = Time.now
|
|
255
|
+
conn_status = connection_snapshot(Connection.instance, skip_cache: skip_cache)
|
|
256
|
+
stream_info = stream_status(conn_status[:connected])
|
|
257
|
+
rtt_ms = measure_nats_rtt if conn_status[:connected]
|
|
258
|
+
health_check_duration_ms = elapsed_ms(start_time)
|
|
259
|
+
|
|
260
|
+
{
|
|
261
|
+
healthy: health_flag(conn_status[:connected], stream_info),
|
|
262
|
+
connection: connection_payload(conn_status),
|
|
263
|
+
stream: stream_info,
|
|
264
|
+
performance: {
|
|
265
|
+
nats_rtt_ms: rtt_ms,
|
|
266
|
+
health_check_duration_ms: health_check_duration_ms
|
|
267
|
+
},
|
|
268
|
+
config: config_summary,
|
|
269
|
+
version: JetstreamBridge::VERSION
|
|
270
|
+
}
|
|
271
|
+
rescue StandardError => e
|
|
272
|
+
{
|
|
273
|
+
healthy: false,
|
|
274
|
+
connection: {
|
|
275
|
+
state: :failed,
|
|
276
|
+
connected: false
|
|
277
|
+
},
|
|
278
|
+
error: "#{e.class}: #{e.message}"
|
|
279
|
+
}
|
|
234
280
|
end
|
|
235
281
|
|
|
236
282
|
# Check if connected to NATS
|
|
237
283
|
#
|
|
238
|
-
# @param skip_cache [Boolean] Force fresh health check
|
|
239
284
|
# @return [Boolean] true if connected and healthy
|
|
285
|
+
def connected?
|
|
286
|
+
Connection.instance.connected?
|
|
287
|
+
rescue StandardError
|
|
288
|
+
false
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Get stream information for the configured stream
|
|
240
292
|
#
|
|
241
|
-
# @
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
# end
|
|
245
|
-
def connected?(skip_cache: false)
|
|
246
|
-
facade.connected?(skip_cache: skip_cache)
|
|
293
|
+
# @return [Hash] Stream information including subjects and message count
|
|
294
|
+
def stream_info
|
|
295
|
+
fetch_stream_info
|
|
247
296
|
end
|
|
248
297
|
|
|
249
|
-
#
|
|
298
|
+
# Convenience method to publish events
|
|
250
299
|
#
|
|
251
|
-
#
|
|
252
|
-
# performance metrics, and configuration. Use this for monitoring and
|
|
253
|
-
# readiness probes.
|
|
300
|
+
# Automatically establishes connection on first use if not already connected.
|
|
254
301
|
#
|
|
255
|
-
#
|
|
302
|
+
# Supports three usage patterns:
|
|
256
303
|
#
|
|
257
|
-
#
|
|
258
|
-
#
|
|
304
|
+
# 1. Structured parameters (recommended):
|
|
305
|
+
# JetstreamBridge.publish(resource_type: 'user', event_type: 'created', payload: { id: 1, name: 'Ada' })
|
|
259
306
|
#
|
|
260
|
-
#
|
|
261
|
-
#
|
|
262
|
-
# puts "Healthy: #{health[:healthy]}"
|
|
263
|
-
# puts "State: #{health[:connection][:state]}"
|
|
264
|
-
# puts "RTT: #{health[:performance][:nats_rtt_ms]}ms"
|
|
307
|
+
# 2. Simplified hash (infers resource_type from event_type):
|
|
308
|
+
# JetstreamBridge.publish(event_type: 'user.created', payload: { id: 1, name: 'Ada' })
|
|
265
309
|
#
|
|
266
|
-
#
|
|
267
|
-
#
|
|
268
|
-
#
|
|
269
|
-
#
|
|
270
|
-
#
|
|
271
|
-
#
|
|
272
|
-
#
|
|
273
|
-
#
|
|
310
|
+
# 3. Complete envelope (advanced):
|
|
311
|
+
# JetstreamBridge.publish({ event_type: 'created', resource_type: 'user', payload: {...}, event_id: '...' })
|
|
312
|
+
#
|
|
313
|
+
# @param event_or_hash [Hash, nil] Event hash or first positional argument
|
|
314
|
+
# @param resource_type [String, nil] Resource type (e.g., 'user', 'order')
|
|
315
|
+
# @param event_type [String, nil] Event type (e.g., 'created', 'updated', 'user.created')
|
|
316
|
+
# @param payload [Hash, nil] Event payload data
|
|
317
|
+
# @param subject [String, nil] Optional subject override
|
|
318
|
+
# @param options [Hash] Additional options (event_id, occurred_at, trace_id)
|
|
319
|
+
# @return [Models::PublishResult] Result object with success status and metadata
|
|
320
|
+
#
|
|
321
|
+
# @example Check result status
|
|
322
|
+
# result = JetstreamBridge.publish(event_type: "user.created", payload: { id: 1 })
|
|
323
|
+
# if result.success?
|
|
324
|
+
# puts "Published event #{result.event_id}"
|
|
325
|
+
# else
|
|
326
|
+
# logger.error("Publish failed: #{result.error}")
|
|
274
327
|
# end
|
|
275
|
-
def
|
|
276
|
-
|
|
328
|
+
def publish(event_or_hash = nil, resource_type: nil, event_type: nil, payload: nil, subject: nil, **)
|
|
329
|
+
connect_if_needed!
|
|
330
|
+
publisher = Publisher.new
|
|
331
|
+
publisher.publish(event_or_hash, resource_type: resource_type, event_type: event_type, payload: payload,
|
|
332
|
+
subject: subject, **)
|
|
277
333
|
end
|
|
278
334
|
|
|
279
|
-
#
|
|
335
|
+
# Publish variant that raises on error
|
|
280
336
|
#
|
|
281
|
-
# @
|
|
282
|
-
#
|
|
283
|
-
#
|
|
284
|
-
|
|
285
|
-
|
|
337
|
+
# @example
|
|
338
|
+
# JetstreamBridge.publish!(event_type: "user.created", payload: { id: 1 })
|
|
339
|
+
# # Raises PublishError if publishing fails
|
|
340
|
+
#
|
|
341
|
+
# @param (see #publish)
|
|
342
|
+
# @return [Models::PublishResult] Result object
|
|
343
|
+
# @raise [PublishError] If publishing fails
|
|
344
|
+
def publish!(...)
|
|
345
|
+
result = publish(...)
|
|
346
|
+
if result.failure?
|
|
347
|
+
raise PublishError.new(result.error&.message, event_id: result.event_id,
|
|
348
|
+
subject: result.subject)
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
result
|
|
286
352
|
end
|
|
287
353
|
|
|
288
|
-
#
|
|
289
|
-
#
|
|
290
|
-
# Returns a simple boolean indicating overall health.
|
|
291
|
-
# Equivalent to `health[:healthy]` but more convenient.
|
|
292
|
-
#
|
|
293
|
-
# @return [Boolean] true if connected and stream exists
|
|
354
|
+
# Batch publish multiple events efficiently
|
|
294
355
|
#
|
|
295
356
|
# @example
|
|
296
|
-
#
|
|
297
|
-
#
|
|
298
|
-
#
|
|
299
|
-
#
|
|
357
|
+
# results = JetstreamBridge.publish_batch do |batch|
|
|
358
|
+
# users.each do |user|
|
|
359
|
+
# batch.add(event_type: "user.created", payload: { id: user.id })
|
|
360
|
+
# end
|
|
300
361
|
# end
|
|
301
|
-
|
|
302
|
-
|
|
362
|
+
# puts "Success: #{results.successful_count}, Failed: #{results.failed_count}"
|
|
363
|
+
#
|
|
364
|
+
# @yield [BatchPublisher] Batch publisher instance
|
|
365
|
+
# @return [BatchPublisher::BatchResult] Result with success/failure counts
|
|
366
|
+
def publish_batch
|
|
367
|
+
batch = BatchPublisher.new
|
|
368
|
+
yield(batch) if block_given?
|
|
369
|
+
batch.publish
|
|
303
370
|
end
|
|
304
371
|
|
|
305
|
-
#
|
|
372
|
+
# Convenience method to start consuming messages
|
|
306
373
|
#
|
|
307
|
-
#
|
|
374
|
+
# Automatically establishes connection on first use if not already connected.
|
|
308
375
|
#
|
|
309
|
-
#
|
|
310
|
-
# info = JetstreamBridge.stream_info
|
|
311
|
-
# puts "Messages: #{info[:messages]}"
|
|
312
|
-
def stream_info
|
|
313
|
-
facade.stream_info
|
|
314
|
-
end
|
|
315
|
-
|
|
316
|
-
# Reset facade (for testing)
|
|
376
|
+
# Supports two usage patterns:
|
|
317
377
|
#
|
|
318
|
-
#
|
|
319
|
-
|
|
320
|
-
|
|
378
|
+
# 1. With a block (recommended):
|
|
379
|
+
# consumer = JetstreamBridge.subscribe do |event|
|
|
380
|
+
# puts "Received: #{event.type} on #{event.subject} (attempt #{event.deliveries})"
|
|
381
|
+
# end
|
|
382
|
+
# consumer.run!
|
|
383
|
+
#
|
|
384
|
+
# 2. With auto-run (returns Thread):
|
|
385
|
+
# thread = JetstreamBridge.subscribe(run: true) do |event|
|
|
386
|
+
# puts "Received: #{event.type}"
|
|
387
|
+
# end
|
|
388
|
+
# thread.join # Wait for consumer to finish
|
|
389
|
+
#
|
|
390
|
+
# 3. With a handler object:
|
|
391
|
+
# handler = ->(event) { puts event.type }
|
|
392
|
+
# consumer = JetstreamBridge.subscribe(handler)
|
|
393
|
+
# consumer.run!
|
|
394
|
+
#
|
|
395
|
+
# @param handler [Proc, #call, nil] Message handler (optional if block given)
|
|
396
|
+
# @param run [Boolean] If true, automatically runs consumer in a background thread
|
|
397
|
+
# @param durable_name [String, nil] Optional durable consumer name override
|
|
398
|
+
# @param batch_size [Integer, nil] Optional batch size override
|
|
399
|
+
# @yield [event] Yields Models::Event object to block
|
|
400
|
+
# @return [Consumer, Thread] Consumer instance or Thread if run: true
|
|
401
|
+
def subscribe(handler = nil, run: false, durable_name: nil, batch_size: nil, &block)
|
|
402
|
+
connect_if_needed!
|
|
403
|
+
handler ||= block
|
|
404
|
+
raise ArgumentError, 'Handler or block required' unless handler
|
|
405
|
+
|
|
406
|
+
consumer = Consumer.new(handler, durable_name: durable_name, batch_size: batch_size)
|
|
407
|
+
|
|
408
|
+
if run
|
|
409
|
+
thread = Thread.new { consumer.run! }
|
|
410
|
+
thread.abort_on_exception = true
|
|
411
|
+
thread
|
|
412
|
+
else
|
|
413
|
+
consumer
|
|
414
|
+
end
|
|
321
415
|
end
|
|
322
416
|
|
|
323
417
|
private
|
|
324
418
|
|
|
325
|
-
def
|
|
326
|
-
|
|
419
|
+
def connection_snapshot(conn_instance, skip_cache:)
|
|
420
|
+
{
|
|
421
|
+
connected: conn_instance.connected?(skip_cache: skip_cache),
|
|
422
|
+
connected_at: conn_instance.connected_at,
|
|
423
|
+
state: conn_instance.state,
|
|
424
|
+
last_error: conn_instance.last_reconnect_error,
|
|
425
|
+
last_error_at: conn_instance.last_reconnect_error_at
|
|
426
|
+
}
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def stream_status(connected)
|
|
430
|
+
return stream_missing unless connected
|
|
431
|
+
return skipped_stream_info unless config.auto_provision
|
|
432
|
+
|
|
433
|
+
fetch_stream_info
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def stream_missing
|
|
437
|
+
{ exists: false, name: config.stream_name }
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def health_flag(connected, stream_info)
|
|
441
|
+
return connected unless config.auto_provision
|
|
442
|
+
|
|
443
|
+
connected && stream_info&.fetch(:exists, false)
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def connection_payload(status)
|
|
447
|
+
{
|
|
448
|
+
state: status[:state],
|
|
449
|
+
connected: status[:connected],
|
|
450
|
+
connected_at: status[:connected_at]&.iso8601,
|
|
451
|
+
last_error: status[:last_error]&.message,
|
|
452
|
+
last_error_at: status[:last_error_at]&.iso8601
|
|
453
|
+
}
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def config_summary
|
|
457
|
+
{
|
|
458
|
+
app_name: config.app_name,
|
|
459
|
+
destination_app: config.destination_app,
|
|
460
|
+
stream_name: config.stream_name,
|
|
461
|
+
auto_provision: config.auto_provision,
|
|
462
|
+
use_outbox: config.use_outbox,
|
|
463
|
+
use_inbox: config.use_inbox,
|
|
464
|
+
use_dlq: config.use_dlq
|
|
465
|
+
}
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def elapsed_ms(start_time)
|
|
469
|
+
((Time.now - start_time) * 1000).round(2)
|
|
327
470
|
end
|
|
328
471
|
end
|
|
329
472
|
end
|