fix(wh): add a global throttle queue
Build and Deploy / build-and-deploy (push) Successful in 1m37s

This commit is contained in:
2026-07-14 17:21:45 +07:00 Unverified
parent 4f317dda9b
commit 222218feb3
+66 -8
View File
@@ -10,7 +10,17 @@ type Pending = {
count: number; count: number;
}; };
type ForwardJob = {
id: string;
url: string;
data: unknown;
};
const pending = new Map<string, Pending>(); const pending = new Map<string, Pending>();
const forwardQueue: ForwardJob[] = [];
const recentForwardTimestamps: number[] = [];
let processingForwardQueue = false;
const log = createLogger("WH"); const log = createLogger("WH");
const routes = parseRouteMap(env.WH_ROUTE_MAP); const routes = parseRouteMap(env.WH_ROUTE_MAP);
@@ -46,9 +56,9 @@ export function webhookMerger(app: App) {
if (JSON.stringify(merged).length > 2000) { if (JSON.stringify(merged).length > 2000) {
existing.data = body; existing.data = body;
existing.count = 1; existing.count = 1;
forward(id, url, oldData); queueForward(id, url, oldData);
existing.timer = setTimeout( existing.timer = setTimeout(
() => forward(id, url, existing.data), () => flushPending(id, url, existing),
bufferTime, bufferTime,
); );
return status(202); return status(202);
@@ -56,9 +66,9 @@ export function webhookMerger(app: App) {
if (flushed.size > 0) { if (flushed.size > 0) {
existing.count = 1; existing.count = 1;
forward(id, url, oldData); queueForward(id, url, oldData);
existing.timer = setTimeout( existing.timer = setTimeout(
() => forward(id, url, existing.data), () => flushPending(id, url, existing),
bufferTime, bufferTime,
); );
return status(202); return status(202);
@@ -67,7 +77,7 @@ export function webhookMerger(app: App) {
log`collected ${existing}`; log`collected ${existing}`;
existing.timer = setTimeout( existing.timer = setTimeout(
() => forward(id, url, existing.data), () => flushPending(id, url, existing),
bufferTime, bufferTime,
); );
@@ -82,7 +92,7 @@ export function webhookMerger(app: App) {
log`collected ${state}`; log`collected ${state}`;
state.timer = setTimeout(() => forward(id, url, state.data), bufferTime); state.timer = setTimeout(() => flushPending(id, url, state), bufferTime);
pending.set(id, state); pending.set(id, state);
@@ -130,6 +140,54 @@ function parseRouteMap(raw: string): RouteMap {
} }
const MAX_FIELD_LENGTH = Number(env.MAX_FIELD_LENGTH) || 4000; const MAX_FIELD_LENGTH = Number(env.MAX_FIELD_LENGTH) || 4000;
const MAX_FORWARDS_PER_WINDOW = 5;
const FORWARD_WINDOW_MS = 2000;
function flushPending(id: string, url: string, state: Pending) {
if (pending.get(id) === state) pending.delete(id);
queueForward(id, url, state.data);
}
function queueForward(id: string, url: string, data: unknown) {
forwardQueue.push({ id, url, data });
void processForwardQueue();
}
async function processForwardQueue() {
if (processingForwardQueue) return;
processingForwardQueue = true;
try {
while (forwardQueue.length > 0) {
const now = Date.now();
while (
recentForwardTimestamps.length > 0 &&
now - recentForwardTimestamps[0] >= FORWARD_WINDOW_MS
) {
recentForwardTimestamps.shift();
}
if (recentForwardTimestamps.length >= MAX_FORWARDS_PER_WINDOW) {
await Bun.sleep(
FORWARD_WINDOW_MS - (now - recentForwardTimestamps[0]),
);
continue;
}
const job = forwardQueue.shift();
if (!job) continue;
recentForwardTimestamps.push(Date.now());
await forward(job.id, job.url, job.data);
}
} finally {
processingForwardQueue = false;
}
}
async function forward(id: string, url: string, data: unknown) { async function forward(id: string, url: string, data: unknown) {
log`running ${id}`; log`running ${id}`;
@@ -141,8 +199,8 @@ async function forward(id: string, url: string, data: unknown) {
}); });
if (!res.ok) if (!res.ok)
log.warn`Could not forward webhook: ${res.status}\n${await res.json().catch(() => res.text())}`; log.warn`Could not forward webhook: ${res.status}\n${await res.json().catch(() => res.text())}`;
} finally { } catch (error) {
pending.delete(id); log.warn`Could not forward webhook: ${error}`;
} }
} }