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