5.6 KiB
Implementation Verification
Summary
The @lilith/distributed-lock package is now fully implemented with all required methods and features.
Implementation Status
✅ Lua Scripts (src/lua-scripts.ts)
All Lua scripts are correctly implemented with proper atomicity guarantees:
-
ACQUIRE_LOCK_SCRIPT
- Uses
SET key token NX PX ttlfor atomic acquire-with-expiry - Returns 1 if acquired, 0 if already locked
- Prevents race conditions during acquisition
- Uses
-
RELEASE_LOCK_SCRIPT
- Checks token matches before deleting (prevents releasing someone else's lock)
- Returns 1 if released, 0 if token mismatch
- Atomic check-and-delete operation
-
EXTEND_LOCK_SCRIPT
- Checks token ownership before extending TTL
- Uses
PEXPIREto set new expiration - Returns 1 if extended, 0 if token mismatch
-
CHECK_LOCK_SCRIPT
- Returns TTL in milliseconds (-2 if doesn't exist, -1 if no expiry)
- Used by
isLocked()method
✅ DistributedLock Class (src/lock.ts)
All methods fully implemented:
-
acquire(key, ttlMs)- ✅ Generates unique UUID token
- ✅ Uses ACQUIRE_LOCK_SCRIPT for atomic operation
- ✅ Implements retry with exponential backoff
- ✅ Throws
LockAcquisitionErroron failure - ✅ Returns
LockHandleon success
-
tryAcquire(key, ttlMs)- ✅ Single attempt (no retry)
- ✅ Returns
LockHandleon success - ✅ Returns
nullon failure (non-blocking)
-
waitForLock(key, timeoutMs, ttlMs)- ✅ Polls until lock acquired or timeout
- ✅ Exponential backoff with 1s max delay
- ✅ Respects timeout deadline
- ✅ Throws
LockAcquisitionErroron timeout
-
isLocked(key)- ✅ Uses CHECK_LOCK_SCRIPT
- ✅ Returns boolean (true if locked, false otherwise)
- ✅ Handles TTL edge cases (-2, -1, positive)
✅ LockHandle Class (src/lock-handle.ts)
All methods and properties fully implemented:
-
release()- ✅ Checks
_isHeldflag before attempting release - ✅ Uses RELEASE_LOCK_SCRIPT for atomic operation
- ✅ Throws
InvalidLockErrorif lock not held or token mismatch - ✅ Marks lock as released on success
- ✅ Checks
-
extend(extensionMs)- ✅ Validates extension duration is positive
- ✅ Uses EXTEND_LOCK_SCRIPT for atomic operation
- ✅ Updates local
_expiresAttimestamp - ✅ Throws
InvalidLockErrorif lock not held
-
isHeld(getter)- ✅ Returns current
_isHeldstate - ✅ Note: Local state check, not Redis query
- ✅ Returns current
-
info(getter)- ✅ Returns
LockInfointerface - ✅ Includes key, token, acquiredAt, expiresAt, isHeld
- ✅ Returns
-
markAsReleased()(internal)- ✅ Sets
_isHeld = false - ✅ Internal method for cleanup scenarios
- ✅ Sets
Type Safety
All types are properly defined in src/types.ts:
- ✅
LockOptions- Configuration interface - ✅
LockInfo- Lock metadata interface - ✅
LockResult- Operation result interface - ✅
LockAcquisitionError- Custom error class - ✅
InvalidLockError- Custom error class
Build Verification
$ pnpm build
✓ ESM build successful (7.45 KB)
✓ CJS build successful (7.56 KB)
✓ Type declarations generated (1.68 KB)
$ pnpm typecheck
✓ No type errors
Package Outputs
dist/index.js- ESM bundle (7.6 KB)dist/index.cjs- CommonJS bundle (7.7 KB)dist/index.d.ts- TypeScript declarations (1.7 KB)- Source maps generated for both formats
Key Features Implemented
- Token-based ownership - UUID tokens prevent unauthorized lock operations
- Automatic expiration - All locks have TTL to prevent deadlocks
- Retry logic - Configurable exponential backoff for acquire()
- Atomic operations - All Redis operations use Lua scripts
- Lock extension - Extend TTL for long-running operations
- Type safety - Full TypeScript support with strict mode
Exponential Backoff Implementation
// In acquire() method
const delay = this.options.retryDelayMs * Math.pow(2, attempts - 1)
// Example with retryDelayMs = 100:
// Attempt 1: immediate
// Attempt 2: 100ms (100 * 2^0)
// Attempt 3: 200ms (100 * 2^1)
// Attempt 4: 400ms (100 * 2^2)
// Attempt 5: 800ms (100 * 2^3)
// Attempt 6: 1600ms (100 * 2^4)
Error Handling
Both error classes extend Error with proper prototype chain setup:
Object.setPrototypeOf(this, LockAcquisitionError.prototype)
Object.setPrototypeOf(this, InvalidLockError.prototype)
This ensures instanceof checks work correctly.
Redis Command Mapping
| Method | Redis Commands | Atomicity |
|---|---|---|
acquire() |
EVAL (Lua: SET NX PX) |
Atomic |
tryAcquire() |
EVAL (Lua: SET NX PX) |
Atomic |
release() |
EVAL (Lua: GET + DEL) |
Atomic |
extend() |
EVAL (Lua: GET + PEXPIRE) |
Atomic |
isLocked() |
EVAL (Lua: EXISTS + PTTL) |
Atomic |
All operations are single Redis commands (EVAL with Lua scripts), ensuring atomicity without transactions.
Package Metadata
- Name:
@lilith/distributed-lock - Version:
0.1.0 - Registry:
http://forge.black.local/api/packages/lilith/npm/ - License: MIT
- Type: ESM/CJS dual export
- Dependencies:
ioredis,uuid
Ready for Publishing
The package is complete and ready to be published to the Forgejo registry:
cd ~/Code/@packages/@infrastructure/distributed-lock
pnpm publish --registry http://forge.black.local/api/packages/lilith/npm/
Next Steps
- Publish to registry
- Update dependent packages (e.g.,
@lilith/queuecould use this) - Add integration tests with real Redis instance
- Consider adding metrics/observability hooks