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
package/docs/publishers-guide.md
CHANGED
|
@@ -100,6 +100,82 @@ const publisher = new HTTP_Function_Publisher({
|
|
|
100
100
|
- Support for common image formats (JPEG, PNG, SVG, etc.)
|
|
101
101
|
- Efficient streaming for large files
|
|
102
102
|
|
|
103
|
+
### HTTP_Observable_Publisher
|
|
104
|
+
|
|
105
|
+
**Purpose:** Streams observable data to clients using Server-Sent Events (SSE).
|
|
106
|
+
|
|
107
|
+
**Key Features:**
|
|
108
|
+
- Real-time streaming of observable events
|
|
109
|
+
- SSE protocol support (`text/event-stream`)
|
|
110
|
+
- Chunked transfer encoding for long-running connections
|
|
111
|
+
- Integration with `fnl` observables
|
|
112
|
+
|
|
113
|
+
**Usage:**
|
|
114
|
+
```javascript
|
|
115
|
+
const { observable } = require('fnl');
|
|
116
|
+
const Observable_Publisher = require('jsgui3-server/publishers/http-observable-publisher');
|
|
117
|
+
|
|
118
|
+
// Create a hot observable that emits continuously
|
|
119
|
+
let tick_count = 0;
|
|
120
|
+
const tick_stream = observable((next, complete, error) => {
|
|
121
|
+
const interval = setInterval(() => {
|
|
122
|
+
tick_count++;
|
|
123
|
+
next({
|
|
124
|
+
tick: tick_count,
|
|
125
|
+
timestamp: Date.now(),
|
|
126
|
+
message: `Server tick #${tick_count}`
|
|
127
|
+
});
|
|
128
|
+
}, 1000);
|
|
129
|
+
|
|
130
|
+
// Return cleanup function
|
|
131
|
+
return [() => clearInterval(interval)];
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Create the SSE publisher
|
|
135
|
+
const publisher = new Observable_Publisher({
|
|
136
|
+
obs: tick_stream
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Register with server router
|
|
140
|
+
server.server_router.set_route('/api/stream', publisher, publisher.handle_http);
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**Client-Side Consumption:**
|
|
144
|
+
```javascript
|
|
145
|
+
// In browser, use EventSource API
|
|
146
|
+
const eventSource = new EventSource('/api/stream');
|
|
147
|
+
|
|
148
|
+
eventSource.onmessage = (event) => {
|
|
149
|
+
if (event.data === 'OK') {
|
|
150
|
+
console.log('SSE handshake complete');
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const data = JSON.parse(event.data);
|
|
154
|
+
console.log('Received:', data);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
eventSource.onerror = () => {
|
|
158
|
+
eventSource.close();
|
|
159
|
+
};
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**SSE Protocol:**
|
|
163
|
+
The publisher sends events in SSE format:
|
|
164
|
+
```
|
|
165
|
+
HTTP/1.1 200 OK
|
|
166
|
+
Content-Type: text/event-stream
|
|
167
|
+
Transfer-Encoding: chunked
|
|
168
|
+
|
|
169
|
+
OK
|
|
170
|
+
event: message
|
|
171
|
+
data:{"tick":1,"timestamp":1234567890,"message":"Server tick #1"}
|
|
172
|
+
|
|
173
|
+
event: message
|
|
174
|
+
data:{"tick":2,"timestamp":1234567891,"message":"Server tick #2"}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**See Also:** [Observable SSE Demo](../examples/controls/15)%20window,%20observable%20SSE/) for a complete working example.
|
|
178
|
+
|
|
103
179
|
## Publisher Architecture
|
|
104
180
|
|
|
105
181
|
### Base Publisher Class
|
package/docs/troubleshooting.md
CHANGED
|
@@ -216,6 +216,57 @@ input.js:1:0: ERROR: Expected identifier but found "}"
|
|
|
216
216
|
}
|
|
217
217
|
```
|
|
218
218
|
|
|
219
|
+
### Text Content Not Rendering
|
|
220
|
+
|
|
221
|
+
**Symptoms:**
|
|
222
|
+
- HTML elements appear but are empty
|
|
223
|
+
- Buttons have text but divs, spans, h2 don't
|
|
224
|
+
- Server-side rendered HTML shows empty tags
|
|
225
|
+
|
|
226
|
+
**Cause:** Using `text` property instead of `.add()` method for HTML elements.
|
|
227
|
+
|
|
228
|
+
**Solutions:**
|
|
229
|
+
|
|
230
|
+
1. **Use `.add()` for HTML elements (div, span, h2, etc.):**
|
|
231
|
+
```javascript
|
|
232
|
+
// ❌ WRONG - text won't render
|
|
233
|
+
const title = new controls.h2({ context, text: 'My Title' });
|
|
234
|
+
|
|
235
|
+
// ❌ WRONG - text property won't render
|
|
236
|
+
const div = new controls.div({ context });
|
|
237
|
+
div.text = 'Content';
|
|
238
|
+
|
|
239
|
+
// ✅ CORRECT - use .add() method
|
|
240
|
+
const title = new controls.h2({ context });
|
|
241
|
+
title.add('My Title');
|
|
242
|
+
|
|
243
|
+
const div = new controls.div({ context });
|
|
244
|
+
div.add('Content');
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
2. **Composite controls (Button, Checkbox) DO support text property:**
|
|
248
|
+
```javascript
|
|
249
|
+
// ✅ CORRECT - Button handles text internally
|
|
250
|
+
const button = new controls.Button({ context, text: 'Click Me' });
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
3. **Test rendering without server:**
|
|
254
|
+
Create a `check.js` file to verify text renders:
|
|
255
|
+
```javascript
|
|
256
|
+
const jsgui = require('./client');
|
|
257
|
+
const { MyControl } = jsgui.controls;
|
|
258
|
+
const Server_Page_Context = require('jsgui3-server/page-context');
|
|
259
|
+
|
|
260
|
+
const context = new Server_Page_Context();
|
|
261
|
+
const control = new MyControl({ context });
|
|
262
|
+
const html = control.all_html_render();
|
|
263
|
+
|
|
264
|
+
// Check if expected text is in HTML
|
|
265
|
+
console.log(html.includes('Expected Text') ? 'PASS' : 'FAIL');
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
**Why this happens:** HTML element controls (`div`, `h2`, `span`) are thin wrappers around DOM elements. Text is a child node, not a property. Composite controls like `Button` have explicit code to handle the `text` property.
|
|
269
|
+
|
|
219
270
|
### CSS Not Loading
|
|
220
271
|
|
|
221
272
|
**Symptoms:**
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Observable SSE Demo
|
|
2
|
+
|
|
3
|
+
This example demonstrates the **HTTP_Observable_Publisher** with Server-Sent Events (SSE), showing how jsgui3-server's observable-first architecture enables real-time streaming from server to client.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
cd "examples/controls/15) window, observable SSE"
|
|
11
|
+
node server.js
|
|
12
|
+
# Open http://localhost:52015
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## What This Demonstrates
|
|
16
|
+
|
|
17
|
+
### Server Side (`server.js`)
|
|
18
|
+
|
|
19
|
+
1. **Observable Creation** with `fnl`:
|
|
20
|
+
```javascript
|
|
21
|
+
const {obs} = require('fnl');
|
|
22
|
+
|
|
23
|
+
const progress_observable = obs((next, complete, error) => {
|
|
24
|
+
// next(data) - emit intermediate values
|
|
25
|
+
// complete(result) - signal completion
|
|
26
|
+
// error(err) - signal failure
|
|
27
|
+
return [cleanup_fn]; // cleanup functions
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
2. **HTTP_Observable_Publisher** setup:
|
|
32
|
+
```javascript
|
|
33
|
+
const Observable_Publisher = require('jsgui3-server/publishers/http-observable-publisher');
|
|
34
|
+
|
|
35
|
+
const publisher = new Observable_Publisher({ obs: progress_observable });
|
|
36
|
+
server.server_router.set_route('/api/progress-stream', publisher, publisher.handle_http);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
3. **SSE Transport**: The publisher automatically serves `text/event-stream` content type with chunked transfer encoding.
|
|
40
|
+
|
|
41
|
+
### Client Side (`client.js`)
|
|
42
|
+
|
|
43
|
+
1. **EventSource API** for consuming SSE:
|
|
44
|
+
```javascript
|
|
45
|
+
const event_source = new EventSource('/api/progress-stream');
|
|
46
|
+
|
|
47
|
+
event_source.onmessage = (event) => {
|
|
48
|
+
const data = JSON.parse(event.data);
|
|
49
|
+
update_ui(data);
|
|
50
|
+
};
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
2. **Reactive UI Updates**: The `Observable_Demo_UI` control updates progress bars, status text, and log entries in real-time as events arrive.
|
|
54
|
+
|
|
55
|
+
## The SSE Protocol
|
|
56
|
+
|
|
57
|
+
Server-Sent Events use HTTP chunked transfer encoding:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
HTTP/1.1 200 OK
|
|
61
|
+
Content-Type: text/event-stream
|
|
62
|
+
Transfer-Encoding: chunked
|
|
63
|
+
|
|
64
|
+
event: message
|
|
65
|
+
data: {"progress": 10, "stage": "Loading..."}
|
|
66
|
+
|
|
67
|
+
event: message
|
|
68
|
+
data: {"progress": 20, "stage": "Processing..."}
|
|
69
|
+
|
|
70
|
+
...
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Observable Pattern Benefits
|
|
74
|
+
|
|
75
|
+
| Approach | When to Use |
|
|
76
|
+
|----------|-------------|
|
|
77
|
+
| **Observable + SSE** | Long-running operations, real-time progress, streaming data |
|
|
78
|
+
| **Promise** | Single async result (most API calls) |
|
|
79
|
+
| **Sync return** | Immediate data, no I/O |
|
|
80
|
+
|
|
81
|
+
## Architecture
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
85
|
+
│ Server │
|
|
86
|
+
│ ┌──────────────────┐ ┌────────────────────────────────┐ │
|
|
87
|
+
│ │ Observable │───►│ HTTP_Observable_Publisher │ │
|
|
88
|
+
│ │ obs((next,...)=>{│ │ • Sets text/event-stream │ │
|
|
89
|
+
│ │ next(progress) │ │ • Writes SSE format │ │
|
|
90
|
+
│ │ }) │ │ • Keeps connection open │ │
|
|
91
|
+
│ └──────────────────┘ └───────────────┬────────────────┘ │
|
|
92
|
+
└──────────────────────────────────────────┼──────────────────┘
|
|
93
|
+
│ HTTP
|
|
94
|
+
▼
|
|
95
|
+
┌──────────────────────────────────────────┴──────────────────┐
|
|
96
|
+
│ Browser │
|
|
97
|
+
│ ┌────────────────────────────────────────────────────────┐ │
|
|
98
|
+
│ │ EventSource('/api/progress-stream') │ │
|
|
99
|
+
│ │ .onmessage = (event) => { │ │
|
|
100
|
+
│ │ update_ui(JSON.parse(event.data)); │ │
|
|
101
|
+
│ │ } │ │
|
|
102
|
+
│ └────────────────────────────────────────────────────────┘ │
|
|
103
|
+
└─────────────────────────────────────────────────────────────┘
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Files
|
|
107
|
+
|
|
108
|
+
- `client.js` - Control with SSE consumer and reactive UI
|
|
109
|
+
- `server.js` - Server with observable publisher setup
|
|
110
|
+
- `README.md` - This documentation
|
|
111
|
+
|
|
112
|
+
## Future Direction
|
|
113
|
+
|
|
114
|
+
The strategic goal is to auto-detect observable returns in function publishers:
|
|
115
|
+
|
|
116
|
+
```javascript
|
|
117
|
+
// Future: This should "just work" with SSE transport
|
|
118
|
+
server.publish('/api/progress', () => {
|
|
119
|
+
return obs((next, complete, error) => {
|
|
120
|
+
// Long-running operation...
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
See `.github/agents/jsgui3-server.agent.md` for the full observable-first strategy.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check.js - Verify that all text renders properly in the Observable SSE Demo control
|
|
3
|
+
*
|
|
4
|
+
* This script instantiates the control server-side and checks that all expected
|
|
5
|
+
* text content is present in the rendered HTML output. No server required.
|
|
6
|
+
*
|
|
7
|
+
* Run with: node check.js
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const jsgui = require('./client');
|
|
11
|
+
const {Observable_Demo_UI} = jsgui.controls;
|
|
12
|
+
|
|
13
|
+
// Create a server-side page context for rendering
|
|
14
|
+
const Server_Page_Context = require('../../../page-context');
|
|
15
|
+
const context = new Server_Page_Context();
|
|
16
|
+
|
|
17
|
+
console.log('Observable SSE Demo - Text Rendering Check');
|
|
18
|
+
console.log('='.repeat(50));
|
|
19
|
+
console.log('');
|
|
20
|
+
|
|
21
|
+
// Instantiate the control (no spec.el, so compose() will run)
|
|
22
|
+
const demo_ui = new Observable_Demo_UI({
|
|
23
|
+
context: context
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Render to HTML
|
|
27
|
+
const html = demo_ui.all_html_render();
|
|
28
|
+
|
|
29
|
+
// Define all expected text content
|
|
30
|
+
const expected_texts = [
|
|
31
|
+
// Header
|
|
32
|
+
{ text: 'Observable SSE Demo - Real-Time Tick Stream', description: 'Page title (h2)' },
|
|
33
|
+
|
|
34
|
+
// Status section
|
|
35
|
+
{ text: 'Status: Not connected', description: 'Initial status text' },
|
|
36
|
+
|
|
37
|
+
// Tick display
|
|
38
|
+
{ text: '--', description: 'Initial tick count placeholder' },
|
|
39
|
+
{ text: 'Server Ticks', description: 'Tick label' },
|
|
40
|
+
|
|
41
|
+
// Event log label
|
|
42
|
+
{ text: 'Event Log (SSE messages):', description: 'Log section label' },
|
|
43
|
+
|
|
44
|
+
// Buttons
|
|
45
|
+
{ text: 'Connect to SSE', description: 'Connect button text' },
|
|
46
|
+
{ text: 'Disconnect', description: 'Disconnect button text' },
|
|
47
|
+
{ text: 'Clear Log', description: 'Clear Log button text' },
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
// Check each expected text
|
|
51
|
+
let pass_count = 0;
|
|
52
|
+
let fail_count = 0;
|
|
53
|
+
|
|
54
|
+
console.log('Checking rendered HTML for expected text content:');
|
|
55
|
+
console.log('');
|
|
56
|
+
|
|
57
|
+
for (const {text, description} of expected_texts) {
|
|
58
|
+
const found = html.includes(text);
|
|
59
|
+
const status = found ? '✓ PASS' : '✗ FAIL';
|
|
60
|
+
const color = found ? '\x1b[32m' : '\x1b[31m';
|
|
61
|
+
const reset = '\x1b[0m';
|
|
62
|
+
|
|
63
|
+
console.log(` ${color}${status}${reset} "${text}"`);
|
|
64
|
+
console.log(` └─ ${description}`);
|
|
65
|
+
|
|
66
|
+
if (found) {
|
|
67
|
+
pass_count++;
|
|
68
|
+
} else {
|
|
69
|
+
fail_count++;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.log('');
|
|
74
|
+
console.log('-'.repeat(50));
|
|
75
|
+
console.log(`Results: ${pass_count} passed, ${fail_count} failed`);
|
|
76
|
+
console.log('');
|
|
77
|
+
|
|
78
|
+
// Also check for important HTML structure
|
|
79
|
+
console.log('Additional structural checks:');
|
|
80
|
+
console.log('');
|
|
81
|
+
|
|
82
|
+
const structural_checks = [
|
|
83
|
+
{ pattern: 'id="status-label"', description: 'Status label has ID' },
|
|
84
|
+
{ pattern: 'id="tick-count"', description: 'Tick count has ID' },
|
|
85
|
+
{ pattern: 'id="event-log"', description: 'Event log has ID' },
|
|
86
|
+
{ pattern: 'id="connect-btn"', description: 'Connect button has ID' },
|
|
87
|
+
{ pattern: 'id="disconnect-btn"', description: 'Disconnect button has ID' },
|
|
88
|
+
{ pattern: 'id="clear-btn"', description: 'Clear button has ID' },
|
|
89
|
+
{ pattern: '<button', description: 'Button elements present' },
|
|
90
|
+
{ pattern: '<h2', description: 'H2 heading present' },
|
|
91
|
+
{ pattern: 'class="sse-container"', description: 'SSE container class' },
|
|
92
|
+
{ pattern: 'class="tick-display"', description: 'Tick display class' },
|
|
93
|
+
{ pattern: 'class="button-container"', description: 'Button container class' },
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
let struct_pass = 0;
|
|
97
|
+
let struct_fail = 0;
|
|
98
|
+
|
|
99
|
+
for (const {pattern, description} of structural_checks) {
|
|
100
|
+
const found = html.includes(pattern);
|
|
101
|
+
const status = found ? '✓ PASS' : '✗ FAIL';
|
|
102
|
+
const color = found ? '\x1b[32m' : '\x1b[31m';
|
|
103
|
+
const reset = '\x1b[0m';
|
|
104
|
+
|
|
105
|
+
console.log(` ${color}${status}${reset} ${pattern}`);
|
|
106
|
+
console.log(` └─ ${description}`);
|
|
107
|
+
|
|
108
|
+
if (found) {
|
|
109
|
+
struct_pass++;
|
|
110
|
+
} else {
|
|
111
|
+
struct_fail++;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.log('');
|
|
116
|
+
console.log('-'.repeat(50));
|
|
117
|
+
console.log(`Structural: ${struct_pass} passed, ${struct_fail} failed`);
|
|
118
|
+
console.log('');
|
|
119
|
+
|
|
120
|
+
// Final summary
|
|
121
|
+
const total_pass = pass_count + struct_pass;
|
|
122
|
+
const total_fail = fail_count + struct_fail;
|
|
123
|
+
const total = total_pass + total_fail;
|
|
124
|
+
|
|
125
|
+
console.log('='.repeat(50));
|
|
126
|
+
if (total_fail === 0) {
|
|
127
|
+
console.log('\x1b[32m✓ ALL CHECKS PASSED\x1b[0m');
|
|
128
|
+
console.log(` ${total_pass}/${total} checks successful`);
|
|
129
|
+
} else {
|
|
130
|
+
console.log('\x1b[31m✗ SOME CHECKS FAILED\x1b[0m');
|
|
131
|
+
console.log(` ${total_pass}/${total} checks passed, ${total_fail} failed`);
|
|
132
|
+
}
|
|
133
|
+
console.log('='.repeat(50));
|
|
134
|
+
|
|
135
|
+
// Optionally output the full HTML for debugging
|
|
136
|
+
if (process.argv.includes('--html') || process.argv.includes('-h')) {
|
|
137
|
+
console.log('');
|
|
138
|
+
console.log('Rendered HTML:');
|
|
139
|
+
console.log('-'.repeat(50));
|
|
140
|
+
console.log(html);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Exit with appropriate code
|
|
144
|
+
process.exit(total_fail > 0 ? 1 : 0);
|