mirror of
https://github.com/ReCloudStudio/WebHooker.git
synced 2026-09-22 16:11:29 +00:00
133 lines
3.8 KiB
TypeScript
133 lines
3.8 KiB
TypeScript
import type { Env } from "../types";
|
|
|
|
export type DeliveryStatus =
|
|
"pending" | "processing" | "delivered" | "retrying" | "failed" | "dead";
|
|
|
|
export interface DeliveryMessage {
|
|
deliveryId: string;
|
|
groupId?: string;
|
|
provider: string;
|
|
event: string;
|
|
payload?: Record<string, unknown>;
|
|
payloadRef?: string;
|
|
installationId?: number;
|
|
receivedAt: number;
|
|
requestId?: string;
|
|
}
|
|
|
|
export interface DispatchFailure {
|
|
target: string;
|
|
error?: string;
|
|
errorCode?: string;
|
|
status?: number;
|
|
}
|
|
|
|
export interface DispatchSummary {
|
|
attempts: number;
|
|
failures: DispatchFailure[];
|
|
}
|
|
|
|
export const DELIVERY_QUEUE = "webhooker-delivery";
|
|
export const DELIVERY_DLQ = "webhooker-delivery-dlq";
|
|
|
|
const MAX_QUEUE_MESSAGE_BYTES = 100_000;
|
|
const PAYLOAD_KV_TTL_SECONDS = 3600;
|
|
const STATE_KV_TTL_SECONDS = 3600;
|
|
|
|
const RETRYABLE_ERROR_CODES = new Set(["DISCORD_5XX", "TELEGRAM_5XX", "NETWORK", "RETRIES"]);
|
|
|
|
const RETRY_DELAYS_SECONDS = [5, 30, 120, 600];
|
|
|
|
export function isRetryableError(code?: string): boolean {
|
|
return code == null || RETRYABLE_ERROR_CODES.has(code);
|
|
}
|
|
|
|
export function classifyDelivery(summary: DispatchSummary): {
|
|
failed: boolean;
|
|
retryable: boolean;
|
|
} {
|
|
if (summary.failures.length === 0) return { failed: false, retryable: false };
|
|
const retryable = summary.failures.every((f) => isRetryableError(f.errorCode));
|
|
const permanent = summary.failures.some((f) => !isRetryableError(f.errorCode));
|
|
return { failed: true, retryable: retryable && !permanent };
|
|
}
|
|
|
|
export function retryDelay(attempt: number): number {
|
|
if (attempt < 1) return RETRY_DELAYS_SECONDS[0];
|
|
const idx = Math.min(attempt - 1, RETRY_DELAYS_SECONDS.length - 1);
|
|
return RETRY_DELAYS_SECONDS[idx];
|
|
}
|
|
|
|
function scopeKey(provider: string, groupId: string | undefined, deliveryId: string): string {
|
|
return `${provider}:${groupId ?? "global"}:${deliveryId}`;
|
|
}
|
|
|
|
export function deliveryStateKey(
|
|
provider: string,
|
|
groupId: string | undefined,
|
|
deliveryId: string,
|
|
): string {
|
|
return `delivery-state:${scopeKey(provider, groupId, deliveryId)}`;
|
|
}
|
|
|
|
function payloadKey(provider: string, groupId: string | undefined, deliveryId: string): string {
|
|
return `queue:payload:${scopeKey(provider, groupId, deliveryId)}`;
|
|
}
|
|
|
|
export async function setDeliveryState(
|
|
env: Env,
|
|
key: string,
|
|
status: DeliveryStatus,
|
|
): Promise<void> {
|
|
await env.KV.put(key, JSON.stringify({ status, at: Date.now() }), {
|
|
expirationTtl: STATE_KV_TTL_SECONDS,
|
|
});
|
|
}
|
|
|
|
export async function getDeliveryState(env: Env, key: string): Promise<DeliveryStatus | null> {
|
|
const raw = await env.KV.get(key);
|
|
if (!raw) return null;
|
|
try {
|
|
return (JSON.parse(raw) as { status?: DeliveryStatus }).status ?? null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function enqueueWebhook(env: Env, message: DeliveryMessage): Promise<void> {
|
|
const queue = env.QUEUE;
|
|
if (!queue) throw new Error("QUEUE binding is not configured");
|
|
const { payload, ...rest } = message;
|
|
const direct: DeliveryMessage = { ...rest, payload };
|
|
if (JSON.stringify(direct).length <= MAX_QUEUE_MESSAGE_BYTES) {
|
|
await queue.send(direct);
|
|
return;
|
|
}
|
|
const payloadRef = payloadKey(message.provider, message.groupId, message.deliveryId);
|
|
await env.KV.put(payloadRef, JSON.stringify(payload ?? {}), {
|
|
expirationTtl: PAYLOAD_KV_TTL_SECONDS,
|
|
});
|
|
await queue.send({ ...rest, payloadRef });
|
|
}
|
|
|
|
export async function resolvePayload(
|
|
env: Env,
|
|
message: DeliveryMessage,
|
|
): Promise<Record<string, unknown>> {
|
|
if (message.payload) return message.payload;
|
|
if (message.payloadRef) {
|
|
const raw = await env.KV.get(message.payloadRef);
|
|
if (raw) {
|
|
try {
|
|
return JSON.parse(raw) as Record<string, unknown>;
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
}
|
|
return {};
|
|
}
|
|
|
|
export async function discardPayload(env: Env, message: DeliveryMessage): Promise<void> {
|
|
if (message.payloadRef) await env.KV.delete(message.payloadRef);
|
|
}
|