jetstream_bridge 2.2.1 β 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/jetstream_bridge/consumer/backoff_strategy.rb +24 -0
- data/lib/jetstream_bridge/consumer/consumer.rb +16 -11
- data/lib/jetstream_bridge/consumer/consumer_config.rb +4 -10
- data/lib/jetstream_bridge/consumer/dlq_publisher.rb +53 -0
- data/lib/jetstream_bridge/consumer/message_context.rb +22 -0
- data/lib/jetstream_bridge/consumer/message_processor.rb +76 -30
- data/lib/jetstream_bridge/consumer/subscription_manager.rb +116 -19
- data/lib/jetstream_bridge/core/connection.rb +2 -7
- data/lib/jetstream_bridge/core/duration.rb +82 -20
- data/lib/jetstream_bridge/publisher/publisher.rb +21 -10
- data/lib/jetstream_bridge/topology/stream.rb +132 -75
- data/lib/jetstream_bridge/version.rb +1 -1
- data/lib/jetstream_bridge.rb +1 -1
- metadata +24 -35
- data/.github/workflows/release.yml +0 -150
- data/.gitignore +0 -56
- data/.idea/.gitignore +0 -8
- data/.idea/dictionaries/project.xml +0 -16
- data/.idea/jetstream_bridge.iml +0 -102
- data/.idea/misc.xml +0 -4
- data/.idea/modules.xml +0 -8
- data/.idea/vcs.xml +0 -6
- data/.rubocop.yml +0 -98
- data/Gemfile +0 -5
- data/Gemfile.lock +0 -268
- data/LICENSE +0 -21
- data/README.md +0 -302
- data/jetstream_bridge.gemspec +0 -60
data/Gemfile.lock
DELETED
@@ -1,268 +0,0 @@
|
|
1
|
-
PATH
|
2
|
-
remote: .
|
3
|
-
specs:
|
4
|
-
jetstream_bridge (2.2.1)
|
5
|
-
activerecord (>= 6.0)
|
6
|
-
activesupport (>= 6.0)
|
7
|
-
nats-pure (~> 2.4)
|
8
|
-
rails (>= 6.0)
|
9
|
-
|
10
|
-
GEM
|
11
|
-
remote: https://rubygems.org/
|
12
|
-
specs:
|
13
|
-
actioncable (8.0.2)
|
14
|
-
actionpack (= 8.0.2)
|
15
|
-
activesupport (= 8.0.2)
|
16
|
-
nio4r (~> 2.0)
|
17
|
-
websocket-driver (>= 0.6.1)
|
18
|
-
zeitwerk (~> 2.6)
|
19
|
-
actionmailbox (8.0.2)
|
20
|
-
actionpack (= 8.0.2)
|
21
|
-
activejob (= 8.0.2)
|
22
|
-
activerecord (= 8.0.2)
|
23
|
-
activestorage (= 8.0.2)
|
24
|
-
activesupport (= 8.0.2)
|
25
|
-
mail (>= 2.8.0)
|
26
|
-
actionmailer (8.0.2)
|
27
|
-
actionpack (= 8.0.2)
|
28
|
-
actionview (= 8.0.2)
|
29
|
-
activejob (= 8.0.2)
|
30
|
-
activesupport (= 8.0.2)
|
31
|
-
mail (>= 2.8.0)
|
32
|
-
rails-dom-testing (~> 2.2)
|
33
|
-
actionpack (8.0.2)
|
34
|
-
actionview (= 8.0.2)
|
35
|
-
activesupport (= 8.0.2)
|
36
|
-
nokogiri (>= 1.8.5)
|
37
|
-
rack (>= 2.2.4)
|
38
|
-
rack-session (>= 1.0.1)
|
39
|
-
rack-test (>= 0.6.3)
|
40
|
-
rails-dom-testing (~> 2.2)
|
41
|
-
rails-html-sanitizer (~> 1.6)
|
42
|
-
useragent (~> 0.16)
|
43
|
-
actiontext (8.0.2)
|
44
|
-
actionpack (= 8.0.2)
|
45
|
-
activerecord (= 8.0.2)
|
46
|
-
activestorage (= 8.0.2)
|
47
|
-
activesupport (= 8.0.2)
|
48
|
-
globalid (>= 0.6.0)
|
49
|
-
nokogiri (>= 1.8.5)
|
50
|
-
actionview (8.0.2)
|
51
|
-
activesupport (= 8.0.2)
|
52
|
-
builder (~> 3.1)
|
53
|
-
erubi (~> 1.11)
|
54
|
-
rails-dom-testing (~> 2.2)
|
55
|
-
rails-html-sanitizer (~> 1.6)
|
56
|
-
activejob (8.0.2)
|
57
|
-
activesupport (= 8.0.2)
|
58
|
-
globalid (>= 0.3.6)
|
59
|
-
activemodel (8.0.2)
|
60
|
-
activesupport (= 8.0.2)
|
61
|
-
activerecord (8.0.2)
|
62
|
-
activemodel (= 8.0.2)
|
63
|
-
activesupport (= 8.0.2)
|
64
|
-
timeout (>= 0.4.0)
|
65
|
-
activestorage (8.0.2)
|
66
|
-
actionpack (= 8.0.2)
|
67
|
-
activejob (= 8.0.2)
|
68
|
-
activerecord (= 8.0.2)
|
69
|
-
activesupport (= 8.0.2)
|
70
|
-
marcel (~> 1.0)
|
71
|
-
activesupport (8.0.2)
|
72
|
-
base64
|
73
|
-
benchmark (>= 0.3)
|
74
|
-
bigdecimal
|
75
|
-
concurrent-ruby (~> 1.0, >= 1.3.1)
|
76
|
-
connection_pool (>= 2.2.5)
|
77
|
-
drb
|
78
|
-
i18n (>= 1.6, < 2)
|
79
|
-
logger (>= 1.4.2)
|
80
|
-
minitest (>= 5.1)
|
81
|
-
securerandom (>= 0.3)
|
82
|
-
tzinfo (~> 2.0, >= 2.0.5)
|
83
|
-
uri (>= 0.13.1)
|
84
|
-
ast (2.4.3)
|
85
|
-
base64 (0.3.0)
|
86
|
-
benchmark (0.4.1)
|
87
|
-
bigdecimal (3.2.2)
|
88
|
-
builder (3.3.0)
|
89
|
-
bundler-audit (0.9.2)
|
90
|
-
bundler (>= 1.2.0, < 3)
|
91
|
-
thor (~> 1.0)
|
92
|
-
concurrent-ruby (1.3.5)
|
93
|
-
connection_pool (2.5.3)
|
94
|
-
crass (1.0.6)
|
95
|
-
date (3.4.1)
|
96
|
-
diff-lcs (1.6.2)
|
97
|
-
drb (2.2.3)
|
98
|
-
erubi (1.13.1)
|
99
|
-
globalid (1.2.1)
|
100
|
-
activesupport (>= 6.1)
|
101
|
-
i18n (1.14.7)
|
102
|
-
concurrent-ruby (~> 1.0)
|
103
|
-
io-console (0.8.0)
|
104
|
-
irb (1.15.2)
|
105
|
-
pp (>= 0.6.0)
|
106
|
-
rdoc (>= 4.0.0)
|
107
|
-
reline (>= 0.4.2)
|
108
|
-
json (2.13.2)
|
109
|
-
language_server-protocol (3.17.0.5)
|
110
|
-
lint_roller (1.1.0)
|
111
|
-
logger (1.7.0)
|
112
|
-
loofah (2.24.0)
|
113
|
-
crass (~> 1.0.2)
|
114
|
-
nokogiri (>= 1.12.0)
|
115
|
-
mail (2.8.1)
|
116
|
-
mini_mime (>= 0.1.1)
|
117
|
-
net-imap
|
118
|
-
net-pop
|
119
|
-
net-smtp
|
120
|
-
marcel (1.0.4)
|
121
|
-
mini_mime (1.1.5)
|
122
|
-
mini_portile2 (2.8.9)
|
123
|
-
minitest (5.25.5)
|
124
|
-
nats-pure (2.5.0)
|
125
|
-
base64
|
126
|
-
concurrent-ruby (~> 1.0)
|
127
|
-
json
|
128
|
-
securerandom
|
129
|
-
uri
|
130
|
-
net-imap (0.5.6)
|
131
|
-
date
|
132
|
-
net-protocol
|
133
|
-
net-pop (0.1.2)
|
134
|
-
net-protocol
|
135
|
-
net-protocol (0.2.2)
|
136
|
-
timeout
|
137
|
-
net-smtp (0.5.1)
|
138
|
-
net-protocol
|
139
|
-
nio4r (2.7.4)
|
140
|
-
nokogiri (1.18.7)
|
141
|
-
mini_portile2 (~> 2.8.2)
|
142
|
-
racc (~> 1.4)
|
143
|
-
nokogiri (1.18.7-arm64-darwin)
|
144
|
-
racc (~> 1.4)
|
145
|
-
parallel (1.27.0)
|
146
|
-
parser (3.3.9.0)
|
147
|
-
ast (~> 2.4.1)
|
148
|
-
racc
|
149
|
-
pp (0.6.2)
|
150
|
-
prettyprint
|
151
|
-
prettyprint (0.2.0)
|
152
|
-
prism (1.4.0)
|
153
|
-
psych (5.2.3)
|
154
|
-
date
|
155
|
-
stringio
|
156
|
-
racc (1.8.1)
|
157
|
-
rack (3.1.13)
|
158
|
-
rack-session (2.1.0)
|
159
|
-
base64 (>= 0.1.0)
|
160
|
-
rack (>= 3.0.0)
|
161
|
-
rack-test (2.2.0)
|
162
|
-
rack (>= 1.3)
|
163
|
-
rackup (2.2.1)
|
164
|
-
rack (>= 3)
|
165
|
-
rails (8.0.2)
|
166
|
-
actioncable (= 8.0.2)
|
167
|
-
actionmailbox (= 8.0.2)
|
168
|
-
actionmailer (= 8.0.2)
|
169
|
-
actionpack (= 8.0.2)
|
170
|
-
actiontext (= 8.0.2)
|
171
|
-
actionview (= 8.0.2)
|
172
|
-
activejob (= 8.0.2)
|
173
|
-
activemodel (= 8.0.2)
|
174
|
-
activerecord (= 8.0.2)
|
175
|
-
activestorage (= 8.0.2)
|
176
|
-
activesupport (= 8.0.2)
|
177
|
-
bundler (>= 1.15.0)
|
178
|
-
railties (= 8.0.2)
|
179
|
-
rails-dom-testing (2.2.0)
|
180
|
-
activesupport (>= 5.0.0)
|
181
|
-
minitest
|
182
|
-
nokogiri (>= 1.6)
|
183
|
-
rails-html-sanitizer (1.6.2)
|
184
|
-
loofah (~> 2.21)
|
185
|
-
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
186
|
-
railties (8.0.2)
|
187
|
-
actionpack (= 8.0.2)
|
188
|
-
activesupport (= 8.0.2)
|
189
|
-
irb (~> 1.13)
|
190
|
-
rackup (>= 1.0.0)
|
191
|
-
rake (>= 12.2)
|
192
|
-
thor (~> 1.0, >= 1.2.2)
|
193
|
-
zeitwerk (~> 2.6)
|
194
|
-
rainbow (3.1.1)
|
195
|
-
rake (13.2.1)
|
196
|
-
rdoc (6.13.1)
|
197
|
-
psych (>= 4.0.0)
|
198
|
-
regexp_parser (2.11.2)
|
199
|
-
reline (0.6.1)
|
200
|
-
io-console (~> 0.5)
|
201
|
-
rspec (3.13.1)
|
202
|
-
rspec-core (~> 3.13.0)
|
203
|
-
rspec-expectations (~> 3.13.0)
|
204
|
-
rspec-mocks (~> 3.13.0)
|
205
|
-
rspec-core (3.13.5)
|
206
|
-
rspec-support (~> 3.13.0)
|
207
|
-
rspec-expectations (3.13.5)
|
208
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
209
|
-
rspec-support (~> 3.13.0)
|
210
|
-
rspec-mocks (3.13.5)
|
211
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
212
|
-
rspec-support (~> 3.13.0)
|
213
|
-
rspec-support (3.13.5)
|
214
|
-
rubocop (1.79.2)
|
215
|
-
json (~> 2.3)
|
216
|
-
language_server-protocol (~> 3.17.0.2)
|
217
|
-
lint_roller (~> 1.1.0)
|
218
|
-
parallel (~> 1.10)
|
219
|
-
parser (>= 3.3.0.2)
|
220
|
-
rainbow (>= 2.2.2, < 4.0)
|
221
|
-
regexp_parser (>= 2.9.3, < 3.0)
|
222
|
-
rubocop-ast (>= 1.46.0, < 2.0)
|
223
|
-
ruby-progressbar (~> 1.7)
|
224
|
-
unicode-display_width (>= 2.4.0, < 4.0)
|
225
|
-
rubocop-ast (1.46.0)
|
226
|
-
parser (>= 3.3.7.2)
|
227
|
-
prism (~> 1.4)
|
228
|
-
rubocop-packaging (0.6.0)
|
229
|
-
lint_roller (~> 1.1.0)
|
230
|
-
rubocop (>= 1.72.1, < 2.0)
|
231
|
-
rubocop-performance (1.25.0)
|
232
|
-
lint_roller (~> 1.1)
|
233
|
-
rubocop (>= 1.75.0, < 2.0)
|
234
|
-
rubocop-ast (>= 1.38.0, < 2.0)
|
235
|
-
ruby-progressbar (1.13.0)
|
236
|
-
securerandom (0.4.1)
|
237
|
-
stringio (3.1.6)
|
238
|
-
thor (1.3.2)
|
239
|
-
timeout (0.4.3)
|
240
|
-
tzinfo (2.0.6)
|
241
|
-
concurrent-ruby (~> 1.0)
|
242
|
-
unicode-display_width (3.1.4)
|
243
|
-
unicode-emoji (~> 4.0, >= 4.0.4)
|
244
|
-
unicode-emoji (4.0.4)
|
245
|
-
uri (1.0.3)
|
246
|
-
useragent (0.16.11)
|
247
|
-
websocket-driver (0.7.7)
|
248
|
-
base64
|
249
|
-
websocket-extensions (>= 0.1.0)
|
250
|
-
websocket-extensions (0.1.5)
|
251
|
-
zeitwerk (2.7.2)
|
252
|
-
|
253
|
-
PLATFORMS
|
254
|
-
arm64-darwin-24
|
255
|
-
ruby
|
256
|
-
x86_64-linux
|
257
|
-
|
258
|
-
DEPENDENCIES
|
259
|
-
bundler-audit (>= 0.9.1)
|
260
|
-
jetstream_bridge!
|
261
|
-
rake (>= 13.0)
|
262
|
-
rspec (>= 3.12)
|
263
|
-
rubocop (~> 1.66)
|
264
|
-
rubocop-packaging (~> 0.5)
|
265
|
-
rubocop-performance (~> 1.21)
|
266
|
-
|
267
|
-
BUNDLED WITH
|
268
|
-
2.6.3
|
data/LICENSE
DELETED
@@ -1,21 +0,0 @@
|
|
1
|
-
MIT License
|
2
|
-
|
3
|
-
Copyright (c) 2025 Mike Attara
|
4
|
-
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
7
|
-
in the Software without restriction, including without limitation the rights
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
10
|
-
furnished to do so, subject to the following conditions:
|
11
|
-
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
13
|
-
copies or substantial portions of the Software.
|
14
|
-
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
-
SOFTWARE.
|
data/README.md
DELETED
@@ -1,302 +0,0 @@
|
|
1
|
-
# Jetstream Bridge
|
2
|
-
|
3
|
-
**Production-safe realtime data bridge** between systems using **NATS JetStream**.
|
4
|
-
Includes durable consumers, backpressure, retries, **DLQ**, optional **Inbox/Outbox**, and **overlap-safe stream provisioning**.
|
5
|
-
|
6
|
-
---
|
7
|
-
|
8
|
-
## β¨ Features
|
9
|
-
|
10
|
-
* π Simple **Publisher** and **Consumer** interfaces
|
11
|
-
* π‘ **Outbox** (reliable send) & **Inbox** (idempotent receive), opt-in
|
12
|
-
* 𧨠**DLQ** for poison messages
|
13
|
-
* βοΈ Durable `pull_subscribe` with backoff & `max_deliver`
|
14
|
-
* π― Clear **source/destination** subject conventions
|
15
|
-
* π§± **Overlap-safe stream ensure** (prevents βsubjects overlapβ BadRequest)
|
16
|
-
* π **Rails generators** for initializer & migrations, plus an install **rake task**
|
17
|
-
* β‘οΈ **Eager-loaded models** via Railtie (production)
|
18
|
-
* π Built-in logging for visibility
|
19
|
-
|
20
|
-
---
|
21
|
-
|
22
|
-
## π¦ Install
|
23
|
-
|
24
|
-
```ruby
|
25
|
-
# Gemfile
|
26
|
-
gem "jetstream_bridge", "~> 2.0"
|
27
|
-
```
|
28
|
-
|
29
|
-
```bash
|
30
|
-
bundle install
|
31
|
-
```
|
32
|
-
|
33
|
-
---
|
34
|
-
|
35
|
-
## π§° Rails Generators & Rake Task
|
36
|
-
|
37
|
-
From your Rails app:
|
38
|
-
|
39
|
-
```bash
|
40
|
-
# Create initializer + migrations
|
41
|
-
bin/rails g jetstream_bridge:install
|
42
|
-
|
43
|
-
# Or run them separately:
|
44
|
-
bin/rails g jetstream_bridge:initializer
|
45
|
-
bin/rails g jetstream_bridge:migrations
|
46
|
-
|
47
|
-
# Rake task (does both initializer + migrations)
|
48
|
-
bin/rake jetstream_bridge:install
|
49
|
-
```
|
50
|
-
|
51
|
-
Then:
|
52
|
-
|
53
|
-
```bash
|
54
|
-
bin/rails db:migrate
|
55
|
-
```
|
56
|
-
|
57
|
-
> The generators create:
|
58
|
-
>
|
59
|
-
> * `config/initializers/jetstream_bridge.rb`
|
60
|
-
> * `db/migrate/*_create_jetstream_outbox_events.rb`
|
61
|
-
> * `db/migrate/*_create_jetstream_inbox_events.rb`
|
62
|
-
|
63
|
-
---
|
64
|
-
|
65
|
-
## π§ Configure (Rails)
|
66
|
-
|
67
|
-
```ruby
|
68
|
-
# config/initializers/jetstream_bridge.rb
|
69
|
-
JetstreamBridge.configure do |config|
|
70
|
-
# NATS connection
|
71
|
-
config.nats_urls = ENV.fetch("NATS_URLS", "nats://localhost:4222")
|
72
|
-
config.env = ENV.fetch("NATS_ENV", "development")
|
73
|
-
config.app_name = ENV.fetch("APP_NAME", "app")
|
74
|
-
config.destination_app = ENV["DESTINATION_APP"] # required
|
75
|
-
|
76
|
-
# Consumer tuning
|
77
|
-
config.max_deliver = 5
|
78
|
-
config.ack_wait = "30s"
|
79
|
-
config.backoff = %w[1s 5s 15s 30s 60s]
|
80
|
-
|
81
|
-
# Reliability features (opt-in)
|
82
|
-
config.use_outbox = true
|
83
|
-
config.use_inbox = true
|
84
|
-
config.use_dlq = true
|
85
|
-
|
86
|
-
# Models (override if you use custom AR classes/table names)
|
87
|
-
config.outbox_model = "JetstreamBridge::OutboxEvent"
|
88
|
-
config.inbox_model = "JetstreamBridge::InboxEvent"
|
89
|
-
end
|
90
|
-
```
|
91
|
-
|
92
|
-
> **Defaults:**
|
93
|
-
>
|
94
|
-
> * `stream_name` β `#{env}-jetstream-bridge-stream`
|
95
|
-
> * `dlq_subject` β `#{env}.data.sync.dlq`
|
96
|
-
|
97
|
-
---
|
98
|
-
|
99
|
-
## π‘ Subject Conventions
|
100
|
-
|
101
|
-
| Direction | Subject Pattern |
|
102
|
-
|---------------|---------------------------|
|
103
|
-
| **Publish** | `{env}.{app}.sync.{dest}` |
|
104
|
-
| **Subscribe** | `{env}.{dest}.sync.{app}` |
|
105
|
-
| **DLQ** | `{env}.sync.dlq` |
|
106
|
-
|
107
|
-
* `{app}`: `app_name`
|
108
|
-
* `{dest}`: `destination_app`
|
109
|
-
* `{env}`: `env`
|
110
|
-
|
111
|
-
---
|
112
|
-
|
113
|
-
## π§± Stream Topology (auto-ensure and overlap-safe)
|
114
|
-
|
115
|
-
On first connection, Jetstream Bridge **ensures** a single stream exists for your `env` and that it covers:
|
116
|
-
|
117
|
-
* `source_subject` (`{env}.{app}.sync.{dest}`)
|
118
|
-
* `destination_subject` (`{env}.{dest}.sync.{app}`)
|
119
|
-
* `dlq_subject` (if enabled)
|
120
|
-
|
121
|
-
Itβs **overlap-safe**:
|
122
|
-
|
123
|
-
* Skips adding subjects already covered by existing wildcards
|
124
|
-
* Pre-filters subjects owned by other streams to avoid `BadRequest: subjects overlap with an existing stream`
|
125
|
-
* Retries once on concurrent races, then logs and continues safely
|
126
|
-
|
127
|
-
---
|
128
|
-
|
129
|
-
## π Database Setup (Inbox / Outbox)
|
130
|
-
|
131
|
-
Inbox/Outbox are **optional**. The library detects columns at runtime and only sets what exists, so you can start minimal and evolve later.
|
132
|
-
|
133
|
-
### Generator-created tables (recommended)
|
134
|
-
|
135
|
-
```ruby
|
136
|
-
# jetstream_outbox_events
|
137
|
-
create_table :jetstream_outbox_events do |t|
|
138
|
-
t.string :event_id, null: false
|
139
|
-
t.string :subject, null: false
|
140
|
-
t.jsonb :payload, null: false, default: {}
|
141
|
-
t.jsonb :headers, null: false, default: {}
|
142
|
-
t.string :status, null: false, default: "pending" # pending|publishing|sent|failed
|
143
|
-
t.integer :attempts, null: false, default: 0
|
144
|
-
t.text :last_error
|
145
|
-
t.datetime :enqueued_at
|
146
|
-
t.datetime :sent_at
|
147
|
-
t.timestamps
|
148
|
-
end
|
149
|
-
add_index :jetstream_outbox_events, :event_id, unique: true
|
150
|
-
add_index :jetstream_outbox_events, :status
|
151
|
-
|
152
|
-
# jetstream_inbox_events
|
153
|
-
create_table :jetstream_inbox_events do |t|
|
154
|
-
t.string :event_id # preferred dedupe key
|
155
|
-
t.string :subject, null: false
|
156
|
-
t.jsonb :payload, null: false, default: {}
|
157
|
-
t.jsonb :headers, null: false, default: {}
|
158
|
-
t.string :stream
|
159
|
-
t.bigint :stream_seq
|
160
|
-
t.integer :deliveries
|
161
|
-
t.string :status, null: false, default: "received" # received|processing|processed|failed
|
162
|
-
t.text :last_error
|
163
|
-
t.datetime :received_at
|
164
|
-
t.datetime :processed_at
|
165
|
-
t.timestamps
|
166
|
-
end
|
167
|
-
add_index :jetstream_inbox_events, :event_id, unique: true, where: 'event_id IS NOT NULL'
|
168
|
-
add_index :jetstream_inbox_events, [:stream, :stream_seq], unique: true, where: 'stream IS NOT NULL AND stream_seq IS NOT NULL'
|
169
|
-
add_index :jetstream_inbox_events, :status
|
170
|
-
```
|
171
|
-
|
172
|
-
> Already have different table names? Point the config to your AR classes via `config.outbox_model` / `config.inbox_model`.
|
173
|
-
|
174
|
-
---
|
175
|
-
|
176
|
-
## π€ Publish Events
|
177
|
-
|
178
|
-
```ruby
|
179
|
-
publisher = JetstreamBridge::Publisher.new
|
180
|
-
publisher.publish(
|
181
|
-
resource_type: "user",
|
182
|
-
event_type: "created",
|
183
|
-
payload: { id: "01H...", name: "Ada" }, # resource_id inferred from payload[:id] / payload["id"]
|
184
|
-
# optional:
|
185
|
-
# event_id: "uuid-or-ulid",
|
186
|
-
# trace_id: "hex",
|
187
|
-
# occurred_at: Time.now.utc
|
188
|
-
)
|
189
|
-
```
|
190
|
-
|
191
|
-
If **Outbox** is enabled, the publish call:
|
192
|
-
|
193
|
-
* Upserts an outbox row by `event_id`
|
194
|
-
* Publishes with `Nats-Msg-Id` (idempotent)
|
195
|
-
* Marks status `sent` or records `failed` with `last_error`
|
196
|
-
|
197
|
-
---
|
198
|
-
|
199
|
-
## π₯ Consume Events
|
200
|
-
|
201
|
-
```ruby
|
202
|
-
JetstreamBridge::Consumer.new(
|
203
|
-
durable_name: "#{Rails.env}-#{app_name}-workers",
|
204
|
-
batch_size: 25
|
205
|
-
) do |event, subject, deliveries|
|
206
|
-
# Your idempotent domain logic here
|
207
|
-
# `event` is the parsed envelope hash
|
208
|
-
UserCreatedHandler.call(event["payload"])
|
209
|
-
end.run!
|
210
|
-
```
|
211
|
-
|
212
|
-
If **Inbox** is enabled, the consumer:
|
213
|
-
|
214
|
-
* Dedupes by `event_id` (falls back to stream sequence if needed)
|
215
|
-
* Records processing state, errors, and timestamps
|
216
|
-
* Skips already-processed messages (acks immediately)
|
217
|
-
|
218
|
-
---
|
219
|
-
|
220
|
-
## π¬ Envelope Format
|
221
|
-
|
222
|
-
```json
|
223
|
-
{
|
224
|
-
"event_id": "01H1234567890ABCDEF",
|
225
|
-
"schema_version": 1,
|
226
|
-
"event_type": "created",
|
227
|
-
"producer": "myapp",
|
228
|
-
"resource_type": "user",
|
229
|
-
"resource_id": "01H1234567890ABCDEF",
|
230
|
-
"occurred_at": "2025-08-13T21:00:00Z",
|
231
|
-
"trace_id": "abc123",
|
232
|
-
"payload": { "id": "01H...", "name": "Ada" }
|
233
|
-
}
|
234
|
-
```
|
235
|
-
|
236
|
-
* `resource_id` is inferred from `payload.id` when publishing.
|
237
|
-
|
238
|
-
---
|
239
|
-
|
240
|
-
## 𧨠Dead-Letter Queue (DLQ)
|
241
|
-
|
242
|
-
When enabled, the topology ensures the DLQ subject exists:
|
243
|
-
**`{env}.data.sync.dlq`**
|
244
|
-
|
245
|
-
You may run a separate process to subscribe and triage messages that exceed `max_deliver` or are NAKβed to the DLQ.
|
246
|
-
|
247
|
-
---
|
248
|
-
|
249
|
-
## π Operations Guide
|
250
|
-
|
251
|
-
### Monitoring
|
252
|
-
|
253
|
-
* **Consumer lag**: `nats consumer info <stream> <durable>`
|
254
|
-
* **DLQ volume**: subscribe/metrics on `{env}.data.sync.dlq`
|
255
|
-
* **Outbox backlog**: alert on `jetstream_outbox_events` with `status != 'sent'` and growing count
|
256
|
-
|
257
|
-
### Scaling
|
258
|
-
|
259
|
-
* Run consumers in **separate processes/containers**
|
260
|
-
* Scale consumers independently of web
|
261
|
-
* Tune `batch_size`, `ack_wait`, `max_deliver`, and `backoff`
|
262
|
-
|
263
|
-
### Health check
|
264
|
-
|
265
|
-
* Force-connect & ensure topology at boot or in a check:
|
266
|
-
|
267
|
-
```ruby
|
268
|
-
JetstreamBridge.ensure_topology!
|
269
|
-
```
|
270
|
-
|
271
|
-
### When to Use
|
272
|
-
|
273
|
-
* **Inbox**: you need idempotent processing and replay safety
|
274
|
-
* **Outbox**: you want βDB commit β event published (or recorded for retry)β guarantees
|
275
|
-
|
276
|
-
---
|
277
|
-
|
278
|
-
## π§© Troubleshooting
|
279
|
-
|
280
|
-
* **`subjects overlap with an existing stream`**
|
281
|
-
The library pre-filters overlapping subjects and retries once. If another team owns a broad wildcard (e.g., `env.data.sync.>`), coordinate subject boundaries.
|
282
|
-
|
283
|
-
* **Consumer exists with mismatched filter**
|
284
|
-
The library detects and recreates the durable with the desired filter subject.
|
285
|
-
|
286
|
-
* **Repeated redeliveries**
|
287
|
-
Increase `ack_wait`, review handler acks/NACKs, or move poison messages to DLQ.
|
288
|
-
|
289
|
-
---
|
290
|
-
|
291
|
-
## π Getting Started
|
292
|
-
|
293
|
-
1. Add the gem & run `bundle install`
|
294
|
-
2. `bin/rails g jetstream_bridge:install`
|
295
|
-
3. `bin/rails db:migrate`
|
296
|
-
4. Start publishing/consuming!
|
297
|
-
|
298
|
-
---
|
299
|
-
|
300
|
-
## π License
|
301
|
-
|
302
|
-
[MIT License](LICENSE)
|
data/jetstream_bridge.gemspec
DELETED
@@ -1,60 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative 'lib/jetstream_bridge/version'
|
4
|
-
|
5
|
-
Gem::Specification.new do |spec|
|
6
|
-
spec.name = 'jetstream_bridge'
|
7
|
-
spec.version = JetstreamBridge::VERSION
|
8
|
-
spec.authors = ['Mike Attara']
|
9
|
-
spec.email = ['mpyebattara@gmail.com']
|
10
|
-
|
11
|
-
# Clear, value-focused copy
|
12
|
-
spec.summary = 'Reliable realtime bridge over NATS JetStream for Rails/Ruby apps'
|
13
|
-
spec.description = <<~DESC.strip
|
14
|
-
Publisher/Consumer utilities for NATS JetStream with environment-scoped subjects,
|
15
|
-
overlap guards, DLQ routing, retries/backoff, and optional Inbox/Outbox patterns.
|
16
|
-
Includes topology setup helpers for production-safe operation.
|
17
|
-
DESC
|
18
|
-
|
19
|
-
spec.license = 'MIT'
|
20
|
-
spec.homepage = 'https://github.com/attaradev/jetstream_bridge'
|
21
|
-
|
22
|
-
# Ruby & RubyGems requirements
|
23
|
-
spec.required_ruby_version = '>= 2.7.0'
|
24
|
-
spec.required_rubygems_version = '>= 3.3.0'
|
25
|
-
|
26
|
-
# Rich metadata for RubyGems.org
|
27
|
-
spec.metadata = {
|
28
|
-
'homepage_uri' => 'https://github.com/attaradev/jetstream_bridge',
|
29
|
-
'source_code_uri' => 'https://github.com/attaradev/jetstream_bridge',
|
30
|
-
'changelog_uri' => 'https://github.com/attaradev/jetstream_bridge/blob/main/CHANGELOG.md',
|
31
|
-
'documentation_uri' => 'https://github.com/attaradev/jetstream_bridge#readme',
|
32
|
-
'bug_tracker_uri' => 'https://github.com/attaradev/jetstream_bridge/issues',
|
33
|
-
'github_repo' => 'ssh://github.com/attaradev/jetstream_bridge',
|
34
|
-
'rubygems_mfa_required' => 'true'
|
35
|
-
}
|
36
|
-
|
37
|
-
# Safer file list for published gem
|
38
|
-
# (falls back to Dir[] if not in a git repo β e.g., CI tarballs)
|
39
|
-
spec.files = if system('git rev-parse --is-inside-work-tree > /dev/null 2>&1')
|
40
|
-
`git ls-files -z`.split("\x0").reject { |f| f.start_with?('spec/fixtures/') }
|
41
|
-
else
|
42
|
-
Dir['lib/**/*', 'README*', 'CHANGELOG*', 'LICENSE*']
|
43
|
-
end
|
44
|
-
|
45
|
-
spec.require_paths = ['lib']
|
46
|
-
|
47
|
-
# Runtime dependencies
|
48
|
-
spec.add_dependency 'activerecord', '>= 6.0'
|
49
|
-
spec.add_dependency 'activesupport', '>= 6.0'
|
50
|
-
spec.add_dependency 'nats-pure', '~> 2.4'
|
51
|
-
spec.add_dependency 'rails', '>= 6.0'
|
52
|
-
|
53
|
-
# Development / quality dependencies
|
54
|
-
spec.add_development_dependency 'bundler-audit', '>= 0.9.1'
|
55
|
-
spec.add_development_dependency 'rake', '>= 13.0'
|
56
|
-
spec.add_development_dependency 'rspec', '>= 3.12'
|
57
|
-
spec.add_development_dependency 'rubocop', '~> 1.66'
|
58
|
-
spec.add_development_dependency 'rubocop-packaging', '~> 0.5'
|
59
|
-
spec.add_development_dependency 'rubocop-performance', '~> 1.21'
|
60
|
-
end
|