teqo/src/utilities/postgresTransactionLocks.ts
Francisco Ignácio Fontoura Solla 1e23b27b4f Deliver C8 scale/DRY for supporters and register C9 follow-ups.
Bulk import and list paths get single-round-trip locks, drizzle returning reads, pg_trgm search, shared drizzleBulk helpers, and consolidated campaign formActions; simplify debts land as C9 in the roadmap.
2026-07-19 00:31:06 -03:00

59 lines
2.1 KiB
TypeScript

import 'server-only'
import { sql } from '@payloadcms/db-postgres'
import type { Payload, PayloadRequest } from 'payload'
export type PostgresTransactionDatabase = {
execute: (query: ReturnType<typeof sql>) => Promise<unknown>
}
type TransactionPayload = Pick<Payload, 'db'>
type TransactionRequest = {
transactionID?: PayloadRequest['transactionID']
}
export const getPostgresTransactionDatabase = async (
payload: TransactionPayload,
req: TransactionRequest,
): Promise<PostgresTransactionDatabase> => {
if (payload.db.name !== 'postgres') {
throw new Error('O bloqueio transacional exige o adaptador PostgreSQL.')
}
const transactionID = await req.transactionID
if (transactionID === undefined || transactionID === null) {
throw new Error('A transação PostgreSQL não está disponível.')
}
const database = payload.db.sessions?.[String(transactionID)]?.db as
| PostgresTransactionDatabase
| undefined
if (!database || typeof database.execute !== 'function') {
throw new Error('A sessão PostgreSQL da transação não está disponível.')
}
return database
}
export const acquireTextAdvisoryLocks = async (
payload: TransactionPayload,
req: TransactionRequest,
keys: string[],
): Promise<void> => {
if (
keys.some((key) => typeof key !== 'string' || key.trim().length === 0)
) {
throw new Error('A chave do bloqueio PostgreSQL deve ser um texto não vazio.')
}
const sortedKeys = [...new Set(keys)].sort()
if (sortedKeys.length === 0) return
const database = await getPostgresTransactionDatabase(payload, req)
// Batch every key into a single round trip: unnest() preserves array order for a
// plain function scan, and pg_advisory_xact_lock is volatile (never parallelized),
// so locks are still acquired sequentially in sorted order — same deadlock-avoidance
// guarantee as the previous per-key loop, at 1 round trip instead of N.
const keyFragments = sortedKeys.map((key) => sql`${key}`)
await database.execute(
sql`SELECT pg_advisory_xact_lock(hashtextextended(k, 0)) FROM unnest(ARRAY[${sql.join(keyFragments, sql`, `)}]::text[]) AS k`,
)
}