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
@@ -1,4 +1,4 @@
1
- /*! bitwrench v2.0.15 | BSD-2-Clause | https://deftio.github.com/bitwrench/pages */
1
+ /*! bitwrench v2.0.16 | BSD-2-Clause | https://deftio.github.com/bitwrench/pages */
2
2
  (function (global, factory) {
3
3
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
4
4
  typeof define === 'function' && define.amd ? define(factory) :
@@ -11,14 +11,14 @@
11
11
  */
12
12
 
13
13
  const VERSION_INFO = {
14
- version: '2.0.15',
14
+ version: '2.0.16',
15
15
  name: 'bitwrench',
16
16
  description: 'A library for javascript UI functions.',
17
17
  license: 'BSD-2-Clause',
18
18
  homepage: 'https://deftio.github.com/bitwrench/pages',
19
19
  repository: 'git+https://github.com/deftio/bitwrench.git',
20
20
  author: 'manu a. chatterjee <deftio@deftio.com> (https://deftio.com/)',
21
- buildDate: '2026-03-10T09:08:17.015Z'
21
+ buildDate: '2026-03-12T08:05:52.043Z'
22
22
  };
23
23
 
24
24
  /**
@@ -436,12 +436,11 @@
436
436
  var lightBase = config.light || hslToHex([h, 8, 97]);
437
437
  var darkBase = config.dark || hslToHex([h, 10, 13]);
438
438
 
439
- // Background & surface tokens — default to light (white/near-white).
440
- // Dark backgrounds require explicit config.background / config.surface.
441
- // Primary/secondary colors are accents, not page backgrounds, so
442
- // isLightPalette should NOT drive bg/surface defaults.
443
- var bgBase = config.background || '#ffffff';
444
- var surfBase = config.surface || '#f8f9fa';
439
+ // Background & surface tokens — tinted with primary hue for theme personality.
440
+ // Very subtle: bg at L=98/S=6, surface at L=96/S=8.
441
+ // User can override with config.background / config.surface.
442
+ var bgBase = config.background || hslToHex([h, 6, 98]);
443
+ var surfBase = config.surface || hslToHex([h, 8, 96]);
445
444
 
446
445
  var palette = {
447
446
  primary: deriveShades(config.primary),
@@ -1574,7 +1573,7 @@
1574
1573
  '@media (min-width: 992px)': { '.bw_container': { 'max-width': '960px' } },
1575
1574
  '@media (min-width: 1200px)': { '.bw_container': { 'max-width': '1140px' } },
1576
1575
  '.bw_container_fluid': {
1577
- 'width': '100%', 'padding-right': '15px', 'padding-left': '15px',
1576
+ 'width': '100%', 'padding-right': '0.75rem', 'padding-left': '0.75rem',
1578
1577
  'margin-right': 'auto', 'margin-left': 'auto'
1579
1578
  },
1580
1579
  '.bw_row': {
@@ -1735,7 +1734,8 @@
1735
1734
  '.bw_badge': {
1736
1735
  'display': 'inline-block', 'font-size': '0.875rem',
1737
1736
  'font-weight': '600', 'line-height': '1.3', 'text-align': 'center',
1738
- 'white-space': 'nowrap', 'vertical-align': 'baseline'
1737
+ 'white-space': 'nowrap', 'vertical-align': 'baseline',
1738
+ 'padding': '0.35rem 0.65rem', 'border-radius': '0.25rem'
1739
1739
  },
1740
1740
  '.bw_badge:empty': { 'display': 'none' },
1741
1741
  '.bw_badge_sm': { 'font-size': '0.75rem', 'padding': '0.25rem 0.5rem' },
@@ -1920,7 +1920,7 @@
1920
1920
  // ---- Code demo ----
1921
1921
  codeDemo: {
1922
1922
  '.bw_code_demo': { 'margin-bottom': '2rem' },
1923
- '.bw_code_pre': { 'margin': '0', 'border': 'none', 'overflow-x': 'auto' },
1923
+ '.bw_code_pre': { 'margin': '0', 'border': 'none', 'overflow-x': 'auto', 'max-width': '100%' },
1924
1924
  '.bw_code_block': {
1925
1925
  'display': 'block', 'padding': '1.25rem',
1926
1926
  'font-family': '"SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, "Courier New", monospace',
@@ -2017,7 +2017,7 @@
2017
2017
  },
2018
2018
  '.bw_modal.bw_modal_show': { 'opacity': '1', 'visibility': 'visible', 'pointer-events': 'auto' },
2019
2019
  '.bw_modal_dialog': {
2020
- 'position': 'relative', 'width': '100%', 'max-width': '500px', 'margin': '1.75rem auto',
2020
+ 'position': 'relative', 'width': 'calc(100% - 1rem)', 'max-width': '500px', 'margin': '1.75rem auto',
2021
2021
  'pointer-events': 'none'
2022
2022
  },
2023
2023
  '.bw_modal.bw_modal_show .bw_modal_dialog': { 'transform': 'translateY(0)' },
@@ -2047,7 +2047,7 @@
2047
2047
  '.bw_toast_container.bw_toast_top_center': { 'top': '0', 'left': '50%', 'transform': 'translateX(-50%)' },
2048
2048
  '.bw_toast_container.bw_toast_bottom_center': { 'bottom': '0', 'left': '50%', 'transform': 'translateX(-50%)' },
2049
2049
  '.bw_toast': {
2050
- 'pointer-events': 'auto', 'width': '350px', 'max-width': '100%', 'background-clip': 'padding-box',
2050
+ 'pointer-events': 'auto', 'width': '350px', 'max-width': 'calc(100vw - 2rem)', 'background-clip': 'padding-box',
2051
2051
  'opacity': '0'
2052
2052
  },
2053
2053
  '.bw_toast.bw_toast_show': { 'opacity': '1', 'transform': 'translateY(0)' },
@@ -2133,7 +2133,7 @@
2133
2133
  '.bw_tooltip_wrapper': { 'position': 'relative', 'display': 'inline-block' },
2134
2134
  '.bw_tooltip': {
2135
2135
  'position': 'absolute', 'z-index': '999',
2136
- 'font-size': '0.875rem', 'white-space': 'nowrap', 'pointer-events': 'none',
2136
+ 'font-size': '0.875rem', 'white-space': 'nowrap', 'max-width': 'min(300px, calc(100vw - 1rem))', 'pointer-events': 'none',
2137
2137
  'opacity': '0', 'visibility': 'hidden'
2138
2138
  },
2139
2139
  '.bw_tooltip.bw_tooltip_show': { 'opacity': '1', 'visibility': 'visible' },
@@ -2153,7 +2153,7 @@
2153
2153
  '.bw_popover_trigger': { 'cursor': 'pointer' },
2154
2154
  '.bw_popover': {
2155
2155
  'position': 'absolute', 'z-index': '1000',
2156
- 'min-width': '200px', 'max-width': '320px',
2156
+ 'min-width': '200px', 'max-width': 'min(320px, calc(100vw - 2rem))',
2157
2157
  'pointer-events': 'none', 'opacity': '0', 'visibility': 'hidden'
2158
2158
  },
2159
2159
  '.bw_popover.bw_popover_show': { 'opacity': '1', 'visibility': 'visible', 'pointer-events': 'auto' },
@@ -2336,7 +2336,18 @@
2336
2336
  '.bw_hero, .bw_hero': { 'padding': '2rem 1rem' },
2337
2337
  '.bw_cta_actions, .bw_cta-actions': { 'flex-direction': 'column' },
2338
2338
  '.bw_hstack, .bw_hstack': { 'flex-direction': 'column' },
2339
- '.bw_feature_grid, .bw_feature-grid': { 'grid-template-columns': '1fr' }
2339
+ '.bw_feature_grid, .bw_feature-grid': { 'grid-template-columns': '1fr' },
2340
+ '.bw_modal_dialog': { 'margin': '0.5rem auto' },
2341
+ '.bw_modal_lg': { 'max-width': 'calc(100% - 1rem)' },
2342
+ '.bw_modal_xl': { 'max-width': 'calc(100% - 1rem)' },
2343
+ '.bw_navbar': { 'padding': '0.5rem 0.75rem' },
2344
+ '.bw_navbar_brand': { 'margin-right': '0.5rem', 'font-size': '1rem' },
2345
+ '.bw_navbar_nav': { 'flex-wrap': 'wrap' },
2346
+ '.bw_tooltip': { 'white-space': 'normal' },
2347
+ '.bw_table': { 'display': 'block', 'overflow-x': 'auto', '-webkit-overflow-scrolling': 'touch' },
2348
+ '.bw_col, .bw_col_1, .bw_col_2, .bw_col_3, .bw_col_4, .bw_col_5, .bw_col_6, .bw_col_7, .bw_col_8, .bw_col_9, .bw_col_10, .bw_col_11, .bw_col_12': { 'flex': '0 0 100%', 'max-width': '100%' },
2349
+ '.bw_container': { 'padding-right': '0.5rem', 'padding-left': '0.5rem' },
2350
+ '.bw_container_fluid': { 'padding-right': '0.5rem', 'padding-left': '0.5rem' }
2340
2351
  }
2341
2352
  }
2342
2353
  };
@@ -9489,6 +9500,391 @@
9489
9500
  return true;
9490
9501
  };
9491
9502
 
9503
+ // ===================================================================================
9504
+ // bw.clientApply() / bw.clientConnect() — Server-driven UI protocol
9505
+ // ===================================================================================
9506
+
9507
+ /**
9508
+ * Registry of named functions sent via register messages.
9509
+ * Populated by clientApply({ type: 'register', name, body }).
9510
+ * Invoked by clientApply({ type: 'call', name, args }).
9511
+ * @private
9512
+ */
9513
+ bw._clientFunctions = {};
9514
+
9515
+ /**
9516
+ * Whether exec messages are allowed. Set by clientConnect opts.allowExec.
9517
+ * Default false — exec messages are rejected unless explicitly opted in.
9518
+ * @private
9519
+ */
9520
+ bw._allowExec = false;
9521
+
9522
+ /**
9523
+ * Built-in client functions available via call() without registration.
9524
+ * @private
9525
+ */
9526
+ bw._builtinClientFunctions = {
9527
+ scrollTo: function(selector) {
9528
+ var el = bw._el(selector);
9529
+ if (el) el.scrollTop = el.scrollHeight;
9530
+ },
9531
+ focus: function(selector) {
9532
+ var el = bw._el(selector);
9533
+ if (el && typeof el.focus === 'function') el.focus();
9534
+ },
9535
+ download: function(filename, content, mimeType) {
9536
+ if (typeof document === 'undefined') return;
9537
+ var blob = new Blob([content], { type: mimeType || 'text/plain' });
9538
+ var a = document.createElement('a');
9539
+ a.href = URL.createObjectURL(blob);
9540
+ a.download = filename;
9541
+ a.click();
9542
+ URL.revokeObjectURL(a.href);
9543
+ },
9544
+ clipboard: function(text) {
9545
+ if (typeof navigator !== 'undefined' && navigator.clipboard) {
9546
+ navigator.clipboard.writeText(text);
9547
+ }
9548
+ },
9549
+ redirect: function(url) {
9550
+ if (typeof window !== 'undefined') window.location.href = url;
9551
+ },
9552
+ log: function() {
9553
+ console.log.apply(console, arguments);
9554
+ }
9555
+ };
9556
+
9557
+ /**
9558
+ * Parse a bwserve protocol message string, supporting both strict JSON
9559
+ * and r-prefixed relaxed JSON (single-quoted strings, trailing commas).
9560
+ *
9561
+ * The r-prefix format is designed for C/C++ string literals where
9562
+ * double-quote escaping is painful. The parser is a state machine
9563
+ * that walks character by character — not a regex replace.
9564
+ *
9565
+ * Escaping: apostrophes inside single-quoted values must be escaped
9566
+ * with backslash: r{'name':'Barry\'s room'}
9567
+ *
9568
+ * @param {string} str - JSON or r-prefixed relaxed JSON string
9569
+ * @returns {Object} Parsed message object
9570
+ * @throws {SyntaxError} If the string is not valid JSON or relaxed JSON
9571
+ * @category Server
9572
+ */
9573
+ bw.clientParse = function(str) {
9574
+ str = (str || '').trim();
9575
+ if (str.charAt(0) !== 'r') return JSON.parse(str);
9576
+ str = str.slice(1);
9577
+
9578
+ var out = [];
9579
+ var i = 0;
9580
+ var len = str.length;
9581
+
9582
+ while (i < len) {
9583
+ var ch = str[i];
9584
+
9585
+ if (ch === "'") {
9586
+ // Single-quoted string → emit as double-quoted
9587
+ out.push('"');
9588
+ i++;
9589
+ while (i < len) {
9590
+ var c = str[i];
9591
+ if (c === '\\' && i + 1 < len) {
9592
+ var next = str[i + 1];
9593
+ if (next === "'") {
9594
+ out.push("'"); // \' in input → ' in output
9595
+ } else {
9596
+ out.push('\\');
9597
+ out.push(next);
9598
+ }
9599
+ i += 2;
9600
+ } else if (c === '"') {
9601
+ out.push('\\"');
9602
+ i++;
9603
+ } else if (c === "'") {
9604
+ break;
9605
+ } else {
9606
+ out.push(c);
9607
+ i++;
9608
+ }
9609
+ }
9610
+ out.push('"');
9611
+ i++; // skip closing '
9612
+
9613
+ } else if (ch === '"') {
9614
+ // Double-quoted string — pass through verbatim
9615
+ out.push(ch);
9616
+ i++;
9617
+ while (i < len) {
9618
+ var c2 = str[i];
9619
+ if (c2 === '\\' && i + 1 < len) {
9620
+ out.push(c2);
9621
+ out.push(str[i + 1]);
9622
+ i += 2;
9623
+ } else {
9624
+ out.push(c2);
9625
+ i++;
9626
+ if (c2 === '"') break;
9627
+ }
9628
+ }
9629
+
9630
+ } else if (ch === ',') {
9631
+ // Trailing comma check: skip comma if next non-whitespace is } or ]
9632
+ var j = i + 1;
9633
+ while (j < len && (str[j] === ' ' || str[j] === '\t' || str[j] === '\n' || str[j] === '\r')) j++;
9634
+ if (j < len && (str[j] === '}' || str[j] === ']')) {
9635
+ i++; // skip trailing comma
9636
+ } else {
9637
+ out.push(ch);
9638
+ i++;
9639
+ }
9640
+
9641
+ } else {
9642
+ out.push(ch);
9643
+ i++;
9644
+ }
9645
+ }
9646
+
9647
+ return JSON.parse(out.join(''));
9648
+ };
9649
+
9650
+ /**
9651
+ * Apply a bwserve protocol message to the DOM.
9652
+ *
9653
+ * Dispatches one of 9 message types:
9654
+ * replace — bw.DOM(target, node)
9655
+ * append — target.appendChild(bw.createDOM(node))
9656
+ * remove — bw.cleanup(target); target.remove()
9657
+ * patch — bw.patch(target, content, attr)
9658
+ * batch — iterate ops, call clientApply for each
9659
+ * message — bw.message(target, action, data)
9660
+ * register — store a named function for later call()
9661
+ * call — invoke a registered or built-in function
9662
+ * exec — execute arbitrary JS (requires allowExec)
9663
+ *
9664
+ * Target resolution:
9665
+ * Starts with '#' or '.' → CSS selector (querySelector)
9666
+ * Otherwise → getElementById, then bw._el fallback
9667
+ *
9668
+ * @param {Object} msg - Protocol message
9669
+ * @returns {boolean} true if the message was applied successfully
9670
+ * @category Server
9671
+ */
9672
+ bw.clientApply = function(msg) {
9673
+ if (!msg || !msg.type) return false;
9674
+
9675
+ var type = msg.type;
9676
+ var target = msg.target;
9677
+
9678
+ if (type === 'replace') {
9679
+ var el = bw._el(target);
9680
+ if (!el) return false;
9681
+ bw.DOM(el, msg.node);
9682
+ return true;
9683
+
9684
+ } else if (type === 'patch') {
9685
+ var patched = bw.patch(target, msg.content, msg.attr);
9686
+ return patched !== null;
9687
+
9688
+ } else if (type === 'append') {
9689
+ var parent = bw._el(target);
9690
+ if (!parent) return false;
9691
+ var child = bw.createDOM(msg.node);
9692
+ parent.appendChild(child);
9693
+ return true;
9694
+
9695
+ } else if (type === 'remove') {
9696
+ var toRemove = bw._el(target);
9697
+ if (!toRemove) return false;
9698
+ if (typeof bw.cleanup === 'function') bw.cleanup(toRemove);
9699
+ toRemove.remove();
9700
+ return true;
9701
+
9702
+ } else if (type === 'batch') {
9703
+ if (!Array.isArray(msg.ops)) return false;
9704
+ var allOk = true;
9705
+ msg.ops.forEach(function(op) {
9706
+ if (!bw.clientApply(op)) allOk = false;
9707
+ });
9708
+ return allOk;
9709
+
9710
+ } else if (type === 'message') {
9711
+ return bw.message(msg.target, msg.action, msg.data);
9712
+
9713
+ } else if (type === 'register') {
9714
+ if (!msg.name || !msg.body) return false;
9715
+ try {
9716
+ bw._clientFunctions[msg.name] = new Function('return ' + msg.body)();
9717
+ return true;
9718
+ } catch (e) {
9719
+ console.error('[bw] register error:', msg.name, e);
9720
+ return false;
9721
+ }
9722
+
9723
+ } else if (type === 'call') {
9724
+ if (!msg.name) return false;
9725
+ var fn = bw._clientFunctions[msg.name] || bw._builtinClientFunctions[msg.name];
9726
+ if (typeof fn !== 'function') return false;
9727
+ try {
9728
+ var args = Array.isArray(msg.args) ? msg.args : [];
9729
+ fn.apply(null, args);
9730
+ return true;
9731
+ } catch (e) {
9732
+ console.error('[bw] call error:', msg.name, e);
9733
+ return false;
9734
+ }
9735
+
9736
+ } else if (type === 'exec') {
9737
+ if (!bw._allowExec) {
9738
+ console.warn('[bw] exec rejected: allowExec is not enabled');
9739
+ return false;
9740
+ }
9741
+ if (!msg.code) return false;
9742
+ try {
9743
+ new Function(msg.code)();
9744
+ return true;
9745
+ } catch (e) {
9746
+ console.error('[bw] exec error:', e);
9747
+ return false;
9748
+ }
9749
+ }
9750
+
9751
+ return false;
9752
+ };
9753
+
9754
+ /**
9755
+ * Connect to a bwserve SSE endpoint and apply protocol messages automatically.
9756
+ *
9757
+ * Returns a connection object with sendAction(), on(), and close() methods.
9758
+ *
9759
+ * @param {string} url - SSE endpoint URL (e.g., '/__bw/events/client-1')
9760
+ * @param {Object} [opts] - Connection options
9761
+ * @param {string} [opts.transport='sse'] - Transport type: 'sse' (default) or 'poll'
9762
+ * @param {number} [opts.interval=2000] - Poll interval in ms (only for 'poll' transport)
9763
+ * @param {string} [opts.actionUrl] - POST endpoint for actions (default: derived from url)
9764
+ * @param {boolean} [opts.reconnect=true] - Auto-reconnect on disconnect
9765
+ * @param {boolean} [opts.allowExec=false] - Enable exec message type (arbitrary JS execution)
9766
+ * @param {Function} [opts.onStatus] - Status callback: 'connecting'|'connected'|'disconnected'
9767
+ * @param {Function} [opts.onMessage] - Raw message callback (before clientApply)
9768
+ * @returns {Object} Connection object { sendAction, on, close, status }
9769
+ * @category Server
9770
+ */
9771
+ bw.clientConnect = function(url, opts) {
9772
+ opts = opts || {};
9773
+ var transport = opts.transport || 'sse';
9774
+ var actionUrl = opts.actionUrl || url.replace(/\/events\//, '/action/');
9775
+ var reconnect = opts.reconnect !== false;
9776
+ var onStatus = opts.onStatus || function() {};
9777
+ var onMessage = opts.onMessage || null;
9778
+ var handlers = {};
9779
+ // Set the global allowExec flag from connection options
9780
+ bw._allowExec = !!opts.allowExec;
9781
+ var conn = {
9782
+ status: 'connecting',
9783
+ _es: null,
9784
+ _pollTimer: null
9785
+ };
9786
+
9787
+ function setStatus(s) {
9788
+ conn.status = s;
9789
+ onStatus(s);
9790
+ }
9791
+
9792
+ function handleMessage(data) {
9793
+ try {
9794
+ var msg = typeof data === 'string' ? bw.clientParse(data) : data;
9795
+ if (onMessage) onMessage(msg);
9796
+ if (handlers.message) handlers.message(msg);
9797
+ bw.clientApply(msg);
9798
+ } catch (e) {
9799
+ if (handlers.error) handlers.error(e);
9800
+ }
9801
+ }
9802
+
9803
+ if (transport === 'sse' && typeof EventSource !== 'undefined') {
9804
+ setStatus('connecting');
9805
+ var es = new EventSource(url);
9806
+ conn._es = es;
9807
+
9808
+ es.onopen = function() {
9809
+ setStatus('connected');
9810
+ if (handlers.open) handlers.open();
9811
+ };
9812
+
9813
+ es.onmessage = function(e) {
9814
+ handleMessage(e.data);
9815
+ };
9816
+
9817
+ es.onerror = function() {
9818
+ if (conn.status === 'connected') {
9819
+ setStatus('disconnected');
9820
+ }
9821
+ if (handlers.error) handlers.error(new Error('SSE connection error'));
9822
+ if (!reconnect) {
9823
+ es.close();
9824
+ }
9825
+ // EventSource auto-reconnects by default when reconnect=true
9826
+ };
9827
+ } else if (transport === 'poll') {
9828
+ var interval = opts.interval || 2000;
9829
+ setStatus('connected');
9830
+ conn._pollTimer = setInterval(function() {
9831
+ fetch(url).then(function(r) { return r.json(); }).then(function(msgs) {
9832
+ if (Array.isArray(msgs)) {
9833
+ msgs.forEach(handleMessage);
9834
+ } else if (msgs && msgs.type) {
9835
+ handleMessage(msgs);
9836
+ }
9837
+ }).catch(function(e) {
9838
+ if (handlers.error) handlers.error(e);
9839
+ });
9840
+ }, interval);
9841
+ }
9842
+
9843
+ /**
9844
+ * Send an action to the server via POST.
9845
+ * @param {string} action - Action name
9846
+ * @param {Object} [data] - Action payload
9847
+ */
9848
+ conn.sendAction = function(action, data) {
9849
+ var body = JSON.stringify({ type: 'action', action: action, data: data || {} });
9850
+ fetch(actionUrl, {
9851
+ method: 'POST',
9852
+ headers: { 'Content-Type': 'application/json' },
9853
+ body: body
9854
+ }).catch(function(e) {
9855
+ if (handlers.error) handlers.error(e);
9856
+ });
9857
+ };
9858
+
9859
+ /**
9860
+ * Register an event handler.
9861
+ * @param {string} event - 'open'|'message'|'error'|'close'
9862
+ * @param {Function} handler
9863
+ */
9864
+ conn.on = function(event, handler) {
9865
+ handlers[event] = handler;
9866
+ return conn;
9867
+ };
9868
+
9869
+ /**
9870
+ * Close the connection.
9871
+ */
9872
+ conn.close = function() {
9873
+ if (conn._es) {
9874
+ conn._es.close();
9875
+ conn._es = null;
9876
+ }
9877
+ if (conn._pollTimer) {
9878
+ clearInterval(conn._pollTimer);
9879
+ conn._pollTimer = null;
9880
+ }
9881
+ setStatus('disconnected');
9882
+ if (handlers.close) handlers.close();
9883
+ };
9884
+
9885
+ return conn;
9886
+ };
9887
+
9492
9888
  // ===================================================================================
9493
9889
  // bw.inspect() — Debug utility
9494
9890
  // ===================================================================================