/* * Veskslop daemon — pacing. * * Outbound sends use a human-emulation policy: the first send of a burst goes * out quickly (30-200 ms), later sends in the same burst are spaced 500-2800 ms. * A burst ends after a random 5-20 s of quiet. Read queries are serialized with * a small gap so they do not look like a scan. * * The core adds a second, hard rate limit. See docs/core-bridge.md. */ export function sleep(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } export const PACING = { firstMinMs: 30, firstMaxMs: 200, burstMinMs: 500, burstMaxMs: 2800, idleMinMs: 5000, idleMaxMs: 20000, readMinMs: 100, readMaxMs: 300, voiceMinMs: 250, voiceMaxMs: 750 }; function randomBetween(min: number, max: number): number { return min + Math.floor(Math.random() * (max - min + 1)); } /** Delay before the first send of a burst. */ export function firstActionDelay(): number { return randomBetween(PACING.firstMinMs, PACING.firstMaxMs); } /** Delay between sends inside one burst. */ export function burstActionDelay(): number { return randomBetween(PACING.burstMinMs, PACING.burstMaxMs); } /** How long the current burst stays active after a send. */ export function burstWindow(): number { return randomBetween(PACING.idleMinMs, PACING.idleMaxMs); } /** * Plan the next outbound send. Pure so the policy is testable. * A send while `now > burstUntil` starts a new burst with a fast first action. */ export function planSend(now: number, burstUntil: number): { delay: number; burstUntil: number } { const delay = now > burstUntil ? firstActionDelay() : burstActionDelay(); return { delay, burstUntil: now + burstWindow() }; } let queue: Promise = Promise.resolve(); let queryCount = 0; let burstUntil = 0; /** Serialize an outbound send with the human-emulation delay. */ export function pacedSend(task: () => Promise): Promise { const run = queue.then(async () => { const plan = planSend(Date.now(), burstUntil); burstUntil = plan.burstUntil; await sleep(plan.delay); queryCount++; if (process.env.DEBUG === "1") { console.error(`[send] query #${queryCount} (+${plan.delay}ms)`); } return task(); }); queue = run.then( () => undefined, () => undefined ); return run; } /** Serialize a read query with a small gap. Reads do not affect the send burst. */ export function paced(task: () => Promise): Promise { const delay = randomBetween(PACING.readMinMs, PACING.readMaxMs); const run = queue.then(async () => { await sleep(delay); queryCount++; if (process.env.DEBUG === "1") { console.error(`[read] query #${queryCount} (+${delay}ms)`); } return task(); }); queue = run.then( () => undefined, () => undefined ); return run; } /** Serialize a voice action with a short gap. Voice state changes are not sends. */ export function pacedVoice(task: () => Promise): Promise { const delay = randomBetween(PACING.voiceMinMs, PACING.voiceMaxMs); const run = queue.then(async () => { await sleep(delay); if (process.env.DEBUG === "1") { console.error(`[voice] action (+${delay}ms)`); } return task(); }); queue = run.then( () => undefined, () => undefined ); return run; } export function getQueryCount(): number { return queryCount; } const inFlight = new Map>(); /** Coalesce identical concurrent queries into a single request. */ export function dedupe(key: string, task: () => Promise): Promise { const existing = inFlight.get(key); if (existing) return existing as Promise; const promise = task().finally(() => inFlight.delete(key)); inFlight.set(key, promise); return promise; }