datastar 1.0.0.beta.1 → 1.0.0.beta.3
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 +58 -6
- data/lib/datastar/configuration.rb +8 -3
- data/lib/datastar/consts.rb +1 -5
- data/lib/datastar/dispatcher.rb +32 -8
- data/lib/datastar/railtie.rb +2 -0
- data/lib/datastar/server_sent_event_generator.rb +7 -1
- data/lib/datastar/version.rb +1 -1
- metadata +6 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 55282db68817596fd27726daa87179e08fcddb3b8deaea5826161d54f7a850cb
|
4
|
+
data.tar.gz: '0864d609f3afa0c950d357f396973b7882c6f834f56c112e4765e3dfc7add400'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1712f02ea8b4d5def9d04381062f35135eb3213863b63536a35d8322b3880efb917acfb5dde900be7181ea2576b825791b301bd392a649bd799784aa93fbf28d
|
7
|
+
data.tar.gz: 4e268347627b41ec8c7506891adbf7c95f32d10d16c4ed3f5e33192f624660456d9a1712dee71921af18390c7998f1755a48a2cc73701a298036fce8ab1a48de
|
data/README.md
CHANGED
@@ -4,10 +4,10 @@ Implement the [Datastart SSE procotocol](https://data-star.dev/reference/sse_eve
|
|
4
4
|
|
5
5
|
## Installation
|
6
6
|
|
7
|
-
|
7
|
+
Add this gem to your `Gemfile`
|
8
8
|
|
9
9
|
```bash
|
10
|
-
|
10
|
+
gem 'datastar'
|
11
11
|
```
|
12
12
|
|
13
13
|
Or point your `Gemfile` to the source
|
@@ -165,17 +165,20 @@ end
|
|
165
165
|
Register server-side code to run when the connection is closed by the client
|
166
166
|
|
167
167
|
```ruby
|
168
|
-
datastar.
|
168
|
+
datastar.on_client_disconnect do
|
169
169
|
puts 'A user has disconnected connected'
|
170
170
|
end
|
171
171
|
```
|
172
172
|
|
173
|
+
This callback's behaviour depends on the configured [heartbeat](#heartbeat)
|
174
|
+
|
173
175
|
#### `on_server_disconnect`
|
176
|
+
|
174
177
|
Register server-side code to run when the connection is closed by the server.
|
175
178
|
Ie when the served is done streaming without errors.
|
176
179
|
|
177
180
|
```ruby
|
178
|
-
datastar.
|
181
|
+
datastar.on_server_disconnect do
|
179
182
|
puts 'Server is done streaming'
|
180
183
|
end
|
181
184
|
```
|
@@ -188,15 +191,64 @@ datastar.on_error do |exception|
|
|
188
191
|
Sentry.notify(exception)
|
189
192
|
end
|
190
193
|
```
|
191
|
-
Note that this callback can be
|
194
|
+
Note that this callback can be [configured globally](#global-configuration), too.
|
195
|
+
|
196
|
+
### heartbeat
|
197
|
+
|
198
|
+
By default, streaming responses (using the `#stream` block) launch a background thread/fiber to periodically check the connection.
|
199
|
+
|
200
|
+
This is because the browser could have disconnected during a long-lived, idle connection (for example waiting on an event bus).
|
201
|
+
|
202
|
+
The default heartbeat is 3 seconds, and it will close the connection and trigger [on_client_disconnect](#on_client_disconnect) callbacks if the client has disconnected.
|
203
|
+
|
204
|
+
In cases where a streaming block doesn't need a heartbeat and you want to save precious threads (for example a regular ticker update, ie non-idle), you can disable the heartbeat:
|
205
|
+
|
206
|
+
```ruby
|
207
|
+
datastar = Datastar.new(request:, response:, view_context:, heartbeat: false)
|
208
|
+
|
209
|
+
datastar.stream do |sse|
|
210
|
+
100.times do |i|
|
211
|
+
sleep 1
|
212
|
+
sse.merge_signals count: i
|
213
|
+
end
|
214
|
+
end
|
215
|
+
```
|
216
|
+
|
217
|
+
You can also set it to a different number (in seconds)
|
218
|
+
|
219
|
+
```ruby
|
220
|
+
heartbeat: 0.5
|
221
|
+
```
|
222
|
+
|
223
|
+
#### Manual connection check
|
224
|
+
|
225
|
+
If you want to check connection status on your own, you can disable the heartbeat and use `sse.check_connection!`, which will close the connection and trigger callbacks if the client is disconnected.
|
226
|
+
|
227
|
+
```ruby
|
228
|
+
datastar = Datastar.new(request:, response:, view_context:, heartbeat: false)
|
229
|
+
|
230
|
+
datastar.stream do |sse|
|
231
|
+
# The event bus implementaton will check connection status when idle
|
232
|
+
# by calling #check_connection! on it
|
233
|
+
EventBus.subscribe('channel', sse) do |event|
|
234
|
+
sse.merge_signals eventName: event.name
|
235
|
+
end
|
236
|
+
end
|
237
|
+
```
|
192
238
|
|
193
239
|
### Global configuration
|
194
240
|
|
195
241
|
```ruby
|
196
242
|
Datastar.configure do |config|
|
243
|
+
# Global on_error callback
|
244
|
+
# Can be overriden on specific instances
|
197
245
|
config.on_error do |exception|
|
198
246
|
Sentry.notify(exception)
|
199
247
|
end
|
248
|
+
|
249
|
+
# Global heartbeat interval (or false, to disable)
|
250
|
+
# Can be overriden on specific instances
|
251
|
+
config.heartbeat = 0.3
|
200
252
|
end
|
201
253
|
```
|
202
254
|
|
@@ -274,7 +326,7 @@ From this library's root, run the bundled-in test Rack app:
|
|
274
326
|
bundle puma examples/test.ru
|
275
327
|
```
|
276
328
|
|
277
|
-
Now run the test bash scripts in the `test` directory in this repo.
|
329
|
+
Now run the test bash scripts in the `sdk/test` directory in this repo.
|
278
330
|
|
279
331
|
```bash
|
280
332
|
./test-all.sh http://localhost:9292
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'thread'
|
4
|
+
require 'logger'
|
4
5
|
|
5
6
|
module Datastar
|
6
7
|
# The default executor based on Ruby threads
|
@@ -30,15 +31,19 @@ module Datastar
|
|
30
31
|
# You'd normally do this on app initialization
|
31
32
|
# For example in a Rails initializer
|
32
33
|
class Configuration
|
33
|
-
NOOP_CALLBACK = ->(_error) {}
|
34
34
|
RACK_FINALIZE = ->(_view_context, response) { response.finish }
|
35
|
+
DEFAULT_HEARTBEAT = 3
|
35
36
|
|
36
|
-
attr_accessor :executor, :error_callback, :finalize
|
37
|
+
attr_accessor :executor, :error_callback, :finalize, :heartbeat, :logger
|
37
38
|
|
38
39
|
def initialize
|
39
40
|
@executor = ThreadExecutor.new
|
40
|
-
@error_callback = NOOP_CALLBACK
|
41
41
|
@finalize = RACK_FINALIZE
|
42
|
+
@heartbeat = DEFAULT_HEARTBEAT
|
43
|
+
@logger = Logger.new(STDOUT)
|
44
|
+
@error_callback = proc do |e|
|
45
|
+
@logger.error("#{e.class} (#{e.message}):\n#{e.backtrace.join("\n")}")
|
46
|
+
end
|
42
47
|
end
|
43
48
|
|
44
49
|
def on_error(callable = nil, &block)
|
data/lib/datastar/consts.rb
CHANGED
@@ -4,10 +4,7 @@
|
|
4
4
|
module Datastar
|
5
5
|
module Consts
|
6
6
|
DATASTAR_KEY = 'datastar'
|
7
|
-
VERSION = '1.0.0-beta.
|
8
|
-
|
9
|
-
# The default duration for settling during fragment merges. Allows for CSS transitions to complete.
|
10
|
-
DEFAULT_FRAGMENTS_SETTLE_DURATION = 300
|
7
|
+
VERSION = '1.0.0-beta.11'
|
11
8
|
|
12
9
|
# The default duration for retrying SSE on connection reset. This is part of the underlying retry mechanism of SSE.
|
13
10
|
DEFAULT_SSE_RETRY_DURATION = 1000
|
@@ -57,7 +54,6 @@ module Datastar
|
|
57
54
|
# Dataline literals.
|
58
55
|
SELECTOR_DATALINE_LITERAL = 'selector'
|
59
56
|
MERGE_MODE_DATALINE_LITERAL = 'mergeMode'
|
60
|
-
SETTLE_DURATION_DATALINE_LITERAL = 'settleDuration'
|
61
57
|
FRAGMENTS_DATALINE_LITERAL = 'fragments'
|
62
58
|
USE_VIEW_TRANSITION_DATALINE_LITERAL = 'useViewTransition'
|
63
59
|
SIGNALS_DATALINE_LITERAL = 'signals'
|
data/lib/datastar/dispatcher.rb
CHANGED
@@ -35,13 +35,15 @@ module Datastar
|
|
35
35
|
# @option executor [Object] the executor object to use for managing threads and queues
|
36
36
|
# @option error_callback [Proc] the callback to call when an error occurs
|
37
37
|
# @option finalize [Proc] the callback to call when the response is finalized
|
38
|
+
# @option heartbeat [Integer, nil, FalseClass] the heartbeat interval in seconds
|
38
39
|
def initialize(
|
39
40
|
request:,
|
40
41
|
response: nil,
|
41
42
|
view_context: nil,
|
42
43
|
executor: Datastar.config.executor,
|
43
44
|
error_callback: Datastar.config.error_callback,
|
44
|
-
finalize: Datastar.config.finalize
|
45
|
+
finalize: Datastar.config.finalize,
|
46
|
+
heartbeat: Datastar.config.heartbeat
|
45
47
|
)
|
46
48
|
@on_connect = []
|
47
49
|
@on_client_disconnect = []
|
@@ -61,6 +63,10 @@ module Datastar
|
|
61
63
|
@response.headers['X-Accel-Buffering'] = 'no'
|
62
64
|
@response.delete_header 'Content-Length'
|
63
65
|
@executor.prepare(@response)
|
66
|
+
raise ArgumentError, ':heartbeat must be a number' if heartbeat && !heartbeat.is_a?(Numeric)
|
67
|
+
|
68
|
+
@heartbeat = heartbeat
|
69
|
+
@heartbeat_on = false
|
64
70
|
end
|
65
71
|
|
66
72
|
# Check if the request accepts SSE responses
|
@@ -124,7 +130,7 @@ module Datastar
|
|
124
130
|
# @param fragments [String, #call(view_context: Object) => Object] the HTML fragment or object
|
125
131
|
# @param options [Hash] the options to send with the message
|
126
132
|
def merge_fragments(fragments, options = BLANK_OPTIONS)
|
127
|
-
|
133
|
+
stream_no_heartbeat do |sse|
|
128
134
|
sse.merge_fragments(fragments, options)
|
129
135
|
end
|
130
136
|
end
|
@@ -138,7 +144,7 @@ module Datastar
|
|
138
144
|
# @param selector [String] a CSS selector for the fragment to remove
|
139
145
|
# @param options [Hash] the options to send with the message
|
140
146
|
def remove_fragments(selector, options = BLANK_OPTIONS)
|
141
|
-
|
147
|
+
stream_no_heartbeat do |sse|
|
142
148
|
sse.remove_fragments(selector, options)
|
143
149
|
end
|
144
150
|
end
|
@@ -152,7 +158,7 @@ module Datastar
|
|
152
158
|
# @param signals [Hash] signals to merge
|
153
159
|
# @param options [Hash] the options to send with the message
|
154
160
|
def merge_signals(signals, options = BLANK_OPTIONS)
|
155
|
-
|
161
|
+
stream_no_heartbeat do |sse|
|
156
162
|
sse.merge_signals(signals, options)
|
157
163
|
end
|
158
164
|
end
|
@@ -166,7 +172,7 @@ module Datastar
|
|
166
172
|
# @param paths [Array<String>] object paths to the signals to remove
|
167
173
|
# @param options [Hash] the options to send with the message
|
168
174
|
def remove_signals(paths, options = BLANK_OPTIONS)
|
169
|
-
|
175
|
+
stream_no_heartbeat do |sse|
|
170
176
|
sse.remove_signals(paths, options)
|
171
177
|
end
|
172
178
|
end
|
@@ -180,7 +186,7 @@ module Datastar
|
|
180
186
|
# @param script [String] the script to execute
|
181
187
|
# @param options [Hash] the options to send with the message
|
182
188
|
def execute_script(script, options = BLANK_OPTIONS)
|
183
|
-
|
189
|
+
stream_no_heartbeat do |sse|
|
184
190
|
sse.execute_script(script, options)
|
185
191
|
end
|
186
192
|
end
|
@@ -190,7 +196,7 @@ module Datastar
|
|
190
196
|
#
|
191
197
|
# @param url [String] the URL or path to redirect to
|
192
198
|
def redirect(url)
|
193
|
-
|
199
|
+
stream_no_heartbeat do |sse|
|
194
200
|
sse.redirect(url)
|
195
201
|
end
|
196
202
|
end
|
@@ -237,6 +243,15 @@ module Datastar
|
|
237
243
|
def stream(streamer = nil, &block)
|
238
244
|
streamer ||= block
|
239
245
|
@streamers << streamer
|
246
|
+
if @heartbeat && !@heartbeat_on
|
247
|
+
@heartbeat_on = true
|
248
|
+
@streamers << proc do |sse|
|
249
|
+
while true
|
250
|
+
sleep @heartbeat
|
251
|
+
sse.check_connection!
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
240
255
|
|
241
256
|
body = if @streamers.size == 1
|
242
257
|
stream_one(streamer)
|
@@ -250,6 +265,14 @@ module Datastar
|
|
250
265
|
|
251
266
|
private
|
252
267
|
|
268
|
+
def stream_no_heartbeat(&block)
|
269
|
+
was = @heartbeat
|
270
|
+
@heartbeat = false
|
271
|
+
stream(&block).tap do
|
272
|
+
@heartbeat = was
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
253
276
|
# Produce a response body for a single stream
|
254
277
|
# In this case, the SSE generator can write directly to the socket
|
255
278
|
#
|
@@ -300,11 +323,12 @@ module Datastar
|
|
300
323
|
|
301
324
|
handling_errors(conn_generator, socket) do
|
302
325
|
done_count = 0
|
326
|
+
threads_size = @heartbeat_on ? threads.size - 1 : threads.size
|
303
327
|
|
304
328
|
while (data = @queue.pop)
|
305
329
|
if data == :done
|
306
330
|
done_count += 1
|
307
|
-
@queue << nil if done_count ==
|
331
|
+
@queue << nil if done_count == threads_size
|
308
332
|
elsif data.is_a?(Exception)
|
309
333
|
raise data
|
310
334
|
else
|
data/lib/datastar/railtie.rb
CHANGED
@@ -14,6 +14,8 @@ module Datastar
|
|
14
14
|
initializer 'datastar' do |_app|
|
15
15
|
Datastar.config.finalize = FINALIZE
|
16
16
|
|
17
|
+
Datastar.config.logger = Rails.logger
|
18
|
+
|
17
19
|
Datastar.config.executor = if config.active_support.isolation_level == :fiber
|
18
20
|
require 'datastar/rails_async_executor'
|
19
21
|
RailsAsyncExecutor.new
|
@@ -17,7 +17,6 @@ module Datastar
|
|
17
17
|
'retry' => Consts::DEFAULT_SSE_RETRY_DURATION,
|
18
18
|
Consts::AUTO_REMOVE_DATALINE_LITERAL => Consts::DEFAULT_EXECUTE_SCRIPT_AUTO_REMOVE,
|
19
19
|
Consts::MERGE_MODE_DATALINE_LITERAL => Consts::DEFAULT_FRAGMENT_MERGE_MODE,
|
20
|
-
Consts::SETTLE_DURATION_DATALINE_LITERAL => Consts::DEFAULT_FRAGMENTS_SETTLE_DURATION,
|
21
20
|
Consts::USE_VIEW_TRANSITION_DATALINE_LITERAL => Consts::DEFAULT_FRAGMENTS_USE_VIEW_TRANSITIONS,
|
22
21
|
Consts::ONLY_IF_MISSING_DATALINE_LITERAL => Consts::DEFAULT_MERGE_SIGNALS_ONLY_IF_MISSING,
|
23
22
|
}.freeze
|
@@ -39,6 +38,13 @@ module Datastar
|
|
39
38
|
@view_context = view_context
|
40
39
|
end
|
41
40
|
|
41
|
+
# Sometimes we'll want to run periodic checks to ensure the connection is still alive
|
42
|
+
# ie. the browser hasn't disconnected
|
43
|
+
# For example when idle listening on an event bus.
|
44
|
+
def check_connection!
|
45
|
+
@stream << MSG_END
|
46
|
+
end
|
47
|
+
|
42
48
|
def merge_fragments(fragments, options = BLANK_OPTIONS)
|
43
49
|
# Support Phlex components
|
44
50
|
# And Rails' #render_in interface
|
data/lib/datastar/version.rb
CHANGED
metadata
CHANGED
@@ -1,28 +1,28 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: datastar
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.0.beta.
|
4
|
+
version: 1.0.0.beta.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ismael Celis
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-02
|
10
|
+
date: 2025-06-02 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: rack
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
15
15
|
requirements:
|
16
|
-
- - "
|
16
|
+
- - ">="
|
17
17
|
- !ruby/object:Gem::Version
|
18
|
-
version:
|
18
|
+
version: 3.1.14
|
19
19
|
type: :runtime
|
20
20
|
prerelease: false
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
22
22
|
requirements:
|
23
|
-
- - "
|
23
|
+
- - ">="
|
24
24
|
- !ruby/object:Gem::Version
|
25
|
-
version:
|
25
|
+
version: 3.1.14
|
26
26
|
email:
|
27
27
|
- ismaelct@gmail.com
|
28
28
|
executables: []
|