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