screentinker/server
ScreenTinker e1ce36b2a8 fix(#148) patch2: per-device session-settle debounce — absorb duplicate-socket storms
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.
2026-07-02 19:12:46 -05:00
..
config PiP overlay MVP: push image/web overlays to a device or group (#109) (#127) 2026-06-18 14:54:44 -05:00
db feat(#146): billing:read scoped token — dual-path auth for the Usage Report (Option C) 2026-07-01 21:16:21 -05:00
lib fix(#148) patch2: per-device session-settle debounce — absorb duplicate-socket storms 2026-07-02 19:12:46 -05:00
middleware feat(#146): billing:read scoped token — dual-path auth for the Usage Report (Option C) 2026-07-01 21:16:21 -05:00
player fix(#146): web player — guard PlayerMediaHealth calls by METHOD, not object (stale-module TypeError) 2026-07-01 22:19:22 -05:00
routes feat(#146): billing:read scoped token — dual-path auth for the Usage Report (Option C) 2026-07-01 21:16:21 -05:00
scripts feat(scheduling): per-item schedule blocks (#74 dayparting, #75 auto-expire) 2026-06-11 15:46:41 -05:00
services fix(#148) Items 2-4: mark-offline closes the socket + tighten ping + TCP keepalive 2026-07-02 14:59:25 -05:00
test fix(#148) patch2: per-device session-settle debounce — absorb duplicate-socket storms 2026-07-02 19:12:46 -05:00
ws fix(#148) patch2: per-device session-settle debounce — absorb duplicate-socket storms 2026-07-02 19:12:46 -05:00
.gitignore feat(email): Microsoft Graph send + alert spam protection + preferences UI 2026-05-12 18:16:40 -05:00
config.js fix(#148) patch2: per-device session-settle debounce — absorb duplicate-socket storms 2026-07-02 19:12:46 -05:00
package-lock.json chore(release): v1.9.2-patch1 2026-07-02 15:00:40 -05:00
package.json chore(release): v1.9.2-patch1 2026-07-02 15:00:40 -05:00
server.js fix(#148) patch2: per-device session-settle debounce — absorb duplicate-socket storms 2026-07-02 19:12:46 -05:00
version.js chore(version): single-source VERSION, env-configurable data paths, bump tooling 2026-06-10 12:56:03 -05:00