feat(catalog): persist idempotent bulk operations and retryable deliveries
CI / check (push) Successful in 3m7s
CI / deploy (push) Successful in 1m37s
CI / publish-container (push) Successful in 44s

This commit is contained in:
Simo committed 2026-09-13 18:05:03 +02:00
1 parent 76e46d295c
commit 13665e0c3c
53 files changed
+1316 -76

No files matched your search

+117
View File
@@ -0,0 +1,117 @@
import "server-only";
import { randomUUID } from "node:crypto";
import { sql } from "drizzle-orm";
import { db } from "@/lib/db";
import {
type EffectClaim,
type EffectTopic,
OperationConflict,
type OperationInput,
operationHash,
retryDelay,
validateOperation,
} from "./model";
export type OperationTransaction = Parameters<
Parameters<typeof db.transaction>[0]
>[0];
export async function runOperation<T>(
input: OperationInput,
work: (tx: OperationTransaction, operationId: string) => Promise<T>,
): Promise<T> {
validateOperation(input);
const hash = operationHash(input.input);
return db.transaction(async (tx) => {
await tx.execute(
sql`INSERT INTO cms_operations (id,actor_id,kind,request_key,request_hash) VALUES (${randomUUID()},${input.actorId},${input.kind},${input.key},${hash}) ON DUPLICATE KEY UPDATE id=id`,
);
const [rows] = await tx.execute(
sql`SELECT id,request_hash AS requestHash,result_json AS resultJson FROM cms_operations WHERE actor_id=${input.actorId} AND kind=${input.kind} AND request_key=${input.key} FOR UPDATE`,
);
const row = (
rows as unknown as Array<{
id: string;
requestHash: string;
resultJson: string | null;
}>
)[0];
if (!row) throw new Error("Operation unavailable");
if (row.requestHash !== hash) throw new OperationConflict();
if (row.resultJson !== null) return JSON.parse(row.resultJson) as T;
const result = await work(tx, row.id);
const serialized = JSON.stringify(result);
if (!serialized || Buffer.byteLength(serialized) > 1048576)
throw new Error("Operation result exceeds storage limit");
await tx.execute(
sql`UPDATE cms_operations SET result_json=${serialized} WHERE id=${row.id}`,
);
return result;
});
}
export async function enqueueEffect(
tx: OperationTransaction,
operationId: string,
topic: EffectTopic,
) {
await tx.execute(
sql`INSERT INTO cms_outbox (id,operation_id,topic) VALUES (${randomUUID()},${operationId},${topic}) ON DUPLICATE KEY UPDATE id=id`,
);
}
export const effectRepository = {
async claim(): Promise<EffectClaim | null> {
return db.transaction(async (tx) => {
const [rows] = await tx.execute(
sql`SELECT id,topic,attempts FROM cms_outbox WHERE (status='pending' AND available_at<=UTC_TIMESTAMP(3)) OR (status='running' AND lease_until<UTC_TIMESTAMP(3)) ORDER BY available_at,id LIMIT 1 FOR UPDATE`,
);
const row = (
rows as unknown as Array<{
id: string;
topic: EffectTopic;
attempts: number;
}>
)[0];
if (!row) return null;
const token = randomUUID();
await tx.execute(
sql`UPDATE cms_outbox SET status='running',lease_token=${token},lease_until=DATE_ADD(UTC_TIMESTAMP(3),INTERVAL 120 SECOND),attempts=attempts+1 WHERE id=${row.id}`,
);
return { ...row, token, attempts: Number(row.attempts) + 1 };
});
},
async complete(claim: EffectClaim) {
await db.execute(
sql`UPDATE cms_outbox SET status='done',lease_token=NULL,lease_until=NULL,last_error=NULL WHERE id=${claim.id} AND status='running' AND lease_token=${claim.token}`,
);
},
async fail(claim: EffectClaim) {
await db.execute(
sql`UPDATE cms_outbox SET status=${claim.attempts >= 8 ? "failed" : "pending"},available_at=DATE_ADD(UTC_TIMESTAMP(3),INTERVAL ${retryDelay(claim.attempts)} SECOND),lease_token=NULL,lease_until=NULL,last_error='Delivery failed; inspect the linked operation and service diagnostics.' WHERE id=${claim.id} AND status='running' AND lease_token=${claim.token}`,
);
},
};
export async function listEffects() {
const [rows] = await db.execute(
sql`SELECT e.id,e.operation_id AS operationId,e.topic,e.status,e.attempts,e.last_error AS lastError,e.created_at AS createdAt,o.actor_id AS actorId,o.kind FROM cms_outbox e JOIN cms_operations o ON o.id=e.operation_id ORDER BY e.created_at DESC,e.id DESC LIMIT 100`,
);
return rows as unknown as Array<{
id: string;
operationId: string;
topic: EffectTopic;
status: string;
attempts: number;
lastError: string | null;
createdAt: string;
actorId: number;
kind: string;
}>;
}
export async function retryEffect(id: string) {
if (
!/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(id)
)
throw new Error("Invalid delivery");
const [result] = await db.execute(
sql`UPDATE cms_outbox SET status='pending',attempts=0,available_at=UTC_TIMESTAMP(3),last_error=NULL WHERE id=${id} AND status='failed'`,
);
if ((result as unknown as { affectedRows: number }).affectedRows !== 1)
throw new Error("Delivery is no longer available for retry");
}