UNPKG

@apiratorjs/locking-redis

Version:

An extension to the core @apiratorjs/locking library, providing Redis-based implementations of distributed mutexes and semaphores for true cross-process concurrency control in Node.js.

132 lines 4.77 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.RedisDistributedMutex = void 0; const node_crypto_1 = __importDefault(require("node:crypto")); const constants_1 = require("./constants"); const distributed_releaser_1 = require("./distributed-releaser"); const base_distributed_primitive_1 = require("./base-distributed-primitive"); class RedisDistributedMutex extends base_distributed_primitive_1.BaseDistributedPrimitive { constructor(props) { super({ ...props, name: `mutex:${props.name}` }); } async destroy() { if (this._isDestroyed) { return; } this._isDestroyed = true; await this._redisClient.del(this.name); if (this._redisSubscriber) { await this._redisSubscriber.unsubscribe(`${this.name}:cancel`); await this._redisSubscriber.unsubscribe(`${this.name}:release`); await this._redisSubscriber.unsubscribe(`${this.name}:destroy`); await this._redisSubscriber.disconnect(); this._redisSubscriber = undefined; } await this._redisClient.publish(`${this.name}:destroy`, "destroyed"); while (this._queue.length > 0) { const deferred = this._queue.shift(); if (deferred.timer) { clearTimeout(deferred.timer); deferred.timer = null; } deferred.reject(new Error("Mutex destroyed")); } } async acquire(params) { this.throwIfDestroyed(); await this.ensureSubscriber(); const { timeoutMs = constants_1.DEFAULT_TTL_MS } = params ?? {}; const acquireToken = await this.tryAcquire(timeoutMs); if (acquireToken) { return new distributed_releaser_1.DistributedReleaser(() => this.release(acquireToken), acquireToken); } // Return a promise that resolves once the lock is eventually acquired. return new Promise((resolve, reject) => { const deferred = { resolve, reject, ttlMs: timeoutMs, timer: null }; deferred.timer = setTimeout(() => { const index = this._queue.indexOf(deferred); if (index !== -1) { this._queue.splice(index, 1); } reject(new Error("Timeout acquiring")); }, timeoutMs); this._queue.push(deferred); }); } async cancel(errMessage) { this.throwIfDestroyed(); const msg = `cancel:${errMessage ?? ""}`; await this._redisClient.publish(`${this.name}:cancel`, msg); } async isLocked() { const val = await this._redisClient.get(this.name); return val !== null; } async runExclusive(...args) { let params; let fn; if (args.length === 1) { fn = args[0]; } else if (args.length === 2) { params = args[0]; fn = args[1]; } else { throw new Error("Invalid arguments for runExclusive"); } const releaser = await this.acquire(params); try { return await fn(); } finally { await releaser.release(); } } async tryAcquire(timeoutMs) { const token = `${this.name}:${node_crypto_1.default.randomUUID()}`; const result = await this._redisClient.set(this.name, token, { NX: true, PX: timeoutMs }); if (result === "OK") { this._lockValue = token; return token; } return undefined; } throwIfDestroyed() { if (this._isDestroyed) { throw new Error("Mutex has been destroyed"); } } async release(token) { this.throwIfDestroyed(); // Only release if the lock key’s value matches our lockValue const RELEASE_LUA = ` if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) end return 0 `; const result = await this._redisClient.eval(RELEASE_LUA, { keys: [this.name], arguments: [this._lockValue ?? ""] }); // If we successfully released, let the next queue item know they can try if (result === 1) { await this._redisClient.publish(`${this.name}:release`, token); this._lockValue = undefined; } } } exports.RedisDistributedMutex = RedisDistributedMutex; //# sourceMappingURL=redis-distributed-mutex.js.map