first init
This commit is contained in:
Vendored
+89
@@ -0,0 +1,89 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user