jetstream_bridge 5.1.0 → 7.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +37 -0
  3. data/README.md +6 -3
  4. data/docs/API.md +395 -0
  5. data/docs/ARCHITECTURE.md +123 -171
  6. data/docs/GETTING_STARTED.md +72 -1
  7. data/docs/PRODUCTION.md +10 -3
  8. data/docs/RESTRICTED_PERMISSIONS.md +7 -14
  9. data/docs/TESTING.md +3 -3
  10. data/lib/generators/jetstream_bridge/migrations/templates/create_jetstream_inbox_events.rb.erb +29 -13
  11. data/lib/jetstream_bridge/config_helpers/lifecycle.rb +34 -0
  12. data/lib/jetstream_bridge/config_helpers.rb +118 -0
  13. data/lib/jetstream_bridge/consumer/consumer.rb +131 -41
  14. data/lib/jetstream_bridge/consumer/consumer_state.rb +58 -0
  15. data/lib/jetstream_bridge/consumer/inbox/inbox_repository.rb +12 -2
  16. data/lib/jetstream_bridge/consumer/pull_subscription_builder.rb +6 -6
  17. data/lib/jetstream_bridge/consumer/subscription_manager.rb +72 -110
  18. data/lib/jetstream_bridge/core/config.rb +31 -0
  19. data/lib/jetstream_bridge/core/connection.rb +97 -31
  20. data/lib/jetstream_bridge/core/consumer_mode_resolver.rb +64 -0
  21. data/lib/jetstream_bridge/core/duration.rb +30 -0
  22. data/lib/jetstream_bridge/models/inbox_event.rb +1 -1
  23. data/lib/jetstream_bridge/models/outbox_event.rb +1 -1
  24. data/lib/jetstream_bridge/provisioner.rb +108 -13
  25. data/lib/jetstream_bridge/publisher/outbox_repository.rb +35 -20
  26. data/lib/jetstream_bridge/publisher/publisher.rb +4 -4
  27. data/lib/jetstream_bridge/tasks/install.rake +2 -2
  28. data/lib/jetstream_bridge/topology/stream.rb +6 -1
  29. data/lib/jetstream_bridge/topology/topology.rb +1 -1
  30. data/lib/jetstream_bridge/version.rb +1 -1
  31. data/lib/jetstream_bridge.rb +8 -12
  32. metadata +7 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '087fb2993c66be217aa3b544a333d0b166aec88f9a006051010e46309faac51f'
4
- data.tar.gz: 2489d32b3047955699c11ae640014baec66855cad384543b3220fe778ff2b65a
3
+ metadata.gz: c1fefccd3db6dc8c8a197300fdb40f2240248aad44357e42a920b0a6b58bf4b5
4
+ data.tar.gz: c83639e8450df86ae9ccc0de89dd54e0e6ffa280b39a276105e9c94fd7b36b5b
5
5
  SHA512:
6
- metadata.gz: 88e316fb7cfc4f5048290ca37e3c065fc7cb6e059627f5900455c14adf59957c9f8db8e428b82f879511979db2e52ff1a15decb754c7156f9a51f81d766d30f3
7
- data.tar.gz: 897a1695d32323c7d33dcc8a3811cb9a7952e45088d14b8c81713f08085869805d97a40de34c3c10f33ae8c39861972ff4f9c1cff1af57699c10aa5e8bf05a50
6
+ metadata.gz: ca4d68e73467236d2b8c38f4d6edbaea1d1a0c335fb19e7eb85a25ebba8e9adab5ddd8df4bf990bfca326ae7b03f7d7138b7289df86cf3a5709a614434ad46ad
7
+ data.tar.gz: 47cab006718f6e1e75f531db8d996e7a2d18e065949996ebd40f12db57bcb90d8329a05381a64292953c218b4076b1e747894c81e7344d5dc1606623b093bf55
data/CHANGELOG.md CHANGED
@@ -5,6 +5,43 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [7.0.0] - 2026-01-30
9
+
10
+ ### Added
11
+
12
+ - Full Rails reference examples (non-restrictive and restrictive) with Docker Compose, provisioner service, and end-to-end test scripts.
13
+ - Architecture and API reference docs covering topology, consumer modes, and public surface.
14
+
15
+ ### Changed
16
+
17
+ - Major refactor separating provisioning from runtime to support least-privilege deployments; `auto_provision=false` now avoids JetStream management APIs at runtime.
18
+ - Config helpers simplified bidirectional setup (`configure_bidirectional`, `setup_rails_lifecycle`) replacing verbose initializer boilerplate.
19
+ - Consumer pipeline hardened: push-consumer mode, safer signal handling, improved drain behavior, and pull subscription shim reliability.
20
+
21
+ ### Fixed
22
+
23
+ - Provisioner keyword argument handling, generator load edge cases, and connection/consumer recovery during JetStream context refresh.
24
+
25
+ ## [5.0.0] - 2026-01-30
26
+
27
+ ### Added
28
+
29
+ - **Push consumer mode** for restricted NATS credentials — `consumer_mode: :push` with optional `delivery_subject`, no `$JS.API.*` permissions required, documented in the restricted permissions guide.
30
+ - **Reference examples**: full Rails apps for non-restrictive (auto-provisioning) and restrictive (least-privilege + provisioner) deployments with Docker Compose and end-to-end test scripts.
31
+ - **ConfigHelpers & docs**: helper to configure bidirectional sync in one call, Rails lifecycle helper, new Architecture/API docs describing topology and public surface.
32
+
33
+ ### Changed
34
+
35
+ - **Provisioning flow** separated from runtime: when `auto_provision=false` runtime skips JetStream management APIs; provisioning handled via rake task/CLI with admin creds. Health/test_connection honor restricted mode.
36
+ - **Consumer reliability**: refactored subscription builder, trap-safe signal handling, push-consumer drain via `next_msg`, sturdier pull subscription shim, JetStream context refresh retries after reconnects.
37
+ - **Quality & coverage**: generator template tweaks, provisioner keyword fixes, and test suite expanded (~96% coverage) across provisioning, consumer, and connection lifecycle paths.
38
+
39
+ ### Fixed
40
+
41
+ - Fixed TypeError/timeout handling in push consumer drain and process loop.
42
+ - Fixed Rails generator load edge case and provisioner keyword argument bugs.
43
+ - Guarded JetStream API calls when constants are private, preserving connection state during JetStream context refresh failures.
44
+
8
45
  ## [4.4.1] - 2026-01-16
9
46
 
10
47
  ### Fixed
data/README.md CHANGED
@@ -20,21 +20,22 @@
20
20
  </a>
21
21
  </p>
22
22
 
23
- Production-ready NATS JetStream bridge for Ruby/Rails with outbox, inbox, DLQ, and overlap-safe stream provisioning.
23
+ Production-ready NATS JetStream bridge for Ruby/Rails with outbox, inbox, DLQ, and overlap-safe stream provisioning.
24
24
 
25
25
  ## Highlights
26
26
 
27
27
  - Transactional outbox and idempotent inbox (optional) for exactly-once pipelines.
28
- - Durable pull consumers with retries, backoff, and DLQ routing.
28
+ - Durable pull (default) or push consumers with retries, backoff, and DLQ routing.
29
29
  - Auto stream/consumer provisioning with overlap protection.
30
30
  - Rails-native: generators, migrations, health check, and eager-loading safety.
31
+ - Least-privilege friendly: run with `auto_provision=false` plus pre-created consumers.
31
32
  - Mock NATS for fast, no-infra testing.
32
33
 
33
34
  ## Quick Start
34
35
 
35
36
  ```ruby
36
37
  # Gemfile
37
- gem "jetstream_bridge", "~> 5.0"
38
+ gem "jetstream_bridge", "~> 7.0"
38
39
  ```
39
40
 
40
41
  ```bash
@@ -63,10 +64,12 @@ consumer.run!
63
64
  ## Documentation
64
65
 
65
66
  - [Getting Started](docs/GETTING_STARTED.md) - Setup, configuration, and basic usage
67
+ - [API Reference](docs/API.md) - Complete API documentation for all public methods
66
68
  - [Architecture & Topology](docs/ARCHITECTURE.md) - Internal architecture, message flow, and patterns
67
69
  - [Production Guide](docs/PRODUCTION.md) - Production deployment and monitoring
68
70
  - [Restricted Permissions & Provisioning](docs/RESTRICTED_PERMISSIONS.md) - Manual provisioning and security
69
71
  - [Testing with Mock NATS](docs/TESTING.md) - Fast, no-infra testing
72
+ - [Reference Examples (Rails 7)](examples/README.md) - Non-restrictive and restrictive Dockerized examples with E2E tests
70
73
 
71
74
  ## License
72
75
 
data/docs/API.md ADDED
@@ -0,0 +1,395 @@
1
+ # API Reference
2
+
3
+ Complete reference for JetstreamBridge public API.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Configuration](#configuration)
8
+ - [Lifecycle Methods](#lifecycle-methods)
9
+ - [Publishing](#publishing)
10
+ - [Consuming](#consuming)
11
+ - [Provisioning](#provisioning)
12
+ - [Health & Diagnostics](#health--diagnostics)
13
+ - [Models](#models)
14
+
15
+ ## Configuration
16
+
17
+ ### `JetstreamBridge.configure`
18
+
19
+ Configure the library. Must be called before connecting.
20
+
21
+ ```ruby
22
+ JetstreamBridge.configure do |config|
23
+ # Required
24
+ config.app_name = "my_app"
25
+ config.destination_app = "other_app"
26
+
27
+ # Connection
28
+ config.nats_urls = "nats://localhost:4222" # or array of URLs
29
+ config.stream_name = "jetstream-bridge-stream"
30
+
31
+ # Features
32
+ config.use_outbox = true # Transactional publish (requires ActiveRecord)
33
+ config.use_inbox = true # Idempotent consume (requires ActiveRecord)
34
+ config.use_dlq = true # Dead letter queue for poison messages
35
+
36
+ # Consumer settings
37
+ config.durable_name = "#{app_name}-workers"
38
+ config.max_deliver = 5 # Max delivery attempts
39
+ config.ack_wait = "30s" # Time to wait for ACK
40
+ config.backoff = ["1s", "5s", "15s", "30s", "60s"]
41
+
42
+ # Provisioning
43
+ config.auto_provision = true # Auto-create stream/consumer on startup
44
+
45
+ # Connection behavior
46
+ config.lazy_connect = false # Set true to skip autostart
47
+ config.connect_retry_attempts = 3
48
+ config.connect_retry_delay = 1 # seconds
49
+ end
50
+ ```
51
+
52
+ ### `JetstreamBridge.config`
53
+
54
+ Returns the current configuration object (read-only).
55
+
56
+ ```ruby
57
+ stream_name = JetstreamBridge.config.stream_name
58
+ ```
59
+
60
+ ## Lifecycle Methods
61
+
62
+ ### `JetstreamBridge.startup!`
63
+
64
+ Explicitly start the connection and provision topology (if `auto_provision=true`).
65
+
66
+ ```ruby
67
+ JetstreamBridge.startup!
68
+ ```
69
+
70
+ **Note:** Rails applications auto-start after initialization. Non-Rails apps should call this manually or rely on auto-connect on first publish/subscribe.
71
+
72
+ ### `JetstreamBridge.shutdown!`
73
+
74
+ Gracefully close the NATS connection.
75
+
76
+ ```ruby
77
+ JetstreamBridge.shutdown!
78
+ ```
79
+
80
+ ### `JetstreamBridge.reset!`
81
+
82
+ Reset all internal state (for testing).
83
+
84
+ ```ruby
85
+ JetstreamBridge.reset!
86
+ ```
87
+
88
+ ## Publishing
89
+
90
+ ### `JetstreamBridge.publish`
91
+
92
+ Publish an event to the destination app.
93
+
94
+ ```ruby
95
+ JetstreamBridge.publish(
96
+ event_type: "user.created", # Required
97
+ resource_type: "user", # Required
98
+ resource_id: user.id, # Optional
99
+ payload: { id: user.id, email: user.email },
100
+ headers: { correlation_id: "..." }, # Optional
101
+ event_id: "custom-uuid" # Optional (auto-generated)
102
+ )
103
+ ```
104
+
105
+ **Returns:** `JetstreamBridge::PublishResult`
106
+
107
+ **With Outbox:**
108
+
109
+ ```ruby
110
+ # Transactional publish (commits with your DB transaction)
111
+ User.transaction do
112
+ user.save!
113
+ JetstreamBridge.publish(event_type: "user.created", resource_type: "user", payload: user)
114
+ end
115
+ ```
116
+
117
+ ### `JetstreamBridge.publish!`
118
+
119
+ Like `publish` but raises on error.
120
+
121
+ ```ruby
122
+ JetstreamBridge.publish!(event_type: "user.created", resource_type: "user", payload: data)
123
+ ```
124
+
125
+ **Raises:** `JetstreamBridge::PublishError` on failure
126
+
127
+ ### `JetstreamBridge.publish_batch`
128
+
129
+ Publish multiple events efficiently.
130
+
131
+ ```ruby
132
+ results = JetstreamBridge.publish_batch do |batch|
133
+ users.each do |user|
134
+ batch.publish(event_type: "user.created", resource_type: "user", payload: user)
135
+ end
136
+ end
137
+
138
+ puts "Published: #{results.success_count}, Failed: #{results.failure_count}"
139
+ ```
140
+
141
+ ## Consuming
142
+
143
+ ### `JetstreamBridge::Consumer.new`
144
+
145
+ Create a consumer to process incoming events.
146
+
147
+ ```ruby
148
+ consumer = JetstreamBridge::Consumer.new do |event|
149
+ # Process event
150
+ User.upsert({ id: event.payload["id"], email: event.payload["email"] })
151
+ end
152
+ ```
153
+
154
+ **Options:**
155
+
156
+ ```ruby
157
+ consumer = JetstreamBridge::Consumer.new(
158
+ batch_size: 10, # Process up to 10 messages at once
159
+ error_handler: ->(error, event) { logger.error(error) }
160
+ ) do |event|
161
+ # ...
162
+ end
163
+ ```
164
+
165
+ ### `Consumer#run!`
166
+
167
+ Start consuming messages (blocks until interrupted).
168
+
169
+ ```ruby
170
+ consumer.run!
171
+ ```
172
+
173
+ ### `Consumer#stop!`
174
+
175
+ Gracefully stop the consumer.
176
+
177
+ ```ruby
178
+ consumer.stop!
179
+ ```
180
+
181
+ ### Event Object
182
+
183
+ The event object passed to your handler:
184
+
185
+ ```ruby
186
+ event.event_id # => "evt_123"
187
+ event.event_type # => "user.created"
188
+ event.resource_type # => "user"
189
+ event.resource_id # => "456"
190
+ event.payload # => { "id" => 456, "email" => "..." }
191
+ event.headers # => { "correlation_id" => "..." }
192
+ event.subject # => "source_app.sync.my_app"
193
+ event.stream # => "jetstream-bridge-stream"
194
+ event.seq # => 123
195
+ event.deliveries # => 1
196
+ ```
197
+
198
+ ## Provisioning
199
+
200
+ ### `JetstreamBridge.provision!`
201
+
202
+ Manually provision stream and consumer.
203
+
204
+ ```ruby
205
+ # Provision both stream and consumer
206
+ JetstreamBridge.provision!
207
+
208
+ # Provision stream only
209
+ JetstreamBridge.provision!(provision_consumer: false)
210
+ ```
211
+
212
+ ### `JetstreamBridge::Provisioner`
213
+
214
+ Dedicated provisioning class for advanced use cases.
215
+
216
+ ```ruby
217
+ provisioner = JetstreamBridge::Provisioner.new
218
+
219
+ # Provision everything
220
+ provisioner.provision!
221
+
222
+ # Or separately
223
+ provisioner.provision_stream!
224
+ provisioner.provision_consumer!
225
+ ```
226
+
227
+ ## Health & Diagnostics
228
+
229
+ ### `JetstreamBridge.health_check`
230
+
231
+ Get comprehensive health status.
232
+
233
+ ```ruby
234
+ health = JetstreamBridge.health_check
235
+
236
+ health[:status] # => "healthy" | "unhealthy"
237
+ health[:connected] # => true/false
238
+ health[:stream_exists] # => true/false
239
+ health[:messages] # => 123
240
+ health[:consumers] # => 2
241
+ health[:nats_rtt_ms] # => 1.2
242
+ health[:version] # => "7.0.0"
243
+ ```
244
+
245
+ ### `JetstreamBridge.stream_info`
246
+
247
+ Get detailed stream information.
248
+
249
+ ```ruby
250
+ info = JetstreamBridge.stream_info
251
+
252
+ info[:name] # => "jetstream-bridge-stream"
253
+ info[:subjects] # => ["app1.sync.app2", ...]
254
+ info[:messages] # => 1000
255
+ info[:bytes] # => 204800
256
+ info[:first_seq] # => 1
257
+ info[:last_seq] # => 1000
258
+ info[:consumer_count] # => 2
259
+ ```
260
+
261
+ ### `JetstreamBridge.connection_info`
262
+
263
+ Get NATS connection details.
264
+
265
+ ```ruby
266
+ info = JetstreamBridge.connection_info
267
+
268
+ info[:connected] # => true
269
+ info[:servers] # => ["nats://localhost:4222"]
270
+ info[:connected_at] # => 2024-01-29 12:00:00 UTC
271
+ ```
272
+
273
+ ## Models
274
+
275
+ ### `JetstreamBridge::OutboxEvent`
276
+
277
+ ActiveRecord model for outbox events (when `use_outbox=true`).
278
+
279
+ ```ruby
280
+ # Create outbox event
281
+ event = JetstreamBridge::OutboxEvent.create!(
282
+ event_id: SecureRandom.uuid,
283
+ event_type: "user.created",
284
+ resource_type: "user",
285
+ resource_id: "123",
286
+ payload: { id: 123, email: "user@example.com" },
287
+ subject: "my_app.sync.other_app",
288
+ status: "pending"
289
+ )
290
+
291
+ # Query
292
+ JetstreamBridge::OutboxEvent.pending.limit(100)
293
+ JetstreamBridge::OutboxEvent.failed
294
+
295
+ # Mark as published
296
+ event.mark_published!
297
+
298
+ # Cleanup old events
299
+ JetstreamBridge::OutboxEvent.cleanup_published(older_than: 7.days)
300
+ ```
301
+
302
+ ### `JetstreamBridge::InboxEvent`
303
+
304
+ ActiveRecord model for inbox events (when `use_inbox=true`).
305
+
306
+ ```ruby
307
+ # Find by event_id
308
+ event = JetstreamBridge::InboxEvent.find_by(event_id: "evt_123")
309
+
310
+ # Query
311
+ JetstreamBridge::InboxEvent.received
312
+ JetstreamBridge::InboxEvent.processing
313
+ JetstreamBridge::InboxEvent.processed
314
+ JetstreamBridge::InboxEvent.failed
315
+ JetstreamBridge::InboxEvent.recent(100)
316
+
317
+ # Mark as processed
318
+ event.mark_processed!
319
+
320
+ # Mark as failed
321
+ event.mark_failed!("Error message")
322
+
323
+ # Cleanup old events
324
+ JetstreamBridge::InboxEvent.cleanup_processed(older_than: 30.days)
325
+
326
+ # Statistics
327
+ stats = JetstreamBridge::InboxEvent.processing_stats
328
+ stats[:total] # => 1000
329
+ stats[:processed] # => 950
330
+ stats[:failed] # => 30
331
+ stats[:pending] # => 20
332
+ ```
333
+
334
+ ## Error Handling
335
+
336
+ ### `JetstreamBridge::PublishError`
337
+
338
+ Raised by `publish!` when publishing fails.
339
+
340
+ ```ruby
341
+ begin
342
+ JetstreamBridge.publish!(event_type: "test", resource_type: "test", payload: {})
343
+ rescue JetstreamBridge::PublishError => e
344
+ logger.error("Publish failed: #{e.message}")
345
+ logger.error("Event ID: #{e.event_id}")
346
+ logger.error("Subject: #{e.subject}")
347
+ end
348
+ ```
349
+
350
+ ### Custom Error Handler
351
+
352
+ ```ruby
353
+ consumer = JetstreamBridge::Consumer.new(
354
+ error_handler: lambda { |error, event|
355
+ logger.error("Failed to process event #{event.event_id}: #{error.message}")
356
+ Sentry.capture_exception(error, extra: { event_id: event.event_id })
357
+ }
358
+ ) do |event|
359
+ # Process event
360
+ end
361
+ ```
362
+
363
+ ## Testing
364
+
365
+ ### Test Mode
366
+
367
+ Enable mock NATS for testing without infrastructure.
368
+
369
+ ```ruby
370
+ # RSpec
371
+ RSpec.configure do |config|
372
+ config.before(:each, :jetstream) do
373
+ JetstreamBridge::TestHelpers.enable_test_mode!
374
+ end
375
+
376
+ config.after(:each, :jetstream) do
377
+ JetstreamBridge::TestHelpers.reset_test_mode!
378
+ end
379
+ end
380
+
381
+ # Test
382
+ it "publishes events", :jetstream do
383
+ result = JetstreamBridge.publish(event_type: "test", resource_type: "test", payload: {})
384
+ expect(result).to be_success
385
+ end
386
+ ```
387
+
388
+ See [TESTING.md](TESTING.md) for comprehensive testing documentation.
389
+
390
+ ## See Also
391
+
392
+ - [Getting Started](GETTING_STARTED.md) - Setup and basic usage
393
+ - [Architecture](ARCHITECTURE.md) - Internal architecture and patterns
394
+ - [Production Guide](PRODUCTION.md) - Production deployment
395
+ - [Testing Guide](TESTING.md) - Testing with Mock NATS