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/src/bitwrench.js
CHANGED
|
@@ -2579,6 +2579,391 @@ bw.message = function(target, action, data) {
|
|
|
2579
2579
|
return true;
|
|
2580
2580
|
};
|
|
2581
2581
|
|
|
2582
|
+
// ===================================================================================
|
|
2583
|
+
// bw.clientApply() / bw.clientConnect() — Server-driven UI protocol
|
|
2584
|
+
// ===================================================================================
|
|
2585
|
+
|
|
2586
|
+
/**
|
|
2587
|
+
* Registry of named functions sent via register messages.
|
|
2588
|
+
* Populated by clientApply({ type: 'register', name, body }).
|
|
2589
|
+
* Invoked by clientApply({ type: 'call', name, args }).
|
|
2590
|
+
* @private
|
|
2591
|
+
*/
|
|
2592
|
+
bw._clientFunctions = {};
|
|
2593
|
+
|
|
2594
|
+
/**
|
|
2595
|
+
* Whether exec messages are allowed. Set by clientConnect opts.allowExec.
|
|
2596
|
+
* Default false — exec messages are rejected unless explicitly opted in.
|
|
2597
|
+
* @private
|
|
2598
|
+
*/
|
|
2599
|
+
bw._allowExec = false;
|
|
2600
|
+
|
|
2601
|
+
/**
|
|
2602
|
+
* Built-in client functions available via call() without registration.
|
|
2603
|
+
* @private
|
|
2604
|
+
*/
|
|
2605
|
+
bw._builtinClientFunctions = {
|
|
2606
|
+
scrollTo: function(selector) {
|
|
2607
|
+
var el = bw._el(selector);
|
|
2608
|
+
if (el) el.scrollTop = el.scrollHeight;
|
|
2609
|
+
},
|
|
2610
|
+
focus: function(selector) {
|
|
2611
|
+
var el = bw._el(selector);
|
|
2612
|
+
if (el && typeof el.focus === 'function') el.focus();
|
|
2613
|
+
},
|
|
2614
|
+
download: function(filename, content, mimeType) {
|
|
2615
|
+
if (typeof document === 'undefined') return;
|
|
2616
|
+
var blob = new Blob([content], { type: mimeType || 'text/plain' });
|
|
2617
|
+
var a = document.createElement('a');
|
|
2618
|
+
a.href = URL.createObjectURL(blob);
|
|
2619
|
+
a.download = filename;
|
|
2620
|
+
a.click();
|
|
2621
|
+
URL.revokeObjectURL(a.href);
|
|
2622
|
+
},
|
|
2623
|
+
clipboard: function(text) {
|
|
2624
|
+
if (typeof navigator !== 'undefined' && navigator.clipboard) {
|
|
2625
|
+
navigator.clipboard.writeText(text);
|
|
2626
|
+
}
|
|
2627
|
+
},
|
|
2628
|
+
redirect: function(url) {
|
|
2629
|
+
if (typeof window !== 'undefined') window.location.href = url;
|
|
2630
|
+
},
|
|
2631
|
+
log: function() {
|
|
2632
|
+
console.log.apply(console, arguments);
|
|
2633
|
+
}
|
|
2634
|
+
};
|
|
2635
|
+
|
|
2636
|
+
/**
|
|
2637
|
+
* Parse a bwserve protocol message string, supporting both strict JSON
|
|
2638
|
+
* and r-prefixed relaxed JSON (single-quoted strings, trailing commas).
|
|
2639
|
+
*
|
|
2640
|
+
* The r-prefix format is designed for C/C++ string literals where
|
|
2641
|
+
* double-quote escaping is painful. The parser is a state machine
|
|
2642
|
+
* that walks character by character — not a regex replace.
|
|
2643
|
+
*
|
|
2644
|
+
* Escaping: apostrophes inside single-quoted values must be escaped
|
|
2645
|
+
* with backslash: r{'name':'Barry\'s room'}
|
|
2646
|
+
*
|
|
2647
|
+
* @param {string} str - JSON or r-prefixed relaxed JSON string
|
|
2648
|
+
* @returns {Object} Parsed message object
|
|
2649
|
+
* @throws {SyntaxError} If the string is not valid JSON or relaxed JSON
|
|
2650
|
+
* @category Server
|
|
2651
|
+
*/
|
|
2652
|
+
bw.clientParse = function(str) {
|
|
2653
|
+
str = (str || '').trim();
|
|
2654
|
+
if (str.charAt(0) !== 'r') return JSON.parse(str);
|
|
2655
|
+
str = str.slice(1);
|
|
2656
|
+
|
|
2657
|
+
var out = [];
|
|
2658
|
+
var i = 0;
|
|
2659
|
+
var len = str.length;
|
|
2660
|
+
|
|
2661
|
+
while (i < len) {
|
|
2662
|
+
var ch = str[i];
|
|
2663
|
+
|
|
2664
|
+
if (ch === "'") {
|
|
2665
|
+
// Single-quoted string → emit as double-quoted
|
|
2666
|
+
out.push('"');
|
|
2667
|
+
i++;
|
|
2668
|
+
while (i < len) {
|
|
2669
|
+
var c = str[i];
|
|
2670
|
+
if (c === '\\' && i + 1 < len) {
|
|
2671
|
+
var next = str[i + 1];
|
|
2672
|
+
if (next === "'") {
|
|
2673
|
+
out.push("'"); // \' in input → ' in output
|
|
2674
|
+
} else {
|
|
2675
|
+
out.push('\\');
|
|
2676
|
+
out.push(next);
|
|
2677
|
+
}
|
|
2678
|
+
i += 2;
|
|
2679
|
+
} else if (c === '"') {
|
|
2680
|
+
out.push('\\"');
|
|
2681
|
+
i++;
|
|
2682
|
+
} else if (c === "'") {
|
|
2683
|
+
break;
|
|
2684
|
+
} else {
|
|
2685
|
+
out.push(c);
|
|
2686
|
+
i++;
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
out.push('"');
|
|
2690
|
+
i++; // skip closing '
|
|
2691
|
+
|
|
2692
|
+
} else if (ch === '"') {
|
|
2693
|
+
// Double-quoted string — pass through verbatim
|
|
2694
|
+
out.push(ch);
|
|
2695
|
+
i++;
|
|
2696
|
+
while (i < len) {
|
|
2697
|
+
var c2 = str[i];
|
|
2698
|
+
if (c2 === '\\' && i + 1 < len) {
|
|
2699
|
+
out.push(c2);
|
|
2700
|
+
out.push(str[i + 1]);
|
|
2701
|
+
i += 2;
|
|
2702
|
+
} else {
|
|
2703
|
+
out.push(c2);
|
|
2704
|
+
i++;
|
|
2705
|
+
if (c2 === '"') break;
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2709
|
+
} else if (ch === ',') {
|
|
2710
|
+
// Trailing comma check: skip comma if next non-whitespace is } or ]
|
|
2711
|
+
var j = i + 1;
|
|
2712
|
+
while (j < len && (str[j] === ' ' || str[j] === '\t' || str[j] === '\n' || str[j] === '\r')) j++;
|
|
2713
|
+
if (j < len && (str[j] === '}' || str[j] === ']')) {
|
|
2714
|
+
i++; // skip trailing comma
|
|
2715
|
+
} else {
|
|
2716
|
+
out.push(ch);
|
|
2717
|
+
i++;
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
} else {
|
|
2721
|
+
out.push(ch);
|
|
2722
|
+
i++;
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
return JSON.parse(out.join(''));
|
|
2727
|
+
};
|
|
2728
|
+
|
|
2729
|
+
/**
|
|
2730
|
+
* Apply a bwserve protocol message to the DOM.
|
|
2731
|
+
*
|
|
2732
|
+
* Dispatches one of 9 message types:
|
|
2733
|
+
* replace — bw.DOM(target, node)
|
|
2734
|
+
* append — target.appendChild(bw.createDOM(node))
|
|
2735
|
+
* remove — bw.cleanup(target); target.remove()
|
|
2736
|
+
* patch — bw.patch(target, content, attr)
|
|
2737
|
+
* batch — iterate ops, call clientApply for each
|
|
2738
|
+
* message — bw.message(target, action, data)
|
|
2739
|
+
* register — store a named function for later call()
|
|
2740
|
+
* call — invoke a registered or built-in function
|
|
2741
|
+
* exec — execute arbitrary JS (requires allowExec)
|
|
2742
|
+
*
|
|
2743
|
+
* Target resolution:
|
|
2744
|
+
* Starts with '#' or '.' → CSS selector (querySelector)
|
|
2745
|
+
* Otherwise → getElementById, then bw._el fallback
|
|
2746
|
+
*
|
|
2747
|
+
* @param {Object} msg - Protocol message
|
|
2748
|
+
* @returns {boolean} true if the message was applied successfully
|
|
2749
|
+
* @category Server
|
|
2750
|
+
*/
|
|
2751
|
+
bw.clientApply = function(msg) {
|
|
2752
|
+
if (!msg || !msg.type) return false;
|
|
2753
|
+
|
|
2754
|
+
var type = msg.type;
|
|
2755
|
+
var target = msg.target;
|
|
2756
|
+
|
|
2757
|
+
if (type === 'replace') {
|
|
2758
|
+
var el = bw._el(target);
|
|
2759
|
+
if (!el) return false;
|
|
2760
|
+
bw.DOM(el, msg.node);
|
|
2761
|
+
return true;
|
|
2762
|
+
|
|
2763
|
+
} else if (type === 'patch') {
|
|
2764
|
+
var patched = bw.patch(target, msg.content, msg.attr);
|
|
2765
|
+
return patched !== null;
|
|
2766
|
+
|
|
2767
|
+
} else if (type === 'append') {
|
|
2768
|
+
var parent = bw._el(target);
|
|
2769
|
+
if (!parent) return false;
|
|
2770
|
+
var child = bw.createDOM(msg.node);
|
|
2771
|
+
parent.appendChild(child);
|
|
2772
|
+
return true;
|
|
2773
|
+
|
|
2774
|
+
} else if (type === 'remove') {
|
|
2775
|
+
var toRemove = bw._el(target);
|
|
2776
|
+
if (!toRemove) return false;
|
|
2777
|
+
if (typeof bw.cleanup === 'function') bw.cleanup(toRemove);
|
|
2778
|
+
toRemove.remove();
|
|
2779
|
+
return true;
|
|
2780
|
+
|
|
2781
|
+
} else if (type === 'batch') {
|
|
2782
|
+
if (!Array.isArray(msg.ops)) return false;
|
|
2783
|
+
var allOk = true;
|
|
2784
|
+
msg.ops.forEach(function(op) {
|
|
2785
|
+
if (!bw.clientApply(op)) allOk = false;
|
|
2786
|
+
});
|
|
2787
|
+
return allOk;
|
|
2788
|
+
|
|
2789
|
+
} else if (type === 'message') {
|
|
2790
|
+
return bw.message(msg.target, msg.action, msg.data);
|
|
2791
|
+
|
|
2792
|
+
} else if (type === 'register') {
|
|
2793
|
+
if (!msg.name || !msg.body) return false;
|
|
2794
|
+
try {
|
|
2795
|
+
bw._clientFunctions[msg.name] = new Function('return ' + msg.body)();
|
|
2796
|
+
return true;
|
|
2797
|
+
} catch (e) {
|
|
2798
|
+
console.error('[bw] register error:', msg.name, e);
|
|
2799
|
+
return false;
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2802
|
+
} else if (type === 'call') {
|
|
2803
|
+
if (!msg.name) return false;
|
|
2804
|
+
var fn = bw._clientFunctions[msg.name] || bw._builtinClientFunctions[msg.name];
|
|
2805
|
+
if (typeof fn !== 'function') return false;
|
|
2806
|
+
try {
|
|
2807
|
+
var args = Array.isArray(msg.args) ? msg.args : [];
|
|
2808
|
+
fn.apply(null, args);
|
|
2809
|
+
return true;
|
|
2810
|
+
} catch (e) {
|
|
2811
|
+
console.error('[bw] call error:', msg.name, e);
|
|
2812
|
+
return false;
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
} else if (type === 'exec') {
|
|
2816
|
+
if (!bw._allowExec) {
|
|
2817
|
+
console.warn('[bw] exec rejected: allowExec is not enabled');
|
|
2818
|
+
return false;
|
|
2819
|
+
}
|
|
2820
|
+
if (!msg.code) return false;
|
|
2821
|
+
try {
|
|
2822
|
+
new Function(msg.code)();
|
|
2823
|
+
return true;
|
|
2824
|
+
} catch (e) {
|
|
2825
|
+
console.error('[bw] exec error:', e);
|
|
2826
|
+
return false;
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2830
|
+
return false;
|
|
2831
|
+
};
|
|
2832
|
+
|
|
2833
|
+
/**
|
|
2834
|
+
* Connect to a bwserve SSE endpoint and apply protocol messages automatically.
|
|
2835
|
+
*
|
|
2836
|
+
* Returns a connection object with sendAction(), on(), and close() methods.
|
|
2837
|
+
*
|
|
2838
|
+
* @param {string} url - SSE endpoint URL (e.g., '/__bw/events/client-1')
|
|
2839
|
+
* @param {Object} [opts] - Connection options
|
|
2840
|
+
* @param {string} [opts.transport='sse'] - Transport type: 'sse' (default) or 'poll'
|
|
2841
|
+
* @param {number} [opts.interval=2000] - Poll interval in ms (only for 'poll' transport)
|
|
2842
|
+
* @param {string} [opts.actionUrl] - POST endpoint for actions (default: derived from url)
|
|
2843
|
+
* @param {boolean} [opts.reconnect=true] - Auto-reconnect on disconnect
|
|
2844
|
+
* @param {boolean} [opts.allowExec=false] - Enable exec message type (arbitrary JS execution)
|
|
2845
|
+
* @param {Function} [opts.onStatus] - Status callback: 'connecting'|'connected'|'disconnected'
|
|
2846
|
+
* @param {Function} [opts.onMessage] - Raw message callback (before clientApply)
|
|
2847
|
+
* @returns {Object} Connection object { sendAction, on, close, status }
|
|
2848
|
+
* @category Server
|
|
2849
|
+
*/
|
|
2850
|
+
bw.clientConnect = function(url, opts) {
|
|
2851
|
+
opts = opts || {};
|
|
2852
|
+
var transport = opts.transport || 'sse';
|
|
2853
|
+
var actionUrl = opts.actionUrl || url.replace(/\/events\//, '/action/');
|
|
2854
|
+
var reconnect = opts.reconnect !== false;
|
|
2855
|
+
var onStatus = opts.onStatus || function() {};
|
|
2856
|
+
var onMessage = opts.onMessage || null;
|
|
2857
|
+
var handlers = {};
|
|
2858
|
+
// Set the global allowExec flag from connection options
|
|
2859
|
+
bw._allowExec = !!opts.allowExec;
|
|
2860
|
+
var conn = {
|
|
2861
|
+
status: 'connecting',
|
|
2862
|
+
_es: null,
|
|
2863
|
+
_pollTimer: null
|
|
2864
|
+
};
|
|
2865
|
+
|
|
2866
|
+
function setStatus(s) {
|
|
2867
|
+
conn.status = s;
|
|
2868
|
+
onStatus(s);
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2871
|
+
function handleMessage(data) {
|
|
2872
|
+
try {
|
|
2873
|
+
var msg = typeof data === 'string' ? bw.clientParse(data) : data;
|
|
2874
|
+
if (onMessage) onMessage(msg);
|
|
2875
|
+
if (handlers.message) handlers.message(msg);
|
|
2876
|
+
bw.clientApply(msg);
|
|
2877
|
+
} catch (e) {
|
|
2878
|
+
if (handlers.error) handlers.error(e);
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
if (transport === 'sse' && typeof EventSource !== 'undefined') {
|
|
2883
|
+
setStatus('connecting');
|
|
2884
|
+
var es = new EventSource(url);
|
|
2885
|
+
conn._es = es;
|
|
2886
|
+
|
|
2887
|
+
es.onopen = function() {
|
|
2888
|
+
setStatus('connected');
|
|
2889
|
+
if (handlers.open) handlers.open();
|
|
2890
|
+
};
|
|
2891
|
+
|
|
2892
|
+
es.onmessage = function(e) {
|
|
2893
|
+
handleMessage(e.data);
|
|
2894
|
+
};
|
|
2895
|
+
|
|
2896
|
+
es.onerror = function() {
|
|
2897
|
+
if (conn.status === 'connected') {
|
|
2898
|
+
setStatus('disconnected');
|
|
2899
|
+
}
|
|
2900
|
+
if (handlers.error) handlers.error(new Error('SSE connection error'));
|
|
2901
|
+
if (!reconnect) {
|
|
2902
|
+
es.close();
|
|
2903
|
+
}
|
|
2904
|
+
// EventSource auto-reconnects by default when reconnect=true
|
|
2905
|
+
};
|
|
2906
|
+
} else if (transport === 'poll') {
|
|
2907
|
+
var interval = opts.interval || 2000;
|
|
2908
|
+
setStatus('connected');
|
|
2909
|
+
conn._pollTimer = setInterval(function() {
|
|
2910
|
+
fetch(url).then(function(r) { return r.json(); }).then(function(msgs) {
|
|
2911
|
+
if (Array.isArray(msgs)) {
|
|
2912
|
+
msgs.forEach(handleMessage);
|
|
2913
|
+
} else if (msgs && msgs.type) {
|
|
2914
|
+
handleMessage(msgs);
|
|
2915
|
+
}
|
|
2916
|
+
}).catch(function(e) {
|
|
2917
|
+
if (handlers.error) handlers.error(e);
|
|
2918
|
+
});
|
|
2919
|
+
}, interval);
|
|
2920
|
+
}
|
|
2921
|
+
|
|
2922
|
+
/**
|
|
2923
|
+
* Send an action to the server via POST.
|
|
2924
|
+
* @param {string} action - Action name
|
|
2925
|
+
* @param {Object} [data] - Action payload
|
|
2926
|
+
*/
|
|
2927
|
+
conn.sendAction = function(action, data) {
|
|
2928
|
+
var body = JSON.stringify({ type: 'action', action: action, data: data || {} });
|
|
2929
|
+
fetch(actionUrl, {
|
|
2930
|
+
method: 'POST',
|
|
2931
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2932
|
+
body: body
|
|
2933
|
+
}).catch(function(e) {
|
|
2934
|
+
if (handlers.error) handlers.error(e);
|
|
2935
|
+
});
|
|
2936
|
+
};
|
|
2937
|
+
|
|
2938
|
+
/**
|
|
2939
|
+
* Register an event handler.
|
|
2940
|
+
* @param {string} event - 'open'|'message'|'error'|'close'
|
|
2941
|
+
* @param {Function} handler
|
|
2942
|
+
*/
|
|
2943
|
+
conn.on = function(event, handler) {
|
|
2944
|
+
handlers[event] = handler;
|
|
2945
|
+
return conn;
|
|
2946
|
+
};
|
|
2947
|
+
|
|
2948
|
+
/**
|
|
2949
|
+
* Close the connection.
|
|
2950
|
+
*/
|
|
2951
|
+
conn.close = function() {
|
|
2952
|
+
if (conn._es) {
|
|
2953
|
+
conn._es.close();
|
|
2954
|
+
conn._es = null;
|
|
2955
|
+
}
|
|
2956
|
+
if (conn._pollTimer) {
|
|
2957
|
+
clearInterval(conn._pollTimer);
|
|
2958
|
+
conn._pollTimer = null;
|
|
2959
|
+
}
|
|
2960
|
+
setStatus('disconnected');
|
|
2961
|
+
if (handlers.close) handlers.close();
|
|
2962
|
+
};
|
|
2963
|
+
|
|
2964
|
+
return conn;
|
|
2965
|
+
};
|
|
2966
|
+
|
|
2582
2967
|
// ===================================================================================
|
|
2583
2968
|
// bw.inspect() — Debug utility
|
|
2584
2969
|
// ===================================================================================
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BwServeClient — per-client connection for bwserve.
|
|
3
|
+
*
|
|
4
|
+
* Represents one browser tab connected via SSE. The server calls methods
|
|
5
|
+
* on this object to push UI updates to the client.
|
|
6
|
+
*
|
|
7
|
+
* Protocol message types (sent as SSE data):
|
|
8
|
+
* { type: 'replace', target: '#app', node: {t,a,c,o} }
|
|
9
|
+
* { type: 'append', target: '#list', node: {t,a,c,o} }
|
|
10
|
+
* { type: 'remove', target: '#item-3' }
|
|
11
|
+
* { type: 'patch', target: 'bw_counter_abc', content: '42', attr: null }
|
|
12
|
+
* { type: 'batch', ops: [ ...messages ] }
|
|
13
|
+
* { type: 'register', name: 'fn', body: 'function(x) { ... }' }
|
|
14
|
+
* { type: 'call', name: 'fn', args: [...] }
|
|
15
|
+
* { type: 'exec', code: 'js code string' }
|
|
16
|
+
*
|
|
17
|
+
* @module bwserve/client
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* BwServeClient — one connected browser tab.
|
|
22
|
+
*/
|
|
23
|
+
export class BwServeClient {
|
|
24
|
+
constructor(id, res) {
|
|
25
|
+
this.id = id;
|
|
26
|
+
this._res = res; // SSE response stream (null in stub)
|
|
27
|
+
this._handlers = {}; // action name → handler
|
|
28
|
+
this._closed = false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Replace the content of a DOM element with a TACO.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} selector - CSS selector or UUID
|
|
35
|
+
* @param {Object} taco - TACO object to render
|
|
36
|
+
*/
|
|
37
|
+
render(selector, taco) {
|
|
38
|
+
this._send({ type: 'replace', target: selector, node: taco });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Patch an element's content or attributes without rebuild.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} id - Element UUID (from bw.uuid())
|
|
45
|
+
* @param {string} content - New text content
|
|
46
|
+
* @param {Object} [attr] - Attributes to update
|
|
47
|
+
*/
|
|
48
|
+
patch(id, content, attr) {
|
|
49
|
+
this._send({ type: 'patch', target: id, content, attr: attr || null });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Append a TACO as a new child of the target element.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} selector - CSS selector of parent
|
|
56
|
+
* @param {Object} taco - TACO object to append
|
|
57
|
+
*/
|
|
58
|
+
append(selector, taco) {
|
|
59
|
+
this._send({ type: 'append', target: selector, node: taco });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Remove an element from the DOM (with cleanup).
|
|
64
|
+
*
|
|
65
|
+
* @param {string} selector - CSS selector or UUID of element to remove
|
|
66
|
+
*/
|
|
67
|
+
remove(selector) {
|
|
68
|
+
this._send({ type: 'remove', target: selector });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Send multiple operations as a single batch.
|
|
73
|
+
*
|
|
74
|
+
* @param {Array} ops - Array of message objects (replace/append/remove/patch)
|
|
75
|
+
*/
|
|
76
|
+
batch(ops) {
|
|
77
|
+
this._send({ type: 'batch', ops });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Send a bw.message() dispatch to a tagged component on the client.
|
|
82
|
+
*
|
|
83
|
+
* @param {string} target - Component userTag or UUID
|
|
84
|
+
* @param {string} action - Method name to call
|
|
85
|
+
* @param {*} data - Data to pass to the method
|
|
86
|
+
*/
|
|
87
|
+
message(target, action, data) {
|
|
88
|
+
this._send({ type: 'message', target, action, data });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Register a named function on the client for later invocation via call().
|
|
93
|
+
*
|
|
94
|
+
* The function body is sent as a string and compiled on the client side.
|
|
95
|
+
* Registered functions persist for the lifetime of the connection.
|
|
96
|
+
*
|
|
97
|
+
* @param {string} name - Function name (used as key for later call())
|
|
98
|
+
* @param {string} body - Function source as string, e.g. "function(el) { el.scrollTop = el.scrollHeight; }"
|
|
99
|
+
*/
|
|
100
|
+
register(name, body) {
|
|
101
|
+
this._send({ type: 'register', name, body });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Call a previously registered or built-in function on the client.
|
|
106
|
+
*
|
|
107
|
+
* Built-in functions (always available, no registration needed):
|
|
108
|
+
* scrollTo, focus, download, clipboard, redirect, log
|
|
109
|
+
*
|
|
110
|
+
* @param {string} name - Function name (registered or built-in)
|
|
111
|
+
* @param {...*} args - Arguments to pass to the function
|
|
112
|
+
*/
|
|
113
|
+
call(name, ...args) {
|
|
114
|
+
this._send({ type: 'call', name, args });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Execute arbitrary JavaScript code on the client.
|
|
119
|
+
*
|
|
120
|
+
* Requires the client connection to be created with { allowExec: true }.
|
|
121
|
+
* Use call() as the safe alternative when possible.
|
|
122
|
+
*
|
|
123
|
+
* @param {string} code - JavaScript code string to execute
|
|
124
|
+
*/
|
|
125
|
+
exec(code) {
|
|
126
|
+
this._send({ type: 'exec', code });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Register a handler for client actions (button clicks, form submits, etc.).
|
|
131
|
+
*
|
|
132
|
+
* @param {string} action - Action name (from o.events declarative handler)
|
|
133
|
+
* @param {Function} handler - Called with (data, client)
|
|
134
|
+
* @returns {BwServeClient} this (for chaining)
|
|
135
|
+
*/
|
|
136
|
+
on(action, handler) {
|
|
137
|
+
this._handlers[action] = handler;
|
|
138
|
+
return this;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Close the SSE connection to this client.
|
|
143
|
+
*/
|
|
144
|
+
close() {
|
|
145
|
+
this._closed = true;
|
|
146
|
+
if (this._res && typeof this._res.end === 'function') {
|
|
147
|
+
try { this._res.end(); } catch (e) { /* ignore */ }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Send a protocol message to the client via SSE.
|
|
153
|
+
* @private
|
|
154
|
+
*/
|
|
155
|
+
_send(msg) {
|
|
156
|
+
if (this._closed) return;
|
|
157
|
+
// Always store for testing / inspection
|
|
158
|
+
if (!this._sent) this._sent = [];
|
|
159
|
+
this._sent.push(msg);
|
|
160
|
+
// Write SSE frame if we have a live response stream
|
|
161
|
+
if (this._res && typeof this._res.write === 'function') {
|
|
162
|
+
try {
|
|
163
|
+
this._res.write('data: ' + JSON.stringify(msg) + '\n\n');
|
|
164
|
+
} catch (e) {
|
|
165
|
+
// Stream may have been closed — ignore write errors
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Dispatch an incoming action from the client.
|
|
172
|
+
* @private
|
|
173
|
+
*/
|
|
174
|
+
_dispatch(action, data) {
|
|
175
|
+
const handler = this._handlers[action];
|
|
176
|
+
if (handler) {
|
|
177
|
+
handler(data, this);
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
}
|