UNPKG

syncguard

Version:

Functional TypeScript library for distributed locking across microservices. Prevents race conditions with Redis, PostgreSQL, Firestore, and custom backends. Features automatic lock management, timeout handling, and extensible architecture.

76 lines (75 loc) 3.24 kB
// SPDX-FileCopyrightText: 2025-present Kriasoft // SPDX-License-Identifier: MIT import { decorateAcquireResult } from "../common/disposable.js"; import { normalizeAndValidateKey } from "../common/validation.js"; import { createRedisConfig } from "./config.js"; import { createAcquireOperation } from "./operations/acquire.js"; import { createExtendOperation } from "./operations/extend.js"; import { createIsLockedOperation } from "./operations/is-locked.js"; import { createLookupOperation } from "./operations/lookup.js"; import { createReleaseOperation } from "./operations/release.js"; import { ACQUIRE_SCRIPT, EXTEND_SCRIPT, IS_LOCKED_SCRIPT, LOOKUP_BY_KEY_SCRIPT, LOOKUP_BY_LOCKID_SCRIPT, RELEASE_SCRIPT, } from "./scripts.js"; /** * Creates Redis-based distributed lock backend using Lua scripts for atomicity. * * Storage: Lock data at {keyPrefix}{lockKey}, lockId index at {keyPrefix}id:{lockId} * * @param redis - ioredis client instance * @param options - Backend configuration (keyPrefix, ttl, tolerance) * @returns LockBackend with server-side time authority * @see docs/specs/redis-backend.md */ export function createRedisBackend(redis, options = {}) { const config = createRedisConfig(options); // Register Lua scripts for server-side caching (avoids re-parsing on each call) if (typeof redis.defineCommand === "function") { redis.defineCommand("acquireLock", { numberOfKeys: 3, lua: ACQUIRE_SCRIPT, }); redis.defineCommand("releaseLock", { numberOfKeys: 1, // ADR-013: Only lockIdKey (no keyPrefix) lua: RELEASE_SCRIPT, }); redis.defineCommand("extendLock", { numberOfKeys: 1, // ADR-013: Only lockIdKey (no keyPrefix) lua: EXTEND_SCRIPT, }); redis.defineCommand("checkLock", { numberOfKeys: 1, lua: IS_LOCKED_SCRIPT, }); redis.defineCommand("lookupByKey", { numberOfKeys: 1, lua: LOOKUP_BY_KEY_SCRIPT, }); redis.defineCommand("lookupByLockId", { numberOfKeys: 1, // ADR-013: Only lockIdKey (no keyPrefix) lua: LOOKUP_BY_LOCKID_SCRIPT, }); } const redisWithCommands = redis; const capabilities = { backend: "redis", supportsFencing: true, timeAuthority: "server", }; // Create base operations const acquireCore = createAcquireOperation(redisWithCommands, config); const releaseOp = createReleaseOperation(redisWithCommands, config); const extendOp = createExtendOperation(redisWithCommands, config); // Create backend object with disposal support const backend = { acquire: async (opts) => { const normalizedKey = normalizeAndValidateKey(opts.key); const result = await acquireCore(opts); return decorateAcquireResult(backend, result, normalizedKey, config.onReleaseError, config.disposeTimeoutMs); }, release: releaseOp, extend: extendOp, isLocked: createIsLockedOperation(redisWithCommands, config), lookup: createLookupOperation(redisWithCommands, config), capabilities, }; return backend; }