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