mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-14 06:16:20 -06:00
Field-safe SERVER net. A device opening duplicate/rapid sockets (the APK duplicate-socket bug, separate track) currently churns through evictions during the reconnect-throttle's 30s post-restart WARM-UP (only the hard ceiling 20 applies then, so an 8-in-9s burst passes undamped and each new socket evicts the prior). This makes the server absorb it: a thrashing PAIRED device converges to ONE stable connection and stays online. - lib/session-settle.js (decision only; bounded, swept): shouldHold(deviceId, incumbentAlive) — true only when a socket was accepted for this device within SESSION_SETTLE_WINDOW_MS (config, default 2500ms) AND the incumbent is alive. Warm-up-independent. - deviceSocket register gate (just before evictPriorSocket): if a LIVE incumbent exists and we're inside the window, SOFT-REFUSE the new socket (device:throttled reason=session_settle + disconnect) and keep the incumbent; else accept + evict + (re)arm the window. - LIVENESS SAFEGUARD (load-bearing): only hold when the incumbent socket is actually in the /device namespace — a dead/half-open incumbent is replaced, NEVER stranding the device (max hold is the 2.5s window from the incumbent's accept, then any new socket is accepted). - Soft refusal, NEVER a quarantine (reuses patch1's paired-safe philosophy); single-session enforcement intact for a legitimate move; unpaired/abusive flapping still caught by the existing limiters. O(1), no loop impact. Tests (liveness first-class): live incumbent holds + DEAD incumbent replaced (not stranded); storm of 6 sockets converges to ONE, stays online, not quarantined (during warm-up); single- session move past the window replaces cleanly; unit decision + bounded sweep. The evicted-socket-rearm test shrinks its settle window so it still exercises the eviction path. Suite 336/336.
49 lines
2.2 KiB
JavaScript
49 lines
2.2 KiB
JavaScript
'use strict';
|
|
|
|
// #148 patch2 — per-device SESSION-SETTLE debounce (the field-safe net). When a device opens
|
|
// duplicate/rapid sockets in a burst, keep it on ONE live incumbent connection and soft-refuse
|
|
// the duplicates, so it converges and STAYS ONLINE — instead of churning through evictions.
|
|
// This closes the gap the reconnect-throttle's 30s post-restart warm-up leaves open (during
|
|
// warm-up only the hard ceiling applies, so an 8-in-9s duplicate burst passes undamped and
|
|
// each new socket evicts the prior). This debounce is warm-up-INDEPENDENT.
|
|
//
|
|
// DECISION ONLY: this module never touches sockets or the DB. The caller supplies whether the
|
|
// incumbent is actually alive (the LIVENESS SAFEGUARD) and does the refuse/disconnect. Bounded:
|
|
// one small timestamp per device_id, idle entries swept.
|
|
|
|
const config = require('../config');
|
|
|
|
const state = new Map(); // device_id -> lastAcceptedMs
|
|
|
|
// Should this NEW socket be soft-refused (keep the incumbent)? TRUE only when a socket was
|
|
// accepted for this device within the settle window AND the incumbent is genuinely alive.
|
|
// incumbentAlive is the load-bearing safeguard: if the incumbent is dead/half-open the caller
|
|
// passes false and we return false -> accept the new socket, never stranding the device.
|
|
function shouldHold(deviceId, incumbentAlive, now = Date.now()) {
|
|
if (!incumbentAlive) return false;
|
|
const last = state.get(deviceId) || 0;
|
|
return (now - last) < config.sessionSettleWindowMs;
|
|
}
|
|
|
|
// Record that a socket was ACCEPTED (evicted+registered) for this device — (re)arms the window.
|
|
function accepted(deviceId, now = Date.now()) { state.set(deviceId, now); }
|
|
|
|
// Bounded: drop entries idle well past the window.
|
|
function sweep(now = Date.now()) {
|
|
let n = 0;
|
|
for (const [k, t] of state) if (now - t > config.sessionSettleWindowMs * 4) { state.delete(k); n++; }
|
|
return n;
|
|
}
|
|
let sweepTimer = null;
|
|
function startSweep() {
|
|
if (sweepTimer) return sweepTimer;
|
|
sweepTimer = setInterval(() => sweep(), 60000);
|
|
if (sweepTimer.unref) sweepTimer.unref();
|
|
return sweepTimer;
|
|
}
|
|
|
|
function reset() { state.clear(); }
|
|
function _size() { return state.size; }
|
|
|
|
module.exports = { shouldHold, accepted, sweep, startSweep, reset, _size };
|