jsgui3-server 0.0.140 → 0.0.141
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.
- package/.github/agents/jsgui3-server.agent.md +699 -0
- package/.github/instructions/copilot.instructions.md +180 -0
- package/.playwright-mcp/page-2025-11-29T23-39-18-629Z.png +0 -0
- package/.playwright-mcp/page-2025-11-29T23-39-31-903Z.png +0 -0
- package/.playwright-mcp/page-2025-11-30T00-33-56-265Z.png +0 -0
- package/.playwright-mcp/page-2025-11-30T00-34-06-619Z.png +0 -0
- package/docs/agent-development-guide.md +108 -4
- package/docs/api-reference.md +116 -0
- package/docs/controls-development.md +127 -0
- package/docs/css/luxuryObsidianCss.js +1203 -0
- package/docs/css/obsidian-scrollbars.css +370 -0
- package/docs/diagrams/jsgui3-stack.svg +568 -0
- package/docs/guides/JSGUI3_UI_ARCHITECTURE_GUIDE.md +2527 -0
- package/docs/guides/OBSIDIAN_LUXURY_DESIGN_GUIDE.md +847 -0
- package/docs/jsgui3-vs-express-comparison.svg +542 -0
- package/docs/jsgui3-vs-nestjs-comparison.svg +550 -0
- package/docs/publishers-guide.md +76 -0
- package/docs/troubleshooting.md +51 -0
- package/examples/controls/15) window, observable SSE/README.md +125 -0
- package/examples/controls/15) window, observable SSE/check.js +144 -0
- package/examples/controls/15) window, observable SSE/client.js +395 -0
- package/examples/controls/15) window, observable SSE/server.js +111 -0
- package/http/responders/static/Static_Route_HTTP_Responder.js +16 -16
- package/module.js +7 -0
- package/package.json +7 -6
- package/port-utils.js +112 -0
- package/serve-factory.js +27 -5
- package/tests/README.md +40 -26
- package/tests/examples-controls.e2e.test.js +164 -0
- package/tests/observable-sse.test.js +363 -0
- package/tests/port-utils.test.js +114 -0
- package/tests/test-runner.js +13 -12
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
const jsgui = require('jsgui3-client');
|
|
2
|
+
const {controls, Control} = jsgui;
|
|
3
|
+
|
|
4
|
+
const Active_HTML_Document = require('../../../controls/Active_HTML_Document');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Demo showing Server-Sent Events (SSE) with observable pattern.
|
|
8
|
+
*
|
|
9
|
+
* This demonstrates:
|
|
10
|
+
* 1. Server publishing an observable that emits events over time
|
|
11
|
+
* 2. Client consuming SSE stream and updating UI reactively
|
|
12
|
+
* 3. Real-time updates from server to client via SSE
|
|
13
|
+
*/
|
|
14
|
+
class Observable_Demo_UI extends Active_HTML_Document {
|
|
15
|
+
constructor(spec = {}) {
|
|
16
|
+
spec.__type_name = spec.__type_name || 'observable_demo_ui';
|
|
17
|
+
super(spec);
|
|
18
|
+
const {context} = this;
|
|
19
|
+
|
|
20
|
+
if (typeof this.body.add_class === 'function') {
|
|
21
|
+
this.body.add_class('observable-demo-ui');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const compose = () => {
|
|
25
|
+
// Container
|
|
26
|
+
const container = new controls.div({
|
|
27
|
+
context: context,
|
|
28
|
+
'class': 'sse-container'
|
|
29
|
+
});
|
|
30
|
+
this.body.add(container);
|
|
31
|
+
|
|
32
|
+
// Header
|
|
33
|
+
const header = new controls.div({
|
|
34
|
+
context: context,
|
|
35
|
+
'class': 'sse-header'
|
|
36
|
+
});
|
|
37
|
+
container.add(header);
|
|
38
|
+
|
|
39
|
+
const title = new controls.h2({
|
|
40
|
+
context: context
|
|
41
|
+
});
|
|
42
|
+
title.add('Observable SSE Demo - Real-Time Tick Stream');
|
|
43
|
+
header.add(title);
|
|
44
|
+
|
|
45
|
+
// Status display
|
|
46
|
+
const status_div = new controls.div({
|
|
47
|
+
context: context,
|
|
48
|
+
'class': 'status-section'
|
|
49
|
+
});
|
|
50
|
+
status_div.dom.attributes.id = 'status-label';
|
|
51
|
+
status_div.add('Status: Not connected');
|
|
52
|
+
container.add(status_div);
|
|
53
|
+
|
|
54
|
+
// Tick counter display
|
|
55
|
+
const tick_display = new controls.div({
|
|
56
|
+
context: context,
|
|
57
|
+
'class': 'tick-display'
|
|
58
|
+
});
|
|
59
|
+
container.add(tick_display);
|
|
60
|
+
|
|
61
|
+
const tick_count = new controls.div({
|
|
62
|
+
context: context,
|
|
63
|
+
'class': 'tick-count'
|
|
64
|
+
});
|
|
65
|
+
tick_count.dom.attributes.id = 'tick-count';
|
|
66
|
+
tick_count.add('--');
|
|
67
|
+
tick_display.add(tick_count);
|
|
68
|
+
|
|
69
|
+
const tick_label = new controls.div({
|
|
70
|
+
context: context,
|
|
71
|
+
'class': 'tick-label'
|
|
72
|
+
});
|
|
73
|
+
tick_label.add('Server Ticks');
|
|
74
|
+
tick_display.add(tick_label);
|
|
75
|
+
|
|
76
|
+
// Event log
|
|
77
|
+
const log_label = new controls.div({
|
|
78
|
+
context: context,
|
|
79
|
+
'class': 'log-label'
|
|
80
|
+
});
|
|
81
|
+
log_label.add('Event Log (SSE messages):');
|
|
82
|
+
container.add(log_label);
|
|
83
|
+
|
|
84
|
+
const event_log = new controls.div({
|
|
85
|
+
context: context,
|
|
86
|
+
'class': 'event-log'
|
|
87
|
+
});
|
|
88
|
+
event_log.dom.attributes.id = 'event-log';
|
|
89
|
+
container.add(event_log);
|
|
90
|
+
|
|
91
|
+
// Control buttons
|
|
92
|
+
const button_container = new controls.div({
|
|
93
|
+
context: context,
|
|
94
|
+
'class': 'button-container'
|
|
95
|
+
});
|
|
96
|
+
container.add(button_container);
|
|
97
|
+
|
|
98
|
+
const connect_button = new controls.Button({
|
|
99
|
+
context: context,
|
|
100
|
+
text: 'Connect to SSE'
|
|
101
|
+
});
|
|
102
|
+
connect_button.dom.attributes.id = 'connect-btn';
|
|
103
|
+
button_container.add(connect_button);
|
|
104
|
+
|
|
105
|
+
const disconnect_button = new controls.Button({
|
|
106
|
+
context: context,
|
|
107
|
+
text: 'Disconnect'
|
|
108
|
+
});
|
|
109
|
+
disconnect_button.dom.attributes.id = 'disconnect-btn';
|
|
110
|
+
button_container.add(disconnect_button);
|
|
111
|
+
|
|
112
|
+
const clear_button = new controls.Button({
|
|
113
|
+
context: context,
|
|
114
|
+
text: 'Clear Log'
|
|
115
|
+
});
|
|
116
|
+
clear_button.dom.attributes.id = 'clear-btn';
|
|
117
|
+
button_container.add(clear_button);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
if (!spec.el) {
|
|
121
|
+
compose();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
activate() {
|
|
126
|
+
if (!this.__active) {
|
|
127
|
+
super.activate();
|
|
128
|
+
|
|
129
|
+
// Get DOM references via getElementById (works on client)
|
|
130
|
+
const status_label = document.getElementById('status-label');
|
|
131
|
+
const tick_count_el = document.getElementById('tick-count');
|
|
132
|
+
const event_log = document.getElementById('event-log');
|
|
133
|
+
const connect_button = document.getElementById('connect-btn');
|
|
134
|
+
const disconnect_button = document.getElementById('disconnect-btn');
|
|
135
|
+
const clear_button = document.getElementById('clear-btn');
|
|
136
|
+
|
|
137
|
+
let event_source = null;
|
|
138
|
+
let message_count = 0;
|
|
139
|
+
|
|
140
|
+
const log_event = (message, type = 'info') => {
|
|
141
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
142
|
+
const log_entry = document.createElement('div');
|
|
143
|
+
log_entry.textContent = `[${timestamp}] ${message}`;
|
|
144
|
+
log_entry.className = `log-entry log-${type}`;
|
|
145
|
+
event_log.appendChild(log_entry);
|
|
146
|
+
|
|
147
|
+
// Auto-scroll to bottom
|
|
148
|
+
event_log.scrollTop = event_log.scrollHeight;
|
|
149
|
+
|
|
150
|
+
// Keep only last 50 entries
|
|
151
|
+
message_count++;
|
|
152
|
+
if (message_count > 50) {
|
|
153
|
+
const first_child = event_log.firstChild;
|
|
154
|
+
if (first_child) {
|
|
155
|
+
event_log.removeChild(first_child);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const connect_sse = () => {
|
|
161
|
+
if (event_source) {
|
|
162
|
+
event_source.close();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
status_label.textContent = 'Status: Connecting...';
|
|
166
|
+
log_event('Connecting to /api/tick-stream...', 'info');
|
|
167
|
+
|
|
168
|
+
// Connect to the observable SSE endpoint
|
|
169
|
+
event_source = new EventSource('/api/tick-stream');
|
|
170
|
+
|
|
171
|
+
event_source.onopen = () => {
|
|
172
|
+
status_label.textContent = 'Status: Connected (streaming)';
|
|
173
|
+
log_event('Connected! Receiving real-time tick events...', 'success');
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
event_source.onmessage = (event) => {
|
|
177
|
+
try {
|
|
178
|
+
// Handle the initial "OK" message
|
|
179
|
+
if (event.data === 'OK') {
|
|
180
|
+
log_event('SSE handshake complete', 'info');
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const data = JSON.parse(event.data);
|
|
185
|
+
|
|
186
|
+
// Update the tick counter display
|
|
187
|
+
if (data.tick !== undefined) {
|
|
188
|
+
tick_count_el.textContent = data.tick.toString();
|
|
189
|
+
log_event(`Tick #${data.tick}: ${data.message}`, 'tick');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (data.error) {
|
|
193
|
+
status_label.textContent = 'Status: Error';
|
|
194
|
+
log_event(`Error: ${data.error}`, 'error');
|
|
195
|
+
}
|
|
196
|
+
} catch (e) {
|
|
197
|
+
// Non-JSON message (like "OK")
|
|
198
|
+
log_event(`Raw: ${event.data}`, 'info');
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
event_source.onerror = (err) => {
|
|
203
|
+
status_label.textContent = 'Status: Disconnected';
|
|
204
|
+
log_event('Connection lost or error occurred', 'warning');
|
|
205
|
+
if (event_source) {
|
|
206
|
+
event_source.close();
|
|
207
|
+
event_source = null;
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const disconnect_sse = () => {
|
|
213
|
+
if (event_source) {
|
|
214
|
+
event_source.close();
|
|
215
|
+
event_source = null;
|
|
216
|
+
status_label.textContent = 'Status: Disconnected';
|
|
217
|
+
log_event('Manually disconnected from SSE stream', 'info');
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// Button handlers
|
|
222
|
+
connect_button.addEventListener('click', () => {
|
|
223
|
+
connect_sse();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
disconnect_button.addEventListener('click', () => {
|
|
227
|
+
disconnect_sse();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
clear_button.addEventListener('click', () => {
|
|
231
|
+
event_log.innerHTML = '';
|
|
232
|
+
message_count = 0;
|
|
233
|
+
tick_count_el.textContent = '--';
|
|
234
|
+
status_label.textContent = 'Status: Not connected';
|
|
235
|
+
log_event('Log cleared', 'info');
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
log_event('Observable SSE Demo ready. Click "Connect to SSE" to begin streaming.', 'info');
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
Observable_Demo_UI.css = `
|
|
244
|
+
* {
|
|
245
|
+
margin: 0;
|
|
246
|
+
padding: 0;
|
|
247
|
+
box-sizing: border-box;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
body {
|
|
251
|
+
overflow-x: hidden;
|
|
252
|
+
overflow-y: auto;
|
|
253
|
+
background-color: #1a1a2e;
|
|
254
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.observable-demo-ui {
|
|
258
|
+
padding: 20px;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.sse-container {
|
|
262
|
+
max-width: 600px;
|
|
263
|
+
margin: 0 auto;
|
|
264
|
+
padding: 20px;
|
|
265
|
+
background: #16213e;
|
|
266
|
+
border-radius: 12px;
|
|
267
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.sse-header {
|
|
271
|
+
text-align: center;
|
|
272
|
+
margin-bottom: 20px;
|
|
273
|
+
padding-bottom: 15px;
|
|
274
|
+
border-bottom: 1px solid #e94560;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.sse-header h2 {
|
|
278
|
+
color: #e94560;
|
|
279
|
+
font-size: 20px;
|
|
280
|
+
font-weight: 600;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.status-section {
|
|
284
|
+
padding: 12px;
|
|
285
|
+
margin-bottom: 15px;
|
|
286
|
+
background: #0f3460;
|
|
287
|
+
border-radius: 6px;
|
|
288
|
+
color: #e0e0e0;
|
|
289
|
+
font-weight: 500;
|
|
290
|
+
text-align: center;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.tick-display {
|
|
294
|
+
text-align: center;
|
|
295
|
+
padding: 25px;
|
|
296
|
+
margin-bottom: 20px;
|
|
297
|
+
background: linear-gradient(135deg, #0f3460 0%, #1a1a2e 100%);
|
|
298
|
+
border-radius: 10px;
|
|
299
|
+
border: 2px solid #e94560;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.tick-count {
|
|
303
|
+
font-size: 64px;
|
|
304
|
+
font-weight: bold;
|
|
305
|
+
color: #e94560;
|
|
306
|
+
text-shadow: 0 0 30px rgba(233, 69, 96, 0.6);
|
|
307
|
+
font-family: 'Consolas', 'Monaco', monospace;
|
|
308
|
+
line-height: 1;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.tick-label {
|
|
312
|
+
color: #7ec8e3;
|
|
313
|
+
font-size: 14px;
|
|
314
|
+
margin-top: 10px;
|
|
315
|
+
text-transform: uppercase;
|
|
316
|
+
letter-spacing: 3px;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.log-label {
|
|
320
|
+
color: #e0e0e0;
|
|
321
|
+
margin-bottom: 8px;
|
|
322
|
+
font-weight: 500;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.event-log {
|
|
326
|
+
height: 180px;
|
|
327
|
+
overflow-y: auto;
|
|
328
|
+
background: #0f0f23;
|
|
329
|
+
border: 1px solid #333;
|
|
330
|
+
border-radius: 6px;
|
|
331
|
+
padding: 10px;
|
|
332
|
+
margin-bottom: 15px;
|
|
333
|
+
font-family: 'Consolas', 'Monaco', monospace;
|
|
334
|
+
font-size: 12px;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.log-entry {
|
|
338
|
+
padding: 4px 0;
|
|
339
|
+
border-bottom: 1px solid #222;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.log-info { color: #7ec8e3; }
|
|
343
|
+
.log-success { color: #50c878; }
|
|
344
|
+
.log-warning { color: #ffa500; }
|
|
345
|
+
.log-error { color: #ff6b6b; }
|
|
346
|
+
.log-tick { color: #e94560; }
|
|
347
|
+
|
|
348
|
+
.button-container {
|
|
349
|
+
display: flex;
|
|
350
|
+
gap: 10px;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.button-container button {
|
|
354
|
+
flex: 1;
|
|
355
|
+
padding: 14px 20px;
|
|
356
|
+
border: none;
|
|
357
|
+
border-radius: 6px;
|
|
358
|
+
cursor: pointer;
|
|
359
|
+
font-weight: 600;
|
|
360
|
+
font-size: 14px;
|
|
361
|
+
transition: all 0.2s ease;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.button-container button:first-child {
|
|
365
|
+
background: linear-gradient(135deg, #0f3460 0%, #e94560 100%);
|
|
366
|
+
color: white;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.button-container button:first-child:hover {
|
|
370
|
+
transform: translateY(-2px);
|
|
371
|
+
box-shadow: 0 4px 15px rgba(233, 69, 96, 0.4);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.button-container button:nth-child(2) {
|
|
375
|
+
background: #e94560;
|
|
376
|
+
color: white;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.button-container button:nth-child(2):hover {
|
|
380
|
+
background: #d63850;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.button-container button:last-child {
|
|
384
|
+
background: #333;
|
|
385
|
+
color: #e0e0e0;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.button-container button:last-child:hover {
|
|
389
|
+
background: #444;
|
|
390
|
+
}
|
|
391
|
+
`;
|
|
392
|
+
|
|
393
|
+
controls.Observable_Demo_UI = Observable_Demo_UI;
|
|
394
|
+
|
|
395
|
+
module.exports = jsgui;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
const jsgui = require('./client');
|
|
2
|
+
const Server = require('../../../server');
|
|
3
|
+
const {Observable_Demo_UI} = jsgui.controls;
|
|
4
|
+
const {obs, observable} = require('fnl');
|
|
5
|
+
|
|
6
|
+
// Import the Observable Publisher
|
|
7
|
+
const Observable_Publisher = require('../../../publishers/http-observable-publisher');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Server demonstrating HTTP_Observable_Publisher with Server-Sent Events (SSE).
|
|
11
|
+
*
|
|
12
|
+
* This example shows:
|
|
13
|
+
* 1. Creating an observable that emits progress events over time
|
|
14
|
+
* 2. Publishing it via HTTP_Observable_Publisher (uses SSE transport)
|
|
15
|
+
* 3. Client consuming the stream with EventSource API
|
|
16
|
+
*
|
|
17
|
+
* The fnl observable pattern:
|
|
18
|
+
* - obs((next, complete, error) => {...}) creates an observable
|
|
19
|
+
* - next(data) emits intermediate values (progress updates)
|
|
20
|
+
* - complete(result) signals successful completion
|
|
21
|
+
* - error(err) signals failure
|
|
22
|
+
*
|
|
23
|
+
* IMPORTANT: This example uses a "hot" observable that emits a continuous
|
|
24
|
+
* tick stream. The HTTP_Observable_Publisher creates an SSE connection
|
|
25
|
+
* and forwards all 'next' events to the client.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
if (require.main === module) {
|
|
29
|
+
|
|
30
|
+
const server = new Server({
|
|
31
|
+
Ctrl: Observable_Demo_UI,
|
|
32
|
+
'src_path_client_js': require.resolve('./client.js'),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
server.on('ready', () => {
|
|
36
|
+
console.log('Server ready, setting up observable endpoints...');
|
|
37
|
+
|
|
38
|
+
// ========================================
|
|
39
|
+
// Create a "hot" observable for tick stream
|
|
40
|
+
// This emits continuously, clients join the stream
|
|
41
|
+
// ========================================
|
|
42
|
+
let tick_count = 0;
|
|
43
|
+
const tick_observable = observable((next, complete, error) => {
|
|
44
|
+
// Start a tick interval that all subscribers share
|
|
45
|
+
const interval = setInterval(() => {
|
|
46
|
+
tick_count++;
|
|
47
|
+
next({
|
|
48
|
+
tick: tick_count,
|
|
49
|
+
timestamp: Date.now(),
|
|
50
|
+
message: `Server tick #${tick_count}`
|
|
51
|
+
});
|
|
52
|
+
}, 1000); // Tick every second
|
|
53
|
+
|
|
54
|
+
// Return cleanup (called when observable is disposed)
|
|
55
|
+
return [() => {
|
|
56
|
+
clearInterval(interval);
|
|
57
|
+
console.log('Tick observable cleanup');
|
|
58
|
+
}];
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Create the observable publisher for the tick stream
|
|
62
|
+
const tick_publisher = new Observable_Publisher({
|
|
63
|
+
obs: tick_observable
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Register the SSE endpoint with the server's router
|
|
67
|
+
server.server_router.set_route('/api/tick-stream', tick_publisher, tick_publisher.handle_http);
|
|
68
|
+
console.log(' ✓ /api/tick-stream - Hot tick stream (SSE)');
|
|
69
|
+
|
|
70
|
+
// ========================================
|
|
71
|
+
// Also publish a simple JSON API endpoint for comparison
|
|
72
|
+
// ========================================
|
|
73
|
+
server.publish('/api/status', () => {
|
|
74
|
+
return {
|
|
75
|
+
status: 'ok',
|
|
76
|
+
tick_count: tick_count,
|
|
77
|
+
uptime: process.uptime(),
|
|
78
|
+
note: 'This is a regular JSON endpoint (one-shot). Use /api/tick-stream for SSE.'
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
console.log(' ✓ /api/status - Regular JSON API (comparison)');
|
|
82
|
+
|
|
83
|
+
// Start the server (allow PORT env variable for testing)
|
|
84
|
+
const port = parseInt(process.env.PORT, 10) || 52015;
|
|
85
|
+
server.start(port, function (err, cb_start) {
|
|
86
|
+
if (err) {
|
|
87
|
+
throw err;
|
|
88
|
+
} else {
|
|
89
|
+
console.log('');
|
|
90
|
+
console.log('='.repeat(60));
|
|
91
|
+
console.log('Observable SSE Demo Server Started');
|
|
92
|
+
console.log('='.repeat(60));
|
|
93
|
+
console.log('');
|
|
94
|
+
console.log(`Open http://localhost:${port} in your browser`);
|
|
95
|
+
console.log('');
|
|
96
|
+
console.log('This demo shows:');
|
|
97
|
+
console.log(' • Server-side observable emitting tick events');
|
|
98
|
+
console.log(' • HTTP_Observable_Publisher serving as SSE endpoint');
|
|
99
|
+
console.log(' • Client consuming stream with EventSource API');
|
|
100
|
+
console.log(' • Real-time UI updates from server events');
|
|
101
|
+
console.log('');
|
|
102
|
+
console.log('Endpoints:');
|
|
103
|
+
console.log(' GET /api/tick-stream - SSE stream (text/event-stream)');
|
|
104
|
+
console.log(' GET /api/status - JSON status (application/json)');
|
|
105
|
+
console.log('');
|
|
106
|
+
console.log('Click "Connect to SSE" to see real-time streaming!');
|
|
107
|
+
console.log('='.repeat(60));
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
}
|
|
@@ -29,13 +29,13 @@ class Static_Route_HTTP_Responder extends HTTP_Responder {
|
|
|
29
29
|
// Give it the bundle object as spec...?
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
}
|
|
33
|
-
handle_http(req, res) {
|
|
34
|
-
const r_headers = req.headers;
|
|
35
|
-
const accept_encoding = r_headers['accept-encoding'];
|
|
36
|
-
|
|
37
|
-
// Need to call it with the correct context.
|
|
38
|
-
// Seems like jsgui3-html Router and Routing_Tree need some more fixes.
|
|
32
|
+
}
|
|
33
|
+
handle_http(req, res) {
|
|
34
|
+
const r_headers = req.headers;
|
|
35
|
+
const accept_encoding = r_headers['accept-encoding'] || '';
|
|
36
|
+
|
|
37
|
+
// Need to call it with the correct context.
|
|
38
|
+
// Seems like jsgui3-html Router and Routing_Tree need some more fixes.
|
|
39
39
|
|
|
40
40
|
const {type, extension, text, route, response_buffers, response_headers} = this;
|
|
41
41
|
|
|
@@ -46,14 +46,14 @@ class Static_Route_HTTP_Responder extends HTTP_Responder {
|
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
|
|
49
|
-
// See about 'gzip'.
|
|
50
|
-
// Not sure why br does not show up as normal.
|
|
51
|
-
|
|
52
|
-
const supported_encodings = {};
|
|
53
|
-
|
|
54
|
-
if (accept_encoding.includes('gzip')) supported_encodings.gzip = true;
|
|
55
|
-
|
|
56
|
-
if (accept_encoding.includes('br')) supported_encodings.br = true;
|
|
49
|
+
// See about 'gzip'.
|
|
50
|
+
// Not sure why br does not show up as normal.
|
|
51
|
+
|
|
52
|
+
const supported_encodings = {};
|
|
53
|
+
|
|
54
|
+
if (typeof accept_encoding === 'string' && accept_encoding.includes('gzip')) supported_encodings.gzip = true;
|
|
55
|
+
|
|
56
|
+
if (typeof accept_encoding === 'string' && accept_encoding.includes('br')) supported_encodings.br = true;
|
|
57
57
|
|
|
58
58
|
//console.log('supported_encodings', supported_encodings);
|
|
59
59
|
|
|
@@ -102,4 +102,4 @@ class Static_Route_HTTP_Responder extends HTTP_Responder {
|
|
|
102
102
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
module.exports = Static_Route_HTTP_Responder;
|
|
105
|
+
module.exports = Static_Route_HTTP_Responder;
|
package/module.js
CHANGED
|
@@ -20,6 +20,13 @@ const Server = require('./server');
|
|
|
20
20
|
jsgui.Server = Server;
|
|
21
21
|
jsgui.serve = Server.serve;
|
|
22
22
|
jsgui.fs2 = require('./fs2');
|
|
23
|
+
|
|
24
|
+
// Port utilities for auto-port selection
|
|
25
|
+
const port_utils = require('./port-utils');
|
|
26
|
+
jsgui.port_utils = port_utils;
|
|
27
|
+
jsgui.get_free_port = port_utils.get_free_port;
|
|
28
|
+
jsgui.is_port_available = port_utils.is_port_available;
|
|
29
|
+
|
|
23
30
|
//jsgui.Resource = Resource;
|
|
24
31
|
//console.log('pre scs');
|
|
25
32
|
//jsgui.Single_Control_Server = require('./single-control-server');
|
package/package.json
CHANGED
|
@@ -7,18 +7,18 @@
|
|
|
7
7
|
"@babel/generator": "^7.28.5",
|
|
8
8
|
"@babel/parser": "^7.28.5",
|
|
9
9
|
"cookies": "^0.9.1",
|
|
10
|
-
"esbuild": "^0.
|
|
10
|
+
"esbuild": "^0.27.1",
|
|
11
11
|
"fnl": "^0.0.37",
|
|
12
12
|
"fnlfs": "^0.0.34",
|
|
13
|
-
"jsgui3-client": "^0.0.
|
|
14
|
-
"jsgui3-html": "^0.0.
|
|
13
|
+
"jsgui3-client": "^0.0.122",
|
|
14
|
+
"jsgui3-html": "^0.0.172",
|
|
15
15
|
"jsgui3-webpage": "^0.0.8",
|
|
16
16
|
"jsgui3-website": "^0.0.8",
|
|
17
|
-
"lang-tools": "^0.0.
|
|
17
|
+
"lang-tools": "^0.0.43",
|
|
18
18
|
"mocha": "^11.7.4",
|
|
19
19
|
"multiparty": "^4.2.3",
|
|
20
20
|
"ncp": "^2.0.0",
|
|
21
|
-
"obext": "^0.0.
|
|
21
|
+
"obext": "^0.0.33",
|
|
22
22
|
"rimraf": "^6.1.0",
|
|
23
23
|
"stream-to-array": "^2.3.0",
|
|
24
24
|
"url-parse": "^1.5.10"
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"type": "git",
|
|
40
40
|
"url": "https://github.com/metabench/jsgui3-server.git"
|
|
41
41
|
},
|
|
42
|
-
"version": "0.0.
|
|
42
|
+
"version": "0.0.141",
|
|
43
43
|
"scripts": {
|
|
44
44
|
"cli": "node cli.js",
|
|
45
45
|
"serve": "node cli.js serve",
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"test:content": "node tests/test-runner.js --test=content-analysis.test.js",
|
|
54
54
|
"test:performance": "node tests/test-runner.js --test=performance.test.js",
|
|
55
55
|
"test:errors": "node tests/test-runner.js --test=error-handling.test.js",
|
|
56
|
+
"test:examples:controls": "node tests/test-runner.js --test=examples-controls.e2e.test.js",
|
|
56
57
|
"test:debug": "node tests/test-runner.js --debug",
|
|
57
58
|
"test:verbose": "node tests/test-runner.js --verbose"
|
|
58
59
|
}
|