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.
- package/README.md +314 -115
- package/documentation/full-server.js +102 -5
- package/documentation/public/data/api.json +154 -33
- package/documentation/public/data/examples.json +35 -11
- package/documentation/public/data/options.json +14 -8
- package/documentation/public/index.html +138 -53
- package/documentation/public/scripts/data-sections.js +23 -4
- package/documentation/public/scripts/helpers.js +4 -4
- package/documentation/public/scripts/playground.js +204 -0
- package/documentation/public/scripts/ui.js +140 -8
- package/documentation/public/scripts/uploads.js +35 -20
- package/documentation/public/styles.css +46 -23
- package/documentation/public/vendor/icons/compress.svg +27 -0
- package/documentation/public/vendor/icons/https.svg +18 -0
- package/documentation/public/vendor/icons/logo.svg +24 -0
- package/documentation/public/vendor/icons/router.svg +27 -0
- package/documentation/public/vendor/icons/sse.svg +22 -0
- package/documentation/public/vendor/icons/websocket.svg +21 -0
- package/index.js +21 -4
- package/lib/app.js +156 -15
- package/lib/body/json.js +3 -0
- package/lib/body/multipart.js +2 -0
- package/lib/body/raw.js +3 -0
- package/lib/body/text.js +3 -0
- package/lib/body/urlencoded.js +3 -0
- package/lib/{fetch.js → fetch/index.js} +30 -1
- package/lib/http/index.js +9 -0
- package/lib/{request.js → http/request.js} +7 -1
- package/lib/{response.js → http/response.js} +70 -1
- package/lib/middleware/compress.js +194 -0
- package/lib/middleware/index.js +12 -0
- package/lib/router/index.js +278 -0
- package/lib/sse/index.js +8 -0
- package/lib/sse/stream.js +322 -0
- package/lib/ws/connection.js +440 -0
- package/lib/ws/handshake.js +122 -0
- package/lib/ws/index.js +12 -0
- package/package.json +1 -1
- package/lib/router.js +0 -87
- /package/lib/{cors.js → middleware/cors.js} +0 -0
- /package/lib/{logger.js → middleware/logger.js} +0 -0
- /package/lib/{rateLimit.js → middleware/rateLimit.js} +0 -0
- /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"
|
|
20
|
-
<script src="/vendor/prism-json.min.js"
|
|
21
|
-
<script src="/vendor/prism-javascript.min.js"
|
|
22
|
-
<script src="/vendor/prism-copy-to-clipboard.min.js"
|
|
23
|
-
<script src="/vendor/prism-toolbar.min.js"
|
|
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>
|
|
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>
|
|
153
|
+
<div class="feature-text"><h5>Built-in fetch</h5>
|
|
154
|
+
<p>Server-side fetch with TLS passthrough.</p></div>
|
|
149
155
|
</div>
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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 & sub-protocols.</p></div>
|
|
161
163
|
</div>
|
|
162
|
-
<div class="
|
|
163
|
-
<div class="
|
|
164
|
-
|
|
165
|
-
|
|
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="
|
|
172
|
-
<div class="
|
|
173
|
-
|
|
174
|
-
|
|
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 & filter.</p></div>
|
|
179
179
|
</div>
|
|
180
|
-
<div class="
|
|
181
|
-
<div class="
|
|
182
|
-
|
|
183
|
-
|
|
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 & 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
|
-
/*
|
|
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
|
-
/*
|
|
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
|
-
/*
|
|
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
|
-
/*
|
|
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
|
-
/*
|
|
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
|
-
/*
|
|
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
|
-
/*
|
|
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
|
-
/*
|
|
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
|
}
|