Replace mutation-lock with document-lock to centralize document mutation serialization and database transaction management. Introduce runInDbTransaction utility to abstract SQLite/Postgres transaction differences. Update document deletion, user data cleanup, and rate limiter logic to delegate transaction handling and locking to shared helpers. Remove dialect-specific branching and inline transaction logic for improved maintainability and testability. BREAKING CHANGE: withDocumentMutationLock is removed in favor of withDocumentLock and runInDbTransaction
59 lines
2.3 KiB
TypeScript
59 lines
2.3 KiB
TypeScript
import { sql } from 'drizzle-orm';
|
|
import { runInDbTransaction } from '@/db/run-in-transaction';
|
|
|
|
// In-process serialization for the single-writer SQLite deployment. SQLite runs
|
|
// in one process, so an in-memory promise chain per document is sufficient (and
|
|
// is the only lock available — there is no advisory lock). On Postgres this is
|
|
// unused: the transaction-scoped advisory lock below provides exclusion that
|
|
// works across a stateless/serverless fleet.
|
|
const localTails = new Map<string, Promise<void>>();
|
|
|
|
async function withLocalDocumentLock<T>(documentId: string, fn: () => Promise<T>): Promise<T> {
|
|
const previous = localTails.get(documentId) ?? Promise.resolve();
|
|
let release!: () => void;
|
|
const current = new Promise<void>((resolve) => {
|
|
release = resolve;
|
|
});
|
|
localTails.set(documentId, current);
|
|
|
|
await previous;
|
|
try {
|
|
return await fn();
|
|
} finally {
|
|
release();
|
|
if (localTails.get(documentId) === current) {
|
|
localTails.delete(documentId);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Serialize all mutations to a single document and run `fn` with exclusive
|
|
* access, passing it the connection to do its work on.
|
|
*
|
|
* The SQLite/Postgres transaction bridge is delegated to `runInDbTransaction`;
|
|
* this helper only adds the exclusion on top:
|
|
* - Postgres: a transaction-scoped advisory lock keyed on the document id,
|
|
* acquired as the first statement of the shared transaction so it covers
|
|
* the whole read-modify-write. It releases on commit, so it is safe under
|
|
* transaction-mode poolers and gives fleet-wide exclusion in a stateless
|
|
* deployment.
|
|
* - SQLite: an in-process lock; the single WAL writer needs nothing more.
|
|
*
|
|
* Because this guarantees exclusivity for the document, `fn` does not need its
|
|
* own transaction or `SELECT ... FOR UPDATE` to read-modify-write safely.
|
|
*/
|
|
export async function withDocumentLock<T>(
|
|
documentId: string,
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
fn: (conn: any) => Promise<T>,
|
|
): Promise<T> {
|
|
if (process.env.POSTGRES_URL) {
|
|
return runInDbTransaction(async (conn) => {
|
|
await conn.execute(sql`SELECT pg_advisory_xact_lock(hashtextextended(${documentId}, 0))`);
|
|
return fn(conn);
|
|
});
|
|
}
|
|
|
|
return withLocalDocumentLock(documentId, () => runInDbTransaction(fn));
|
|
}
|