bitwrench 2.0.15 → 2.0.17

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 (53) hide show
  1. package/README.md +57 -21
  2. package/dist/bitwrench-bccl.cjs.js +3750 -0
  3. package/dist/bitwrench-bccl.cjs.min.js +40 -0
  4. package/dist/bitwrench-bccl.esm.js +3745 -0
  5. package/dist/bitwrench-bccl.esm.min.js +40 -0
  6. package/dist/bitwrench-bccl.umd.js +3756 -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 +905 -157
  17. package/dist/bitwrench-lean.cjs.min.js +7 -7
  18. package/dist/bitwrench-lean.es5.js +931 -157
  19. package/dist/bitwrench-lean.es5.min.js +5 -5
  20. package/dist/bitwrench-lean.esm.js +904 -157
  21. package/dist/bitwrench-lean.esm.min.js +7 -7
  22. package/dist/bitwrench-lean.umd.js +905 -157
  23. package/dist/bitwrench-lean.umd.min.js +7 -7
  24. package/dist/bitwrench.cjs.js +910 -158
  25. package/dist/bitwrench.cjs.min.js +8 -8
  26. package/dist/bitwrench.css +60 -17
  27. package/dist/bitwrench.es5.js +939 -158
  28. package/dist/bitwrench.es5.min.js +6 -6
  29. package/dist/bitwrench.esm.js +909 -158
  30. package/dist/bitwrench.esm.min.js +8 -8
  31. package/dist/bitwrench.min.css +1 -1
  32. package/dist/bitwrench.umd.js +910 -158
  33. package/dist/bitwrench.umd.min.js +8 -8
  34. package/dist/builds.json +168 -80
  35. package/dist/bwserve.cjs.js +660 -0
  36. package/dist/bwserve.esm.js +652 -0
  37. package/dist/sri.json +36 -28
  38. package/package.json +20 -3
  39. package/readme.html +62 -23
  40. package/src/bitwrench-bccl-entry.js +72 -0
  41. package/src/bitwrench-bccl.js +5 -1
  42. package/src/bitwrench-code-edit.js +56 -6
  43. package/src/bitwrench-color-utils.js +5 -6
  44. package/src/bitwrench-styles.js +20 -8
  45. package/src/bitwrench.js +876 -140
  46. package/src/bwserve/client.js +182 -0
  47. package/src/bwserve/index.js +363 -0
  48. package/src/bwserve/shell.js +106 -0
  49. package/src/cli/index.js +36 -15
  50. package/src/cli/layout-default.js +47 -32
  51. package/src/cli/serve.js +325 -0
  52. package/src/version.js +3 -3
  53. /package/bin/{bitwrench.js → bwcli.js} +0 -0
@@ -0,0 +1,652 @@
1
+ /*! bwserve v2.0.17 | 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
+ if (opts.allowExec) {
257
+ script.push(' allowExec: true,');
258
+ }
259
+ script.push(' onStatus: function(s) {');
260
+ script.push(' if (typeof console !== "undefined") console.log("[bwserve] " + s);');
261
+ script.push(' }');
262
+ script.push(' });');
263
+
264
+ // data-bw-action click delegation
265
+ script.push(' document.addEventListener("click", function(e) {');
266
+ script.push(' var el = e.target.closest ? e.target.closest("[data-bw-action]") : null;');
267
+ script.push(' if (!el) return;');
268
+ script.push(' e.preventDefault();');
269
+ script.push(' var actionData = {};');
270
+ script.push(' if (el.getAttribute("data-bw-id")) actionData.bwId = el.getAttribute("data-bw-id");');
271
+ script.push(' var form = el.closest("div") || document;');
272
+ script.push(' var inp = form.querySelector("input[type=text],input:not([type])");');
273
+ script.push(' if (inp) { actionData.inputValue = inp.value; inp.value = ""; }');
274
+ script.push(' conn.sendAction(el.getAttribute("data-bw-action"), actionData);');
275
+ script.push(' });');
276
+
277
+ // Enter key on inputs
278
+ script.push(' document.addEventListener("keydown", function(e) {');
279
+ script.push(' if (e.key === "Enter" && e.target.tagName === "INPUT") {');
280
+ script.push(' var form = e.target.closest("div") || document;');
281
+ script.push(' var btn = form.querySelector("[data-bw-action]");');
282
+ script.push(' if (btn) {');
283
+ script.push(' conn.sendAction(btn.getAttribute("data-bw-action"), { inputValue: e.target.value });');
284
+ script.push(' e.target.value = "";');
285
+ script.push(' }');
286
+ script.push(' }');
287
+ script.push(' });');
288
+
289
+ script.push('})();');
290
+ script.push('</script>');
291
+ script.push('</body>');
292
+ script.push('</html>');
293
+
294
+ return head.concat(script).join('\n');
295
+ }
296
+
297
+ /**
298
+ * bwserve — Server-driven UI library for bitwrench
299
+ *
300
+ * Programmatic API for building server-push UIs (Streamlit-style).
301
+ * Uses SSE (Server-Sent Events) by default, with WebSocket opt-in.
302
+ * Zero runtime dependencies — only Node.js stdlib (http, fs, path).
303
+ *
304
+ * Usage:
305
+ * import bwserve from 'bitwrench/bwserve';
306
+ * const app = bwserve.create({ port: 7902 });
307
+ * app.page('/', (client) => {
308
+ * client.render('#app', bw.makeCard({ title: 'Hello' }));
309
+ * });
310
+ * app.listen();
311
+ *
312
+ * @module bwserve
313
+ */
314
+
315
+
316
+ var __dirname$1 = dirname(fileURLToPath(import.meta.url));
317
+
318
+ // Resolve dist/ — try source layout (src/bwserve/), then npm install layout,
319
+ // then dist/ itself (when running from dist/bwserve.esm.js)
320
+ var DIST_DIR = resolve(__dirname$1, '..', '..', 'dist');
321
+ if (!existsSync(DIST_DIR)) {
322
+ DIST_DIR = resolve(__dirname$1, '..', 'dist');
323
+ }
324
+ if (!existsSync(DIST_DIR)) {
325
+ DIST_DIR = __dirname$1;
326
+ }
327
+
328
+ // MIME type lookup for static file serving
329
+ var MIME_TYPES = {
330
+ '.html': 'text/html; charset=utf-8',
331
+ '.js': 'application/javascript; charset=utf-8',
332
+ '.css': 'text/css; charset=utf-8',
333
+ '.json': 'application/json; charset=utf-8',
334
+ '.png': 'image/png',
335
+ '.jpg': 'image/jpeg',
336
+ '.jpeg': 'image/jpeg',
337
+ '.gif': 'image/gif',
338
+ '.svg': 'image/svg+xml',
339
+ '.ico': 'image/x-icon',
340
+ '.woff': 'font/woff',
341
+ '.woff2': 'font/woff2',
342
+ '.ttf': 'font/ttf',
343
+ '.map': 'application/json'
344
+ };
345
+
346
+ /**
347
+ * Create a bwserve application.
348
+ *
349
+ * @param {Object} opts - Server options
350
+ * @param {number} [opts.port=7902] - Port to listen on
351
+ * @param {string} [opts.title='bwserve'] - Page title
352
+ * @param {string} [opts.static] - Directory to serve static files from
353
+ * @param {boolean} [opts.injectBitwrench=true] - Auto-inject bitwrench client JS
354
+ * @param {string|Object} [opts.theme] - Theme preset name or config object
355
+ * @returns {BwServeApp} Application instance
356
+ */
357
+ function create(opts) {
358
+ return new BwServeApp(opts || {});
359
+ }
360
+
361
+ /**
362
+ * BwServeApp — the server application object.
363
+ *
364
+ * Manages pages, client connections, and the HTTP/SSE server.
365
+ */
366
+ class BwServeApp {
367
+ constructor(opts) {
368
+ this.port = opts.port || 7902;
369
+ this.title = opts.title || 'bwserve';
370
+ this.staticDir = opts.static || null;
371
+ this.injectBitwrench = opts.injectBitwrench !== false;
372
+ this.theme = opts.theme || null;
373
+ this.allowExec = opts.allowExec || false;
374
+ this.keepAliveInterval = opts.keepAliveInterval || 15000;
375
+ this._pages = new Map();
376
+ this._clients = new Map();
377
+ this._server = null;
378
+ this._clientCounter = 0;
379
+ }
380
+
381
+ /**
382
+ * Register a page handler.
383
+ *
384
+ * @param {string} path - URL path (e.g., '/', '/dashboard')
385
+ * @param {Function} handler - Called with (client: BwServeClient) on connection
386
+ * @returns {BwServeApp} this (for chaining)
387
+ */
388
+ page(path, handler) {
389
+ this._pages.set(path, handler);
390
+ return this;
391
+ }
392
+
393
+ /**
394
+ * Start the HTTP server and begin accepting SSE connections.
395
+ *
396
+ * @param {Function} [callback] - Called when server is listening
397
+ * @returns {Promise<void>}
398
+ */
399
+ listen(callback) {
400
+ var self = this;
401
+
402
+ return new Promise(function(res) {
403
+ self._server = createServer(function(req, rawRes) {
404
+ self._handleRequest(req, rawRes);
405
+ });
406
+
407
+ self._server.listen(self.port, function() {
408
+ if (callback) callback();
409
+ res();
410
+ });
411
+ });
412
+ }
413
+
414
+ /**
415
+ * Stop the server and close all client connections.
416
+ */
417
+ close() {
418
+ var self = this;
419
+ return new Promise(function(res) {
420
+ // Close all SSE streams
421
+ for (var record of self._clients.values()) {
422
+ if (record.client && typeof record.client.close === 'function') {
423
+ record.client.close();
424
+ }
425
+ }
426
+ self._clients.clear();
427
+
428
+ if (self._server) {
429
+ self._server.close(function() {
430
+ self._server = null;
431
+ res();
432
+ });
433
+ } else {
434
+ res();
435
+ }
436
+ });
437
+ }
438
+
439
+ /**
440
+ * Get count of active client connections.
441
+ * @returns {number}
442
+ */
443
+ get clientCount() {
444
+ return this._clients.size;
445
+ }
446
+
447
+ /**
448
+ * Broadcast a protocol message to all connected clients.
449
+ *
450
+ * If msg has a clientId field, send only to that client.
451
+ * Otherwise, broadcast to all.
452
+ *
453
+ * @param {Object} msg - Protocol message (replace, patch, append, remove, batch)
454
+ * @returns {number} Number of clients that received the message
455
+ */
456
+ broadcast(msg) {
457
+ if (msg.clientId) {
458
+ var record = this._clients.get(msg.clientId);
459
+ if (record && record.client) {
460
+ record.client._send(msg);
461
+ return 1;
462
+ }
463
+ return 0;
464
+ }
465
+ var count = 0;
466
+ for (var record of this._clients.values()) {
467
+ if (record.client && !record.client._closed) {
468
+ record.client._send(msg);
469
+ count++;
470
+ }
471
+ }
472
+ return count;
473
+ }
474
+
475
+ /**
476
+ * Internal: route incoming HTTP requests.
477
+ * @private
478
+ */
479
+ _handleRequest(req, res) {
480
+ var url = req.url || '/';
481
+ var method = req.method || 'GET';
482
+
483
+ // Parse URL path (strip query string)
484
+ var path = url.split('?')[0];
485
+
486
+ // /__bw/bitwrench.umd.js — serve bitwrench client library
487
+ if (path === '/__bw/bitwrench.umd.js' && method === 'GET') {
488
+ return this._serveDistFile(res, 'bitwrench.umd.js');
489
+ }
490
+
491
+ // /__bw/bitwrench.umd.min.js — serve minified
492
+ if (path === '/__bw/bitwrench.umd.min.js' && method === 'GET') {
493
+ return this._serveDistFile(res, 'bitwrench.umd.min.js');
494
+ }
495
+
496
+ // /__bw/bitwrench.css — serve bitwrench CSS
497
+ if (path === '/__bw/bitwrench.css' && method === 'GET') {
498
+ return this._serveDistFile(res, 'bitwrench.css');
499
+ }
500
+
501
+ // /__bw/events/:clientId — SSE stream
502
+ if (path.startsWith('/__bw/events/') && method === 'GET') {
503
+ var clientId = path.slice('/__bw/events/'.length);
504
+ return this._handleSSE(req, res, clientId);
505
+ }
506
+
507
+ // /__bw/action/:clientId — action POST
508
+ if (path.startsWith('/__bw/action/') && method === 'POST') {
509
+ var actionClientId = path.slice('/__bw/action/'.length);
510
+ return this._handleAction(req, res, actionClientId);
511
+ }
512
+
513
+ // Registered page routes — serve shell HTML
514
+ if (method === 'GET' && this._pages.has(path)) {
515
+ var clientId2 = 'c' + (++this._clientCounter);
516
+ var shell = generateShell({
517
+ clientId: clientId2,
518
+ title: this.title,
519
+ theme: this.theme,
520
+ injectBitwrench: this.injectBitwrench,
521
+ allowExec: this.allowExec
522
+ });
523
+ // Store the page path for this client so SSE knows which handler to call
524
+ this._clients.set(clientId2, { pagePath: path, client: null });
525
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
526
+ res.end(shell);
527
+ return;
528
+ }
529
+
530
+ // Static file serving
531
+ if (method === 'GET' && this.staticDir) {
532
+ var filePath = join(this.staticDir, path);
533
+ if (existsSync(filePath) && statSync(filePath).isFile()) {
534
+ var ext = extname(filePath);
535
+ var mime = MIME_TYPES[ext] || 'application/octet-stream';
536
+ var content = readFileSync(filePath);
537
+ res.writeHead(200, { 'Content-Type': mime });
538
+ res.end(content);
539
+ return;
540
+ }
541
+ }
542
+
543
+ // 404
544
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
545
+ res.end('Not Found');
546
+ }
547
+
548
+ /**
549
+ * Serve a file from the dist/ directory.
550
+ * @private
551
+ */
552
+ _serveDistFile(res, filename) {
553
+ var filePath = join(DIST_DIR, filename);
554
+ if (!existsSync(filePath)) {
555
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
556
+ res.end('Not Found: ' + filename);
557
+ return;
558
+ }
559
+ var ext = extname(filename);
560
+ var mime = MIME_TYPES[ext] || 'application/octet-stream';
561
+ var content = readFileSync(filePath);
562
+ res.writeHead(200, {
563
+ 'Content-Type': mime,
564
+ 'Cache-Control': 'public, max-age=3600'
565
+ });
566
+ res.end(content);
567
+ }
568
+
569
+ /**
570
+ * Handle an SSE connection.
571
+ * @private
572
+ */
573
+ _handleSSE(req, res, clientId) {
574
+ var self = this;
575
+
576
+ // Set SSE headers
577
+ res.writeHead(200, {
578
+ 'Content-Type': 'text/event-stream',
579
+ 'Cache-Control': 'no-cache',
580
+ 'Connection': 'keep-alive',
581
+ 'Access-Control-Allow-Origin': '*'
582
+ });
583
+
584
+ // Create client instance
585
+ var client = new BwServeClient(clientId, res);
586
+
587
+ // Look up the pending client record (set during page serve)
588
+ var pending = self._clients.get(clientId);
589
+ var pagePath = pending ? pending.pagePath : '/';
590
+ self._clients.set(clientId, { pagePath: pagePath, client: client });
591
+
592
+ // Keep-alive: send SSE comment periodically
593
+ var keepAlive = setInterval(function() {
594
+ if (!client._closed) {
595
+ try { res.write(':keepalive\n\n'); } catch (e) { /* ignore */ }
596
+ }
597
+ }, self.keepAliveInterval);
598
+
599
+ // Clean up on disconnect
600
+ req.on('close', function() {
601
+ clearInterval(keepAlive);
602
+ client._closed = true;
603
+ self._clients.delete(clientId);
604
+ });
605
+
606
+ // Call the page handler
607
+ var handler = self._pages.get(pagePath);
608
+ if (handler) {
609
+ try {
610
+ handler(client);
611
+ } catch (e) {
612
+ console.error('[bwserve] Page handler error:', e);
613
+ }
614
+ }
615
+ }
616
+
617
+ /**
618
+ * Handle an action POST from a client.
619
+ * @private
620
+ */
621
+ _handleAction(req, res, clientId) {
622
+ var record = this._clients.get(clientId);
623
+ if (!record || !record.client) {
624
+ res.writeHead(404, { 'Content-Type': 'application/json' });
625
+ res.end(JSON.stringify({ error: 'Unknown client' }));
626
+ return;
627
+ }
628
+
629
+ var body = '';
630
+ req.on('data', function(chunk) {
631
+ body += chunk;
632
+ });
633
+ req.on('end', function() {
634
+ try {
635
+ var data = JSON.parse(body);
636
+ var action = data.action;
637
+ var payload = data.data || data;
638
+ record.client._dispatch(action, payload);
639
+ res.writeHead(200, { 'Content-Type': 'application/json' });
640
+ res.end(JSON.stringify({ ok: true }));
641
+ } catch (e) {
642
+ res.writeHead(400, { 'Content-Type': 'application/json' });
643
+ res.end(JSON.stringify({ error: e.message }));
644
+ }
645
+ });
646
+ }
647
+ }
648
+
649
+ var index = { create, BwServeApp, BwServeClient };
650
+
651
+ export { BwServeApp, BwServeClient, create, index as default };
652
+ //# sourceMappingURL=bwserve.esm.js.map