UNPKG

@shagital/atomic-lock

Version:

Universal atomic locking with pluggable drivers (Redis, SQLite, File, Memory)

146 lines (139 loc) 4.57 kB
"use strict"; 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;