datastar 1.0.0.pre.2 → 1.0.1

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: f7e29e806ab458bbc154d64f12998a7cc9d66b69a93dd5940a140a1488f976cf
4
- data.tar.gz: 4ada005096ab64dd593c3527acce4534ddd034682e7fcf7aa99c3aeb658e6b39
3
+ metadata.gz: cc94dbbe291bd86a1aaae4302cd8fdaa0262f06bcbf0bb81ce201099a92de0b7
4
+ data.tar.gz: 804810f5e0633c690d329f79e3eb220734cb558767f951be18b383bfdd97fcd9
5
5
  SHA512:
6
- metadata.gz: 9c8cd42612fec2a37934adb7d282555f967d7f1812b76cad562ace2c916a543976afc3a4ebb367aa80965816b359dc5d1f9e0151633ff1565e40c6ad0a8c2d0f
7
- data.tar.gz: 411d02ce73b1fe37a6b46f1b3306f26507083bcddcb73c1213344976516f666eb3c7f859322a20fcab8a09042fd1286e714f82add2369c81472ff66e13542c15
6
+ metadata.gz: 22716ea763849a6f6aae0a762809b29662e8dfb279dd2bcd0165b2af3a53cb8cbed31f57c8c4af8db55e1f9e54764243741265b01c6d9772eb28b2197790977b
7
+ data.tar.gz: 88efa4fb0a989d5d05548a8264650b89a08f1af6cb6bdcbb848da36e6aa09cc8c37729233ed867fbfdb2a9cfeb3fd8944f27a7b8c618259a569b451d8f0b3a92
data/LICENSE.md CHANGED
@@ -1,19 +1,7 @@
1
- Copyright (c) Ismael Celis
1
+ Copyright © Star Federation
2
2
 
3
- Permission is hereby granted, free of charge, to any person obtaining a copy
4
- of this software and associated documentation files (the "Software"), to deal
5
- in the Software without restriction, including without limitation the rights
6
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
- copies of the Software, and to permit persons to whom the Software is
8
- furnished to do so, subject to the following conditions:
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
9
4
 
10
- The above copyright notice and this permission notice shall be included in all
11
- copies or substantial portions of the Software.
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
12
6
 
13
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
- SOFTWARE.
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -160,7 +160,7 @@ sse.execute_script(%(alert('Hello World!')), auto_remove: false)
160
160
  ```
161
161
 
162
162
  #### `signals`
163
- See https://data-star.dev/guide/getting_started#data-signals
163
+ See https://data-star.dev/guide/reactive_signals
164
164
 
165
165
  Returns signals sent by the browser.
166
166
 
@@ -348,13 +348,14 @@ bundle install
348
348
  From this library's root, run the bundled-in test Rack app:
349
349
 
350
350
  ```bash
351
- bundle puma examples/test.ru
351
+ bundle puma -p 8000 examples/test.ru
352
352
  ```
353
353
 
354
- Now run the test bash scripts in the `sdk/test` directory in this repo.
354
+ From the main [Datastar](https://github.com/starfederation/datastar) repo (you'll need Go installed)
355
355
 
356
356
  ```bash
357
- ./test-all.sh http://localhost:9292
357
+ cd sdk/tests
358
+ go run ./cmd/datastar-sdk-tests -server http://localhost:8000
358
359
  ```
359
360
 
360
361
  ## Development
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gem 'puma'
6
+ gem 'rack'
7
+ # gem 'datastar'
8
+ gem 'datastar', path: '../../../'
@@ -0,0 +1,29 @@
1
+ PATH
2
+ remote: ../../..
3
+ specs:
4
+ datastar (1.0.0)
5
+ json
6
+ logger
7
+ rack (>= 3.1.14)
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ json (2.16.0)
13
+ logger (1.7.0)
14
+ nio4r (2.7.4)
15
+ puma (6.6.0)
16
+ nio4r (~> 2.0)
17
+ rack (3.1.16)
18
+
19
+ PLATFORMS
20
+ arm64-darwin-24
21
+ ruby
22
+
23
+ DEPENDENCIES
24
+ datastar!
25
+ puma
26
+ rack
27
+
28
+ BUNDLED WITH
29
+ 2.6.3
@@ -0,0 +1,35 @@
1
+ <!-- This is auto-generated by Datastar. DO NOT EDIT. -->
2
+
3
+ <!DOCTYPE html>
4
+ <html lang="en">
5
+ <head>
6
+ <title>Datastar SDK Demo</title>
7
+ <script src="https://unpkg.com/@tailwindcss/browser@4"></script>
8
+ <script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/datastar@1.0.0-RC.6/bundles/datastar.js"></script>
9
+ </head>
10
+ <body class="bg-white dark:bg-gray-900 text-lg max-w-xl mx-auto my-16">
11
+ <div data-signals:delay="400" class="bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400 rounded-lg px-6 py-8 ring shadow-xl ring-gray-900/5 space-y-2">
12
+ <div class="flex justify-between items-center">
13
+ <h1 class="text-gray-900 dark:text-white text-3xl font-semibold">
14
+ Datastar SDK Demo
15
+ </h1>
16
+ <img src="https://data-star.dev/static/images/rocket-64x64.png" alt="Rocket" width="64" height="64"/>
17
+ </div>
18
+ <p class="mt-2">
19
+ SSE events will be streamed from the backend to the frontend.
20
+ </p>
21
+ <div class="space-x-2">
22
+ <label for="delay">
23
+ Delay in milliseconds
24
+ </label>
25
+ <input data-bind:delay id="delay" type="number" step="100" min="0" class="w-36 rounded-md border border-gray-300 px-3 py-2 placeholder-gray-400 shadow-sm focus:border-sky-500 focus:outline focus:outline-sky-500 dark:disabled:border-gray-700 dark:disabled:bg-gray-800/20" />
26
+ </div>
27
+ <button data-on:click="@get(&#39;/hello-world&#39;)" class="rounded-md bg-sky-500 px-5 py-2.5 leading-5 font-semibold text-white hover:bg-sky-700 hover:text-gray-100 cursor-pointer">
28
+ Start
29
+ </button>
30
+ </div>
31
+ <div class="my-16 text-8xl font-bold text-transparent" style="background: linear-gradient(to right in oklch, red, orange, yellow, green, blue, blue, violet); background-clip: text">
32
+ <div id="message">Hello, world!</div>
33
+ </div>
34
+ </body>
35
+ </html>
@@ -0,0 +1,37 @@
1
+ require 'bundler/setup'
2
+
3
+ require 'datastar'
4
+
5
+ # This is a test Rack endpoint
6
+ # with a hello world example using Datastar.
7
+ # To run:
8
+ #
9
+ # # install dependencies
10
+ # bundle install
11
+ # # run this endpoint with Puma server
12
+ # bundle exec puma ./hello-world.ru
13
+ #
14
+ # Then open http://localhost:9292
15
+ #
16
+ HTML = File.read(File.expand_path('hello-world.html', __dir__))
17
+
18
+ run do |env|
19
+ datastar = Datastar.from_rack_env(env)
20
+
21
+ if datastar.sse?
22
+ delay = (datastar.signals['delay'] || 0).to_i
23
+ delay /= 1000.0 if delay.positive?
24
+ message = 'Hello, world!'
25
+
26
+ datastar.stream do |sse|
27
+ message.size.times do |i|
28
+ sse.patch_elements(%(<div id="message">#{message[0..i]}</div>))
29
+ sleep delay
30
+ end
31
+ end
32
+ else
33
+ [200, { 'content-type' => 'text/html' }, [HTML]]
34
+ end
35
+ end
36
+
37
+ trap('INT') { exit }
@@ -0,0 +1,307 @@
1
+ require 'bundler'
2
+ Bundler.setup(:test)
3
+
4
+ require 'datastar'
5
+
6
+ # This is a demo Rack app to showcase patching components and signals
7
+ # from the server to the client.
8
+ # To run:
9
+ #
10
+ # # install dependencies
11
+ # bundle install
12
+ # # run this endpoint with Puma server
13
+ # bundle exec puma ./progress.ru
14
+ #
15
+ # Then open http://localhost:9292
16
+ #
17
+ # A Web Component for circular progress
18
+ # Progress is controlled by a `progress` signal
19
+ PROGRESS = <<~JAVASCRIPT
20
+ class CircularProgress extends HTMLElement {
21
+ constructor() {
22
+ super();
23
+ this.attachShadow({ mode: 'open' });
24
+ this._progress = 0;
25
+ this.radius = 90;
26
+ this.circumference = 2 * Math.PI * this.radius;
27
+ }
28
+
29
+ static get observedAttributes() {
30
+ return ['progress'];
31
+ }
32
+
33
+ get progress() {
34
+ return this._progress;
35
+ }
36
+
37
+ attributeChangedCallback(name, oldValue, newValue) {
38
+ if (name === 'progress' && oldValue !== newValue) {
39
+ this._progress = Math.max(0, Math.min(100, parseFloat(newValue) || 0));
40
+ this.updateProgress();
41
+ }
42
+ }
43
+
44
+ connectedCallback() {
45
+ this.render();
46
+ }
47
+
48
+ render() {
49
+ this.shadowRoot.innerHTML = `
50
+ <slot></slot>
51
+ <svg
52
+ width="200"
53
+ height="200"
54
+ viewBox="-25 -25 250 250"
55
+ style="transform: rotate(-90deg)"
56
+ >
57
+ <!-- Background circle -->
58
+ <circle
59
+ r="${this.radius}"
60
+ cx="100"
61
+ cy="100"
62
+ fill="transparent"
63
+ stroke="#e0e0e0"
64
+ stroke-width="16px"
65
+ stroke-dasharray="${this.circumference}px"
66
+ stroke-dashoffset="${this.circumference}px"
67
+ ></circle>
68
+
69
+ <!-- Progress circle -->
70
+ <circle
71
+ id="progress-circle"
72
+ r="${this.radius}"
73
+ cx="100"
74
+ cy="100"
75
+ fill="transparent"
76
+ stroke="#6bdba7"
77
+ stroke-width="16px"
78
+ stroke-linecap="round"
79
+ stroke-dasharray="${this.circumference}px"
80
+ style="transition: stroke-dashoffset 0.1s ease-in-out"
81
+ ></circle>
82
+
83
+ <!-- Progress text -->
84
+ <text
85
+ id="progress-text"
86
+ x="44px"
87
+ y="115px"
88
+ fill="#6bdba7"
89
+ font-size="52px"
90
+ font-weight="bold"
91
+ style="transform:rotate(90deg) translate(0px, -196px)"
92
+ ></text>
93
+ </svg>
94
+ `;
95
+ }
96
+
97
+ updateProgress() {
98
+ if (!this.shadowRoot) return;
99
+
100
+ const progressCircle = this.shadowRoot.getElementById('progress-circle');
101
+ const progressText = this.shadowRoot.getElementById('progress-text');
102
+
103
+ if (progressCircle && progressText) {
104
+ // Calculate stroke-dashoffset based on progress
105
+ const offset = this.circumference - (this._progress / 100) * this.circumference;
106
+ progressCircle.style.strokeDashoffset = `${offset}px`;
107
+
108
+ // Update text
109
+ progressText.textContent = `${Math.round(this._progress)}%`;
110
+ }
111
+ }
112
+ }
113
+
114
+ // Register the custom element
115
+ customElements.define('circular-progress', CircularProgress);
116
+ JAVASCRIPT
117
+
118
+ # The initial index HTML page
119
+ INDEX = <<~HTML
120
+ <!DOCTYPE html>
121
+ <html>
122
+ <head>
123
+ <meta charset="UTF-8">
124
+ <title>Datastar progress-circle</title>
125
+ <style>
126
+ body {
127
+ font-family: Arial, sans-serif;
128
+ padding: 20px;
129
+ background-color: #f5f5f5;
130
+ }
131
+ .demo-container {
132
+ max-width: 800px;
133
+ margin: 0 auto;
134
+ background: white;
135
+ padding: 30px;
136
+ border-radius: 10px;
137
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
138
+ }
139
+ button {
140
+ background: linear-gradient(135deg, #6bdba7 0%, #5bc399 100%);
141
+ color: white;
142
+ border: none;
143
+ padding: 12px 24px;
144
+ font-size: 16px;
145
+ font-weight: 600;
146
+ border-radius: 8px;
147
+ cursor: pointer;
148
+ transition: all 0.2s ease;
149
+ box-shadow: 0 2px 4px rgba(107, 219, 167, 0.3);
150
+ margin-bottom: 20px;
151
+ }
152
+ button:hover:not([aria-disabled="true"]) {
153
+ background: linear-gradient(135deg, #5bc399 0%, #4db389 100%);
154
+ transform: translateY(-1px);
155
+ box-shadow: 0 4px 8px rgba(107, 219, 167, 0.4);
156
+ }
157
+ button:active:not([aria-disabled="true"]) {
158
+ transform: translateY(0);
159
+ box-shadow: 0 2px 4px rgba(107, 219, 167, 0.3);
160
+ }
161
+ button[aria-disabled="true"] {
162
+ background: #e0e0e0;
163
+ color: #999;
164
+ cursor: not-allowed;
165
+ box-shadow: none;
166
+ }
167
+ .col {
168
+ flex: 1;
169
+ padding: 0 15px;
170
+ min-height: 340px;
171
+ }
172
+ .col:first-child {
173
+ padding-left: 0;
174
+ display: flex;
175
+ flex-direction: column;
176
+ align-items: center;
177
+ justify-content: center;
178
+ }
179
+ .col:last-child {
180
+ padding-right: 0;
181
+ }
182
+ @media (min-width: 768px) {
183
+ .demo-container {
184
+ display: flex;
185
+ gap: 30px;
186
+ }
187
+ }
188
+ #activity {
189
+ overflow-y: auto;
190
+ border: 1px solid #e0e0e0;
191
+ border-radius: 8px;
192
+ padding: 16px;
193
+ background: #fafafa;
194
+ }
195
+ .a-item {
196
+ background: white;
197
+ border: 1px solid #e8e8e8;
198
+ border-radius: 6px;
199
+ padding: 12px 16px;
200
+ margin-bottom: 8px;
201
+ font-size: 14px;
202
+ color: #333;
203
+ box-shadow: 0 1px 2px rgba(0,0,0,0.05);
204
+ transition: all 0.2s ease;
205
+ }
206
+ .a-item:last-child {
207
+ margin-bottom: 0;
208
+ }
209
+ .a-item:hover {
210
+ background: #f8f9fa;
211
+ border-color: #d0d0d0;
212
+ }
213
+ .a-item .time {
214
+ display: block;
215
+ font-size: 11px;
216
+ color: #888;
217
+ margin-bottom: 4px;
218
+ font-family: monospace;
219
+ }
220
+ .a-item.done {
221
+ background: #f0f9f4;
222
+ border-color: #6bdba7;
223
+ color: #2d5a3d;
224
+ }
225
+ .a-item.done .time {
226
+ color: #5a8a6a;
227
+ }
228
+ #title {
229
+ text-align: center;
230
+ }
231
+ </style>
232
+ <script type="module">#{PROGRESS}</script>
233
+ <script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/datastar@1.0.0-RC.6/bundles/datastar.js"></script>
234
+ </head>
235
+ <body>
236
+ <div class="demo-container">
237
+ <div class="col">
238
+ <p>
239
+ <button
240
+ data-indicator:_fetching
241
+ data-on:click="!$_fetching && @get('/', {openWhenHidden: true})"
242
+ data-attr:aria-disabled="`${$_fetching}`"
243
+ >Start</button>
244
+ </p>
245
+ <div id="work">
246
+ </div>
247
+ </div>
248
+
249
+ <div class="col" id="activity">
250
+ </div>
251
+ </div>
252
+ </body>
253
+ <html>
254
+ HTML
255
+
256
+ trap('INT') { exit }
257
+
258
+ # The server-side app
259
+ # It handles the initial page load and serves the initial HTML.
260
+ # It also handles Datastar SSE requests and streams updates to the client.
261
+ run do |env|
262
+ datastar = Datastar
263
+ .from_rack_env(env)
264
+ .on_connect do |socket|
265
+ p ['connect', socket]
266
+ end.on_server_disconnect do |socket|
267
+ p ['server disconnect', socket]
268
+ end.on_client_disconnect do |socket|
269
+ p ['client disconnect', socket]
270
+ end.on_error do |error|
271
+ p ['exception', error]
272
+ puts error.backtrace.join("\n")
273
+ end
274
+
275
+ if datastar.sse? # <= we're in a Datastar SSE request
276
+
277
+ # A thread to simulate the work and control the progress component
278
+ datastar.stream do |sse|
279
+ # Reset activity
280
+ sse.patch_elements(%(<div id="activity" class="col"></div>))
281
+
282
+ # step 1: add the initial progress component to the DOM
283
+ sse.patch_elements(%(<circular-progress id="work" data-bind:progress data-attr:progress="$progress"><h1 id="title">Processing...</h1></circular-progress>))
284
+
285
+ # step 2: simulate work and update the progress signal
286
+ 0.upto(100) do |i|
287
+ sleep rand(0.03..0.09) # Simulate work
288
+ sse.patch_signals(progress: i)
289
+ end
290
+
291
+ # step 3: update the DOM to indicate completion
292
+ # sse.patch_elements(%(<p id="work">Done!</p>))
293
+ sse.patch_elements(%(<div class="a-item done"><span class="time">#{Time.now.iso8601}</span>Done!</div>), selector: '#activity', mode: 'append')
294
+ sse.patch_elements(%(<h1 id="title">Done!</h1>))
295
+ end
296
+
297
+ # A second thread to push activity updates to the UI
298
+ datastar.stream do |sse|
299
+ ['Work started', 'Connecting to API', 'downloading data', 'processing data'].each do |activity|
300
+ sse.patch_elements(%(<div class="a-item"><span class="time">#{Time.now.iso8601}</span>#{activity}</div>), selector: '#activity', mode: 'append')
301
+ sleep rand(0.5..1.7) # Simulate time taken for each activity
302
+ end
303
+ end
304
+ else # <= We're in a regular HTTP request
305
+ [200, { 'content-type' => 'text/html' }, [INDEX]]
306
+ end
307
+ end
data/examples/test.ru CHANGED
@@ -32,7 +32,7 @@ run do |env|
32
32
  end
33
33
 
34
34
  datastar.stream do |sse|
35
- sse.signals['events'].each do |event|
35
+ (sse.signals['events'] || []).each do |event|
36
36
  type = event.delete('type')
37
37
  case type
38
38
  when 'patchSignals'
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gem 'puma'
6
+ gem 'rack'
7
+ # gem 'datastar'
8
+ gem 'datastar', path: '../../../'
@@ -0,0 +1,29 @@
1
+ PATH
2
+ remote: ../../..
3
+ specs:
4
+ datastar (1.0.0)
5
+ json
6
+ logger
7
+ rack (>= 3.1.14)
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ json (2.16.0)
13
+ logger (1.7.0)
14
+ nio4r (2.7.4)
15
+ puma (6.6.0)
16
+ nio4r (~> 2.0)
17
+ rack (3.1.16)
18
+
19
+ PLATFORMS
20
+ arm64-darwin-24
21
+ ruby
22
+
23
+ DEPENDENCIES
24
+ datastar!
25
+ puma
26
+ rack
27
+
28
+ BUNDLED WITH
29
+ 2.6.3
@@ -0,0 +1,84 @@
1
+ require 'bundler/setup'
2
+
3
+ require 'datastar'
4
+
5
+ # This is a test Rack endpoint
6
+ # to demo streaming Datastar updates from multiple threads.
7
+ # To run:
8
+ #
9
+ # # install dependencies
10
+ # bundle install
11
+ # # run this endpoint with Puma server
12
+ # bundle exec puma threads.ru
13
+ #
14
+ # visit http://localhost:9292
15
+ #
16
+ INDEX = <<~HTML
17
+ <!DOCTYPE html>
18
+ <html>
19
+ <head>
20
+ <meta charset="UTF-8">
21
+ <title>Datastar counter</title>
22
+ <style>
23
+ body { padding: 10em; }
24
+ .counter {#{' '}
25
+ font-size: 2em;#{' '}
26
+ span { font-weight: bold; }
27
+ }
28
+ </style>
29
+ <script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/datastar@1.0.0-RC.6/bundles/datastar.js"></script>
30
+ </head>
31
+ <body>
32
+ <button#{' '}
33
+ data-on:click="@get('/')"#{' '}
34
+ data-indicator:heartbeat#{' '}
35
+ >Start</button>
36
+ <p class="counter">Slow thread: <span id="slow">waiting</span></p>
37
+ <p class="counter">Fast thread: <span id="fast">waiting</span></p>
38
+ <p id="connection">Disconnected...</p>
39
+ </body>
40
+ <html>
41
+ HTML
42
+
43
+ trap('INT') { exit }
44
+
45
+ run do |env|
46
+ # Initialize Datastar with callbacks
47
+ datastar = Datastar
48
+ .from_rack_env(env)
49
+ .on_connect do |sse|
50
+ sse.patch_elements(%(<p id="connection">Connected...</p>))
51
+ p ['connect', sse]
52
+ end.on_server_disconnect do |sse|
53
+ sse.patch_elements(%(<p id="connection">Done...</p>))
54
+ p ['server disconnect', sse]
55
+ end.on_client_disconnect do |socket|
56
+ p ['client disconnect', socket]
57
+ end.on_error do |error|
58
+ p ['exception', error]
59
+ puts error.backtrace.join("\n")
60
+ end
61
+
62
+ if datastar.sse?
63
+ # This will run in its own thread / fiber
64
+ datastar.stream do |sse|
65
+ 11.times do |i|
66
+ sleep 1
67
+ # Raising an error to demonstrate error handling
68
+ # raise ArgumentError, 'This is an error' if i > 5
69
+
70
+ sse.patch_elements(%(<span id="slow">#{i}</span>))
71
+ end
72
+ end
73
+
74
+ # Another thread / fiber
75
+ datastar.stream do |sse|
76
+ 1000.times do |i|
77
+ sleep 0.01
78
+ sse.patch_elements(%(<span id="fast">#{i}</span>))
79
+ end
80
+ end
81
+ else
82
+ [200, { 'content-type' => 'text/html' }, [INDEX]]
83
+ end
84
+ end
@@ -22,7 +22,9 @@ module Datastar
22
22
 
23
23
  def new_queue = Async::Queue.new
24
24
 
25
- def prepare(response); end
25
+ def prepare(response)
26
+ response.delete_header 'Connection'
27
+ end
26
28
 
27
29
  def spawn(&block)
28
30
  Async(&block)
@@ -285,7 +285,7 @@ module Datastar
285
285
  proc do |socket|
286
286
  generator = ServerSentEventGenerator.new(socket, signals:, view_context: @view_context)
287
287
  @on_connect.each { |callable| callable.call(generator) }
288
- handling_errors(generator, socket) do
288
+ handling_sync_errors(generator, socket) do
289
289
  streamer.call(generator)
290
290
  end
291
291
  ensure
@@ -313,9 +313,10 @@ module Datastar
313
313
  @on_connect.each { |callable| callable.call(conn_generator) }
314
314
 
315
315
  threads = @streamers.map do |streamer|
316
+ duped_signals = signs.dup.freeze
316
317
  @executor.spawn do
317
318
  # TODO: Review thread-safe view context
318
- generator = ServerSentEventGenerator.new(@queue, signals: signs, view_context: @view_context)
319
+ generator = ServerSentEventGenerator.new(@queue, signals: duped_signals, view_context: @view_context)
319
320
  streamer.call(generator)
320
321
  @queue << :done
321
322
  rescue StandardError => e
@@ -323,7 +324,12 @@ module Datastar
323
324
  end
324
325
  end
325
326
 
326
- handling_errors(conn_generator, socket) do
327
+ # Now launch the control thread that actually writes to the socket
328
+ # We don't want to block the main thread, so that servers like Puma
329
+ # which have a limited thread pool can keep serving other requests
330
+ # Other streamers will push any StandardError exceptions to the queue
331
+ # So we handle them here
332
+ @executor.spawn do
327
333
  done_count = 0
328
334
  threads_size = @heartbeat_on ? threads.size - 1 : threads.size
329
335
 
@@ -332,24 +338,46 @@ module Datastar
332
338
  done_count += 1
333
339
  @queue << nil if done_count == threads_size
334
340
  elsif data.is_a?(Exception)
335
- raise data
341
+ handle_streaming_error(data, socket)
342
+ @queue << nil
336
343
  else
337
- socket << data
344
+ # Here we attempt writing to the actual socket
345
+ # which may raise an IOError if the client disconnected
346
+ begin
347
+ socket << data
348
+ rescue Exception => e
349
+ handle_streaming_error(e, socket)
350
+ @queue << nil
351
+ end
338
352
  end
339
353
  end
354
+
355
+ ensure
356
+ @on_server_disconnect.each { |callable| callable.call(conn_generator) }
357
+ @executor.stop(threads) if threads
358
+ socket.close
340
359
  end
341
- ensure
342
- @executor.stop(threads) if threads
343
- socket.close
344
360
  end
345
361
  end
346
362
 
347
- # Run a streaming block while handling errors
363
+ # Handle errors caught during streaming
364
+ # @param error [Exception] the error that occurred
365
+ # @param socket [IO] the socket to pass to error handlers
366
+ def handle_streaming_error(error, socket)
367
+ case error
368
+ when IOError, Errno::EPIPE, Errno::ECONNRESET
369
+ @on_client_disconnect.each { |callable| callable.call(socket) }
370
+ when Exception
371
+ @on_error.each { |callable| callable.call(error) }
372
+ end
373
+ end
374
+
375
+ # Run a block while handling errors
348
376
  # @param generator [ServerSentEventGenerator]
349
377
  # @param socket [IO]
350
378
  # @yield
351
379
  # @api private
352
- def handling_errors(generator, socket, &)
380
+ def handling_sync_errors(generator, socket, &)
353
381
  yield
354
382
 
355
383
  @on_server_disconnect.each { |callable| callable.call(generator) }
@@ -24,6 +24,9 @@ module Datastar
24
24
 
25
25
  attr_reader :signals
26
26
 
27
+ # @param stream [IO, Queue] The IO stream or Queue to write to
28
+ # @option signals [Hash] A hash of signals (params)
29
+ # @option view_context [Object] The view context for rendering elements, if applicable.
27
30
  def initialize(stream, signals:, view_context: nil)
28
31
  @stream = stream
29
32
  @signals = signals
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Datastar
4
- VERSION = '1.0.0.pre.2'
4
+ VERSION = '1.0.1'
5
5
  end
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.pre.2
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ismael Celis
@@ -75,7 +75,15 @@ files:
75
75
  - LICENSE.md
76
76
  - README.md
77
77
  - Rakefile
78
+ - examples/hello-world/Gemfile
79
+ - examples/hello-world/Gemfile.lock
80
+ - examples/hello-world/hello-world.html
81
+ - examples/hello-world/hello-world.ru
82
+ - examples/progress/progress.ru
78
83
  - examples/test.ru
84
+ - examples/threads/Gemfile
85
+ - examples/threads/Gemfile.lock
86
+ - examples/threads/threads.ru
79
87
  - lib/datastar.rb
80
88
  - lib/datastar/async_executor.rb
81
89
  - lib/datastar/configuration.rb
@@ -87,11 +95,11 @@ files:
87
95
  - lib/datastar/server_sent_event_generator.rb
88
96
  - lib/datastar/version.rb
89
97
  - sig/datastar.rbs
90
- homepage: https://github.com/starfederation/datastar#readme
98
+ homepage: https://github.com/starfederation/datastar-ruby#readme
91
99
  licenses: []
92
100
  metadata:
93
- homepage_uri: https://github.com/starfederation/datastar#readme
94
- source_code_uri: https://github.com/starfederation/datastar
101
+ homepage_uri: https://github.com/starfederation/datastar-ruby#readme
102
+ source_code_uri: https://github.com/starfederation/datastar-ruby
95
103
  rdoc_options: []
96
104
  require_paths:
97
105
  - lib
@@ -106,7 +114,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
106
114
  - !ruby/object:Gem::Version
107
115
  version: '0'
108
116
  requirements: []
109
- rubygems_version: 3.6.9
117
+ rubygems_version: 3.7.2
110
118
  specification_version: 4
111
119
  summary: Ruby SDK for Datastar. Rack-compatible.
112
120
  test_files: []