datastar 1.0.0 → 1.0.2

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.
@@ -44,7 +44,8 @@ module Datastar
44
44
  executor: Datastar.config.executor,
45
45
  error_callback: Datastar.config.error_callback,
46
46
  finalize: Datastar.config.finalize,
47
- heartbeat: Datastar.config.heartbeat
47
+ heartbeat: Datastar.config.heartbeat,
48
+ compression: Datastar.config.compression
48
49
  )
49
50
  @on_connect = []
50
51
  @on_client_disconnect = []
@@ -68,6 +69,11 @@ module Datastar
68
69
 
69
70
  @heartbeat = heartbeat
70
71
  @heartbeat_on = false
72
+
73
+ # Negotiate compression
74
+ compression = CompressionConfig.build(compression) unless compression.is_a?(CompressionConfig)
75
+ @compressor = compression.negotiate(request)
76
+ @compressor.prepare_response(@response)
71
77
  end
72
78
 
73
79
  # Check if the request accepts SSE responses
@@ -283,9 +289,10 @@ module Datastar
283
289
  # @api private
284
290
  def stream_one(streamer)
285
291
  proc do |socket|
292
+ socket = wrap_socket(socket)
286
293
  generator = ServerSentEventGenerator.new(socket, signals:, view_context: @view_context)
287
294
  @on_connect.each { |callable| callable.call(generator) }
288
- handling_errors(generator, socket) do
295
+ handling_sync_errors(generator, socket) do
289
296
  streamer.call(generator)
290
297
  end
291
298
  ensure
@@ -308,14 +315,16 @@ module Datastar
308
315
  @queue ||= @executor.new_queue
309
316
 
310
317
  proc do |socket|
318
+ socket = wrap_socket(socket)
311
319
  signs = signals
312
320
  conn_generator = ServerSentEventGenerator.new(socket, signals: signs, view_context: @view_context)
313
321
  @on_connect.each { |callable| callable.call(conn_generator) }
314
322
 
315
323
  threads = @streamers.map do |streamer|
324
+ duped_signals = signs.dup.freeze
316
325
  @executor.spawn do
317
326
  # TODO: Review thread-safe view context
318
- generator = ServerSentEventGenerator.new(@queue, signals: signs, view_context: @view_context)
327
+ generator = ServerSentEventGenerator.new(@queue, signals: duped_signals, view_context: @view_context)
319
328
  streamer.call(generator)
320
329
  @queue << :done
321
330
  rescue StandardError => e
@@ -323,7 +332,12 @@ module Datastar
323
332
  end
324
333
  end
325
334
 
326
- handling_errors(conn_generator, socket) do
335
+ # Now launch the control thread that actually writes to the socket
336
+ # We don't want to block the main thread, so that servers like Puma
337
+ # which have a limited thread pool can keep serving other requests
338
+ # Other streamers will push any StandardError exceptions to the queue
339
+ # So we handle them here
340
+ @executor.spawn do
327
341
  done_count = 0
328
342
  threads_size = @heartbeat_on ? threads.size - 1 : threads.size
329
343
 
@@ -332,24 +346,53 @@ module Datastar
332
346
  done_count += 1
333
347
  @queue << nil if done_count == threads_size
334
348
  elsif data.is_a?(Exception)
335
- raise data
349
+ handle_streaming_error(data, socket)
350
+ @queue << nil
336
351
  else
337
- socket << data
352
+ # Here we attempt writing to the actual socket
353
+ # which may raise an IOError if the client disconnected
354
+ begin
355
+ socket << data
356
+ rescue Exception => e
357
+ handle_streaming_error(e, socket)
358
+ @queue << nil
359
+ end
338
360
  end
339
361
  end
362
+
363
+ ensure
364
+ @on_server_disconnect.each { |callable| callable.call(conn_generator) }
365
+ @executor.stop(threads) if threads
366
+ socket.close
340
367
  end
341
- ensure
342
- @executor.stop(threads) if threads
343
- socket.close
344
368
  end
345
369
  end
346
370
 
347
- # Run a streaming block while handling errors
371
+ # Wrap socket in a CompressedSocket if compression is negotiated
372
+ # @param socket [IO]
373
+ # @return [CompressedSocket, IO]
374
+ def wrap_socket(socket)
375
+ @compressor.wrap_socket(socket)
376
+ end
377
+
378
+ # Handle errors caught during streaming
379
+ # @param error [Exception] the error that occurred
380
+ # @param socket [IO] the socket to pass to error handlers
381
+ def handle_streaming_error(error, socket)
382
+ case error
383
+ when IOError, Errno::EPIPE, Errno::ECONNRESET
384
+ @on_client_disconnect.each { |callable| callable.call(socket) }
385
+ when Exception
386
+ @on_error.each { |callable| callable.call(error) }
387
+ end
388
+ end
389
+
390
+ # Run a block while handling errors
348
391
  # @param generator [ServerSentEventGenerator]
349
392
  # @param socket [IO]
350
393
  # @yield
351
394
  # @api private
352
- def handling_errors(generator, socket, &)
395
+ def handling_sync_errors(generator, socket, &)
353
396
  yield
354
397
 
355
398
  @on_server_disconnect.each { |callable| callable.call(generator) }
@@ -3,9 +3,46 @@
3
3
  require 'json'
4
4
 
5
5
  module Datastar
6
+ module ElementPatchMode
7
+ # Morphs the element into the existing element.
8
+ OUTER = 'outer'
9
+
10
+ # Replaces the inner HTML of the existing element.
11
+ INNER = 'inner'
12
+
13
+ # Removes the existing element.
14
+ REMOVE = 'remove'
15
+
16
+ # Replaces the existing element with the new element.
17
+ REPLACE = 'replace'
18
+
19
+ # Prepends the element inside to the existing element.
20
+ PREPEND = 'prepend'
21
+
22
+ # Appends the element inside the existing element.
23
+ APPEND = 'append'
24
+
25
+ # Inserts the element before the existing element.
26
+ BEFORE = 'before'
27
+
28
+ # Inserts the element after the existing element.
29
+ AFTER = 'after'
30
+ end
31
+
6
32
  class ServerSentEventGenerator
7
33
  MSG_END = "\n"
8
34
 
35
+ DEFAULT_SSE_RETRY_DURATION = 1000
36
+ DEFAULT_ELEMENTS_USE_VIEW_TRANSITIONS = false
37
+ DEFAULT_PATCH_SIGNALS_ONLY_IF_MISSING = false
38
+
39
+ SELECTOR_DATALINE_LITERAL = 'selector'
40
+ MODE_DATALINE_LITERAL = 'mode'
41
+ ELEMENTS_DATALINE_LITERAL = 'elements'
42
+ USE_VIEW_TRANSITION_DATALINE_LITERAL = 'useViewTransition'
43
+ SIGNALS_DATALINE_LITERAL = 'signals'
44
+ ONLY_IF_MISSING_DATALINE_LITERAL = 'onlyIfMissing'
45
+
9
46
  SSE_OPTION_MAPPING = {
10
47
  'eventId' => 'id',
11
48
  'retryDuration' => 'retry',
@@ -13,17 +50,22 @@ module Datastar
13
50
  'retry' => 'retry',
14
51
  }.freeze
15
52
 
53
+ DEFAULT_ELEMENT_PATCH_MODE = ElementPatchMode::OUTER
54
+
16
55
  OPTION_DEFAULTS = {
17
- 'retry' => Consts::DEFAULT_SSE_RETRY_DURATION,
18
- Consts::MODE_DATALINE_LITERAL => Consts::DEFAULT_ELEMENT_PATCH_MODE,
19
- Consts::USE_VIEW_TRANSITION_DATALINE_LITERAL => Consts::DEFAULT_ELEMENTS_USE_VIEW_TRANSITIONS,
20
- Consts::ONLY_IF_MISSING_DATALINE_LITERAL => Consts::DEFAULT_PATCH_SIGNALS_ONLY_IF_MISSING,
56
+ 'retry' => DEFAULT_SSE_RETRY_DURATION,
57
+ MODE_DATALINE_LITERAL => DEFAULT_ELEMENT_PATCH_MODE,
58
+ USE_VIEW_TRANSITION_DATALINE_LITERAL => DEFAULT_ELEMENTS_USE_VIEW_TRANSITIONS,
59
+ ONLY_IF_MISSING_DATALINE_LITERAL => DEFAULT_PATCH_SIGNALS_ONLY_IF_MISSING,
21
60
  }.freeze
22
61
 
23
62
  SIGNAL_SEPARATOR = '.'
24
63
 
25
64
  attr_reader :signals
26
65
 
66
+ # @param stream [IO, Queue] The IO stream or Queue to write to
67
+ # @option signals [Hash] A hash of signals (params)
68
+ # @option view_context [Object] The view context for rendering elements, if applicable.
27
69
  def initialize(stream, signals:, view_context: nil)
28
70
  @stream = stream
29
71
  @signals = signals
@@ -49,7 +91,7 @@ module Datastar
49
91
 
50
92
  buffer = +"event: datastar-patch-elements\n"
51
93
  build_options(options, buffer)
52
- element_lines.each { |line| buffer << "data: #{Consts::ELEMENTS_DATALINE_LITERAL} #{line}\n" }
94
+ element_lines.each { |line| buffer << "data: #{ELEMENTS_DATALINE_LITERAL} #{line}\n" }
53
95
 
54
96
  write(buffer)
55
97
  end
@@ -58,7 +100,7 @@ module Datastar
58
100
  patch_elements(
59
101
  nil,
60
102
  options.merge(
61
- Consts::MODE_DATALINE_LITERAL => Consts::ElementPatchMode::REMOVE,
103
+ MODE_DATALINE_LITERAL => ElementPatchMode::REMOVE,
62
104
  selector:
63
105
  )
64
106
  )
@@ -72,7 +114,7 @@ module Datastar
72
114
  signals = JSON.dump(signals)
73
115
  buffer << "data: signals #{signals}\n"
74
116
  when String
75
- multi_data_lines(signals, buffer, Consts::SIGNALS_DATALINE_LITERAL)
117
+ multi_data_lines(signals, buffer, SIGNALS_DATALINE_LITERAL)
76
118
  end
77
119
  write(buffer)
78
120
  end
@@ -98,8 +140,8 @@ module Datastar
98
140
  script_tag << %( data-effect="el.remove()") if auto_remove
99
141
  script_tag << ">#{script}</script>"
100
142
 
101
- options[Consts::SELECTOR_DATALINE_LITERAL] = 'body'
102
- options[Consts::MODE_DATALINE_LITERAL] = Consts::ElementPatchMode::APPEND
143
+ options[SELECTOR_DATALINE_LITERAL] = 'body'
144
+ options[MODE_DATALINE_LITERAL] = ElementPatchMode::APPEND
103
145
 
104
146
  patch_elements(script_tag, options)
105
147
  end
@@ -140,7 +182,7 @@ module Datastar
140
182
  buffer << "data: #{k} #{kk} #{vv}\n"
141
183
  end
142
184
  elsif v.is_a?(Array)
143
- if k == Consts::SELECTOR_DATALINE_LITERAL
185
+ if k == SELECTOR_DATALINE_LITERAL
144
186
  buffer << "data: #{k} #{v.join(', ')}\n"
145
187
  else
146
188
  buffer << "data: #{k} #{v.join(' ')}\n"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Datastar
4
- VERSION = '1.0.0'
4
+ VERSION = '1.0.2'
5
5
  end
data/lib/datastar.rb CHANGED
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'datastar/version'
4
- require_relative 'datastar/consts'
5
-
6
4
  module Datastar
7
5
  BLANK_OPTIONS = {}.freeze
8
6
 
@@ -27,6 +25,7 @@ module Datastar
27
25
  end
28
26
 
29
27
  require_relative 'datastar/configuration'
28
+ require_relative 'datastar/compression_config'
30
29
  require_relative 'datastar/dispatcher'
31
30
  require_relative 'datastar/server_sent_event_generator'
32
31
  require_relative 'datastar/railtie' if defined?(Rails::Railtie)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: datastar
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ismael Celis
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: 3.1.14
18
+ version: '3.2'
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.1.14
25
+ version: '3.2'
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: json
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -75,18 +75,22 @@ files:
75
75
  - LICENSE.md
76
76
  - README.md
77
77
  - Rakefile
78
+ - benchmarks/compression.rb
78
79
  - examples/hello-world/Gemfile
79
80
  - examples/hello-world/Gemfile.lock
80
81
  - examples/hello-world/hello-world.html
81
82
  - examples/hello-world/hello-world.ru
83
+ - examples/progress/progress.ru
82
84
  - examples/test.ru
83
85
  - examples/threads/Gemfile
84
86
  - examples/threads/Gemfile.lock
85
87
  - examples/threads/threads.ru
86
88
  - lib/datastar.rb
87
89
  - lib/datastar/async_executor.rb
90
+ - lib/datastar/compression_config.rb
91
+ - lib/datastar/compressor/brotli.rb
92
+ - lib/datastar/compressor/gzip.rb
88
93
  - lib/datastar/configuration.rb
89
- - lib/datastar/consts.rb
90
94
  - lib/datastar/dispatcher.rb
91
95
  - lib/datastar/rails_async_executor.rb
92
96
  - lib/datastar/rails_thread_executor.rb
@@ -113,7 +117,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
113
117
  - !ruby/object:Gem::Version
114
118
  version: '0'
115
119
  requirements: []
116
- rubygems_version: 3.6.9
120
+ rubygems_version: 4.0.8
117
121
  specification_version: 4
118
122
  summary: Ruby SDK for Datastar. Rack-compatible.
119
123
  test_files: []
@@ -1,57 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # This is auto-generated by Datastar. DO NOT EDIT.
4
- module Datastar
5
- module Consts
6
- DATASTAR_KEY = 'datastar'
7
- VERSION = '1.0.0-RC.1'
8
-
9
- # The default duration for retrying SSE on connection reset. This is part of the underlying retry mechanism of SSE.
10
- DEFAULT_SSE_RETRY_DURATION = 1000
11
-
12
- # Should elements be patched using the ViewTransition API?
13
- DEFAULT_ELEMENTS_USE_VIEW_TRANSITIONS = false
14
-
15
- # Should a given set of signals patch if they are missing?
16
- DEFAULT_PATCH_SIGNALS_ONLY_IF_MISSING = false
17
-
18
- module ElementPatchMode
19
-
20
- # Morphs the element into the existing element.
21
- OUTER = 'outer';
22
-
23
- # Replaces the inner HTML of the existing element.
24
- INNER = 'inner';
25
-
26
- # Removes the existing element.
27
- REMOVE = 'remove';
28
-
29
- # Replaces the existing element with the new element.
30
- REPLACE = 'replace';
31
-
32
- # Prepends the element inside to the existing element.
33
- PREPEND = 'prepend';
34
-
35
- # Appends the element inside the existing element.
36
- APPEND = 'append';
37
-
38
- # Inserts the element before the existing element.
39
- BEFORE = 'before';
40
-
41
- # Inserts the element after the existing element.
42
- AFTER = 'after';
43
- end
44
-
45
-
46
- # The mode in which an element is patched into the DOM.
47
- DEFAULT_ELEMENT_PATCH_MODE = ElementPatchMode::OUTER
48
-
49
- # Dataline literals.
50
- SELECTOR_DATALINE_LITERAL = 'selector'
51
- MODE_DATALINE_LITERAL = 'mode'
52
- ELEMENTS_DATALINE_LITERAL = 'elements'
53
- USE_VIEW_TRANSITION_DATALINE_LITERAL = 'useViewTransition'
54
- SIGNALS_DATALINE_LITERAL = 'signals'
55
- ONLY_IF_MISSING_DATALINE_LITERAL = 'onlyIfMissing'
56
- end
57
- end