jetstream_bridge 4.0.4 → 4.2.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 +106 -0
- data/README.md +22 -1402
- data/docs/GETTING_STARTED.md +92 -0
- data/docs/PRODUCTION.md +503 -0
- data/docs/TESTING.md +414 -0
- data/lib/jetstream_bridge/consumer/consumer.rb +101 -5
- data/lib/jetstream_bridge/consumer/inbox/inbox_processor.rb +17 -3
- data/lib/jetstream_bridge/consumer/inbox/inbox_repository.rb +19 -7
- data/lib/jetstream_bridge/consumer/message_processor.rb +88 -52
- data/lib/jetstream_bridge/consumer/subscription_manager.rb +24 -15
- data/lib/jetstream_bridge/core/bridge_helpers.rb +85 -0
- data/lib/jetstream_bridge/core/config.rb +27 -4
- data/lib/jetstream_bridge/core/connection.rb +162 -13
- data/lib/jetstream_bridge/core.rb +8 -0
- data/lib/jetstream_bridge/models/inbox_event.rb +13 -7
- data/lib/jetstream_bridge/models/outbox_event.rb +2 -2
- data/lib/jetstream_bridge/publisher/publisher.rb +10 -5
- data/lib/jetstream_bridge/rails/integration.rb +153 -0
- data/lib/jetstream_bridge/rails/railtie.rb +53 -0
- data/lib/jetstream_bridge/rails.rb +5 -0
- data/lib/jetstream_bridge/tasks/install.rake +1 -1
- data/lib/jetstream_bridge/test_helpers/fixtures.rb +41 -0
- data/lib/jetstream_bridge/test_helpers/integration_helpers.rb +77 -0
- data/lib/jetstream_bridge/test_helpers/matchers.rb +98 -0
- data/lib/jetstream_bridge/test_helpers/mock_nats.rb +524 -0
- data/lib/jetstream_bridge/test_helpers.rb +85 -121
- data/lib/jetstream_bridge/topology/overlap_guard.rb +46 -0
- data/lib/jetstream_bridge/topology/stream.rb +7 -4
- data/lib/jetstream_bridge/version.rb +1 -1
- data/lib/jetstream_bridge.rb +138 -63
- metadata +32 -12
- data/lib/jetstream_bridge/railtie.rb +0 -49
data/README.md
CHANGED
|
@@ -2,16 +2,6 @@
|
|
|
2
2
|
<img src="logo.svg" alt="JetStream Bridge Logo" width="200"/>
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
-
<h1 align="center">JetStream Bridge</h1>
|
|
6
|
-
|
|
7
|
-
<p align="center">
|
|
8
|
-
<strong>Production-safe realtime data bridge</strong> between systems using <strong>NATS JetStream</strong>
|
|
9
|
-
</p>
|
|
10
|
-
|
|
11
|
-
<p align="center">
|
|
12
|
-
Includes durable consumers, backpressure, retries, <strong>DLQ</strong>, optional <strong>Inbox/Outbox</strong>, and <strong>overlap-safe stream provisioning</strong>
|
|
13
|
-
</p>
|
|
14
|
-
|
|
15
5
|
<p align="center">
|
|
16
6
|
<a href="https://github.com/attaradev/jetstream_bridge/actions/workflows/ci.yml">
|
|
17
7
|
<img src="https://github.com/attaradev/jetstream_bridge/actions/workflows/ci.yml/badge.svg" alt="CI Status"/>
|
|
@@ -30,65 +20,17 @@
|
|
|
30
20
|
</a>
|
|
31
21
|
</p>
|
|
32
22
|
|
|
33
|
-
|
|
34
|
-
<a href="#-why-jetstream-bridge">Why?</a> •
|
|
35
|
-
<a href="#-features">Features</a> •
|
|
36
|
-
<a href="#-quick-start">Quick Start</a> •
|
|
37
|
-
<a href="#-documentation">Documentation</a> •
|
|
38
|
-
<a href="#-contributing">Contributing</a>
|
|
39
|
-
</p>
|
|
40
|
-
|
|
41
|
-
---
|
|
42
|
-
|
|
43
|
-
## 🎯 Why JetStream Bridge?
|
|
44
|
-
|
|
45
|
-
Building event-driven systems with NATS JetStream is powerful, but comes with challenges:
|
|
46
|
-
|
|
47
|
-
* **Reliability**: How do you guarantee messages aren't lost during deploys or network failures?
|
|
48
|
-
* **Idempotency**: How do you prevent duplicate processing when messages are redelivered?
|
|
49
|
-
* **Stream Management**: How do you avoid "subjects overlap" errors in production?
|
|
50
|
-
* **Monitoring**: How do you know if your consumers are healthy and processing messages?
|
|
51
|
-
* **Rails Integration**: How do you integrate cleanly with ActiveRecord transactions?
|
|
52
|
-
|
|
53
|
-
**JetStream Bridge solves these problems** with production-tested patterns:
|
|
54
|
-
|
|
55
|
-
* ✅ **Transactional Outbox** - Never lose events, even if NATS is down
|
|
56
|
-
* ✅ **Idempotent Inbox** - Process each message exactly once, safely
|
|
57
|
-
* ✅ **Automatic Stream Provisioning** - No more manual stream management or overlap conflicts
|
|
58
|
-
* ✅ **Built-in Health Checks** - K8s-ready monitoring endpoints
|
|
59
|
-
* ✅ **Rails-Native** - Works seamlessly with ActiveRecord and Rails conventions
|
|
23
|
+
Production-ready NATS JetStream bridge for Ruby/Rails with outbox, inbox, DLQ, and overlap-safe stream provisioning.
|
|
60
24
|
|
|
61
|
-
|
|
25
|
+
## Highlights
|
|
62
26
|
|
|
63
|
-
|
|
27
|
+
- Transactional outbox and idempotent inbox (optional) for exactly-once pipelines.
|
|
28
|
+
- Durable pull consumers with retries, backoff, and DLQ routing.
|
|
29
|
+
- Auto stream/consumer provisioning with overlap protection.
|
|
30
|
+
- Rails-native: generators, migrations, health check, and eager-loading safety.
|
|
31
|
+
- Mock NATS for fast, no-infra testing.
|
|
64
32
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
* 🔌 **Simple Publisher and Consumer interfaces** - Write event-driven code in minutes with intuitive APIs that abstract NATS complexity
|
|
68
|
-
* 🛡 **Outbox & Inbox patterns (opt-in)** - Guarantee exactly-once delivery with reliable send (Outbox) and idempotent receive (Inbox)
|
|
69
|
-
* 🧨 **Dead Letter Queue (DLQ)** - Isolate and triage poison messages automatically instead of blocking your entire pipeline
|
|
70
|
-
* ⚙️ **Durable pull subscriptions** - Never lose messages with configurable backoff strategies and max delivery attempts
|
|
71
|
-
* 🎯 **Clear subject conventions** - Organized source/destination routing that scales across multiple services
|
|
72
|
-
* 🧱 **Overlap-safe stream provisioning** - Automatically prevents "subjects overlap" errors with intelligent conflict detection
|
|
73
|
-
* 🚂 **Rails generators included** - Generate initializers, migrations, and health checks with a single command
|
|
74
|
-
* ⚡️ **Zero-downtime deploys** - Eager-loaded models via Railtie ensure production stability
|
|
75
|
-
* 📊 **Observable by default** - Configurable logging with sensible defaults for debugging and monitoring
|
|
76
|
-
|
|
77
|
-
### Production-Ready Reliability
|
|
78
|
-
|
|
79
|
-
* 🏥 **Built-in health checks** - Monitor NATS connection, stream status, and configuration for K8s readiness/liveness probes
|
|
80
|
-
* 🔄 **Automatic reconnection** - Recover from network failures and NATS restarts without manual intervention
|
|
81
|
-
* 🔒 **Race condition protection** - Pessimistic locking prevents duplicate publishes in high-concurrency scenarios
|
|
82
|
-
* 🛡️ **Transaction safety** - All database operations are atomic with automatic rollback on failures
|
|
83
|
-
* 🎯 **Subject validation** - Catch configuration errors early by preventing NATS wildcards where they don't belong
|
|
84
|
-
* 🚦 **Graceful shutdown** - Proper signal handling and message draining prevent data loss during deploys
|
|
85
|
-
* 📈 **Pluggable retry strategies** - Choose exponential or linear backoff, or implement your own custom strategy
|
|
86
|
-
|
|
87
|
-
---
|
|
88
|
-
|
|
89
|
-
## 🚀 Quick Start
|
|
90
|
-
|
|
91
|
-
### 1. Install the Gem
|
|
33
|
+
## Quick Start
|
|
92
34
|
|
|
93
35
|
```ruby
|
|
94
36
|
# Gemfile
|
|
@@ -97,1366 +39,44 @@ gem "jetstream_bridge", "~> 4.0"
|
|
|
97
39
|
|
|
98
40
|
```bash
|
|
99
41
|
bundle install
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
### 2. Generate Configuration and Migrations
|
|
103
|
-
|
|
104
|
-
```bash
|
|
105
|
-
# Creates initializer and migrations
|
|
106
42
|
bin/rails g jetstream_bridge:install
|
|
107
|
-
|
|
108
|
-
# Run migrations
|
|
109
43
|
bin/rails db:migrate
|
|
110
44
|
```
|
|
111
45
|
|
|
112
|
-
### 3. Configure Your Application
|
|
113
|
-
|
|
114
46
|
```ruby
|
|
115
47
|
# config/initializers/jetstream_bridge.rb
|
|
116
48
|
JetstreamBridge.configure do |config|
|
|
117
49
|
config.nats_urls = ENV.fetch("NATS_URLS", "nats://localhost:4222")
|
|
118
50
|
config.env = ENV.fetch("RAILS_ENV", "development")
|
|
119
51
|
config.app_name = "my_app"
|
|
120
|
-
config.destination_app = "
|
|
121
|
-
|
|
122
|
-
# Enable reliability features (recommended for production)
|
|
123
|
-
config.use_outbox = true # Transactional outbox pattern
|
|
124
|
-
config.use_inbox = true # Idempotent message processing
|
|
125
|
-
config.use_dlq = true # Dead letter queue for failed messages
|
|
126
|
-
end
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
### 4. Publish Your First Event
|
|
130
|
-
|
|
131
|
-
```ruby
|
|
132
|
-
# In your Rails application
|
|
133
|
-
JetstreamBridge.publish(
|
|
134
|
-
resource_type: "user",
|
|
135
|
-
event_type: "created",
|
|
136
|
-
payload: { id: user.id, email: user.email }
|
|
137
|
-
)
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
### 5. Consume Events
|
|
141
|
-
|
|
142
|
-
```ruby
|
|
143
|
-
# Create a consumer (e.g., in a rake task or separate process)
|
|
144
|
-
consumer = JetstreamBridge::Consumer.new do |event|
|
|
145
|
-
user_data = event.payload
|
|
146
|
-
# Your idempotent business logic here
|
|
147
|
-
User.find_or_create_by(id: user_data.id) do |user|
|
|
148
|
-
user.email = user_data.email
|
|
149
|
-
end
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
consumer.run! # Starts consuming messages
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
That's it! You're now publishing and consuming events with JetStream.
|
|
156
|
-
|
|
157
|
-
---
|
|
158
|
-
|
|
159
|
-
## 📖 Documentation
|
|
160
|
-
|
|
161
|
-
### Table of Contents
|
|
162
|
-
|
|
163
|
-
* [Installation & Setup](#-rails-generators--rake-tasks)
|
|
164
|
-
* [Configuration](#-configuration)
|
|
165
|
-
* [Publishing Events](#-publish-events)
|
|
166
|
-
* [Consuming Events](#-consume-events)
|
|
167
|
-
* [Database Setup](#-database-setup-inbox--outbox)
|
|
168
|
-
* [Stream Topology](#-stream-topology-auto-ensure-and-overlap-safe)
|
|
169
|
-
* [Operations Guide](#-operations-guide)
|
|
170
|
-
* [Troubleshooting](#-troubleshooting)
|
|
171
|
-
|
|
172
|
-
---
|
|
173
|
-
|
|
174
|
-
## 🧰 Rails Generators & Rake Tasks
|
|
175
|
-
|
|
176
|
-
### Installation
|
|
177
|
-
|
|
178
|
-
From your Rails app:
|
|
179
|
-
|
|
180
|
-
```bash
|
|
181
|
-
# Create initializer + migrations
|
|
182
|
-
bin/rails g jetstream_bridge:install
|
|
183
|
-
|
|
184
|
-
# Or run them separately:
|
|
185
|
-
bin/rails g jetstream_bridge:initializer
|
|
186
|
-
bin/rails g jetstream_bridge:migrations
|
|
187
|
-
|
|
188
|
-
# Create health check endpoint
|
|
189
|
-
bin/rails g jetstream_bridge:health_check
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
Then:
|
|
193
|
-
|
|
194
|
-
```bash
|
|
195
|
-
bin/rails db:migrate
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
> The generators create:
|
|
199
|
-
>
|
|
200
|
-
> * `config/initializers/jetstream_bridge.rb`
|
|
201
|
-
> * `db/migrate/*_create_jetstream_outbox_events.rb`
|
|
202
|
-
> * `db/migrate/*_create_jetstream_inbox_events.rb`
|
|
203
|
-
> * `app/controllers/jetstream_health_controller.rb` (if health_check generator used)
|
|
204
|
-
|
|
205
|
-
### Rake Tasks
|
|
206
|
-
|
|
207
|
-
```bash
|
|
208
|
-
# Check health and connection status
|
|
209
|
-
bin/rake jetstream_bridge:health
|
|
210
|
-
|
|
211
|
-
# Validate configuration
|
|
212
|
-
bin/rake jetstream_bridge:validate
|
|
213
|
-
|
|
214
|
-
# Test NATS connection
|
|
215
|
-
bin/rake jetstream_bridge:test_connection
|
|
216
|
-
|
|
217
|
-
# Show comprehensive debug information
|
|
218
|
-
bin/rake jetstream_bridge:debug
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
---
|
|
222
|
-
|
|
223
|
-
## 🔧 Configuration
|
|
224
|
-
|
|
225
|
-
### Basic Configuration
|
|
226
|
-
|
|
227
|
-
```ruby
|
|
228
|
-
# config/initializers/jetstream_bridge.rb
|
|
229
|
-
JetstreamBridge.configure do |config|
|
|
230
|
-
# === Required Settings ===
|
|
231
|
-
|
|
232
|
-
# NATS server URLs (comma-separated for multiple servers)
|
|
233
|
-
config.nats_urls = ENV.fetch("NATS_URLS", "nats://localhost:4222")
|
|
234
|
-
|
|
235
|
-
# Environment namespace (development, staging, production)
|
|
236
|
-
config.env = ENV.fetch("RAILS_ENV", "development")
|
|
237
|
-
|
|
238
|
-
# Your application name (used in subject routing)
|
|
239
|
-
config.app_name = ENV.fetch("APP_NAME", "my_app")
|
|
240
|
-
|
|
241
|
-
# The application you're communicating with (REQUIRED)
|
|
242
|
-
config.destination_app = ENV.fetch("DESTINATION_APP")
|
|
243
|
-
|
|
244
|
-
# === Reliability Features (Recommended for Production) ===
|
|
245
|
-
|
|
246
|
-
# Transactional Outbox: Ensures events are never lost
|
|
52
|
+
config.destination_app = "worker_app"
|
|
247
53
|
config.use_outbox = true
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
config.use_inbox = true
|
|
251
|
-
|
|
252
|
-
# Dead Letter Queue: Isolates poison messages
|
|
253
|
-
config.use_dlq = true
|
|
254
|
-
|
|
255
|
-
# === Consumer Tuning ===
|
|
256
|
-
|
|
257
|
-
# Maximum delivery attempts before moving to DLQ
|
|
258
|
-
config.max_deliver = 5
|
|
259
|
-
|
|
260
|
-
# Time to wait for acknowledgment before redelivery
|
|
261
|
-
config.ack_wait = "30s"
|
|
262
|
-
|
|
263
|
-
# Backoff delays between retries (exponential backoff)
|
|
264
|
-
config.backoff = %w[1s 5s 15s 30s 60s]
|
|
265
|
-
|
|
266
|
-
# === Advanced Options ===
|
|
267
|
-
|
|
268
|
-
# Custom ActiveRecord models (if you have your own tables)
|
|
269
|
-
# config.outbox_model = "CustomOutboxEvent"
|
|
270
|
-
# config.inbox_model = "CustomInboxEvent"
|
|
271
|
-
|
|
272
|
-
# Custom logger
|
|
273
|
-
# config.logger = Rails.logger
|
|
274
|
-
end
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
### Configuration Reference
|
|
278
|
-
|
|
279
|
-
| Setting | Type | Default | Description |
|
|
280
|
-
|---------|------|---------|-------------|
|
|
281
|
-
| `nats_urls` | String | `"nats://localhost:4222"` | NATS server URL(s), comma-separated |
|
|
282
|
-
| `env` | String | `"development"` | Environment namespace for streams |
|
|
283
|
-
| `app_name` | String | (required) | Your application identifier |
|
|
284
|
-
| `destination_app` | String | (required) | Target application for events |
|
|
285
|
-
| `use_outbox` | Boolean | `false` | Enable transactional outbox pattern |
|
|
286
|
-
| `use_inbox` | Boolean | `false` | Enable idempotent inbox pattern |
|
|
287
|
-
| `use_dlq` | Boolean | `false` | Enable dead letter queue |
|
|
288
|
-
| `max_deliver` | Integer | `5` | Max delivery attempts before DLQ |
|
|
289
|
-
| `ack_wait` | String/Integer | `"30s"` | Acknowledgment timeout |
|
|
290
|
-
| `backoff` | Array | `["1s", "5s", "15s"]` | Retry backoff schedule |
|
|
291
|
-
|
|
292
|
-
### Understanding Configuration Options
|
|
293
|
-
|
|
294
|
-
#### When to Use Outbox
|
|
295
|
-
|
|
296
|
-
Enable `use_outbox` when you need:
|
|
297
|
-
|
|
298
|
-
* **Transactional guarantees**: Publish events as part of database transactions
|
|
299
|
-
* **Reliability**: Ensure events are never lost, even if NATS is temporarily down
|
|
300
|
-
* **Audit trail**: Keep a permanent record of all published events
|
|
301
|
-
|
|
302
|
-
```ruby
|
|
303
|
-
# Example: Publishing with outbox ensures atomicity
|
|
304
|
-
ActiveRecord::Base.transaction do
|
|
305
|
-
user.save!
|
|
306
|
-
JetstreamBridge.publish(
|
|
307
|
-
resource_type: "user",
|
|
308
|
-
event_type: "created",
|
|
309
|
-
payload: { id: user.id }
|
|
310
|
-
) # Event is saved in outbox table first
|
|
311
|
-
end
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
#### When to Use Inbox
|
|
315
|
-
|
|
316
|
-
Enable `use_inbox` when you need:
|
|
317
|
-
|
|
318
|
-
* **Idempotency**: Process each message exactly once, even with redeliveries
|
|
319
|
-
* **Duplicate protection**: Prevent duplicate processing across restarts
|
|
320
|
-
* **Processing history**: Track which messages have been processed
|
|
321
|
-
|
|
322
|
-
```ruby
|
|
323
|
-
# Example: Inbox prevents duplicate processing
|
|
324
|
-
# Even if the message is redelivered, it will be skipped
|
|
325
|
-
# if already marked as processed
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
#### When to Use DLQ
|
|
329
|
-
|
|
330
|
-
Enable `use_dlq` when you need:
|
|
331
|
-
|
|
332
|
-
* **Poison message handling**: Isolate messages that repeatedly fail
|
|
333
|
-
* **Manual intervention**: Review and retry failed messages later
|
|
334
|
-
* **Pipeline protection**: Prevent one bad message from blocking the queue
|
|
335
|
-
|
|
336
|
-
### Logging Configuration
|
|
337
|
-
|
|
338
|
-
JetstreamBridge integrates with your application's logger:
|
|
339
|
-
|
|
340
|
-
```ruby
|
|
341
|
-
# Use Rails logger (default)
|
|
342
|
-
config.logger = Rails.logger
|
|
343
|
-
|
|
344
|
-
# Use custom logger
|
|
345
|
-
config.logger = Logger.new(STDOUT)
|
|
346
|
-
|
|
347
|
-
# Disable logging
|
|
348
|
-
config.logger = Logger.new(IO::NULL)
|
|
349
|
-
```
|
|
350
|
-
|
|
351
|
-
### Environment Variables
|
|
352
|
-
|
|
353
|
-
Recommended environment variable setup:
|
|
354
|
-
|
|
355
|
-
```bash
|
|
356
|
-
# .env (or your deployment configuration)
|
|
357
|
-
NATS_URLS=nats://nats1:4222,nats://nats2:4222,nats://nats3:4222
|
|
358
|
-
RAILS_ENV=production
|
|
359
|
-
APP_NAME=api_service
|
|
360
|
-
DESTINATION_APP=notification_service
|
|
361
|
-
```
|
|
362
|
-
|
|
363
|
-
### Generated Streams and Subjects
|
|
364
|
-
|
|
365
|
-
Based on your configuration, JetStream Bridge automatically creates:
|
|
366
|
-
|
|
367
|
-
* **Stream Name**: `{env}-jetstream-bridge-stream`
|
|
368
|
-
* Example: `production-jetstream-bridge-stream`
|
|
369
|
-
|
|
370
|
-
* **Publish Subject**: `{env}.{app_name}.sync.{destination_app}`
|
|
371
|
-
* Example: `production.api_service.sync.notification_service`
|
|
372
|
-
|
|
373
|
-
* **Subscribe Subject**: `{env}.{destination_app}.sync.{app_name}`
|
|
374
|
-
* Example: `production.notification_service.sync.api_service`
|
|
375
|
-
|
|
376
|
-
* **DLQ Subject**: `{env}.{app}.sync.dlq` (per-app DLQ for isolation)
|
|
377
|
-
* Example: `production.api_service.sync.dlq`
|
|
378
|
-
|
|
379
|
-
---
|
|
380
|
-
|
|
381
|
-
## 📡 Subject Conventions
|
|
382
|
-
|
|
383
|
-
| Direction | Subject Pattern |
|
|
384
|
-
|---------------|------------------------------|
|
|
385
|
-
| **Publish** | `{env}.{app}.sync.{dest}` |
|
|
386
|
-
| **Subscribe** | `{env}.{dest}.sync.{app}` |
|
|
387
|
-
| **DLQ** | `{env}.{app}.sync.dlq` |
|
|
388
|
-
|
|
389
|
-
* `{app}`: `app_name`
|
|
390
|
-
* `{dest}`: `destination_app`
|
|
391
|
-
* `{env}`: `env`
|
|
392
|
-
|
|
393
|
-
**Note**: Each application has its own DLQ (`{env}.{app}.sync.dlq`) for better isolation, monitoring, and debugging. This allows you to track failed messages per service.
|
|
394
|
-
|
|
395
|
-
---
|
|
396
|
-
|
|
397
|
-
## 🧱 Stream Topology (auto-ensure and overlap-safe)
|
|
398
|
-
|
|
399
|
-
On first connection, Jetstream Bridge **ensures** a single stream exists for your `env` and that it covers:
|
|
400
|
-
|
|
401
|
-
* `source_subject` (`{env}.{app}.sync.{dest}`)
|
|
402
|
-
* `destination_subject` (`{env}.{dest}.sync.{app}`)
|
|
403
|
-
* `dlq_subject` (if enabled)
|
|
404
|
-
|
|
405
|
-
It’s **overlap-safe**:
|
|
406
|
-
|
|
407
|
-
* Skips adding subjects already covered by existing wildcards
|
|
408
|
-
* Pre-filters subjects owned by other streams to avoid `BadRequest: subjects overlap with an existing stream`
|
|
409
|
-
* Retries once on concurrent races, then logs and continues safely
|
|
410
|
-
|
|
411
|
-
---
|
|
412
|
-
|
|
413
|
-
## 🗃 Database Setup (Inbox / Outbox)
|
|
414
|
-
|
|
415
|
-
Inbox/Outbox are **optional**. The library detects columns at runtime and only sets what exists, so you can start minimal and evolve later.
|
|
416
|
-
|
|
417
|
-
### Generator-created tables (recommended)
|
|
418
|
-
|
|
419
|
-
```ruby
|
|
420
|
-
# jetstream_outbox_events
|
|
421
|
-
create_table :jetstream_outbox_events do |t|
|
|
422
|
-
t.string :event_id, null: false
|
|
423
|
-
t.string :subject, null: false
|
|
424
|
-
t.jsonb :payload, null: false, default: {}
|
|
425
|
-
t.jsonb :headers, null: false, default: {}
|
|
426
|
-
t.string :status, null: false, default: "pending" # pending|publishing|sent|failed
|
|
427
|
-
t.integer :attempts, null: false, default: 0
|
|
428
|
-
t.text :last_error
|
|
429
|
-
t.datetime :enqueued_at
|
|
430
|
-
t.datetime :sent_at
|
|
431
|
-
t.timestamps
|
|
432
|
-
end
|
|
433
|
-
add_index :jetstream_outbox_events, :event_id, unique: true
|
|
434
|
-
add_index :jetstream_outbox_events, :status
|
|
435
|
-
|
|
436
|
-
# jetstream_inbox_events
|
|
437
|
-
create_table :jetstream_inbox_events do |t|
|
|
438
|
-
t.string :event_id # preferred dedupe key
|
|
439
|
-
t.string :subject, null: false
|
|
440
|
-
t.jsonb :payload, null: false, default: {}
|
|
441
|
-
t.jsonb :headers, null: false, default: {}
|
|
442
|
-
t.string :stream
|
|
443
|
-
t.bigint :stream_seq
|
|
444
|
-
t.integer :deliveries
|
|
445
|
-
t.string :status, null: false, default: "received" # received|processing|processed|failed
|
|
446
|
-
t.text :last_error
|
|
447
|
-
t.datetime :received_at
|
|
448
|
-
t.datetime :processed_at
|
|
449
|
-
t.timestamps
|
|
450
|
-
end
|
|
451
|
-
add_index :jetstream_inbox_events, :event_id, unique: true, where: 'event_id IS NOT NULL'
|
|
452
|
-
add_index :jetstream_inbox_events, [:stream, :stream_seq], unique: true, where: 'stream IS NOT NULL AND stream_seq IS NOT NULL'
|
|
453
|
-
add_index :jetstream_inbox_events, :status
|
|
454
|
-
```
|
|
455
|
-
|
|
456
|
-
> Already have different table names? Point the config to your AR classes via `config.outbox_model` / `config.inbox_model`.
|
|
457
|
-
|
|
458
|
-
---
|
|
459
|
-
|
|
460
|
-
## 📤 Publish Events
|
|
461
|
-
|
|
462
|
-
JetStream Bridge provides two ways to publish events: a convenience method and a direct publisher instance.
|
|
463
|
-
|
|
464
|
-
### Using the Convenience Method to Consume Events
|
|
465
|
-
|
|
466
|
-
The simplest way to publish events:
|
|
467
|
-
|
|
468
|
-
```ruby
|
|
469
|
-
# Basic usage with structured parameters
|
|
470
|
-
JetstreamBridge.publish(
|
|
471
|
-
resource_type: "user",
|
|
472
|
-
event_type: "created",
|
|
473
|
-
payload: { id: user.id, email: user.email, name: user.name }
|
|
474
|
-
)
|
|
475
|
-
|
|
476
|
-
# Returns true on success, raises error on failure
|
|
477
|
-
```
|
|
478
|
-
|
|
479
|
-
### Publishing Patterns
|
|
480
|
-
|
|
481
|
-
#### 1. Structured Parameters (Recommended)
|
|
482
|
-
|
|
483
|
-
Best for explicit, clear code:
|
|
484
|
-
|
|
485
|
-
```ruby
|
|
486
|
-
JetstreamBridge.publish(
|
|
487
|
-
resource_type: "user",
|
|
488
|
-
event_type: "created",
|
|
489
|
-
payload: { id: "01H...", email: "ada@example.com" },
|
|
490
|
-
# Optional parameters:
|
|
491
|
-
event_id: SecureRandom.uuid, # Auto-generated if not provided
|
|
492
|
-
trace_id: request_id, # For distributed tracing
|
|
493
|
-
occurred_at: Time.now.utc, # Defaults to current time
|
|
494
|
-
subject: "custom.subject.override" # Override default subject
|
|
495
|
-
)
|
|
496
|
-
```
|
|
497
|
-
|
|
498
|
-
#### 2. Simplified Hash (Infers resource_type)
|
|
499
|
-
|
|
500
|
-
Use dot notation in `event_type` to infer `resource_type`:
|
|
501
|
-
|
|
502
|
-
```ruby
|
|
503
|
-
JetstreamBridge.publish(
|
|
504
|
-
event_type: "user.created", # "user" becomes resource_type
|
|
505
|
-
payload: { id: "01H...", email: "ada@example.com" }
|
|
506
|
-
)
|
|
507
|
-
```
|
|
508
|
-
|
|
509
|
-
#### 3. Complete Envelope (Advanced)
|
|
510
|
-
|
|
511
|
-
Pass a full envelope hash for maximum control:
|
|
512
|
-
|
|
513
|
-
```ruby
|
|
514
|
-
JetstreamBridge.publish(
|
|
515
|
-
event_type: "created",
|
|
516
|
-
resource_type: "user",
|
|
517
|
-
payload: { id: "01H...", email: "ada@example.com" },
|
|
518
|
-
event_id: "custom-event-id",
|
|
519
|
-
occurred_at: 1.hour.ago.iso8601,
|
|
520
|
-
producer: "custom-producer"
|
|
521
|
-
)
|
|
522
|
-
```
|
|
523
|
-
|
|
524
|
-
### Using Publisher Instances
|
|
525
|
-
|
|
526
|
-
For more control or batch operations:
|
|
527
|
-
|
|
528
|
-
```ruby
|
|
529
|
-
publisher = JetstreamBridge::Publisher.new
|
|
530
|
-
|
|
531
|
-
# Publish multiple events
|
|
532
|
-
publisher.publish(
|
|
533
|
-
resource_type: "user",
|
|
534
|
-
event_type: "created",
|
|
535
|
-
payload: { id: user.id }
|
|
536
|
-
)
|
|
537
|
-
|
|
538
|
-
publisher.publish(
|
|
539
|
-
resource_type: "user",
|
|
540
|
-
event_type: "updated",
|
|
541
|
-
payload: { id: user.id, email: user.email }
|
|
542
|
-
)
|
|
543
|
-
```
|
|
544
|
-
|
|
545
|
-
#### Thread Safety
|
|
546
|
-
|
|
547
|
-
Publisher instances are **thread-safe** and can be shared across multiple threads:
|
|
548
|
-
|
|
549
|
-
```ruby
|
|
550
|
-
# Safe: Share publisher across threads
|
|
551
|
-
@publisher = JetstreamBridge::Publisher.new
|
|
552
|
-
|
|
553
|
-
# Multiple threads can publish concurrently
|
|
554
|
-
threads = 10.times.map do |i|
|
|
555
|
-
Thread.new do
|
|
556
|
-
@publisher.publish(
|
|
557
|
-
event_type: "user.created",
|
|
558
|
-
payload: { id: i, name: "User #{i}" }
|
|
559
|
-
)
|
|
560
|
-
end
|
|
561
|
-
end
|
|
562
|
-
|
|
563
|
-
threads.each(&:join)
|
|
564
|
-
```
|
|
565
|
-
|
|
566
|
-
The underlying NATS connection is managed through a thread-safe singleton, ensuring safe concurrent access. However, for high-throughput scenarios, consider:
|
|
567
|
-
|
|
568
|
-
* Using the convenience method `JetstreamBridge.publish(...)` which handles connection management automatically
|
|
569
|
-
* Creating a connection pool if you need isolated error handling per thread
|
|
570
|
-
* Using batch publishing for bulk operations: `JetstreamBridge.publish_batch { ... }`
|
|
571
|
-
|
|
572
|
-
### Publishing with Transactions (Outbox Pattern)
|
|
573
|
-
|
|
574
|
-
When `use_outbox` is enabled, events are saved to the database first:
|
|
575
|
-
|
|
576
|
-
```ruby
|
|
577
|
-
# Atomic: both user creation and event publishing succeed or fail together
|
|
578
|
-
ActiveRecord::Base.transaction do
|
|
579
|
-
user = User.create!(email: "ada@example.com")
|
|
580
|
-
|
|
581
|
-
JetstreamBridge.publish(
|
|
582
|
-
resource_type: "user",
|
|
583
|
-
event_type: "created",
|
|
584
|
-
payload: { id: user.id, email: user.email }
|
|
585
|
-
)
|
|
586
|
-
end
|
|
587
|
-
# Event is saved in outbox table, published to NATS asynchronously
|
|
588
|
-
```
|
|
589
|
-
|
|
590
|
-
### Outbox Behavior
|
|
591
|
-
|
|
592
|
-
If **Outbox** is enabled (`config.use_outbox = true`):
|
|
593
|
-
|
|
594
|
-
* ✅ Events are saved to `jetstream_outbox_events` table first
|
|
595
|
-
* ✅ Published with `nats-msg-id` header for idempotency
|
|
596
|
-
* ✅ Marked as `sent` on success or `failed` with error details
|
|
597
|
-
* ✅ Survives NATS downtime - events queued for retry
|
|
598
|
-
* ✅ Provides audit trail of all published events
|
|
599
|
-
|
|
600
|
-
### Real-World Publishing Examples
|
|
601
|
-
|
|
602
|
-
#### Publishing Domain Events
|
|
603
|
-
|
|
604
|
-
```ruby
|
|
605
|
-
# After user registration
|
|
606
|
-
class UsersController < ApplicationController
|
|
607
|
-
def create
|
|
608
|
-
ActiveRecord::Base.transaction do
|
|
609
|
-
@user = User.create!(user_params)
|
|
610
|
-
|
|
611
|
-
JetstreamBridge.publish(
|
|
612
|
-
resource_type: "user",
|
|
613
|
-
event_type: "registered",
|
|
614
|
-
payload: {
|
|
615
|
-
id: @user.id,
|
|
616
|
-
email: @user.email,
|
|
617
|
-
name: @user.name,
|
|
618
|
-
plan: @user.plan
|
|
619
|
-
},
|
|
620
|
-
trace_id: request.request_id
|
|
621
|
-
)
|
|
622
|
-
end
|
|
623
|
-
|
|
624
|
-
redirect_to @user
|
|
625
|
-
end
|
|
626
|
-
end
|
|
627
|
-
```
|
|
628
|
-
|
|
629
|
-
#### Publishing State Changes
|
|
630
|
-
|
|
631
|
-
```ruby
|
|
632
|
-
# In your model or service object
|
|
633
|
-
class Order
|
|
634
|
-
after_commit :publish_status_change, if: :saved_change_to_status?
|
|
635
|
-
|
|
636
|
-
private
|
|
637
|
-
|
|
638
|
-
def publish_status_change
|
|
639
|
-
JetstreamBridge.publish(
|
|
640
|
-
resource_type: "order",
|
|
641
|
-
event_type: "status_changed",
|
|
642
|
-
payload: {
|
|
643
|
-
id: id,
|
|
644
|
-
status: status,
|
|
645
|
-
previous_status: status_before_last_save,
|
|
646
|
-
changed_at: updated_at
|
|
647
|
-
}
|
|
648
|
-
)
|
|
649
|
-
end
|
|
650
|
-
end
|
|
651
|
-
```
|
|
652
|
-
|
|
653
|
-
#### Publishing with Distributed Tracing
|
|
654
|
-
|
|
655
|
-
```ruby
|
|
656
|
-
# Include trace_id for correlation across services
|
|
657
|
-
JetstreamBridge.publish(
|
|
658
|
-
resource_type: "payment",
|
|
659
|
-
event_type: "processed",
|
|
660
|
-
payload: { order_id: order.id, amount: payment.amount },
|
|
661
|
-
trace_id: Current.request_id,
|
|
662
|
-
event_id: payment.transaction_id
|
|
663
|
-
)
|
|
664
|
-
```
|
|
665
|
-
|
|
666
|
-
---
|
|
667
|
-
|
|
668
|
-
## 📥 Consume Events
|
|
669
|
-
|
|
670
|
-
JetStream Bridge provides two ways to consume events: a convenience method and direct consumer instances.
|
|
671
|
-
|
|
672
|
-
### Using the Convenience Method (Recommended)
|
|
673
|
-
|
|
674
|
-
The simplest way to start consuming events:
|
|
675
|
-
|
|
676
|
-
```ruby
|
|
677
|
-
# Start consumer and run in current thread (receives Event object)
|
|
678
|
-
consumer = JetstreamBridge.subscribe do |event|
|
|
679
|
-
# event is a Models::Event object with structured access
|
|
680
|
-
puts "Processing: #{event.type}"
|
|
681
|
-
puts "Payload: #{event.payload.to_h}"
|
|
682
|
-
puts "Event ID: #{event.event_id}"
|
|
683
|
-
puts "Deliveries: #{event.deliveries}"
|
|
684
|
-
|
|
685
|
-
User.find_or_create_by(id: event.payload.id) do |user|
|
|
686
|
-
user.email = event.payload.email
|
|
687
|
-
end
|
|
688
|
-
end
|
|
689
|
-
|
|
690
|
-
consumer.run! # Blocks and processes messages
|
|
691
|
-
```
|
|
692
|
-
|
|
693
|
-
**Note:** The consumer handler receives a structured `Models::Event` object (not a raw hash) with convenient accessor methods for event data and metadata.
|
|
694
|
-
|
|
695
|
-
### Understanding the Event Object
|
|
696
|
-
|
|
697
|
-
Consumers receive a `JetstreamBridge::Models::Event` object that provides structured access to event data and metadata:
|
|
698
|
-
|
|
699
|
-
```ruby
|
|
700
|
-
JetstreamBridge.subscribe do |event|
|
|
701
|
-
# Event data
|
|
702
|
-
event.event_id # => "abc-123-def"
|
|
703
|
-
event.type # => "user.created" (event_type)
|
|
704
|
-
event.resource_type # => "user"
|
|
705
|
-
event.resource_id # => "123"
|
|
706
|
-
event.producer # => "api_service"
|
|
707
|
-
event.occurred_at # => 2025-01-15 10:30:00 UTC (Time object)
|
|
708
|
-
event.trace_id # => "xyz789"
|
|
709
|
-
|
|
710
|
-
# Payload access (method-style or hash-style)
|
|
711
|
-
event.payload.id # => 123
|
|
712
|
-
event.payload.email # => "ada@example.com"
|
|
713
|
-
event.payload["id"] # => 123 (also works)
|
|
714
|
-
event.payload.to_h # => { "id" => 123, "email" => "ada@example.com" }
|
|
715
|
-
|
|
716
|
-
# Delivery metadata
|
|
717
|
-
event.deliveries # => 1 (delivery attempt count)
|
|
718
|
-
event.subject # => "production.api.sync.worker"
|
|
719
|
-
event.stream # => "production-jetstream-bridge-stream"
|
|
720
|
-
event.sequence # => 42 (stream sequence number)
|
|
721
|
-
|
|
722
|
-
# Access all metadata
|
|
723
|
-
event.metadata.to_h # => { subject: "...", deliveries: 1, ... }
|
|
724
|
-
|
|
725
|
-
# Convert to hash (for backwards compatibility)
|
|
726
|
-
event.to_h # => Full event as hash
|
|
727
|
-
event["event_type"] # => "user.created" (hash-style access)
|
|
728
|
-
end
|
|
729
|
-
```
|
|
730
|
-
|
|
731
|
-
This structured approach provides type safety, cleaner code, and better IDE support compared to raw hashes.
|
|
732
|
-
|
|
733
|
-
### Consuming Patterns
|
|
734
|
-
|
|
735
|
-
#### 1. Basic Consumer with Block
|
|
736
|
-
|
|
737
|
-
```ruby
|
|
738
|
-
consumer = JetstreamBridge.subscribe do |event|
|
|
739
|
-
# event: Models::Event object with structured access
|
|
740
|
-
# event.type: Event type (e.g., "created", "user.created")
|
|
741
|
-
# event.payload: PayloadAccessor for event data
|
|
742
|
-
# event.deliveries: Number of delivery attempts (starts at 1)
|
|
743
|
-
# event.metadata: Delivery metadata (subject, stream, sequence, etc.)
|
|
744
|
-
|
|
745
|
-
case event.type
|
|
746
|
-
when "created"
|
|
747
|
-
UserCreatedHandler.call(event.payload.to_h)
|
|
748
|
-
when "updated"
|
|
749
|
-
UserUpdatedHandler.call(event.payload.to_h)
|
|
750
|
-
when "deleted"
|
|
751
|
-
UserDeletedHandler.call(event.payload.to_h)
|
|
752
|
-
end
|
|
54
|
+
config.use_inbox = true
|
|
55
|
+
config.use_dlq = true
|
|
753
56
|
end
|
|
754
|
-
|
|
755
|
-
consumer.run! # Start consuming
|
|
756
57
|
```
|
|
757
58
|
|
|
758
|
-
|
|
59
|
+
Publish:
|
|
759
60
|
|
|
760
61
|
```ruby
|
|
761
|
-
|
|
762
|
-
thread = JetstreamBridge.subscribe(run: true) do |event|
|
|
763
|
-
ProcessEventJob.perform_later(event.to_h)
|
|
764
|
-
end
|
|
765
|
-
|
|
766
|
-
# Consumer runs in background
|
|
767
|
-
# Your application continues
|
|
768
|
-
|
|
769
|
-
# Later, to stop:
|
|
770
|
-
thread.kill
|
|
62
|
+
JetstreamBridge.publish(event_type: "user.created", resource_type: "user", payload: { id: 1 })
|
|
771
63
|
```
|
|
772
64
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
```ruby
|
|
776
|
-
class EventHandler
|
|
777
|
-
def call(event)
|
|
778
|
-
logger.info "Processing #{event.type} from #{event.subject} (attempt #{event.deliveries})"
|
|
779
|
-
# Your logic here
|
|
780
|
-
end
|
|
781
|
-
end
|
|
782
|
-
|
|
783
|
-
handler = EventHandler.new
|
|
784
|
-
consumer = JetstreamBridge.subscribe(handler)
|
|
785
|
-
consumer.run!
|
|
786
|
-
```
|
|
787
|
-
|
|
788
|
-
#### 4. Consumer with Custom Configuration
|
|
789
|
-
|
|
790
|
-
```ruby
|
|
791
|
-
consumer = JetstreamBridge.subscribe(
|
|
792
|
-
durable_name: "my-custom-consumer",
|
|
793
|
-
batch_size: 10
|
|
794
|
-
) do |event|
|
|
795
|
-
# Process events in batches of 10
|
|
796
|
-
end
|
|
797
|
-
|
|
798
|
-
consumer.run!
|
|
799
|
-
```
|
|
800
|
-
|
|
801
|
-
#### 5. Consumer with Middleware
|
|
802
|
-
|
|
803
|
-
Add cross-cutting concerns like logging, metrics, and tracing using middleware:
|
|
804
|
-
|
|
805
|
-
```ruby
|
|
806
|
-
consumer = JetstreamBridge.subscribe do |event|
|
|
807
|
-
process_event(event)
|
|
808
|
-
end
|
|
809
|
-
|
|
810
|
-
# Add built-in middleware
|
|
811
|
-
consumer.use(JetstreamBridge::Consumer::LoggingMiddleware.new)
|
|
812
|
-
consumer.use(JetstreamBridge::Consumer::MetricsMiddleware.new(
|
|
813
|
-
on_success: ->(event, duration) { StatsD.timing("event.process", duration) },
|
|
814
|
-
on_failure: ->(event, error) { StatsD.increment("event.failed") }
|
|
815
|
-
))
|
|
816
|
-
consumer.use(JetstreamBridge::Consumer::TimeoutMiddleware.new(timeout: 30))
|
|
817
|
-
|
|
818
|
-
consumer.run!
|
|
819
|
-
```
|
|
820
|
-
|
|
821
|
-
**Available Middleware:**
|
|
822
|
-
|
|
823
|
-
* `LoggingMiddleware` - Logs event processing start, completion, and errors
|
|
824
|
-
* `ErrorHandlingMiddleware` - Custom error handling with callbacks
|
|
825
|
-
* `MetricsMiddleware` - Track processing metrics and timing
|
|
826
|
-
* `TracingMiddleware` - Distributed tracing support (sets Current.trace_id)
|
|
827
|
-
* `TimeoutMiddleware` - Prevent long-running handlers from blocking
|
|
828
|
-
|
|
829
|
-
**Custom Middleware Example:**
|
|
830
|
-
|
|
831
|
-
```ruby
|
|
832
|
-
class RetryMiddleware
|
|
833
|
-
def initialize(max_retries: 3)
|
|
834
|
-
@max_retries = max_retries
|
|
835
|
-
end
|
|
836
|
-
|
|
837
|
-
def call(event)
|
|
838
|
-
retries = 0
|
|
839
|
-
begin
|
|
840
|
-
yield
|
|
841
|
-
rescue TransientError => e
|
|
842
|
-
retries += 1
|
|
843
|
-
if retries < @max_retries
|
|
844
|
-
sleep(retries)
|
|
845
|
-
retry
|
|
846
|
-
else
|
|
847
|
-
raise
|
|
848
|
-
end
|
|
849
|
-
end
|
|
850
|
-
end
|
|
851
|
-
end
|
|
852
|
-
|
|
853
|
-
consumer.use(RetryMiddleware.new(max_retries: 5))
|
|
854
|
-
```
|
|
855
|
-
|
|
856
|
-
### Using Consumer Instances Directly
|
|
857
|
-
|
|
858
|
-
For more control over the consumer lifecycle:
|
|
65
|
+
Consume:
|
|
859
66
|
|
|
860
67
|
```ruby
|
|
861
68
|
consumer = JetstreamBridge::Consumer.new do |event|
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
logger.info "Processing #{event.type} (attempt #{event.deliveries})"
|
|
865
|
-
|
|
866
|
-
begin
|
|
867
|
-
process_event(event)
|
|
868
|
-
rescue RecoverableError => e
|
|
869
|
-
# Let JetStream retry with backoff
|
|
870
|
-
raise
|
|
871
|
-
rescue UnrecoverableError => e
|
|
872
|
-
# Log and acknowledge to prevent infinite retries
|
|
873
|
-
logger.error "Unrecoverable error: #{e.message}"
|
|
874
|
-
# Automatically moved to DLQ if configured
|
|
875
|
-
end
|
|
876
|
-
end
|
|
877
|
-
|
|
878
|
-
consumer.run! # Start processing
|
|
879
|
-
```
|
|
880
|
-
|
|
881
|
-
### Understanding the Handler Signature
|
|
882
|
-
|
|
883
|
-
Your handler receives a single `Models::Event` parameter with all event and metadata:
|
|
884
|
-
|
|
885
|
-
```ruby
|
|
886
|
-
JetstreamBridge.subscribe do |event|
|
|
887
|
-
# event: Models::Event - structured event object
|
|
888
|
-
|
|
889
|
-
puts "Event type: #{event.type}"
|
|
890
|
-
puts "Event ID: #{event.event_id}"
|
|
891
|
-
puts "Subject: #{event.subject}"
|
|
892
|
-
puts "Delivery attempt: #{event.deliveries}"
|
|
893
|
-
puts "Trace ID: #{event.trace_id}"
|
|
894
|
-
|
|
895
|
-
# Access payload
|
|
896
|
-
puts "User ID: #{event.payload.id}"
|
|
897
|
-
puts "Email: #{event.payload.email}"
|
|
898
|
-
|
|
899
|
-
# Implement retry logic based on deliveries if needed
|
|
900
|
-
raise "Transient error, retry" if event.deliveries < 3 && some_transient_condition?
|
|
901
|
-
end
|
|
902
|
-
```
|
|
903
|
-
|
|
904
|
-
### Consumer Options
|
|
905
|
-
|
|
906
|
-
| Option | Type | Default | Description |
|
|
907
|
-
|--------|------|---------|-------------|
|
|
908
|
-
| `handler` | Proc/Callable | (required) | Block or object that responds to `call` |
|
|
909
|
-
| `run` | Boolean | `false` | Start consumer in background thread |
|
|
910
|
-
| `durable_name` | String | From config | Custom durable consumer name |
|
|
911
|
-
| `batch_size` | Integer | From config | Number of messages to fetch at once |
|
|
912
|
-
|
|
913
|
-
### Inbox Behavior
|
|
914
|
-
|
|
915
|
-
If **Inbox** is enabled (`config.use_inbox = true`):
|
|
916
|
-
|
|
917
|
-
* ✅ Deduplicates by `event_id` (or stream sequence as fallback)
|
|
918
|
-
* ✅ Records processing state, errors, and timestamps
|
|
919
|
-
* ✅ Skips already-processed messages automatically
|
|
920
|
-
* ✅ Provides processing history and audit trail
|
|
921
|
-
* ✅ Safe across restarts and redeliveries
|
|
922
|
-
|
|
923
|
-
```ruby
|
|
924
|
-
# With inbox enabled, this is automatically idempotent:
|
|
925
|
-
JetstreamBridge.subscribe do |event|
|
|
926
|
-
# Even if message is redelivered, it won't execute twice
|
|
927
|
-
User.create!(email: event.payload.email)
|
|
928
|
-
end
|
|
929
|
-
```
|
|
930
|
-
|
|
931
|
-
### Real-World Consuming Examples
|
|
932
|
-
|
|
933
|
-
#### Processing in Rake Task
|
|
934
|
-
|
|
935
|
-
```ruby
|
|
936
|
-
# lib/tasks/consume_events.rake
|
|
937
|
-
namespace :jetstream do
|
|
938
|
-
desc "Start event consumer"
|
|
939
|
-
task consume: :environment do
|
|
940
|
-
consumer = JetstreamBridge.subscribe do |event|
|
|
941
|
-
Rails.logger.info "Processing #{event.type} (attempt #{event.deliveries})"
|
|
942
|
-
|
|
943
|
-
case event.resource_type
|
|
944
|
-
when "user"
|
|
945
|
-
UserEventHandler.process(event)
|
|
946
|
-
when "order"
|
|
947
|
-
OrderEventHandler.process(event)
|
|
948
|
-
else
|
|
949
|
-
Rails.logger.warn "Unknown resource type: #{event.resource_type}"
|
|
950
|
-
end
|
|
951
|
-
end
|
|
952
|
-
|
|
953
|
-
# Graceful shutdown on SIGTERM
|
|
954
|
-
trap("TERM") { consumer.stop! }
|
|
955
|
-
|
|
956
|
-
consumer.run!
|
|
957
|
-
end
|
|
958
|
-
end
|
|
959
|
-
```
|
|
960
|
-
|
|
961
|
-
#### Background Job Processing
|
|
962
|
-
|
|
963
|
-
```ruby
|
|
964
|
-
# Offload to background jobs for complex processing
|
|
965
|
-
JetstreamBridge.subscribe(run: true) do |event|
|
|
966
|
-
ProcessEventJob.perform_later(
|
|
967
|
-
event: event.to_h.to_json,
|
|
968
|
-
event_id: event.event_id,
|
|
969
|
-
trace_id: event.trace_id
|
|
970
|
-
)
|
|
971
|
-
end
|
|
972
|
-
|
|
973
|
-
# app/jobs/process_event_job.rb
|
|
974
|
-
class ProcessEventJob < ApplicationJob
|
|
975
|
-
queue_as :events
|
|
976
|
-
|
|
977
|
-
def perform(event:, event_id:, trace_id:)
|
|
978
|
-
event_data = JSON.parse(event)
|
|
979
|
-
|
|
980
|
-
# Complex processing with retries
|
|
981
|
-
case event_data["type"]
|
|
982
|
-
when "user.created"
|
|
983
|
-
SendWelcomeEmailService.call(event_data["payload"])
|
|
984
|
-
when "order.completed"
|
|
985
|
-
GenerateInvoiceService.call(event_data["payload"])
|
|
986
|
-
end
|
|
987
|
-
rescue => e
|
|
988
|
-
logger.error "Failed to process event #{event_id}: #{e.message}"
|
|
989
|
-
raise # Let Sidekiq retry
|
|
990
|
-
end
|
|
991
|
-
end
|
|
992
|
-
```
|
|
993
|
-
|
|
994
|
-
#### Event Router Pattern
|
|
995
|
-
|
|
996
|
-
```ruby
|
|
997
|
-
# app/services/event_router.rb
|
|
998
|
-
class EventRouter
|
|
999
|
-
def self.route(event)
|
|
1000
|
-
handler_class = "#{event.resource_type.camelize}#{event.type.camelize}Handler"
|
|
1001
|
-
|
|
1002
|
-
if Object.const_defined?(handler_class)
|
|
1003
|
-
handler_class.constantize.new.call(event)
|
|
1004
|
-
else
|
|
1005
|
-
Rails.logger.warn "No handler for #{event.resource_type}.#{event.type}"
|
|
1006
|
-
end
|
|
1007
|
-
end
|
|
1008
|
-
end
|
|
1009
|
-
|
|
1010
|
-
# Start consumer with router
|
|
1011
|
-
JetstreamBridge.subscribe do |event|
|
|
1012
|
-
EventRouter.route(event)
|
|
1013
|
-
end.run!
|
|
1014
|
-
```
|
|
1015
|
-
|
|
1016
|
-
#### Multi-Service Consumer
|
|
1017
|
-
|
|
1018
|
-
```ruby
|
|
1019
|
-
# app/consumers/application_consumer.rb
|
|
1020
|
-
class ApplicationConsumer
|
|
1021
|
-
def self.start!
|
|
1022
|
-
consumer = JetstreamBridge.subscribe do |event|
|
|
1023
|
-
begin
|
|
1024
|
-
new.process(event)
|
|
1025
|
-
rescue => e
|
|
1026
|
-
Rails.logger.error "Error processing event: #{e.message}"
|
|
1027
|
-
raise # Trigger retry/DLQ
|
|
1028
|
-
end
|
|
1029
|
-
end
|
|
1030
|
-
|
|
1031
|
-
# Handle signals gracefully
|
|
1032
|
-
%w[INT TERM].each do |signal|
|
|
1033
|
-
trap(signal) do
|
|
1034
|
-
Rails.logger.info "Shutting down consumer..."
|
|
1035
|
-
consumer.stop!
|
|
1036
|
-
exit
|
|
1037
|
-
end
|
|
1038
|
-
end
|
|
1039
|
-
|
|
1040
|
-
Rails.logger.info "Consumer started. Press Ctrl+C to stop."
|
|
1041
|
-
consumer.run!
|
|
1042
|
-
end
|
|
1043
|
-
|
|
1044
|
-
def process(event)
|
|
1045
|
-
# Log for observability
|
|
1046
|
-
Rails.logger.info(
|
|
1047
|
-
message: "Processing event",
|
|
1048
|
-
event_id: event.event_id,
|
|
1049
|
-
event_type: event.type,
|
|
1050
|
-
resource_type: event.resource_type,
|
|
1051
|
-
trace_id: event.trace_id,
|
|
1052
|
-
subject: event.subject,
|
|
1053
|
-
deliveries: event.deliveries
|
|
1054
|
-
)
|
|
1055
|
-
|
|
1056
|
-
# Route to specific handler
|
|
1057
|
-
handler = handler_for(event)
|
|
1058
|
-
handler.call(event.payload.to_h, event)
|
|
1059
|
-
end
|
|
1060
|
-
|
|
1061
|
-
private
|
|
1062
|
-
|
|
1063
|
-
def handler_for(event)
|
|
1064
|
-
case [event.resource_type, event.type]
|
|
1065
|
-
when ["user", "created"]
|
|
1066
|
-
UserCreatedHandler
|
|
1067
|
-
when ["user", "updated"]
|
|
1068
|
-
UserUpdatedHandler
|
|
1069
|
-
when ["order", "completed"]
|
|
1070
|
-
OrderCompletedHandler
|
|
1071
|
-
else
|
|
1072
|
-
UnknownEventHandler
|
|
1073
|
-
end
|
|
1074
|
-
end
|
|
1075
|
-
end
|
|
1076
|
-
```
|
|
1077
|
-
|
|
1078
|
-
#### Dockerized Consumer
|
|
1079
|
-
|
|
1080
|
-
```dockerfile
|
|
1081
|
-
# Dockerfile.consumer
|
|
1082
|
-
FROM ruby:3.2
|
|
1083
|
-
|
|
1084
|
-
WORKDIR /app
|
|
1085
|
-
COPY . .
|
|
1086
|
-
RUN bundle install
|
|
1087
|
-
|
|
1088
|
-
CMD ["bundle", "exec", "rake", "jetstream:consume"]
|
|
1089
|
-
```
|
|
1090
|
-
|
|
1091
|
-
```yaml
|
|
1092
|
-
# docker-compose.yml
|
|
1093
|
-
services:
|
|
1094
|
-
consumer:
|
|
1095
|
-
build:
|
|
1096
|
-
context: .
|
|
1097
|
-
dockerfile: Dockerfile.consumer
|
|
1098
|
-
environment:
|
|
1099
|
-
- NATS_URLS=nats://nats:4222
|
|
1100
|
-
- RAILS_ENV=production
|
|
1101
|
-
depends_on:
|
|
1102
|
-
- nats
|
|
1103
|
-
restart: unless-stopped
|
|
1104
|
-
```
|
|
1105
|
-
|
|
1106
|
-
---
|
|
1107
|
-
|
|
1108
|
-
## 📬 Envelope Format
|
|
1109
|
-
|
|
1110
|
-
```json
|
|
1111
|
-
{
|
|
1112
|
-
"event_id": "01H1234567890ABCDEF",
|
|
1113
|
-
"schema_version": 1,
|
|
1114
|
-
"event_type": "created",
|
|
1115
|
-
"producer": "myapp",
|
|
1116
|
-
"resource_type": "user",
|
|
1117
|
-
"resource_id": "01H1234567890ABCDEF",
|
|
1118
|
-
"occurred_at": "2025-08-13T21:00:00Z",
|
|
1119
|
-
"trace_id": "abc123",
|
|
1120
|
-
"payload": { "id": "01H...", "name": "Ada" }
|
|
1121
|
-
}
|
|
1122
|
-
```
|
|
1123
|
-
|
|
1124
|
-
* `resource_id` is inferred from `payload.id` when publishing.
|
|
1125
|
-
|
|
1126
|
-
---
|
|
1127
|
-
|
|
1128
|
-
## 🧨 Dead-Letter Queue (DLQ)
|
|
1129
|
-
|
|
1130
|
-
When enabled, the topology ensures each app has its own DLQ subject:
|
|
1131
|
-
**`{env}.{app_name}.sync.dlq`**
|
|
1132
|
-
|
|
1133
|
-
This per-app DLQ approach provides:
|
|
1134
|
-
|
|
1135
|
-
* **Isolation**: Failed messages from different services don't mix
|
|
1136
|
-
* **Easier Monitoring**: Track DLQ metrics per service
|
|
1137
|
-
* **Simpler Debugging**: Identify which service is having issues
|
|
1138
|
-
* **Independent Processing**: Each team can manage their own DLQ consumer
|
|
1139
|
-
|
|
1140
|
-
### How DLQ Works
|
|
1141
|
-
|
|
1142
|
-
Messages are automatically moved to the DLQ when:
|
|
1143
|
-
|
|
1144
|
-
* Delivery attempts exceed `max_deliver` (default: 5)
|
|
1145
|
-
* Handler raises an unrecoverable error
|
|
1146
|
-
* Message cannot be processed successfully after all retries
|
|
1147
|
-
|
|
1148
|
-
### Consuming DLQ Messages
|
|
1149
|
-
|
|
1150
|
-
You can run a separate consumer to monitor and process DLQ messages:
|
|
1151
|
-
|
|
1152
|
-
```ruby
|
|
1153
|
-
# lib/tasks/dlq_consumer.rake
|
|
1154
|
-
namespace :jetstream do
|
|
1155
|
-
desc "Process Dead Letter Queue messages"
|
|
1156
|
-
task consume_dlq: :environment do
|
|
1157
|
-
# Create a custom consumer for DLQ subject
|
|
1158
|
-
dlq_subject = JetstreamBridge.config.dlq_subject
|
|
1159
|
-
|
|
1160
|
-
consumer = JetstreamBridge::Consumer.new(
|
|
1161
|
-
durable_name: "#{JetstreamBridge.config.env}-dlq-processor"
|
|
1162
|
-
) do |event|
|
|
1163
|
-
# Log failed event for manual review
|
|
1164
|
-
Rails.logger.error(
|
|
1165
|
-
"DLQ Event: #{event.event_id}",
|
|
1166
|
-
event_type: event.type,
|
|
1167
|
-
deliveries: event.deliveries,
|
|
1168
|
-
payload: event.payload.to_h,
|
|
1169
|
-
trace_id: event.trace_id
|
|
1170
|
-
)
|
|
1171
|
-
|
|
1172
|
-
# Optionally: store in database for manual intervention
|
|
1173
|
-
FailedEvent.create!(
|
|
1174
|
-
event_id: event.event_id,
|
|
1175
|
-
event_type: event.type,
|
|
1176
|
-
payload: event.payload.to_h,
|
|
1177
|
-
deliveries: event.deliveries,
|
|
1178
|
-
failed_at: Time.current
|
|
1179
|
-
)
|
|
1180
|
-
|
|
1181
|
-
# Or attempt recovery logic
|
|
1182
|
-
case event.type
|
|
1183
|
-
when "payment.processed"
|
|
1184
|
-
# Manual payment reconciliation
|
|
1185
|
-
PaymentReconciliationService.call(event.payload.to_h)
|
|
1186
|
-
else
|
|
1187
|
-
# Alert on-call team
|
|
1188
|
-
AlertService.notify("DLQ event requires attention", event: event.to_h)
|
|
1189
|
-
end
|
|
1190
|
-
end
|
|
1191
|
-
|
|
1192
|
-
# Graceful shutdown
|
|
1193
|
-
trap("TERM") { consumer.stop! }
|
|
1194
|
-
|
|
1195
|
-
Rails.logger.info "DLQ Consumer started on #{dlq_subject}"
|
|
1196
|
-
consumer.run!
|
|
1197
|
-
end
|
|
69
|
+
User.upsert({ id: event.payload["id"] })
|
|
1198
70
|
end
|
|
71
|
+
consumer.run!
|
|
1199
72
|
```
|
|
1200
73
|
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
```bash
|
|
1204
|
-
bundle exec rake jetstream:consume_dlq
|
|
1205
|
-
```
|
|
1206
|
-
|
|
1207
|
-
### DLQ Monitoring
|
|
1208
|
-
|
|
1209
|
-
Monitor DLQ health and volume:
|
|
1210
|
-
|
|
1211
|
-
```ruby
|
|
1212
|
-
# Check DLQ message count
|
|
1213
|
-
stream_info = JetstreamBridge.stream_info
|
|
1214
|
-
dlq_count = stream_info[:messages] # Messages in DLQ subject
|
|
1215
|
-
|
|
1216
|
-
# Alert if DLQ is growing
|
|
1217
|
-
if dlq_count > 100
|
|
1218
|
-
AlertService.notify("DLQ has #{dlq_count} messages")
|
|
1219
|
-
end
|
|
1220
|
-
```
|
|
1221
|
-
|
|
1222
|
-
---
|
|
1223
|
-
|
|
1224
|
-
## 🛠 Operations Guide
|
|
1225
|
-
|
|
1226
|
-
### Monitoring
|
|
1227
|
-
|
|
1228
|
-
* **Consumer lag**: `nats consumer info <stream> <durable>`
|
|
1229
|
-
* **DLQ volume**: Monitor your app's DLQ subject `{env}.{app_name}.sync.dlq`
|
|
1230
|
-
* Example: `nats sub "production.api.sync.dlq" --count`
|
|
1231
|
-
* **Outbox backlog**: Alert on `jetstream_outbox_events` with `status != 'sent'` and growing count
|
|
1232
|
-
|
|
1233
|
-
### Scaling
|
|
1234
|
-
|
|
1235
|
-
* Run consumers in **separate processes/containers**
|
|
1236
|
-
* Scale consumers independently of web
|
|
1237
|
-
* Tune `batch_size`, `ack_wait`, `max_deliver`, and `backoff`
|
|
1238
|
-
|
|
1239
|
-
### Health Checks
|
|
1240
|
-
|
|
1241
|
-
The gem provides built-in health check functionality for monitoring:
|
|
1242
|
-
|
|
1243
|
-
```ruby
|
|
1244
|
-
# Get comprehensive health status
|
|
1245
|
-
health = JetstreamBridge.health_check
|
|
1246
|
-
# => {
|
|
1247
|
-
# healthy: true,
|
|
1248
|
-
# nats_connected: true,
|
|
1249
|
-
# connected_at: "2025-11-22T20:00:00Z",
|
|
1250
|
-
# stream: { exists: true, name: "...", ... },
|
|
1251
|
-
# config: { env: "production", ... },
|
|
1252
|
-
# version: "4.0.1"
|
|
1253
|
-
# }
|
|
1254
|
-
|
|
1255
|
-
# Force-connect & ensure topology at boot or in a check
|
|
1256
|
-
JetstreamBridge.ensure_topology!
|
|
1257
|
-
|
|
1258
|
-
# Debug helper for troubleshooting
|
|
1259
|
-
JetstreamBridge::DebugHelper.debug_info
|
|
1260
|
-
```
|
|
1261
|
-
|
|
1262
|
-
### When to Use
|
|
1263
|
-
|
|
1264
|
-
* **Inbox**: you need idempotent processing and replay safety
|
|
1265
|
-
* **Outbox**: you want “DB commit ⇒ event published (or recorded for retry)” guarantees
|
|
1266
|
-
|
|
1267
|
-
---
|
|
1268
|
-
|
|
1269
|
-
## 🧩 Troubleshooting
|
|
1270
|
-
|
|
1271
|
-
* **`subjects overlap with an existing stream`**
|
|
1272
|
-
The library pre-filters overlapping subjects and retries once. If another team owns a broad wildcard (e.g., `env.data.sync.>`), coordinate subject boundaries.
|
|
1273
|
-
|
|
1274
|
-
* **Consumer exists with mismatched filter**
|
|
1275
|
-
The library detects and recreates the durable with the desired filter subject.
|
|
1276
|
-
|
|
1277
|
-
* **Repeated redeliveries**
|
|
1278
|
-
Increase `ack_wait`, review handler acks/NACKs, or move poison messages to DLQ.
|
|
1279
|
-
|
|
1280
|
-
---
|
|
1281
|
-
|
|
1282
|
-
## 🚀 Getting Started
|
|
1283
|
-
|
|
1284
|
-
1. Add the gem & run `bundle install`
|
|
1285
|
-
2. `bin/rails g jetstream_bridge:install`
|
|
1286
|
-
3. `bin/rails db:migrate`
|
|
1287
|
-
4. Start publishing/consuming!
|
|
1288
|
-
|
|
1289
|
-
---
|
|
1290
|
-
|
|
1291
|
-
## 🤝 Contributing
|
|
1292
|
-
|
|
1293
|
-
We welcome contributions from the community! Here's how you can help:
|
|
1294
|
-
|
|
1295
|
-
### Getting Started
|
|
1296
|
-
|
|
1297
|
-
1. **Fork the repository** on GitHub
|
|
1298
|
-
2. **Clone your fork** locally:
|
|
1299
|
-
|
|
1300
|
-
```bash
|
|
1301
|
-
git clone https://github.com/YOUR_USERNAME/jetstream_bridge.git
|
|
1302
|
-
cd jetstream_bridge
|
|
1303
|
-
```
|
|
1304
|
-
|
|
1305
|
-
3. **Install dependencies**:
|
|
1306
|
-
|
|
1307
|
-
```bash
|
|
1308
|
-
bundle install
|
|
1309
|
-
```
|
|
1310
|
-
|
|
1311
|
-
4. **Set up NATS** for testing (requires Docker):
|
|
1312
|
-
|
|
1313
|
-
```bash
|
|
1314
|
-
docker run -d -p 4222:4222 nats:latest -js
|
|
1315
|
-
```
|
|
1316
|
-
|
|
1317
|
-
### Development Workflow
|
|
1318
|
-
|
|
1319
|
-
1. **Create a feature branch**:
|
|
1320
|
-
|
|
1321
|
-
```bash
|
|
1322
|
-
git checkout -b feature/your-feature-name
|
|
1323
|
-
```
|
|
1324
|
-
|
|
1325
|
-
2. **Make your changes** with tests:
|
|
1326
|
-
* Write meaningful commit messages
|
|
1327
|
-
* Add tests for new functionality
|
|
1328
|
-
* Update documentation as needed
|
|
1329
|
-
|
|
1330
|
-
3. **Run the test suite**:
|
|
1331
|
-
|
|
1332
|
-
```bash
|
|
1333
|
-
bundle exec rspec
|
|
1334
|
-
```
|
|
1335
|
-
|
|
1336
|
-
4. **Check code quality**:
|
|
1337
|
-
|
|
1338
|
-
```bash
|
|
1339
|
-
bundle exec rubocop
|
|
1340
|
-
```
|
|
1341
|
-
|
|
1342
|
-
5. **Push to your fork** and submit a pull request
|
|
1343
|
-
|
|
1344
|
-
### Code Quality Standards
|
|
1345
|
-
|
|
1346
|
-
* **Test Coverage**: Maintain >80% line coverage and >70% branch coverage
|
|
1347
|
-
* **RuboCop**: All code must pass RuboCop checks with zero offenses
|
|
1348
|
-
* **Tests**: All tests must pass before merging
|
|
1349
|
-
* **Documentation**: Update README and inline docs for new features
|
|
1350
|
-
|
|
1351
|
-
### Pull Request Guidelines
|
|
1352
|
-
|
|
1353
|
-
* **Title**: Use clear, descriptive titles (e.g., "Add health check endpoint generator")
|
|
1354
|
-
* **Description**: Explain what changes were made and why
|
|
1355
|
-
* **Tests**: Include tests for bug fixes and new features
|
|
1356
|
-
* **Documentation**: Update relevant documentation
|
|
1357
|
-
* **One feature per PR**: Keep pull requests focused and reviewable
|
|
1358
|
-
|
|
1359
|
-
### Reporting Issues
|
|
1360
|
-
|
|
1361
|
-
When reporting bugs, please include:
|
|
1362
|
-
|
|
1363
|
-
* **Ruby version**: Output of `ruby -v`
|
|
1364
|
-
* **Gem version**: Output of `bundle show jetstream_bridge`
|
|
1365
|
-
* **NATS version**: Version of NATS server
|
|
1366
|
-
* **Steps to reproduce**: Minimal example that reproduces the issue
|
|
1367
|
-
* **Expected behavior**: What you expected to happen
|
|
1368
|
-
* **Actual behavior**: What actually happened
|
|
1369
|
-
* **Logs/errors**: Relevant error messages or stack traces
|
|
1370
|
-
|
|
1371
|
-
### Feature Requests
|
|
1372
|
-
|
|
1373
|
-
We love hearing your ideas! When proposing features:
|
|
1374
|
-
|
|
1375
|
-
* Search existing issues to avoid duplicates
|
|
1376
|
-
* Describe the problem you're trying to solve
|
|
1377
|
-
* Explain your proposed solution
|
|
1378
|
-
* Consider backwards compatibility
|
|
1379
|
-
|
|
1380
|
-
## 🏗️ Development
|
|
1381
|
-
|
|
1382
|
-
### Running Tests
|
|
1383
|
-
|
|
1384
|
-
```bash
|
|
1385
|
-
# Run all tests
|
|
1386
|
-
bundle exec rspec
|
|
1387
|
-
|
|
1388
|
-
# Run specific test file
|
|
1389
|
-
bundle exec rspec spec/publisher/publisher_spec.rb
|
|
1390
|
-
|
|
1391
|
-
# Run with coverage report
|
|
1392
|
-
COVERAGE=true bundle exec rspec
|
|
1393
|
-
```
|
|
1394
|
-
|
|
1395
|
-
### Code Coverage
|
|
1396
|
-
|
|
1397
|
-
Coverage reports are generated automatically and saved to `coverage/`. View the HTML report:
|
|
1398
|
-
|
|
1399
|
-
```bash
|
|
1400
|
-
open coverage/index.html
|
|
1401
|
-
```
|
|
1402
|
-
|
|
1403
|
-
### Linting
|
|
1404
|
-
|
|
1405
|
-
```bash
|
|
1406
|
-
# Run RuboCop
|
|
1407
|
-
bundle exec rubocop
|
|
1408
|
-
|
|
1409
|
-
# Auto-fix violations
|
|
1410
|
-
bundle exec rubocop -A
|
|
1411
|
-
```
|
|
1412
|
-
|
|
1413
|
-
### Local Testing with Rails
|
|
1414
|
-
|
|
1415
|
-
To test the gem in a Rails application:
|
|
1416
|
-
|
|
1417
|
-
1. Point your Gemfile to the local path:
|
|
1418
|
-
|
|
1419
|
-
```ruby
|
|
1420
|
-
gem "jetstream_bridge", path: "../jetstream_bridge"
|
|
1421
|
-
```
|
|
1422
|
-
|
|
1423
|
-
2. Run bundle:
|
|
1424
|
-
|
|
1425
|
-
```bash
|
|
1426
|
-
bundle install
|
|
1427
|
-
```
|
|
1428
|
-
|
|
1429
|
-
## 📋 Code of Conduct
|
|
1430
|
-
|
|
1431
|
-
This project follows the [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to the project maintainers.
|
|
1432
|
-
|
|
1433
|
-
### Our Standards
|
|
1434
|
-
|
|
1435
|
-
* **Be respectful**: Treat everyone with respect and consideration
|
|
1436
|
-
* **Be inclusive**: Welcome diverse perspectives and experiences
|
|
1437
|
-
* **Be collaborative**: Work together constructively
|
|
1438
|
-
* **Be professional**: Keep discussions focused and constructive
|
|
1439
|
-
|
|
1440
|
-
## 🌟 Community
|
|
1441
|
-
|
|
1442
|
-
* **Discussions**: Use GitHub Discussions for questions and ideas
|
|
1443
|
-
* **Issues**: Report bugs and request features via GitHub Issues
|
|
1444
|
-
* **Pull Requests**: Submit improvements via pull requests
|
|
1445
|
-
|
|
1446
|
-
## 🙏 Acknowledgments
|
|
1447
|
-
|
|
1448
|
-
Built with:
|
|
1449
|
-
|
|
1450
|
-
* [NATS.io](https://nats.io) - High-performance messaging system
|
|
1451
|
-
* [nats-pure.rb](https://github.com/nats-io/nats-pure.rb) - Ruby client for NATS
|
|
1452
|
-
|
|
1453
|
-
## 📊 Project Status
|
|
74
|
+
## Documentation
|
|
1454
75
|
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
* **Semantic Versioning**: Follows [SemVer](https://semver.org/)
|
|
76
|
+
- [Getting Started](docs/GETTING_STARTED.md)
|
|
77
|
+
- [Production Guide](docs/PRODUCTION.md)
|
|
78
|
+
- [Testing with Mock NATS](docs/TESTING.md)
|
|
1459
79
|
|
|
1460
|
-
##
|
|
80
|
+
## License
|
|
1461
81
|
|
|
1462
|
-
|
|
82
|
+
MIT
|