durable_streams-rails 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +274 -0
- data/Rakefile +11 -2
- data/app/controllers/durable_streams/auth_controller.rb +24 -0
- data/config/routes.rb +3 -0
- data/lib/durable_streams/engine.rb +11 -1
- data/lib/durable_streams/server_config.rb +62 -0
- data/lib/durable_streams/testing.rb +1 -1
- data/lib/durable_streams/version.rb +1 -1
- data/lib/durable_streams-rails.rb +16 -0
- data/lib/generators/durable_streams/install/USAGE +16 -0
- data/lib/generators/durable_streams/install/install_generator.rb +62 -0
- data/lib/generators/durable_streams/install/templates/bin/durable-streams.tt +27 -0
- data/lib/generators/durable_streams/install/templates/durable_streams.yml.tt +32 -0
- data/lib/generators/durable_streams/install/templates/initializer.rb.tt +1 -0
- data/lib/tasks/durable_streams.rake +42 -0
- metadata +18 -11
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 486bb79705018c3e6bfbdaa56f2b072b0ac3781c54e07a9cc16c62e9834bfed9
|
|
4
|
+
data.tar.gz: 9f73ab9352ba33d19eb079503d04a865567eb0cddb4810acba6e223d8b9de461
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 86aae6ff003e0f17253c8efa06780b0cabb18a58f6c0aad1abd0373c85797db95ce08ecc42785d72ffb3a46c756a0702324df9f0fa4f50dc751e1f1ff375a5ae
|
|
7
|
+
data.tar.gz: 44c1c3aac3c5a9d0d96e76cd39e80471d2796dae296cbb60c1fc73bc46b8f63fdcea1d2cfb4d1a270639b3c8318b6716f98e322cfce289a7ee0bc22099903cd0
|
data/README.md
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# Durable Streams Rails
|
|
2
|
+
|
|
3
|
+
Durable Streams integration for Rails. Stream State Protocol events from your models to clients
|
|
4
|
+
over SSE with the same developer experience as Turbo Broadcasts — but with offset-based
|
|
5
|
+
resumability, persistent event logs, and no WebSocket infrastructure.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
class Comment < ApplicationRecord
|
|
9
|
+
belongs_to :post
|
|
10
|
+
streams_to :post
|
|
11
|
+
end
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
That's it. Creates, updates, and destroys are broadcast as State Protocol events to all clients
|
|
15
|
+
subscribed to the post's stream. No manual stream management, no JSON serialization, no offset
|
|
16
|
+
tracking.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
Add to your Gemfile:
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
gem "durable_streams-rails"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Run the install generator:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
bin/rails generate durable_streams:install
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
This creates:
|
|
33
|
+
|
|
34
|
+
- `config/initializers/durable_streams.rb` — sets the stream server URL
|
|
35
|
+
- `config/durable_streams.yml` — server configuration (per environment)
|
|
36
|
+
- `bin/durable-streams` — binstub that auto-downloads and runs the server binary
|
|
37
|
+
- Updates `Procfile.dev` with a `streams:` entry
|
|
38
|
+
- Updates `.gitignore` to exclude `bin/dist/`
|
|
39
|
+
|
|
40
|
+
### Pin a server version
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
bin/rails generate durable_streams:install --version=0.1.0
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Disable route auto-mounting
|
|
47
|
+
|
|
48
|
+
The gem auto-mounts the auth verification endpoint at `/durable_streams/auth/verify`.
|
|
49
|
+
To disable:
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
# config/initializers/durable_streams.rb
|
|
53
|
+
Rails.application.config.durable_streams.draw_routes = false
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Configuration
|
|
57
|
+
|
|
58
|
+
The server reads `config/durable_streams.yml`:
|
|
59
|
+
|
|
60
|
+
```yaml
|
|
61
|
+
default: &default
|
|
62
|
+
port: 4437
|
|
63
|
+
# route: /v1/streams/*
|
|
64
|
+
auth:
|
|
65
|
+
url: http://localhost:3000
|
|
66
|
+
path: /durable_streams/auth/verify
|
|
67
|
+
copy_headers:
|
|
68
|
+
- Cookie
|
|
69
|
+
|
|
70
|
+
development:
|
|
71
|
+
<<: *default
|
|
72
|
+
|
|
73
|
+
production:
|
|
74
|
+
<<: *default
|
|
75
|
+
domain: streams.example.com
|
|
76
|
+
auth:
|
|
77
|
+
url: https://app.example.com
|
|
78
|
+
path: /durable_streams/auth/verify
|
|
79
|
+
copy_headers:
|
|
80
|
+
- Cookie
|
|
81
|
+
- Authorization
|
|
82
|
+
storage:
|
|
83
|
+
data_dir: /var/data/durable-streams
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Power users can bypass the YAML entirely:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
DURABLE_STREAMS_CONFIG=/path/to/Caddyfile bin/durable-streams
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Deployment with Kamal
|
|
93
|
+
|
|
94
|
+
The server binary runs as a separate role in the same Docker image — same pattern as Solid Queue:
|
|
95
|
+
|
|
96
|
+
```yaml
|
|
97
|
+
# config/deploy.yml
|
|
98
|
+
servers:
|
|
99
|
+
web:
|
|
100
|
+
cmd: bin/rails server
|
|
101
|
+
jobs:
|
|
102
|
+
cmd: bin/jobs
|
|
103
|
+
streams:
|
|
104
|
+
cmd: bin/durable-streams
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Pre-download the binary during Docker build:
|
|
108
|
+
|
|
109
|
+
```dockerfile
|
|
110
|
+
RUN bin/rails durable_streams:download
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Or set a specific version:
|
|
114
|
+
|
|
115
|
+
```dockerfile
|
|
116
|
+
RUN DURABLE_STREAMS_VERSION=0.1.0 bin/rails durable_streams:download
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Usage
|
|
120
|
+
|
|
121
|
+
### Declarative streaming
|
|
122
|
+
|
|
123
|
+
Stream to an association:
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
class Comment < ApplicationRecord
|
|
127
|
+
belongs_to :post
|
|
128
|
+
streams_to :post
|
|
129
|
+
end
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Stream to self:
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
class Board < ApplicationRecord
|
|
136
|
+
streams
|
|
137
|
+
end
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
With a proc for custom stream targets:
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
class Comment < ApplicationRecord
|
|
144
|
+
belongs_to :post
|
|
145
|
+
streams_to ->(comment) { [comment.post, :comments] }
|
|
146
|
+
end
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Instance methods
|
|
150
|
+
|
|
151
|
+
Synchronous (returns `txid` for optimistic update confirmation):
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
txid = comment.stream_insert_to post
|
|
155
|
+
txid = comment.stream_update_to post
|
|
156
|
+
txid = comment.stream_upsert_to post
|
|
157
|
+
comment.stream_delete_to post
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Asynchronous (via `DurableStreams::BroadcastJob`):
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
comment.stream_insert_later_to post
|
|
164
|
+
comment.stream_update_later_to post
|
|
165
|
+
comment.stream_upsert_later_to post
|
|
166
|
+
comment.stream_delete_later_to post
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Self-targeting (streams to the model itself):
|
|
170
|
+
|
|
171
|
+
```ruby
|
|
172
|
+
comment.stream_insert # same as stream_insert_to(self)
|
|
173
|
+
comment.stream_update_later # same as stream_update_later_to(self)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Direct broadcasting
|
|
177
|
+
|
|
178
|
+
For non-ActiveRecord use cases (presence, typing indicators):
|
|
179
|
+
|
|
180
|
+
```ruby
|
|
181
|
+
DurableStreams.broadcast_to(room, :presence,
|
|
182
|
+
type: "presence",
|
|
183
|
+
key: user.id.to_s,
|
|
184
|
+
value: { id: user.id, name: user.name },
|
|
185
|
+
headers: { operation: "insert" }
|
|
186
|
+
)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Signed stream URLs
|
|
190
|
+
|
|
191
|
+
Generate signed, expirable URLs for client SSE connections:
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
url = DurableStreams.signed_stream_url(room, :messages)
|
|
195
|
+
# => "http://localhost:4437/v1/streams/gid://app/Room/1/messages?token=eyJ..."
|
|
196
|
+
|
|
197
|
+
url = DurableStreams.signed_stream_url(room, :messages, expires_in: 1.hour)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Suppressing broadcasts
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
Comment.suppressing_streams do
|
|
204
|
+
Comment.create!(post: post) # no broadcast
|
|
205
|
+
end
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Testing
|
|
209
|
+
|
|
210
|
+
The gem provides test helpers that mirror Turbo's `assert_turbo_stream_broadcasts`. They are
|
|
211
|
+
automatically included in `ActiveSupport::TestCase` via the engine.
|
|
212
|
+
|
|
213
|
+
During tests, `DurableStreams::Testing` intercepts all broadcasts at the transport layer —
|
|
214
|
+
events are captured in-memory instead of being sent to the stream server. This means tests
|
|
215
|
+
verify the Rails integration (DSL wiring, event shape, callbacks, jobs, suppression) without
|
|
216
|
+
requiring a running Durable Streams server. Same pattern as Turbo, where `ActionCable::TestHelper`
|
|
217
|
+
captures broadcasts without a WebSocket connection.
|
|
218
|
+
|
|
219
|
+
```ruby
|
|
220
|
+
class CommentTest < ActiveSupport::TestCase
|
|
221
|
+
test "creating comment streams to post" do
|
|
222
|
+
assert_stream_broadcasts @post, count: 1 do
|
|
223
|
+
@post.comments.create!(body: "Hello")
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
test "capture stream events" do
|
|
228
|
+
events = capture_stream_broadcasts @post do
|
|
229
|
+
@post.comments.create!(body: "Hello")
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
assert_equal "insert", events.first["headers"]["operation"]
|
|
233
|
+
assert_equal "Hello", events.first["value"]["body"]
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
test "no broadcasts when suppressed" do
|
|
237
|
+
assert_no_stream_broadcasts @post do
|
|
238
|
+
Comment.suppressing_streams do
|
|
239
|
+
@post.comments.create!(body: "Silent")
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Architecture
|
|
247
|
+
|
|
248
|
+
This gem mirrors `turbo-rails` 1:1 in structure and design:
|
|
249
|
+
|
|
250
|
+
| Turbo Rails | Durable Streams Rails | Role |
|
|
251
|
+
|---|---|---|
|
|
252
|
+
| `Turbo::Streams::StreamName` | `DurableStreams::StreamName` | Name generation (private `stream_name_from`) |
|
|
253
|
+
| `Turbo::Streams::Broadcasts` | `DurableStreams::Broadcasts` | Flatten/compact guards, serialization, transport |
|
|
254
|
+
| `Turbo::StreamsChannel` | `DurableStreams` module | Entry point (extends StreamName + Broadcasts) |
|
|
255
|
+
| `Turbo::Broadcastable` | `DurableStreams::Broadcastable` | Model concern, delegates to module |
|
|
256
|
+
| `Turbo::Streams::ActionBroadcastJob` | `DurableStreams::BroadcastJob` | Async job, delegates to module |
|
|
257
|
+
| `ActionCable::TestHelper` | `DurableStreams::Testing` | Test transport interception |
|
|
258
|
+
| `Turbo::Broadcastable::TestHelper` | `DurableStreams::Broadcastable::TestHelper` | Test assertions |
|
|
259
|
+
|
|
260
|
+
The key architectural difference: Turbo broadcasts HTML fragments over WebSockets (Action Cable).
|
|
261
|
+
This gem broadcasts JSON State Protocol events over HTTP/SSE (Durable Streams).
|
|
262
|
+
|
|
263
|
+
## Dependencies
|
|
264
|
+
|
|
265
|
+
- [`durable_streams`](https://github.com/tokimonki/durable_streams) — Ruby client for the Durable Streams protocol (resolved automatically via RubyGems)
|
|
266
|
+
- `railties` >= 8.0
|
|
267
|
+
|
|
268
|
+
## Future
|
|
269
|
+
|
|
270
|
+
Brewing secretly — an async-backed durable streams server inside Rails.
|
|
271
|
+
|
|
272
|
+
## License
|
|
273
|
+
|
|
274
|
+
MIT
|
data/Rakefile
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
require "bundler/setup"
|
|
2
|
+
require "bundler/gem_tasks"
|
|
2
3
|
require "rake/testtask"
|
|
3
4
|
|
|
5
|
+
APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
|
|
6
|
+
load "rails/tasks/engine.rake"
|
|
7
|
+
|
|
4
8
|
Rake::TestTask.new(:test) do |t|
|
|
5
9
|
t.libs << "test"
|
|
6
|
-
t.
|
|
10
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
|
7
11
|
t.verbose = false
|
|
8
12
|
end
|
|
9
13
|
|
|
10
|
-
task
|
|
14
|
+
task :test_prereq do
|
|
15
|
+
puts "Preparing test database"
|
|
16
|
+
`cd test/dummy && RAILS_ENV=test bin/rails db:migrate`
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
task default: [ :test_prereq, :test ]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module DurableStreams
|
|
2
|
+
class AuthController < ActionController::API
|
|
3
|
+
def verify
|
|
4
|
+
if stream_name
|
|
5
|
+
head :ok
|
|
6
|
+
else
|
|
7
|
+
head :unauthorized
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
def stream_name
|
|
13
|
+
if token = extract_token
|
|
14
|
+
DurableStreams.verified_stream_name(token)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def extract_token
|
|
19
|
+
if uri = request.headers["X-Forwarded-Uri"]
|
|
20
|
+
DurableStreams.extract_token_from_url(uri)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
data/config/routes.rb
ADDED
|
@@ -6,11 +6,14 @@ module DurableStreams
|
|
|
6
6
|
config.eager_load_namespaces << DurableStreams
|
|
7
7
|
config.durable_streams = ActiveSupport::OrderedOptions.new
|
|
8
8
|
config.autoload_once_paths = %W(
|
|
9
|
+
#{root}/app/controllers
|
|
9
10
|
#{root}/app/models
|
|
10
11
|
#{root}/app/models/concerns
|
|
11
12
|
#{root}/app/jobs
|
|
12
13
|
)
|
|
13
14
|
|
|
15
|
+
# If the parent application does not use Active Job, app/jobs cannot
|
|
16
|
+
# be eager loaded, because it references the ActiveJob constant.
|
|
14
17
|
initializer "durable_streams.no_active_job", before: :set_eager_load_paths do
|
|
15
18
|
unless defined?(ActiveJob)
|
|
16
19
|
Rails.autoloaders.once.do_not_eager_load("#{root}/app/jobs")
|
|
@@ -25,14 +28,21 @@ module DurableStreams
|
|
|
25
28
|
end
|
|
26
29
|
end
|
|
27
30
|
|
|
31
|
+
initializer "durable_streams.configs" do
|
|
32
|
+
config.after_initialize do |app|
|
|
33
|
+
DurableStreams.draw_routes = app.config.durable_streams.draw_routes != false
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
28
37
|
initializer "durable_streams.signed_stream_verifier_key" do
|
|
29
38
|
config.after_initialize do
|
|
30
39
|
DurableStreams.signed_stream_verifier_key =
|
|
31
|
-
config.durable_streams
|
|
40
|
+
config.durable_streams.signed_stream_verifier_key ||
|
|
32
41
|
Rails.application.key_generator.generate_key("durable_streams/signed_stream_verifier_key")
|
|
33
42
|
end
|
|
34
43
|
end
|
|
35
44
|
|
|
45
|
+
# No Action Cable dependency -- load test helpers directly.
|
|
36
46
|
initializer "durable_streams.test_assertions" do
|
|
37
47
|
ActiveSupport.on_load(:active_support_test_case) do
|
|
38
48
|
if defined?(ActiveJob)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
require "yaml"
|
|
2
|
+
require "erb"
|
|
3
|
+
|
|
4
|
+
module DurableStreams
|
|
5
|
+
class ServerConfig
|
|
6
|
+
TEMPLATE = ERB.new(<<~'CADDYFILE', trim_mode: "-")
|
|
7
|
+
{
|
|
8
|
+
admin off
|
|
9
|
+
<%- unless @config["domain"] -%>
|
|
10
|
+
auto_https off
|
|
11
|
+
<%- end -%>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
<%= address %> {
|
|
15
|
+
route <%= route_path %> {
|
|
16
|
+
forward_auth <%= auth["url"] %> {
|
|
17
|
+
uri <%= auth["path"] %>
|
|
18
|
+
<%- copy_headers.each do |header| -%>
|
|
19
|
+
copy_headers <%= header %>
|
|
20
|
+
<%- end -%>
|
|
21
|
+
}
|
|
22
|
+
durable_streams<%= storage_block %>
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
CADDYFILE
|
|
26
|
+
|
|
27
|
+
def self.generate(config_path, environment)
|
|
28
|
+
new(config_path, environment).generate
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def initialize(config_path, environment)
|
|
32
|
+
@config = YAML.load_file(config_path, aliases: true).fetch(environment)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def generate
|
|
36
|
+
TEMPLATE.result(binding)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
def address
|
|
41
|
+
@config["domain"] || ":#{@config.fetch("port", 4437)}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def route_path
|
|
45
|
+
@config.fetch("route", "/v1/streams/*")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def auth
|
|
49
|
+
@config.fetch("auth")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def copy_headers
|
|
53
|
+
auth.fetch("copy_headers", [ "Cookie" ])
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def storage_block
|
|
57
|
+
if dir = @config.dig("storage", "data_dir")
|
|
58
|
+
" {\n\t\t\tdata_dir #{dir}\n\t\t}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -5,10 +5,12 @@ require "durable_streams/engine"
|
|
|
5
5
|
require "active_support/core_ext/module/attribute_accessors_per_thread"
|
|
6
6
|
|
|
7
7
|
module DurableStreams
|
|
8
|
+
extend ActiveSupport::Autoload
|
|
8
9
|
extend DurableStreams::StreamName
|
|
9
10
|
extend DurableStreams::Broadcasts
|
|
10
11
|
|
|
11
12
|
mattr_accessor :base_url
|
|
13
|
+
mattr_accessor :draw_routes, default: true
|
|
12
14
|
|
|
13
15
|
class << self
|
|
14
16
|
attr_writer :signed_stream_verifier_key
|
|
@@ -26,5 +28,19 @@ module DurableStreams
|
|
|
26
28
|
token = signed_stream_verifier.generate(path, expires_in: expires_in)
|
|
27
29
|
"#{base_url}/#{path}?token=#{token}"
|
|
28
30
|
end
|
|
31
|
+
|
|
32
|
+
# Verifies the signed token embedded in a stream URL. Used by the auth controller
|
|
33
|
+
# to validate forward_auth requests from the Durable Streams server.
|
|
34
|
+
def verify_signed_url(url)
|
|
35
|
+
if token = extract_token_from_url(url)
|
|
36
|
+
verified_stream_name(token)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def extract_token_from_url(url)
|
|
41
|
+
if query = URI(url).query
|
|
42
|
+
Rack::Utils.parse_query(query)["token"]
|
|
43
|
+
end
|
|
44
|
+
end
|
|
29
45
|
end
|
|
30
46
|
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Description:
|
|
2
|
+
Sets up Durable Streams for your Rails application.
|
|
3
|
+
|
|
4
|
+
Creates the server config (config/durable_streams.yml), binstub
|
|
5
|
+
(bin/durable-streams), initializer, and updates Procfile.dev.
|
|
6
|
+
The server binary downloads automatically on first run.
|
|
7
|
+
|
|
8
|
+
Set DURABLE_STREAMS_CONFIG to a file path to use a raw server
|
|
9
|
+
config instead of the YAML-based configuration.
|
|
10
|
+
|
|
11
|
+
Examples:
|
|
12
|
+
bin/rails generate durable_streams:install
|
|
13
|
+
|
|
14
|
+
bin/rails generate durable_streams:install --version=0.1.0
|
|
15
|
+
|
|
16
|
+
bin/rails generate durable_streams:install --skip-procfile
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module DurableStreams
|
|
2
|
+
module Generators
|
|
3
|
+
class InstallGenerator < ::Rails::Generators::Base
|
|
4
|
+
source_root File.expand_path("templates", __dir__)
|
|
5
|
+
|
|
6
|
+
class_option :version,
|
|
7
|
+
type: :string,
|
|
8
|
+
desc: "Durable Streams server version (default: latest)",
|
|
9
|
+
default: "latest"
|
|
10
|
+
class_option :skip_procfile,
|
|
11
|
+
type: :boolean,
|
|
12
|
+
desc: "Skip Procfile.dev update",
|
|
13
|
+
default: false
|
|
14
|
+
|
|
15
|
+
def create_initializer
|
|
16
|
+
template "initializer.rb", "config/initializers/durable_streams.rb"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def create_server_config
|
|
20
|
+
template "durable_streams.yml", "config/durable_streams.yml"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def create_binstub
|
|
24
|
+
template "bin/durable-streams", "bin/durable-streams"
|
|
25
|
+
chmod "bin/durable-streams", 0755, verbose: false
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def update_procfile
|
|
29
|
+
return if options[:skip_procfile]
|
|
30
|
+
|
|
31
|
+
procfile = "Procfile.dev"
|
|
32
|
+
entry = "streams: bin/durable-streams\n"
|
|
33
|
+
|
|
34
|
+
if File.exist?(procfile) && !File.read(procfile).match?(/^streams:/)
|
|
35
|
+
append_file procfile, entry
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def update_gitignore
|
|
40
|
+
if File.exist?(".gitignore") && !File.read(".gitignore").match?(/^bin\/dist/)
|
|
41
|
+
append_file ".gitignore", "\n# Durable Streams server binary\nbin/dist/\n"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def show_instructions
|
|
46
|
+
say ""
|
|
47
|
+
say " Durable Streams installed!", :green
|
|
48
|
+
say ""
|
|
49
|
+
say " Configuration: config/durable_streams.yml"
|
|
50
|
+
say " Server binary: bin/durable-streams (auto-downloads on first run)"
|
|
51
|
+
say ""
|
|
52
|
+
say " Start development:"
|
|
53
|
+
say " bin/dev"
|
|
54
|
+
say ""
|
|
55
|
+
say " Stream endpoint: http://localhost:4437/v1/streams/*"
|
|
56
|
+
say ""
|
|
57
|
+
say " Power users: set DURABLE_STREAMS_CONFIG to use a raw server config file."
|
|
58
|
+
say ""
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
APP_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
5
|
+
BIN_PATH="${APP_ROOT}/bin/dist/durable-streams-server"
|
|
6
|
+
|
|
7
|
+
# Download if not cached
|
|
8
|
+
if [ ! -x "$BIN_PATH" ]; then
|
|
9
|
+
DURABLE_STREAMS_VERSION="${DURABLE_STREAMS_VERSION:-<%= options[:version] %>}" \
|
|
10
|
+
bundle exec rails durable_streams:download
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
# Power user: use raw config file directly
|
|
14
|
+
if [ -n "${DURABLE_STREAMS_CONFIG:-}" ]; then
|
|
15
|
+
exec "$BIN_PATH" run --config "$DURABLE_STREAMS_CONFIG" "$@"
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
# Standard: generate config from YAML
|
|
19
|
+
RAILS_ENV="${RAILS_ENV:-development}"
|
|
20
|
+
CONFIG_FILE="${APP_ROOT}/tmp/durable_streams.caddyfile"
|
|
21
|
+
|
|
22
|
+
mkdir -p "${APP_ROOT}/tmp"
|
|
23
|
+
APP_ROOT="$APP_ROOT" RAILS_ENV="$RAILS_ENV" bundle exec ruby -r durable_streams/server_config \
|
|
24
|
+
-e "puts DurableStreams::ServerConfig.generate(File.join(ENV['APP_ROOT'], 'config/durable_streams.yml'), ENV['RAILS_ENV'])" \
|
|
25
|
+
> "$CONFIG_FILE"
|
|
26
|
+
|
|
27
|
+
exec "$BIN_PATH" run --config "$CONFIG_FILE" "$@"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Durable Streams server configuration.
|
|
2
|
+
# The server binary reads this on startup via bin/durable-streams.
|
|
3
|
+
#
|
|
4
|
+
# Power users: set DURABLE_STREAMS_CONFIG=/path/to/config to use a
|
|
5
|
+
# custom server config file directly, bypassing this YAML.
|
|
6
|
+
|
|
7
|
+
default: &default
|
|
8
|
+
port: 4437
|
|
9
|
+
# route: /v1/streams/*
|
|
10
|
+
auth:
|
|
11
|
+
url: http://localhost:3000
|
|
12
|
+
path: /durable_streams/auth/verify
|
|
13
|
+
copy_headers:
|
|
14
|
+
- Cookie
|
|
15
|
+
|
|
16
|
+
development:
|
|
17
|
+
<<: *default
|
|
18
|
+
|
|
19
|
+
test:
|
|
20
|
+
<<: *default
|
|
21
|
+
|
|
22
|
+
production:
|
|
23
|
+
<<: *default
|
|
24
|
+
domain: streams.example.com
|
|
25
|
+
auth:
|
|
26
|
+
url: https://app.example.com
|
|
27
|
+
path: /durable_streams/auth/verify
|
|
28
|
+
copy_headers:
|
|
29
|
+
- Cookie
|
|
30
|
+
- Authorization
|
|
31
|
+
storage:
|
|
32
|
+
data_dir: /var/data/durable-streams
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
DurableStreams.base_url = ENV.fetch("DURABLE_STREAMS_URL", "http://localhost:4437/v1/streams")
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
namespace :durable_streams do
|
|
2
|
+
desc "Download the Durable Streams server binary for the current platform"
|
|
3
|
+
task :download do
|
|
4
|
+
version = ENV.fetch("DURABLE_STREAMS_VERSION", "latest")
|
|
5
|
+
install_dir = Rails.root.join("bin", "dist")
|
|
6
|
+
bin_path = install_dir.join("durable-streams-server")
|
|
7
|
+
|
|
8
|
+
if bin_path.exist?
|
|
9
|
+
puts "Durable Streams server already installed at #{bin_path}"
|
|
10
|
+
next
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
os = RbConfig::CONFIG["host_os"].then { |s|
|
|
14
|
+
if s.match?(/darwin/i) then "darwin"
|
|
15
|
+
elsif s.match?(/linux/i) then "linux"
|
|
16
|
+
else abort "Unsupported OS: #{s}"
|
|
17
|
+
end
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
arch = RbConfig::CONFIG["host_cpu"].then { |s|
|
|
21
|
+
case s
|
|
22
|
+
when /x86_64|amd64/ then "amd64"
|
|
23
|
+
when /aarch64|arm64/ then "arm64"
|
|
24
|
+
else abort "Unsupported architecture: #{s}"
|
|
25
|
+
end
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
FileUtils.mkdir_p(install_dir)
|
|
29
|
+
|
|
30
|
+
if version == "latest"
|
|
31
|
+
url = "https://github.com/durable-streams/durable-streams/releases/latest/download/durable-streams-server_#{os}_#{arch}.tar.gz"
|
|
32
|
+
else
|
|
33
|
+
url = "https://github.com/durable-streams/durable-streams/releases/download/caddy-v#{version}/durable-streams-server_#{version}_#{os}_#{arch}.tar.gz"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
puts "Downloading durable-streams-server #{version} (#{os}/#{arch})..."
|
|
37
|
+
system("curl", "-sL", url, "-o", "-", out: IO.popen(["tar", "xz", "-C", install_dir.to_s], "w").fileno) ||
|
|
38
|
+
abort("Download failed")
|
|
39
|
+
FileUtils.chmod(0755, bin_path)
|
|
40
|
+
puts "Installed to #{bin_path}"
|
|
41
|
+
end
|
|
42
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: durable_streams-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- tokimonki
|
|
@@ -27,38 +27,45 @@ dependencies:
|
|
|
27
27
|
name: railties
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
|
-
- - "~>"
|
|
31
|
-
- !ruby/object:Gem::Version
|
|
32
|
-
version: '7.1'
|
|
33
30
|
- - ">="
|
|
34
31
|
- !ruby/object:Gem::Version
|
|
35
|
-
version:
|
|
32
|
+
version: '8.0'
|
|
36
33
|
type: :runtime
|
|
37
34
|
prerelease: false
|
|
38
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
39
36
|
requirements:
|
|
40
|
-
- - "~>"
|
|
41
|
-
- !ruby/object:Gem::Version
|
|
42
|
-
version: '7.1'
|
|
43
37
|
- - ">="
|
|
44
38
|
- !ruby/object:Gem::Version
|
|
45
|
-
version:
|
|
39
|
+
version: '8.0'
|
|
40
|
+
description: Stream State Protocol events from Active Record models with the same
|
|
41
|
+
DX as Turbo Broadcasts — declarative streaming, automatic callbacks, and async jobs
|
|
42
|
+
— but with offset-based resumability and persistent event logs over SSE.
|
|
46
43
|
email: opensource@tokimonki.com
|
|
47
44
|
executables: []
|
|
48
45
|
extensions: []
|
|
49
46
|
extra_rdoc_files: []
|
|
50
47
|
files:
|
|
51
48
|
- MIT-LICENSE
|
|
49
|
+
- README.md
|
|
52
50
|
- Rakefile
|
|
51
|
+
- app/controllers/durable_streams/auth_controller.rb
|
|
53
52
|
- app/jobs/durable_streams/broadcast_job.rb
|
|
54
53
|
- app/models/concerns/durable_streams/broadcastable.rb
|
|
54
|
+
- config/routes.rb
|
|
55
55
|
- lib/durable_streams-rails.rb
|
|
56
56
|
- lib/durable_streams/broadcastable/test_helper.rb
|
|
57
57
|
- lib/durable_streams/broadcasts.rb
|
|
58
58
|
- lib/durable_streams/engine.rb
|
|
59
|
+
- lib/durable_streams/server_config.rb
|
|
59
60
|
- lib/durable_streams/stream_name.rb
|
|
60
61
|
- lib/durable_streams/testing.rb
|
|
61
62
|
- lib/durable_streams/version.rb
|
|
63
|
+
- lib/generators/durable_streams/install/USAGE
|
|
64
|
+
- lib/generators/durable_streams/install/install_generator.rb
|
|
65
|
+
- lib/generators/durable_streams/install/templates/bin/durable-streams.tt
|
|
66
|
+
- lib/generators/durable_streams/install/templates/durable_streams.yml.tt
|
|
67
|
+
- lib/generators/durable_streams/install/templates/initializer.rb.tt
|
|
68
|
+
- lib/tasks/durable_streams.rake
|
|
62
69
|
homepage: https://github.com/tokimonki/durable_streams-rails
|
|
63
70
|
licenses:
|
|
64
71
|
- MIT
|
|
@@ -70,14 +77,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
70
77
|
requirements:
|
|
71
78
|
- - ">="
|
|
72
79
|
- !ruby/object:Gem::Version
|
|
73
|
-
version: '3.
|
|
80
|
+
version: '3.2'
|
|
74
81
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
75
82
|
requirements:
|
|
76
83
|
- - ">="
|
|
77
84
|
- !ruby/object:Gem::Version
|
|
78
85
|
version: '0'
|
|
79
86
|
requirements: []
|
|
80
|
-
rubygems_version:
|
|
87
|
+
rubygems_version: 4.0.6
|
|
81
88
|
specification_version: 4
|
|
82
89
|
summary: Durable Streams integration for Rails
|
|
83
90
|
test_files: []
|