48 lines
1.5 KiB
TypeScript
48 lines
1.5 KiB
TypeScript
/**
|
|
* Helper transaksi Serializable + retry. Single source of truth supaya semua
|
|
* operasi yang rawan race (join/cancel trip, refund, payment) konsisten:
|
|
* - Isolation level Serializable → Postgres SSI mendeteksi konflik.
|
|
* - Retry otomatis saat serialization failure (Prisma error code `P2034`).
|
|
*/
|
|
import { Prisma } from "@/app/generated/prisma/client";
|
|
import { prisma } from "@/lib/prisma";
|
|
|
|
const SERIAL_TX_ATTEMPTS = 6;
|
|
|
|
export function isSerializationConflict(err: unknown): boolean {
|
|
return (
|
|
typeof err === "object" &&
|
|
err !== null &&
|
|
"code" in err &&
|
|
(err as { code: string }).code === "P2034"
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Jalankan `fn` dalam transaksi Serializable. Kalau gagal karena konflik
|
|
* serialisasi, ulang sampai `SERIAL_TX_ATTEMPTS` kali. Error bisnis (non-P2034)
|
|
* langsung dilempar tanpa retry.
|
|
*/
|
|
export async function runSerializable<T>(
|
|
fn: (tx: Prisma.TransactionClient) => Promise<T>,
|
|
fallbackMessage = "Operasi sedang ramai. Coba lagi sebentar."
|
|
): Promise<T> {
|
|
let lastErr: unknown;
|
|
for (let attempt = 0; attempt < SERIAL_TX_ATTEMPTS; attempt++) {
|
|
try {
|
|
return await prisma.$transaction(fn, {
|
|
isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
|
|
maxWait: 5000,
|
|
timeout: 15000,
|
|
});
|
|
} catch (e) {
|
|
lastErr = e;
|
|
if (isSerializationConflict(e) && attempt < SERIAL_TX_ATTEMPTS - 1) {
|
|
continue;
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
throw lastErr instanceof Error ? lastErr : new Error(fallbackMessage);
|
|
}
|