131 lines
3.9 KiB
TypeScript
131 lines
3.9 KiB
TypeScript
/*
|
|
* 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<void> {
|
|
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<unknown> = Promise.resolve();
|
|
let queryCount = 0;
|
|
let burstUntil = 0;
|
|
|
|
/** Serialize an outbound send with the human-emulation delay. */
|
|
export function pacedSend<T>(task: () => Promise<T>): Promise<T> {
|
|
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<T>(task: () => Promise<T>): Promise<T> {
|
|
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<T>(task: () => Promise<T>): Promise<T> {
|
|
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<string, Promise<unknown>>();
|
|
|
|
/** Coalesce identical concurrent queries into a single request. */
|
|
export function dedupe<T>(key: string, task: () => Promise<T>): Promise<T> {
|
|
const existing = inFlight.get(key);
|
|
if (existing) return existing as Promise<T>;
|
|
const promise = task().finally(() => inFlight.delete(key));
|
|
inFlight.set(key, promise);
|
|
return promise;
|
|
}
|