import { Logger } from '@nestjs/common'; import { EntityManager, OptimisticLockVersionMismatchError } from 'typeorm'; const logger = new Logger('OptimisticLock'); /** * Optimistic Lock retry wrapper. * Retries the operation when a version conflict is detected. * * Critical for financial operations: * - Wallet balance updates (prevent double-spending) * - Order status transitions * - Coupon inventory (prevent overselling) * - Settlement records * * Usage: * await withOptimisticLock(manager, 3, async (mgr) => { * const wallet = await mgr.findOne(Wallet, { where: { id }, lock: { mode: 'optimistic', version } }); * wallet.balance = wallet.balance.minus(amount); * await mgr.save(wallet); * }); */ export async function withOptimisticLock( manager: EntityManager, maxRetries: number, operation: (manager: EntityManager) => Promise, ): Promise { let attempt = 0; while (attempt <= maxRetries) { try { return await manager.transaction(async (txManager) => { return await operation(txManager); }); } catch (error) { if ( error instanceof OptimisticLockVersionMismatchError || error.message?.includes('version') ) { attempt++; if (attempt > maxRetries) { logger.error( `Optimistic lock failed after ${maxRetries} retries: ${error.message}`, ); throw error; } logger.warn( `Optimistic lock conflict, retry ${attempt}/${maxRetries}`, ); // Exponential backoff: 50ms, 100ms, 200ms... await new Promise((r) => setTimeout(r, 50 * Math.pow(2, attempt - 1))); } else { throw error; } } } throw new Error('Optimistic lock: unreachable'); } /** * Pessimistic lock helper for critical inventory operations. * Uses SELECT ... FOR UPDATE to serialize access. * * Usage (coupon inventory): * await withPessimisticLock(manager, Coupon, couponId, async (coupon, mgr) => { * if (coupon.remainingQuantity <= 0) throw new Error('Sold out'); * coupon.remainingQuantity -= 1; * await mgr.save(coupon); * }); */ export async function withPessimisticLock( manager: EntityManager, entityClass: new () => Entity, entityId: string, operation: (entity: Entity, manager: EntityManager) => Promise, ): Promise { await manager.transaction(async (txManager) => { const entity = await txManager.findOne(entityClass as any, { where: { id: entityId } as any, lock: { mode: 'pessimistic_write' }, }); if (!entity) { throw new Error( `${entityClass.name} with id ${entityId} not found`, ); } await operation(entity as Entity, txManager); }); }