earlect
Version:
Leader Election Generic Implementation for TypeScript
200 lines (199 loc) • 7.79 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const crypto_1 = __importDefault(require("crypto"));
const events_1 = require("events");
const pino_1 = __importDefault(require("pino"));
const logger = pino_1.default();
var VikingState;
(function (VikingState) {
VikingState["WANDERER"] = "Wanderer";
VikingState["FOLLOWER"] = "Follower";
VikingState["CHALLENGER"] = "Challenger";
VikingState["LEADER"] = "Leader";
})(VikingState = exports.VikingState || (exports.VikingState = {}));
var VikingEvent;
(function (VikingEvent) {
VikingEvent["LEADER_SHOUT"] = "Leader Shout";
VikingEvent["CHALLENGE"] = "Challenge";
VikingEvent["CHALLENGE_RUMOUR"] = "Challenge Rumour";
})(VikingEvent = exports.VikingEvent || (exports.VikingEvent = {}));
class Viking {
constructor(hall) {
this.hall = hall;
this.name = crypto_1.default.randomBytes(13).toString("hex");
this.leaderName = Viking.NO_LEADER_NAME;
this.state = VikingState.WANDERER;
this.comm = new events_1.EventEmitter();
this.challengerHP = 0;
this.leaderWatchers = [];
}
static async createInHall(hall) {
const viking = new Viking(hall);
hall.registerViking(viking);
viking.setLeaderShoutTimeout();
viking.comm.on(VikingEvent.LEADER_SHOUT, (leaderShout) => {
viking.resetLeaderShoutTimeout();
if (viking.state === VikingState.WANDERER ||
viking.state === VikingState.CHALLENGER) {
viking.setLeaderName(leaderShout.leaderName);
viking.changeStateTo(VikingState.FOLLOWER);
}
else if (viking.state === VikingState.FOLLOWER) {
viking.setLeaderName(leaderShout.leaderName);
}
else if (viking.state === VikingState.LEADER && leaderShout.leaderName !== viking.name) {
logger.debug(`We have another leader '${leaderShout.leaderName}'. Going rogue...`);
viking.setLeaderName(Viking.NO_LEADER_NAME);
viking.changeStateTo(VikingState.CHALLENGER);
}
});
viking.comm.on(VikingEvent.CHALLENGE, (challenge) => {
if (viking.state === VikingState.CHALLENGER) {
viking.resetChallengeTimeout();
logger.debug(`I still have ${viking.challengerHP} ...`);
viking.challengerHP -= challenge.power;
if (viking.challengerHP <= 0) {
logger.debug("... and I lose all my force");
viking.changeStateTo(VikingState.FOLLOWER);
}
else {
logger.debug("... and I still fight");
}
}
});
viking.comm.on(VikingEvent.CHALLENGE_RUMOUR, (challenge) => {
if (viking.state === VikingState.CHALLENGER && viking.name !== challenge.challengerName) {
logger.debug("Hearing noise of battle...");
viking.resetChallengeTimeout();
}
});
setInterval(() => {
if (viking.state === VikingState.LEADER) {
const leaderShout = { leaderName: viking.name };
hall.sendLeaderShout(leaderShout);
}
}, Viking.LEADER_SHOUT_INTERVAL_MS);
setInterval(() => {
if (viking.state === VikingState.CHALLENGER) {
const challenge = {
challengerName: viking.name,
power: Viking.generateChallengePower(),
};
hall.sendChallenge(challenge);
}
}, Viking.CHALLENGE_SEND_INTERVAL_MS);
return viking;
}
static generateChallengePower() {
return Viking.MIN_CHALLENGE_HP
+ Math.floor((Viking.MAX_CHALLENGE_HP - Viking.MIN_CHALLENGE_HP) * Math.random());
}
receiveLeaderShout(leaderShout) {
const viking = this;
viking.comm.emit(VikingEvent.LEADER_SHOUT, leaderShout);
}
receiveChallenge(challenge) {
const viking = this;
if (challenge.challengerName !== viking.name) {
viking.comm.emit(VikingEvent.CHALLENGE, challenge);
}
}
receiveChallengeRumour(challenge) {
const viking = this;
viking.comm.emit(VikingEvent.CHALLENGE_RUMOUR, challenge);
}
addLeaderWatcher(leaderWatcher) {
const viking = this;
viking.leaderWatchers.push(leaderWatcher);
if (viking.state === VikingState.LEADER) {
leaderWatcher.becomeLeader();
}
else {
leaderWatcher.dropLeader();
}
}
setLeaderName(leaderName) {
const viking = this;
viking.leaderName = leaderName;
}
changeStateTo(newState) {
const viking = this;
logger.debug(`${viking.state} => ${newState}`);
// Before state change actions
if (viking.state === VikingState.CHALLENGER) {
viking.cancelChallengeTimeout();
}
else if (viking.state === VikingState.LEADER) {
for (const leaderWatcher of viking.leaderWatchers) {
leaderWatcher.dropLeader();
}
}
// State change
viking.state = newState;
// After state change actions
if (viking.state === VikingState.CHALLENGER) {
viking.challengerHP = Viking.INITIAL_HP;
viking.setChallengeTimeout();
}
else if (viking.state === VikingState.LEADER) {
for (const leaderWatcher of viking.leaderWatchers) {
leaderWatcher.becomeLeader();
}
}
}
setLeaderShoutTimeout() {
const viking = this;
viking.leaderShoutTimeout = setTimeout(() => {
if (viking.state === VikingState.WANDERER
|| viking.state === VikingState.FOLLOWER) {
logger.debug(viking.state + ": No leader viking around => becoming a Challenger");
viking.changeStateTo(VikingState.CHALLENGER);
}
else if (viking.state === VikingState.LEADER) {
logger.debug(viking.state + ": Can't hear any leader shout... Not even my own... Have I been raptured??? => becoming a Wanderer");
viking.changeStateTo(VikingState.WANDERER);
}
}, Viking.LEADER_SHOUT_TIMEOUT_MS);
}
cancelLeaderShoutTimeout() {
const viking = this;
if (viking.leaderShoutTimeout) {
clearTimeout(viking.leaderShoutTimeout);
}
}
resetLeaderShoutTimeout() {
const viking = this;
viking.cancelLeaderShoutTimeout();
viking.setLeaderShoutTimeout();
}
setChallengeTimeout() {
const viking = this;
viking.challengeTimeout = setTimeout(() => {
logger.debug(viking.state + ": No other viking challenged me => becoming a Leader");
viking.changeStateTo(VikingState.LEADER);
}, Viking.CHALLENGE_TIMEOUT_MS);
}
cancelChallengeTimeout() {
const viking = this;
if (viking.challengeTimeout) {
clearTimeout(viking.challengeTimeout);
}
}
resetChallengeTimeout() {
const viking = this;
viking.cancelChallengeTimeout();
viking.setChallengeTimeout();
}
}
exports.Viking = Viking;
Viking.INITIAL_HP = 100;
Viking.MIN_CHALLENGE_HP = 30;
Viking.MAX_CHALLENGE_HP = 50;
Viking.LEADER_SHOUT_INTERVAL_MS = 100;
Viking.LEADER_SHOUT_TIMEOUT_MS = 2000;
Viking.CHALLENGE_SEND_INTERVAL_MS = 100;
Viking.CHALLENGE_TIMEOUT_MS = 1000;
Viking.NO_LEADER_NAME = "";