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.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +106 -0
  3. data/README.md +22 -1402
  4. data/docs/GETTING_STARTED.md +92 -0
  5. data/docs/PRODUCTION.md +503 -0
  6. data/docs/TESTING.md +414 -0
  7. data/lib/jetstream_bridge/consumer/consumer.rb +101 -5
  8. data/lib/jetstream_bridge/consumer/inbox/inbox_processor.rb +17 -3
  9. data/lib/jetstream_bridge/consumer/inbox/inbox_repository.rb +19 -7
  10. data/lib/jetstream_bridge/consumer/message_processor.rb +88 -52
  11. data/lib/jetstream_bridge/consumer/subscription_manager.rb +24 -15
  12. data/lib/jetstream_bridge/core/bridge_helpers.rb +85 -0
  13. data/lib/jetstream_bridge/core/config.rb +27 -4
  14. data/lib/jetstream_bridge/core/connection.rb +162 -13
  15. data/lib/jetstream_bridge/core.rb +8 -0
  16. data/lib/jetstream_bridge/models/inbox_event.rb +13 -7
  17. data/lib/jetstream_bridge/models/outbox_event.rb +2 -2
  18. data/lib/jetstream_bridge/publisher/publisher.rb +10 -5
  19. data/lib/jetstream_bridge/rails/integration.rb +153 -0
  20. data/lib/jetstream_bridge/rails/railtie.rb +53 -0
  21. data/lib/jetstream_bridge/rails.rb +5 -0
  22. data/lib/jetstream_bridge/tasks/install.rake +1 -1
  23. data/lib/jetstream_bridge/test_helpers/fixtures.rb +41 -0
  24. data/lib/jetstream_bridge/test_helpers/integration_helpers.rb +77 -0
  25. data/lib/jetstream_bridge/test_helpers/matchers.rb +98 -0
  26. data/lib/jetstream_bridge/test_helpers/mock_nats.rb +524 -0
  27. data/lib/jetstream_bridge/test_helpers.rb +85 -121
  28. data/lib/jetstream_bridge/topology/overlap_guard.rb +46 -0
  29. data/lib/jetstream_bridge/topology/stream.rb +7 -4
  30. data/lib/jetstream_bridge/version.rb +1 -1
  31. data/lib/jetstream_bridge.rb +138 -63
  32. metadata +32 -12
  33. 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
- <p align="center">
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
- ## Features
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
- ### Core Capabilities
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 = "other_app" # Required: The app you're communicating with
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
- # Idempotent Inbox: Prevents duplicate processing
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
- #### 2. Run Consumer in Background Thread
59
+ Publish:
759
60
 
760
61
  ```ruby
761
- # Returns a Thread instead of Consumer
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
- #### 3. Consumer with Handler Object
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
- # event: Models::Event object with structured access
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
- **Run the DLQ consumer:**
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
- * **CI/CD**: Automated testing and code quality checks
1456
- * **Code Coverage**: 85%+ maintained
1457
- * **Active Development**: Regular updates and maintenance
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
- ## 📄 License
80
+ ## License
1461
81
 
1462
- [MIT License](LICENSE) - Copyright (c) 2025 Mike Attara
82
+ MIT