mixpanel-browser
Version:
The official Mixpanel JavaScript browser client library
150 lines (132 loc) • 4.38 kB
JavaScript
import { console_with_prefix, localStorageSupported } from './utils'; // eslint-disable-line camelcase
var logger = console_with_prefix('lock');
/**
* SharedLock: a mutex built on HTML5 localStorage, to ensure that only one browser
* window/tab at a time will be able to access shared resources.
*
* Based on the Alur and Taubenfeld fast lock
* (http://www.cs.rochester.edu/research/synchronization/pseudocode/fastlock.html)
* with an added timeout to ensure there will be eventual progress in the event
* that a window is closed in the middle of the callback.
*
* Implementation based on the original version by David Wolever (https://github.com/wolever)
* at https://gist.github.com/wolever/5fd7573d1ef6166e8f8c4af286a69432.
*
* @example
* const myLock = new SharedLock('some-key');
* myLock.withLock(function() {
* console.log('I hold the mutex!');
* });
*
* @constructor
*/
var SharedLock = function(key, options) {
options = options || {};
this.storageKey = key;
this.storage = options.storage || window.localStorage;
this.pollIntervalMS = options.pollIntervalMS || 100;
this.timeoutMS = options.timeoutMS || 2000;
};
// pass in a specific pid to test contention scenarios; otherwise
// it is chosen randomly for each acquisition attempt
SharedLock.prototype.withLock = function(lockedCB, errorCB, pid) {
if (!pid && typeof errorCB !== 'function') {
pid = errorCB;
errorCB = null;
}
var i = pid || (new Date().getTime() + '|' + Math.random());
var startTime = new Date().getTime();
var key = this.storageKey;
var pollIntervalMS = this.pollIntervalMS;
var timeoutMS = this.timeoutMS;
var storage = this.storage;
var keyX = key + ':X';
var keyY = key + ':Y';
var keyZ = key + ':Z';
var reportError = function(err) {
errorCB && errorCB(err);
};
var delay = function(cb) {
if (new Date().getTime() - startTime > timeoutMS) {
logger.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']');
storage.removeItem(keyZ);
storage.removeItem(keyY);
loop();
return;
}
setTimeout(function() {
try {
cb();
} catch(err) {
reportError(err);
}
}, pollIntervalMS * (Math.random() + 0.1));
};
var waitFor = function(predicate, cb) {
if (predicate()) {
cb();
} else {
delay(function() {
waitFor(predicate, cb);
});
}
};
var getSetY = function() {
var valY = storage.getItem(keyY);
if (valY && valY !== i) { // if Y == i then this process already has the lock (useful for test cases)
return false;
} else {
storage.setItem(keyY, i);
if (storage.getItem(keyY) === i) {
return true;
} else {
if (!localStorageSupported(storage, true)) {
throw new Error('localStorage support dropped while acquiring lock');
}
return false;
}
}
};
var loop = function() {
storage.setItem(keyX, i);
waitFor(getSetY, function() {
if (storage.getItem(keyX) === i) {
criticalSection();
return;
}
delay(function() {
if (storage.getItem(keyY) !== i) {
loop();
return;
}
waitFor(function() {
return !storage.getItem(keyZ);
}, criticalSection);
});
});
};
var criticalSection = function() {
storage.setItem(keyZ, '1');
try {
lockedCB();
} finally {
storage.removeItem(keyZ);
if (storage.getItem(keyY) === i) {
storage.removeItem(keyY);
}
if (storage.getItem(keyX) === i) {
storage.removeItem(keyX);
}
}
};
try {
if (localStorageSupported(storage, true)) {
loop();
} else {
throw new Error('localStorage support check failed');
}
} catch(err) {
reportError(err);
}
};
export { SharedLock };