feat(push): smart push queueing with page-visibility fast path and app-ping/pong fallback

This commit is contained in:
2026-06-04 22:10:48 -04:00
parent fc0527e9e7
commit 4e6dfb4726
16 changed files with 192 additions and 60 deletions
+2
View File
@@ -12,6 +12,8 @@ export abstract class ClientConnection {
readonly transportName: string;
sessionId: string | null = null;
onDisconnect: ((conn: ClientConnection) => void) | null = null;
/** True while the client tab/app is in the foreground. Starts true (assume visible until told otherwise). */
pageVisible: boolean = true;
constructor(transportName: string) {
this.transportName = transportName;
+12 -3
View File
@@ -18,6 +18,9 @@ import type { ClientMessage } from '../types/messages.js';
export class WebSocketTransport extends EventEmitter {
private wss: WebSocketServer | null = null;
private pingInterval: ReturnType<typeof setInterval> | null = null;
// Tracks whether each WS responded to the last ping. Initialized true so a
// connection is never terminated before it has a chance to respond.
private alive = new WeakMap<WebSocket, boolean>();
/** Create WebSocketServer on /ws path with JWT verification and ping/pong keepalive. */
setup(server: HttpServer | HttpsServer): void {
@@ -36,6 +39,9 @@ export class WebSocketTransport extends EventEmitter {
});
this.wss.on('connection', (ws: WebSocket) => {
this.alive.set(ws, true);
ws.on('pong', () => this.alive.set(ws, true));
const conn = new WebSocketConnection(ws);
this.emit('connection', conn);
@@ -52,13 +58,16 @@ export class WebSocketTransport extends EventEmitter {
});
});
// Ping/pong keepalive every 30s
// Ping/pong keepalive every 30s — terminate connections that miss a pong.
this.pingInterval = setInterval(() => {
if (!this.wss) return;
for (const ws of this.wss.clients) {
if (ws.readyState === 1) {
ws.ping();
if (!this.alive.get(ws)) {
ws.terminate(); // no pong since last ping — dead connection
continue;
}
this.alive.set(ws, false);
if (ws.readyState === 1) ws.ping();
}
}, 30_000);
this.pingInterval.unref();