zero-http 0.2.0 → 0.2.2

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 (43) hide show
  1. package/README.md +314 -115
  2. package/documentation/full-server.js +102 -5
  3. package/documentation/public/data/api.json +154 -33
  4. package/documentation/public/data/examples.json +35 -11
  5. package/documentation/public/data/options.json +14 -8
  6. package/documentation/public/index.html +138 -53
  7. package/documentation/public/scripts/data-sections.js +23 -4
  8. package/documentation/public/scripts/helpers.js +4 -4
  9. package/documentation/public/scripts/playground.js +204 -0
  10. package/documentation/public/scripts/ui.js +140 -8
  11. package/documentation/public/scripts/uploads.js +35 -20
  12. package/documentation/public/styles.css +46 -23
  13. package/documentation/public/vendor/icons/compress.svg +27 -0
  14. package/documentation/public/vendor/icons/https.svg +18 -0
  15. package/documentation/public/vendor/icons/logo.svg +24 -0
  16. package/documentation/public/vendor/icons/router.svg +27 -0
  17. package/documentation/public/vendor/icons/sse.svg +22 -0
  18. package/documentation/public/vendor/icons/websocket.svg +21 -0
  19. package/index.js +21 -4
  20. package/lib/app.js +156 -15
  21. package/lib/body/json.js +3 -0
  22. package/lib/body/multipart.js +2 -0
  23. package/lib/body/raw.js +3 -0
  24. package/lib/body/text.js +3 -0
  25. package/lib/body/urlencoded.js +3 -0
  26. package/lib/{fetch.js → fetch/index.js} +30 -1
  27. package/lib/http/index.js +9 -0
  28. package/lib/{request.js → http/request.js} +7 -1
  29. package/lib/{response.js → http/response.js} +70 -1
  30. package/lib/middleware/compress.js +194 -0
  31. package/lib/middleware/index.js +12 -0
  32. package/lib/router/index.js +278 -0
  33. package/lib/sse/index.js +8 -0
  34. package/lib/sse/stream.js +322 -0
  35. package/lib/ws/connection.js +440 -0
  36. package/lib/ws/handshake.js +122 -0
  37. package/lib/ws/index.js +12 -0
  38. package/package.json +1 -1
  39. package/lib/router.js +0 -87
  40. /package/lib/{cors.js → middleware/cors.js} +0 -0
  41. /package/lib/{logger.js → middleware/logger.js} +0 -0
  42. /package/lib/{rateLimit.js → middleware/rateLimit.js} +0 -0
  43. /package/lib/{static.js → middleware/static.js} +0 -0
@@ -6,9 +6,9 @@
6
6
  <meta name="viewport" content="width=device-width,initial-scale=1" />
7
7
  <title>zero-http — demo & reference</title>
8
8
  <meta name="description"
9
- content="zero-http is a zero-dependency, Express-like HTTP server for Node.js with built-in parsers, static serving, and upload handling." />
9
+ content="zero-http is a zero-dependency, Express-like HTTP/HTTPS server for Node.js with built-in WebSocket, Server-Sent Events, body parsers, response compression, static serving, and upload handling." />
10
10
  <meta name="keywords"
11
- content="nodejs,http,server,express,expressjs,middleware,body-parser,multipart,uploads,static files,fetch,http client" />
11
+ content="nodejs,http,https,server,express,expressjs,middleware,body-parser,multipart,uploads,static files,fetch,http client,websocket,sse,server-sent events,compression,router" />
12
12
  <meta name="author" content="Anthony Wiedman" />
13
13
 
14
14
  <link rel="stylesheet" href="/styles.css" />
@@ -16,11 +16,11 @@
16
16
  <link rel="stylesheet" href="/vendor/prism-toolbar.css" />
17
17
  <link rel="stylesheet" href="/prism-overrides.css" />
18
18
 
19
- <script src="/vendor/prism.min.js" async></script>
20
- <script src="/vendor/prism-json.min.js" async></script>
21
- <script src="/vendor/prism-javascript.min.js" async></script>
22
- <script src="/vendor/prism-copy-to-clipboard.min.js" async></script>
23
- <script src="/vendor/prism-toolbar.min.js" async></script>
19
+ <script src="/vendor/prism.min.js" defer></script>
20
+ <script src="/vendor/prism-json.min.js" defer></script>
21
+ <script src="/vendor/prism-javascript.min.js" defer></script>
22
+ <script src="/vendor/prism-copy-to-clipboard.min.js" defer></script>
23
+ <script src="/vendor/prism-toolbar.min.js" defer></script>
24
24
 
25
25
  <script src="/scripts/helpers.js" defer></script>
26
26
  <script src="/scripts/uploads.js" defer></script>
@@ -71,24 +71,30 @@
71
71
  <aside class="toc-sidebar" aria-label="Table of contents">
72
72
  <nav>
73
73
  <div class="toc-toolbar">
74
+ <div class="toc-search-wrap">
75
+ <svg class="toc-search-icon" width="13" height="13" viewBox="0 0 16 16" fill="none"><circle cx="6.5" cy="6.5" r="5.5" stroke="currentColor" stroke-width="2"/><path d="M11 11l4 4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
76
+ <input id="toc-search" class="toc-search" type="text" placeholder="Filter…" autocomplete="off" spellcheck="false" />
77
+ </div>
78
+ <button class="toc-tool-btn" id="toc-toggle-acc" title="Collapse all" aria-label="Collapse all sections">
79
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 4l6 4 6-4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M2 9l6 4 6-4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
80
+ </button>
74
81
  <button class="toc-tool-btn" id="toc-top-btn" title="Back to top" aria-label="Scroll to top">
75
82
  <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 13V3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><path d="M3 7l5-5 5 5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
76
83
  </button>
77
- <button class="toc-tool-btn" id="toc-toggle-acc" title="Expand all" aria-label="Expand all sections">
78
- <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 4l6 4 6-4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M2 9l6 4 6-4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
79
- </button>
80
84
  </div>
81
85
  <ul>
82
86
  <li><a href="#features">Features</a></li>
83
87
  <li><a href="#quickstart">Quickstart</a></li>
84
88
  <li><a href="#options">Options</a></li>
85
- <li><a href="#api-reference">API Reference</a></li>
86
- <li><a href="#simple-examples">Simple Examples</a></li>
87
- <li><a href="#playground">Playground</a>
89
+ <li class="toc-collapsible"><a href="#api-reference">API Reference</a></li>
90
+ <li class="toc-collapsible"><a href="#simple-examples">Simple Examples</a></li>
91
+ <li class="toc-collapsible"><a href="#playground">Playground</a>
88
92
  <ul class="toc-sub">
89
93
  <li class="toc-sub-item"><a href="#upload-testing">Upload multipart data</a></li>
90
94
  <li class="toc-sub-item"><a href="#parser-tests">Parser tests</a></li>
91
95
  <li class="toc-sub-item"><a href="#proxy-test">Proxy test</a></li>
96
+ <li class="toc-sub-item"><a href="#ws-chat">WebSocket chat</a></li>
97
+ <li class="toc-sub-item"><a href="#sse-viewer">SSE viewer</a></li>
92
98
  </ul>
93
99
  </li>
94
100
  </ul>
@@ -112,78 +118,107 @@
112
118
  <img class="icon-svg" src="/vendor/icons/zero.svg" alt="" aria-hidden="true"
113
119
  width="48" height="48" />
114
120
  </div>
115
- <h5>Zero-dependency</h5>
116
- <p>Small, Express-like API without external deps.</p>
121
+ <div class="feature-text"><h5>Zero-dependency</h5>
122
+ <p>Small, Express-like API without external deps.</p></div>
117
123
  </div>
118
124
  <div class="feature-card">
119
125
  <div class="feature-icon">
120
126
  <img class="icon-svg" src="/vendor/icons/plug.svg" alt="" aria-hidden="true"
121
127
  width="48" height="48" />
122
128
  </div>
123
- <h5>Pluggable parsers</h5>
124
- <p>json(), urlencoded(), text(), raw() built-in.</p>
129
+ <div class="feature-text"><h5>Pluggable parsers</h5>
130
+ <p>json(), urlencoded(), text(), raw() built-in.</p></div>
125
131
  </div>
126
132
  <div class="feature-card">
127
133
  <div class="feature-icon">
128
134
  <img class="icon-svg" src="/vendor/icons/stream.svg" alt="" aria-hidden="true"
129
135
  width="48" height="48" />
130
136
  </div>
131
- <h5>Streaming uploads</h5>
132
- <p>Multipart streaming to disk for large files.</p>
137
+ <div class="feature-text"><h5>Streaming uploads</h5>
138
+ <p>Multipart streaming to disk for large files.</p></div>
133
139
  </div>
134
140
  <div class="feature-card">
135
141
  <div class="feature-icon">
136
142
  <img class="icon-svg" src="/vendor/icons/static.svg" alt="" aria-hidden="true"
137
143
  width="48" height="48" />
138
144
  </div>
139
- <h5>Static serving</h5>
140
- <p>Correct Content-Type handling and caching options.</p>
145
+ <div class="feature-text"><h5>Static serving</h5>
146
+ <p>Content-Type handling and cache options.</p></div>
141
147
  </div>
142
148
  <div class="feature-card">
143
149
  <div class="feature-icon">
144
150
  <img class="icon-svg" src="/vendor/icons/fetch.svg" alt="" aria-hidden="true"
145
151
  width="48" height="48" />
146
152
  </div>
147
- <h5>Built-in fetch</h5>
148
- <p>Small server-side fetch replacement for HTTP requests.</p>
153
+ <div class="feature-text"><h5>Built-in fetch</h5>
154
+ <p>Server-side fetch with TLS passthrough.</p></div>
149
155
  </div>
150
- </div>
151
- </div>
152
- <div id="tab-behavior" class="tab-panel">
153
- <div class="behavior-list">
154
- <div class="behavior-step">
155
- <div class="step-num">1</div>
156
- <div class="step-body">
157
- <h5>Registration order</h5>
158
- <p>Routing and middleware run in the order they're registered—add parsers before
159
- handlers.</p>
156
+ <div class="feature-card">
157
+ <div class="feature-icon">
158
+ <img class="icon-svg" src="/vendor/icons/websocket.svg" alt="" aria-hidden="true"
159
+ width="48" height="48" />
160
160
  </div>
161
+ <div class="feature-text"><h5>WebSocket</h5>
162
+ <p>RFC 6455 with ping/pong &amp; sub-protocols.</p></div>
161
163
  </div>
162
- <div class="behavior-step">
163
- <div class="step-num">2</div>
164
- <div class="step-body">
165
- <h5>Body parsing</h5>
166
- <p>Body parsers populate <code>req.body</code>; multipart uploads expose
167
- <code>req.body.files</code> and <code>req.body.fields</code>.
168
- </p>
164
+ <div class="feature-card">
165
+ <div class="feature-icon">
166
+ <img class="icon-svg" src="/vendor/icons/sse.svg" alt="" aria-hidden="true"
167
+ width="48" height="48" />
169
168
  </div>
169
+ <div class="feature-text"><h5>Server-Sent Events</h5>
170
+ <p>Auto-IDs, named events, keep-alive.</p></div>
170
171
  </div>
171
- <div class="behavior-step">
172
- <div class="step-num">3</div>
173
- <div class="step-body">
174
- <h5>Handler model</h5>
175
- <p>Handlers receive <code>req</code> and <code>res</code>. Use
176
- <code>res.json()</code> or <code>res.send()</code> to respond.
177
- </p>
172
+ <div class="feature-card">
173
+ <div class="feature-icon">
174
+ <img class="icon-svg" src="/vendor/icons/compress.svg" alt="" aria-hidden="true"
175
+ width="48" height="48" />
178
176
  </div>
177
+ <div class="feature-text"><h5>Compression</h5>
178
+ <p>Gzip/deflate with threshold &amp; filter.</p></div>
179
179
  </div>
180
- <div class="behavior-step">
181
- <div class="step-num">4</div>
182
- <div class="step-body">
183
- <h5>Static middleware</h5>
184
- <p>Serves files with Content-Type derived from extension and supports caching
185
- options.</p>
180
+ <div class="feature-card">
181
+ <div class="feature-icon">
182
+ <img class="icon-svg" src="/vendor/icons/https.svg" alt="" aria-hidden="true"
183
+ width="48" height="48" />
186
184
  </div>
185
+ <div class="feature-text"><h5>HTTPS &amp; TLS</h5>
186
+ <p>Native HTTPS, req.secure, secure routing.</p></div>
187
+ </div>
188
+ <div class="feature-card">
189
+ <div class="feature-icon">
190
+ <img class="icon-svg" src="/vendor/icons/router.svg" alt="" aria-hidden="true"
191
+ width="48" height="48" />
192
+ </div>
193
+ <div class="feature-text"><h5>Modular Router</h5>
194
+ <p>Sub-apps, chaining, introspection.</p></div>
195
+ </div>
196
+ </div>
197
+ </div>
198
+ <div id="tab-behavior" class="tab-panel">
199
+ <div class="pipeline">
200
+ <div class="pipe-stage">
201
+ <div class="pipe-icon"><svg viewBox="0 0 24 24"><polygon points="12 2 2 7 12 12 22 7"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></svg></div>
202
+ <h5>Registration order</h5>
203
+ <p>Middleware runs in the order registered—add parsers before handlers.</p>
204
+ </div>
205
+ <div class="pipe-arrow" aria-hidden="true" style="--i:0"><svg viewBox="0 0 24 24"><polyline points="9 6 15 12 9 18"/></svg></div>
206
+ <div class="pipe-stage">
207
+ <div class="pipe-icon"><svg viewBox="0 0 24 24"><path d="M9 3H7a2 2 0 0 0-2 2v4a2 2 0 0 1-2 2 2 2 0 0 1 2 2v4a2 2 0 0 0 2 2h2"/><path d="M15 3h2a2 2 0 0 1 2 2v4a2 2 0 0 0 2 2 2 2 0 0 0-2 2v4a2 2 0 0 1-2 2h-2"/></svg></div>
208
+ <h5>Body parsing</h5>
209
+ <p>Populates <code>req.body</code>; multipart exposes <code>req.body.files</code> and <code>req.body.fields</code>.</p>
210
+ </div>
211
+ <div class="pipe-arrow" aria-hidden="true" style="--i:1"><svg viewBox="0 0 24 24"><polyline points="9 6 15 12 9 18"/></svg></div>
212
+ <div class="pipe-stage">
213
+ <div class="pipe-icon"><svg viewBox="0 0 24 24"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg></div>
214
+ <h5>Handler model</h5>
215
+ <p>Handlers receive <code>req</code> + <code>res</code>. Respond with <code>res.json()</code> or <code>res.send()</code>.</p>
216
+ </div>
217
+ <div class="pipe-arrow" aria-hidden="true" style="--i:2"><svg viewBox="0 0 24 24"><polyline points="9 6 15 12 9 18"/></svg></div>
218
+ <div class="pipe-stage">
219
+ <div class="pipe-icon"><svg viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg></div>
220
+ <h5>Static middleware</h5>
221
+ <p>Serves files with Content-Type from extension and supports caching options.</p>
187
222
  </div>
188
223
  </div>
189
224
  </div>
@@ -401,6 +436,56 @@ node test/test.js
401
436
  </div>
402
437
  </div>
403
438
 
439
+ <div class="card play-card constrained">
440
+ <div class="card-head">
441
+ <h3 id="ws-chat">WebSocket Chat</h3>
442
+ <div class="small muted">Connect to the built-in WebSocket server and send messages in real time.</div>
443
+ </div>
444
+ <div class="card-body">
445
+ <div class="playgrid" style="grid-template-columns:1fr">
446
+ <div class="play" id="wsChat">
447
+ <div style="display:flex;gap:8px;align-items:end;margin-bottom:8px">
448
+ <label style="flex:1">Name
449
+ <input id="wsName" class="textInput" placeholder="anon" value="" />
450
+ </label>
451
+ <button id="wsConnectBtn" class="btn primary" type="button">Connect</button>
452
+ <button id="wsDisconnectBtn" class="btn warn" type="button" disabled>Disconnect</button>
453
+ </div>
454
+ <div id="wsMessages" class="result mono" style="min-height:120px;max-height:260px;overflow-y:auto;margin-bottom:8px;white-space:pre-wrap"></div>
455
+ <div style="display:flex;gap:8px">
456
+ <input id="wsMsgInput" class="textInput" placeholder="Type a message…" disabled style="flex:1" />
457
+ <button id="wsSendBtn" class="btn" type="button" disabled>Send</button>
458
+ </div>
459
+ </div>
460
+ </div>
461
+ </div>
462
+ </div>
463
+
464
+ <div class="card play-card constrained">
465
+ <div class="card-head">
466
+ <h3 id="sse-viewer">SSE Event Viewer</h3>
467
+ <div class="small muted">Subscribe to the Server-Sent Events endpoint and view incoming events live. Use the broadcast form to push a custom event to all subscribers.</div>
468
+ </div>
469
+ <div class="card-body">
470
+ <div class="playgrid" style="grid-template-columns:1fr">
471
+ <div class="play" id="sseViewer">
472
+ <div style="display:flex;gap:8px;align-items:end;margin-bottom:8px">
473
+ <button id="sseConnectBtn" class="btn primary" type="button">Connect</button>
474
+ <button id="sseDisconnectBtn" class="btn warn" type="button" disabled>Disconnect</button>
475
+ <span id="sseStatus" class="small muted" style="margin-left:8px">Disconnected</span>
476
+ </div>
477
+ <div id="sseMessages" class="result mono" style="min-height:120px;max-height:260px;overflow-y:auto;margin-bottom:8px;white-space:pre-wrap"></div>
478
+ <div style="display:flex;gap:8px;align-items:end">
479
+ <label style="flex:1">Broadcast message (JSON)
480
+ <textarea id="sseBroadcastInput" class="textInput" rows="2">{"hello":"world"}</textarea>
481
+ </label>
482
+ <button id="sseBroadcastBtn" class="btn" type="button">Broadcast</button>
483
+ </div>
484
+ </div>
485
+ </div>
486
+ </div>
487
+ </div>
488
+
404
489
  <div class="card footnote" style="max-width:1300px;margin:18px auto;">
405
490
  <p class="muted">This demo doubles as a readable API reference and playground — try uploading files,
406
491
  inspect the returned JSON, and use the playground to test parsers.</p>
@@ -12,7 +12,7 @@
12
12
  * highlightAllPre)
13
13
  */
14
14
 
15
- /* ── TOC Helpers ───────────────────────────────────────────────────────────── */
15
+ /* -- TOC Helpers ------------------------------------------------------------- */
16
16
 
17
17
  /**
18
18
  * Find a top-level `<li>` in the sidebar TOC whose link matches the given
@@ -36,6 +36,25 @@ function populateTocSub(href, items)
36
36
  const existing = parentLi.querySelector('ul.toc-sub');
37
37
  if (existing) existing.remove();
38
38
 
39
+ /* Ensure parent has collapsible behaviour */
40
+ if (!parentLi.classList.contains('toc-collapsible'))
41
+ {
42
+ parentLi.classList.add('toc-collapsible', 'toc-collapsed');
43
+ parentLi.style.paddingLeft = '20px';
44
+
45
+ const toggle = document.createElement('button');
46
+ toggle.className = 'toc-collapse-btn';
47
+ toggle.setAttribute('aria-label', 'Toggle section');
48
+ toggle.innerHTML = '<svg width="10" height="10" viewBox="0 0 10 10" fill="none"><path d="M3 1l4 4-4 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>';
49
+ toggle.addEventListener('click', (e) =>
50
+ {
51
+ e.preventDefault();
52
+ e.stopPropagation();
53
+ parentLi.classList.toggle('toc-collapsed');
54
+ });
55
+ parentLi.insertBefore(toggle, parentLi.firstChild);
56
+ }
57
+
39
58
  const sub = document.createElement('ul');
40
59
  sub.className = 'toc-sub';
41
60
 
@@ -56,7 +75,7 @@ function populateTocSub(href, items)
56
75
  parentLi.appendChild(sub);
57
76
  }
58
77
 
59
- /* ── API Reference ─────────────────────────────────────────────────────────── */
78
+ /* -- API Reference ----------------------------------------------------------- */
60
79
 
61
80
  /**
62
81
  * Render a list of API items into the `#api-items` container and Prism-highlight
@@ -196,7 +215,7 @@ async function loadApiReference()
196
215
  } catch (e) { }
197
216
  }
198
217
 
199
- /* ── Options Table ─────────────────────────────────────────────────────────── */
218
+ /* -- Options Table ----------------------------------------------------------- */
200
219
 
201
220
  /**
202
221
  * Fetch the options JSON and render a `<table>` into `#options-items`.
@@ -236,7 +255,7 @@ async function loadOptions()
236
255
  } catch (e) { console.error('loadOptions error', e); }
237
256
  }
238
257
 
239
- /* ── Code Examples ─────────────────────────────────────────────────────────── */
258
+ /* -- Code Examples ----------------------------------------------------------- */
240
259
 
241
260
  /**
242
261
  * Fetch the examples JSON, render each as a collapsible accordion, and
@@ -4,7 +4,7 @@
4
4
  * documentation scripts. Loaded first so every other module can rely on these.
5
5
  */
6
6
 
7
- /* ── DOM Selectors ─────────────────────────────────────────────────────────── */
7
+ /* -- DOM Selectors ----------------------------------------------------------- */
8
8
 
9
9
  /**
10
10
  * Query a single element by CSS selector.
@@ -34,7 +34,7 @@ function on(el, evt, cb)
34
34
  el.addEventListener(evt, cb);
35
35
  }
36
36
 
37
- /* ── String Utilities ──────────────────────────────────────────────────────── */
37
+ /* -- String Utilities -------------------------------------------------------- */
38
38
 
39
39
  /**
40
40
  * Escape HTML special characters for safe insertion into the DOM.
@@ -76,7 +76,7 @@ function formatBytes(n)
76
76
  return (n / Math.pow(k, i)).toFixed(i ? 1 : 0) + ' ' + sizes[i];
77
77
  }
78
78
 
79
- /* ── JSON / Code Rendering ─────────────────────────────────────────────────── */
79
+ /* -- JSON / Code Rendering --------------------------------------------------- */
80
80
 
81
81
  /**
82
82
  * Build a highlighted `<pre>` block containing pretty-printed JSON.
@@ -100,7 +100,7 @@ function showJsonResult(container, obj)
100
100
  try { highlightAllPre(); } catch (e) { }
101
101
  }
102
102
 
103
- /* ── Prism / Code-Block Helpers ────────────────────────────────────────────── */
103
+ /* -- Prism / Code-Block Helpers ---------------------------------------------- */
104
104
 
105
105
  /**
106
106
  * Trigger Prism syntax highlighting on all `<pre class="code">` blocks, or
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * playground.js
3
3
  * Echo playground forms — JSON, URL-encoded, and plain-text body parsers.
4
+ * WebSocket chat client and SSE event viewer.
4
5
  *
5
6
  * Depends on: helpers.js (provides $, on, escapeHtml, showJsonResult,
6
7
  * highlightAllPre)
@@ -68,4 +69,207 @@ function initPlayground()
68
69
  playResult.innerHTML = `<pre class="code"><code>${escapeHtml(text)}</code></pre>`;
69
70
  try { highlightAllPre(); } catch (e) { }
70
71
  });
72
+
73
+ /* --- WebSocket Chat --- */
74
+ initWsChat();
75
+
76
+ /* --- SSE Viewer --- */
77
+ initSseViewer();
78
+ }
79
+
80
+ /* ------------------------------------------------------------------ */
81
+ /* WebSocket Chat */
82
+ /* ------------------------------------------------------------------ */
83
+ function initWsChat()
84
+ {
85
+ let ws = null;
86
+ const connectBtn = $('#wsConnectBtn');
87
+ const disconnectBtn = $('#wsDisconnectBtn');
88
+ const nameInput = $('#wsName');
89
+ const msgInput = $('#wsMsgInput');
90
+ const sendBtn = $('#wsSendBtn');
91
+ const messages = $('#wsMessages');
92
+
93
+ if (!connectBtn) return; // guard if elements not in DOM
94
+
95
+ function appendMsg(html)
96
+ {
97
+ const div = document.createElement('div');
98
+ div.innerHTML = html;
99
+ messages.appendChild(div);
100
+ messages.scrollTop = messages.scrollHeight;
101
+ }
102
+
103
+ function setConnected(connected)
104
+ {
105
+ connectBtn.disabled = connected;
106
+ disconnectBtn.disabled = !connected;
107
+ msgInput.disabled = !connected;
108
+ sendBtn.disabled = !connected;
109
+ nameInput.disabled = connected;
110
+ }
111
+
112
+ on(connectBtn, 'click', () =>
113
+ {
114
+ const name = encodeURIComponent(nameInput.value || 'anon');
115
+ const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
116
+ // Use the direct HTTPS host+port when behind a reverse proxy that
117
+ // doesn't forward WebSocket upgrades.
118
+ const wsHost = location.port ? location.host : (location.hostname + ':7273');
119
+ ws = new WebSocket(`${proto}//${wsHost}/ws/chat?name=${name}`);
120
+
121
+ ws.onopen = () =>
122
+ {
123
+ setConnected(true);
124
+ appendMsg('<span style="color:#6f6">● Connected</span>');
125
+ };
126
+
127
+ ws.onmessage = (e) =>
128
+ {
129
+ try
130
+ {
131
+ const msg = JSON.parse(e.data);
132
+ if (msg.type === 'system')
133
+ {
134
+ appendMsg(`<span style="color:#aaa">» ${escapeHtml(msg.text)}</span>`);
135
+ } else
136
+ {
137
+ appendMsg(`<strong>${escapeHtml(msg.name)}</strong>: ${escapeHtml(msg.text)}`);
138
+ }
139
+ } catch (_)
140
+ {
141
+ appendMsg(escapeHtml(e.data));
142
+ }
143
+ };
144
+
145
+ ws.onclose = () =>
146
+ {
147
+ setConnected(false);
148
+ appendMsg('<span style="color:#f66">● Disconnected</span>');
149
+ ws = null;
150
+ };
151
+
152
+ ws.onerror = () =>
153
+ {
154
+ appendMsg('<span style="color:#f66">● Connection error</span>');
155
+ };
156
+ });
157
+
158
+ on(disconnectBtn, 'click', () =>
159
+ {
160
+ if (ws) ws.close();
161
+ });
162
+
163
+ on(sendBtn, 'click', () =>
164
+ {
165
+ if (ws && ws.readyState === WebSocket.OPEN && msgInput.value.trim())
166
+ {
167
+ ws.send(msgInput.value.trim());
168
+ msgInput.value = '';
169
+ }
170
+ });
171
+
172
+ on(msgInput, 'keydown', (e) =>
173
+ {
174
+ if (e.key === 'Enter')
175
+ {
176
+ e.preventDefault();
177
+ sendBtn.click();
178
+ }
179
+ });
180
+ }
181
+
182
+ /* ------------------------------------------------------------------ */
183
+ /* SSE Event Viewer */
184
+ /* ------------------------------------------------------------------ */
185
+ function initSseViewer()
186
+ {
187
+ let es = null;
188
+ const connectBtn = $('#sseConnectBtn');
189
+ const disconnectBtn = $('#sseDisconnectBtn');
190
+ const status = $('#sseStatus');
191
+ const messages = $('#sseMessages');
192
+ const broadcastBtn = $('#sseBroadcastBtn');
193
+ const broadcastInput = $('#sseBroadcastInput');
194
+
195
+ if (!connectBtn) return; // guard
196
+
197
+ function appendMsg(html)
198
+ {
199
+ const div = document.createElement('div');
200
+ div.innerHTML = html;
201
+ messages.appendChild(div);
202
+ messages.scrollTop = messages.scrollHeight;
203
+ }
204
+
205
+ function setConnected(connected)
206
+ {
207
+ connectBtn.disabled = connected;
208
+ disconnectBtn.disabled = !connected;
209
+ status.textContent = connected ? 'Connected' : 'Disconnected';
210
+ status.style.color = connected ? '#6f6' : '';
211
+ }
212
+
213
+ on(connectBtn, 'click', () =>
214
+ {
215
+ es = new EventSource('/sse/events');
216
+
217
+ es.onopen = () =>
218
+ {
219
+ setConnected(true);
220
+ appendMsg('<span style="color:#6f6">● SSE connected</span>');
221
+ };
222
+
223
+ es.onmessage = (e) =>
224
+ {
225
+ const ts = new Date().toLocaleTimeString();
226
+ appendMsg(`<span style="color:#aaa">[${ts}]</span> ${escapeHtml(e.data)}`);
227
+ };
228
+
229
+ es.addEventListener('broadcast', (e) =>
230
+ {
231
+ const ts = new Date().toLocaleTimeString();
232
+ appendMsg(`<span style="color:#ff0">[${ts} broadcast]</span> ${escapeHtml(e.data)}`);
233
+ });
234
+
235
+ es.onerror = () =>
236
+ {
237
+ if (es.readyState === EventSource.CLOSED)
238
+ {
239
+ setConnected(false);
240
+ appendMsg('<span style="color:#f66">● SSE closed</span>');
241
+ es = null;
242
+ } else
243
+ {
244
+ appendMsg('<span style="color:#fa0">● SSE reconnecting…</span>');
245
+ }
246
+ };
247
+ });
248
+
249
+ on(disconnectBtn, 'click', () =>
250
+ {
251
+ if (es) { es.close(); es = null; }
252
+ setConnected(false);
253
+ appendMsg('<span style="color:#f66">● Disconnected</span>');
254
+ });
255
+
256
+ on(broadcastBtn, 'click', async () =>
257
+ {
258
+ const raw = broadcastInput.value || '{}';
259
+ let body;
260
+ try { body = JSON.parse(raw); }
261
+ catch (err)
262
+ {
263
+ appendMsg(`<span style="color:#f66">Invalid JSON: ${escapeHtml(err.message)}</span>`);
264
+ return;
265
+ }
266
+
267
+ const r = await fetch('/sse/broadcast', {
268
+ method: 'POST',
269
+ headers: { 'Content-Type': 'application/json' },
270
+ body: JSON.stringify(body),
271
+ });
272
+ const j = await r.json();
273
+ appendMsg(`<span style="color:#aaa">» Broadcast sent to ${j.sent} client(s)</span>`);
274
+ });
71
275
  }