UNPKG

earlect

Version:

Leader Election Generic Implementation for TypeScript

200 lines (199 loc) 7.79 kB
"use strict"; 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 = "";