90 lines
3.0 KiB
JavaScript
90 lines
3.0 KiB
JavaScript
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;
|
|
}
|
|
}
|