Page MenuHomec4science

leader-election.js
No OneTemporary

File Metadata

Created
Mon, Jan 27, 07:28

leader-election.js

import { sleep, randomToken } from './util.js';
import unload from 'unload';
var LeaderElection = function LeaderElection(channel, options) {
this._channel = channel;
this._options = options;
this.isLeader = false;
this.isDead = false;
this.token = randomToken();
this._isApl = false; // _isApplying
this._reApply = false; // things to clean up
this._unl = []; // _unloads
this._lstns = []; // _listeners
this._invs = []; // _intervals
this._dpL = function () {}; // onduplicate listener
this._dpLC = false; // true when onduplicate called
};
LeaderElection.prototype = {
applyOnce: function applyOnce() {
var _this = this;
if (this.isLeader) return Promise.resolve(false);
if (this.isDead) return Promise.resolve(false); // do nothing if already running
if (this._isApl) {
this._reApply = true;
return Promise.resolve(false);
}
this._isApl = true;
var stopCriteria = false;
var recieved = [];
var handleMessage = function handleMessage(msg) {
if (msg.context === 'leader' && msg.token != _this.token) {
recieved.push(msg);
if (msg.action === 'apply') {
// other is applying
if (msg.token > _this.token) {
// other has higher token, stop applying
stopCriteria = true;
}
}
if (msg.action === 'tell') {
// other is already leader
stopCriteria = true;
}
}
};
this._channel.addEventListener('internal', handleMessage);
var ret = _sendMessage(this, 'apply') // send out that this one is applying
.then(function () {
return sleep(_this._options.responseTime);
}) // let others time to respond
.then(function () {
if (stopCriteria) return Promise.reject(new Error());else return _sendMessage(_this, 'apply');
}).then(function () {
return sleep(_this._options.responseTime);
}) // let others time to respond
.then(function () {
if (stopCriteria) return Promise.reject(new Error());else return _sendMessage(_this);
}).then(function () {
return beLeader(_this);
}) // no one disagreed -> this one is now leader
.then(function () {
return true;
})["catch"](function () {
return false;
}) // apply not successfull
.then(function (success) {
_this._channel.removeEventListener('internal', handleMessage);
_this._isApl = false;
if (!success && _this._reApply) {
_this._reApply = false;
return _this.applyOnce();
} else return success;
});
return ret;
},
awaitLeadership: function awaitLeadership() {
if (
/* _awaitLeadershipPromise */
!this._aLP) {
this._aLP = _awaitLeadershipOnce(this);
}
return this._aLP;
},
set onduplicate(fn) {
this._dpL = fn;
},
die: function die() {
var _this2 = this;
if (this.isDead) return;
this.isDead = true;
this._lstns.forEach(function (listener) {
return _this2._channel.removeEventListener('internal', listener);
});
this._invs.forEach(function (interval) {
return clearInterval(interval);
});
this._unl.forEach(function (uFn) {
uFn.remove();
});
return _sendMessage(this, 'death');
}
};
function _awaitLeadershipOnce(leaderElector) {
if (leaderElector.isLeader) return Promise.resolve();
return new Promise(function (res) {
var resolved = false;
function finish() {
if (resolved) {
return;
}
resolved = true;
clearInterval(interval);
leaderElector._channel.removeEventListener('internal', whenDeathListener);
res(true);
} // try once now
leaderElector.applyOnce().then(function () {
if (leaderElector.isLeader) {
finish();
}
}); // try on fallbackInterval
var interval = setInterval(function () {
leaderElector.applyOnce().then(function () {
if (leaderElector.isLeader) {
finish();
}
});
}, leaderElector._options.fallbackInterval);
leaderElector._invs.push(interval); // try when other leader dies
var whenDeathListener = function whenDeathListener(msg) {
if (msg.context === 'leader' && msg.action === 'death') {
leaderElector.applyOnce().then(function () {
if (leaderElector.isLeader) finish();
});
}
};
leaderElector._channel.addEventListener('internal', whenDeathListener);
leaderElector._lstns.push(whenDeathListener);
});
}
/**
* sends and internal message over the broadcast-channel
*/
function _sendMessage(leaderElector, action) {
var msgJson = {
context: 'leader',
action: action,
token: leaderElector.token
};
return leaderElector._channel.postInternal(msgJson);
}
export function beLeader(leaderElector) {
leaderElector.isLeader = true;
var unloadFn = unload.add(function () {
return leaderElector.die();
});
leaderElector._unl.push(unloadFn);
var isLeaderListener = function isLeaderListener(msg) {
if (msg.context === 'leader' && msg.action === 'apply') {
_sendMessage(leaderElector, 'tell');
}
if (msg.context === 'leader' && msg.action === 'tell' && !leaderElector._dpLC) {
/**
* another instance is also leader!
* This can happen on rare events
* like when the CPU is at 100% for long time
* or the tabs are open very long and the browser throttles them.
* @link https://github.com/pubkey/broadcast-channel/issues/414
* @link https://github.com/pubkey/broadcast-channel/issues/385
*/
leaderElector._dpLC = true;
leaderElector._dpL(); // message the lib user so the app can handle the problem
_sendMessage(leaderElector, 'tell'); // ensure other leader also knows the problem
}
};
leaderElector._channel.addEventListener('internal', isLeaderListener);
leaderElector._lstns.push(isLeaderListener);
return _sendMessage(leaderElector, 'tell');
}
function fillOptionsWithDefaults(options, channel) {
if (!options) options = {};
options = JSON.parse(JSON.stringify(options));
if (!options.fallbackInterval) {
options.fallbackInterval = 3000;
}
if (!options.responseTime) {
options.responseTime = channel.method.averageResponseTime(channel.options);
}
return options;
}
export function createLeaderElection(channel, options) {
if (channel._leaderElector) {
throw new Error('BroadcastChannel already has a leader-elector');
}
options = fillOptionsWithDefaults(options, channel);
var elector = new LeaderElection(channel, options);
channel._befC.push(function () {
return elector.die();
});
channel._leaderElector = elector;
return elector;
}

Event Timeline