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