2026-06-08 22:33:34 -05:00
|
|
|
import { upsertClientRows, type ClientDbRow } from "./db";
|
|
|
|
|
import { metrics } from "./metrics";
|
2026-05-29 15:41:08 -05:00
|
|
|
|
|
|
|
|
export const CLIENT_DB_SYNC_INTERVAL_MS = Number(process.env.OVERLORD_CLIENT_DB_SYNC_MS || 5000);
|
2026-06-08 22:33:34 -05:00
|
|
|
export const CLIENT_DB_SYNC_BATCH_SIZE = Math.max(
|
|
|
|
|
1,
|
|
|
|
|
Number(process.env.OVERLORD_CLIENT_DB_SYNC_BATCH_SIZE || 100),
|
|
|
|
|
);
|
2026-05-29 15:41:08 -05:00
|
|
|
|
|
|
|
|
const lastClientDbSync = new Map<string, number>();
|
|
|
|
|
const pendingClientDbUpdates = new Map<string, ClientDbRow>();
|
2026-06-08 22:33:34 -05:00
|
|
|
let flushSoonTimer: ReturnType<typeof setTimeout> | null = null;
|
2026-05-29 15:41:08 -05:00
|
|
|
|
|
|
|
|
export function queueClientDbUpdate(partial: ClientDbRow): void {
|
|
|
|
|
const existing = pendingClientDbUpdates.get(partial.id);
|
|
|
|
|
if (!existing) {
|
|
|
|
|
pendingClientDbUpdates.set(partial.id, { ...partial });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
pendingClientDbUpdates.set(partial.id, {
|
|
|
|
|
...existing,
|
|
|
|
|
...partial,
|
|
|
|
|
id: partial.id,
|
|
|
|
|
lastSeen: partial.lastSeen ?? existing.lastSeen,
|
|
|
|
|
online: partial.online ?? existing.online,
|
|
|
|
|
pingMs: partial.pingMs ?? existing.pingMs,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function flushQueuedClientDbUpdates(): void {
|
|
|
|
|
if (pendingClientDbUpdates.size === 0) return;
|
2026-06-08 22:33:34 -05:00
|
|
|
|
|
|
|
|
const startedAt = Date.now();
|
|
|
|
|
let processed = 0;
|
|
|
|
|
const updates: ClientDbRow[] = [];
|
|
|
|
|
for (const [clientId, update] of pendingClientDbUpdates.entries()) {
|
|
|
|
|
updates.push(update);
|
|
|
|
|
pendingClientDbUpdates.delete(clientId);
|
|
|
|
|
processed += 1;
|
|
|
|
|
if (processed >= CLIENT_DB_SYNC_BATCH_SIZE) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
2026-05-29 15:41:08 -05:00
|
|
|
}
|
2026-06-08 22:33:34 -05:00
|
|
|
upsertClientRows(updates);
|
|
|
|
|
|
|
|
|
|
if (pendingClientDbUpdates.size > 0 && !flushSoonTimer) {
|
|
|
|
|
flushSoonTimer = setTimeout(() => {
|
|
|
|
|
flushSoonTimer = null;
|
|
|
|
|
flushQueuedClientDbUpdates();
|
|
|
|
|
}, 25);
|
|
|
|
|
}
|
|
|
|
|
metrics.recordInternalTask("client-db-flush", Date.now() - startedAt);
|
2026-05-29 15:41:08 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setInterval(flushQueuedClientDbUpdates, CLIENT_DB_SYNC_INTERVAL_MS);
|
|
|
|
|
|
|
|
|
|
export function shouldSyncClientToDb(clientId: string, now: number): boolean {
|
|
|
|
|
const last = lastClientDbSync.get(clientId) || 0;
|
|
|
|
|
if (now - last < CLIENT_DB_SYNC_INTERVAL_MS) return false;
|
|
|
|
|
lastClientDbSync.set(clientId, now);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function markClientDbSynced(clientId: string, now: number): void {
|
|
|
|
|
lastClientDbSync.set(clientId, now);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function clearClientSyncState(clientId: string): void {
|
|
|
|
|
lastClientDbSync.delete(clientId);
|
|
|
|
|
pendingClientDbUpdates.delete(clientId);
|
|
|
|
|
}
|
2026-06-08 22:33:34 -05:00
|
|
|
|
|
|
|
|
export function getClientDbSyncStats(): {
|
|
|
|
|
trackedClients: number;
|
|
|
|
|
pendingUpdates: number;
|
|
|
|
|
flushScheduled: boolean;
|
|
|
|
|
} {
|
|
|
|
|
return {
|
|
|
|
|
trackedClients: lastClientDbSync.size,
|
|
|
|
|
pendingUpdates: pendingClientDbUpdates.size,
|
|
|
|
|
flushScheduled: flushSoonTimer !== null,
|
|
|
|
|
};
|
|
|
|
|
}
|