/
kochkatech
/
CorpGame
Обзор
Документация
Войти
/
kochkatech
/
CorpGame
Код
Запросы
1
Задачи
Вики
Пакеты
0
Релизы
1
CI/CD
Аналитика
dev
backend/src/common/team-lock.ts
49 строк
4 KB
kochkareal
feat(devops): свод к одному процессу Node на SQLite, портативная сборка без Docker; аудит безопасности
06 авг 2026, 15:06
06 авг 2026, 15:06
3266bcd
Код
Авторство
О чём код?
import { Prisma, PrismaClient, Team } from '@prisma/client'; import { KeyedMutex } from './keyed-mutex'; /** Очередь на команду. Одна на процесс — игра и работает одним процессом. */ const teamMutex = new KeyedMutex(); /** * Выполняет fn в транзакции, предварительно встав в очередь на указанные команды. * * Зачем: ресурсы команды хранятся JSON-полем и меняются паттерном read-modify-write целого * объекта (сбор, открытие/апгрейд, кража, трансфер). Без сериализации два одновременных * действия над одной командой могут перезаписать друг друга (lost update). Очередь + * повторное чтение под ней делают такую последовательность безопасной. * * Локи берутся по одному в отсортированном по id порядке — детерминированный порядок захвата * исключает deadlock при встречных операциях (например, кража A→B и трансфер B→A одновременно). * Внутри fn команды уже перечитаны под локом (актуальные resources/score). * * Раньше здесь был `SELECT ... FOR UPDATE` — синтаксис Postgres, которого нет в SQLite. * Формально под SQLite лок и не нужен: Prisma держит пул из одного соединения, поэтому * интерактивная транзакция сериализует вообще всю работу с БД в процессе. Но опираться на * размер пула — значит повесить корректность на недокументированную деталь реализации и * потерять дисциплину упорядоченного захвата, ради которой этот файл написан. * * ИНВАРИАНТ: внутри fn нельзя await'ить ничего, кроме работы с БД. У интерактивных транзакций * Prisma бюджет 5 с (timeout), и посторонний await — сеть, таймер, эмит события — сжигает его * и роняет всю операцию с P2028. События отправляются ПОСЛЕ возврата из withTeamLock. */ export async function withTeamLock<T>( prisma: PrismaClient, teamIds: string[], fn: (tx: Prisma.TransactionClient, teams: Map<string, Team>) => Promise<T>, ): Promise<T> { const ids = Array.from(new Set(teamIds)).sort(); const releases: Array<() => void> = []; try { // Очередь занимаем ДО открытия транзакции и отпускаем после коммита: ожидание внутри // транзакции сжигало бы её пятисекундный бюджет при шквале кликов. for (const id of ids) releases.push(await teamMutex.acquire(id)); return await prisma.$transaction(async (tx) => { const rows = await tx.team.findMany({ where: { id: { in: ids } } }); const byId = new Map(rows.map((r) => [r.id, r])); return fn(tx, byId); }); } finally { for (const release of releases.reverse()) release(); } }