@apiratorjs/locking
Version:
A lightweight library providing both local and distributed locking primitives (mutexes, semaphores, and read-write locks) for managing concurrency in Node.js.
149 lines • 5.39 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.Semaphore = void 0;
const node_assert_1 = __importDefault(require("node:assert"));
const node_crypto_1 = __importDefault(require("node:crypto"));
const constants_1 = require("./constants");
const errors_1 = require("./errors");
class Releaser {
constructor(_onRelease, _token) {
this._onRelease = _onRelease;
this._token = _token;
}
async release() {
await this._onRelease();
}
getToken() {
return this._token;
}
}
class Semaphore {
constructor(maxCount) {
node_assert_1.default.ok(maxCount > 0, "maxCount must be greater than 0");
this.maxCount = maxCount;
this._freeCount = maxCount;
this._queue = [];
this._waitingForAnyUnlockListeners = [];
this._waitingForFullyUnlockListeners = [];
}
async waitForFullyUnlock() {
if (this.maxCount === this._freeCount) {
return;
}
return new Promise((resolve, reject) => {
this._waitingForFullyUnlockListeners.push({
resolve,
reject
});
});
}
async waitForAnyUnlock() {
if (this._freeCount > 0) {
return;
}
return new Promise((resolve, reject) => {
this._waitingForAnyUnlockListeners.push({
resolve,
reject
});
});
}
async runExclusive(...args) {
let callback;
let params;
if (args.length === 1) {
callback = args[0];
}
else {
params = args[0];
callback = args[1];
}
const releaser = await this.acquire(params);
try {
return await callback();
}
finally {
await releaser.release();
}
}
async freeCount() {
return this._freeCount;
}
async acquire(params, acquireToken) {
const timeoutMs = params?.timeoutMs || constants_1.DEFAULT_TIMEOUT_IN_MS;
const token = (acquireToken ?? node_crypto_1.default.randomUUID());
const releaser = new Releaser(this.release.bind(this), token);
if (this._freeCount > 0) {
this._freeCount--;
return releaser;
}
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
const index = this._queue.indexOf(deferred);
if (index !== -1) {
this._queue.splice(index, 1);
}
reject(new errors_1.TimeoutLockingError("Timeout acquiring semaphore"));
}, timeoutMs);
const deferred = {
resolve: () => {
if (timer) {
clearTimeout(timer);
}
resolve(releaser);
},
reject: (err) => {
if (timer) {
clearTimeout(timer);
}
reject(err);
}
};
this._queue.push(deferred);
});
}
async cancelAll(errMessage) {
const cancellationList = [...this._queue];
await Promise.all(cancellationList.map(deferred => deferred.reject(new errors_1.CancelledLockingError(errMessage ?? "Semaphore cancelled"))));
this._queue = [];
this._freeCount = this.maxCount;
// Notify all waitingForAnyUnlockListeners listeners since we're resetting to unlocked state
const waitingForAnyUnlockListeners = [...this._waitingForAnyUnlockListeners];
this._waitingForAnyUnlockListeners = [];
waitingForAnyUnlockListeners.forEach(listener => listener.reject(new errors_1.CancelledLockingError(errMessage ?? "Semaphore cancelled")));
// Notify all waitingForFullyUnlockListeners listeners since we're resetting to unlocked state
const fullyUnlockListeners = [...this._waitingForFullyUnlockListeners];
this._waitingForFullyUnlockListeners = [];
fullyUnlockListeners.forEach(listener => listener.reject(new errors_1.CancelledLockingError(errMessage ?? "Semaphore cancelled")));
}
async isLocked() {
return this._freeCount === 0;
}
async release() {
if (this._freeCount === this.maxCount) {
return;
}
if (this._queue.length > 0) {
const { resolve } = this._queue.shift();
resolve();
}
else {
this._freeCount++;
if (this._freeCount > 0 && this._waitingForAnyUnlockListeners.length > 0) {
const listeners = [...this._waitingForAnyUnlockListeners];
this._waitingForAnyUnlockListeners = [];
listeners.forEach(listener => listener.resolve());
}
if (this._freeCount === this.maxCount) {
const listeners = [...this._waitingForFullyUnlockListeners];
this._waitingForFullyUnlockListeners = [];
listeners.forEach(listener => listener.resolve());
}
}
}
}
exports.Semaphore = Semaphore;
//# sourceMappingURL=semaphore.js.map