export class LockManager { // Map of lockKey (`${ticketId}:${field}`) -> FieldLockInfo static locks = new Map(); // Auto-expire locks after 15 seconds of inactivity to prevent ghost locks static LOCK_EXPIRATION_MS = 15000; static getLockKey(ticketId, field) { return `${ticketId}:${field}`; } /** * Attempt to acquire or refresh a lock. * Returns { success: boolean, lock?: FieldLockInfo, currentLock?: FieldLockInfo } */ static acquireLock(ticketId, field, socketId, userId, userName) { const lockKey = this.getLockKey(ticketId, field); const existing = this.locks.get(lockKey); const now = Date.now(); // If lock exists and hasn't expired if (existing && now - existing.lockedAt < this.LOCK_EXPIRATION_MS) { // If the current socket or user holds it, refresh the timestamp if (existing.socketId === socketId || existing.userId === userId) { existing.lockedAt = now; existing.userName = userName; return { success: true, lock: existing }; } // Held by another user return { success: false, currentHolder: existing }; } // Lock is either free or expired, acquire it const newLock = { ticketId, field, socketId, userId, userName, lockedAt: now, }; this.locks.set(lockKey, newLock); return { success: true, lock: newLock }; } /** * Release a lock by ticketId and field. */ static releaseLock(ticketId, field, socketId) { const lockKey = this.getLockKey(ticketId, field); const existing = this.locks.get(lockKey); if (!existing) return null; // If socketId specified, verify ownership if (socketId && existing.socketId !== socketId) { // If it has expired, allow release if (Date.now() - existing.lockedAt >= this.LOCK_EXPIRATION_MS) { this.locks.delete(lockKey); return existing; } return null; } this.locks.delete(lockKey); return existing; } /** * Release all locks held by a disconnected socket. */ static releaseSocketLocks(socketId) { const released = []; for (const [key, lock] of this.locks.entries()) { if (lock.socketId === socketId) { released.push(lock); this.locks.delete(key); } } return released; } /** * Get all active (non-expired) locks for a board/tickets. */ static getActiveLocks() { const now = Date.now(); const active = []; for (const [key, lock] of this.locks.entries()) { if (now - lock.lockedAt < this.LOCK_EXPIRATION_MS) { active.push(lock); } else { this.locks.delete(key); } } return active; } }