@shagital/atomic-lock
Version:
Universal atomic locking with pluggable drivers (Redis, SQLite, File, Memory)
146 lines (139 loc) • 4.57 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.RedisLockDriver = void 0;
const ioredis_1 = require("ioredis");
/**
* Redis driver implementation using ioredis
*/
class RedisLockDriver {
constructor(config) {
this.config = config;
this.client = this.createRedisClient(config);
}
createRedisClient(config) {
// Option 1: Use existing client instance
if (config.client) {
return config.client;
}
// Option 2: Create client from URL
if (config.url) {
return config.options ? new ioredis_1.Redis(config.url, config.options) : new ioredis_1.Redis(config.url);
}
// Option 3: Create client from individual parameters
const connectionOptions = {
host: config.host || 'localhost',
port: config.port || 6379,
password: config.password,
username: config.username,
db: config.db || 0,
...config.options
};
return new ioredis_1.Redis(connectionOptions);
}
async tryAcquire(key, lockValue, expiryInSeconds) {
const result = await this.client.set(key, lockValue, 'EX', expiryInSeconds, 'NX');
return result === 'OK';
}
async tryAcquireMultiple(keys, lockValue, expiryInSeconds) {
const script = `
local keys = KEYS
local value = ARGV[1]
local expiry = ARGV[2]
-- Check if any key is already locked
for i = 1, #keys do
if redis.call("EXISTS", keys[i]) == 1 then
return 0
end
end
-- Acquire all locks atomically
for i = 1, #keys do
redis.call("SET", keys[i], value, "EX", expiry)
end
return 1
`;
const result = await this.client.eval(script, keys.length, ...keys, lockValue, expiryInSeconds.toString());
return result === 1;
}
async release(key, lockValue) {
const script = `
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
`;
const result = await this.client.eval(script, 1, key, lockValue);
return result === 1;
}
async releaseMultiple(keys, lockValue) {
const script = `
local released = 0
for i = 1, #KEYS do
if redis.call("GET", KEYS[i]) == ARGV[1] then
redis.call("DEL", KEYS[i])
released = released + 1
end
end
return released
`;
const result = await this.client.eval(script, keys.length, ...keys, lockValue);
return result;
}
async exists(key) {
const result = await this.client.exists(key);
return result === 1;
}
async getLockInfo(key) {
const [value, ttl] = await Promise.all([
this.client.get(key),
this.client.ttl(key)
]);
if (!value)
return null;
return {
key,
value,
expiresAt: Date.now() + (ttl * 1000),
createdAt: Date.now() - (this.getTtlFromValue(value) - ttl) * 1000
};
}
async cleanup() {
// Redis handles expiry automatically, but we can implement
// a manual cleanup if needed for specific use cases
const script = `
local cursor = "0"
local deleted = 0
repeat
local scan_result = redis.call("SCAN", cursor, "MATCH", "*", "COUNT", 1000)
cursor = scan_result[1]
local keys = scan_result[2]
for i = 1, #keys do
local ttl = redis.call("TTL", keys[i])
if ttl == -1 then
-- Key exists but has no expiry, skip
elseif ttl == -2 then
-- Key doesn't exist, skip
elseif ttl <= 0 then
-- Key is expired, delete it
redis.call("DEL", keys[i])
deleted = deleted + 1
end
end
until cursor == "0"
return deleted
`;
await this.client.eval(script, 0);
}
async close() {
// Only close if we created the client (not passed an existing one)
if (!this.config.client) {
await this.client.quit();
}
}
getTtlFromValue(value) {
// Extract timestamp from lock value if available
const match = value.match(/^(\d+)-/);
return match ? parseInt(match[1]) : Date.now();
}
}
exports.RedisLockDriver = RedisLockDriver;