@devlearning/mutex-fast-lock
Version:
Mutex fast lock localstorage
228 lines (218 loc) • 9.82 kB
JavaScript
import * as i0 from '@angular/core';
import { InjectionToken, Injectable, Inject, NgModule } from '@angular/core';
import { Observable } from 'rxjs';
class MutexFastLockConfig {
lockPrefix;
timeout;
debugEnabled;
}
class MutexLockStats {
restartCount;
locksLost;
contentionCount;
acquireDuration;
acquireStart;
acquireEnd;
lockStart;
lockEnd;
lockDuration;
}
const MUTEX_FAST_LOCK_CONFIG = new InjectionToken('MUTEX_FAST_LOCK_CONFIG');
class LockItem {
clientId;
expiresAt;
}
class MutexFastLockService {
_config;
_clientId;
_xPrefix;
_yPrefix;
constructor(_config) {
this._config = _config;
this._clientId = this._generateRandomId();
this._xPrefix = _config.lockPrefix + '_X_';
this._yPrefix = _config.lockPrefix + '_Y_';
let that = this;
window.addEventListener("beforeunload", function (ev) {
var arr = [];
for (var i = 0; i < localStorage.length; i++) {
if (localStorage.key(i).indexOf(that._xPrefix) == 0
|| localStorage.key(i).indexOf(that._yPrefix) == 0) {
arr.push(localStorage.key(i));
}
}
for (var i = 0; i < arr.length; i++) {
localStorage.removeItem(arr[i]);
}
});
}
lock(key, timeout = -1) {
let that = this;
if (timeout == -1)
timeout = this._config.timeout;
let xLock = that._xPrefix + key;
let yLock = that._yPrefix + key;
let lockStats = new MutexLockStats();
that.resetStats(lockStats);
this._config.debugEnabled ?? console.debug('Attempting to acquire Lock on "%s" using FastMutex instance "%s"', key, this._clientId);
lockStats.acquireStart = new Date().getTime();
//return new Promise(function (resolve, reject) {
return new Observable(subscriber => {
// we need to differentiate between API calls to lock() and our internal
// recursive calls so that we can timeout based on the original lock() and
// not each subsequent call. Therefore, create a new function here within
// the promise closure that we use for subsequent calls:
let acquireLock = function acquireLock(key) {
that._releaseExpiredLock(xLock);
that._releaseExpiredLock(yLock);
var elapsedTime = new Date().getTime() - lockStats.acquireStart;
if (elapsedTime >= timeout) {
that._config.debugEnabled ?? console.debug('Lock on "%s" could not be acquired within %sms by FastMutex client "%s"', key, timeout, that._clientId);
subscriber.error(new Error('Lock could not be acquired within ' + timeout + 'ms'));
}
that._setItem(xLock, that._clientId, timeout);
// if y exists, another client is getting a lock, so retry in a bit
var lsY = that._getItem(yLock, timeout);
if (lsY) {
that._config.debugEnabled ?? console.debug('Lock exists on Y (%s), restarting...', lsY);
lockStats.restartCount++;
setTimeout(function () {
return acquireLock(key);
}, 10);
return;
}
// ask for inner lock
that._setItem(yLock, that._clientId, timeout);
// if x was changed, another client is contending for an inner lock
var lsX = that._getItem(xLock, timeout);
if (lsX !== that._clientId) {
lockStats.contentionCount++;
that._config.debugEnabled ?? console.debug('Lock contention detected. X="%s"', lsX);
// Give enough time for critical section:
setTimeout(function () {
lsY = that._getItem(yLock, timeout);
if (lsY === that._clientId) {
// we have a lock
that._config.debugEnabled ?? console.debug('FastMutex client "%s" won the lock contention on "%s"', that._clientId, key);
that.resolveWithStats(subscriber, lockStats);
}
else {
// we lost the lock, restart the process again
lockStats.restartCount++;
lockStats.locksLost++;
that._config.debugEnabled ?? console.debug('FastMutex client "%s" lost the lock contention on "%s" to another process (%s). Restarting...', that._clientId, key, lsY);
setTimeout(function () {
return acquireLock(key);
}, 10);
}
}, 100);
return;
}
// no contention:
that._config.debugEnabled ?? console.debug('FastMutex client "%s" acquired a lock on "%s" with no contention', that._clientId, key);
that.resolveWithStats(subscriber, lockStats);
};
acquireLock(key);
});
}
release(key) {
this._config.debugEnabled ?? console.debug('FastMutex client "%s" is releasing lock on "%s"', this._clientId, key);
let x = this._xPrefix + key;
let y = this._yPrefix + key;
localStorage.removeItem(x);
localStorage.removeItem(y);
//that.lockStats.lockEnd = new Date().getTime();
//that.lockStats.lockDuration = that.lockStats.lockEnd - that.lockStats.lockStart;
//let retStats = angular.copy(that.lockStats);
//that.resetStats();
//return retStats;
}
_generateRandomId() {
return Math.floor(Math.random() * 10000000000) + '';
}
resetStats(lockStats) {
lockStats.restartCount = 0;
lockStats.locksLost = 0;
lockStats.contentionCount = 0;
lockStats.acquireDuration = 0;
lockStats.acquireStart = null;
}
resolveWithStats(subscriber, stats) {
var currentTime = new Date().getTime();
stats.acquireEnd = currentTime;
stats.acquireDuration = stats.acquireEnd - stats.acquireStart;
stats.lockStart = currentTime;
subscriber.next(stats);
subscriber.complete();
}
/**
* Helper function to wrap all values in an object that includes the time (so
* that we can expire it in the future) and json.stringify's it
*/
_setItem(key, clientId, timeout) {
let lockItem = new LockItem();
lockItem.clientId = clientId;
lockItem.expiresAt = new Date().getTime() + timeout;
return localStorage.setItem(key, JSON.stringify(lockItem));
}
/**
* Helper function to parse JSON encoded values set in localStorage
*/
_getItem(key, timeout) {
var item = localStorage.getItem(key);
if (!item)
return null;
var lockItem = JSON.parse(item);
if (new Date().getTime() - lockItem.expiresAt >= timeout) {
this._config.debugEnabled ?? console.debug('FastMutex client "%s" removed an expired record on "%s"', this._clientId, key);
localStorage.removeItem(key);
return null;
}
return lockItem.clientId;
}
_releaseExpiredLock(key) {
var item = localStorage.getItem(key);
if (!item)
return null;
var lockItem = JSON.parse(item);
if (lockItem.expiresAt <= new Date().getTime()) {
this._config.debugEnabled ?? console.debug('FastMutex auto removed an expired record on "%s"', key);
localStorage.removeItem(key);
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.1.6", ngImport: i0, type: MutexFastLockService, deps: [{ token: MUTEX_FAST_LOCK_CONFIG }], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.1.6", ngImport: i0, type: MutexFastLockService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.1.6", ngImport: i0, type: MutexFastLockService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}], ctorParameters: function () { return [{ type: MutexFastLockConfig, decorators: [{
type: Inject,
args: [MUTEX_FAST_LOCK_CONFIG]
}] }]; } });
class MutexFastLockModule {
static forRoot(config) {
return ({
ngModule: MutexFastLockModule,
providers: [
{ provide: MUTEX_FAST_LOCK_CONFIG, useValue: config },
]
});
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.1.6", ngImport: i0, type: MutexFastLockModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "16.1.6", ngImport: i0, type: MutexFastLockModule });
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "16.1.6", ngImport: i0, type: MutexFastLockModule });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.1.6", ngImport: i0, type: MutexFastLockModule, decorators: [{
type: NgModule
}] });
/*
* Public API Surface of cnet-mutex-fast-lock
*/
/**
* Generated bundle index. Do not edit.
*/
export { MutexFastLockConfig, MutexFastLockModule, MutexFastLockService, MutexLockStats };
//# sourceMappingURL=devlearning-mutex-fast-lock.mjs.map