64 lines
2.1 KiB
TypeScript
64 lines
2.1 KiB
TypeScript
import db from "../../db.server";
|
|
|
|
/**
|
|
* Periodic TTL cleanup for the `ProcessedWebhook` idempotency table.
|
|
*
|
|
* The table grows by one row per Shopify webhook delivery and is never read
|
|
* after the retry window closes, so without pruning it grows unbounded —
|
|
* eventually a disk/space DoS. We only need rows for as long as Shopify might
|
|
* retry a delivery (hours), so a generous retention window of a few days is
|
|
* ample while keeping the table small.
|
|
*/
|
|
const RETENTION_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
const INTERVAL_MS = 60 * 60 * 1000; // hourly
|
|
|
|
export interface CleanupDeps {
|
|
db: {
|
|
processedWebhook: {
|
|
deleteMany: (args: {
|
|
where: { receivedAt: { lt: Date } };
|
|
}) => Promise<{ count: number }>;
|
|
};
|
|
};
|
|
}
|
|
|
|
let scheduled = false;
|
|
|
|
async function runCleanup(deps: CleanupDeps): Promise<void> {
|
|
try {
|
|
const cutoff = new Date(Date.now() - RETENTION_MS);
|
|
const { count } = await deps.db.processedWebhook.deleteMany({
|
|
where: { receivedAt: { lt: cutoff } },
|
|
});
|
|
if (count > 0) {
|
|
console.log(`webhook-cleanup: removed ${count} ProcessedWebhook row(s) older than 7d`);
|
|
}
|
|
} catch (err) {
|
|
// Best-effort housekeeping — never throw into the caller.
|
|
console.warn("webhook-cleanup: prune failed:", err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Idempotently schedule the hourly cleanup. Safe to call on every webhook —
|
|
* the first call starts a single unref'd interval and runs an immediate
|
|
* sweep; subsequent calls are no-ops.
|
|
*
|
|
* Because this is only ever invoked while handling a live webhook request, it
|
|
* never runs during `prisma generate` / `react-router build` or other CLI
|
|
* contexts. The interval is `unref`'d so it can never keep the process alive.
|
|
*/
|
|
export function ensureWebhookCleanupScheduled(deps: CleanupDeps = { db }): void {
|
|
if (scheduled) return;
|
|
scheduled = true;
|
|
|
|
const timer = setInterval(() => {
|
|
void runCleanup(deps);
|
|
}, INTERVAL_MS);
|
|
// Don't let the housekeeping interval keep the event loop alive on shutdown.
|
|
if (typeof timer.unref === "function") timer.unref();
|
|
|
|
// Kick off an immediate sweep so a long-lived process prunes promptly.
|
|
void runCleanup(deps);
|
|
}
|