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.

150 lines 5.91 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.BaseDistributedLockPrimitive = void 0; const node_assert_1 = __importDefault(require("node:assert")); const locking_1 = require("@apiratorjs/locking"); const distributed_releaser_1 = require("./distributed-releaser"); class BaseDistributedLockPrimitive { constructor(props) { this.implementation = "redis"; this.unlockWaiters = new Set(); this.destroyed = false; const { name, redisClient } = props; node_assert_1.default.ok(name, "name must be provided"); this.name = name; this.redisClient = redisClient; this.queue = []; } get isDestroyed() { return this.destroyed; } async ensureSubscriber() { if (this.redisSubscriber) { return; } this.redisSubscriber = this.redisClient.duplicate(); await this.redisSubscriber.connect(); await this.redisSubscriber.subscribe(`${this.name}:cancel`, (message) => { if (!message.startsWith("cancel:")) { return; } const errMessage = message.slice("cancel:".length); this.rejectQueuedAcquirers(new locking_1.CancelledLockingError(errMessage || "Cancelled")); // Unlock waiters keep waiting: cancel only drops pending acquisitions. // Held locks stay held, so waitForUnlock* is still meaningful. }); await this.redisSubscriber.subscribe(`${this.name}:release`, async () => { while (this.queue.length > 0) { const nextInQueue = this.queue.shift(); const acquireToken = await this.tryAcquire(nextInQueue.ttlMs); if (!acquireToken) { this.queue.unshift(nextInQueue); break; } if (nextInQueue.timer) { clearTimeout(nextInQueue.timer); nextInQueue.timer = null; } const releaser = new distributed_releaser_1.DistributedReleaser(() => this.release(acquireToken), acquireToken); nextInQueue.resolve(releaser); } // Queued acquirers get first refusal; only what is left over frees up // the waiters of waitForUnlock / waitForAnyUnlock / waitForFullyUnlock. await this.notifyUnlockWaiters(); }); await this.redisSubscriber.subscribe(`${this.name}:destroy`, async () => { await this.destroy(); }); } /** * Waits until `isSatisfied` holds after a release event. * * Waiters are kept locally and driven by the single `:release` subscription * opened in `ensureSubscriber`. They must never subscribe to that channel * themselves: node-redis appends listeners per `subscribe` call, and * `unsubscribe(channel)` without a listener drops *all* of them - including * the one that drains `queue`. */ async waitForUnlockEvent(isSatisfied) { if (await isSatisfied()) { return; } await this.ensureSubscriber(); return new Promise((resolve, reject) => { const waiter = { isSatisfied, resolve: () => { this.unlockWaiters.delete(waiter); resolve(); }, reject: (error) => { this.unlockWaiters.delete(waiter); reject(error); }, }; this.unlockWaiters.add(waiter); // A release may have landed while the subscriber was being set up. isSatisfied().then((satisfied) => { if (satisfied) { waiter.resolve(); } }, (error) => { // destroy() flips `destroyed` before settling waiters; isLocked / // freeCount then throw. Prefer resolve so we don't race ahead of // resolveUnlockWaiters() and leave waitForUnlock* rejected. if (this.destroyed) { waiter.resolve(); return; } waiter.reject(error); }); }); } async notifyUnlockWaiters() { if (this.destroyed) { // In-flight `:release` handling can outlive destroy()'s unsubscribe. // Satisfaction checks throw once destroyed; settle the same way destroy does. this.resolveUnlockWaiters(); return; } for (const waiter of [...this.unlockWaiters]) { try { if (await waiter.isSatisfied()) { waiter.resolve(); } } catch (error) { if (this.destroyed) { waiter.resolve(); continue; } waiter.reject(error); } } } rejectQueuedAcquirers(error) { while (this.queue.length > 0) { const deferred = this.queue.shift(); if (deferred.timer) { clearTimeout(deferred.timer); deferred.timer = null; } deferred.reject(error); } } /** * A destroyed lock cannot be held: settle unlock waiters successfully so a * fire-and-forget `void lock.waitForUnlock()` does not become an unhandled * rejection. */ resolveUnlockWaiters() { for (const waiter of [...this.unlockWaiters]) { waiter.resolve(); } } } exports.BaseDistributedLockPrimitive = BaseDistributedLockPrimitive; //# sourceMappingURL=base-distributed-lock-primitive.js.map