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.es5.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
|
(function (global, factory) {
|
|
3
3
|
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
|
4
4
|
typeof define === 'function' && define.amd ? define(factory) :
|
|
@@ -189,14 +189,14 @@
|
|
|
189
189
|
*/
|
|
190
190
|
|
|
191
191
|
var VERSION_INFO = {
|
|
192
|
-
version: '2.0.
|
|
192
|
+
version: '2.0.16',
|
|
193
193
|
name: 'bitwrench',
|
|
194
194
|
description: 'A library for javascript UI functions.',
|
|
195
195
|
license: 'BSD-2-Clause',
|
|
196
196
|
homepage: 'https://deftio.github.com/bitwrench/pages',
|
|
197
197
|
repository: 'git+https://github.com/deftio/bitwrench.git',
|
|
198
198
|
author: 'manu a. chatterjee <deftio@deftio.com> (https://deftio.com/)',
|
|
199
|
-
buildDate: '2026-03-
|
|
199
|
+
buildDate: '2026-03-12T08:05:52.043Z'
|
|
200
200
|
};
|
|
201
201
|
|
|
202
202
|
/**
|
|
@@ -609,12 +609,11 @@
|
|
|
609
609
|
var lightBase = config.light || hslToHex([h, 8, 97]);
|
|
610
610
|
var darkBase = config.dark || hslToHex([h, 10, 13]);
|
|
611
611
|
|
|
612
|
-
// Background & surface tokens —
|
|
613
|
-
//
|
|
614
|
-
//
|
|
615
|
-
|
|
616
|
-
var
|
|
617
|
-
var surfBase = config.surface || '#f8f9fa';
|
|
612
|
+
// Background & surface tokens — tinted with primary hue for theme personality.
|
|
613
|
+
// Very subtle: bg at L=98/S=6, surface at L=96/S=8.
|
|
614
|
+
// User can override with config.background / config.surface.
|
|
615
|
+
var bgBase = config.background || hslToHex([h, 6, 98]);
|
|
616
|
+
var surfBase = config.surface || hslToHex([h, 8, 96]);
|
|
618
617
|
var palette = {
|
|
619
618
|
primary: deriveShades(config.primary),
|
|
620
619
|
secondary: deriveShades(config.secondary),
|
|
@@ -1857,8 +1856,8 @@
|
|
|
1857
1856
|
},
|
|
1858
1857
|
'.bw_container_fluid': {
|
|
1859
1858
|
'width': '100%',
|
|
1860
|
-
'padding-right': '
|
|
1861
|
-
'padding-left': '
|
|
1859
|
+
'padding-right': '0.75rem',
|
|
1860
|
+
'padding-left': '0.75rem',
|
|
1862
1861
|
'margin-right': 'auto',
|
|
1863
1862
|
'margin-left': 'auto'
|
|
1864
1863
|
},
|
|
@@ -2234,7 +2233,9 @@
|
|
|
2234
2233
|
'line-height': '1.3',
|
|
2235
2234
|
'text-align': 'center',
|
|
2236
2235
|
'white-space': 'nowrap',
|
|
2237
|
-
'vertical-align': 'baseline'
|
|
2236
|
+
'vertical-align': 'baseline',
|
|
2237
|
+
'padding': '0.35rem 0.65rem',
|
|
2238
|
+
'border-radius': '0.25rem'
|
|
2238
2239
|
},
|
|
2239
2240
|
'.bw_badge:empty': {
|
|
2240
2241
|
'display': 'none'
|
|
@@ -2699,7 +2700,8 @@
|
|
|
2699
2700
|
'.bw_code_pre': {
|
|
2700
2701
|
'margin': '0',
|
|
2701
2702
|
'border': 'none',
|
|
2702
|
-
'overflow-x': 'auto'
|
|
2703
|
+
'overflow-x': 'auto',
|
|
2704
|
+
'max-width': '100%'
|
|
2703
2705
|
},
|
|
2704
2706
|
'.bw_code_block': {
|
|
2705
2707
|
'display': 'block',
|
|
@@ -2928,7 +2930,7 @@
|
|
|
2928
2930
|
},
|
|
2929
2931
|
'.bw_modal_dialog': {
|
|
2930
2932
|
'position': 'relative',
|
|
2931
|
-
'width': '100%',
|
|
2933
|
+
'width': 'calc(100% - 1rem)',
|
|
2932
2934
|
'max-width': '500px',
|
|
2933
2935
|
'margin': '1.75rem auto',
|
|
2934
2936
|
'pointer-events': 'none'
|
|
@@ -3017,7 +3019,7 @@
|
|
|
3017
3019
|
'.bw_toast': {
|
|
3018
3020
|
'pointer-events': 'auto',
|
|
3019
3021
|
'width': '350px',
|
|
3020
|
-
'max-width': '
|
|
3022
|
+
'max-width': 'calc(100vw - 2rem)',
|
|
3021
3023
|
'background-clip': 'padding-box',
|
|
3022
3024
|
'opacity': '0'
|
|
3023
3025
|
},
|
|
@@ -3224,6 +3226,7 @@
|
|
|
3224
3226
|
'z-index': '999',
|
|
3225
3227
|
'font-size': '0.875rem',
|
|
3226
3228
|
'white-space': 'nowrap',
|
|
3229
|
+
'max-width': 'min(300px, calc(100vw - 1rem))',
|
|
3227
3230
|
'pointer-events': 'none',
|
|
3228
3231
|
'opacity': '0',
|
|
3229
3232
|
'visibility': 'hidden'
|
|
@@ -3282,7 +3285,7 @@
|
|
|
3282
3285
|
'position': 'absolute',
|
|
3283
3286
|
'z-index': '1000',
|
|
3284
3287
|
'min-width': '200px',
|
|
3285
|
-
'max-width': '320px',
|
|
3288
|
+
'max-width': 'min(320px, calc(100vw - 2rem))',
|
|
3286
3289
|
'pointer-events': 'none',
|
|
3287
3290
|
'opacity': '0',
|
|
3288
3291
|
'visibility': 'hidden'
|
|
@@ -3828,6 +3831,45 @@
|
|
|
3828
3831
|
},
|
|
3829
3832
|
'.bw_feature_grid, .bw_feature-grid': {
|
|
3830
3833
|
'grid-template-columns': '1fr'
|
|
3834
|
+
},
|
|
3835
|
+
'.bw_modal_dialog': {
|
|
3836
|
+
'margin': '0.5rem auto'
|
|
3837
|
+
},
|
|
3838
|
+
'.bw_modal_lg': {
|
|
3839
|
+
'max-width': 'calc(100% - 1rem)'
|
|
3840
|
+
},
|
|
3841
|
+
'.bw_modal_xl': {
|
|
3842
|
+
'max-width': 'calc(100% - 1rem)'
|
|
3843
|
+
},
|
|
3844
|
+
'.bw_navbar': {
|
|
3845
|
+
'padding': '0.5rem 0.75rem'
|
|
3846
|
+
},
|
|
3847
|
+
'.bw_navbar_brand': {
|
|
3848
|
+
'margin-right': '0.5rem',
|
|
3849
|
+
'font-size': '1rem'
|
|
3850
|
+
},
|
|
3851
|
+
'.bw_navbar_nav': {
|
|
3852
|
+
'flex-wrap': 'wrap'
|
|
3853
|
+
},
|
|
3854
|
+
'.bw_tooltip': {
|
|
3855
|
+
'white-space': 'normal'
|
|
3856
|
+
},
|
|
3857
|
+
'.bw_table': {
|
|
3858
|
+
'display': 'block',
|
|
3859
|
+
'overflow-x': 'auto',
|
|
3860
|
+
'-webkit-overflow-scrolling': 'touch'
|
|
3861
|
+
},
|
|
3862
|
+
'.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': {
|
|
3863
|
+
'flex': '0 0 100%',
|
|
3864
|
+
'max-width': '100%'
|
|
3865
|
+
},
|
|
3866
|
+
'.bw_container': {
|
|
3867
|
+
'padding-right': '0.5rem',
|
|
3868
|
+
'padding-left': '0.5rem'
|
|
3869
|
+
},
|
|
3870
|
+
'.bw_container_fluid': {
|
|
3871
|
+
'padding-right': '0.5rem',
|
|
3872
|
+
'padding-left': '0.5rem'
|
|
3831
3873
|
}
|
|
3832
3874
|
}
|
|
3833
3875
|
}
|
|
@@ -11711,6 +11753,376 @@
|
|
|
11711
11753
|
return true;
|
|
11712
11754
|
};
|
|
11713
11755
|
|
|
11756
|
+
// ===================================================================================
|
|
11757
|
+
// bw.clientApply() / bw.clientConnect() — Server-driven UI protocol
|
|
11758
|
+
// ===================================================================================
|
|
11759
|
+
|
|
11760
|
+
/**
|
|
11761
|
+
* Registry of named functions sent via register messages.
|
|
11762
|
+
* Populated by clientApply({ type: 'register', name, body }).
|
|
11763
|
+
* Invoked by clientApply({ type: 'call', name, args }).
|
|
11764
|
+
* @private
|
|
11765
|
+
*/
|
|
11766
|
+
bw._clientFunctions = {};
|
|
11767
|
+
|
|
11768
|
+
/**
|
|
11769
|
+
* Whether exec messages are allowed. Set by clientConnect opts.allowExec.
|
|
11770
|
+
* Default false — exec messages are rejected unless explicitly opted in.
|
|
11771
|
+
* @private
|
|
11772
|
+
*/
|
|
11773
|
+
bw._allowExec = false;
|
|
11774
|
+
|
|
11775
|
+
/**
|
|
11776
|
+
* Built-in client functions available via call() without registration.
|
|
11777
|
+
* @private
|
|
11778
|
+
*/
|
|
11779
|
+
bw._builtinClientFunctions = {
|
|
11780
|
+
scrollTo: function scrollTo(selector) {
|
|
11781
|
+
var el = bw._el(selector);
|
|
11782
|
+
if (el) el.scrollTop = el.scrollHeight;
|
|
11783
|
+
},
|
|
11784
|
+
focus: function focus(selector) {
|
|
11785
|
+
var el = bw._el(selector);
|
|
11786
|
+
if (el && typeof el.focus === 'function') el.focus();
|
|
11787
|
+
},
|
|
11788
|
+
download: function download(filename, content, mimeType) {
|
|
11789
|
+
if (typeof document === 'undefined') return;
|
|
11790
|
+
var blob = new Blob([content], {
|
|
11791
|
+
type: mimeType || 'text/plain'
|
|
11792
|
+
});
|
|
11793
|
+
var a = document.createElement('a');
|
|
11794
|
+
a.href = URL.createObjectURL(blob);
|
|
11795
|
+
a.download = filename;
|
|
11796
|
+
a.click();
|
|
11797
|
+
URL.revokeObjectURL(a.href);
|
|
11798
|
+
},
|
|
11799
|
+
clipboard: function clipboard(text) {
|
|
11800
|
+
if (typeof navigator !== 'undefined' && navigator.clipboard) {
|
|
11801
|
+
navigator.clipboard.writeText(text);
|
|
11802
|
+
}
|
|
11803
|
+
},
|
|
11804
|
+
redirect: function redirect(url) {
|
|
11805
|
+
if (typeof window !== 'undefined') window.location.href = url;
|
|
11806
|
+
},
|
|
11807
|
+
log: function log() {
|
|
11808
|
+
console.log.apply(console, arguments);
|
|
11809
|
+
}
|
|
11810
|
+
};
|
|
11811
|
+
|
|
11812
|
+
/**
|
|
11813
|
+
* Parse a bwserve protocol message string, supporting both strict JSON
|
|
11814
|
+
* and r-prefixed relaxed JSON (single-quoted strings, trailing commas).
|
|
11815
|
+
*
|
|
11816
|
+
* The r-prefix format is designed for C/C++ string literals where
|
|
11817
|
+
* double-quote escaping is painful. The parser is a state machine
|
|
11818
|
+
* that walks character by character — not a regex replace.
|
|
11819
|
+
*
|
|
11820
|
+
* Escaping: apostrophes inside single-quoted values must be escaped
|
|
11821
|
+
* with backslash: r{'name':'Barry\'s room'}
|
|
11822
|
+
*
|
|
11823
|
+
* @param {string} str - JSON or r-prefixed relaxed JSON string
|
|
11824
|
+
* @returns {Object} Parsed message object
|
|
11825
|
+
* @throws {SyntaxError} If the string is not valid JSON or relaxed JSON
|
|
11826
|
+
* @category Server
|
|
11827
|
+
*/
|
|
11828
|
+
bw.clientParse = function (str) {
|
|
11829
|
+
str = (str || '').trim();
|
|
11830
|
+
if (str.charAt(0) !== 'r') return JSON.parse(str);
|
|
11831
|
+
str = str.slice(1);
|
|
11832
|
+
var out = [];
|
|
11833
|
+
var i = 0;
|
|
11834
|
+
var len = str.length;
|
|
11835
|
+
while (i < len) {
|
|
11836
|
+
var ch = str[i];
|
|
11837
|
+
if (ch === "'") {
|
|
11838
|
+
// Single-quoted string → emit as double-quoted
|
|
11839
|
+
out.push('"');
|
|
11840
|
+
i++;
|
|
11841
|
+
while (i < len) {
|
|
11842
|
+
var c = str[i];
|
|
11843
|
+
if (c === '\\' && i + 1 < len) {
|
|
11844
|
+
var next = str[i + 1];
|
|
11845
|
+
if (next === "'") {
|
|
11846
|
+
out.push("'"); // \' in input → ' in output
|
|
11847
|
+
} else {
|
|
11848
|
+
out.push('\\');
|
|
11849
|
+
out.push(next);
|
|
11850
|
+
}
|
|
11851
|
+
i += 2;
|
|
11852
|
+
} else if (c === '"') {
|
|
11853
|
+
out.push('\\"');
|
|
11854
|
+
i++;
|
|
11855
|
+
} else if (c === "'") {
|
|
11856
|
+
break;
|
|
11857
|
+
} else {
|
|
11858
|
+
out.push(c);
|
|
11859
|
+
i++;
|
|
11860
|
+
}
|
|
11861
|
+
}
|
|
11862
|
+
out.push('"');
|
|
11863
|
+
i++; // skip closing '
|
|
11864
|
+
} else if (ch === '"') {
|
|
11865
|
+
// Double-quoted string — pass through verbatim
|
|
11866
|
+
out.push(ch);
|
|
11867
|
+
i++;
|
|
11868
|
+
while (i < len) {
|
|
11869
|
+
var c2 = str[i];
|
|
11870
|
+
if (c2 === '\\' && i + 1 < len) {
|
|
11871
|
+
out.push(c2);
|
|
11872
|
+
out.push(str[i + 1]);
|
|
11873
|
+
i += 2;
|
|
11874
|
+
} else {
|
|
11875
|
+
out.push(c2);
|
|
11876
|
+
i++;
|
|
11877
|
+
if (c2 === '"') break;
|
|
11878
|
+
}
|
|
11879
|
+
}
|
|
11880
|
+
} else if (ch === ',') {
|
|
11881
|
+
// Trailing comma check: skip comma if next non-whitespace is } or ]
|
|
11882
|
+
var j = i + 1;
|
|
11883
|
+
while (j < len && (str[j] === ' ' || str[j] === '\t' || str[j] === '\n' || str[j] === '\r')) j++;
|
|
11884
|
+
if (j < len && (str[j] === '}' || str[j] === ']')) {
|
|
11885
|
+
i++; // skip trailing comma
|
|
11886
|
+
} else {
|
|
11887
|
+
out.push(ch);
|
|
11888
|
+
i++;
|
|
11889
|
+
}
|
|
11890
|
+
} else {
|
|
11891
|
+
out.push(ch);
|
|
11892
|
+
i++;
|
|
11893
|
+
}
|
|
11894
|
+
}
|
|
11895
|
+
return JSON.parse(out.join(''));
|
|
11896
|
+
};
|
|
11897
|
+
|
|
11898
|
+
/**
|
|
11899
|
+
* Apply a bwserve protocol message to the DOM.
|
|
11900
|
+
*
|
|
11901
|
+
* Dispatches one of 9 message types:
|
|
11902
|
+
* replace — bw.DOM(target, node)
|
|
11903
|
+
* append — target.appendChild(bw.createDOM(node))
|
|
11904
|
+
* remove — bw.cleanup(target); target.remove()
|
|
11905
|
+
* patch — bw.patch(target, content, attr)
|
|
11906
|
+
* batch — iterate ops, call clientApply for each
|
|
11907
|
+
* message — bw.message(target, action, data)
|
|
11908
|
+
* register — store a named function for later call()
|
|
11909
|
+
* call — invoke a registered or built-in function
|
|
11910
|
+
* exec — execute arbitrary JS (requires allowExec)
|
|
11911
|
+
*
|
|
11912
|
+
* Target resolution:
|
|
11913
|
+
* Starts with '#' or '.' → CSS selector (querySelector)
|
|
11914
|
+
* Otherwise → getElementById, then bw._el fallback
|
|
11915
|
+
*
|
|
11916
|
+
* @param {Object} msg - Protocol message
|
|
11917
|
+
* @returns {boolean} true if the message was applied successfully
|
|
11918
|
+
* @category Server
|
|
11919
|
+
*/
|
|
11920
|
+
bw.clientApply = function (msg) {
|
|
11921
|
+
if (!msg || !msg.type) return false;
|
|
11922
|
+
var type = msg.type;
|
|
11923
|
+
var target = msg.target;
|
|
11924
|
+
if (type === 'replace') {
|
|
11925
|
+
var el = bw._el(target);
|
|
11926
|
+
if (!el) return false;
|
|
11927
|
+
bw.DOM(el, msg.node);
|
|
11928
|
+
return true;
|
|
11929
|
+
} else if (type === 'patch') {
|
|
11930
|
+
var patched = bw.patch(target, msg.content, msg.attr);
|
|
11931
|
+
return patched !== null;
|
|
11932
|
+
} else if (type === 'append') {
|
|
11933
|
+
var parent = bw._el(target);
|
|
11934
|
+
if (!parent) return false;
|
|
11935
|
+
var child = bw.createDOM(msg.node);
|
|
11936
|
+
parent.appendChild(child);
|
|
11937
|
+
return true;
|
|
11938
|
+
} else if (type === 'remove') {
|
|
11939
|
+
var toRemove = bw._el(target);
|
|
11940
|
+
if (!toRemove) return false;
|
|
11941
|
+
if (typeof bw.cleanup === 'function') bw.cleanup(toRemove);
|
|
11942
|
+
toRemove.remove();
|
|
11943
|
+
return true;
|
|
11944
|
+
} else if (type === 'batch') {
|
|
11945
|
+
if (!Array.isArray(msg.ops)) return false;
|
|
11946
|
+
var allOk = true;
|
|
11947
|
+
msg.ops.forEach(function (op) {
|
|
11948
|
+
if (!bw.clientApply(op)) allOk = false;
|
|
11949
|
+
});
|
|
11950
|
+
return allOk;
|
|
11951
|
+
} else if (type === 'message') {
|
|
11952
|
+
return bw.message(msg.target, msg.action, msg.data);
|
|
11953
|
+
} else if (type === 'register') {
|
|
11954
|
+
if (!msg.name || !msg.body) return false;
|
|
11955
|
+
try {
|
|
11956
|
+
bw._clientFunctions[msg.name] = new Function('return ' + msg.body)();
|
|
11957
|
+
return true;
|
|
11958
|
+
} catch (e) {
|
|
11959
|
+
console.error('[bw] register error:', msg.name, e);
|
|
11960
|
+
return false;
|
|
11961
|
+
}
|
|
11962
|
+
} else if (type === 'call') {
|
|
11963
|
+
if (!msg.name) return false;
|
|
11964
|
+
var fn = bw._clientFunctions[msg.name] || bw._builtinClientFunctions[msg.name];
|
|
11965
|
+
if (typeof fn !== 'function') return false;
|
|
11966
|
+
try {
|
|
11967
|
+
var args = Array.isArray(msg.args) ? msg.args : [];
|
|
11968
|
+
fn.apply(null, args);
|
|
11969
|
+
return true;
|
|
11970
|
+
} catch (e) {
|
|
11971
|
+
console.error('[bw] call error:', msg.name, e);
|
|
11972
|
+
return false;
|
|
11973
|
+
}
|
|
11974
|
+
} else if (type === 'exec') {
|
|
11975
|
+
if (!bw._allowExec) {
|
|
11976
|
+
console.warn('[bw] exec rejected: allowExec is not enabled');
|
|
11977
|
+
return false;
|
|
11978
|
+
}
|
|
11979
|
+
if (!msg.code) return false;
|
|
11980
|
+
try {
|
|
11981
|
+
new Function(msg.code)();
|
|
11982
|
+
return true;
|
|
11983
|
+
} catch (e) {
|
|
11984
|
+
console.error('[bw] exec error:', e);
|
|
11985
|
+
return false;
|
|
11986
|
+
}
|
|
11987
|
+
}
|
|
11988
|
+
return false;
|
|
11989
|
+
};
|
|
11990
|
+
|
|
11991
|
+
/**
|
|
11992
|
+
* Connect to a bwserve SSE endpoint and apply protocol messages automatically.
|
|
11993
|
+
*
|
|
11994
|
+
* Returns a connection object with sendAction(), on(), and close() methods.
|
|
11995
|
+
*
|
|
11996
|
+
* @param {string} url - SSE endpoint URL (e.g., '/__bw/events/client-1')
|
|
11997
|
+
* @param {Object} [opts] - Connection options
|
|
11998
|
+
* @param {string} [opts.transport='sse'] - Transport type: 'sse' (default) or 'poll'
|
|
11999
|
+
* @param {number} [opts.interval=2000] - Poll interval in ms (only for 'poll' transport)
|
|
12000
|
+
* @param {string} [opts.actionUrl] - POST endpoint for actions (default: derived from url)
|
|
12001
|
+
* @param {boolean} [opts.reconnect=true] - Auto-reconnect on disconnect
|
|
12002
|
+
* @param {boolean} [opts.allowExec=false] - Enable exec message type (arbitrary JS execution)
|
|
12003
|
+
* @param {Function} [opts.onStatus] - Status callback: 'connecting'|'connected'|'disconnected'
|
|
12004
|
+
* @param {Function} [opts.onMessage] - Raw message callback (before clientApply)
|
|
12005
|
+
* @returns {Object} Connection object { sendAction, on, close, status }
|
|
12006
|
+
* @category Server
|
|
12007
|
+
*/
|
|
12008
|
+
bw.clientConnect = function (url, opts) {
|
|
12009
|
+
opts = opts || {};
|
|
12010
|
+
var transport = opts.transport || 'sse';
|
|
12011
|
+
var actionUrl = opts.actionUrl || url.replace(/\/events\//, '/action/');
|
|
12012
|
+
var reconnect = opts.reconnect !== false;
|
|
12013
|
+
var onStatus = opts.onStatus || function () {};
|
|
12014
|
+
var onMessage = opts.onMessage || null;
|
|
12015
|
+
var handlers = {};
|
|
12016
|
+
// Set the global allowExec flag from connection options
|
|
12017
|
+
bw._allowExec = !!opts.allowExec;
|
|
12018
|
+
var conn = {
|
|
12019
|
+
status: 'connecting',
|
|
12020
|
+
_es: null,
|
|
12021
|
+
_pollTimer: null
|
|
12022
|
+
};
|
|
12023
|
+
function setStatus(s) {
|
|
12024
|
+
conn.status = s;
|
|
12025
|
+
onStatus(s);
|
|
12026
|
+
}
|
|
12027
|
+
function handleMessage(data) {
|
|
12028
|
+
try {
|
|
12029
|
+
var msg = typeof data === 'string' ? bw.clientParse(data) : data;
|
|
12030
|
+
if (onMessage) onMessage(msg);
|
|
12031
|
+
if (handlers.message) handlers.message(msg);
|
|
12032
|
+
bw.clientApply(msg);
|
|
12033
|
+
} catch (e) {
|
|
12034
|
+
if (handlers.error) handlers.error(e);
|
|
12035
|
+
}
|
|
12036
|
+
}
|
|
12037
|
+
if (transport === 'sse' && typeof EventSource !== 'undefined') {
|
|
12038
|
+
setStatus('connecting');
|
|
12039
|
+
var es = new EventSource(url);
|
|
12040
|
+
conn._es = es;
|
|
12041
|
+
es.onopen = function () {
|
|
12042
|
+
setStatus('connected');
|
|
12043
|
+
if (handlers.open) handlers.open();
|
|
12044
|
+
};
|
|
12045
|
+
es.onmessage = function (e) {
|
|
12046
|
+
handleMessage(e.data);
|
|
12047
|
+
};
|
|
12048
|
+
es.onerror = function () {
|
|
12049
|
+
if (conn.status === 'connected') {
|
|
12050
|
+
setStatus('disconnected');
|
|
12051
|
+
}
|
|
12052
|
+
if (handlers.error) handlers.error(new Error('SSE connection error'));
|
|
12053
|
+
if (!reconnect) {
|
|
12054
|
+
es.close();
|
|
12055
|
+
}
|
|
12056
|
+
// EventSource auto-reconnects by default when reconnect=true
|
|
12057
|
+
};
|
|
12058
|
+
} else if (transport === 'poll') {
|
|
12059
|
+
var interval = opts.interval || 2000;
|
|
12060
|
+
setStatus('connected');
|
|
12061
|
+
conn._pollTimer = setInterval(function () {
|
|
12062
|
+
fetch(url).then(function (r) {
|
|
12063
|
+
return r.json();
|
|
12064
|
+
}).then(function (msgs) {
|
|
12065
|
+
if (Array.isArray(msgs)) {
|
|
12066
|
+
msgs.forEach(handleMessage);
|
|
12067
|
+
} else if (msgs && msgs.type) {
|
|
12068
|
+
handleMessage(msgs);
|
|
12069
|
+
}
|
|
12070
|
+
})["catch"](function (e) {
|
|
12071
|
+
if (handlers.error) handlers.error(e);
|
|
12072
|
+
});
|
|
12073
|
+
}, interval);
|
|
12074
|
+
}
|
|
12075
|
+
|
|
12076
|
+
/**
|
|
12077
|
+
* Send an action to the server via POST.
|
|
12078
|
+
* @param {string} action - Action name
|
|
12079
|
+
* @param {Object} [data] - Action payload
|
|
12080
|
+
*/
|
|
12081
|
+
conn.sendAction = function (action, data) {
|
|
12082
|
+
var body = JSON.stringify({
|
|
12083
|
+
type: 'action',
|
|
12084
|
+
action: action,
|
|
12085
|
+
data: data || {}
|
|
12086
|
+
});
|
|
12087
|
+
fetch(actionUrl, {
|
|
12088
|
+
method: 'POST',
|
|
12089
|
+
headers: {
|
|
12090
|
+
'Content-Type': 'application/json'
|
|
12091
|
+
},
|
|
12092
|
+
body: body
|
|
12093
|
+
})["catch"](function (e) {
|
|
12094
|
+
if (handlers.error) handlers.error(e);
|
|
12095
|
+
});
|
|
12096
|
+
};
|
|
12097
|
+
|
|
12098
|
+
/**
|
|
12099
|
+
* Register an event handler.
|
|
12100
|
+
* @param {string} event - 'open'|'message'|'error'|'close'
|
|
12101
|
+
* @param {Function} handler
|
|
12102
|
+
*/
|
|
12103
|
+
conn.on = function (event, handler) {
|
|
12104
|
+
handlers[event] = handler;
|
|
12105
|
+
return conn;
|
|
12106
|
+
};
|
|
12107
|
+
|
|
12108
|
+
/**
|
|
12109
|
+
* Close the connection.
|
|
12110
|
+
*/
|
|
12111
|
+
conn.close = function () {
|
|
12112
|
+
if (conn._es) {
|
|
12113
|
+
conn._es.close();
|
|
12114
|
+
conn._es = null;
|
|
12115
|
+
}
|
|
12116
|
+
if (conn._pollTimer) {
|
|
12117
|
+
clearInterval(conn._pollTimer);
|
|
12118
|
+
conn._pollTimer = null;
|
|
12119
|
+
}
|
|
12120
|
+
setStatus('disconnected');
|
|
12121
|
+
if (handlers.close) handlers.close();
|
|
12122
|
+
};
|
|
12123
|
+
return conn;
|
|
12124
|
+
};
|
|
12125
|
+
|
|
11714
12126
|
// ===================================================================================
|
|
11715
12127
|
// bw.inspect() — Debug utility
|
|
11716
12128
|
// ===================================================================================
|