@shagital/atomic-lock
Version:
Universal atomic locking with pluggable drivers (Redis, SQLite, File, Memory)
138 lines (137 loc) • 4.81 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.SQLiteLockDriver = void 0;
/**
* SQLite driver implementation using better-sqlite3 or similar
*/
class SQLiteLockDriver {
constructor(config) {
this.config = config;
this.tableName = config.tableName || 'atomic_locks';
this.initializeTable();
}
initializeTable() {
const sql = `
CREATE TABLE IF NOT EXISTS ${this.tableName} (
lock_key TEXT PRIMARY KEY,
lock_value TEXT NOT NULL,
expires_at INTEGER NOT NULL,
created_at INTEGER NOT NULL
)
`;
this.config.db.exec(sql);
// Create index for expiry cleanup
const indexSql = `
CREATE INDEX IF NOT EXISTS idx_${this.tableName}_expires_at
ON ${this.tableName}(expires_at)
`;
this.config.db.exec(indexSql);
}
async tryAcquire(key, lockValue, expiryInSeconds) {
const expiresAt = Date.now() + (expiryInSeconds * 1000);
const createdAt = Date.now();
try {
// First, clean up expired locks for this key
this.config.db.prepare(`
DELETE FROM ${this.tableName}
WHERE lock_key = ? AND expires_at < ?
`).run(key, Date.now());
// Try to insert the lock
const stmt = this.config.db.prepare(`
INSERT INTO ${this.tableName} (lock_key, lock_value, expires_at, created_at)
VALUES (?, ?, ?, ?)
`);
stmt.run(key, lockValue, expiresAt, createdAt);
return true;
}
catch (error) {
// SQLite will throw UNIQUE constraint error if lock exists
if (error.code === 'SQLITE_CONSTRAINT_PRIMARYKEY') {
return false;
}
throw error;
}
}
async tryAcquireMultiple(keys, lockValue, expiryInSeconds) {
const expiresAt = Date.now() + (expiryInSeconds * 1000);
const createdAt = Date.now();
const now = Date.now();
return this.config.db.transaction(() => {
// Clean up expired locks for all keys
const cleanupStmt = this.config.db.prepare(`
DELETE FROM ${this.tableName}
WHERE lock_key IN (${keys.map(() => '?').join(',')}) AND expires_at < ?
`);
cleanupStmt.run(...keys, now);
// Check if any locks exist
const checkStmt = this.config.db.prepare(`
SELECT COUNT(*) as count FROM ${this.tableName}
WHERE lock_key IN (${keys.map(() => '?').join(',')}) AND expires_at >= ?
`);
const result = checkStmt.get(...keys, now);
if (result.count > 0) {
return false;
}
// Acquire all locks
const insertStmt = this.config.db.prepare(`
INSERT INTO ${this.tableName} (lock_key, lock_value, expires_at, created_at)
VALUES (?, ?, ?, ?)
`);
for (const key of keys) {
insertStmt.run(key, lockValue, expiresAt, createdAt);
}
return true;
})();
}
async release(key, lockValue) {
const stmt = this.config.db.prepare(`
DELETE FROM ${this.tableName}
WHERE lock_key = ? AND lock_value = ?
`);
const result = stmt.run(key, lockValue);
return result.changes > 0;
}
async releaseMultiple(keys, lockValue) {
const stmt = this.config.db.prepare(`
DELETE FROM ${this.tableName}
WHERE lock_key IN (${keys.map(() => '?').join(',')}) AND lock_value = ?
`);
const result = stmt.run(...keys, lockValue);
return result.changes;
}
async exists(key) {
// Clean up expired lock first
this.config.db.prepare(`
DELETE FROM ${this.tableName}
WHERE lock_key = ? AND expires_at < ?
`).run(key, Date.now());
const stmt = this.config.db.prepare(`
SELECT 1 FROM ${this.tableName}
WHERE lock_key = ? AND expires_at >= ?
`);
const result = stmt.get(key, Date.now());
return result !== undefined;
}
async getLockInfo(key) {
const stmt = this.config.db.prepare(`
SELECT lock_value as value, expires_at as expiresAt, created_at as createdAt
FROM ${this.tableName}
WHERE lock_key = ? AND expires_at >= ?
`);
const result = stmt.get(key, Date.now());
return result || null;
}
async cleanup() {
const stmt = this.config.db.prepare(`
DELETE FROM ${this.tableName}
WHERE expires_at < ?
`);
stmt.run(Date.now());
}
async close() {
if (this.config.db.close) {
this.config.db.close();
}
}
}
exports.SQLiteLockDriver = SQLiteLockDriver;