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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +37 -0
- data/README.md +6 -3
- data/docs/API.md +395 -0
- data/docs/ARCHITECTURE.md +66 -4
- data/docs/GETTING_STARTED.md +72 -1
- data/docs/PRODUCTION.md +1 -1
- data/docs/RESTRICTED_PERMISSIONS.md +1 -1
- data/docs/TESTING.md +3 -3
- data/lib/generators/jetstream_bridge/migrations/templates/create_jetstream_inbox_events.rb.erb +29 -13
- data/lib/jetstream_bridge/config_helpers.rb +122 -0
- data/lib/jetstream_bridge/consumer/consumer.rb +56 -27
- data/lib/jetstream_bridge/consumer/consumer_state.rb +58 -0
- data/lib/jetstream_bridge/consumer/inbox/inbox_repository.rb +12 -2
- data/lib/jetstream_bridge/consumer/pull_subscription_builder.rb +6 -6
- data/lib/jetstream_bridge/consumer/subscription_manager.rb +44 -106
- data/lib/jetstream_bridge/core/connection.rb +56 -10
- data/lib/jetstream_bridge/core/duration.rb +30 -0
- data/lib/jetstream_bridge/models/inbox_event.rb +1 -1
- data/lib/jetstream_bridge/models/outbox_event.rb +1 -1
- data/lib/jetstream_bridge/provisioner.rb +69 -13
- data/lib/jetstream_bridge/publisher/outbox_repository.rb +35 -20
- data/lib/jetstream_bridge/publisher/publisher.rb +4 -4
- data/lib/jetstream_bridge/tasks/install.rake +2 -2
- data/lib/jetstream_bridge/topology/stream.rb +6 -1
- data/lib/jetstream_bridge/topology/topology.rb +1 -1
- data/lib/jetstream_bridge/version.rb +1 -1
- data/lib/jetstream_bridge.rb +7 -12
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 471b7dd68739c9de058cbaeedb800dbb5c0e1eadaa2d54602eee34d8469f2668
|
|
4
|
+
data.tar.gz: 7d16630ce0e83b6d6fb1ab6f9e6c5151a6aa5df7467810f950fd984e035bb9f0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d58a47de952678819cd5681b750907ca9d56be3bf9799d5e6f614acd828c9d86da80c7be1006280cbfa42c88ebe8fd6a804da829b5f73672dd59766955df1d16
|
|
7
|
+
data.tar.gz: f963140e8a639fc285b633f112df806ff3638e3d83f1b834702d91a47d4a2f28c704071c8ae2f3d763c6d750aa708e12babe33872dbb3778d53058965cd987e0
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,43 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [7.0.0] - 2026-01-30
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Full Rails reference examples (non-restrictive and restrictive) with Docker Compose, provisioner service, and end-to-end test scripts.
|
|
13
|
+
- Architecture and API reference docs covering topology, consumer modes, and public surface.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Major refactor separating provisioning from runtime to support least-privilege deployments; `auto_provision=false` now avoids JetStream management APIs at runtime.
|
|
18
|
+
- Config helpers simplified bidirectional setup (`configure_bidirectional`, `setup_rails_lifecycle`) replacing verbose initializer boilerplate.
|
|
19
|
+
- Consumer pipeline hardened: push-consumer mode, safer signal handling, improved drain behavior, and pull subscription shim reliability.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Provisioner keyword argument handling, generator load edge cases, and connection/consumer recovery during JetStream context refresh.
|
|
24
|
+
|
|
25
|
+
## [5.0.0] - 2026-01-30
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- **Push consumer mode** for restricted NATS credentials — `consumer_mode: :push` with optional `delivery_subject`, no `$JS.API.*` permissions required, documented in the restricted permissions guide.
|
|
30
|
+
- **Reference examples**: full Rails apps for non-restrictive (auto-provisioning) and restrictive (least-privilege + provisioner) deployments with Docker Compose and end-to-end test scripts.
|
|
31
|
+
- **ConfigHelpers & docs**: helper to configure bidirectional sync in one call, Rails lifecycle helper, new Architecture/API docs describing topology and public surface.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- **Provisioning flow** separated from runtime: when `auto_provision=false` runtime skips JetStream management APIs; provisioning handled via rake task/CLI with admin creds. Health/test_connection honor restricted mode.
|
|
36
|
+
- **Consumer reliability**: refactored subscription builder, trap-safe signal handling, push-consumer drain via `next_msg`, sturdier pull subscription shim, JetStream context refresh retries after reconnects.
|
|
37
|
+
- **Quality & coverage**: generator template tweaks, provisioner keyword fixes, and test suite expanded (~96% coverage) across provisioning, consumer, and connection lifecycle paths.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- Fixed TypeError/timeout handling in push consumer drain and process loop.
|
|
42
|
+
- Fixed Rails generator load edge case and provisioner keyword argument bugs.
|
|
43
|
+
- Guarded JetStream API calls when constants are private, preserving connection state during JetStream context refresh failures.
|
|
44
|
+
|
|
8
45
|
## [4.4.1] - 2026-01-16
|
|
9
46
|
|
|
10
47
|
### Fixed
|
data/README.md
CHANGED
|
@@ -20,21 +20,22 @@
|
|
|
20
20
|
</a>
|
|
21
21
|
</p>
|
|
22
22
|
|
|
23
|
-
Production-ready NATS JetStream bridge for Ruby/Rails with outbox, inbox, DLQ, and overlap-safe stream provisioning.
|
|
23
|
+
Production-ready NATS JetStream bridge for Ruby/Rails with outbox, inbox, DLQ, and overlap-safe stream provisioning.
|
|
24
24
|
|
|
25
25
|
## Highlights
|
|
26
26
|
|
|
27
27
|
- Transactional outbox and idempotent inbox (optional) for exactly-once pipelines.
|
|
28
|
-
- Durable pull consumers with retries, backoff, and DLQ routing.
|
|
28
|
+
- Durable pull (default) or push consumers with retries, backoff, and DLQ routing.
|
|
29
29
|
- Auto stream/consumer provisioning with overlap protection.
|
|
30
30
|
- Rails-native: generators, migrations, health check, and eager-loading safety.
|
|
31
|
+
- Least-privilege friendly: run with `auto_provision=false` plus pre-created consumers.
|
|
31
32
|
- Mock NATS for fast, no-infra testing.
|
|
32
33
|
|
|
33
34
|
## Quick Start
|
|
34
35
|
|
|
35
36
|
```ruby
|
|
36
37
|
# Gemfile
|
|
37
|
-
gem "jetstream_bridge", "~>
|
|
38
|
+
gem "jetstream_bridge", "~> 7.0"
|
|
38
39
|
```
|
|
39
40
|
|
|
40
41
|
```bash
|
|
@@ -63,10 +64,12 @@ consumer.run!
|
|
|
63
64
|
## Documentation
|
|
64
65
|
|
|
65
66
|
- [Getting Started](docs/GETTING_STARTED.md) - Setup, configuration, and basic usage
|
|
67
|
+
- [API Reference](docs/API.md) - Complete API documentation for all public methods
|
|
66
68
|
- [Architecture & Topology](docs/ARCHITECTURE.md) - Internal architecture, message flow, and patterns
|
|
67
69
|
- [Production Guide](docs/PRODUCTION.md) - Production deployment and monitoring
|
|
68
70
|
- [Restricted Permissions & Provisioning](docs/RESTRICTED_PERMISSIONS.md) - Manual provisioning and security
|
|
69
71
|
- [Testing with Mock NATS](docs/TESTING.md) - Fast, no-infra testing
|
|
72
|
+
- [Reference Examples (Rails 7)](examples/README.md) - Non-restrictive and restrictive Dockerized examples with E2E tests
|
|
70
73
|
|
|
71
74
|
## License
|
|
72
75
|
|
data/docs/API.md
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
# API Reference
|
|
2
|
+
|
|
3
|
+
Complete reference for JetstreamBridge public API.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Configuration](#configuration)
|
|
8
|
+
- [Lifecycle Methods](#lifecycle-methods)
|
|
9
|
+
- [Publishing](#publishing)
|
|
10
|
+
- [Consuming](#consuming)
|
|
11
|
+
- [Provisioning](#provisioning)
|
|
12
|
+
- [Health & Diagnostics](#health--diagnostics)
|
|
13
|
+
- [Models](#models)
|
|
14
|
+
|
|
15
|
+
## Configuration
|
|
16
|
+
|
|
17
|
+
### `JetstreamBridge.configure`
|
|
18
|
+
|
|
19
|
+
Configure the library. Must be called before connecting.
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
JetstreamBridge.configure do |config|
|
|
23
|
+
# Required
|
|
24
|
+
config.app_name = "my_app"
|
|
25
|
+
config.destination_app = "other_app"
|
|
26
|
+
|
|
27
|
+
# Connection
|
|
28
|
+
config.nats_urls = "nats://localhost:4222" # or array of URLs
|
|
29
|
+
config.stream_name = "jetstream-bridge-stream"
|
|
30
|
+
|
|
31
|
+
# Features
|
|
32
|
+
config.use_outbox = true # Transactional publish (requires ActiveRecord)
|
|
33
|
+
config.use_inbox = true # Idempotent consume (requires ActiveRecord)
|
|
34
|
+
config.use_dlq = true # Dead letter queue for poison messages
|
|
35
|
+
|
|
36
|
+
# Consumer settings
|
|
37
|
+
config.durable_name = "#{app_name}-workers"
|
|
38
|
+
config.max_deliver = 5 # Max delivery attempts
|
|
39
|
+
config.ack_wait = "30s" # Time to wait for ACK
|
|
40
|
+
config.backoff = ["1s", "5s", "15s", "30s", "60s"]
|
|
41
|
+
|
|
42
|
+
# Provisioning
|
|
43
|
+
config.auto_provision = true # Auto-create stream/consumer on startup
|
|
44
|
+
|
|
45
|
+
# Connection behavior
|
|
46
|
+
config.lazy_connect = false # Set true to skip autostart
|
|
47
|
+
config.connect_retry_attempts = 3
|
|
48
|
+
config.connect_retry_delay = 1 # seconds
|
|
49
|
+
end
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### `JetstreamBridge.config`
|
|
53
|
+
|
|
54
|
+
Returns the current configuration object (read-only).
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
stream_name = JetstreamBridge.config.stream_name
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Lifecycle Methods
|
|
61
|
+
|
|
62
|
+
### `JetstreamBridge.startup!`
|
|
63
|
+
|
|
64
|
+
Explicitly start the connection and provision topology (if `auto_provision=true`).
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
JetstreamBridge.startup!
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Note:** Rails applications auto-start after initialization. Non-Rails apps should call this manually or rely on auto-connect on first publish/subscribe.
|
|
71
|
+
|
|
72
|
+
### `JetstreamBridge.shutdown!`
|
|
73
|
+
|
|
74
|
+
Gracefully close the NATS connection.
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
JetstreamBridge.shutdown!
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### `JetstreamBridge.reset!`
|
|
81
|
+
|
|
82
|
+
Reset all internal state (for testing).
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
JetstreamBridge.reset!
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Publishing
|
|
89
|
+
|
|
90
|
+
### `JetstreamBridge.publish`
|
|
91
|
+
|
|
92
|
+
Publish an event to the destination app.
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
JetstreamBridge.publish(
|
|
96
|
+
event_type: "user.created", # Required
|
|
97
|
+
resource_type: "user", # Required
|
|
98
|
+
resource_id: user.id, # Optional
|
|
99
|
+
payload: { id: user.id, email: user.email },
|
|
100
|
+
headers: { correlation_id: "..." }, # Optional
|
|
101
|
+
event_id: "custom-uuid" # Optional (auto-generated)
|
|
102
|
+
)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Returns:** `JetstreamBridge::PublishResult`
|
|
106
|
+
|
|
107
|
+
**With Outbox:**
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
# Transactional publish (commits with your DB transaction)
|
|
111
|
+
User.transaction do
|
|
112
|
+
user.save!
|
|
113
|
+
JetstreamBridge.publish(event_type: "user.created", resource_type: "user", payload: user)
|
|
114
|
+
end
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### `JetstreamBridge.publish!`
|
|
118
|
+
|
|
119
|
+
Like `publish` but raises on error.
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
JetstreamBridge.publish!(event_type: "user.created", resource_type: "user", payload: data)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Raises:** `JetstreamBridge::PublishError` on failure
|
|
126
|
+
|
|
127
|
+
### `JetstreamBridge.publish_batch`
|
|
128
|
+
|
|
129
|
+
Publish multiple events efficiently.
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
results = JetstreamBridge.publish_batch do |batch|
|
|
133
|
+
users.each do |user|
|
|
134
|
+
batch.publish(event_type: "user.created", resource_type: "user", payload: user)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
puts "Published: #{results.success_count}, Failed: #{results.failure_count}"
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Consuming
|
|
142
|
+
|
|
143
|
+
### `JetstreamBridge::Consumer.new`
|
|
144
|
+
|
|
145
|
+
Create a consumer to process incoming events.
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
consumer = JetstreamBridge::Consumer.new do |event|
|
|
149
|
+
# Process event
|
|
150
|
+
User.upsert({ id: event.payload["id"], email: event.payload["email"] })
|
|
151
|
+
end
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Options:**
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
consumer = JetstreamBridge::Consumer.new(
|
|
158
|
+
batch_size: 10, # Process up to 10 messages at once
|
|
159
|
+
error_handler: ->(error, event) { logger.error(error) }
|
|
160
|
+
) do |event|
|
|
161
|
+
# ...
|
|
162
|
+
end
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### `Consumer#run!`
|
|
166
|
+
|
|
167
|
+
Start consuming messages (blocks until interrupted).
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
consumer.run!
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### `Consumer#stop!`
|
|
174
|
+
|
|
175
|
+
Gracefully stop the consumer.
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
consumer.stop!
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Event Object
|
|
182
|
+
|
|
183
|
+
The event object passed to your handler:
|
|
184
|
+
|
|
185
|
+
```ruby
|
|
186
|
+
event.event_id # => "evt_123"
|
|
187
|
+
event.event_type # => "user.created"
|
|
188
|
+
event.resource_type # => "user"
|
|
189
|
+
event.resource_id # => "456"
|
|
190
|
+
event.payload # => { "id" => 456, "email" => "..." }
|
|
191
|
+
event.headers # => { "correlation_id" => "..." }
|
|
192
|
+
event.subject # => "source_app.sync.my_app"
|
|
193
|
+
event.stream # => "jetstream-bridge-stream"
|
|
194
|
+
event.seq # => 123
|
|
195
|
+
event.deliveries # => 1
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Provisioning
|
|
199
|
+
|
|
200
|
+
### `JetstreamBridge.provision!`
|
|
201
|
+
|
|
202
|
+
Manually provision stream and consumer.
|
|
203
|
+
|
|
204
|
+
```ruby
|
|
205
|
+
# Provision both stream and consumer
|
|
206
|
+
JetstreamBridge.provision!
|
|
207
|
+
|
|
208
|
+
# Provision stream only
|
|
209
|
+
JetstreamBridge.provision!(provision_consumer: false)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### `JetstreamBridge::Provisioner`
|
|
213
|
+
|
|
214
|
+
Dedicated provisioning class for advanced use cases.
|
|
215
|
+
|
|
216
|
+
```ruby
|
|
217
|
+
provisioner = JetstreamBridge::Provisioner.new
|
|
218
|
+
|
|
219
|
+
# Provision everything
|
|
220
|
+
provisioner.provision!
|
|
221
|
+
|
|
222
|
+
# Or separately
|
|
223
|
+
provisioner.provision_stream!
|
|
224
|
+
provisioner.provision_consumer!
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Health & Diagnostics
|
|
228
|
+
|
|
229
|
+
### `JetstreamBridge.health_check`
|
|
230
|
+
|
|
231
|
+
Get comprehensive health status.
|
|
232
|
+
|
|
233
|
+
```ruby
|
|
234
|
+
health = JetstreamBridge.health_check
|
|
235
|
+
|
|
236
|
+
health[:status] # => "healthy" | "unhealthy"
|
|
237
|
+
health[:connected] # => true/false
|
|
238
|
+
health[:stream_exists] # => true/false
|
|
239
|
+
health[:messages] # => 123
|
|
240
|
+
health[:consumers] # => 2
|
|
241
|
+
health[:nats_rtt_ms] # => 1.2
|
|
242
|
+
health[:version] # => "7.0.0"
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### `JetstreamBridge.stream_info`
|
|
246
|
+
|
|
247
|
+
Get detailed stream information.
|
|
248
|
+
|
|
249
|
+
```ruby
|
|
250
|
+
info = JetstreamBridge.stream_info
|
|
251
|
+
|
|
252
|
+
info[:name] # => "jetstream-bridge-stream"
|
|
253
|
+
info[:subjects] # => ["app1.sync.app2", ...]
|
|
254
|
+
info[:messages] # => 1000
|
|
255
|
+
info[:bytes] # => 204800
|
|
256
|
+
info[:first_seq] # => 1
|
|
257
|
+
info[:last_seq] # => 1000
|
|
258
|
+
info[:consumer_count] # => 2
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### `JetstreamBridge.connection_info`
|
|
262
|
+
|
|
263
|
+
Get NATS connection details.
|
|
264
|
+
|
|
265
|
+
```ruby
|
|
266
|
+
info = JetstreamBridge.connection_info
|
|
267
|
+
|
|
268
|
+
info[:connected] # => true
|
|
269
|
+
info[:servers] # => ["nats://localhost:4222"]
|
|
270
|
+
info[:connected_at] # => 2024-01-29 12:00:00 UTC
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## Models
|
|
274
|
+
|
|
275
|
+
### `JetstreamBridge::OutboxEvent`
|
|
276
|
+
|
|
277
|
+
ActiveRecord model for outbox events (when `use_outbox=true`).
|
|
278
|
+
|
|
279
|
+
```ruby
|
|
280
|
+
# Create outbox event
|
|
281
|
+
event = JetstreamBridge::OutboxEvent.create!(
|
|
282
|
+
event_id: SecureRandom.uuid,
|
|
283
|
+
event_type: "user.created",
|
|
284
|
+
resource_type: "user",
|
|
285
|
+
resource_id: "123",
|
|
286
|
+
payload: { id: 123, email: "user@example.com" },
|
|
287
|
+
subject: "my_app.sync.other_app",
|
|
288
|
+
status: "pending"
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
# Query
|
|
292
|
+
JetstreamBridge::OutboxEvent.pending.limit(100)
|
|
293
|
+
JetstreamBridge::OutboxEvent.failed
|
|
294
|
+
|
|
295
|
+
# Mark as published
|
|
296
|
+
event.mark_published!
|
|
297
|
+
|
|
298
|
+
# Cleanup old events
|
|
299
|
+
JetstreamBridge::OutboxEvent.cleanup_published(older_than: 7.days)
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### `JetstreamBridge::InboxEvent`
|
|
303
|
+
|
|
304
|
+
ActiveRecord model for inbox events (when `use_inbox=true`).
|
|
305
|
+
|
|
306
|
+
```ruby
|
|
307
|
+
# Find by event_id
|
|
308
|
+
event = JetstreamBridge::InboxEvent.find_by(event_id: "evt_123")
|
|
309
|
+
|
|
310
|
+
# Query
|
|
311
|
+
JetstreamBridge::InboxEvent.received
|
|
312
|
+
JetstreamBridge::InboxEvent.processing
|
|
313
|
+
JetstreamBridge::InboxEvent.processed
|
|
314
|
+
JetstreamBridge::InboxEvent.failed
|
|
315
|
+
JetstreamBridge::InboxEvent.recent(100)
|
|
316
|
+
|
|
317
|
+
# Mark as processed
|
|
318
|
+
event.mark_processed!
|
|
319
|
+
|
|
320
|
+
# Mark as failed
|
|
321
|
+
event.mark_failed!("Error message")
|
|
322
|
+
|
|
323
|
+
# Cleanup old events
|
|
324
|
+
JetstreamBridge::InboxEvent.cleanup_processed(older_than: 30.days)
|
|
325
|
+
|
|
326
|
+
# Statistics
|
|
327
|
+
stats = JetstreamBridge::InboxEvent.processing_stats
|
|
328
|
+
stats[:total] # => 1000
|
|
329
|
+
stats[:processed] # => 950
|
|
330
|
+
stats[:failed] # => 30
|
|
331
|
+
stats[:pending] # => 20
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
## Error Handling
|
|
335
|
+
|
|
336
|
+
### `JetstreamBridge::PublishError`
|
|
337
|
+
|
|
338
|
+
Raised by `publish!` when publishing fails.
|
|
339
|
+
|
|
340
|
+
```ruby
|
|
341
|
+
begin
|
|
342
|
+
JetstreamBridge.publish!(event_type: "test", resource_type: "test", payload: {})
|
|
343
|
+
rescue JetstreamBridge::PublishError => e
|
|
344
|
+
logger.error("Publish failed: #{e.message}")
|
|
345
|
+
logger.error("Event ID: #{e.event_id}")
|
|
346
|
+
logger.error("Subject: #{e.subject}")
|
|
347
|
+
end
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### Custom Error Handler
|
|
351
|
+
|
|
352
|
+
```ruby
|
|
353
|
+
consumer = JetstreamBridge::Consumer.new(
|
|
354
|
+
error_handler: lambda { |error, event|
|
|
355
|
+
logger.error("Failed to process event #{event.event_id}: #{error.message}")
|
|
356
|
+
Sentry.capture_exception(error, extra: { event_id: event.event_id })
|
|
357
|
+
}
|
|
358
|
+
) do |event|
|
|
359
|
+
# Process event
|
|
360
|
+
end
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
## Testing
|
|
364
|
+
|
|
365
|
+
### Test Mode
|
|
366
|
+
|
|
367
|
+
Enable mock NATS for testing without infrastructure.
|
|
368
|
+
|
|
369
|
+
```ruby
|
|
370
|
+
# RSpec
|
|
371
|
+
RSpec.configure do |config|
|
|
372
|
+
config.before(:each, :jetstream) do
|
|
373
|
+
JetstreamBridge::TestHelpers.enable_test_mode!
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
config.after(:each, :jetstream) do
|
|
377
|
+
JetstreamBridge::TestHelpers.reset_test_mode!
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
# Test
|
|
382
|
+
it "publishes events", :jetstream do
|
|
383
|
+
result = JetstreamBridge.publish(event_type: "test", resource_type: "test", payload: {})
|
|
384
|
+
expect(result).to be_success
|
|
385
|
+
end
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
See [TESTING.md](TESTING.md) for comprehensive testing documentation.
|
|
389
|
+
|
|
390
|
+
## See Also
|
|
391
|
+
|
|
392
|
+
- [Getting Started](GETTING_STARTED.md) - Setup and basic usage
|
|
393
|
+
- [Architecture](ARCHITECTURE.md) - Internal architecture and patterns
|
|
394
|
+
- [Production Guide](PRODUCTION.md) - Production deployment
|
|
395
|
+
- [Testing Guide](TESTING.md) - Testing with Mock NATS
|
data/docs/ARCHITECTURE.md
CHANGED
|
@@ -331,7 +331,7 @@ Overlap detection ensures messages route to exactly one stream.
|
|
|
331
331
|
▼
|
|
332
332
|
┌──────────────────────────────────────────────────────────────┐
|
|
333
333
|
│ 3. [OPTIONAL] Outbox pattern │
|
|
334
|
-
│ - OutboxRepository.
|
|
334
|
+
│ - OutboxRepository.record_publish_attempt() │
|
|
335
335
|
│ - State: "publishing" │
|
|
336
336
|
│ - Database transaction commits │
|
|
337
337
|
└──────────────────────┬───────────────────────────────────────┘
|
|
@@ -347,8 +347,8 @@ Overlap detection ensures messages route to exactly one stream.
|
|
|
347
347
|
▼
|
|
348
348
|
┌──────────────────────────────────────────────────────────────┐
|
|
349
349
|
│ 5. [OPTIONAL] Outbox update │
|
|
350
|
-
│ - Success: OutboxRepository.
|
|
351
|
-
│ - Failure: OutboxRepository.
|
|
350
|
+
│ - Success: OutboxRepository.record_publish_success() │
|
|
351
|
+
│ - Failure: OutboxRepository.record_publish_failure() │
|
|
352
352
|
└──────────────────────┬───────────────────────────────────────┘
|
|
353
353
|
│
|
|
354
354
|
▼
|
|
@@ -437,7 +437,7 @@ Overlap detection ensures messages route to exactly one stream.
|
|
|
437
437
|
|
|
438
438
|
### Outbox Pattern (Publisher Side)
|
|
439
439
|
|
|
440
|
-
**Purpose:** Guarantee at-
|
|
440
|
+
**Purpose:** Guarantee at-most-once delivery by persisting events to database before publishing.
|
|
441
441
|
|
|
442
442
|
**Configuration:**
|
|
443
443
|
|
|
@@ -486,6 +486,50 @@ config.inbox_model = 'JetstreamBridge::InboxEvent'
|
|
|
486
486
|
- `processed` - Successfully processed
|
|
487
487
|
- `failed` - Failed processing
|
|
488
488
|
|
|
489
|
+
**Schema Requirements:**
|
|
490
|
+
|
|
491
|
+
Generate the inbox events table migration:
|
|
492
|
+
|
|
493
|
+
```bash
|
|
494
|
+
rails generate jetstream_bridge:migration
|
|
495
|
+
rails db:migrate
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
**Required Fields:**
|
|
499
|
+
|
|
500
|
+
| Field | Type | Nullable | Description |
|
|
501
|
+
| ----- | ---- | -------- | ----------- |
|
|
502
|
+
| `event_id` | string | NO | Unique event identifier for deduplication |
|
|
503
|
+
| `event_type` | string | NO | Type of event (e.g., 'created', 'updated') |
|
|
504
|
+
| `payload` | text | NO | Full event payload as JSON |
|
|
505
|
+
| `status` | string | NO | Processing status (received/processing/processed/failed) |
|
|
506
|
+
| `processing_attempts` | integer | NO | Number of processing attempts (default: 0) |
|
|
507
|
+
| `created_at` | timestamp | NO | When the record was created |
|
|
508
|
+
| `updated_at` | timestamp | NO | When the record was last updated |
|
|
509
|
+
|
|
510
|
+
**Optional Fields (useful for debugging and querying):**
|
|
511
|
+
|
|
512
|
+
| Field | Type | Nullable | Description |
|
|
513
|
+
| ----- | ---- | -------- | ----------- |
|
|
514
|
+
| `resource_type` | string | YES | Type of resource (e.g., 'organization', 'user') |
|
|
515
|
+
| `resource_id` | string | YES | ID of the resource being synced |
|
|
516
|
+
| `subject` | string | YES | NATS subject the message was received on |
|
|
517
|
+
| `headers` | jsonb | YES | NATS message headers |
|
|
518
|
+
| `stream` | string | YES | JetStream stream name |
|
|
519
|
+
| `stream_seq` | bigint | YES | Stream sequence number (fallback deduplication key) |
|
|
520
|
+
| `deliveries` | integer | YES | Number of delivery attempts from NATS |
|
|
521
|
+
| `error_message` | text | YES | Error message if processing failed |
|
|
522
|
+
| `received_at` | timestamp | YES | When the event was first received |
|
|
523
|
+
| `processed_at` | timestamp | YES | When the event was successfully processed |
|
|
524
|
+
| `failed_at` | timestamp | YES | When the event failed processing |
|
|
525
|
+
|
|
526
|
+
**Indexes:**
|
|
527
|
+
|
|
528
|
+
- `event_id` - Unique index for fast deduplication
|
|
529
|
+
- `status` - Index for querying by processing status
|
|
530
|
+
- `created_at` - Index for time-based queries
|
|
531
|
+
- `(stream, stream_seq)` - Unique composite index for fallback deduplication
|
|
532
|
+
|
|
489
533
|
**Deduplication:**
|
|
490
534
|
|
|
491
535
|
- Uses `event_id` for primary deduplication
|
|
@@ -508,6 +552,24 @@ inbox.processed_at # => 2024-01-01 00:00:00
|
|
|
508
552
|
# Skip processing, already done
|
|
509
553
|
```
|
|
510
554
|
|
|
555
|
+
**Field Population:**
|
|
556
|
+
|
|
557
|
+
The InboxRepository automatically extracts and sets fields from the message payload:
|
|
558
|
+
|
|
559
|
+
```ruby
|
|
560
|
+
# Extracted from message body
|
|
561
|
+
event_type: msg.body['type'] || msg.body['event_type']
|
|
562
|
+
resource_type: msg.body['resource_type']
|
|
563
|
+
resource_id: msg.body['resource_id']
|
|
564
|
+
|
|
565
|
+
# NATS metadata
|
|
566
|
+
subject: msg.subject
|
|
567
|
+
headers: msg.headers
|
|
568
|
+
stream: msg.stream
|
|
569
|
+
stream_seq: msg.seq
|
|
570
|
+
deliveries: msg.deliveries
|
|
571
|
+
```
|
|
572
|
+
|
|
511
573
|
### Dead Letter Queue (DLQ)
|
|
512
574
|
|
|
513
575
|
**Purpose:** Route unrecoverable messages to separate subject for manual intervention.
|
data/docs/GETTING_STARTED.md
CHANGED
|
@@ -6,7 +6,7 @@ This guide covers installation, Rails setup, configuration, and basic publish/co
|
|
|
6
6
|
|
|
7
7
|
```ruby
|
|
8
8
|
# Gemfile
|
|
9
|
-
gem "jetstream_bridge", "~>
|
|
9
|
+
gem "jetstream_bridge", "~> 7.0"
|
|
10
10
|
```
|
|
11
11
|
|
|
12
12
|
```bash
|
|
@@ -33,6 +33,64 @@ Generators create:
|
|
|
33
33
|
- `db/migrate/*_create_jetstream_inbox_events.rb`
|
|
34
34
|
- `app/controllers/jetstream_health_controller.rb` (health check)
|
|
35
35
|
|
|
36
|
+
### Database Migrations
|
|
37
|
+
|
|
38
|
+
The generated migrations create tables for inbox and outbox patterns:
|
|
39
|
+
|
|
40
|
+
**Outbox Events** (`jetstream_bridge_outbox_events`):
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
create_table :jetstream_bridge_outbox_events do |t|
|
|
44
|
+
t.string :event_id, null: false, index: { unique: true }
|
|
45
|
+
t.string :event_type, null: false
|
|
46
|
+
t.string :resource_type, null: false
|
|
47
|
+
t.string :resource_id
|
|
48
|
+
t.jsonb :payload, default: {}, null: false
|
|
49
|
+
t.string :subject, null: false
|
|
50
|
+
t.jsonb :headers, default: {}
|
|
51
|
+
t.string :status, default: "pending", null: false, index: true
|
|
52
|
+
t.text :error_message
|
|
53
|
+
t.integer :publish_attempts, default: 0
|
|
54
|
+
t.datetime :published_at, index: true
|
|
55
|
+
t.datetime :failed_at
|
|
56
|
+
t.timestamps
|
|
57
|
+
end
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Inbox Events** (`jetstream_bridge_inbox_events`):
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
create_table :jetstream_bridge_inbox_events do |t|
|
|
64
|
+
t.string :event_id, null: false, index: { unique: true }
|
|
65
|
+
t.string :event_type
|
|
66
|
+
t.string :resource_type
|
|
67
|
+
t.string :resource_id
|
|
68
|
+
t.jsonb :payload, default: {}, null: false
|
|
69
|
+
t.string :subject, null: false
|
|
70
|
+
t.jsonb :headers, default: {}
|
|
71
|
+
t.string :stream
|
|
72
|
+
t.bigint :stream_seq, index: true
|
|
73
|
+
t.integer :deliveries, default: 0
|
|
74
|
+
t.string :status, default: "received", null: false, index: true
|
|
75
|
+
t.text :error_message
|
|
76
|
+
t.integer :processing_attempts, default: 0
|
|
77
|
+
t.datetime :received_at, index: true
|
|
78
|
+
t.datetime :processed_at, index: true
|
|
79
|
+
t.datetime :failed_at
|
|
80
|
+
t.timestamps
|
|
81
|
+
end
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Key Fields:**
|
|
85
|
+
|
|
86
|
+
- `event_id`: Unique identifier for deduplication
|
|
87
|
+
- `event_type`: Type of event (e.g., "user.created")
|
|
88
|
+
- `resource_type`/`resource_id`: Entity being synchronized
|
|
89
|
+
- `payload`: Event data (JSON)
|
|
90
|
+
- `status`: Event lifecycle state (pending/processing/processed/failed)
|
|
91
|
+
- `stream_seq`: NATS JetStream sequence number (inbox only)
|
|
92
|
+
- `deliveries`: Delivery attempt count (inbox only)
|
|
93
|
+
|
|
36
94
|
## Configuration
|
|
37
95
|
|
|
38
96
|
```ruby
|
|
@@ -61,6 +119,19 @@ end
|
|
|
61
119
|
|
|
62
120
|
Rails autostart runs after initialization (including in console). You can opt out for rake tasks or other tooling with `config.lazy_connect = true` or `JETSTREAM_BRIDGE_DISABLE_AUTOSTART=1`; it will then connect on first publish/subscribe.
|
|
63
121
|
|
|
122
|
+
### Push consumer mode (restricted credentials)
|
|
123
|
+
|
|
124
|
+
If your NATS user cannot publish to `$JS.API.*`, switch to push consumers and pre-create the durable consumer:
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
config.consumer_mode = :push
|
|
128
|
+
# Optional: override delivery subject (defaults to "#{config.destination_subject}.worker")
|
|
129
|
+
# config.delivery_subject = "worker.sync.my_app.worker"
|
|
130
|
+
config.auto_provision = false # pre-create stream/consumer with admin creds
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Provision the consumer with NATS CLI (`--deliver <subject>`) or `bundle exec rake jetstream_bridge:provision` using admin credentials. See [docs/RESTRICTED_PERMISSIONS.md](RESTRICTED_PERMISSIONS.md) for the full least-privilege guide.
|
|
134
|
+
|
|
64
135
|
## Publish
|
|
65
136
|
|
|
66
137
|
```ruby
|