/** * Lua Scripts for Atomic Redis Operations * Optimized scripts based on analysis of multiple implementations */ /** * Script for acquiring locks with optional data storage */ export declare const ACQUIRE_SCRIPT = "\n-- RedlockToolkit Acquire Script\n-- KEYS[1..n]: lock keys\n-- ARGV[1]: lock identifier (UUID)\n-- ARGV[2]: TTL in milliseconds\n-- ARGV[3]: data to store (JSON string, optional)\n\nlocal identifier = ARGV[1]\nlocal ttl = tonumber(ARGV[2])\nlocal data = ARGV[3]\n\n-- Check if any of the resources are already locked\nfor i = 1, #KEYS do\n local current = redis.call('GET', KEYS[i])\n if current and current ~= identifier then\n return 0 -- Lock exists and is held by different client\n end\nend\n\n-- Acquire all locks atomically\nfor i = 1, #KEYS do\n redis.call('SET', KEYS[i], identifier, 'PX', ttl)\n \n -- Store data if provided\n if data and data ~= '' then\n local dataKey = KEYS[i] .. ':data'\n redis.call('SET', dataKey, data, 'PX', ttl)\n end\nend\n\nreturn #KEYS -- Return number of locks acquired\n"; /** * Script for extending locks */ export declare const EXTEND_SCRIPT = "\n-- RedlockToolkit Extend Script\n-- KEYS[1..n]: lock keys\n-- ARGV[1]: lock identifier (UUID)\n-- ARGV[2]: new TTL in milliseconds\n\nlocal identifier = ARGV[1]\nlocal ttl = tonumber(ARGV[2])\n\n-- Verify ownership of all locks before extending\nfor i = 1, #KEYS do\n local current = redis.call('GET', KEYS[i])\n if current ~= identifier then\n return 0 -- Not owner of all locks\n end\nend\n\n-- Extend all locks atomically\nfor i = 1, #KEYS do\n redis.call('SET', KEYS[i], identifier, 'PX', ttl)\n \n -- Extend data TTL if exists (use PEXPIRE for millisecond precision)\n local dataKey = KEYS[i] .. ':data'\n if redis.call('EXISTS', dataKey) == 1 then\n redis.call('PEXPIRE', dataKey, ttl)\n end\nend\n\nreturn #KEYS -- Return number of locks extended\n"; /** * Script for releasing locks */ export declare const RELEASE_SCRIPT = "\n-- RedlockToolkit Release Script\n-- KEYS[1..n]: lock keys\n-- ARGV[1]: lock identifier (UUID)\n\nlocal identifier = ARGV[1]\nlocal released = 0\n\n-- Release only locks owned by this identifier\nfor i = 1, #KEYS do\n local current = redis.call('GET', KEYS[i])\n if current == identifier then\n redis.call('DEL', KEYS[i])\n \n -- Clean up data if exists\n local dataKey = KEYS[i] .. ':data'\n redis.call('DEL', dataKey)\n \n released = released + 1\n end\nend\n\nreturn released -- Return number of locks released\n"; /** * Script for force releasing locks (admin operation) */ export declare const FORCE_RELEASE_SCRIPT = "\n-- RedlockToolkit Force Release Script\n-- KEYS[1..n]: lock keys\n\nlocal released = 0\n\nfor i = 1, #KEYS do\n if redis.call('EXISTS', KEYS[i]) == 1 then\n redis.call('DEL', KEYS[i])\n \n -- Clean up data if exists\n local dataKey = KEYS[i] .. ':data'\n redis.call('DEL', dataKey)\n \n released = released + 1\n end\nend\n\nreturn released -- Return number of locks released\n"; /** * Script for checking lock status. * Returns a flat array: [key1, holder1_or_empty, ttl1, key2, holder2_or_empty, ttl2, ...] * Empty string for holder means the key is not locked. */ export declare const STATUS_SCRIPT = "\n-- RedlockToolkit Status Script\n-- KEYS[1..n]: lock keys\n--\n-- Returns flat array of triples: {key, holder_or_empty_string, ttl, ...}\n\nlocal result = {}\n\nfor i = 1, #KEYS do\n local key = KEYS[i]\n local value = redis.call('GET', key)\n local ttl = redis.call('PTTL', key)\n\n result[#result + 1] = key\n if value then\n result[#result + 1] = value\n else\n result[#result + 1] = ''\n end\n result[#result + 1] = ttl\nend\n\nreturn result\n"; /** * Script for acquiring locks with wait-if-locked functionality. * Returns a flat array: * [acquiredCount, waitTime, blockerCount, key1, holder1, ttl1, key2, holder2, ttl2, ...] */ export declare const ACQUIRE_WITH_WAIT_SCRIPT = "\n-- RedlockToolkit Acquire with Wait Script\n-- KEYS[1..n]: lock keys\n-- ARGV[1]: lock identifier (UUID)\n-- ARGV[2]: TTL in milliseconds\n-- ARGV[3]: max wait time in milliseconds\n-- ARGV[4]: data to store (JSON string, optional)\n--\n-- Returns flat array:\n-- {acquiredCount, waitTime, blockerCount, key1, holder1, ttl1, ...}\n\nlocal identifier = ARGV[1]\nlocal ttl = tonumber(ARGV[2])\nlocal maxWait = tonumber(ARGV[3])\nlocal data = ARGV[4]\n\n-- Check current lock status\nlocal blockers = {}\nlocal minTtl = maxWait\n\nfor i = 1, #KEYS do\n local current = redis.call('GET', KEYS[i])\n if current and current ~= identifier then\n local keyTtl = redis.call('PTTL', KEYS[i])\n if keyTtl > 0 and keyTtl < minTtl then\n minTtl = keyTtl\n end\n blockers[#blockers + 1] = KEYS[i]\n blockers[#blockers + 1] = current\n blockers[#blockers + 1] = keyTtl\n end\nend\n\nlocal blockerCount = #blockers / 3\n\n-- If blocked and min TTL is within wait time, return wait suggestion\nif blockerCount > 0 and minTtl <= maxWait then\n local result = {0, minTtl, blockerCount}\n for i = 1, #blockers do\n result[#result + 1] = blockers[i]\n end\n return result\nend\n\n-- If blocked but TTL too long, fail immediately\nif blockerCount > 0 then\n local result = {0, -1, blockerCount}\n for i = 1, #blockers do\n result[#result + 1] = blockers[i]\n end\n return result\nend\n\n-- Acquire all locks\nfor i = 1, #KEYS do\n redis.call('SET', KEYS[i], identifier, 'PX', ttl)\n\n if data and data ~= '' then\n local dataKey = KEYS[i] .. ':data'\n redis.call('SET', dataKey, data, 'PX', ttl)\n end\nend\n\nreturn {#KEYS, 0, 0}\n"; /** * Script for cleaning up specific keys that have no TTL (persistent locks). * Called from Node.js after scanning keys in batches. * KEYS[1..n]: specific keys to check * Returns: number of keys deleted */ export declare const CLEANUP_CHECK_SCRIPT = "\n-- RedlockToolkit Cleanup Check Script\n-- KEYS[1..n]: specific keys to check and clean\nlocal cleaned = 0\n\nfor i = 1, #KEYS do\n local ttl = redis.call('PTTL', KEYS[i])\n if ttl == -1 then\n redis.call('DEL', KEYS[i])\n cleaned = cleaned + 1\n end\nend\n\nreturn cleaned\n"; /** * Script for optimistic locking with version control */ export declare const OPTIMISTIC_ACQUIRE_SCRIPT = "\n-- RedlockToolkit Optimistic Acquire Script\n-- KEYS[1..n]: lock keys\n-- ARGV[1]: lock identifier (UUID)\n-- ARGV[2]: TTL in milliseconds\n-- ARGV[3]: expected version (optional)\n-- ARGV[4]: expected value (JSON, optional)\n--\n-- Returns array:\n-- On conflict: {0, currentVersion, 'reason'}\n-- On success: {1, newVersion, acquiredCount}\n\nlocal identifier = ARGV[1]\nlocal ttl = tonumber(ARGV[2])\nlocal expectedVersion = (ARGV[3] ~= nil and ARGV[3] ~= '') and tonumber(ARGV[3]) or nil\nlocal expectedValue = ARGV[4]\n\n-- Check version and value constraints for optimistic locking\nfor i = 1, #KEYS do\n local key = KEYS[i]\n local current = redis.call('GET', key)\n local versionKey = key .. ':version'\n local currentVersion = redis.call('GET', versionKey)\n local currentVersionNum = currentVersion and tonumber(currentVersion) or 0\n\n -- If key exists and is locked by someone else\n if current and current ~= identifier then\n return {0, currentVersionNum, 'locked'}\n end\n\n -- Check version constraint\n if expectedVersion and currentVersion then\n local ver = tonumber(currentVersion)\n if ver and ver ~= expectedVersion then\n return {0, ver, 'version_mismatch'}\n end\n end\n\n -- Check value constraint\n if expectedValue and expectedValue ~= '' then\n local valueKey = key .. ':value'\n local currentValue = redis.call('GET', valueKey)\n if currentValue and currentValue ~= expectedValue then\n return {0, currentVersionNum, 'value_mismatch'}\n end\n end\nend\n\n-- Acquire all locks with version increment\n-- Use current server-side version as base to prevent rollback\nlocal baseVersion = 0\nif expectedVersion then\n baseVersion = expectedVersion\nelse\n -- If a version already exists on ANY key but no expectedVersion was\n -- provided, this is a conflict - the caller must provide expectedVersion\n -- for existing resources. Checking only the first key would silently\n -- overwrite existing versions on the remaining keys.\n for i = 1, #KEYS do\n local versionKey = KEYS[i] .. ':version'\n local existingVersion = redis.call('GET', versionKey)\n if existingVersion then\n return {0, tonumber(existingVersion) or 0, 'version_required'}\n end\n end\nend\nlocal newVersion = baseVersion + 1\n\nfor i = 1, #KEYS do\n local key = KEYS[i]\n redis.call('SET', KEYS[i], identifier, 'PX', ttl)\n\n -- Update version\n local versionKey = key .. ':version'\n redis.call('SET', versionKey, tostring(newVersion), 'PX', ttl)\n\n -- Store expected value if provided\n if expectedValue and expectedValue ~= '' then\n local valueKey = key .. ':value'\n redis.call('SET', valueKey, expectedValue, 'PX', ttl)\n end\nend\n\nreturn {1, newVersion, #KEYS}\n"; /** * Script for optimistic update with conflict detection */ export declare const OPTIMISTIC_UPDATE_SCRIPT = "\n-- RedlockToolkit Optimistic Update Script\n-- KEYS[1..n]: lock keys\n-- ARGV[1]: lock identifier (UUID)\n-- ARGV[2]: new TTL in milliseconds\n-- ARGV[3]: expected version\n-- ARGV[4]: new value (JSON, optional)\n--\n-- Returns array:\n-- On conflict: {0, currentVersion, 'reason'}\n-- On success: {1, newVersion, updatedCount}\n\nlocal identifier = ARGV[1]\nlocal ttl = tonumber(ARGV[2])\nlocal expectedVersion = tonumber(ARGV[3])\nlocal newValue = ARGV[4]\n\n-- Verify ownership and version\nfor i = 1, #KEYS do\n local key = KEYS[i]\n local current = redis.call('GET', key)\n local versionKey = key .. ':version'\n local currentVersion = redis.call('GET', versionKey)\n\n -- Check ownership\n if not current or current ~= identifier then\n local currentVersionNum = currentVersion and tonumber(currentVersion) or 0\n return {0, currentVersionNum, 'not_owner'}\n end\n\n -- Check version\n if currentVersion then\n local ver = tonumber(currentVersion)\n if ver and ver ~= expectedVersion then\n return {0, ver, 'version_mismatch'}\n end\n end\nend\n\n-- Update all locks with new version\nlocal newVersion = expectedVersion + 1\nfor i = 1, #KEYS do\n local key = KEYS[i]\n redis.call('SET', KEYS[i], identifier, 'PX', ttl)\n\n -- Update version\n local versionKey = key .. ':version'\n redis.call('SET', versionKey, tostring(newVersion), 'PX', ttl)\n\n -- Update value if provided\n if newValue and newValue ~= '' then\n local valueKey = key .. ':value'\n redis.call('SET', valueKey, newValue, 'PX', ttl)\n end\nend\n\nreturn {1, newVersion, #KEYS}\n"; /** * Script for acquiring a semaphore permit. * Uses a Sorted Set where member=identifier, score=expirationTimestamp. */ export declare const SEMAPHORE_ACQUIRE_SCRIPT = "\n-- RedlockToolkit Semaphore Acquire Script\n-- KEYS[1]: semaphore key (ZSET)\n-- ARGV[1]: identifier (UUID for this permit holder)\n-- ARGV[2]: TTL in milliseconds\n-- ARGV[3]: max permits\n\nlocal key = KEYS[1]\nlocal identifier = ARGV[1]\nlocal ttl = tonumber(ARGV[2])\nlocal maxPermits = tonumber(ARGV[3])\n-- Use Redis server time to avoid client clock skew\nlocal timeResult = redis.call('TIME')\nlocal now = tonumber(timeResult[1]) * 1000 + math.floor(tonumber(timeResult[2]) / 1000)\n\n-- Remove expired permits\nredis.call('ZREMRANGEBYSCORE', key, '-inf', now)\n\n-- Check if already holding a permit (idempotent re-acquire)\nlocal existingScore = redis.call('ZSCORE', key, identifier)\nif existingScore then\n redis.call('ZADD', key, now + ttl, identifier)\n local count = redis.call('ZCARD', key)\n return {1, count, maxPermits}\nend\n\n-- Check available permits\nlocal currentCount = redis.call('ZCARD', key)\nif currentCount < maxPermits then\n redis.call('ZADD', key, now + ttl, identifier)\n redis.call('PEXPIRE', key, ttl * 2)\n return {1, currentCount + 1, maxPermits}\nend\n\nreturn {0, currentCount, maxPermits}\n"; /** * Script for releasing a semaphore permit. */ export declare const SEMAPHORE_RELEASE_SCRIPT = "\n-- RedlockToolkit Semaphore Release Script\n-- KEYS[1]: semaphore key (ZSET)\n-- ARGV[1]: identifier\n\nlocal key = KEYS[1]\nlocal identifier = ARGV[1]\n-- Use Redis server time to avoid client clock skew\nlocal timeResult = redis.call('TIME')\nlocal now = tonumber(timeResult[1]) * 1000 + math.floor(tonumber(timeResult[2]) / 1000)\n\nredis.call('ZREMRANGEBYSCORE', key, '-inf', now)\nlocal removed = redis.call('ZREM', key, identifier)\nlocal remaining = redis.call('ZCARD', key)\n\nreturn {removed, remaining}\n"; /** * Script for extending a semaphore permit. */ export declare const SEMAPHORE_EXTEND_SCRIPT = "\n-- RedlockToolkit Semaphore Extend Script\n-- KEYS[1]: semaphore key (ZSET)\n-- ARGV[1]: identifier\n-- ARGV[2]: TTL in milliseconds\n\nlocal key = KEYS[1]\nlocal identifier = ARGV[1]\nlocal ttl = tonumber(ARGV[2])\n-- Use Redis server time to avoid client clock skew\nlocal timeResult = redis.call('TIME')\nlocal now = tonumber(timeResult[1]) * 1000 + math.floor(tonumber(timeResult[2]) / 1000)\n\nlocal existingScore = redis.call('ZSCORE', key, identifier)\nif not existingScore then\n return 0\nend\n\nif tonumber(existingScore) < now then\n redis.call('ZREM', key, identifier)\n return 0\nend\n\nredis.call('ZADD', key, now + ttl, identifier)\nredis.call('PEXPIRE', key, ttl * 2)\nreturn 1\n"; /** * Script for checking semaphore status. */ export declare const SEMAPHORE_STATUS_SCRIPT = "\n-- RedlockToolkit Semaphore Status Script\n-- KEYS[1]: semaphore key (ZSET)\n-- ARGV[1]: max permits\n\nlocal key = KEYS[1]\n-- Use Redis server time to avoid client clock skew\nlocal timeResult = redis.call('TIME')\nlocal now = tonumber(timeResult[1]) * 1000 + math.floor(tonumber(timeResult[2]) / 1000)\nlocal maxPermits = tonumber(ARGV[1])\n\nredis.call('ZREMRANGEBYSCORE', key, '-inf', now)\nlocal members = redis.call('ZRANGE', key, 0, -1, 'WITHSCORES')\nlocal count = #members / 2\n\nlocal result = {count, maxPermits}\nfor i = 1, #members, 2 do\n result[#result + 1] = members[i]\n result[#result + 1] = members[i + 1]\nend\nreturn result\n"; /** * Script for creating a CountDownLatch. */ export declare const LATCH_CREATE_SCRIPT = "\n-- RedlockToolkit Latch Create Script\n-- KEYS[1]: latch count key\n-- KEYS[2]: latch target key\n-- ARGV[1]: count (N)\n-- ARGV[2]: TTL in milliseconds\n\nlocal countKey = KEYS[1]\nlocal targetKey = KEYS[2]\nlocal count = tonumber(ARGV[1])\nlocal ttl = tonumber(ARGV[2])\n\nlocal existing = redis.call('GET', countKey)\nif existing then\n return 0\nend\n\nredis.call('SET', countKey, count, 'PX', ttl)\nredis.call('SET', targetKey, count, 'PX', ttl)\nreturn 1\n"; /** * Script for counting down a latch (idempotent per eventId). */ export declare const LATCH_COUNTDOWN_SCRIPT = "\n-- RedlockToolkit Latch CountDown Script\n-- KEYS[1]: latch count key\n-- KEYS[2]: latch events set key\n-- ARGV[1]: event identifier (for idempotency)\n-- ARGV[2]: pub/sub channel name\n\nlocal countKey = KEYS[1]\nlocal eventsKey = KEYS[2]\nlocal eventId = ARGV[1]\nlocal channel = ARGV[2]\n\nlocal current = redis.call('GET', countKey)\nif not current then\n return {-1, 0, 0}\nend\n\nlocal currentCount = tonumber(current)\nif currentCount <= 0 then\n return {0, 0, -1}\nend\n\n-- Idempotency check using SET (O(1) vs O(n) with LPOS)\nif redis.call('SISMEMBER', eventsKey, eventId) == 1 then\n return {currentCount, 0, -2}\nend\n\nlocal newCount = redis.call('DECR', countKey)\n\nredis.call('SADD', eventsKey, eventId)\nlocal ttl = redis.call('PTTL', countKey)\nif ttl > 0 then\n redis.call('PEXPIRE', eventsKey, ttl)\nend\n\nif newCount <= 0 then\n if channel and channel ~= '' then\n redis.call('PUBLISH', channel, 'latch_complete')\n end\n return {0, 0, 2}\nend\n\nreturn {newCount, 0, 1}\n"; /** * Script for checking latch status. */ export declare const LATCH_STATUS_SCRIPT = "\n-- RedlockToolkit Latch Status Script\n-- KEYS[1]: latch count key\n-- KEYS[2]: latch target key\n\nlocal countKey = KEYS[1]\nlocal targetKey = KEYS[2]\n\nlocal current = redis.call('GET', countKey)\nif not current then\n return {-1, -1, -1}\nend\n\nlocal target = redis.call('GET', targetKey)\nlocal ttl = redis.call('PTTL', countKey)\n\nreturn {tonumber(current), tonumber(target or 0), ttl}\n"; /** * Pre-computed script hashes for performance */ export declare const SCRIPT_HASHES: { acquire: string; extend: string; release: string; forceRelease: string; status: string; acquireWithWait: string; cleanupCheck: string; optimisticAcquire: string; optimisticUpdate: string; semaphoreAcquire: string; semaphoreRelease: string; semaphoreExtend: string; semaphoreStatus: string; latchCreate: string; latchCountDown: string; latchStatus: string; }; /** * Script definition with hash */ export interface LuaScript { source: string; hash: string; } /** * All available scripts */ export declare const SCRIPTS: Record; /** * Export raw scripts for compatibility */ export declare const scripts: { acquire: string; extend: string; release: string; forceRelease: string; status: string; acquireWithWait: string; cleanupCheck: string; optimisticAcquire: string; optimisticUpdate: string; semaphoreAcquire: string; semaphoreRelease: string; semaphoreExtend: string; semaphoreStatus: string; latchCreate: string; latchCountDown: string; latchStatus: string; }; /** * Utility function to calculate script hash */ export declare function calculateScriptHash(script: string): string; /** * Validate that pre-computed hashes are correct */ export declare function validateScriptHashes(): boolean; /** * Get script by name with runtime validation */ export declare function getScript(name: keyof typeof SCRIPTS): LuaScript; //# sourceMappingURL=scripts.d.ts.map