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.
Files changed (51) hide show
  1. package/README.md +57 -21
  2. package/dist/bitwrench-bccl.cjs.js +3746 -0
  3. package/dist/bitwrench-bccl.cjs.min.js +40 -0
  4. package/dist/bitwrench-bccl.esm.js +3741 -0
  5. package/dist/bitwrench-bccl.esm.min.js +40 -0
  6. package/dist/bitwrench-bccl.umd.js +3752 -0
  7. package/dist/bitwrench-bccl.umd.min.js +40 -0
  8. package/dist/bitwrench-code-edit.cjs.js +57 -7
  9. package/dist/bitwrench-code-edit.cjs.min.js +9 -2
  10. package/dist/bitwrench-code-edit.es5.js +74 -11
  11. package/dist/bitwrench-code-edit.es5.min.js +9 -2
  12. package/dist/bitwrench-code-edit.esm.js +57 -7
  13. package/dist/bitwrench-code-edit.esm.min.js +9 -2
  14. package/dist/bitwrench-code-edit.umd.js +57 -7
  15. package/dist/bitwrench-code-edit.umd.min.js +9 -2
  16. package/dist/bitwrench-lean.cjs.js +413 -17
  17. package/dist/bitwrench-lean.cjs.min.js +7 -7
  18. package/dist/bitwrench-lean.es5.js +428 -16
  19. package/dist/bitwrench-lean.es5.min.js +5 -5
  20. package/dist/bitwrench-lean.esm.js +413 -17
  21. package/dist/bitwrench-lean.esm.min.js +7 -7
  22. package/dist/bitwrench-lean.umd.js +413 -17
  23. package/dist/bitwrench-lean.umd.min.js +7 -7
  24. package/dist/bitwrench.cjs.js +413 -17
  25. package/dist/bitwrench.cjs.min.js +7 -7
  26. package/dist/bitwrench.css +60 -17
  27. package/dist/bitwrench.es5.js +428 -16
  28. package/dist/bitwrench.es5.min.js +6 -6
  29. package/dist/bitwrench.esm.js +413 -17
  30. package/dist/bitwrench.esm.min.js +7 -7
  31. package/dist/bitwrench.min.css +1 -1
  32. package/dist/bitwrench.umd.js +413 -17
  33. package/dist/bitwrench.umd.min.js +7 -7
  34. package/dist/builds.json +168 -80
  35. package/dist/bwserve.cjs.js +646 -0
  36. package/dist/bwserve.esm.js +638 -0
  37. package/dist/sri.json +36 -28
  38. package/package.json +18 -3
  39. package/readme.html +62 -23
  40. package/src/bitwrench-bccl-entry.js +72 -0
  41. package/src/bitwrench-code-edit.js +56 -6
  42. package/src/bitwrench-color-utils.js +5 -6
  43. package/src/bitwrench-styles.js +20 -8
  44. package/src/bitwrench.js +385 -0
  45. package/src/bwserve/client.js +182 -0
  46. package/src/bwserve/index.js +352 -0
  47. package/src/bwserve/shell.js +103 -0
  48. package/src/cli/index.js +36 -15
  49. package/src/cli/serve.js +325 -0
  50. package/src/version.js +3 -3
  51. /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