bitwrench 2.0.15 → 2.0.16
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 +57 -21
- package/dist/bitwrench-bccl.cjs.js +3746 -0
- package/dist/bitwrench-bccl.cjs.min.js +40 -0
- package/dist/bitwrench-bccl.esm.js +3741 -0
- package/dist/bitwrench-bccl.esm.min.js +40 -0
- package/dist/bitwrench-bccl.umd.js +3752 -0
- package/dist/bitwrench-bccl.umd.min.js +40 -0
- package/dist/bitwrench-code-edit.cjs.js +57 -7
- package/dist/bitwrench-code-edit.cjs.min.js +9 -2
- package/dist/bitwrench-code-edit.es5.js +74 -11
- package/dist/bitwrench-code-edit.es5.min.js +9 -2
- package/dist/bitwrench-code-edit.esm.js +57 -7
- package/dist/bitwrench-code-edit.esm.min.js +9 -2
- package/dist/bitwrench-code-edit.umd.js +57 -7
- package/dist/bitwrench-code-edit.umd.min.js +9 -2
- package/dist/bitwrench-lean.cjs.js +413 -17
- package/dist/bitwrench-lean.cjs.min.js +7 -7
- package/dist/bitwrench-lean.es5.js +428 -16
- package/dist/bitwrench-lean.es5.min.js +5 -5
- package/dist/bitwrench-lean.esm.js +413 -17
- package/dist/bitwrench-lean.esm.min.js +7 -7
- package/dist/bitwrench-lean.umd.js +413 -17
- package/dist/bitwrench-lean.umd.min.js +7 -7
- package/dist/bitwrench.cjs.js +413 -17
- package/dist/bitwrench.cjs.min.js +7 -7
- package/dist/bitwrench.css +60 -17
- package/dist/bitwrench.es5.js +428 -16
- package/dist/bitwrench.es5.min.js +6 -6
- package/dist/bitwrench.esm.js +413 -17
- package/dist/bitwrench.esm.min.js +7 -7
- package/dist/bitwrench.min.css +1 -1
- package/dist/bitwrench.umd.js +413 -17
- package/dist/bitwrench.umd.min.js +7 -7
- package/dist/builds.json +168 -80
- package/dist/bwserve.cjs.js +646 -0
- package/dist/bwserve.esm.js +638 -0
- package/dist/sri.json +36 -28
- package/package.json +18 -3
- package/readme.html +62 -23
- package/src/bitwrench-bccl-entry.js +72 -0
- package/src/bitwrench-code-edit.js +56 -6
- package/src/bitwrench-color-utils.js +5 -6
- package/src/bitwrench-styles.js +20 -8
- package/src/bitwrench.js +385 -0
- package/src/bwserve/client.js +182 -0
- package/src/bwserve/index.js +352 -0
- package/src/bwserve/shell.js +103 -0
- package/src/cli/index.js +36 -15
- package/src/cli/serve.js +325 -0
- package/src/version.js +3 -3
- /package/bin/{bitwrench.js → bwcli.js} +0 -0
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
/*! bwserve v2.0.16 | BSD-2-Clause | https://deftio.github.com/bitwrench/pages */
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
5
|
+
|
|
6
|
+
var url = require('url');
|
|
7
|
+
var path = require('path');
|
|
8
|
+
var http = require('http');
|
|
9
|
+
var fs = require('fs');
|
|
10
|
+
|
|
11
|
+
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
12
|
+
/**
|
|
13
|
+
* BwServeClient — per-client connection for bwserve.
|
|
14
|
+
*
|
|
15
|
+
* Represents one browser tab connected via SSE. The server calls methods
|
|
16
|
+
* on this object to push UI updates to the client.
|
|
17
|
+
*
|
|
18
|
+
* Protocol message types (sent as SSE data):
|
|
19
|
+
* { type: 'replace', target: '#app', node: {t,a,c,o} }
|
|
20
|
+
* { type: 'append', target: '#list', node: {t,a,c,o} }
|
|
21
|
+
* { type: 'remove', target: '#item-3' }
|
|
22
|
+
* { type: 'patch', target: 'bw_counter_abc', content: '42', attr: null }
|
|
23
|
+
* { type: 'batch', ops: [ ...messages ] }
|
|
24
|
+
* { type: 'register', name: 'fn', body: 'function(x) { ... }' }
|
|
25
|
+
* { type: 'call', name: 'fn', args: [...] }
|
|
26
|
+
* { type: 'exec', code: 'js code string' }
|
|
27
|
+
*
|
|
28
|
+
* @module bwserve/client
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* BwServeClient — one connected browser tab.
|
|
33
|
+
*/
|
|
34
|
+
class BwServeClient {
|
|
35
|
+
constructor(id, res) {
|
|
36
|
+
this.id = id;
|
|
37
|
+
this._res = res; // SSE response stream (null in stub)
|
|
38
|
+
this._handlers = {}; // action name → handler
|
|
39
|
+
this._closed = false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Replace the content of a DOM element with a TACO.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} selector - CSS selector or UUID
|
|
46
|
+
* @param {Object} taco - TACO object to render
|
|
47
|
+
*/
|
|
48
|
+
render(selector, taco) {
|
|
49
|
+
this._send({ type: 'replace', target: selector, node: taco });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Patch an element's content or attributes without rebuild.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} id - Element UUID (from bw.uuid())
|
|
56
|
+
* @param {string} content - New text content
|
|
57
|
+
* @param {Object} [attr] - Attributes to update
|
|
58
|
+
*/
|
|
59
|
+
patch(id, content, attr) {
|
|
60
|
+
this._send({ type: 'patch', target: id, content, attr: attr || null });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Append a TACO as a new child of the target element.
|
|
65
|
+
*
|
|
66
|
+
* @param {string} selector - CSS selector of parent
|
|
67
|
+
* @param {Object} taco - TACO object to append
|
|
68
|
+
*/
|
|
69
|
+
append(selector, taco) {
|
|
70
|
+
this._send({ type: 'append', target: selector, node: taco });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Remove an element from the DOM (with cleanup).
|
|
75
|
+
*
|
|
76
|
+
* @param {string} selector - CSS selector or UUID of element to remove
|
|
77
|
+
*/
|
|
78
|
+
remove(selector) {
|
|
79
|
+
this._send({ type: 'remove', target: selector });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Send multiple operations as a single batch.
|
|
84
|
+
*
|
|
85
|
+
* @param {Array} ops - Array of message objects (replace/append/remove/patch)
|
|
86
|
+
*/
|
|
87
|
+
batch(ops) {
|
|
88
|
+
this._send({ type: 'batch', ops });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Send a bw.message() dispatch to a tagged component on the client.
|
|
93
|
+
*
|
|
94
|
+
* @param {string} target - Component userTag or UUID
|
|
95
|
+
* @param {string} action - Method name to call
|
|
96
|
+
* @param {*} data - Data to pass to the method
|
|
97
|
+
*/
|
|
98
|
+
message(target, action, data) {
|
|
99
|
+
this._send({ type: 'message', target, action, data });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Register a named function on the client for later invocation via call().
|
|
104
|
+
*
|
|
105
|
+
* The function body is sent as a string and compiled on the client side.
|
|
106
|
+
* Registered functions persist for the lifetime of the connection.
|
|
107
|
+
*
|
|
108
|
+
* @param {string} name - Function name (used as key for later call())
|
|
109
|
+
* @param {string} body - Function source as string, e.g. "function(el) { el.scrollTop = el.scrollHeight; }"
|
|
110
|
+
*/
|
|
111
|
+
register(name, body) {
|
|
112
|
+
this._send({ type: 'register', name, body });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Call a previously registered or built-in function on the client.
|
|
117
|
+
*
|
|
118
|
+
* Built-in functions (always available, no registration needed):
|
|
119
|
+
* scrollTo, focus, download, clipboard, redirect, log
|
|
120
|
+
*
|
|
121
|
+
* @param {string} name - Function name (registered or built-in)
|
|
122
|
+
* @param {...*} args - Arguments to pass to the function
|
|
123
|
+
*/
|
|
124
|
+
call(name, ...args) {
|
|
125
|
+
this._send({ type: 'call', name, args });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Execute arbitrary JavaScript code on the client.
|
|
130
|
+
*
|
|
131
|
+
* Requires the client connection to be created with { allowExec: true }.
|
|
132
|
+
* Use call() as the safe alternative when possible.
|
|
133
|
+
*
|
|
134
|
+
* @param {string} code - JavaScript code string to execute
|
|
135
|
+
*/
|
|
136
|
+
exec(code) {
|
|
137
|
+
this._send({ type: 'exec', code });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Register a handler for client actions (button clicks, form submits, etc.).
|
|
142
|
+
*
|
|
143
|
+
* @param {string} action - Action name (from o.events declarative handler)
|
|
144
|
+
* @param {Function} handler - Called with (data, client)
|
|
145
|
+
* @returns {BwServeClient} this (for chaining)
|
|
146
|
+
*/
|
|
147
|
+
on(action, handler) {
|
|
148
|
+
this._handlers[action] = handler;
|
|
149
|
+
return this;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Close the SSE connection to this client.
|
|
154
|
+
*/
|
|
155
|
+
close() {
|
|
156
|
+
this._closed = true;
|
|
157
|
+
if (this._res && typeof this._res.end === 'function') {
|
|
158
|
+
try { this._res.end(); } catch (e) { /* ignore */ }
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Send a protocol message to the client via SSE.
|
|
164
|
+
* @private
|
|
165
|
+
*/
|
|
166
|
+
_send(msg) {
|
|
167
|
+
if (this._closed) return;
|
|
168
|
+
// Always store for testing / inspection
|
|
169
|
+
if (!this._sent) this._sent = [];
|
|
170
|
+
this._sent.push(msg);
|
|
171
|
+
// Write SSE frame if we have a live response stream
|
|
172
|
+
if (this._res && typeof this._res.write === 'function') {
|
|
173
|
+
try {
|
|
174
|
+
this._res.write('data: ' + JSON.stringify(msg) + '\n\n');
|
|
175
|
+
} catch (e) {
|
|
176
|
+
// Stream may have been closed — ignore write errors
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Dispatch an incoming action from the client.
|
|
183
|
+
* @private
|
|
184
|
+
*/
|
|
185
|
+
_dispatch(action, data) {
|
|
186
|
+
const handler = this._handlers[action];
|
|
187
|
+
if (handler) {
|
|
188
|
+
handler(data, this);
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* bwserve shell — generates the HTML page shell served to browsers.
|
|
197
|
+
*
|
|
198
|
+
* The shell is a minimal HTML doc that:
|
|
199
|
+
* - Loads bitwrench UMD + CSS from /__bw/ routes
|
|
200
|
+
* - Calls bw.loadDefaultStyles()
|
|
201
|
+
* - Optionally applies a theme
|
|
202
|
+
* - Creates a #app div
|
|
203
|
+
* - Opens an SSE connection via bw.clientConnect()
|
|
204
|
+
* - Delegates data-bw-action clicks to the server via POST
|
|
205
|
+
*
|
|
206
|
+
* @module bwserve/shell
|
|
207
|
+
*/
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Generate the shell HTML page for a bwserve app.
|
|
211
|
+
*
|
|
212
|
+
* @param {Object} opts
|
|
213
|
+
* @param {string} opts.clientId - Unique client ID for this connection
|
|
214
|
+
* @param {string} [opts.title='bwserve'] - Page title
|
|
215
|
+
* @param {string} [opts.theme] - Theme preset name or config
|
|
216
|
+
* @param {boolean} [opts.injectBitwrench=true] - Whether to inject bitwrench scripts
|
|
217
|
+
* @returns {string} Complete HTML document
|
|
218
|
+
*/
|
|
219
|
+
function generateShell(opts) {
|
|
220
|
+
opts = opts || {};
|
|
221
|
+
var clientId = opts.clientId || 'default';
|
|
222
|
+
var title = opts.title || 'bwserve';
|
|
223
|
+
var inject = opts.injectBitwrench !== false;
|
|
224
|
+
|
|
225
|
+
var head = [
|
|
226
|
+
'<!DOCTYPE html>',
|
|
227
|
+
'<html lang="en">',
|
|
228
|
+
'<head>',
|
|
229
|
+
'<meta charset="UTF-8">',
|
|
230
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1.0">',
|
|
231
|
+
'<title>' + title + '</title>'
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
if (inject) {
|
|
235
|
+
head.push('<script src="/__bw/bitwrench.umd.js"></script>');
|
|
236
|
+
head.push('<link rel="stylesheet" href="/__bw/bitwrench.css">');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
head.push('</head>');
|
|
240
|
+
head.push('<body>');
|
|
241
|
+
head.push('<div id="app"></div>');
|
|
242
|
+
|
|
243
|
+
var script = [
|
|
244
|
+
'<script>',
|
|
245
|
+
'(function() {',
|
|
246
|
+
' "use strict";',
|
|
247
|
+
' bw.loadDefaultStyles();'
|
|
248
|
+
];
|
|
249
|
+
|
|
250
|
+
if (opts.theme) {
|
|
251
|
+
script.push(' bw.generateTheme("bwserve", ' + JSON.stringify(
|
|
252
|
+
typeof opts.theme === 'string'
|
|
253
|
+
? { primary: '#006666', secondary: '#333333' }
|
|
254
|
+
: opts.theme
|
|
255
|
+
) + ');');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
script.push(' var clientId = ' + JSON.stringify(clientId) + ';');
|
|
259
|
+
script.push(' var conn = bw.clientConnect("/__bw/events/" + clientId, {');
|
|
260
|
+
script.push(' actionUrl: "/__bw/action/" + clientId,');
|
|
261
|
+
script.push(' onStatus: function(s) {');
|
|
262
|
+
script.push(' if (typeof console !== "undefined") console.log("[bwserve] " + s);');
|
|
263
|
+
script.push(' }');
|
|
264
|
+
script.push(' });');
|
|
265
|
+
|
|
266
|
+
// data-bw-action click delegation
|
|
267
|
+
script.push(' document.addEventListener("click", function(e) {');
|
|
268
|
+
script.push(' var el = e.target.closest ? e.target.closest("[data-bw-action]") : null;');
|
|
269
|
+
script.push(' if (!el) return;');
|
|
270
|
+
script.push(' e.preventDefault();');
|
|
271
|
+
script.push(' var actionData = {};');
|
|
272
|
+
script.push(' if (el.getAttribute("data-bw-id")) actionData.bwId = el.getAttribute("data-bw-id");');
|
|
273
|
+
script.push(' var form = el.closest("div") || document;');
|
|
274
|
+
script.push(' var inp = form.querySelector("input[type=text],input:not([type])");');
|
|
275
|
+
script.push(' if (inp) { actionData.inputValue = inp.value; inp.value = ""; }');
|
|
276
|
+
script.push(' conn.sendAction(el.getAttribute("data-bw-action"), actionData);');
|
|
277
|
+
script.push(' });');
|
|
278
|
+
|
|
279
|
+
// Enter key on inputs
|
|
280
|
+
script.push(' document.addEventListener("keydown", function(e) {');
|
|
281
|
+
script.push(' if (e.key === "Enter" && e.target.tagName === "INPUT") {');
|
|
282
|
+
script.push(' var form = e.target.closest("div") || document;');
|
|
283
|
+
script.push(' var btn = form.querySelector("[data-bw-action]");');
|
|
284
|
+
script.push(' if (btn) {');
|
|
285
|
+
script.push(' conn.sendAction(btn.getAttribute("data-bw-action"), { inputValue: e.target.value });');
|
|
286
|
+
script.push(' e.target.value = "";');
|
|
287
|
+
script.push(' }');
|
|
288
|
+
script.push(' }');
|
|
289
|
+
script.push(' });');
|
|
290
|
+
|
|
291
|
+
script.push('})();');
|
|
292
|
+
script.push('</script>');
|
|
293
|
+
script.push('</body>');
|
|
294
|
+
script.push('</html>');
|
|
295
|
+
|
|
296
|
+
return head.concat(script).join('\n');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* bwserve — Server-driven UI library for bitwrench
|
|
301
|
+
*
|
|
302
|
+
* Programmatic API for building server-push UIs (Streamlit-style).
|
|
303
|
+
* Uses SSE (Server-Sent Events) by default, with WebSocket opt-in.
|
|
304
|
+
* Zero runtime dependencies — only Node.js stdlib (http, fs, path).
|
|
305
|
+
*
|
|
306
|
+
* Usage:
|
|
307
|
+
* import bwserve from 'bitwrench/bwserve';
|
|
308
|
+
* const app = bwserve.create({ port: 7902 });
|
|
309
|
+
* app.page('/', (client) => {
|
|
310
|
+
* client.render('#app', bw.makeCard({ title: 'Hello' }));
|
|
311
|
+
* });
|
|
312
|
+
* app.listen();
|
|
313
|
+
*
|
|
314
|
+
* @module bwserve
|
|
315
|
+
*/
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
var __dirname$1 = path.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('bwserve.cjs.js', document.baseURI).href))));
|
|
319
|
+
var DIST_DIR = path.resolve(__dirname$1, '..', '..', 'dist');
|
|
320
|
+
|
|
321
|
+
// MIME type lookup for static file serving
|
|
322
|
+
var MIME_TYPES = {
|
|
323
|
+
'.html': 'text/html; charset=utf-8',
|
|
324
|
+
'.js': 'application/javascript; charset=utf-8',
|
|
325
|
+
'.css': 'text/css; charset=utf-8',
|
|
326
|
+
'.json': 'application/json; charset=utf-8',
|
|
327
|
+
'.png': 'image/png',
|
|
328
|
+
'.jpg': 'image/jpeg',
|
|
329
|
+
'.jpeg': 'image/jpeg',
|
|
330
|
+
'.gif': 'image/gif',
|
|
331
|
+
'.svg': 'image/svg+xml',
|
|
332
|
+
'.ico': 'image/x-icon',
|
|
333
|
+
'.woff': 'font/woff',
|
|
334
|
+
'.woff2': 'font/woff2',
|
|
335
|
+
'.ttf': 'font/ttf',
|
|
336
|
+
'.map': 'application/json'
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Create a bwserve application.
|
|
341
|
+
*
|
|
342
|
+
* @param {Object} opts - Server options
|
|
343
|
+
* @param {number} [opts.port=7902] - Port to listen on
|
|
344
|
+
* @param {string} [opts.title='bwserve'] - Page title
|
|
345
|
+
* @param {string} [opts.static] - Directory to serve static files from
|
|
346
|
+
* @param {boolean} [opts.injectBitwrench=true] - Auto-inject bitwrench client JS
|
|
347
|
+
* @param {string|Object} [opts.theme] - Theme preset name or config object
|
|
348
|
+
* @returns {BwServeApp} Application instance
|
|
349
|
+
*/
|
|
350
|
+
function create(opts) {
|
|
351
|
+
return new BwServeApp(opts || {});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* BwServeApp — the server application object.
|
|
356
|
+
*
|
|
357
|
+
* Manages pages, client connections, and the HTTP/SSE server.
|
|
358
|
+
*/
|
|
359
|
+
class BwServeApp {
|
|
360
|
+
constructor(opts) {
|
|
361
|
+
this.port = opts.port || 7902;
|
|
362
|
+
this.title = opts.title || 'bwserve';
|
|
363
|
+
this.staticDir = opts.static || null;
|
|
364
|
+
this.injectBitwrench = opts.injectBitwrench !== false;
|
|
365
|
+
this.theme = opts.theme || null;
|
|
366
|
+
this.keepAliveInterval = opts.keepAliveInterval || 15000;
|
|
367
|
+
this._pages = new Map();
|
|
368
|
+
this._clients = new Map();
|
|
369
|
+
this._server = null;
|
|
370
|
+
this._clientCounter = 0;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Register a page handler.
|
|
375
|
+
*
|
|
376
|
+
* @param {string} path - URL path (e.g., '/', '/dashboard')
|
|
377
|
+
* @param {Function} handler - Called with (client: BwServeClient) on connection
|
|
378
|
+
* @returns {BwServeApp} this (for chaining)
|
|
379
|
+
*/
|
|
380
|
+
page(path, handler) {
|
|
381
|
+
this._pages.set(path, handler);
|
|
382
|
+
return this;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Start the HTTP server and begin accepting SSE connections.
|
|
387
|
+
*
|
|
388
|
+
* @param {Function} [callback] - Called when server is listening
|
|
389
|
+
* @returns {Promise<void>}
|
|
390
|
+
*/
|
|
391
|
+
listen(callback) {
|
|
392
|
+
var self = this;
|
|
393
|
+
|
|
394
|
+
return new Promise(function(res) {
|
|
395
|
+
self._server = http.createServer(function(req, rawRes) {
|
|
396
|
+
self._handleRequest(req, rawRes);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
self._server.listen(self.port, function() {
|
|
400
|
+
if (callback) callback();
|
|
401
|
+
res();
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Stop the server and close all client connections.
|
|
408
|
+
*/
|
|
409
|
+
close() {
|
|
410
|
+
var self = this;
|
|
411
|
+
return new Promise(function(res) {
|
|
412
|
+
// Close all SSE streams
|
|
413
|
+
for (var record of self._clients.values()) {
|
|
414
|
+
if (record.client && typeof record.client.close === 'function') {
|
|
415
|
+
record.client.close();
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
self._clients.clear();
|
|
419
|
+
|
|
420
|
+
if (self._server) {
|
|
421
|
+
self._server.close(function() {
|
|
422
|
+
self._server = null;
|
|
423
|
+
res();
|
|
424
|
+
});
|
|
425
|
+
} else {
|
|
426
|
+
res();
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Get count of active client connections.
|
|
433
|
+
* @returns {number}
|
|
434
|
+
*/
|
|
435
|
+
get clientCount() {
|
|
436
|
+
return this._clients.size;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Broadcast a protocol message to all connected clients.
|
|
441
|
+
*
|
|
442
|
+
* If msg has a clientId field, send only to that client.
|
|
443
|
+
* Otherwise, broadcast to all.
|
|
444
|
+
*
|
|
445
|
+
* @param {Object} msg - Protocol message (replace, patch, append, remove, batch)
|
|
446
|
+
* @returns {number} Number of clients that received the message
|
|
447
|
+
*/
|
|
448
|
+
broadcast(msg) {
|
|
449
|
+
if (msg.clientId) {
|
|
450
|
+
var record = this._clients.get(msg.clientId);
|
|
451
|
+
if (record && record.client) {
|
|
452
|
+
record.client._send(msg);
|
|
453
|
+
return 1;
|
|
454
|
+
}
|
|
455
|
+
return 0;
|
|
456
|
+
}
|
|
457
|
+
var count = 0;
|
|
458
|
+
for (var record of this._clients.values()) {
|
|
459
|
+
if (record.client && !record.client._closed) {
|
|
460
|
+
record.client._send(msg);
|
|
461
|
+
count++;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return count;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Internal: route incoming HTTP requests.
|
|
469
|
+
* @private
|
|
470
|
+
*/
|
|
471
|
+
_handleRequest(req, res) {
|
|
472
|
+
var url = req.url || '/';
|
|
473
|
+
var method = req.method || 'GET';
|
|
474
|
+
|
|
475
|
+
// Parse URL path (strip query string)
|
|
476
|
+
var path$1 = url.split('?')[0];
|
|
477
|
+
|
|
478
|
+
// /__bw/bitwrench.umd.js — serve bitwrench client library
|
|
479
|
+
if (path$1 === '/__bw/bitwrench.umd.js' && method === 'GET') {
|
|
480
|
+
return this._serveDistFile(res, 'bitwrench.umd.js');
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// /__bw/bitwrench.umd.min.js — serve minified
|
|
484
|
+
if (path$1 === '/__bw/bitwrench.umd.min.js' && method === 'GET') {
|
|
485
|
+
return this._serveDistFile(res, 'bitwrench.umd.min.js');
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// /__bw/bitwrench.css — serve bitwrench CSS
|
|
489
|
+
if (path$1 === '/__bw/bitwrench.css' && method === 'GET') {
|
|
490
|
+
return this._serveDistFile(res, 'bitwrench.css');
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// /__bw/events/:clientId — SSE stream
|
|
494
|
+
if (path$1.startsWith('/__bw/events/') && method === 'GET') {
|
|
495
|
+
var clientId = path$1.slice('/__bw/events/'.length);
|
|
496
|
+
return this._handleSSE(req, res, clientId);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// /__bw/action/:clientId — action POST
|
|
500
|
+
if (path$1.startsWith('/__bw/action/') && method === 'POST') {
|
|
501
|
+
var actionClientId = path$1.slice('/__bw/action/'.length);
|
|
502
|
+
return this._handleAction(req, res, actionClientId);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Registered page routes — serve shell HTML
|
|
506
|
+
if (method === 'GET' && this._pages.has(path$1)) {
|
|
507
|
+
var clientId2 = 'c' + (++this._clientCounter);
|
|
508
|
+
var shell = generateShell({
|
|
509
|
+
clientId: clientId2,
|
|
510
|
+
title: this.title,
|
|
511
|
+
theme: this.theme,
|
|
512
|
+
injectBitwrench: this.injectBitwrench
|
|
513
|
+
});
|
|
514
|
+
// Store the page path for this client so SSE knows which handler to call
|
|
515
|
+
this._clients.set(clientId2, { pagePath: path$1, client: null });
|
|
516
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
517
|
+
res.end(shell);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Static file serving
|
|
522
|
+
if (method === 'GET' && this.staticDir) {
|
|
523
|
+
var filePath = path.join(this.staticDir, path$1);
|
|
524
|
+
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
|
|
525
|
+
var ext = path.extname(filePath);
|
|
526
|
+
var mime = MIME_TYPES[ext] || 'application/octet-stream';
|
|
527
|
+
var content = fs.readFileSync(filePath);
|
|
528
|
+
res.writeHead(200, { 'Content-Type': mime });
|
|
529
|
+
res.end(content);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// 404
|
|
535
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
536
|
+
res.end('Not Found');
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Serve a file from the dist/ directory.
|
|
541
|
+
* @private
|
|
542
|
+
*/
|
|
543
|
+
_serveDistFile(res, filename) {
|
|
544
|
+
var filePath = path.join(DIST_DIR, filename);
|
|
545
|
+
if (!fs.existsSync(filePath)) {
|
|
546
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
547
|
+
res.end('Not Found: ' + filename);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
var ext = path.extname(filename);
|
|
551
|
+
var mime = MIME_TYPES[ext] || 'application/octet-stream';
|
|
552
|
+
var content = fs.readFileSync(filePath);
|
|
553
|
+
res.writeHead(200, {
|
|
554
|
+
'Content-Type': mime,
|
|
555
|
+
'Cache-Control': 'public, max-age=3600'
|
|
556
|
+
});
|
|
557
|
+
res.end(content);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Handle an SSE connection.
|
|
562
|
+
* @private
|
|
563
|
+
*/
|
|
564
|
+
_handleSSE(req, res, clientId) {
|
|
565
|
+
var self = this;
|
|
566
|
+
|
|
567
|
+
// Set SSE headers
|
|
568
|
+
res.writeHead(200, {
|
|
569
|
+
'Content-Type': 'text/event-stream',
|
|
570
|
+
'Cache-Control': 'no-cache',
|
|
571
|
+
'Connection': 'keep-alive',
|
|
572
|
+
'Access-Control-Allow-Origin': '*'
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
// Create client instance
|
|
576
|
+
var client = new BwServeClient(clientId, res);
|
|
577
|
+
|
|
578
|
+
// Look up the pending client record (set during page serve)
|
|
579
|
+
var pending = self._clients.get(clientId);
|
|
580
|
+
var pagePath = pending ? pending.pagePath : '/';
|
|
581
|
+
self._clients.set(clientId, { pagePath: pagePath, client: client });
|
|
582
|
+
|
|
583
|
+
// Keep-alive: send SSE comment periodically
|
|
584
|
+
var keepAlive = setInterval(function() {
|
|
585
|
+
if (!client._closed) {
|
|
586
|
+
try { res.write(':keepalive\n\n'); } catch (e) { /* ignore */ }
|
|
587
|
+
}
|
|
588
|
+
}, self.keepAliveInterval);
|
|
589
|
+
|
|
590
|
+
// Clean up on disconnect
|
|
591
|
+
req.on('close', function() {
|
|
592
|
+
clearInterval(keepAlive);
|
|
593
|
+
client._closed = true;
|
|
594
|
+
self._clients.delete(clientId);
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
// Call the page handler
|
|
598
|
+
var handler = self._pages.get(pagePath);
|
|
599
|
+
if (handler) {
|
|
600
|
+
try {
|
|
601
|
+
handler(client);
|
|
602
|
+
} catch (e) {
|
|
603
|
+
console.error('[bwserve] Page handler error:', e);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Handle an action POST from a client.
|
|
610
|
+
* @private
|
|
611
|
+
*/
|
|
612
|
+
_handleAction(req, res, clientId) {
|
|
613
|
+
var record = this._clients.get(clientId);
|
|
614
|
+
if (!record || !record.client) {
|
|
615
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
616
|
+
res.end(JSON.stringify({ error: 'Unknown client' }));
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
var body = '';
|
|
621
|
+
req.on('data', function(chunk) {
|
|
622
|
+
body += chunk;
|
|
623
|
+
});
|
|
624
|
+
req.on('end', function() {
|
|
625
|
+
try {
|
|
626
|
+
var data = JSON.parse(body);
|
|
627
|
+
var action = data.action;
|
|
628
|
+
var payload = data.data || data;
|
|
629
|
+
record.client._dispatch(action, payload);
|
|
630
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
631
|
+
res.end(JSON.stringify({ ok: true }));
|
|
632
|
+
} catch (e) {
|
|
633
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
634
|
+
res.end(JSON.stringify({ error: e.message }));
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
var index = { create, BwServeApp, BwServeClient };
|
|
641
|
+
|
|
642
|
+
exports.BwServeApp = BwServeApp;
|
|
643
|
+
exports.BwServeClient = BwServeClient;
|
|
644
|
+
exports.create = create;
|
|
645
|
+
exports.default = index;
|
|
646
|
+
//# sourceMappingURL=bwserve.cjs.js.map
|