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.
Files changed (32) hide show
  1. package/.github/agents/jsgui3-server.agent.md +699 -0
  2. package/.github/instructions/copilot.instructions.md +180 -0
  3. package/.playwright-mcp/page-2025-11-29T23-39-18-629Z.png +0 -0
  4. package/.playwright-mcp/page-2025-11-29T23-39-31-903Z.png +0 -0
  5. package/.playwright-mcp/page-2025-11-30T00-33-56-265Z.png +0 -0
  6. package/.playwright-mcp/page-2025-11-30T00-34-06-619Z.png +0 -0
  7. package/docs/agent-development-guide.md +108 -4
  8. package/docs/api-reference.md +116 -0
  9. package/docs/controls-development.md +127 -0
  10. package/docs/css/luxuryObsidianCss.js +1203 -0
  11. package/docs/css/obsidian-scrollbars.css +370 -0
  12. package/docs/diagrams/jsgui3-stack.svg +568 -0
  13. package/docs/guides/JSGUI3_UI_ARCHITECTURE_GUIDE.md +2527 -0
  14. package/docs/guides/OBSIDIAN_LUXURY_DESIGN_GUIDE.md +847 -0
  15. package/docs/jsgui3-vs-express-comparison.svg +542 -0
  16. package/docs/jsgui3-vs-nestjs-comparison.svg +550 -0
  17. package/docs/publishers-guide.md +76 -0
  18. package/docs/troubleshooting.md +51 -0
  19. package/examples/controls/15) window, observable SSE/README.md +125 -0
  20. package/examples/controls/15) window, observable SSE/check.js +144 -0
  21. package/examples/controls/15) window, observable SSE/client.js +395 -0
  22. package/examples/controls/15) window, observable SSE/server.js +111 -0
  23. package/http/responders/static/Static_Route_HTTP_Responder.js +16 -16
  24. package/module.js +7 -0
  25. package/package.json +7 -6
  26. package/port-utils.js +112 -0
  27. package/serve-factory.js +27 -5
  28. package/tests/README.md +40 -26
  29. package/tests/examples-controls.e2e.test.js +164 -0
  30. package/tests/observable-sse.test.js +363 -0
  31. package/tests/port-utils.test.js +114 -0
  32. 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.25.12",
10
+ "esbuild": "^0.27.1",
11
11
  "fnl": "^0.0.37",
12
12
  "fnlfs": "^0.0.34",
13
- "jsgui3-client": "^0.0.120",
14
- "jsgui3-html": "^0.0.170",
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.41",
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.31",
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.140",
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
  }