UNPKG

@gguf/claw

Version:

Multi-channel AI gateway with extensible messaging integrations

219 lines (217 loc) 7.26 kB
import { t as __exportAll } from "./rolldown-runtime-Cbj13DAv.js"; import { Yt as resolveStateDir } from "./entry.js"; import path from "node:path"; import fs from "node:fs"; import crypto from "node:crypto"; //#region src/infra/outbound/delivery-queue.ts var delivery_queue_exports = /* @__PURE__ */ __exportAll({ MAX_RETRIES: () => MAX_RETRIES, ackDelivery: () => ackDelivery, computeBackoffMs: () => computeBackoffMs, enqueueDelivery: () => enqueueDelivery, ensureQueueDir: () => ensureQueueDir, failDelivery: () => failDelivery, loadPendingDeliveries: () => loadPendingDeliveries, moveToFailed: () => moveToFailed, recoverPendingDeliveries: () => recoverPendingDeliveries }); const QUEUE_DIRNAME = "delivery-queue"; const FAILED_DIRNAME = "failed"; const MAX_RETRIES = 5; /** Backoff delays in milliseconds indexed by retry count (1-based). */ const BACKOFF_MS = [ 5e3, 25e3, 12e4, 6e5 ]; function resolveQueueDir(stateDir) { const base = stateDir ?? resolveStateDir(); return path.join(base, QUEUE_DIRNAME); } function resolveFailedDir(stateDir) { return path.join(resolveQueueDir(stateDir), FAILED_DIRNAME); } /** Ensure the queue directory (and failed/ subdirectory) exist. */ async function ensureQueueDir(stateDir) { const queueDir = resolveQueueDir(stateDir); await fs.promises.mkdir(queueDir, { recursive: true, mode: 448 }); await fs.promises.mkdir(resolveFailedDir(stateDir), { recursive: true, mode: 448 }); return queueDir; } async function enqueueDelivery(params, stateDir) { const queueDir = await ensureQueueDir(stateDir); const id = crypto.randomUUID(); const entry = { id, enqueuedAt: Date.now(), channel: params.channel, to: params.to, accountId: params.accountId, payloads: params.payloads, threadId: params.threadId, replyToId: params.replyToId, bestEffort: params.bestEffort, gifPlayback: params.gifPlayback, silent: params.silent, mirror: params.mirror, retryCount: 0 }; const filePath = path.join(queueDir, `${id}.json`); const tmp = `${filePath}.${process.pid}.tmp`; const json = JSON.stringify(entry, null, 2); await fs.promises.writeFile(tmp, json, { encoding: "utf-8", mode: 384 }); await fs.promises.rename(tmp, filePath); return id; } /** Remove a successfully delivered entry from the queue. */ async function ackDelivery(id, stateDir) { const filePath = path.join(resolveQueueDir(stateDir), `${id}.json`); try { await fs.promises.unlink(filePath); } catch (err) { if ((err && typeof err === "object" && "code" in err ? String(err.code) : null) !== "ENOENT") throw err; } } /** Update a queue entry after a failed delivery attempt. */ async function failDelivery(id, error, stateDir) { const filePath = path.join(resolveQueueDir(stateDir), `${id}.json`); const raw = await fs.promises.readFile(filePath, "utf-8"); const entry = JSON.parse(raw); entry.retryCount += 1; entry.lastError = error; const tmp = `${filePath}.${process.pid}.tmp`; await fs.promises.writeFile(tmp, JSON.stringify(entry, null, 2), { encoding: "utf-8", mode: 384 }); await fs.promises.rename(tmp, filePath); } /** Load all pending delivery entries from the queue directory. */ async function loadPendingDeliveries(stateDir) { const queueDir = resolveQueueDir(stateDir); let files; try { files = await fs.promises.readdir(queueDir); } catch (err) { if ((err && typeof err === "object" && "code" in err ? String(err.code) : null) === "ENOENT") return []; throw err; } const entries = []; for (const file of files) { if (!file.endsWith(".json")) continue; const filePath = path.join(queueDir, file); try { if (!(await fs.promises.stat(filePath)).isFile()) continue; const raw = await fs.promises.readFile(filePath, "utf-8"); entries.push(JSON.parse(raw)); } catch {} } return entries; } /** Move a queue entry to the failed/ subdirectory. */ async function moveToFailed(id, stateDir) { const queueDir = resolveQueueDir(stateDir); const failedDir = resolveFailedDir(stateDir); await fs.promises.mkdir(failedDir, { recursive: true, mode: 448 }); const src = path.join(queueDir, `${id}.json`); const dest = path.join(failedDir, `${id}.json`); await fs.promises.rename(src, dest); } /** Compute the backoff delay in ms for a given retry count. */ function computeBackoffMs(retryCount) { if (retryCount <= 0) return 0; return BACKOFF_MS[Math.min(retryCount - 1, BACKOFF_MS.length - 1)] ?? BACKOFF_MS.at(-1) ?? 0; } /** * On gateway startup, scan the delivery queue and retry any pending entries. * Uses exponential backoff and moves entries that exceed MAX_RETRIES to failed/. */ async function recoverPendingDeliveries(opts) { const pending = await loadPendingDeliveries(opts.stateDir); if (pending.length === 0) return { recovered: 0, failed: 0, skipped: 0 }; pending.sort((a, b) => a.enqueuedAt - b.enqueuedAt); opts.log.info(`Found ${pending.length} pending delivery entries — starting recovery`); const delayFn = opts.delay ?? ((ms) => new Promise((r) => setTimeout(r, ms))); const deadline = Date.now() + (opts.maxRecoveryMs ?? 6e4); let recovered = 0; let failed = 0; let skipped = 0; for (const entry of pending) { const now = Date.now(); if (now >= deadline) { const deferred = pending.length - recovered - failed - skipped; opts.log.warn(`Recovery time budget exceeded — ${deferred} entries deferred to next restart`); break; } if (entry.retryCount >= MAX_RETRIES) { opts.log.warn(`Delivery ${entry.id} exceeded max retries (${entry.retryCount}/${MAX_RETRIES}) — moving to failed/`); try { await moveToFailed(entry.id, opts.stateDir); } catch (err) { opts.log.error(`Failed to move entry ${entry.id} to failed/: ${String(err)}`); } skipped += 1; continue; } const backoff = computeBackoffMs(entry.retryCount + 1); if (backoff > 0) { if (now + backoff >= deadline) { const deferred = pending.length - recovered - failed - skipped; opts.log.warn(`Recovery time budget exceeded — ${deferred} entries deferred to next restart`); break; } opts.log.info(`Waiting ${backoff}ms before retrying delivery ${entry.id}`); await delayFn(backoff); } try { await opts.deliver({ cfg: opts.cfg, channel: entry.channel, to: entry.to, accountId: entry.accountId, payloads: entry.payloads, threadId: entry.threadId, replyToId: entry.replyToId, bestEffort: entry.bestEffort, gifPlayback: entry.gifPlayback, silent: entry.silent, mirror: entry.mirror, skipQueue: true }); await ackDelivery(entry.id, opts.stateDir); recovered += 1; opts.log.info(`Recovered delivery ${entry.id} to ${entry.channel}:${entry.to}`); } catch (err) { try { await failDelivery(entry.id, err instanceof Error ? err.message : String(err), opts.stateDir); } catch {} failed += 1; opts.log.warn(`Retry failed for delivery ${entry.id}: ${err instanceof Error ? err.message : String(err)}`); } } opts.log.info(`Delivery recovery complete: ${recovered} recovered, ${failed} failed, ${skipped} skipped (max retries)`); return { recovered, failed, skipped }; } //#endregion export { failDelivery as i, delivery_queue_exports as n, enqueueDelivery as r, ackDelivery as t };