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.
- checksums.yaml +4 -4
- data/LICENSE.md +4 -16
- data/README.md +101 -14
- data/benchmarks/compression.rb +251 -0
- data/examples/hello-world/Gemfile +1 -1
- data/examples/hello-world/Gemfile.lock +3 -3
- data/examples/hello-world/hello-world.html +5 -5
- data/examples/progress/progress.ru +307 -0
- data/examples/threads/Gemfile +1 -1
- data/examples/threads/Gemfile.lock +3 -3
- data/examples/threads/threads.ru +3 -3
- data/lib/datastar/async_executor.rb +3 -1
- data/lib/datastar/compression_config.rb +167 -0
- data/lib/datastar/compressor/brotli.rb +56 -0
- data/lib/datastar/compressor/gzip.rb +60 -0
- data/lib/datastar/configuration.rb +6 -0
- data/lib/datastar/dispatcher.rb +54 -11
- data/lib/datastar/server_sent_event_generator.rb +52 -10
- data/lib/datastar/version.rb +1 -1
- data/lib/datastar.rb +1 -2
- metadata +9 -5
- data/lib/datastar/consts.rb +0 -57
data/lib/datastar/dispatcher.rb
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
349
|
+
handle_streaming_error(data, socket)
|
|
350
|
+
@queue << nil
|
|
336
351
|
else
|
|
337
|
-
|
|
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
|
-
#
|
|
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
|
|
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' =>
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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: #{
|
|
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
|
-
|
|
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,
|
|
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[
|
|
102
|
-
options[
|
|
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 ==
|
|
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"
|
data/lib/datastar/version.rb
CHANGED
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.
|
|
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.
|
|
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.
|
|
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:
|
|
120
|
+
rubygems_version: 4.0.8
|
|
117
121
|
specification_version: 4
|
|
118
122
|
summary: Ruby SDK for Datastar. Rack-compatible.
|
|
119
123
|
test_files: []
|
data/lib/datastar/consts.rb
DELETED
|
@@ -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
|