socketspec 0.1.0__tar.gz → 0.1.2__tar.gz
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.
- {socketspec-0.1.0 → socketspec-0.1.2}/PKG-INFO +3 -3
- {socketspec-0.1.0 → socketspec-0.1.2}/README.md +1 -1
- {socketspec-0.1.0 → socketspec-0.1.2}/pyproject.toml +2 -2
- socketspec-0.1.2/src/socketspec/docs/ui/index.html +36 -0
- socketspec-0.1.2/src/socketspec/docs/ui/main.js +610 -0
- socketspec-0.1.2/src/socketspec/docs/ui/style.css +280 -0
- socketspec-0.1.0/src/socketspec/docs/ui/index.html +0 -72
- socketspec-0.1.0/src/socketspec/docs/ui/main.js +0 -557
- socketspec-0.1.0/src/socketspec/docs/ui/style.css +0 -699
- {socketspec-0.1.0 → socketspec-0.1.2}/.gitignore +0 -0
- {socketspec-0.1.0 → socketspec-0.1.2}/AUTHORS +0 -0
- {socketspec-0.1.0 → socketspec-0.1.2}/LICENSE +0 -0
- {socketspec-0.1.0 → socketspec-0.1.2}/NOTICE +0 -0
- {socketspec-0.1.0 → socketspec-0.1.2}/src/socketspec/docs/ui/__init__.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: socketspec
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: FastAPI-style WebSocket framework with built-in docs and testing
|
|
5
5
|
Project-URL: Homepage, https://github.com/ByteCraftByLaiba/socketspec
|
|
6
|
-
Project-URL: Documentation, https://socketspec
|
|
6
|
+
Project-URL: Documentation, https://ByteCraftByLaiba.github.io/socketspec/
|
|
7
7
|
Project-URL: Repository, https://github.com/ByteCraftByLaiba/socketspec
|
|
8
8
|
Project-URL: Issues, https://github.com/ByteCraftByLaiba/socketspec/issues
|
|
9
9
|
Project-URL: Changelog, https://github.com/ByteCraftByLaiba/socketspec/blob/main/CHANGELOG.md
|
|
@@ -181,7 +181,7 @@ async def test_send_message():
|
|
|
181
181
|
|
|
182
182
|
## Links
|
|
183
183
|
|
|
184
|
-
- [Documentation](https://socketspec
|
|
184
|
+
- [Documentation](https://ByteCraftByLaiba.github.io/socketspec/)
|
|
185
185
|
- [GitHub](https://github.com/ByteCraftByLaiba/socketspec)
|
|
186
186
|
- [PyPI](https://pypi.org/project/socketspec/)
|
|
187
187
|
- [Changelog](CHANGELOG.md)
|
|
@@ -123,7 +123,7 @@ async def test_send_message():
|
|
|
123
123
|
|
|
124
124
|
## Links
|
|
125
125
|
|
|
126
|
-
- [Documentation](https://socketspec
|
|
126
|
+
- [Documentation](https://ByteCraftByLaiba.github.io/socketspec/)
|
|
127
127
|
- [GitHub](https://github.com/ByteCraftByLaiba/socketspec)
|
|
128
128
|
- [PyPI](https://pypi.org/project/socketspec/)
|
|
129
129
|
- [Changelog](CHANGELOG.md)
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "socketspec"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.2"
|
|
8
8
|
description = "FastAPI-style WebSocket framework with built-in docs and testing"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "Apache-2.0" }
|
|
@@ -53,7 +53,7 @@ dev = [
|
|
|
53
53
|
|
|
54
54
|
[project.urls]
|
|
55
55
|
Homepage = "https://github.com/ByteCraftByLaiba/socketspec"
|
|
56
|
-
Documentation = "https://socketspec
|
|
56
|
+
Documentation = "https://ByteCraftByLaiba.github.io/socketspec/"
|
|
57
57
|
Repository = "https://github.com/ByteCraftByLaiba/socketspec"
|
|
58
58
|
Issues = "https://github.com/ByteCraftByLaiba/socketspec/issues"
|
|
59
59
|
Changelog = "https://github.com/ByteCraftByLaiba/socketspec/blob/main/CHANGELOG.md"
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>SocketSpec Docs</title>
|
|
7
|
+
<link rel="stylesheet"
|
|
8
|
+
href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css" />
|
|
9
|
+
<style>{{INLINE_CSS}}</style>
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<div id="socketspec-ui"></div>
|
|
13
|
+
|
|
14
|
+
<dialog id="auth-modal">
|
|
15
|
+
<form method="dialog">
|
|
16
|
+
<h2>Authorize</h2>
|
|
17
|
+
<label>
|
|
18
|
+
Bearer Token
|
|
19
|
+
<input id="auth-token" type="password" placeholder="eyJhbGci..." />
|
|
20
|
+
</label>
|
|
21
|
+
<label>
|
|
22
|
+
API Key
|
|
23
|
+
<input id="auth-api-key" type="text" placeholder="x-api-key value" />
|
|
24
|
+
</label>
|
|
25
|
+
<div class="auth-btn-wrapper">
|
|
26
|
+
<button value="cancel" class="btn cancel">Close</button>
|
|
27
|
+
<button id="auth-save" value="default" class="btn authorize">
|
|
28
|
+
Authorize
|
|
29
|
+
</button>
|
|
30
|
+
</div>
|
|
31
|
+
</form>
|
|
32
|
+
</dialog>
|
|
33
|
+
|
|
34
|
+
<script>{{INLINE_JS}}</script>
|
|
35
|
+
</body>
|
|
36
|
+
</html>
|
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SocketSpec Docs UI - main.js
|
|
3
|
+
* Renders socket events using Swagger UI HTML class names for pixel-perfect
|
|
4
|
+
* visual parity. No emojis anywhere in this file.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/* --- Constants --- */
|
|
8
|
+
const DOCS_BASE = window.location.pathname.replace(/\/+$/, '');
|
|
9
|
+
|
|
10
|
+
/* --- State --- */
|
|
11
|
+
let schema = { version: '', events: [] };
|
|
12
|
+
let socket = null;
|
|
13
|
+
let authToken = '';
|
|
14
|
+
let authApiKey = '';
|
|
15
|
+
let connId = null;
|
|
16
|
+
let isConnected = false;
|
|
17
|
+
|
|
18
|
+
/** Maps event name to { editor, responseBlock, tryItActive } */
|
|
19
|
+
const tryItContexts = {};
|
|
20
|
+
/** Maps trigger event name to Set of server-sent event names */
|
|
21
|
+
const responseEventIndex = {};
|
|
22
|
+
|
|
23
|
+
/* --- DOM refs (created dynamically) --- */
|
|
24
|
+
let statusDotEl = null;
|
|
25
|
+
let statusTextEl = null;
|
|
26
|
+
let statusConnIdEl = null;
|
|
27
|
+
let statusHintEl = null;
|
|
28
|
+
let connectBtn = null;
|
|
29
|
+
let wsUrlInput = null;
|
|
30
|
+
let logOutputEl = null;
|
|
31
|
+
let logFilterEl = null;
|
|
32
|
+
const authModal = document.getElementById('auth-modal');
|
|
33
|
+
|
|
34
|
+
/* --- Utility: build HH:MM:SS timestamp --- */
|
|
35
|
+
function timeStamp() {
|
|
36
|
+
return new Date().toTimeString().slice(0, 8);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* --- Logging --- */
|
|
40
|
+
function logMessage(direction, data) {
|
|
41
|
+
if (!logOutputEl) return;
|
|
42
|
+
const text = typeof data === 'string' ? data : JSON.stringify(data);
|
|
43
|
+
const line = `[${timeStamp()}] ${direction} ${text}`;
|
|
44
|
+
const filter = logFilterEl ? logFilterEl.value.toLowerCase() : '';
|
|
45
|
+
if (filter && !line.toLowerCase().includes(filter)) return;
|
|
46
|
+
|
|
47
|
+
const div = document.createElement('div');
|
|
48
|
+
div.className = `log-${direction.toLowerCase()}`;
|
|
49
|
+
div.textContent = line;
|
|
50
|
+
logOutputEl.appendChild(div);
|
|
51
|
+
logOutputEl.scrollTop = logOutputEl.scrollHeight;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* --- Schema helpers --- */
|
|
55
|
+
function exampleFromSchema(s) {
|
|
56
|
+
if (!s || !s.properties) return {};
|
|
57
|
+
const out = {};
|
|
58
|
+
for (const [k, v] of Object.entries(s.properties)) {
|
|
59
|
+
const t = v.type;
|
|
60
|
+
if (t === 'string') out[k] = v.examples?.[0] ?? '';
|
|
61
|
+
else if (t === 'integer') out[k] = 0;
|
|
62
|
+
else if (t === 'number') out[k] = 0.0;
|
|
63
|
+
else if (t === 'boolean') out[k] = false;
|
|
64
|
+
else if (t === 'array') out[k] = [];
|
|
65
|
+
else if (t === 'object') out[k] = {};
|
|
66
|
+
else out[k] = null;
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function buildSchemaTable(s) {
|
|
72
|
+
if (!s || !s.properties) {
|
|
73
|
+
const p = document.createElement('p');
|
|
74
|
+
p.style.cssText = 'color:#999;font-style:italic;margin:4px 0';
|
|
75
|
+
p.textContent = 'No payload';
|
|
76
|
+
return p;
|
|
77
|
+
}
|
|
78
|
+
const required = new Set(s.required || []);
|
|
79
|
+
const table = document.createElement('table');
|
|
80
|
+
table.className = 'socketspec-table';
|
|
81
|
+
|
|
82
|
+
const thead = table.createTHead();
|
|
83
|
+
const hrow = thead.insertRow();
|
|
84
|
+
for (const col of ['Name', 'Type', 'Required', 'Description']) {
|
|
85
|
+
const th = document.createElement('th');
|
|
86
|
+
th.textContent = col;
|
|
87
|
+
hrow.appendChild(th);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const tbody = table.createTBody();
|
|
91
|
+
for (const [name, def] of Object.entries(s.properties)) {
|
|
92
|
+
const row = tbody.insertRow();
|
|
93
|
+
|
|
94
|
+
const tdName = row.insertCell(); tdName.className = 'col-name'; tdName.textContent = name;
|
|
95
|
+
const tdType = row.insertCell(); tdType.className = 'col-type';
|
|
96
|
+
tdType.textContent = def.type ?? (def.$ref ? 'object' : 'any');
|
|
97
|
+
|
|
98
|
+
const tdReq = row.insertCell();
|
|
99
|
+
if (required.has(name)) {
|
|
100
|
+
const star = document.createElement('span');
|
|
101
|
+
star.className = 'required-star'; star.textContent = '*';
|
|
102
|
+
tdReq.appendChild(star);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const tdDesc = row.insertCell(); tdDesc.className = 'col-desc';
|
|
106
|
+
tdDesc.textContent = def.description ?? def.title ?? '';
|
|
107
|
+
}
|
|
108
|
+
return table;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* --- Card building --- */
|
|
112
|
+
function makeOpblock(direction, eventName, descText, schemaObj, isSubcard) {
|
|
113
|
+
// direction: 'emit' | 'listen' | 'broadcast'
|
|
114
|
+
const block = document.createElement('div');
|
|
115
|
+
block.className = `opblock opblock-${direction}`;
|
|
116
|
+
if (!isSubcard) block.id = `card-${eventName}`;
|
|
117
|
+
|
|
118
|
+
// Summary row
|
|
119
|
+
const summary = document.createElement('div');
|
|
120
|
+
summary.className = 'opblock-summary';
|
|
121
|
+
|
|
122
|
+
const method = document.createElement('button');
|
|
123
|
+
method.className = 'opblock-summary-method';
|
|
124
|
+
method.type = 'button';
|
|
125
|
+
method.textContent = direction.toUpperCase();
|
|
126
|
+
|
|
127
|
+
const pathEl = document.createElement('div');
|
|
128
|
+
pathEl.className = 'opblock-summary-path';
|
|
129
|
+
const pathSpan = document.createElement('span');
|
|
130
|
+
pathSpan.textContent = eventName;
|
|
131
|
+
pathEl.appendChild(pathSpan);
|
|
132
|
+
|
|
133
|
+
const descEl = document.createElement('div');
|
|
134
|
+
descEl.className = 'opblock-summary-description';
|
|
135
|
+
descEl.textContent = descText || '';
|
|
136
|
+
|
|
137
|
+
summary.appendChild(method);
|
|
138
|
+
summary.appendChild(pathEl);
|
|
139
|
+
summary.appendChild(descEl);
|
|
140
|
+
|
|
141
|
+
// Toggle on summary click
|
|
142
|
+
summary.addEventListener('click', () => block.classList.toggle('is-open'));
|
|
143
|
+
|
|
144
|
+
block.appendChild(summary);
|
|
145
|
+
|
|
146
|
+
// Body
|
|
147
|
+
const body = document.createElement('div');
|
|
148
|
+
body.className = 'opblock-body';
|
|
149
|
+
|
|
150
|
+
if (schemaObj !== undefined) {
|
|
151
|
+
// Schema section inside subcard
|
|
152
|
+
const sec = document.createElement('div');
|
|
153
|
+
sec.className = 'opblock-section';
|
|
154
|
+
const secHead = document.createElement('div');
|
|
155
|
+
secHead.className = 'opblock-section-header';
|
|
156
|
+
const h4 = document.createElement('h4'); h4.textContent = 'Schema';
|
|
157
|
+
secHead.appendChild(h4);
|
|
158
|
+
sec.appendChild(secHead);
|
|
159
|
+
const inner = document.createElement('div');
|
|
160
|
+
inner.className = 'table-container';
|
|
161
|
+
inner.appendChild(buildSchemaTable(schemaObj));
|
|
162
|
+
sec.appendChild(inner);
|
|
163
|
+
body.appendChild(sec);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
block.appendChild(body);
|
|
167
|
+
return { block, body };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function buildTryItSection(event) {
|
|
171
|
+
const wrapper = document.createElement('div');
|
|
172
|
+
wrapper.style.cssText = 'display:flex;flex-direction:column;gap:8px;';
|
|
173
|
+
|
|
174
|
+
const tryItBtn = document.createElement('button');
|
|
175
|
+
tryItBtn.className = 'try-out__btn';
|
|
176
|
+
tryItBtn.type = 'button';
|
|
177
|
+
tryItBtn.textContent = 'Try it out';
|
|
178
|
+
|
|
179
|
+
const editorArea = document.createElement('div');
|
|
180
|
+
editorArea.style.display = 'none';
|
|
181
|
+
|
|
182
|
+
const editor = document.createElement('textarea');
|
|
183
|
+
editor.className = 'payload-editor';
|
|
184
|
+
editor.value = JSON.stringify(exampleFromSchema(event.payload), null, 2);
|
|
185
|
+
|
|
186
|
+
const btnRow = document.createElement('div');
|
|
187
|
+
btnRow.style.cssText = 'display:flex;gap:8px;align-items:center;';
|
|
188
|
+
|
|
189
|
+
const execBtn = document.createElement('button');
|
|
190
|
+
execBtn.className = 'btn-execute';
|
|
191
|
+
execBtn.type = 'button';
|
|
192
|
+
execBtn.textContent = 'Execute';
|
|
193
|
+
|
|
194
|
+
const cancelBtn = document.createElement('button');
|
|
195
|
+
cancelBtn.className = 'btn-cancel-try';
|
|
196
|
+
cancelBtn.type = 'button';
|
|
197
|
+
cancelBtn.textContent = 'Cancel';
|
|
198
|
+
|
|
199
|
+
btnRow.appendChild(execBtn);
|
|
200
|
+
btnRow.appendChild(cancelBtn);
|
|
201
|
+
editorArea.appendChild(editor);
|
|
202
|
+
editorArea.appendChild(btnRow);
|
|
203
|
+
|
|
204
|
+
const responseBlock = document.createElement('pre');
|
|
205
|
+
responseBlock.className = 'response-block';
|
|
206
|
+
responseBlock.style.display = 'none';
|
|
207
|
+
|
|
208
|
+
tryItBtn.addEventListener('click', () => {
|
|
209
|
+
tryItBtn.style.display = 'none';
|
|
210
|
+
editorArea.style.display = 'block';
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
cancelBtn.addEventListener('click', () => {
|
|
214
|
+
tryItBtn.style.display = '';
|
|
215
|
+
editorArea.style.display = 'none';
|
|
216
|
+
responseBlock.style.display = 'none';
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
execBtn.addEventListener('click', () => {
|
|
220
|
+
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
|
221
|
+
responseBlock.style.display = 'block';
|
|
222
|
+
responseBlock.textContent = 'Error: Connect first';
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
let payload;
|
|
226
|
+
try { payload = JSON.parse(editor.value || '{}'); }
|
|
227
|
+
catch (err) {
|
|
228
|
+
responseBlock.style.display = 'block';
|
|
229
|
+
responseBlock.textContent = `Invalid JSON: ${err.message}`;
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const msg = { event: event.name, payload };
|
|
233
|
+
socket.send(JSON.stringify(msg));
|
|
234
|
+
logMessage('OUT', msg);
|
|
235
|
+
responseBlock.style.display = 'block';
|
|
236
|
+
responseBlock.textContent = 'Sent. Waiting for response...';
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
tryItContexts[event.name] = { editor, responseBlock, editorArea, tryItBtn };
|
|
240
|
+
|
|
241
|
+
wrapper.appendChild(tryItBtn);
|
|
242
|
+
wrapper.appendChild(editorArea);
|
|
243
|
+
wrapper.appendChild(responseBlock);
|
|
244
|
+
return wrapper;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function buildErrorTable() {
|
|
248
|
+
const errors = [
|
|
249
|
+
{ code: 'VALIDATION_ERROR', desc: 'Payload failed Pydantic validation or JSON parsing failed.' },
|
|
250
|
+
{ code: 'RATE_LIMIT_ERROR', desc: 'Connection exceeded the allowed rate limit.' },
|
|
251
|
+
{ code: 'UNKNOWN_EVENT', desc: 'The sent event name has no registered handler.' },
|
|
252
|
+
{ code: 'PAYLOAD_TOO_LARGE', desc: 'Incoming frame exceeded the maximum payload size.' },
|
|
253
|
+
];
|
|
254
|
+
const table = document.createElement('table');
|
|
255
|
+
table.className = 'socketspec-table';
|
|
256
|
+
const thead = table.createTHead();
|
|
257
|
+
const hrow = thead.insertRow();
|
|
258
|
+
['Code', 'When it occurs'].forEach(col => {
|
|
259
|
+
const th = document.createElement('th'); th.textContent = col; hrow.appendChild(th);
|
|
260
|
+
});
|
|
261
|
+
const tbody = table.createTBody();
|
|
262
|
+
for (const e of errors) {
|
|
263
|
+
const row = tbody.insertRow();
|
|
264
|
+
const c1 = row.insertCell(); c1.className = 'col-name'; c1.textContent = e.code;
|
|
265
|
+
const c2 = row.insertCell(); c2.className = 'col-desc'; c2.textContent = e.desc;
|
|
266
|
+
}
|
|
267
|
+
return table;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function buildSection(label, child) {
|
|
271
|
+
const sec = document.createElement('div');
|
|
272
|
+
sec.className = 'opblock-section';
|
|
273
|
+
|
|
274
|
+
const head = document.createElement('div');
|
|
275
|
+
head.className = 'opblock-section-header';
|
|
276
|
+
const h5 = document.createElement('h5'); h5.textContent = label;
|
|
277
|
+
head.appendChild(h5);
|
|
278
|
+
sec.appendChild(head);
|
|
279
|
+
|
|
280
|
+
const inner = document.createElement('div');
|
|
281
|
+
inner.className = 'table-container';
|
|
282
|
+
inner.appendChild(child);
|
|
283
|
+
sec.appendChild(inner);
|
|
284
|
+
return sec;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function buildEventCard(event) {
|
|
288
|
+
const { block, body } = makeOpblock('emit', event.name, event.description || '', undefined, false);
|
|
289
|
+
|
|
290
|
+
// Try it out button row (top right of body header)
|
|
291
|
+
const tryHeader = document.createElement('div');
|
|
292
|
+
tryHeader.className = 'opblock-section-header';
|
|
293
|
+
const tryLabel = document.createElement('h4');
|
|
294
|
+
tryLabel.textContent = 'Parameters';
|
|
295
|
+
tryHeader.appendChild(tryLabel);
|
|
296
|
+
tryHeader.appendChild(buildTryItSection(event));
|
|
297
|
+
body.appendChild(tryHeader);
|
|
298
|
+
|
|
299
|
+
// Parameters table
|
|
300
|
+
const paramInner = document.createElement('div');
|
|
301
|
+
paramInner.className = 'table-container';
|
|
302
|
+
paramInner.appendChild(buildSchemaTable(event.payload));
|
|
303
|
+
body.appendChild(paramInner);
|
|
304
|
+
|
|
305
|
+
// Server responds section
|
|
306
|
+
if (event.emits && event.emits.length > 0) {
|
|
307
|
+
const emitWrap = document.createElement('div');
|
|
308
|
+
emitWrap.style.cssText = 'display:flex;flex-direction:column;gap:8px;margin-top:8px';
|
|
309
|
+
for (const em of event.emits) {
|
|
310
|
+
const { block: sub } = makeOpblock('listen', em.event, em.description || '', em.schema, true);
|
|
311
|
+
emitWrap.appendChild(sub);
|
|
312
|
+
}
|
|
313
|
+
body.appendChild(buildSection('Server responds to sender', emitWrap));
|
|
314
|
+
responseEventIndex[event.name] = responseEventIndex[event.name] || new Set();
|
|
315
|
+
for (const em of event.emits) responseEventIndex[event.name].add(em.event);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Room broadcast section
|
|
319
|
+
if (event.broadcasts && event.broadcasts.length > 0) {
|
|
320
|
+
const bcastWrap = document.createElement('div');
|
|
321
|
+
bcastWrap.style.cssText = 'display:flex;flex-direction:column;gap:8px;margin-top:8px';
|
|
322
|
+
for (const bc of event.broadcasts) {
|
|
323
|
+
const label = `${bc.event} — Room: ${bc.room || '?'}`;
|
|
324
|
+
const { block: sub } = makeOpblock('broadcast', label, bc.description || '', bc.schema, true);
|
|
325
|
+
bcastWrap.appendChild(sub);
|
|
326
|
+
}
|
|
327
|
+
body.appendChild(buildSection('Broadcast to room', bcastWrap));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Error responses section (always shown)
|
|
331
|
+
body.appendChild(buildSection('Error responses', buildErrorTable()));
|
|
332
|
+
|
|
333
|
+
return block;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/* --- Render all events --- */
|
|
337
|
+
function renderEvents() {
|
|
338
|
+
const root = document.getElementById('socketspec-ui');
|
|
339
|
+
root.innerHTML = '';
|
|
340
|
+
|
|
341
|
+
// swagger-ui wrapper
|
|
342
|
+
const ui = document.createElement('div');
|
|
343
|
+
ui.className = 'swagger-ui';
|
|
344
|
+
|
|
345
|
+
// Topbar
|
|
346
|
+
const topbar = document.createElement('div');
|
|
347
|
+
topbar.className = 'socketspec-topbar';
|
|
348
|
+
|
|
349
|
+
const brandDiv = document.createElement('div');
|
|
350
|
+
brandDiv.style.cssText = 'display:flex;align-items:center;gap:8px;';
|
|
351
|
+
const titleEl = document.createElement('h1');
|
|
352
|
+
titleEl.className = 'title';
|
|
353
|
+
titleEl.textContent = 'SocketSpec';
|
|
354
|
+
const verEl = document.createElement('span');
|
|
355
|
+
verEl.className = 'version';
|
|
356
|
+
verEl.id = 'ver-badge';
|
|
357
|
+
verEl.textContent = schema.version ? `v${schema.version}` : '';
|
|
358
|
+
brandDiv.appendChild(titleEl);
|
|
359
|
+
brandDiv.appendChild(verEl);
|
|
360
|
+
|
|
361
|
+
const ctrlDiv = document.createElement('div');
|
|
362
|
+
ctrlDiv.className = 'ws-controls';
|
|
363
|
+
|
|
364
|
+
wsUrlInput = document.createElement('input');
|
|
365
|
+
wsUrlInput.type = 'text';
|
|
366
|
+
wsUrlInput.value = `ws://${location.host}/ws`;
|
|
367
|
+
wsUrlInput.placeholder = 'ws://localhost:8000/ws';
|
|
368
|
+
|
|
369
|
+
connectBtn = document.createElement('button');
|
|
370
|
+
connectBtn.className = 'btn-connect';
|
|
371
|
+
connectBtn.type = 'button';
|
|
372
|
+
connectBtn.textContent = 'Connect';
|
|
373
|
+
connectBtn.addEventListener('click', handleConnect);
|
|
374
|
+
|
|
375
|
+
const authBtnEl = document.createElement('button');
|
|
376
|
+
authBtnEl.className = 'btn-authorize';
|
|
377
|
+
authBtnEl.type = 'button';
|
|
378
|
+
authBtnEl.textContent = 'Authorize';
|
|
379
|
+
authBtnEl.addEventListener('click', () => authModal.showModal());
|
|
380
|
+
|
|
381
|
+
ctrlDiv.appendChild(wsUrlInput);
|
|
382
|
+
ctrlDiv.appendChild(connectBtn);
|
|
383
|
+
ctrlDiv.appendChild(authBtnEl);
|
|
384
|
+
topbar.appendChild(brandDiv);
|
|
385
|
+
topbar.appendChild(ctrlDiv);
|
|
386
|
+
ui.appendChild(topbar);
|
|
387
|
+
|
|
388
|
+
// Status bar
|
|
389
|
+
const statusBar = document.createElement('div');
|
|
390
|
+
statusBar.className = 'socketspec-status-bar';
|
|
391
|
+
statusDotEl = document.createElement('span');
|
|
392
|
+
statusDotEl.className = 'status-dot';
|
|
393
|
+
statusTextEl = document.createElement('span');
|
|
394
|
+
statusTextEl.textContent = 'Disconnected';
|
|
395
|
+
statusConnIdEl = document.createElement('span');
|
|
396
|
+
statusConnIdEl.className = 'status-conn-id';
|
|
397
|
+
statusHintEl = document.createElement('span');
|
|
398
|
+
statusHintEl.className = 'status-hint';
|
|
399
|
+
statusBar.appendChild(statusDotEl);
|
|
400
|
+
statusBar.appendChild(statusTextEl);
|
|
401
|
+
statusBar.appendChild(statusConnIdEl);
|
|
402
|
+
statusBar.appendChild(statusHintEl);
|
|
403
|
+
ui.appendChild(statusBar);
|
|
404
|
+
|
|
405
|
+
// Info block
|
|
406
|
+
const infoContainer = document.createElement('div');
|
|
407
|
+
infoContainer.className = 'information-container wrapper';
|
|
408
|
+
const infoDiv = document.createElement('div');
|
|
409
|
+
infoDiv.className = 'info';
|
|
410
|
+
const infoTitle = document.createElement('h2');
|
|
411
|
+
infoTitle.className = 'title';
|
|
412
|
+
infoTitle.textContent = 'SocketSpec';
|
|
413
|
+
const infoVersion = document.createElement('span');
|
|
414
|
+
infoVersion.className = 'version';
|
|
415
|
+
infoVersion.textContent = schema.version ? `v${schema.version}` : '';
|
|
416
|
+
infoTitle.appendChild(infoVersion);
|
|
417
|
+
infoDiv.appendChild(infoTitle);
|
|
418
|
+
infoContainer.appendChild(infoDiv);
|
|
419
|
+
ui.appendChild(infoContainer);
|
|
420
|
+
|
|
421
|
+
// Group by tag
|
|
422
|
+
const groups = {};
|
|
423
|
+
for (const ev of schema.events) {
|
|
424
|
+
const tag = ev.tags?.[0] || ev.namespace || 'default';
|
|
425
|
+
(groups[tag] = groups[tag] || []).push(ev);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const wrapper = document.createElement('div');
|
|
429
|
+
wrapper.className = 'wrapper';
|
|
430
|
+
|
|
431
|
+
for (const [tag, events] of Object.entries(groups)) {
|
|
432
|
+
const tagSection = document.createElement('div');
|
|
433
|
+
tagSection.className = 'opblock-tag-section';
|
|
434
|
+
|
|
435
|
+
const tagHeader = document.createElement('div');
|
|
436
|
+
tagHeader.className = 'opblock-tag';
|
|
437
|
+
const tagH4 = document.createElement('h4');
|
|
438
|
+
tagH4.className = 'opblock-tag';
|
|
439
|
+
tagH4.style.cssText = 'text-transform:uppercase;font-size:1rem;';
|
|
440
|
+
tagH4.textContent = tag;
|
|
441
|
+
tagHeader.appendChild(tagH4);
|
|
442
|
+
tagSection.appendChild(tagHeader);
|
|
443
|
+
|
|
444
|
+
for (const ev of events) {
|
|
445
|
+
tagSection.appendChild(buildEventCard(ev));
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
wrapper.appendChild(tagSection);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
ui.appendChild(wrapper);
|
|
452
|
+
|
|
453
|
+
// Log drawer
|
|
454
|
+
const logDrawer = document.createElement('div');
|
|
455
|
+
logDrawer.className = 'socketspec-log';
|
|
456
|
+
|
|
457
|
+
const logHeader = document.createElement('div');
|
|
458
|
+
logHeader.className = 'log-header';
|
|
459
|
+
const logTitle = document.createElement('strong');
|
|
460
|
+
logTitle.textContent = 'Live Log';
|
|
461
|
+
logFilterEl = document.createElement('input');
|
|
462
|
+
logFilterEl.type = 'text';
|
|
463
|
+
logFilterEl.placeholder = 'Filter...';
|
|
464
|
+
logFilterEl.addEventListener('input', () => {
|
|
465
|
+
const val = logFilterEl.value.toLowerCase();
|
|
466
|
+
for (const el of (logOutputEl ? logOutputEl.children : [])) {
|
|
467
|
+
el.style.display = val && !el.textContent.toLowerCase().includes(val) ? 'none' : '';
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
const clearBtn = document.createElement('button');
|
|
471
|
+
clearBtn.textContent = 'Clear';
|
|
472
|
+
clearBtn.addEventListener('click', () => { if (logOutputEl) logOutputEl.innerHTML = ''; });
|
|
473
|
+
logHeader.appendChild(logTitle);
|
|
474
|
+
logHeader.appendChild(logFilterEl);
|
|
475
|
+
logHeader.appendChild(clearBtn);
|
|
476
|
+
|
|
477
|
+
logOutputEl = document.createElement('pre');
|
|
478
|
+
logOutputEl.id = 'log-output';
|
|
479
|
+
|
|
480
|
+
logDrawer.appendChild(logHeader);
|
|
481
|
+
logDrawer.appendChild(logOutputEl);
|
|
482
|
+
ui.appendChild(logDrawer);
|
|
483
|
+
|
|
484
|
+
root.appendChild(ui);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/* --- Schema load --- */
|
|
488
|
+
async function loadSchema() {
|
|
489
|
+
try {
|
|
490
|
+
const res = await fetch(`${DOCS_BASE}/schema`);
|
|
491
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
492
|
+
schema = await res.json();
|
|
493
|
+
renderEvents();
|
|
494
|
+
logMessage('SYS', `Schema loaded — ${schema.events.length} event(s)`);
|
|
495
|
+
} catch (err) {
|
|
496
|
+
console.error('Schema load failed:', err);
|
|
497
|
+
const root = document.getElementById('socketspec-ui');
|
|
498
|
+
root.innerHTML = `<p style="color:red;padding:20px">Failed to load schema: ${err.message}</p>`;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/* --- Connection status update --- */
|
|
503
|
+
function setConnected(connected, id) {
|
|
504
|
+
isConnected = connected;
|
|
505
|
+
connId = id || null;
|
|
506
|
+
if (!statusDotEl) return;
|
|
507
|
+
statusDotEl.className = `status-dot${connected ? ' connected' : ''}`;
|
|
508
|
+
statusTextEl.textContent = connected ? 'Connected' : 'Disconnected';
|
|
509
|
+
statusConnIdEl.textContent = connected && id ? id : '';
|
|
510
|
+
statusHintEl.textContent = connected ? 'Open another tab to test as a different user' : '';
|
|
511
|
+
if (connectBtn) {
|
|
512
|
+
connectBtn.textContent = connected ? 'Disconnect' : 'Connect';
|
|
513
|
+
connectBtn.classList.toggle('connected', connected);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/* --- Route incoming to try-it context --- */
|
|
518
|
+
function routeIncoming(data) {
|
|
519
|
+
const ev = data?.event;
|
|
520
|
+
if (!ev) return;
|
|
521
|
+
|
|
522
|
+
// Route to sender's try-it block
|
|
523
|
+
for (const [trigger, listenSet] of Object.entries(responseEventIndex)) {
|
|
524
|
+
if (listenSet.has(ev)) {
|
|
525
|
+
const ctx = tryItContexts[trigger];
|
|
526
|
+
if (ctx && ctx.editorArea.style.display !== 'none') {
|
|
527
|
+
ctx.responseBlock.style.display = 'block';
|
|
528
|
+
ctx.responseBlock.textContent = JSON.stringify(data, null, 2);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Route __error__ to all open try-it blocks
|
|
534
|
+
if (ev === '__error__') {
|
|
535
|
+
for (const ctx of Object.values(tryItContexts)) {
|
|
536
|
+
if (ctx.editorArea.style.display !== 'none') {
|
|
537
|
+
ctx.responseBlock.style.display = 'block';
|
|
538
|
+
ctx.responseBlock.textContent = JSON.stringify(data, null, 2);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Grab conn_id from first welcome/connect message
|
|
544
|
+
if (!connId && data.payload?.conn_id) {
|
|
545
|
+
setConnected(true, data.payload.conn_id);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/* --- WebSocket URL builder --- */
|
|
550
|
+
function buildWsUrl() {
|
|
551
|
+
let url = wsUrlInput ? wsUrlInput.value.trim() : `ws://${location.host}/ws`;
|
|
552
|
+
const params = new URLSearchParams();
|
|
553
|
+
if (authToken) params.set('token', authToken);
|
|
554
|
+
if (authApiKey) params.set('api_key', authApiKey);
|
|
555
|
+
const q = params.toString();
|
|
556
|
+
if (q) url += (url.includes('?') ? '&' : '?') + q;
|
|
557
|
+
return url;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/* --- Connect / Disconnect handler --- */
|
|
561
|
+
function handleConnect() {
|
|
562
|
+
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
563
|
+
socket.close();
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
try {
|
|
567
|
+
socket = new WebSocket(buildWsUrl());
|
|
568
|
+
} catch (err) {
|
|
569
|
+
logMessage('SYS', `Invalid URL: ${err.message}`);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
socket.onopen = () => {
|
|
574
|
+
setConnected(true, null);
|
|
575
|
+
logMessage('SYS', `Connected to ${wsUrlInput ? wsUrlInput.value : ''}`);
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
socket.onclose = (ev) => {
|
|
579
|
+
setConnected(false, null);
|
|
580
|
+
logMessage('SYS', `Disconnected (code ${ev.code})`);
|
|
581
|
+
socket = null;
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
socket.onmessage = (ev) => {
|
|
585
|
+
try {
|
|
586
|
+
const data = JSON.parse(ev.data);
|
|
587
|
+
// Handle ping — respond with pong silently
|
|
588
|
+
if (data.event === '__ping__') {
|
|
589
|
+
socket.send(JSON.stringify({ event: '__pong__', payload: {} }));
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
logMessage('IN', data);
|
|
593
|
+
routeIncoming(data);
|
|
594
|
+
} catch {
|
|
595
|
+
logMessage('IN', ev.data);
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
socket.onerror = () => logMessage('SYS', 'WebSocket error');
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/* --- Auth modal --- */
|
|
603
|
+
document.getElementById('auth-save').addEventListener('click', () => {
|
|
604
|
+
authToken = document.getElementById('auth-token').value.trim();
|
|
605
|
+
authApiKey = document.getElementById('auth-api-key').value.trim();
|
|
606
|
+
logMessage('SYS', 'Credentials saved');
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
/* --- Boot --- */
|
|
610
|
+
loadSchema();
|