addition of new dashboard
This commit is contained in:
78
backend/util/cacheAgreement.js
Normal file
78
backend/util/cacheAgreement.js
Normal file
@@ -0,0 +1,78 @@
|
||||
import axios from "axios";
|
||||
import { createHash } from "crypto";
|
||||
import { getPool } from "../db/index.js";
|
||||
import { getConfig } from "../common/config.js";
|
||||
import { firstNonBlankLine } from "./firstNonBlankLine.js";
|
||||
|
||||
// Delay (ms) before the next attempt, given how many have already failed.
|
||||
// Exponential (startingDelay * factor^priorFailures), capped at maxDelay.
|
||||
function backoffMs(priorFailures, { startingDelayMs, maxDelayMs, factor }) {
|
||||
return Math.min(maxDelayMs, startingDelayMs * Math.pow(factor, priorFailures));
|
||||
}
|
||||
|
||||
// Best-effort remote agreement fetch. A failure is logged, never thrown. Local
|
||||
// URLs and already-cached agreements are skipped. Failures are tracked in
|
||||
// agreement_fetch with exponential backoff so an invalid URL isn't re-fetched on
|
||||
// every referencing create; after maxRetries we stop trying.
|
||||
export async function cacheAgreementMarkdown({ userId = null, agreementUuid, uri }, _client) {
|
||||
const config = getConfig();
|
||||
if (typeof uri !== "string" || !uri || !agreementUuid) return;
|
||||
if (uri.startsWith(config.publicCoreUrl)) return;
|
||||
|
||||
const client = _client || await getPool();
|
||||
try {
|
||||
const cached = await client.query(
|
||||
`SELECT 1 FROM agreement_content WHERE agreement_uuid = $1 AND markdown IS NOT NULL`,
|
||||
[agreementUuid],
|
||||
);
|
||||
if (cached.rowCount > 0) return;
|
||||
|
||||
const { maxRetries } = config.agreementCache;
|
||||
const prior = await client.query(
|
||||
`SELECT attempts, next_attempt_ts FROM agreement_fetch WHERE agreement_uuid = $1`,
|
||||
[agreementUuid],
|
||||
);
|
||||
const attempts = prior.rows[0]?.attempts ?? 0;
|
||||
if (attempts >= maxRetries) return; // gave up
|
||||
if (prior.rowCount > 0 && new Date(prior.rows[0].next_attempt_ts) > new Date()) return; // not due yet
|
||||
|
||||
try {
|
||||
const res = await axios.get(uri, { responseType: "text" });
|
||||
const markdown = String(res.data ?? "").trim();
|
||||
if (!markdown) throw new Error("empty response");
|
||||
|
||||
const title = firstNonBlankLine(markdown);
|
||||
const hash = createHash("sha256").update(markdown).digest("hex");
|
||||
// Content is immutable once inserted — INSERT only, never overwrite.
|
||||
await client.query(
|
||||
`INSERT INTO agreement_content (user_id, title, markdown, hash, agreement_uuid, remote_url)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
ON CONFLICT DO NOTHING`,
|
||||
[userId, title, markdown, hash, agreementUuid, uri],
|
||||
);
|
||||
await client.query(`DELETE FROM agreement_fetch WHERE agreement_uuid = $1`, [agreementUuid]);
|
||||
} catch (fetchErr) {
|
||||
await recordFailure(client, agreementUuid, uri, attempts, fetchErr.message, config.agreementCache);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`cacheAgreementMarkdown(${agreementUuid}):`, e.message);
|
||||
} finally {
|
||||
if (!_client) await client.release();
|
||||
}
|
||||
}
|
||||
|
||||
// Bump the attempt counter and schedule the next try with exponential backoff.
|
||||
async function recordFailure(client, agreementUuid, uri, priorFailures, message, cfg) {
|
||||
const delayMs = Math.round(backoffMs(priorFailures, cfg));
|
||||
await client.query(
|
||||
`INSERT INTO agreement_fetch (agreement_uuid, remote_url, attempts, next_attempt_ts, last_error)
|
||||
VALUES ($1, $2, $3, NOW() + ($4 || ' milliseconds')::interval, $5)
|
||||
ON CONFLICT (agreement_uuid) DO UPDATE SET
|
||||
attempts = $3,
|
||||
next_attempt_ts = NOW() + ($4 || ' milliseconds')::interval,
|
||||
last_error = $5,
|
||||
remote_url = $2,
|
||||
updated_ts = NOW()`,
|
||||
[agreementUuid, uri, priorFailures + 1, String(delayMs), message],
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user