@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
JavaScript
"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