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 +4 -4
- data/LICENSE.md +4 -16
- data/README.md +5 -4
- data/examples/hello-world/Gemfile +8 -0
- data/examples/hello-world/Gemfile.lock +29 -0
- data/examples/hello-world/hello-world.html +35 -0
- data/examples/hello-world/hello-world.ru +37 -0
- data/examples/progress/progress.ru +307 -0
- data/examples/test.ru +1 -1
- data/examples/threads/Gemfile +8 -0
- data/examples/threads/Gemfile.lock +29 -0
- data/examples/threads/threads.ru +84 -0
- data/lib/datastar/async_executor.rb +3 -1
- data/lib/datastar/dispatcher.rb +38 -10
- data/lib/datastar/server_sent_event_generator.rb +3 -0
- data/lib/datastar/version.rb +1 -1
- metadata +13 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cc94dbbe291bd86a1aaae4302cd8fdaa0262f06bcbf0bb81ce201099a92de0b7
|
|
4
|
+
data.tar.gz: 804810f5e0633c690d329f79e3eb220734cb558767f951be18b383bfdd97fcd9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 22716ea763849a6f6aae0a762809b29662e8dfb279dd2bcd0165b2af3a53cb8cbed31f57c8c4af8db55e1f9e54764243741265b01c6d9772eb28b2197790977b
|
|
7
|
+
data.tar.gz: 88efa4fb0a989d5d05548a8264650b89a08f1af6cb6bdcbb848da36e6aa09cc8c37729233ed867fbfdb2a9cfeb3fd8944f27a7b8c618259a569b451d8f0b3a92
|
data/LICENSE.md
CHANGED
|
@@ -1,19 +1,7 @@
|
|
|
1
|
-
Copyright
|
|
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/
|
|
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
|
-
|
|
354
|
+
From the main [Datastar](https://github.com/starfederation/datastar) repo (you'll need Go installed)
|
|
355
355
|
|
|
356
356
|
```bash
|
|
357
|
-
|
|
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,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('/hello-world')" 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
|
@@ -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
|
data/lib/datastar/dispatcher.rb
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
341
|
+
handle_streaming_error(data, socket)
|
|
342
|
+
@queue << nil
|
|
336
343
|
else
|
|
337
|
-
|
|
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
|
-
#
|
|
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
|
|
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
|
data/lib/datastar/version.rb
CHANGED
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.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.
|
|
117
|
+
rubygems_version: 3.7.2
|
|
110
118
|
specification_version: 4
|
|
111
119
|
summary: Ruby SDK for Datastar. Rack-compatible.
|
|
112
120
|
test_files: []
|