termbeam 1.11.0 → 1.11.1
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/package.json +2 -2
- package/public/sw.js +1 -1
- package/public/terminal.html +114 -21
- package/src/server.js +7 -2
- package/src/sessions.js +9 -3
- package/src/version.js +23 -5
- package/src/websocket.js +47 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "termbeam",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.1",
|
|
4
4
|
"description": "Beam your terminal to any device — mobile-optimized web terminal with multi-session support",
|
|
5
5
|
"main": "src/server.js",
|
|
6
6
|
"bin": {
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"url": "https://github.com/dorlugasigal/TermBeam/issues"
|
|
48
48
|
},
|
|
49
49
|
"engines": {
|
|
50
|
-
"node": ">=
|
|
50
|
+
"node": ">=20.0.0"
|
|
51
51
|
},
|
|
52
52
|
"files": [
|
|
53
53
|
"bin/",
|
package/public/sw.js
CHANGED
package/public/terminal.html
CHANGED
|
@@ -2293,6 +2293,7 @@
|
|
|
2293
2293
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js"></script>
|
|
2294
2294
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0.11.0/lib/addon-web-links.min.js"></script>
|
|
2295
2295
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-search@0.15.0/lib/addon-search.min.js"></script>
|
|
2296
|
+
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-canvas@0.7.0/lib/addon-canvas.min.js"></script>
|
|
2296
2297
|
<script src="/js/shared.js"></script>
|
|
2297
2298
|
<script src="/js/themes.js"></script>
|
|
2298
2299
|
<script src="/js/terminal-themes.js"></script>
|
|
@@ -2376,6 +2377,9 @@
|
|
|
2376
2377
|
const terminalsWrapper = document.getElementById('terminals-wrapper');
|
|
2377
2378
|
|
|
2378
2379
|
// ===== Font Loading (non-blocking) =====
|
|
2380
|
+
// Hook for font-load refit (set inside init, called by font loader)
|
|
2381
|
+
let onFontReady = null;
|
|
2382
|
+
|
|
2379
2383
|
const nerdFont = new FontFace(
|
|
2380
2384
|
'NerdFont',
|
|
2381
2385
|
"url('https://cdn.jsdelivr.net/gh/ryanoasis/nerd-fonts@v3.4.0/patched-fonts/JetBrainsMono/Ligatures/Regular/JetBrainsMonoNerdFont-Regular.ttf')",
|
|
@@ -2387,6 +2391,10 @@
|
|
|
2387
2391
|
})
|
|
2388
2392
|
.catch(() => {
|
|
2389
2393
|
console.warn('Nerd Font failed to load, using fallback');
|
|
2394
|
+
})
|
|
2395
|
+
.finally(() => {
|
|
2396
|
+
// Refit all terminals once font metrics are stable
|
|
2397
|
+
if (onFontReady) onFontReady();
|
|
2390
2398
|
});
|
|
2391
2399
|
|
|
2392
2400
|
// Start immediately — don't wait for font
|
|
@@ -2511,6 +2519,7 @@
|
|
|
2511
2519
|
setupPreviewModal();
|
|
2512
2520
|
loadShellsForModal();
|
|
2513
2521
|
startPolling();
|
|
2522
|
+
setTimeout(requestWakeLock, 0);
|
|
2514
2523
|
|
|
2515
2524
|
// Pinch-to-zoom
|
|
2516
2525
|
(function setupPinchZoom() {
|
|
@@ -2577,9 +2586,22 @@
|
|
|
2577
2586
|
}
|
|
2578
2587
|
}
|
|
2579
2588
|
}
|
|
2589
|
+
onFontReady = doResize;
|
|
2580
2590
|
window.addEventListener('resize', doResize);
|
|
2581
2591
|
screen.orientation?.addEventListener('change', () => setTimeout(doResize, 150));
|
|
2582
2592
|
|
|
2593
|
+
// ResizeObserver — catches container size changes that don't trigger window resize
|
|
2594
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
2595
|
+
let resizeTimer = null;
|
|
2596
|
+
new ResizeObserver(() => {
|
|
2597
|
+
clearTimeout(resizeTimer);
|
|
2598
|
+
resizeTimer = setTimeout(doResize, 100);
|
|
2599
|
+
}).observe(terminalsWrapper);
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2602
|
+
// Refit after all fonts finish loading (catches bold variant, system fonts)
|
|
2603
|
+
document.fonts.ready.then(() => doResize());
|
|
2604
|
+
|
|
2583
2605
|
// Mobile soft keyboard
|
|
2584
2606
|
if (window.visualViewport) {
|
|
2585
2607
|
const keyBar = document.getElementById('key-bar');
|
|
@@ -2639,24 +2661,70 @@
|
|
|
2639
2661
|
);
|
|
2640
2662
|
}
|
|
2641
2663
|
|
|
2642
|
-
//
|
|
2664
|
+
// Wake Lock — helps keep mobile browser tab alive
|
|
2665
|
+
let wakeLock = null;
|
|
2666
|
+
|
|
2667
|
+
async function requestWakeLock() {
|
|
2668
|
+
if (!('wakeLock' in navigator)) return;
|
|
2669
|
+
try {
|
|
2670
|
+
wakeLock = await navigator.wakeLock.request('screen');
|
|
2671
|
+
wakeLock.addEventListener('release', () => {
|
|
2672
|
+
wakeLock = null;
|
|
2673
|
+
});
|
|
2674
|
+
} catch {
|
|
2675
|
+
// Wake Lock request failed (e.g. low battery, not visible)
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
function releaseWakeLock() {
|
|
2680
|
+
if (wakeLock) {
|
|
2681
|
+
wakeLock.release().catch(() => {});
|
|
2682
|
+
wakeLock = null;
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
// Reconnect & refit when returning from idle / tab switch
|
|
2643
2687
|
document.addEventListener('visibilitychange', () => {
|
|
2644
|
-
if (!document.hidden
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
ms
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2688
|
+
if (!document.hidden) {
|
|
2689
|
+
if (activeId) {
|
|
2690
|
+
clearUnreadIndicator();
|
|
2691
|
+
const ms = managed.get(activeId);
|
|
2692
|
+
if (ms) {
|
|
2693
|
+
// Reconnect immediately if WebSocket died in background
|
|
2694
|
+
if (!ms.exited && (!ms.ws || ms.ws.readyState !== WebSocket.OPEN)) {
|
|
2695
|
+
if (ms.reconnectTimer) {
|
|
2696
|
+
clearTimeout(ms.reconnectTimer);
|
|
2697
|
+
ms.reconnectTimer = null;
|
|
2698
|
+
}
|
|
2699
|
+
ms.reconnectDelay = 3000;
|
|
2700
|
+
connectSession(ms);
|
|
2701
|
+
} else {
|
|
2702
|
+
ms.term.scrollToBottom();
|
|
2703
|
+
ms.fitAddon.fit();
|
|
2704
|
+
sendResize(ms);
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
if (splitMode && splitSecondId) {
|
|
2708
|
+
const ms2 = managed.get(splitSecondId);
|
|
2709
|
+
if (ms2) {
|
|
2710
|
+
if (!ms2.exited && (!ms2.ws || ms2.ws.readyState !== WebSocket.OPEN)) {
|
|
2711
|
+
if (ms2.reconnectTimer) {
|
|
2712
|
+
clearTimeout(ms2.reconnectTimer);
|
|
2713
|
+
ms2.reconnectTimer = null;
|
|
2714
|
+
}
|
|
2715
|
+
ms2.reconnectDelay = 3000;
|
|
2716
|
+
connectSession(ms2);
|
|
2717
|
+
} else {
|
|
2718
|
+
ms2.term.scrollToBottom();
|
|
2719
|
+
ms2.fitAddon.fit();
|
|
2720
|
+
sendResize(ms2);
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2658
2723
|
}
|
|
2659
2724
|
}
|
|
2725
|
+
requestWakeLock();
|
|
2726
|
+
} else {
|
|
2727
|
+
releaseWakeLock();
|
|
2660
2728
|
}
|
|
2661
2729
|
});
|
|
2662
2730
|
|
|
@@ -2708,7 +2776,7 @@
|
|
|
2708
2776
|
fontWeight: 'normal',
|
|
2709
2777
|
fontWeightBold: 'bold',
|
|
2710
2778
|
letterSpacing: 0,
|
|
2711
|
-
lineHeight: 1.
|
|
2779
|
+
lineHeight: 1.0,
|
|
2712
2780
|
theme: TERM_THEMES[getTheme()] || darkTermTheme,
|
|
2713
2781
|
allowProposedApi: true,
|
|
2714
2782
|
scrollback: 10000,
|
|
@@ -2728,6 +2796,17 @@
|
|
|
2728
2796
|
terminalsWrapper.appendChild(container);
|
|
2729
2797
|
term.open(container);
|
|
2730
2798
|
|
|
2799
|
+
// Canvas renderer — sharper text and more accurate selection.
|
|
2800
|
+
// Skip in automated browsers (navigator.webdriver) where canvas
|
|
2801
|
+
// rendering makes .xterm-rows inaccessible to DOM text queries.
|
|
2802
|
+
if (window.CanvasAddon && !navigator.webdriver) {
|
|
2803
|
+
try {
|
|
2804
|
+
term.loadAddon(new window.CanvasAddon.CanvasAddon());
|
|
2805
|
+
} catch {
|
|
2806
|
+
// Fall back to DOM renderer
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2731
2810
|
// Scroll-to-bottom button
|
|
2732
2811
|
const scrollBtn = document.createElement('button');
|
|
2733
2812
|
scrollBtn.className = 'scroll-bottom-btn';
|
|
@@ -2739,17 +2818,28 @@
|
|
|
2739
2818
|
});
|
|
2740
2819
|
container.appendChild(scrollBtn);
|
|
2741
2820
|
|
|
2742
|
-
// Write coalescer — batch rapid term.write() calls
|
|
2743
|
-
//
|
|
2821
|
+
// Write coalescer — batch rapid term.write() calls to reduce flicker.
|
|
2822
|
+
// Single RAF for interactive typing (low latency). During bursts
|
|
2823
|
+
// (>512 bytes buffered, e.g. Ctrl+O expand), waits one extra frame
|
|
2824
|
+
// so cursor-movement sequences arrive as one atomic write.
|
|
2744
2825
|
let writeBuf = '';
|
|
2745
2826
|
let writeRaf = null;
|
|
2827
|
+
function flushWrite() {
|
|
2828
|
+
writeRaf = null;
|
|
2829
|
+
if (writeBuf) {
|
|
2830
|
+
term.write(writeBuf);
|
|
2831
|
+
writeBuf = '';
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2746
2834
|
function coalescedWrite(data) {
|
|
2747
2835
|
writeBuf += data;
|
|
2748
2836
|
if (!writeRaf) {
|
|
2749
2837
|
writeRaf = requestAnimationFrame(() => {
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2838
|
+
if (writeBuf.length > 512) {
|
|
2839
|
+
writeRaf = requestAnimationFrame(flushWrite);
|
|
2840
|
+
} else {
|
|
2841
|
+
flushWrite();
|
|
2842
|
+
}
|
|
2753
2843
|
});
|
|
2754
2844
|
}
|
|
2755
2845
|
}
|
|
@@ -3042,6 +3132,9 @@
|
|
|
3042
3132
|
if (ms.id === activeId) {
|
|
3043
3133
|
statusDot.className = '';
|
|
3044
3134
|
statusText.textContent = 'Disconnected';
|
|
3135
|
+
reconnectOverlay.querySelector('.msg').textContent =
|
|
3136
|
+
'Disconnected. Attempting to reconnect\u2026';
|
|
3137
|
+
reconnectOverlay.classList.add('visible');
|
|
3045
3138
|
}
|
|
3046
3139
|
if (!ms.exited) {
|
|
3047
3140
|
ms.reconnectTimer = setTimeout(() => {
|
package/src/server.js
CHANGED
|
@@ -275,10 +275,15 @@ function createTermBeamServer(overrides = {}) {
|
|
|
275
275
|
console.log('');
|
|
276
276
|
|
|
277
277
|
// Non-blocking update check — runs after banner, never delays startup.
|
|
278
|
-
// Skip under the Node test runner to avoid network requests in tests.
|
|
278
|
+
// Skip under the Node test runner and CI to avoid network requests in tests.
|
|
279
279
|
// Accept any version containing a semver-like pattern (including dev builds).
|
|
280
280
|
const versionParts = config.version.match(/(\d{1,10})\.(\d{1,10})\.(\d{1,10})/);
|
|
281
|
-
if (
|
|
281
|
+
if (
|
|
282
|
+
versionParts &&
|
|
283
|
+
!process.env.NODE_TEST_CONTEXT &&
|
|
284
|
+
!process.env.CI &&
|
|
285
|
+
!process.argv.includes('--test')
|
|
286
|
+
) {
|
|
282
287
|
const installInfo = detectInstallMethod();
|
|
283
288
|
checkForUpdate({ currentVersion: config.version })
|
|
284
289
|
.then((info) => {
|
package/src/sessions.js
CHANGED
|
@@ -144,9 +144,15 @@ class SessionManager {
|
|
|
144
144
|
ptyProcess.onData((data) => {
|
|
145
145
|
session.lastActivity = Date.now();
|
|
146
146
|
session.scrollbackBuf += data;
|
|
147
|
-
//
|
|
148
|
-
if (session.scrollbackBuf.length >
|
|
149
|
-
|
|
147
|
+
// High/low water scrollback cap: trim to 500k chars when buffer exceeds 1,000,000 chars
|
|
148
|
+
if (session.scrollbackBuf.length > 1000000) {
|
|
149
|
+
let buf = session.scrollbackBuf.slice(-500000);
|
|
150
|
+
// Advance to first newline to avoid starting mid-line
|
|
151
|
+
const nlIdx = buf.indexOf('\n');
|
|
152
|
+
if (nlIdx > 0 && nlIdx < 200) {
|
|
153
|
+
buf = buf.slice(nlIdx + 1);
|
|
154
|
+
}
|
|
155
|
+
session.scrollbackBuf = buf;
|
|
150
156
|
}
|
|
151
157
|
for (const ws of session.clients) {
|
|
152
158
|
if (ws.readyState === 1) ws.send(JSON.stringify({ type: 'output', data }));
|
package/src/version.js
CHANGED
|
@@ -10,17 +10,35 @@ function getVersion() {
|
|
|
10
10
|
return base;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
// Running from source —
|
|
13
|
+
// Running from source — git tags are the version source of truth.
|
|
14
|
+
// This avoids drift between package.json and tagged releases.
|
|
14
15
|
try {
|
|
15
16
|
const gitDesc = execSync('git describe --tags --always --dirty', {
|
|
16
17
|
cwd: path.join(__dirname, '..'),
|
|
17
18
|
encoding: 'utf-8',
|
|
18
19
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
19
20
|
}).trim();
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
|
|
22
|
+
const tagMatch = gitDesc.match(/^v(\d+\.\d+\.\d+)(?:-(\d+)-g([0-9a-f]+))?(-dirty)?$/);
|
|
23
|
+
if (tagMatch) {
|
|
24
|
+
const gitVersion = tagMatch[1];
|
|
25
|
+
const commits = tagMatch[2];
|
|
26
|
+
const hash = tagMatch[3];
|
|
27
|
+
const dirty = tagMatch[4];
|
|
28
|
+
|
|
29
|
+
// Exactly on a clean tag — return the tag version
|
|
30
|
+
if (!commits && !dirty) return gitVersion;
|
|
31
|
+
|
|
32
|
+
// Build a combined semver-style dev string
|
|
33
|
+
let ver = `${gitVersion}-dev`;
|
|
34
|
+
if (commits) ver += `.${commits}`;
|
|
35
|
+
const meta = [hash ? `g${hash}` : null, dirty ? 'dirty' : null].filter(Boolean).join('.');
|
|
36
|
+
if (meta) ver += `+${meta}`;
|
|
37
|
+
return ver;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// No semver tag found (e.g. bare commit hash) — fall back to package.json
|
|
41
|
+
return `${base}-dev+${gitDesc}`;
|
|
24
42
|
} catch {
|
|
25
43
|
return `${base}-dev`;
|
|
26
44
|
}
|
package/src/websocket.js
CHANGED
|
@@ -1,14 +1,37 @@
|
|
|
1
1
|
const log = require('./logger');
|
|
2
2
|
|
|
3
|
+
const ACTIVE_THRESHOLD = 60000; // 60 seconds
|
|
4
|
+
|
|
5
|
+
// OSC color query/response sequences (OSC 4/10/11/12) cause garbled output
|
|
6
|
+
// on replay: color queries trigger xterm.js to generate responses that echo
|
|
7
|
+
// through the PTY as visible text, accumulating on each refresh.
|
|
8
|
+
const OSC_COLOR_RE = /\x1b\](?:4;\d+|10|11|12);[^\x07\x1b]*(?:\x07|\x1b\\)/g;
|
|
9
|
+
function sanitizeForReplay(buf) {
|
|
10
|
+
return buf.replace(OSC_COLOR_RE, '');
|
|
11
|
+
}
|
|
12
|
+
|
|
3
13
|
function recalcPtySize(session) {
|
|
4
|
-
|
|
5
|
-
let
|
|
14
|
+
const now = Date.now();
|
|
15
|
+
let activeCols = Infinity;
|
|
16
|
+
let activeRows = Infinity;
|
|
17
|
+
let allCols = Infinity;
|
|
18
|
+
let allRows = Infinity;
|
|
19
|
+
let hasActive = false;
|
|
20
|
+
|
|
6
21
|
for (const client of session.clients) {
|
|
7
|
-
if (client._dims)
|
|
8
|
-
|
|
9
|
-
|
|
22
|
+
if (!client._dims) continue;
|
|
23
|
+
allCols = Math.min(allCols, client._dims.cols);
|
|
24
|
+
allRows = Math.min(allRows, client._dims.rows);
|
|
25
|
+
if (client._lastActivity && now - client._lastActivity < ACTIVE_THRESHOLD) {
|
|
26
|
+
activeCols = Math.min(activeCols, client._dims.cols);
|
|
27
|
+
activeRows = Math.min(activeRows, client._dims.rows);
|
|
28
|
+
hasActive = true;
|
|
10
29
|
}
|
|
11
30
|
}
|
|
31
|
+
|
|
32
|
+
const minCols = hasActive ? activeCols : allCols;
|
|
33
|
+
const minRows = hasActive ? activeRows : allRows;
|
|
34
|
+
|
|
12
35
|
if (minCols === Infinity || minRows === Infinity) return;
|
|
13
36
|
if (minCols === session._lastCols && minRows === session._lastRows) return;
|
|
14
37
|
session._lastCols = minCols;
|
|
@@ -39,6 +62,11 @@ function setupWebSocket(wss, { auth, sessions }) {
|
|
|
39
62
|
}
|
|
40
63
|
}
|
|
41
64
|
|
|
65
|
+
const pingInterval = setInterval(() => {
|
|
66
|
+
if (ws.readyState === 1) ws.ping();
|
|
67
|
+
}, 30000);
|
|
68
|
+
if (typeof pingInterval.unref === 'function') pingInterval.unref();
|
|
69
|
+
|
|
42
70
|
let authenticated = !auth.password;
|
|
43
71
|
let attached = null;
|
|
44
72
|
|
|
@@ -95,6 +123,7 @@ function setupWebSocket(wss, { auth, sessions }) {
|
|
|
95
123
|
log.warn(`WS: attach failed — session ${msg.sessionId} not found`);
|
|
96
124
|
return;
|
|
97
125
|
}
|
|
126
|
+
ws._lastActivity = Date.now();
|
|
98
127
|
attached = session;
|
|
99
128
|
// First client: defer adding to session.clients until after the
|
|
100
129
|
// first resize so we can decide whether the PTY needs resizing.
|
|
@@ -104,7 +133,9 @@ function setupWebSocket(wss, { auth, sessions }) {
|
|
|
104
133
|
} else {
|
|
105
134
|
session.clients.add(ws);
|
|
106
135
|
if (session.scrollbackBuf.length > 0) {
|
|
107
|
-
ws.send(
|
|
136
|
+
ws.send(
|
|
137
|
+
JSON.stringify({ type: 'output', data: sanitizeForReplay(session.scrollbackBuf) }),
|
|
138
|
+
);
|
|
108
139
|
}
|
|
109
140
|
}
|
|
110
141
|
ws.send(JSON.stringify({ type: 'attached', sessionId: msg.sessionId }));
|
|
@@ -115,12 +146,14 @@ function setupWebSocket(wss, { auth, sessions }) {
|
|
|
115
146
|
if (!attached) return;
|
|
116
147
|
|
|
117
148
|
if (msg.type === 'input') {
|
|
149
|
+
ws._lastActivity = Date.now();
|
|
118
150
|
attached.pty.write(msg.data);
|
|
119
151
|
} else if (msg.type === 'resize') {
|
|
120
152
|
const cols = Math.floor(msg.cols);
|
|
121
153
|
const rows = Math.floor(msg.rows);
|
|
122
154
|
if (cols > 0 && cols <= 500 && rows > 0 && rows <= 200) {
|
|
123
155
|
ws._dims = { cols, rows };
|
|
156
|
+
ws._lastActivity = Date.now();
|
|
124
157
|
if (ws._pendingResize) {
|
|
125
158
|
ws._pendingResize = false;
|
|
126
159
|
// Only discard scrollback and send SIGWINCH if the PTY was
|
|
@@ -136,7 +169,12 @@ function setupWebSocket(wss, { auth, sessions }) {
|
|
|
136
169
|
} else {
|
|
137
170
|
attached.clients.add(ws);
|
|
138
171
|
if (attached.scrollbackBuf.length > 0) {
|
|
139
|
-
ws.send(
|
|
172
|
+
ws.send(
|
|
173
|
+
JSON.stringify({
|
|
174
|
+
type: 'output',
|
|
175
|
+
data: sanitizeForReplay(attached.scrollbackBuf),
|
|
176
|
+
}),
|
|
177
|
+
);
|
|
140
178
|
}
|
|
141
179
|
}
|
|
142
180
|
} else {
|
|
@@ -150,6 +188,7 @@ function setupWebSocket(wss, { auth, sessions }) {
|
|
|
150
188
|
});
|
|
151
189
|
|
|
152
190
|
ws.on('close', () => {
|
|
191
|
+
clearInterval(pingInterval);
|
|
153
192
|
if (attached) {
|
|
154
193
|
attached.clients.delete(ws);
|
|
155
194
|
recalcPtySize(attached);
|
|
@@ -159,4 +198,4 @@ function setupWebSocket(wss, { auth, sessions }) {
|
|
|
159
198
|
});
|
|
160
199
|
}
|
|
161
200
|
|
|
162
|
-
module.exports = { setupWebSocket };
|
|
201
|
+
module.exports = { setupWebSocket, ACTIVE_THRESHOLD, sanitizeForReplay };
|