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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 07c2774d8c0274b50336a6163a1e22b4451bfd2ec271601d0a5a9f21c0e14fba
4
- data.tar.gz: bebe0a2d43cf8ab1e03a1693bbd2bfc206e8f1e650bf24333d39a8cfa59a4ebb
3
+ metadata.gz: 55282db68817596fd27726daa87179e08fcddb3b8deaea5826161d54f7a850cb
4
+ data.tar.gz: '0864d609f3afa0c950d357f396973b7882c6f834f56c112e4765e3dfc7add400'
5
5
  SHA512:
6
- metadata.gz: f98e8f6b5de65c9250b2ab678970f9ecdac84edfbc5088951570bd8b1f5f08d134180216d8b92d272628cd392bdb0ce1b71bb80f7263451a18de7308c7bf24d7
7
- data.tar.gz: d179c5dd59e5a5de18d2688a721e10dc7d62042c40b59cd1bd707f7a147e942ba234ebf6c2ca101b3cbce2d8c6288e6809d77fe3c7282569786c59e21b81282d
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
- Install the gem and add to the application's Gemfile by executing:
7
+ Add this gem to your `Gemfile`
8
8
 
9
9
  ```bash
10
- bundle add datastar
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.on_client_connect do
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.on_server_connect do
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 registered globally, too.
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)
@@ -4,10 +4,7 @@
4
4
  module Datastar
5
5
  module Consts
6
6
  DATASTAR_KEY = 'datastar'
7
- VERSION = '1.0.0-beta.5'
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'
@@ -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
- stream do |sse|
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
- stream do |sse|
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
- stream do |sse|
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
- stream do |sse|
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
- stream do |sse|
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
- stream do |sse|
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 == threads.size
331
+ @queue << nil if done_count == threads_size
308
332
  elsif data.is_a?(Exception)
309
333
  raise data
310
334
  else
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Datastar
4
- VERSION = '1.0.0.beta.1'
4
+ VERSION = '1.0.0.beta.3'
5
5
  end
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.1
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-11 00:00:00.000000000 Z
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: '3.0'
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: '3.0'
25
+ version: 3.1.14
26
26
  email:
27
27
  - ismaelct@gmail.com
28
28
  executables: []