From b41ed928490c454a7641a9cdf630126e22a716e6 Mon Sep 17 00:00:00 2001 From: pubkey <8926560+pubkey@users.noreply.github.com> Date: Thu, 2 Dec 2021 21:57:50 +0100 Subject: [PATCH] 4.6.0 --- CHANGELOG.md | 2 + dist/es5node/broadcast-channel.js | 14 +- dist/es5node/index.js | 6 + dist/es5node/leader-election.js | 172 ++++++++++++++---------- dist/es5node/methods/node.js | 5 +- dist/es5node/util.js | 4 +- dist/esbrowser/broadcast-channel.js | 11 ++ dist/esbrowser/index.js | 2 +- dist/esbrowser/leader-election.js | 174 +++++++++++++++---------- dist/esbrowser/methods/node.js | 5 +- dist/esbrowser/util.js | 1 + dist/esnode/broadcast-channel.js | 11 ++ dist/esnode/index.js | 2 +- dist/esnode/leader-election.js | 174 +++++++++++++++---------- dist/esnode/methods/node.js | 5 +- dist/esnode/util.js | 1 + dist/lib/broadcast-channel.js | 14 +- dist/lib/browser.js | 194 ++++++++++++++++++---------- dist/lib/browser.min.js | 2 +- dist/lib/index.js | 6 + dist/lib/leader-election.js | 172 ++++++++++++++---------- dist/lib/methods/node.js | 5 +- dist/lib/util.js | 4 +- docs/e2e.js | 194 ++++++++++++++++++---------- docs/iframe.js | 194 ++++++++++++++++++---------- docs/index.js | 194 ++++++++++++++++++---------- docs/leader-iframe.js | 194 ++++++++++++++++++---------- docs/worker.js | 194 ++++++++++++++++++---------- 28 files changed, 1257 insertions(+), 699 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1ea5faa..4bfe57d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## X.X.X (comming soon) +## 4.6.0 (2 December 2021) + Other: - Added `broadcastChannel.id()` for debugging diff --git a/dist/es5node/broadcast-channel.js b/dist/es5node/broadcast-channel.js index 5e2822a4..7b0114f1 100644 --- a/dist/es5node/broadcast-channel.js +++ b/dist/es5node/broadcast-channel.js @@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); -exports.BroadcastChannel = void 0; +exports.OPEN_BROADCAST_CHANNELS = exports.BroadcastChannel = void 0; exports.clearNodeFolder = clearNodeFolder; exports.enforceOptions = enforceOptions; @@ -13,7 +13,18 @@ var _methodChooser = require("./method-chooser.js"); var _options = require("./options.js"); +/** + * Contains all open channels, + * used in tests to ensure everything is closed. + */ +var OPEN_BROADCAST_CHANNELS = new Set(); +exports.OPEN_BROADCAST_CHANNELS = OPEN_BROADCAST_CHANNELS; +var lastId = 0; + var BroadcastChannel = function BroadcastChannel(name, options) { + // identifier of the channel to debug stuff + this.id = lastId++; + OPEN_BROADCAST_CHANNELS.add(this); this.name = name; if (ENFORCED_OPTIONS) { @@ -154,6 +165,7 @@ BroadcastChannel.prototype = { return; } + OPEN_BROADCAST_CHANNELS["delete"](this); this.closed = true; var awaitPrepare = this._prepP ? this._prepP : _util.PROMISE_RESOLVED_VOID; this._onML = null; diff --git a/dist/es5node/index.js b/dist/es5node/index.js index 8bc266af..f02b5343 100644 --- a/dist/es5node/index.js +++ b/dist/es5node/index.js @@ -9,6 +9,12 @@ Object.defineProperty(exports, "BroadcastChannel", { return _broadcastChannel.BroadcastChannel; } }); +Object.defineProperty(exports, "OPEN_BROADCAST_CHANNELS", { + enumerable: true, + get: function get() { + return _broadcastChannel.OPEN_BROADCAST_CHANNELS; + } +}); Object.defineProperty(exports, "beLeader", { enumerable: true, get: function get() { diff --git a/dist/es5node/leader-election.js b/dist/es5node/leader-election.js index 86fe742c..31424cb0 100644 --- a/dist/es5node/leader-election.js +++ b/dist/es5node/leader-election.js @@ -20,15 +20,14 @@ var LeaderElection = function LeaderElection(broadcastChannel, options) { this.isDead = false; this.token = (0, _util.randomToken)(); /** - * _isApplying - * Only set when a leader application is - * running at the moment. - * @type {Promise | null} + * Apply Queue, + * used to ensure we do not run applyOnce() + * in parallel. */ - this._isApl = false; // _isApplying + this._aplQ = _util.PROMISE_RESOLVED_VOID; // amount of unfinished applyOnce() calls - this._reApply = false; // things to clean up + this._aplQC = 0; // things to clean up this._unl = []; // _unloads @@ -65,6 +64,11 @@ var LeaderElection = function LeaderElection(broadcastChannel, options) { }; LeaderElection.prototype = { + /** + * Returns true if the instance is leader, + * false if not. + * @async + */ applyOnce: function applyOnce() { var _this2 = this; @@ -73,82 +77,113 @@ LeaderElection.prototype = { } if (this.isDead) { - return _util.PROMISE_RESOLVED_FALSE; - } // do nothing if already running + return (0, _util.sleep)(0, false); + } + /** + * Already applying more then once, + * -> wait for the apply queue to be finished. + */ - if (this._isApl) { - this._reApply = true; - return (0, _util.sleep)(0, false); + if (this._aplQC > 1) { + return this._aplQ; } + /** + * Add a new apply-run + */ - var stopCriteria = false; - var recieved = []; - var handleMessage = function handleMessage(msg) { - if (msg.context === 'leader' && msg.token != _this2.token) { - recieved.push(msg); + var applyRun = function applyRun() { + /** + * Optimization shortcuts. + * Directly return if a previous run + * has already elected a leader. + */ + if (_this2.isLeader) { + return _util.PROMISE_RESOLVED_TRUE; + } - if (msg.action === 'apply') { - // other is applying - if (msg.token > _this2.token) { - // other has higher token, stop applying - stopCriteria = true; - } - } + var stopCriteria = false; + var stopCriteriaPromiseResolve; + /** + * Resolves when a stop criteria is reached. + * Uses as a performance shortcut so we do not + * have to await the responseTime when it is already clear + * that the election failed. + */ - if (msg.action === 'tell') { - // other is already leader + var stopCriteriaPromise = new Promise(function (res) { + stopCriteriaPromiseResolve = function stopCriteriaPromiseResolve() { stopCriteria = true; - _this2.hasLeader = true; + res(); + }; + }); + var recieved = []; + + var handleMessage = function handleMessage(msg) { + if (msg.context === 'leader' && msg.token != _this2.token) { + recieved.push(msg); + + if (msg.action === 'apply') { + // other is applying + if (msg.token > _this2.token) { + /** + * other has higher token + * -> stop applying and let other become leader. + */ + stopCriteriaPromiseResolve(); + } + } + + if (msg.action === 'tell') { + // other is already leader + stopCriteriaPromiseResolve(); + _this2.hasLeader = true; + } } - } - }; + }; - this.broadcastChannel.addEventListener('internal', handleMessage); + _this2.broadcastChannel.addEventListener('internal', handleMessage); - var applyPromise = _sendMessage(this, 'apply') // send out that this one is applying - .then(function () { - return (0, _util.sleep)(_this2._options.responseTime); - }) // let others time to respond - .then(function () { - if (stopCriteria) { - return Promise.reject(new Error()); - } else { + var applyPromise = _sendMessage(_this2, 'apply') // send out that this one is applying + .then(function () { + return Promise.race([(0, _util.sleep)(_this2._options.responseTime / 2), stopCriteriaPromise.then(function () { + return Promise.reject(new Error()); + })]); + }) // send again in case another instance was just created + .then(function () { return _sendMessage(_this2, 'apply'); - } - }).then(function () { - return (0, _util.sleep)(_this2._options.responseTime); - }) // let others time to respond - .then(function () { - if (stopCriteria) { - return Promise.reject(new Error()); - } else { - return _sendMessage(_this2); - } + }) // let others time to respond + .then(function () { + return Promise.race([(0, _util.sleep)(_this2._options.responseTime / 2), stopCriteriaPromise.then(function () { + return Promise.reject(new Error()); + })]); + })["catch"](function () {}).then(function () { + _this2.broadcastChannel.removeEventListener('internal', handleMessage); + + if (!stopCriteria) { + // no stop criteria -> own is leader + return beLeader(_this2).then(function () { + return true; + }); + } else { + // other is leader + return false; + } + }); + + return applyPromise; + }; + + this._aplQC = this._aplQC + 1; + this._aplQ = this._aplQ.then(function () { + return applyRun(); }).then(function () { - return beLeader(_this2); - }) // no one disagreed -> this one is now leader - .then(function () { - return true; - })["catch"](function () { - return false; - }) // apply not successfull - .then(function (success) { - _this2.broadcastChannel.removeEventListener('internal', handleMessage); - - _this2._isApl = false; - - if (!success && _this2._reApply) { - _this2._reApply = false; - return _this2.applyOnce(); - } else { - return success; - } + _this2._aplQC = _this2._aplQC - 1; + }); + return this._aplQ.then(function () { + return _this2.isLeader; }); - - this._isApl = applyPromise; - return applyPromise; }, awaitLeadership: function awaitLeadership() { if ( @@ -225,6 +260,7 @@ function _awaitLeadershipOnce(leaderElector) { }); // try on fallbackInterval var interval = setInterval(function () { + console.log('applyOnce via fallbackInterval'); leaderElector.applyOnce().then(function () { if (leaderElector.isLeader) { finish(); diff --git a/dist/es5node/methods/node.js b/dist/es5node/methods/node.js index 3a0f716b..4c142626 100644 --- a/dist/es5node/methods/node.js +++ b/dist/es5node/methods/node.js @@ -1066,7 +1066,10 @@ function postMessage(channelState, messageJson) { function emitOverFastPath(state, msgObj, messageJson) { - if (!state.options.node.useFastPath) return; // disabled + if (!state.options.node.useFastPath) { + // disabled + return; + } var others = OTHER_INSTANCES[state.channelName].filter(function (s) { return s !== state; diff --git a/dist/es5node/util.js b/dist/es5node/util.js index 1afac5ba..c70aec48 100644 --- a/dist/es5node/util.js +++ b/dist/es5node/util.js @@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); -exports.isNode = exports.PROMISE_RESOLVED_VOID = exports.PROMISE_RESOLVED_FALSE = void 0; +exports.isNode = exports.PROMISE_RESOLVED_VOID = exports.PROMISE_RESOLVED_TRUE = exports.PROMISE_RESOLVED_FALSE = void 0; exports.isPromise = isPromise; exports.microSeconds = microSeconds; exports.randomInt = randomInt; @@ -23,6 +23,8 @@ function isPromise(obj) { var PROMISE_RESOLVED_FALSE = Promise.resolve(false); exports.PROMISE_RESOLVED_FALSE = PROMISE_RESOLVED_FALSE; +var PROMISE_RESOLVED_TRUE = Promise.resolve(true); +exports.PROMISE_RESOLVED_TRUE = PROMISE_RESOLVED_TRUE; var PROMISE_RESOLVED_VOID = Promise.resolve(); exports.PROMISE_RESOLVED_VOID = PROMISE_RESOLVED_VOID; diff --git a/dist/esbrowser/broadcast-channel.js b/dist/esbrowser/broadcast-channel.js index bdccd05e..02e5f09b 100644 --- a/dist/esbrowser/broadcast-channel.js +++ b/dist/esbrowser/broadcast-channel.js @@ -1,7 +1,17 @@ import { isPromise, PROMISE_RESOLVED_FALSE, PROMISE_RESOLVED_VOID } from './util.js'; import { chooseMethod } from './method-chooser.js'; import { fillOptionsWithDefaults } from './options.js'; +/** + * Contains all open channels, + * used in tests to ensure everything is closed. + */ + +export var OPEN_BROADCAST_CHANNELS = new Set(); +var lastId = 0; export var BroadcastChannel = function BroadcastChannel(name, options) { + // identifier of the channel to debug stuff + this.id = lastId++; + OPEN_BROADCAST_CHANNELS.add(this); this.name = name; if (ENFORCED_OPTIONS) { @@ -137,6 +147,7 @@ BroadcastChannel.prototype = { return; } + OPEN_BROADCAST_CHANNELS["delete"](this); this.closed = true; var awaitPrepare = this._prepP ? this._prepP : PROMISE_RESOLVED_VOID; this._onML = null; diff --git a/dist/esbrowser/index.js b/dist/esbrowser/index.js index 0338ab48..cc1337b1 100644 --- a/dist/esbrowser/index.js +++ b/dist/esbrowser/index.js @@ -1,2 +1,2 @@ -export { BroadcastChannel, clearNodeFolder, enforceOptions } from './broadcast-channel'; +export { BroadcastChannel, clearNodeFolder, enforceOptions, OPEN_BROADCAST_CHANNELS } from './broadcast-channel'; export { createLeaderElection, beLeader } from './leader-election'; \ No newline at end of file diff --git a/dist/esbrowser/leader-election.js b/dist/esbrowser/leader-election.js index 642409b4..ee16e48d 100644 --- a/dist/esbrowser/leader-election.js +++ b/dist/esbrowser/leader-election.js @@ -1,4 +1,4 @@ -import { sleep, randomToken, PROMISE_RESOLVED_FALSE, PROMISE_RESOLVED_VOID } from './util.js'; +import { sleep, randomToken, PROMISE_RESOLVED_VOID, PROMISE_RESOLVED_TRUE } from './util.js'; import { add as unloadAdd } from 'unload'; var LeaderElection = function LeaderElection(broadcastChannel, options) { @@ -11,15 +11,14 @@ var LeaderElection = function LeaderElection(broadcastChannel, options) { this.isDead = false; this.token = randomToken(); /** - * _isApplying - * Only set when a leader application is - * running at the moment. - * @type {Promise | null} + * Apply Queue, + * used to ensure we do not run applyOnce() + * in parallel. */ - this._isApl = false; // _isApplying + this._aplQ = PROMISE_RESOLVED_VOID; // amount of unfinished applyOnce() calls - this._reApply = false; // things to clean up + this._aplQC = 0; // things to clean up this._unl = []; // _unloads @@ -56,6 +55,11 @@ var LeaderElection = function LeaderElection(broadcastChannel, options) { }; LeaderElection.prototype = { + /** + * Returns true if the instance is leader, + * false if not. + * @async + */ applyOnce: function applyOnce() { var _this2 = this; @@ -64,82 +68,113 @@ LeaderElection.prototype = { } if (this.isDead) { - return PROMISE_RESOLVED_FALSE; - } // do nothing if already running + return sleep(0, false); + } + /** + * Already applying more then once, + * -> wait for the apply queue to be finished. + */ - if (this._isApl) { - this._reApply = true; - return sleep(0, false); + if (this._aplQC > 1) { + return this._aplQ; } + /** + * Add a new apply-run + */ - var stopCriteria = false; - var recieved = []; - var handleMessage = function handleMessage(msg) { - if (msg.context === 'leader' && msg.token != _this2.token) { - recieved.push(msg); + var applyRun = function applyRun() { + /** + * Optimization shortcuts. + * Directly return if a previous run + * has already elected a leader. + */ + if (_this2.isLeader) { + return PROMISE_RESOLVED_TRUE; + } - if (msg.action === 'apply') { - // other is applying - if (msg.token > _this2.token) { - // other has higher token, stop applying - stopCriteria = true; - } - } + var stopCriteria = false; + var stopCriteriaPromiseResolve; + /** + * Resolves when a stop criteria is reached. + * Uses as a performance shortcut so we do not + * have to await the responseTime when it is already clear + * that the election failed. + */ - if (msg.action === 'tell') { - // other is already leader + var stopCriteriaPromise = new Promise(function (res) { + stopCriteriaPromiseResolve = function stopCriteriaPromiseResolve() { stopCriteria = true; - _this2.hasLeader = true; + res(); + }; + }); + var recieved = []; + + var handleMessage = function handleMessage(msg) { + if (msg.context === 'leader' && msg.token != _this2.token) { + recieved.push(msg); + + if (msg.action === 'apply') { + // other is applying + if (msg.token > _this2.token) { + /** + * other has higher token + * -> stop applying and let other become leader. + */ + stopCriteriaPromiseResolve(); + } + } + + if (msg.action === 'tell') { + // other is already leader + stopCriteriaPromiseResolve(); + _this2.hasLeader = true; + } } - } - }; + }; - this.broadcastChannel.addEventListener('internal', handleMessage); + _this2.broadcastChannel.addEventListener('internal', handleMessage); - var applyPromise = _sendMessage(this, 'apply') // send out that this one is applying - .then(function () { - return sleep(_this2._options.responseTime); - }) // let others time to respond - .then(function () { - if (stopCriteria) { - return Promise.reject(new Error()); - } else { + var applyPromise = _sendMessage(_this2, 'apply') // send out that this one is applying + .then(function () { + return Promise.race([sleep(_this2._options.responseTime / 2), stopCriteriaPromise.then(function () { + return Promise.reject(new Error()); + })]); + }) // send again in case another instance was just created + .then(function () { return _sendMessage(_this2, 'apply'); - } - }).then(function () { - return sleep(_this2._options.responseTime); - }) // let others time to respond - .then(function () { - if (stopCriteria) { - return Promise.reject(new Error()); - } else { - return _sendMessage(_this2); - } + }) // let others time to respond + .then(function () { + return Promise.race([sleep(_this2._options.responseTime / 2), stopCriteriaPromise.then(function () { + return Promise.reject(new Error()); + })]); + })["catch"](function () {}).then(function () { + _this2.broadcastChannel.removeEventListener('internal', handleMessage); + + if (!stopCriteria) { + // no stop criteria -> own is leader + return beLeader(_this2).then(function () { + return true; + }); + } else { + // other is leader + return false; + } + }); + + return applyPromise; + }; + + this._aplQC = this._aplQC + 1; + this._aplQ = this._aplQ.then(function () { + return applyRun(); }).then(function () { - return beLeader(_this2); - }) // no one disagreed -> this one is now leader - .then(function () { - return true; - })["catch"](function () { - return false; - }) // apply not successfull - .then(function (success) { - _this2.broadcastChannel.removeEventListener('internal', handleMessage); - - _this2._isApl = false; - - if (!success && _this2._reApply) { - _this2._reApply = false; - return _this2.applyOnce(); - } else { - return success; - } + _this2._aplQC = _this2._aplQC - 1; + }); + return this._aplQ.then(function () { + return _this2.isLeader; }); - - this._isApl = applyPromise; - return applyPromise; }, awaitLeadership: function awaitLeadership() { if ( @@ -216,6 +251,7 @@ function _awaitLeadershipOnce(leaderElector) { }); // try on fallbackInterval var interval = setInterval(function () { + console.log('applyOnce via fallbackInterval'); leaderElector.applyOnce().then(function () { if (leaderElector.isLeader) { finish(); diff --git a/dist/esbrowser/methods/node.js b/dist/esbrowser/methods/node.js index 5bb0e50f..880bf616 100644 --- a/dist/esbrowser/methods/node.js +++ b/dist/esbrowser/methods/node.js @@ -975,7 +975,10 @@ export function postMessage(channelState, messageJson) { */ export function emitOverFastPath(state, msgObj, messageJson) { - if (!state.options.node.useFastPath) return; // disabled + if (!state.options.node.useFastPath) { + // disabled + return; + } var others = OTHER_INSTANCES[state.channelName].filter(function (s) { return s !== state; diff --git a/dist/esbrowser/util.js b/dist/esbrowser/util.js index c6febc5a..5d81fdcd 100644 --- a/dist/esbrowser/util.js +++ b/dist/esbrowser/util.js @@ -9,6 +9,7 @@ export function isPromise(obj) { } } export var PROMISE_RESOLVED_FALSE = Promise.resolve(false); +export var PROMISE_RESOLVED_TRUE = Promise.resolve(true); export var PROMISE_RESOLVED_VOID = Promise.resolve(); export function sleep(time, resolveWith) { if (!time) time = 0; diff --git a/dist/esnode/broadcast-channel.js b/dist/esnode/broadcast-channel.js index bdccd05e..02e5f09b 100644 --- a/dist/esnode/broadcast-channel.js +++ b/dist/esnode/broadcast-channel.js @@ -1,7 +1,17 @@ import { isPromise, PROMISE_RESOLVED_FALSE, PROMISE_RESOLVED_VOID } from './util.js'; import { chooseMethod } from './method-chooser.js'; import { fillOptionsWithDefaults } from './options.js'; +/** + * Contains all open channels, + * used in tests to ensure everything is closed. + */ + +export var OPEN_BROADCAST_CHANNELS = new Set(); +var lastId = 0; export var BroadcastChannel = function BroadcastChannel(name, options) { + // identifier of the channel to debug stuff + this.id = lastId++; + OPEN_BROADCAST_CHANNELS.add(this); this.name = name; if (ENFORCED_OPTIONS) { @@ -137,6 +147,7 @@ BroadcastChannel.prototype = { return; } + OPEN_BROADCAST_CHANNELS["delete"](this); this.closed = true; var awaitPrepare = this._prepP ? this._prepP : PROMISE_RESOLVED_VOID; this._onML = null; diff --git a/dist/esnode/index.js b/dist/esnode/index.js index 0338ab48..cc1337b1 100644 --- a/dist/esnode/index.js +++ b/dist/esnode/index.js @@ -1,2 +1,2 @@ -export { BroadcastChannel, clearNodeFolder, enforceOptions } from './broadcast-channel'; +export { BroadcastChannel, clearNodeFolder, enforceOptions, OPEN_BROADCAST_CHANNELS } from './broadcast-channel'; export { createLeaderElection, beLeader } from './leader-election'; \ No newline at end of file diff --git a/dist/esnode/leader-election.js b/dist/esnode/leader-election.js index 642409b4..ee16e48d 100644 --- a/dist/esnode/leader-election.js +++ b/dist/esnode/leader-election.js @@ -1,4 +1,4 @@ -import { sleep, randomToken, PROMISE_RESOLVED_FALSE, PROMISE_RESOLVED_VOID } from './util.js'; +import { sleep, randomToken, PROMISE_RESOLVED_VOID, PROMISE_RESOLVED_TRUE } from './util.js'; import { add as unloadAdd } from 'unload'; var LeaderElection = function LeaderElection(broadcastChannel, options) { @@ -11,15 +11,14 @@ var LeaderElection = function LeaderElection(broadcastChannel, options) { this.isDead = false; this.token = randomToken(); /** - * _isApplying - * Only set when a leader application is - * running at the moment. - * @type {Promise | null} + * Apply Queue, + * used to ensure we do not run applyOnce() + * in parallel. */ - this._isApl = false; // _isApplying + this._aplQ = PROMISE_RESOLVED_VOID; // amount of unfinished applyOnce() calls - this._reApply = false; // things to clean up + this._aplQC = 0; // things to clean up this._unl = []; // _unloads @@ -56,6 +55,11 @@ var LeaderElection = function LeaderElection(broadcastChannel, options) { }; LeaderElection.prototype = { + /** + * Returns true if the instance is leader, + * false if not. + * @async + */ applyOnce: function applyOnce() { var _this2 = this; @@ -64,82 +68,113 @@ LeaderElection.prototype = { } if (this.isDead) { - return PROMISE_RESOLVED_FALSE; - } // do nothing if already running + return sleep(0, false); + } + /** + * Already applying more then once, + * -> wait for the apply queue to be finished. + */ - if (this._isApl) { - this._reApply = true; - return sleep(0, false); + if (this._aplQC > 1) { + return this._aplQ; } + /** + * Add a new apply-run + */ - var stopCriteria = false; - var recieved = []; - var handleMessage = function handleMessage(msg) { - if (msg.context === 'leader' && msg.token != _this2.token) { - recieved.push(msg); + var applyRun = function applyRun() { + /** + * Optimization shortcuts. + * Directly return if a previous run + * has already elected a leader. + */ + if (_this2.isLeader) { + return PROMISE_RESOLVED_TRUE; + } - if (msg.action === 'apply') { - // other is applying - if (msg.token > _this2.token) { - // other has higher token, stop applying - stopCriteria = true; - } - } + var stopCriteria = false; + var stopCriteriaPromiseResolve; + /** + * Resolves when a stop criteria is reached. + * Uses as a performance shortcut so we do not + * have to await the responseTime when it is already clear + * that the election failed. + */ - if (msg.action === 'tell') { - // other is already leader + var stopCriteriaPromise = new Promise(function (res) { + stopCriteriaPromiseResolve = function stopCriteriaPromiseResolve() { stopCriteria = true; - _this2.hasLeader = true; + res(); + }; + }); + var recieved = []; + + var handleMessage = function handleMessage(msg) { + if (msg.context === 'leader' && msg.token != _this2.token) { + recieved.push(msg); + + if (msg.action === 'apply') { + // other is applying + if (msg.token > _this2.token) { + /** + * other has higher token + * -> stop applying and let other become leader. + */ + stopCriteriaPromiseResolve(); + } + } + + if (msg.action === 'tell') { + // other is already leader + stopCriteriaPromiseResolve(); + _this2.hasLeader = true; + } } - } - }; + }; - this.broadcastChannel.addEventListener('internal', handleMessage); + _this2.broadcastChannel.addEventListener('internal', handleMessage); - var applyPromise = _sendMessage(this, 'apply') // send out that this one is applying - .then(function () { - return sleep(_this2._options.responseTime); - }) // let others time to respond - .then(function () { - if (stopCriteria) { - return Promise.reject(new Error()); - } else { + var applyPromise = _sendMessage(_this2, 'apply') // send out that this one is applying + .then(function () { + return Promise.race([sleep(_this2._options.responseTime / 2), stopCriteriaPromise.then(function () { + return Promise.reject(new Error()); + })]); + }) // send again in case another instance was just created + .then(function () { return _sendMessage(_this2, 'apply'); - } - }).then(function () { - return sleep(_this2._options.responseTime); - }) // let others time to respond - .then(function () { - if (stopCriteria) { - return Promise.reject(new Error()); - } else { - return _sendMessage(_this2); - } + }) // let others time to respond + .then(function () { + return Promise.race([sleep(_this2._options.responseTime / 2), stopCriteriaPromise.then(function () { + return Promise.reject(new Error()); + })]); + })["catch"](function () {}).then(function () { + _this2.broadcastChannel.removeEventListener('internal', handleMessage); + + if (!stopCriteria) { + // no stop criteria -> own is leader + return beLeader(_this2).then(function () { + return true; + }); + } else { + // other is leader + return false; + } + }); + + return applyPromise; + }; + + this._aplQC = this._aplQC + 1; + this._aplQ = this._aplQ.then(function () { + return applyRun(); }).then(function () { - return beLeader(_this2); - }) // no one disagreed -> this one is now leader - .then(function () { - return true; - })["catch"](function () { - return false; - }) // apply not successfull - .then(function (success) { - _this2.broadcastChannel.removeEventListener('internal', handleMessage); - - _this2._isApl = false; - - if (!success && _this2._reApply) { - _this2._reApply = false; - return _this2.applyOnce(); - } else { - return success; - } + _this2._aplQC = _this2._aplQC - 1; + }); + return this._aplQ.then(function () { + return _this2.isLeader; }); - - this._isApl = applyPromise; - return applyPromise; }, awaitLeadership: function awaitLeadership() { if ( @@ -216,6 +251,7 @@ function _awaitLeadershipOnce(leaderElector) { }); // try on fallbackInterval var interval = setInterval(function () { + console.log('applyOnce via fallbackInterval'); leaderElector.applyOnce().then(function () { if (leaderElector.isLeader) { finish(); diff --git a/dist/esnode/methods/node.js b/dist/esnode/methods/node.js index 5bb0e50f..880bf616 100644 --- a/dist/esnode/methods/node.js +++ b/dist/esnode/methods/node.js @@ -975,7 +975,10 @@ export function postMessage(channelState, messageJson) { */ export function emitOverFastPath(state, msgObj, messageJson) { - if (!state.options.node.useFastPath) return; // disabled + if (!state.options.node.useFastPath) { + // disabled + return; + } var others = OTHER_INSTANCES[state.channelName].filter(function (s) { return s !== state; diff --git a/dist/esnode/util.js b/dist/esnode/util.js index c6febc5a..5d81fdcd 100644 --- a/dist/esnode/util.js +++ b/dist/esnode/util.js @@ -9,6 +9,7 @@ export function isPromise(obj) { } } export var PROMISE_RESOLVED_FALSE = Promise.resolve(false); +export var PROMISE_RESOLVED_TRUE = Promise.resolve(true); export var PROMISE_RESOLVED_VOID = Promise.resolve(); export function sleep(time, resolveWith) { if (!time) time = 0; diff --git a/dist/lib/broadcast-channel.js b/dist/lib/broadcast-channel.js index 5e2822a4..7b0114f1 100644 --- a/dist/lib/broadcast-channel.js +++ b/dist/lib/broadcast-channel.js @@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); -exports.BroadcastChannel = void 0; +exports.OPEN_BROADCAST_CHANNELS = exports.BroadcastChannel = void 0; exports.clearNodeFolder = clearNodeFolder; exports.enforceOptions = enforceOptions; @@ -13,7 +13,18 @@ var _methodChooser = require("./method-chooser.js"); var _options = require("./options.js"); +/** + * Contains all open channels, + * used in tests to ensure everything is closed. + */ +var OPEN_BROADCAST_CHANNELS = new Set(); +exports.OPEN_BROADCAST_CHANNELS = OPEN_BROADCAST_CHANNELS; +var lastId = 0; + var BroadcastChannel = function BroadcastChannel(name, options) { + // identifier of the channel to debug stuff + this.id = lastId++; + OPEN_BROADCAST_CHANNELS.add(this); this.name = name; if (ENFORCED_OPTIONS) { @@ -154,6 +165,7 @@ BroadcastChannel.prototype = { return; } + OPEN_BROADCAST_CHANNELS["delete"](this); this.closed = true; var awaitPrepare = this._prepP ? this._prepP : _util.PROMISE_RESOLVED_VOID; this._onML = null; diff --git a/dist/lib/browser.js b/dist/lib/browser.js index 0e415fb6..66d38400 100644 --- a/dist/lib/browser.js +++ b/dist/lib/browser.js @@ -4,7 +4,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); -exports.BroadcastChannel = void 0; +exports.OPEN_BROADCAST_CHANNELS = exports.BroadcastChannel = void 0; exports.clearNodeFolder = clearNodeFolder; exports.enforceOptions = enforceOptions; @@ -14,7 +14,18 @@ var _methodChooser = require("./method-chooser.js"); var _options = require("./options.js"); +/** + * Contains all open channels, + * used in tests to ensure everything is closed. + */ +var OPEN_BROADCAST_CHANNELS = new Set(); +exports.OPEN_BROADCAST_CHANNELS = OPEN_BROADCAST_CHANNELS; +var lastId = 0; + var BroadcastChannel = function BroadcastChannel(name, options) { + // identifier of the channel to debug stuff + this.id = lastId++; + OPEN_BROADCAST_CHANNELS.add(this); this.name = name; if (ENFORCED_OPTIONS) { @@ -155,6 +166,7 @@ BroadcastChannel.prototype = { return; } + OPEN_BROADCAST_CHANNELS["delete"](this); this.closed = true; var awaitPrepare = this._prepP ? this._prepP : _util.PROMISE_RESOLVED_VOID; this._onML = null; @@ -319,6 +331,12 @@ Object.defineProperty(exports, "BroadcastChannel", { return _broadcastChannel.BroadcastChannel; } }); +Object.defineProperty(exports, "OPEN_BROADCAST_CHANNELS", { + enumerable: true, + get: function get() { + return _broadcastChannel.OPEN_BROADCAST_CHANNELS; + } +}); Object.defineProperty(exports, "beLeader", { enumerable: true, get: function get() { @@ -370,15 +388,14 @@ var LeaderElection = function LeaderElection(broadcastChannel, options) { this.isDead = false; this.token = (0, _util.randomToken)(); /** - * _isApplying - * Only set when a leader application is - * running at the moment. - * @type {Promise | null} + * Apply Queue, + * used to ensure we do not run applyOnce() + * in parallel. */ - this._isApl = false; // _isApplying + this._aplQ = _util.PROMISE_RESOLVED_VOID; // amount of unfinished applyOnce() calls - this._reApply = false; // things to clean up + this._aplQC = 0; // things to clean up this._unl = []; // _unloads @@ -415,6 +432,11 @@ var LeaderElection = function LeaderElection(broadcastChannel, options) { }; LeaderElection.prototype = { + /** + * Returns true if the instance is leader, + * false if not. + * @async + */ applyOnce: function applyOnce() { var _this2 = this; @@ -423,82 +445,113 @@ LeaderElection.prototype = { } if (this.isDead) { - return _util.PROMISE_RESOLVED_FALSE; - } // do nothing if already running + return (0, _util.sleep)(0, false); + } + /** + * Already applying more then once, + * -> wait for the apply queue to be finished. + */ - if (this._isApl) { - this._reApply = true; - return (0, _util.sleep)(0, false); + if (this._aplQC > 1) { + return this._aplQ; } + /** + * Add a new apply-run + */ - var stopCriteria = false; - var recieved = []; - var handleMessage = function handleMessage(msg) { - if (msg.context === 'leader' && msg.token != _this2.token) { - recieved.push(msg); + var applyRun = function applyRun() { + /** + * Optimization shortcuts. + * Directly return if a previous run + * has already elected a leader. + */ + if (_this2.isLeader) { + return _util.PROMISE_RESOLVED_TRUE; + } - if (msg.action === 'apply') { - // other is applying - if (msg.token > _this2.token) { - // other has higher token, stop applying - stopCriteria = true; - } - } + var stopCriteria = false; + var stopCriteriaPromiseResolve; + /** + * Resolves when a stop criteria is reached. + * Uses as a performance shortcut so we do not + * have to await the responseTime when it is already clear + * that the election failed. + */ - if (msg.action === 'tell') { - // other is already leader + var stopCriteriaPromise = new Promise(function (res) { + stopCriteriaPromiseResolve = function stopCriteriaPromiseResolve() { stopCriteria = true; - _this2.hasLeader = true; + res(); + }; + }); + var recieved = []; + + var handleMessage = function handleMessage(msg) { + if (msg.context === 'leader' && msg.token != _this2.token) { + recieved.push(msg); + + if (msg.action === 'apply') { + // other is applying + if (msg.token > _this2.token) { + /** + * other has higher token + * -> stop applying and let other become leader. + */ + stopCriteriaPromiseResolve(); + } + } + + if (msg.action === 'tell') { + // other is already leader + stopCriteriaPromiseResolve(); + _this2.hasLeader = true; + } } - } - }; + }; - this.broadcastChannel.addEventListener('internal', handleMessage); + _this2.broadcastChannel.addEventListener('internal', handleMessage); - var applyPromise = _sendMessage(this, 'apply') // send out that this one is applying - .then(function () { - return (0, _util.sleep)(_this2._options.responseTime); - }) // let others time to respond - .then(function () { - if (stopCriteria) { - return Promise.reject(new Error()); - } else { + var applyPromise = _sendMessage(_this2, 'apply') // send out that this one is applying + .then(function () { + return Promise.race([(0, _util.sleep)(_this2._options.responseTime / 2), stopCriteriaPromise.then(function () { + return Promise.reject(new Error()); + })]); + }) // send again in case another instance was just created + .then(function () { return _sendMessage(_this2, 'apply'); - } - }).then(function () { - return (0, _util.sleep)(_this2._options.responseTime); - }) // let others time to respond - .then(function () { - if (stopCriteria) { - return Promise.reject(new Error()); - } else { - return _sendMessage(_this2); - } - }).then(function () { - return beLeader(_this2); - }) // no one disagreed -> this one is now leader - .then(function () { - return true; - })["catch"](function () { - return false; - }) // apply not successfull - .then(function (success) { - _this2.broadcastChannel.removeEventListener('internal', handleMessage); + }) // let others time to respond + .then(function () { + return Promise.race([(0, _util.sleep)(_this2._options.responseTime / 2), stopCriteriaPromise.then(function () { + return Promise.reject(new Error()); + })]); + })["catch"](function () {}).then(function () { + _this2.broadcastChannel.removeEventListener('internal', handleMessage); + + if (!stopCriteria) { + // no stop criteria -> own is leader + return beLeader(_this2).then(function () { + return true; + }); + } else { + // other is leader + return false; + } + }); - _this2._isApl = false; + return applyPromise; + }; - if (!success && _this2._reApply) { - _this2._reApply = false; - return _this2.applyOnce(); - } else { - return success; - } + this._aplQC = this._aplQC + 1; + this._aplQ = this._aplQ.then(function () { + return applyRun(); + }).then(function () { + _this2._aplQC = _this2._aplQC - 1; + }); + return this._aplQ.then(function () { + return _this2.isLeader; }); - - this._isApl = applyPromise; - return applyPromise; }, awaitLeadership: function awaitLeadership() { if ( @@ -575,6 +628,7 @@ function _awaitLeadershipOnce(leaderElector) { }); // try on fallbackInterval var interval = setInterval(function () { + console.log('applyOnce via fallbackInterval'); leaderElector.applyOnce().then(function () { if (leaderElector.isLeader) { finish(); @@ -1510,7 +1564,7 @@ function fillOptionsWithDefaults() { Object.defineProperty(exports, "__esModule", { value: true }); -exports.isNode = exports.PROMISE_RESOLVED_VOID = exports.PROMISE_RESOLVED_FALSE = void 0; +exports.isNode = exports.PROMISE_RESOLVED_VOID = exports.PROMISE_RESOLVED_TRUE = exports.PROMISE_RESOLVED_FALSE = void 0; exports.isPromise = isPromise; exports.microSeconds = microSeconds; exports.randomInt = randomInt; @@ -1530,6 +1584,8 @@ function isPromise(obj) { var PROMISE_RESOLVED_FALSE = Promise.resolve(false); exports.PROMISE_RESOLVED_FALSE = PROMISE_RESOLVED_FALSE; +var PROMISE_RESOLVED_TRUE = Promise.resolve(true); +exports.PROMISE_RESOLVED_TRUE = PROMISE_RESOLVED_TRUE; var PROMISE_RESOLVED_VOID = Promise.resolve(); exports.PROMISE_RESOLVED_VOID = PROMISE_RESOLVED_VOID; diff --git a/dist/lib/browser.min.js b/dist/lib/browser.min.js index 0e1ab23f..13b802fa 100644 --- a/dist/lib/browser.min.js +++ b/dist/lib/browser.min.js @@ -1 +1 @@ -!function r(o,i,s){function a(t,e){if(!i[t]){if(!o[t]){var n="function"==typeof require&&require;if(!e&&n)return n(t,!0);if(u)return u(t,!0);throw(n=new Error("Cannot find module '"+t+"'")).code="MODULE_NOT_FOUND",n}n=i[t]={exports:{}},o[t][0].call(n.exports,function(e){return a(o[t][1][e]||e)},n,n.exports,r,o,i,s)}return i[t].exports}for(var u="function"==typeof require&&require,e=0;e=e.time&&e.fn(t.data)})},n=e.method.microSeconds(),e._prepP?e._prepP.then(function(){e._iL=!0,e.method.onMessage(e._state,t,n)}):(e._iL=!0,e.method.onMessage(e._state,t,n)))}}(e)}function l(e,t,n){e._addEL[t]=e._addEL[t].filter(function(e){return e!==n}),function(e){{var t;e._iL&&!u(e)&&(e._iL=!1,t=e.method.microSeconds(),e.method.onMessage(e._state,null,t))}}(e)}(n.BroadcastChannel=e)._pubkey=!0,e.prototype={postMessage:function(e){if(this.closed)throw new Error("BroadcastChannel.postMessage(): Cannot post message after channel has closed");return a(this,"message",e)},postInternal:function(e){return a(this,"internal",e)},set onmessage(e){var t={time:this.method.microSeconds(),fn:e};l(this,"message",this._onML),e&&"function"==typeof e?(this._onML=t,c(this,"message",t)):this._onML=null},addEventListener:function(e,t){var n=this.method.microSeconds();c(this,e,{time:n,fn:t})},removeEventListener:function(e,t){var n=this._addEL[e].find(function(e){return e.fn===t});l(this,e,n)},close:function(){var e=this;if(!this.closed){this.closed=!0;var t=this._prepP||o.PROMISE_RESOLVED_VOID;return this._onML=null,this._addEL.message=[],t.then(function(){return Promise.all(Array.from(e._uMP))}).then(function(){return Promise.all(e._befC.map(function(e){return e()}))}).then(function(){return e.method.close(e._state)})}},get type(){return this.method.type},get isClosed(){return this.closed}}},{"./method-chooser.js":6,"./options.js":11,"./util.js":12}],2:[function(e,t,n){"use strict";var r=e("./index.es5.js"),e=r.BroadcastChannel,r=r.createLeaderElection;window.BroadcastChannel2=e,window.createLeaderElection=r},{"./index.es5.js":3}],3:[function(e,t,n){"use strict";e=e("./index.js");t.exports={BroadcastChannel:e.BroadcastChannel,createLeaderElection:e.createLeaderElection,clearNodeFolder:e.clearNodeFolder,enforceOptions:e.enforceOptions,beLeader:e.beLeader}},{"./index.js":4}],4:[function(e,t,n){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),Object.defineProperty(n,"BroadcastChannel",{enumerable:!0,get:function(){return r.BroadcastChannel}}),Object.defineProperty(n,"beLeader",{enumerable:!0,get:function(){return o.beLeader}}),Object.defineProperty(n,"clearNodeFolder",{enumerable:!0,get:function(){return r.clearNodeFolder}}),Object.defineProperty(n,"createLeaderElection",{enumerable:!0,get:function(){return o.createLeaderElection}}),Object.defineProperty(n,"enforceOptions",{enumerable:!0,get:function(){return r.enforceOptions}});var r=e("./broadcast-channel"),o=e("./leader-election")},{"./broadcast-channel":1,"./leader-election":5}],5:[function(e,t,n){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.beLeader=a,n.createLeaderElection=function(e,t){if(e._leaderElector)throw new Error("BroadcastChannel already has a leader-elector");t=function(e,t){e=e||{};(e=JSON.parse(JSON.stringify(e))).fallbackInterval||(e.fallbackInterval=3e3);e.responseTime||(e.responseTime=t.method.averageResponseTime(t.options));return e}(t,e);var n=new o(e,t);return e._befC.push(function(){return n.die()}),e._leaderElector=n};var s=e("./util.js"),r=e("unload"),o=function(e,t){var n=this;this.broadcastChannel=e,this._options=t,this.isLeader=!1,this.hasLeader=!1,this.isDead=!1,this.token=(0,s.randomToken)(),this._isApl=!1,this._reApply=!1,this._unl=[],this._lstns=[],this._invs=[],this._dpL=function(){},this._dpLC=!1;t=function(e){"leader"===e.context&&("death"===e.action&&(n.hasLeader=!1),"tell"===e.action&&(n.hasLeader=!0))};this.broadcastChannel.addEventListener("internal",t),this._lstns.push(t)};function i(e,t){t={context:"leader",action:t,token:e.token};return e.broadcastChannel.postInternal(t)}function a(t){t.isLeader=!0,t.hasLeader=!0;var e=(0,r.add)(function(){return t.die()});t._unl.push(e);e=function(e){"leader"===e.context&&"apply"===e.action&&i(t,"tell"),"leader"!==e.context||"tell"!==e.action||t._dpLC||(t._dpLC=!0,t._dpL(),i(t,"tell"))};return t.broadcastChannel.addEventListener("internal",e),t._lstns.push(e),i(t,"tell")}o.prototype={applyOnce:function(){var t=this;if(this.isLeader)return(0,s.sleep)(0,!0);if(this.isDead)return s.PROMISE_RESOLVED_FALSE;if(this._isApl)return this._reApply=!0,(0,s.sleep)(0,!1);function n(e){"leader"===e.context&&e.token!=t.token&&(o.push(e),"apply"===e.action&&e.token>t.token&&(r=!0),"tell"===e.action&&(r=!0,t.hasLeader=!0))}var r=!1,o=[];this.broadcastChannel.addEventListener("internal",n);var e=i(this,"apply").then(function(){return(0,s.sleep)(t._options.responseTime)}).then(function(){return r?Promise.reject(new Error):i(t,"apply")}).then(function(){return(0,s.sleep)(t._options.responseTime)}).then(function(){return r?Promise.reject(new Error):i(t)}).then(function(){return a(t)}).then(function(){return!0}).catch(function(){return!1}).then(function(e){return t.broadcastChannel.removeEventListener("internal",n),t._isApl=!1,!e&&t._reApply?(t._reApply=!1,t.applyOnce()):e});return this._isApl=e},awaitLeadership:function(){return this._aLP||(this._aLP=function(i){if(i.isLeader)return s.PROMISE_RESOLVED_VOID;return new Promise(function(e){var t=!1;function n(){t||(t=!0,clearInterval(r),i.broadcastChannel.removeEventListener("internal",o),e(!0))}i.applyOnce().then(function(){i.isLeader&&n()});var r=setInterval(function(){i.applyOnce().then(function(){i.isLeader&&n()})},i._options.fallbackInterval);i._invs.push(r);var o=function(e){"leader"===e.context&&"death"===e.action&&(i.hasLeader=!1,i.applyOnce().then(function(){i.isLeader&&n()}))};i.broadcastChannel.addEventListener("internal",o),i._lstns.push(o)})}(this)),this._aLP},set onduplicate(e){this._dpL=e},die:function(){var t=this;return this._lstns.forEach(function(e){return t.broadcastChannel.removeEventListener("internal",e)}),this._lstns=[],this._invs.forEach(function(e){return clearInterval(e)}),this._invs=[],this._unl.forEach(function(e){return e.remove()}),this._unl=[],this.isLeader&&(this.hasLeader=!1,this.isLeader=!1),this.isDead=!0,i(this,"death")}}},{"./util.js":12,unload:20}],6:[function(e,t,n){"use strict";var r=e("@babel/runtime/helpers/interopRequireDefault");e("@babel/runtime/helpers/typeof");Object.defineProperty(n,"__esModule",{value:!0}),n.chooseMethod=function(t){var e=[].concat(t.methods,u).filter(Boolean);if(t.type){if("simulate"===t.type)return s.default;var n=e.find(function(e){return e.type===t.type});if(n)return n;throw new Error("method-type "+t.type+" not found")}t.webWorkerSupport||a.isNode||(e=e.filter(function(e){return"idb"!==e.type}));e=e.find(function(e){return e.canBeUsed()});{if(e)return e;throw new Error("No useable method found in "+JSON.stringify(u.map(function(e){return e.type})))}};var o=r(e("./methods/native.js")),i=r(e("./methods/indexed-db.js")),n=r(e("./methods/localstorage.js")),s=r(e("./methods/simulate.js")),a=e("./util");var u=[o.default,i.default,n.default]},{"./methods/indexed-db.js":7,"./methods/localstorage.js":8,"./methods/native.js":9,"./methods/simulate.js":10,"./util":12,"@babel/runtime/helpers/interopRequireDefault":13,"@babel/runtime/helpers/typeof":14}],7:[function(e,t,n){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.averageResponseTime=w,n.canBeUsed=y,n.cleanOldMessages=p,n.close=b,n.create=m,n.createDatabase=c,n.default=void 0,n.getAllMessages=function(e){var n=e.transaction(a).objectStore(a),r=[];return new Promise(function(t){n.openCursor().onsuccess=function(e){e=e.target.result;e?(r.push(e.value),e.continue()):t(r)}})},n.getIdb=u,n.getMessagesHigherThan=d,n.getOldMessages=h,n.microSeconds=void 0,n.onMessage=g,n.postMessage=_,n.removeMessageById=f,n.type=void 0,n.writeMessage=l;var o=e("../util.js"),i=e("oblivious-set"),s=e("../options"),e=o.microSeconds;n.microSeconds=e;var r="pubkey.broadcast-channel-0-",a="messages";function u(){if("undefined"!=typeof indexedDB)return indexedDB;if("undefined"!=typeof window){if(void 0!==window.mozIndexedDB)return window.mozIndexedDB;if(void 0!==window.webkitIndexedDB)return window.webkitIndexedDB;if(void 0!==window.msIndexedDB)return window.msIndexedDB}return!1}function c(e){var n=u().open(r+e,1);return n.onupgradeneeded=function(e){e.target.result.createObjectStore(a,{keyPath:"id",autoIncrement:!0})},new Promise(function(e,t){n.onerror=function(e){return t(e)},n.onsuccess=function(){e(n.result)}})}function l(e,t,n){var r={uuid:t,time:(new Date).getTime(),data:n},o=e.transaction([a],"readwrite");return new Promise(function(e,t){o.oncomplete=function(){return e()},o.onerror=function(e){return t(e)},o.objectStore(a).add(r)})}function d(e,n){var r=e.transaction(a).objectStore(a),o=[];return new Promise(function(t){(function(){try{var e=IDBKeyRange.bound(n+1,1/0);return r.openCursor(e)}catch(e){return r.openCursor()}})().onsuccess=function(e){e=e.target.result;e?e.value.idn.lastCursorId&&(n.lastCursorId=e.id),e}).filter(function(e){return t=n,(e=e).uuid!==t.uuid&&(!t.eMIs.has(e.id)&&!(e.data.time=e.time&&e.fn(t.data)})},n=e.method.microSeconds(),e._prepP?e._prepP.then(function(){e._iL=!0,e.method.onMessage(e._state,t,n)}):(e._iL=!0,e.method.onMessage(e._state,t,n)))}}(e)}function h(e,t,n){e._addEL[t]=e._addEL[t].filter(function(e){return e!==n}),function(e){{var t;e._iL&&!d(e)&&(e._iL=!1,t=e.method.microSeconds(),e.method.onMessage(e._state,null,t))}}(e)}(n.BroadcastChannel=a)._pubkey=!0,a.prototype={postMessage:function(e){if(this.closed)throw new Error("BroadcastChannel.postMessage(): Cannot post message after channel has closed");return l(this,"message",e)},postInternal:function(e){return l(this,"internal",e)},set onmessage(e){var t={time:this.method.microSeconds(),fn:e};h(this,"message",this._onML),e&&"function"==typeof e?(this._onML=t,f(this,"message",t)):this._onML=null},addEventListener:function(e,t){var n=this.method.microSeconds();f(this,e,{time:n,fn:t})},removeEventListener:function(e,t){var n=this._addEL[e].find(function(e){return e.fn===t});h(this,e,n)},close:function(){var e=this;if(!this.closed){s.delete(this),this.closed=!0;var t=this._prepP||o.PROMISE_RESOLVED_VOID;return this._onML=null,this._addEL.message=[],t.then(function(){return Promise.all(Array.from(e._uMP))}).then(function(){return Promise.all(e._befC.map(function(e){return e()}))}).then(function(){return e.method.close(e._state)})}},get type(){return this.method.type},get isClosed(){return this.closed}}},{"./method-chooser.js":6,"./options.js":11,"./util.js":12}],2:[function(e,t,n){"use strict";var r=e("./index.es5.js"),e=r.BroadcastChannel,r=r.createLeaderElection;window.BroadcastChannel2=e,window.createLeaderElection=r},{"./index.es5.js":3}],3:[function(e,t,n){"use strict";e=e("./index.js");t.exports={BroadcastChannel:e.BroadcastChannel,createLeaderElection:e.createLeaderElection,clearNodeFolder:e.clearNodeFolder,enforceOptions:e.enforceOptions,beLeader:e.beLeader}},{"./index.js":4}],4:[function(e,t,n){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),Object.defineProperty(n,"BroadcastChannel",{enumerable:!0,get:function(){return r.BroadcastChannel}}),Object.defineProperty(n,"OPEN_BROADCAST_CHANNELS",{enumerable:!0,get:function(){return r.OPEN_BROADCAST_CHANNELS}}),Object.defineProperty(n,"beLeader",{enumerable:!0,get:function(){return o.beLeader}}),Object.defineProperty(n,"clearNodeFolder",{enumerable:!0,get:function(){return r.clearNodeFolder}}),Object.defineProperty(n,"createLeaderElection",{enumerable:!0,get:function(){return o.createLeaderElection}}),Object.defineProperty(n,"enforceOptions",{enumerable:!0,get:function(){return r.enforceOptions}});var r=e("./broadcast-channel"),o=e("./leader-election")},{"./broadcast-channel":1,"./leader-election":5}],5:[function(e,t,n){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.beLeader=u,n.createLeaderElection=function(e,t){if(e._leaderElector)throw new Error("BroadcastChannel already has a leader-elector");t=function(e,t){e=e||{};(e=JSON.parse(JSON.stringify(e))).fallbackInterval||(e.fallbackInterval=3e3);e.responseTime||(e.responseTime=t.method.averageResponseTime(t.options));return e}(t,e);var n=new o(e,t);return e._befC.push(function(){return n.die()}),e._leaderElector=n};var s=e("./util.js"),r=e("unload"),o=function(e,t){var n=this;this.broadcastChannel=e,this._options=t,this.isLeader=!1,this.hasLeader=!1,this.isDead=!1,this.token=(0,s.randomToken)(),this._aplQ=s.PROMISE_RESOLVED_VOID,this._aplQC=0,this._unl=[],this._lstns=[],this._invs=[],this._dpL=function(){},this._dpLC=!1;function r(e){"leader"===e.context&&("death"===e.action&&(n.hasLeader=!1),"tell"===e.action&&(n.hasLeader=!0))}this.broadcastChannel.addEventListener("internal",r),this._lstns.push(r)};function a(e,t){t={context:"leader",action:t,token:e.token};return e.broadcastChannel.postInternal(t)}function u(t){t.isLeader=!0,t.hasLeader=!0;var e=(0,r.add)(function(){return t.die()});t._unl.push(e);function n(e){"leader"===e.context&&"apply"===e.action&&a(t,"tell"),"leader"!==e.context||"tell"!==e.action||t._dpLC||(t._dpLC=!0,t._dpL(),a(t,"tell"))}return t.broadcastChannel.addEventListener("internal",n),t._lstns.push(n),a(t,"tell")}o.prototype={applyOnce:function(){var i=this;if(this.isLeader)return(0,s.sleep)(0,!0);if(this.isDead)return(0,s.sleep)(0,!1);if(1i.token&&t(),"tell"===e.action&&(t(),i.hasLeader=!0))}var t,n=!1,r=new Promise(function(e){t=function(){n=!0,e()}}),o=[];return i.broadcastChannel.addEventListener("internal",e),a(i,"apply").then(function(){return Promise.race([(0,s.sleep)(i._options.responseTime/2),r.then(function(){return Promise.reject(new Error)})])}).then(function(){return a(i,"apply")}).then(function(){return Promise.race([(0,s.sleep)(i._options.responseTime/2),r.then(function(){return Promise.reject(new Error)})])}).catch(function(){}).then(function(){return i.broadcastChannel.removeEventListener("internal",e),!n&&u(i).then(function(){return!0})})}return this._aplQC=this._aplQC+1,this._aplQ=this._aplQ.then(e).then(function(){i._aplQC=i._aplQC-1}),this._aplQ.then(function(){return i.isLeader})},awaitLeadership:function(){return this._aLP||(this._aLP=function(i){if(i.isLeader)return s.PROMISE_RESOLVED_VOID;return new Promise(function(e){var t=!1;function n(){t||(t=!0,clearInterval(r),i.broadcastChannel.removeEventListener("internal",o),e(!0))}i.applyOnce().then(function(){i.isLeader&&n()});var r=setInterval(function(){console.log("applyOnce via fallbackInterval"),i.applyOnce().then(function(){i.isLeader&&n()})},i._options.fallbackInterval);i._invs.push(r);var o=function(e){"leader"===e.context&&"death"===e.action&&(i.hasLeader=!1,i.applyOnce().then(function(){i.isLeader&&n()}))};i.broadcastChannel.addEventListener("internal",o),i._lstns.push(o)})}(this)),this._aLP},set onduplicate(e){this._dpL=e},die:function(){var t=this;return this._lstns.forEach(function(e){return t.broadcastChannel.removeEventListener("internal",e)}),this._lstns=[],this._invs.forEach(function(e){return clearInterval(e)}),this._invs=[],this._unl.forEach(function(e){return e.remove()}),this._unl=[],this.isLeader&&(this.hasLeader=!1,this.isLeader=!1),this.isDead=!0,a(this,"death")}}},{"./util.js":12,unload:20}],6:[function(e,t,n){"use strict";var r=e("@babel/runtime/helpers/interopRequireDefault");e("@babel/runtime/helpers/typeof");Object.defineProperty(n,"__esModule",{value:!0}),n.chooseMethod=function(t){var e=[].concat(t.methods,u).filter(Boolean);if(t.type){if("simulate"===t.type)return s.default;var n=e.find(function(e){return e.type===t.type});if(n)return n;throw new Error("method-type "+t.type+" not found")}t.webWorkerSupport||a.isNode||(e=e.filter(function(e){return"idb"!==e.type}));e=e.find(function(e){return e.canBeUsed()});{if(e)return e;throw new Error("No useable method found in "+JSON.stringify(u.map(function(e){return e.type})))}};var o=r(e("./methods/native.js")),i=r(e("./methods/indexed-db.js")),n=r(e("./methods/localstorage.js")),s=r(e("./methods/simulate.js")),a=e("./util");var u=[o.default,i.default,n.default]},{"./methods/indexed-db.js":7,"./methods/localstorage.js":8,"./methods/native.js":9,"./methods/simulate.js":10,"./util":12,"@babel/runtime/helpers/interopRequireDefault":13,"@babel/runtime/helpers/typeof":14}],7:[function(e,t,n){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.averageResponseTime=w,n.canBeUsed=y,n.cleanOldMessages=p,n.close=b,n.create=m,n.createDatabase=c,n.default=void 0,n.getAllMessages=function(e){var n=e.transaction(a).objectStore(a),r=[];return new Promise(function(t){n.openCursor().onsuccess=function(e){e=e.target.result;e?(r.push(e.value),e.continue()):t(r)}})},n.getIdb=u,n.getMessagesHigherThan=d,n.getOldMessages=h,n.microSeconds=void 0,n.onMessage=g,n.postMessage=_,n.removeMessageById=f,n.type=void 0,n.writeMessage=l;var o=e("../util.js"),i=e("oblivious-set"),s=e("../options"),e=o.microSeconds;n.microSeconds=e;var r="pubkey.broadcast-channel-0-",a="messages";function u(){if("undefined"!=typeof indexedDB)return indexedDB;if("undefined"!=typeof window){if(void 0!==window.mozIndexedDB)return window.mozIndexedDB;if(void 0!==window.webkitIndexedDB)return window.webkitIndexedDB;if(void 0!==window.msIndexedDB)return window.msIndexedDB}return!1}function c(e){var n=u().open(r+e,1);return n.onupgradeneeded=function(e){e.target.result.createObjectStore(a,{keyPath:"id",autoIncrement:!0})},new Promise(function(e,t){n.onerror=function(e){return t(e)},n.onsuccess=function(){e(n.result)}})}function l(e,t,n){var r={uuid:t,time:(new Date).getTime(),data:n},o=e.transaction([a],"readwrite");return new Promise(function(e,t){o.oncomplete=function(){return e()},o.onerror=function(e){return t(e)},o.objectStore(a).add(r)})}function d(e,n){var r=e.transaction(a).objectStore(a),o=[];return new Promise(function(t){(function(){try{var e=IDBKeyRange.bound(n+1,1/0);return r.openCursor(e)}catch(e){return r.openCursor()}})().onsuccess=function(e){e=e.target.result;e?e.value.idn.lastCursorId&&(n.lastCursorId=e.id),e}).filter(function(e){return t=n,(e=e).uuid!==t.uuid&&(!t.eMIs.has(e.id)&&!(e.data.time | null} + * Apply Queue, + * used to ensure we do not run applyOnce() + * in parallel. */ - this._isApl = false; // _isApplying + this._aplQ = _util.PROMISE_RESOLVED_VOID; // amount of unfinished applyOnce() calls - this._reApply = false; // things to clean up + this._aplQC = 0; // things to clean up this._unl = []; // _unloads @@ -65,6 +64,11 @@ var LeaderElection = function LeaderElection(broadcastChannel, options) { }; LeaderElection.prototype = { + /** + * Returns true if the instance is leader, + * false if not. + * @async + */ applyOnce: function applyOnce() { var _this2 = this; @@ -73,82 +77,113 @@ LeaderElection.prototype = { } if (this.isDead) { - return _util.PROMISE_RESOLVED_FALSE; - } // do nothing if already running + return (0, _util.sleep)(0, false); + } + /** + * Already applying more then once, + * -> wait for the apply queue to be finished. + */ - if (this._isApl) { - this._reApply = true; - return (0, _util.sleep)(0, false); + if (this._aplQC > 1) { + return this._aplQ; } + /** + * Add a new apply-run + */ - var stopCriteria = false; - var recieved = []; - var handleMessage = function handleMessage(msg) { - if (msg.context === 'leader' && msg.token != _this2.token) { - recieved.push(msg); + var applyRun = function applyRun() { + /** + * Optimization shortcuts. + * Directly return if a previous run + * has already elected a leader. + */ + if (_this2.isLeader) { + return _util.PROMISE_RESOLVED_TRUE; + } - if (msg.action === 'apply') { - // other is applying - if (msg.token > _this2.token) { - // other has higher token, stop applying - stopCriteria = true; - } - } + var stopCriteria = false; + var stopCriteriaPromiseResolve; + /** + * Resolves when a stop criteria is reached. + * Uses as a performance shortcut so we do not + * have to await the responseTime when it is already clear + * that the election failed. + */ - if (msg.action === 'tell') { - // other is already leader + var stopCriteriaPromise = new Promise(function (res) { + stopCriteriaPromiseResolve = function stopCriteriaPromiseResolve() { stopCriteria = true; - _this2.hasLeader = true; + res(); + }; + }); + var recieved = []; + + var handleMessage = function handleMessage(msg) { + if (msg.context === 'leader' && msg.token != _this2.token) { + recieved.push(msg); + + if (msg.action === 'apply') { + // other is applying + if (msg.token > _this2.token) { + /** + * other has higher token + * -> stop applying and let other become leader. + */ + stopCriteriaPromiseResolve(); + } + } + + if (msg.action === 'tell') { + // other is already leader + stopCriteriaPromiseResolve(); + _this2.hasLeader = true; + } } - } - }; + }; - this.broadcastChannel.addEventListener('internal', handleMessage); + _this2.broadcastChannel.addEventListener('internal', handleMessage); - var applyPromise = _sendMessage(this, 'apply') // send out that this one is applying - .then(function () { - return (0, _util.sleep)(_this2._options.responseTime); - }) // let others time to respond - .then(function () { - if (stopCriteria) { - return Promise.reject(new Error()); - } else { + var applyPromise = _sendMessage(_this2, 'apply') // send out that this one is applying + .then(function () { + return Promise.race([(0, _util.sleep)(_this2._options.responseTime / 2), stopCriteriaPromise.then(function () { + return Promise.reject(new Error()); + })]); + }) // send again in case another instance was just created + .then(function () { return _sendMessage(_this2, 'apply'); - } - }).then(function () { - return (0, _util.sleep)(_this2._options.responseTime); - }) // let others time to respond - .then(function () { - if (stopCriteria) { - return Promise.reject(new Error()); - } else { - return _sendMessage(_this2); - } + }) // let others time to respond + .then(function () { + return Promise.race([(0, _util.sleep)(_this2._options.responseTime / 2), stopCriteriaPromise.then(function () { + return Promise.reject(new Error()); + })]); + })["catch"](function () {}).then(function () { + _this2.broadcastChannel.removeEventListener('internal', handleMessage); + + if (!stopCriteria) { + // no stop criteria -> own is leader + return beLeader(_this2).then(function () { + return true; + }); + } else { + // other is leader + return false; + } + }); + + return applyPromise; + }; + + this._aplQC = this._aplQC + 1; + this._aplQ = this._aplQ.then(function () { + return applyRun(); }).then(function () { - return beLeader(_this2); - }) // no one disagreed -> this one is now leader - .then(function () { - return true; - })["catch"](function () { - return false; - }) // apply not successfull - .then(function (success) { - _this2.broadcastChannel.removeEventListener('internal', handleMessage); - - _this2._isApl = false; - - if (!success && _this2._reApply) { - _this2._reApply = false; - return _this2.applyOnce(); - } else { - return success; - } + _this2._aplQC = _this2._aplQC - 1; + }); + return this._aplQ.then(function () { + return _this2.isLeader; }); - - this._isApl = applyPromise; - return applyPromise; }, awaitLeadership: function awaitLeadership() { if ( @@ -225,6 +260,7 @@ function _awaitLeadershipOnce(leaderElector) { }); // try on fallbackInterval var interval = setInterval(function () { + console.log('applyOnce via fallbackInterval'); leaderElector.applyOnce().then(function () { if (leaderElector.isLeader) { finish(); diff --git a/dist/lib/methods/node.js b/dist/lib/methods/node.js index 3a0f716b..4c142626 100644 --- a/dist/lib/methods/node.js +++ b/dist/lib/methods/node.js @@ -1066,7 +1066,10 @@ function postMessage(channelState, messageJson) { function emitOverFastPath(state, msgObj, messageJson) { - if (!state.options.node.useFastPath) return; // disabled + if (!state.options.node.useFastPath) { + // disabled + return; + } var others = OTHER_INSTANCES[state.channelName].filter(function (s) { return s !== state; diff --git a/dist/lib/util.js b/dist/lib/util.js index 1afac5ba..c70aec48 100644 --- a/dist/lib/util.js +++ b/dist/lib/util.js @@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); -exports.isNode = exports.PROMISE_RESOLVED_VOID = exports.PROMISE_RESOLVED_FALSE = void 0; +exports.isNode = exports.PROMISE_RESOLVED_VOID = exports.PROMISE_RESOLVED_TRUE = exports.PROMISE_RESOLVED_FALSE = void 0; exports.isPromise = isPromise; exports.microSeconds = microSeconds; exports.randomInt = randomInt; @@ -23,6 +23,8 @@ function isPromise(obj) { var PROMISE_RESOLVED_FALSE = Promise.resolve(false); exports.PROMISE_RESOLVED_FALSE = PROMISE_RESOLVED_FALSE; +var PROMISE_RESOLVED_TRUE = Promise.resolve(true); +exports.PROMISE_RESOLVED_TRUE = PROMISE_RESOLVED_TRUE; var PROMISE_RESOLVED_VOID = Promise.resolve(); exports.PROMISE_RESOLVED_VOID = PROMISE_RESOLVED_VOID; diff --git a/docs/e2e.js b/docs/e2e.js index 567210c8..1964ca43 100644 --- a/docs/e2e.js +++ b/docs/e2e.js @@ -4,7 +4,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); -exports.BroadcastChannel = void 0; +exports.OPEN_BROADCAST_CHANNELS = exports.BroadcastChannel = void 0; exports.clearNodeFolder = clearNodeFolder; exports.enforceOptions = enforceOptions; @@ -14,7 +14,18 @@ var _methodChooser = require("./method-chooser.js"); var _options = require("./options.js"); +/** + * Contains all open channels, + * used in tests to ensure everything is closed. + */ +var OPEN_BROADCAST_CHANNELS = new Set(); +exports.OPEN_BROADCAST_CHANNELS = OPEN_BROADCAST_CHANNELS; +var lastId = 0; + var BroadcastChannel = function BroadcastChannel(name, options) { + // identifier of the channel to debug stuff + this.id = lastId++; + OPEN_BROADCAST_CHANNELS.add(this); this.name = name; if (ENFORCED_OPTIONS) { @@ -155,6 +166,7 @@ BroadcastChannel.prototype = { return; } + OPEN_BROADCAST_CHANNELS["delete"](this); this.closed = true; var awaitPrepare = this._prepP ? this._prepP : _util.PROMISE_RESOLVED_VOID; this._onML = null; @@ -310,6 +322,12 @@ Object.defineProperty(exports, "BroadcastChannel", { return _broadcastChannel.BroadcastChannel; } }); +Object.defineProperty(exports, "OPEN_BROADCAST_CHANNELS", { + enumerable: true, + get: function get() { + return _broadcastChannel.OPEN_BROADCAST_CHANNELS; + } +}); Object.defineProperty(exports, "beLeader", { enumerable: true, get: function get() { @@ -361,15 +379,14 @@ var LeaderElection = function LeaderElection(broadcastChannel, options) { this.isDead = false; this.token = (0, _util.randomToken)(); /** - * _isApplying - * Only set when a leader application is - * running at the moment. - * @type {Promise | null} + * Apply Queue, + * used to ensure we do not run applyOnce() + * in parallel. */ - this._isApl = false; // _isApplying + this._aplQ = _util.PROMISE_RESOLVED_VOID; // amount of unfinished applyOnce() calls - this._reApply = false; // things to clean up + this._aplQC = 0; // things to clean up this._unl = []; // _unloads @@ -406,6 +423,11 @@ var LeaderElection = function LeaderElection(broadcastChannel, options) { }; LeaderElection.prototype = { + /** + * Returns true if the instance is leader, + * false if not. + * @async + */ applyOnce: function applyOnce() { var _this2 = this; @@ -414,82 +436,113 @@ LeaderElection.prototype = { } if (this.isDead) { - return _util.PROMISE_RESOLVED_FALSE; - } // do nothing if already running + return (0, _util.sleep)(0, false); + } + /** + * Already applying more then once, + * -> wait for the apply queue to be finished. + */ - if (this._isApl) { - this._reApply = true; - return (0, _util.sleep)(0, false); + if (this._aplQC > 1) { + return this._aplQ; } + /** + * Add a new apply-run + */ - var stopCriteria = false; - var recieved = []; - var handleMessage = function handleMessage(msg) { - if (msg.context === 'leader' && msg.token != _this2.token) { - recieved.push(msg); + var applyRun = function applyRun() { + /** + * Optimization shortcuts. + * Directly return if a previous run + * has already elected a leader. + */ + if (_this2.isLeader) { + return _util.PROMISE_RESOLVED_TRUE; + } - if (msg.action === 'apply') { - // other is applying - if (msg.token > _this2.token) { - // other has higher token, stop applying - stopCriteria = true; - } - } + var stopCriteria = false; + var stopCriteriaPromiseResolve; + /** + * Resolves when a stop criteria is reached. + * Uses as a performance shortcut so we do not + * have to await the responseTime when it is already clear + * that the election failed. + */ - if (msg.action === 'tell') { - // other is already leader + var stopCriteriaPromise = new Promise(function (res) { + stopCriteriaPromiseResolve = function stopCriteriaPromiseResolve() { stopCriteria = true; - _this2.hasLeader = true; + res(); + }; + }); + var recieved = []; + + var handleMessage = function handleMessage(msg) { + if (msg.context === 'leader' && msg.token != _this2.token) { + recieved.push(msg); + + if (msg.action === 'apply') { + // other is applying + if (msg.token > _this2.token) { + /** + * other has higher token + * -> stop applying and let other become leader. + */ + stopCriteriaPromiseResolve(); + } + } + + if (msg.action === 'tell') { + // other is already leader + stopCriteriaPromiseResolve(); + _this2.hasLeader = true; + } } - } - }; + }; - this.broadcastChannel.addEventListener('internal', handleMessage); + _this2.broadcastChannel.addEventListener('internal', handleMessage); - var applyPromise = _sendMessage(this, 'apply') // send out that this one is applying - .then(function () { - return (0, _util.sleep)(_this2._options.responseTime); - }) // let others time to respond - .then(function () { - if (stopCriteria) { - return Promise.reject(new Error()); - } else { + var applyPromise = _sendMessage(_this2, 'apply') // send out that this one is applying + .then(function () { + return Promise.race([(0, _util.sleep)(_this2._options.responseTime / 2), stopCriteriaPromise.then(function () { + return Promise.reject(new Error()); + })]); + }) // send again in case another instance was just created + .then(function () { return _sendMessage(_this2, 'apply'); - } - }).then(function () { - return (0, _util.sleep)(_this2._options.responseTime); - }) // let others time to respond - .then(function () { - if (stopCriteria) { - return Promise.reject(new Error()); - } else { - return _sendMessage(_this2); - } - }).then(function () { - return beLeader(_this2); - }) // no one disagreed -> this one is now leader - .then(function () { - return true; - })["catch"](function () { - return false; - }) // apply not successfull - .then(function (success) { - _this2.broadcastChannel.removeEventListener('internal', handleMessage); + }) // let others time to respond + .then(function () { + return Promise.race([(0, _util.sleep)(_this2._options.responseTime / 2), stopCriteriaPromise.then(function () { + return Promise.reject(new Error()); + })]); + })["catch"](function () {}).then(function () { + _this2.broadcastChannel.removeEventListener('internal', handleMessage); + + if (!stopCriteria) { + // no stop criteria -> own is leader + return beLeader(_this2).then(function () { + return true; + }); + } else { + // other is leader + return false; + } + }); - _this2._isApl = false; + return applyPromise; + }; - if (!success && _this2._reApply) { - _this2._reApply = false; - return _this2.applyOnce(); - } else { - return success; - } + this._aplQC = this._aplQC + 1; + this._aplQ = this._aplQ.then(function () { + return applyRun(); + }).then(function () { + _this2._aplQC = _this2._aplQC - 1; + }); + return this._aplQ.then(function () { + return _this2.isLeader; }); - - this._isApl = applyPromise; - return applyPromise; }, awaitLeadership: function awaitLeadership() { if ( @@ -566,6 +619,7 @@ function _awaitLeadershipOnce(leaderElector) { }); // try on fallbackInterval var interval = setInterval(function () { + console.log('applyOnce via fallbackInterval'); leaderElector.applyOnce().then(function () { if (leaderElector.isLeader) { finish(); @@ -1501,7 +1555,7 @@ function fillOptionsWithDefaults() { Object.defineProperty(exports, "__esModule", { value: true }); -exports.isNode = exports.PROMISE_RESOLVED_VOID = exports.PROMISE_RESOLVED_FALSE = void 0; +exports.isNode = exports.PROMISE_RESOLVED_VOID = exports.PROMISE_RESOLVED_TRUE = exports.PROMISE_RESOLVED_FALSE = void 0; exports.isPromise = isPromise; exports.microSeconds = microSeconds; exports.randomInt = randomInt; @@ -1521,6 +1575,8 @@ function isPromise(obj) { var PROMISE_RESOLVED_FALSE = Promise.resolve(false); exports.PROMISE_RESOLVED_FALSE = PROMISE_RESOLVED_FALSE; +var PROMISE_RESOLVED_TRUE = Promise.resolve(true); +exports.PROMISE_RESOLVED_TRUE = PROMISE_RESOLVED_TRUE; var PROMISE_RESOLVED_VOID = Promise.resolve(); exports.PROMISE_RESOLVED_VOID = PROMISE_RESOLVED_VOID; diff --git a/docs/iframe.js b/docs/iframe.js index e7693790..11d0b6de 100644 --- a/docs/iframe.js +++ b/docs/iframe.js @@ -4,7 +4,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); -exports.BroadcastChannel = void 0; +exports.OPEN_BROADCAST_CHANNELS = exports.BroadcastChannel = void 0; exports.clearNodeFolder = clearNodeFolder; exports.enforceOptions = enforceOptions; @@ -14,7 +14,18 @@ var _methodChooser = require("./method-chooser.js"); var _options = require("./options.js"); +/** + * Contains all open channels, + * used in tests to ensure everything is closed. + */ +var OPEN_BROADCAST_CHANNELS = new Set(); +exports.OPEN_BROADCAST_CHANNELS = OPEN_BROADCAST_CHANNELS; +var lastId = 0; + var BroadcastChannel = function BroadcastChannel(name, options) { + // identifier of the channel to debug stuff + this.id = lastId++; + OPEN_BROADCAST_CHANNELS.add(this); this.name = name; if (ENFORCED_OPTIONS) { @@ -155,6 +166,7 @@ BroadcastChannel.prototype = { return; } + OPEN_BROADCAST_CHANNELS["delete"](this); this.closed = true; var awaitPrepare = this._prepP ? this._prepP : _util.PROMISE_RESOLVED_VOID; this._onML = null; @@ -310,6 +322,12 @@ Object.defineProperty(exports, "BroadcastChannel", { return _broadcastChannel.BroadcastChannel; } }); +Object.defineProperty(exports, "OPEN_BROADCAST_CHANNELS", { + enumerable: true, + get: function get() { + return _broadcastChannel.OPEN_BROADCAST_CHANNELS; + } +}); Object.defineProperty(exports, "beLeader", { enumerable: true, get: function get() { @@ -361,15 +379,14 @@ var LeaderElection = function LeaderElection(broadcastChannel, options) { this.isDead = false; this.token = (0, _util.randomToken)(); /** - * _isApplying - * Only set when a leader application is - * running at the moment. - * @type {Promise | null} + * Apply Queue, + * used to ensure we do not run applyOnce() + * in parallel. */ - this._isApl = false; // _isApplying + this._aplQ = _util.PROMISE_RESOLVED_VOID; // amount of unfinished applyOnce() calls - this._reApply = false; // things to clean up + this._aplQC = 0; // things to clean up this._unl = []; // _unloads @@ -406,6 +423,11 @@ var LeaderElection = function LeaderElection(broadcastChannel, options) { }; LeaderElection.prototype = { + /** + * Returns true if the instance is leader, + * false if not. + * @async + */ applyOnce: function applyOnce() { var _this2 = this; @@ -414,82 +436,113 @@ LeaderElection.prototype = { } if (this.isDead) { - return _util.PROMISE_RESOLVED_FALSE; - } // do nothing if already running + return (0, _util.sleep)(0, false); + } + /** + * Already applying more then once, + * -> wait for the apply queue to be finished. + */ - if (this._isApl) { - this._reApply = true; - return (0, _util.sleep)(0, false); + if (this._aplQC > 1) { + return this._aplQ; } + /** + * Add a new apply-run + */ - var stopCriteria = false; - var recieved = []; - var handleMessage = function handleMessage(msg) { - if (msg.context === 'leader' && msg.token != _this2.token) { - recieved.push(msg); + var applyRun = function applyRun() { + /** + * Optimization shortcuts. + * Directly return if a previous run + * has already elected a leader. + */ + if (_this2.isLeader) { + return _util.PROMISE_RESOLVED_TRUE; + } - if (msg.action === 'apply') { - // other is applying - if (msg.token > _this2.token) { - // other has higher token, stop applying - stopCriteria = true; - } - } + var stopCriteria = false; + var stopCriteriaPromiseResolve; + /** + * Resolves when a stop criteria is reached. + * Uses as a performance shortcut so we do not + * have to await the responseTime when it is already clear + * that the election failed. + */ - if (msg.action === 'tell') { - // other is already leader + var stopCriteriaPromise = new Promise(function (res) { + stopCriteriaPromiseResolve = function stopCriteriaPromiseResolve() { stopCriteria = true; - _this2.hasLeader = true; + res(); + }; + }); + var recieved = []; + + var handleMessage = function handleMessage(msg) { + if (msg.context === 'leader' && msg.token != _this2.token) { + recieved.push(msg); + + if (msg.action === 'apply') { + // other is applying + if (msg.token > _this2.token) { + /** + * other has higher token + * -> stop applying and let other become leader. + */ + stopCriteriaPromiseResolve(); + } + } + + if (msg.action === 'tell') { + // other is already leader + stopCriteriaPromiseResolve(); + _this2.hasLeader = true; + } } - } - }; + }; - this.broadcastChannel.addEventListener('internal', handleMessage); + _this2.broadcastChannel.addEventListener('internal', handleMessage); - var applyPromise = _sendMessage(this, 'apply') // send out that this one is applying - .then(function () { - return (0, _util.sleep)(_this2._options.responseTime); - }) // let others time to respond - .then(function () { - if (stopCriteria) { - return Promise.reject(new Error()); - } else { + var applyPromise = _sendMessage(_this2, 'apply') // send out that this one is applying + .then(function () { + return Promise.race([(0, _util.sleep)(_this2._options.responseTime / 2), stopCriteriaPromise.then(function () { + return Promise.reject(new Error()); + })]); + }) // send again in case another instance was just created + .then(function () { return _sendMessage(_this2, 'apply'); - } - }).then(function () { - return (0, _util.sleep)(_this2._options.responseTime); - }) // let others time to respond - .then(function () { - if (stopCriteria) { - return Promise.reject(new Error()); - } else { - return _sendMessage(_this2); - } - }).then(function () { - return beLeader(_this2); - }) // no one disagreed -> this one is now leader - .then(function () { - return true; - })["catch"](function () { - return false; - }) // apply not successfull - .then(function (success) { - _this2.broadcastChannel.removeEventListener('internal', handleMessage); + }) // let others time to respond + .then(function () { + return Promise.race([(0, _util.sleep)(_this2._options.responseTime / 2), stopCriteriaPromise.then(function () { + return Promise.reject(new Error()); + })]); + })["catch"](function () {}).then(function () { + _this2.broadcastChannel.removeEventListener('internal', handleMessage); + + if (!stopCriteria) { + // no stop criteria -> own is leader + return beLeader(_this2).then(function () { + return true; + }); + } else { + // other is leader + return false; + } + }); - _this2._isApl = false; + return applyPromise; + }; - if (!success && _this2._reApply) { - _this2._reApply = false; - return _this2.applyOnce(); - } else { - return success; - } + this._aplQC = this._aplQC + 1; + this._aplQ = this._aplQ.then(function () { + return applyRun(); + }).then(function () { + _this2._aplQC = _this2._aplQC - 1; + }); + return this._aplQ.then(function () { + return _this2.isLeader; }); - - this._isApl = applyPromise; - return applyPromise; }, awaitLeadership: function awaitLeadership() { if ( @@ -566,6 +619,7 @@ function _awaitLeadershipOnce(leaderElector) { }); // try on fallbackInterval var interval = setInterval(function () { + console.log('applyOnce via fallbackInterval'); leaderElector.applyOnce().then(function () { if (leaderElector.isLeader) { finish(); @@ -1501,7 +1555,7 @@ function fillOptionsWithDefaults() { Object.defineProperty(exports, "__esModule", { value: true }); -exports.isNode = exports.PROMISE_RESOLVED_VOID = exports.PROMISE_RESOLVED_FALSE = void 0; +exports.isNode = exports.PROMISE_RESOLVED_VOID = exports.PROMISE_RESOLVED_TRUE = exports.PROMISE_RESOLVED_FALSE = void 0; exports.isPromise = isPromise; exports.microSeconds = microSeconds; exports.randomInt = randomInt; @@ -1521,6 +1575,8 @@ function isPromise(obj) { var PROMISE_RESOLVED_FALSE = Promise.resolve(false); exports.PROMISE_RESOLVED_FALSE = PROMISE_RESOLVED_FALSE; +var PROMISE_RESOLVED_TRUE = Promise.resolve(true); +exports.PROMISE_RESOLVED_TRUE = PROMISE_RESOLVED_TRUE; var PROMISE_RESOLVED_VOID = Promise.resolve(); exports.PROMISE_RESOLVED_VOID = PROMISE_RESOLVED_VOID; diff --git a/docs/index.js b/docs/index.js index ec721460..3dc1eb4c 100644 --- a/docs/index.js +++ b/docs/index.js @@ -4,7 +4,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); -exports.BroadcastChannel = void 0; +exports.OPEN_BROADCAST_CHANNELS = exports.BroadcastChannel = void 0; exports.clearNodeFolder = clearNodeFolder; exports.enforceOptions = enforceOptions; @@ -14,7 +14,18 @@ var _methodChooser = require("./method-chooser.js"); var _options = require("./options.js"); +/** + * Contains all open channels, + * used in tests to ensure everything is closed. + */ +var OPEN_BROADCAST_CHANNELS = new Set(); +exports.OPEN_BROADCAST_CHANNELS = OPEN_BROADCAST_CHANNELS; +var lastId = 0; + var BroadcastChannel = function BroadcastChannel(name, options) { + // identifier of the channel to debug stuff + this.id = lastId++; + OPEN_BROADCAST_CHANNELS.add(this); this.name = name; if (ENFORCED_OPTIONS) { @@ -155,6 +166,7 @@ BroadcastChannel.prototype = { return; } + OPEN_BROADCAST_CHANNELS["delete"](this); this.closed = true; var awaitPrepare = this._prepP ? this._prepP : _util.PROMISE_RESOLVED_VOID; this._onML = null; @@ -310,6 +322,12 @@ Object.defineProperty(exports, "BroadcastChannel", { return _broadcastChannel.BroadcastChannel; } }); +Object.defineProperty(exports, "OPEN_BROADCAST_CHANNELS", { + enumerable: true, + get: function get() { + return _broadcastChannel.OPEN_BROADCAST_CHANNELS; + } +}); Object.defineProperty(exports, "beLeader", { enumerable: true, get: function get() { @@ -361,15 +379,14 @@ var LeaderElection = function LeaderElection(broadcastChannel, options) { this.isDead = false; this.token = (0, _util.randomToken)(); /** - * _isApplying - * Only set when a leader application is - * running at the moment. - * @type {Promise | null} + * Apply Queue, + * used to ensure we do not run applyOnce() + * in parallel. */ - this._isApl = false; // _isApplying + this._aplQ = _util.PROMISE_RESOLVED_VOID; // amount of unfinished applyOnce() calls - this._reApply = false; // things to clean up + this._aplQC = 0; // things to clean up this._unl = []; // _unloads @@ -406,6 +423,11 @@ var LeaderElection = function LeaderElection(broadcastChannel, options) { }; LeaderElection.prototype = { + /** + * Returns true if the instance is leader, + * false if not. + * @async + */ applyOnce: function applyOnce() { var _this2 = this; @@ -414,82 +436,113 @@ LeaderElection.prototype = { } if (this.isDead) { - return _util.PROMISE_RESOLVED_FALSE; - } // do nothing if already running + return (0, _util.sleep)(0, false); + } + /** + * Already applying more then once, + * -> wait for the apply queue to be finished. + */ - if (this._isApl) { - this._reApply = true; - return (0, _util.sleep)(0, false); + if (this._aplQC > 1) { + return this._aplQ; } + /** + * Add a new apply-run + */ - var stopCriteria = false; - var recieved = []; - var handleMessage = function handleMessage(msg) { - if (msg.context === 'leader' && msg.token != _this2.token) { - recieved.push(msg); + var applyRun = function applyRun() { + /** + * Optimization shortcuts. + * Directly return if a previous run + * has already elected a leader. + */ + if (_this2.isLeader) { + return _util.PROMISE_RESOLVED_TRUE; + } - if (msg.action === 'apply') { - // other is applying - if (msg.token > _this2.token) { - // other has higher token, stop applying - stopCriteria = true; - } - } + var stopCriteria = false; + var stopCriteriaPromiseResolve; + /** + * Resolves when a stop criteria is reached. + * Uses as a performance shortcut so we do not + * have to await the responseTime when it is already clear + * that the election failed. + */ - if (msg.action === 'tell') { - // other is already leader + var stopCriteriaPromise = new Promise(function (res) { + stopCriteriaPromiseResolve = function stopCriteriaPromiseResolve() { stopCriteria = true; - _this2.hasLeader = true; + res(); + }; + }); + var recieved = []; + + var handleMessage = function handleMessage(msg) { + if (msg.context === 'leader' && msg.token != _this2.token) { + recieved.push(msg); + + if (msg.action === 'apply') { + // other is applying + if (msg.token > _this2.token) { + /** + * other has higher token + * -> stop applying and let other become leader. + */ + stopCriteriaPromiseResolve(); + } + } + + if (msg.action === 'tell') { + // other is already leader + stopCriteriaPromiseResolve(); + _this2.hasLeader = true; + } } - } - }; + }; - this.broadcastChannel.addEventListener('internal', handleMessage); + _this2.broadcastChannel.addEventListener('internal', handleMessage); - var applyPromise = _sendMessage(this, 'apply') // send out that this one is applying - .then(function () { - return (0, _util.sleep)(_this2._options.responseTime); - }) // let others time to respond - .then(function () { - if (stopCriteria) { - return Promise.reject(new Error()); - } else { + var applyPromise = _sendMessage(_this2, 'apply') // send out that this one is applying + .then(function () { + return Promise.race([(0, _util.sleep)(_this2._options.responseTime / 2), stopCriteriaPromise.then(function () { + return Promise.reject(new Error()); + })]); + }) // send again in case another instance was just created + .then(function () { return _sendMessage(_this2, 'apply'); - } - }).then(function () { - return (0, _util.sleep)(_this2._options.responseTime); - }) // let others time to respond - .then(function () { - if (stopCriteria) { - return Promise.reject(new Error()); - } else { - return _sendMessage(_this2); - } - }).then(function () { - return beLeader(_this2); - }) // no one disagreed -> this one is now leader - .then(function () { - return true; - })["catch"](function () { - return false; - }) // apply not successfull - .then(function (success) { - _this2.broadcastChannel.removeEventListener('internal', handleMessage); + }) // let others time to respond + .then(function () { + return Promise.race([(0, _util.sleep)(_this2._options.responseTime / 2), stopCriteriaPromise.then(function () { + return Promise.reject(new Error()); + })]); + })["catch"](function () {}).then(function () { + _this2.broadcastChannel.removeEventListener('internal', handleMessage); + + if (!stopCriteria) { + // no stop criteria -> own is leader + return beLeader(_this2).then(function () { + return true; + }); + } else { + // other is leader + return false; + } + }); - _this2._isApl = false; + return applyPromise; + }; - if (!success && _this2._reApply) { - _this2._reApply = false; - return _this2.applyOnce(); - } else { - return success; - } + this._aplQC = this._aplQC + 1; + this._aplQ = this._aplQ.then(function () { + return applyRun(); + }).then(function () { + _this2._aplQC = _this2._aplQC - 1; + }); + return this._aplQ.then(function () { + return _this2.isLeader; }); - - this._isApl = applyPromise; - return applyPromise; }, awaitLeadership: function awaitLeadership() { if ( @@ -566,6 +619,7 @@ function _awaitLeadershipOnce(leaderElector) { }); // try on fallbackInterval var interval = setInterval(function () { + console.log('applyOnce via fallbackInterval'); leaderElector.applyOnce().then(function () { if (leaderElector.isLeader) { finish(); @@ -1501,7 +1555,7 @@ function fillOptionsWithDefaults() { Object.defineProperty(exports, "__esModule", { value: true }); -exports.isNode = exports.PROMISE_RESOLVED_VOID = exports.PROMISE_RESOLVED_FALSE = void 0; +exports.isNode = exports.PROMISE_RESOLVED_VOID = exports.PROMISE_RESOLVED_TRUE = exports.PROMISE_RESOLVED_FALSE = void 0; exports.isPromise = isPromise; exports.microSeconds = microSeconds; exports.randomInt = randomInt; @@ -1521,6 +1575,8 @@ function isPromise(obj) { var PROMISE_RESOLVED_FALSE = Promise.resolve(false); exports.PROMISE_RESOLVED_FALSE = PROMISE_RESOLVED_FALSE; +var PROMISE_RESOLVED_TRUE = Promise.resolve(true); +exports.PROMISE_RESOLVED_TRUE = PROMISE_RESOLVED_TRUE; var PROMISE_RESOLVED_VOID = Promise.resolve(); exports.PROMISE_RESOLVED_VOID = PROMISE_RESOLVED_VOID; diff --git a/docs/leader-iframe.js b/docs/leader-iframe.js index d481046f..6b60c269 100644 --- a/docs/leader-iframe.js +++ b/docs/leader-iframe.js @@ -4,7 +4,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); -exports.BroadcastChannel = void 0; +exports.OPEN_BROADCAST_CHANNELS = exports.BroadcastChannel = void 0; exports.clearNodeFolder = clearNodeFolder; exports.enforceOptions = enforceOptions; @@ -14,7 +14,18 @@ var _methodChooser = require("./method-chooser.js"); var _options = require("./options.js"); +/** + * Contains all open channels, + * used in tests to ensure everything is closed. + */ +var OPEN_BROADCAST_CHANNELS = new Set(); +exports.OPEN_BROADCAST_CHANNELS = OPEN_BROADCAST_CHANNELS; +var lastId = 0; + var BroadcastChannel = function BroadcastChannel(name, options) { + // identifier of the channel to debug stuff + this.id = lastId++; + OPEN_BROADCAST_CHANNELS.add(this); this.name = name; if (ENFORCED_OPTIONS) { @@ -155,6 +166,7 @@ BroadcastChannel.prototype = { return; } + OPEN_BROADCAST_CHANNELS["delete"](this); this.closed = true; var awaitPrepare = this._prepP ? this._prepP : _util.PROMISE_RESOLVED_VOID; this._onML = null; @@ -310,6 +322,12 @@ Object.defineProperty(exports, "BroadcastChannel", { return _broadcastChannel.BroadcastChannel; } }); +Object.defineProperty(exports, "OPEN_BROADCAST_CHANNELS", { + enumerable: true, + get: function get() { + return _broadcastChannel.OPEN_BROADCAST_CHANNELS; + } +}); Object.defineProperty(exports, "beLeader", { enumerable: true, get: function get() { @@ -361,15 +379,14 @@ var LeaderElection = function LeaderElection(broadcastChannel, options) { this.isDead = false; this.token = (0, _util.randomToken)(); /** - * _isApplying - * Only set when a leader application is - * running at the moment. - * @type {Promise | null} + * Apply Queue, + * used to ensure we do not run applyOnce() + * in parallel. */ - this._isApl = false; // _isApplying + this._aplQ = _util.PROMISE_RESOLVED_VOID; // amount of unfinished applyOnce() calls - this._reApply = false; // things to clean up + this._aplQC = 0; // things to clean up this._unl = []; // _unloads @@ -406,6 +423,11 @@ var LeaderElection = function LeaderElection(broadcastChannel, options) { }; LeaderElection.prototype = { + /** + * Returns true if the instance is leader, + * false if not. + * @async + */ applyOnce: function applyOnce() { var _this2 = this; @@ -414,82 +436,113 @@ LeaderElection.prototype = { } if (this.isDead) { - return _util.PROMISE_RESOLVED_FALSE; - } // do nothing if already running + return (0, _util.sleep)(0, false); + } + /** + * Already applying more then once, + * -> wait for the apply queue to be finished. + */ - if (this._isApl) { - this._reApply = true; - return (0, _util.sleep)(0, false); + if (this._aplQC > 1) { + return this._aplQ; } + /** + * Add a new apply-run + */ - var stopCriteria = false; - var recieved = []; - var handleMessage = function handleMessage(msg) { - if (msg.context === 'leader' && msg.token != _this2.token) { - recieved.push(msg); + var applyRun = function applyRun() { + /** + * Optimization shortcuts. + * Directly return if a previous run + * has already elected a leader. + */ + if (_this2.isLeader) { + return _util.PROMISE_RESOLVED_TRUE; + } - if (msg.action === 'apply') { - // other is applying - if (msg.token > _this2.token) { - // other has higher token, stop applying - stopCriteria = true; - } - } + var stopCriteria = false; + var stopCriteriaPromiseResolve; + /** + * Resolves when a stop criteria is reached. + * Uses as a performance shortcut so we do not + * have to await the responseTime when it is already clear + * that the election failed. + */ - if (msg.action === 'tell') { - // other is already leader + var stopCriteriaPromise = new Promise(function (res) { + stopCriteriaPromiseResolve = function stopCriteriaPromiseResolve() { stopCriteria = true; - _this2.hasLeader = true; + res(); + }; + }); + var recieved = []; + + var handleMessage = function handleMessage(msg) { + if (msg.context === 'leader' && msg.token != _this2.token) { + recieved.push(msg); + + if (msg.action === 'apply') { + // other is applying + if (msg.token > _this2.token) { + /** + * other has higher token + * -> stop applying and let other become leader. + */ + stopCriteriaPromiseResolve(); + } + } + + if (msg.action === 'tell') { + // other is already leader + stopCriteriaPromiseResolve(); + _this2.hasLeader = true; + } } - } - }; + }; - this.broadcastChannel.addEventListener('internal', handleMessage); + _this2.broadcastChannel.addEventListener('internal', handleMessage); - var applyPromise = _sendMessage(this, 'apply') // send out that this one is applying - .then(function () { - return (0, _util.sleep)(_this2._options.responseTime); - }) // let others time to respond - .then(function () { - if (stopCriteria) { - return Promise.reject(new Error()); - } else { + var applyPromise = _sendMessage(_this2, 'apply') // send out that this one is applying + .then(function () { + return Promise.race([(0, _util.sleep)(_this2._options.responseTime / 2), stopCriteriaPromise.then(function () { + return Promise.reject(new Error()); + })]); + }) // send again in case another instance was just created + .then(function () { return _sendMessage(_this2, 'apply'); - } - }).then(function () { - return (0, _util.sleep)(_this2._options.responseTime); - }) // let others time to respond - .then(function () { - if (stopCriteria) { - return Promise.reject(new Error()); - } else { - return _sendMessage(_this2); - } - }).then(function () { - return beLeader(_this2); - }) // no one disagreed -> this one is now leader - .then(function () { - return true; - })["catch"](function () { - return false; - }) // apply not successfull - .then(function (success) { - _this2.broadcastChannel.removeEventListener('internal', handleMessage); + }) // let others time to respond + .then(function () { + return Promise.race([(0, _util.sleep)(_this2._options.responseTime / 2), stopCriteriaPromise.then(function () { + return Promise.reject(new Error()); + })]); + })["catch"](function () {}).then(function () { + _this2.broadcastChannel.removeEventListener('internal', handleMessage); + + if (!stopCriteria) { + // no stop criteria -> own is leader + return beLeader(_this2).then(function () { + return true; + }); + } else { + // other is leader + return false; + } + }); - _this2._isApl = false; + return applyPromise; + }; - if (!success && _this2._reApply) { - _this2._reApply = false; - return _this2.applyOnce(); - } else { - return success; - } + this._aplQC = this._aplQC + 1; + this._aplQ = this._aplQ.then(function () { + return applyRun(); + }).then(function () { + _this2._aplQC = _this2._aplQC - 1; + }); + return this._aplQ.then(function () { + return _this2.isLeader; }); - - this._isApl = applyPromise; - return applyPromise; }, awaitLeadership: function awaitLeadership() { if ( @@ -566,6 +619,7 @@ function _awaitLeadershipOnce(leaderElector) { }); // try on fallbackInterval var interval = setInterval(function () { + console.log('applyOnce via fallbackInterval'); leaderElector.applyOnce().then(function () { if (leaderElector.isLeader) { finish(); @@ -1501,7 +1555,7 @@ function fillOptionsWithDefaults() { Object.defineProperty(exports, "__esModule", { value: true }); -exports.isNode = exports.PROMISE_RESOLVED_VOID = exports.PROMISE_RESOLVED_FALSE = void 0; +exports.isNode = exports.PROMISE_RESOLVED_VOID = exports.PROMISE_RESOLVED_TRUE = exports.PROMISE_RESOLVED_FALSE = void 0; exports.isPromise = isPromise; exports.microSeconds = microSeconds; exports.randomInt = randomInt; @@ -1521,6 +1575,8 @@ function isPromise(obj) { var PROMISE_RESOLVED_FALSE = Promise.resolve(false); exports.PROMISE_RESOLVED_FALSE = PROMISE_RESOLVED_FALSE; +var PROMISE_RESOLVED_TRUE = Promise.resolve(true); +exports.PROMISE_RESOLVED_TRUE = PROMISE_RESOLVED_TRUE; var PROMISE_RESOLVED_VOID = Promise.resolve(); exports.PROMISE_RESOLVED_VOID = PROMISE_RESOLVED_VOID; diff --git a/docs/worker.js b/docs/worker.js index 116912f7..6076532b 100644 --- a/docs/worker.js +++ b/docs/worker.js @@ -4,7 +4,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); -exports.BroadcastChannel = void 0; +exports.OPEN_BROADCAST_CHANNELS = exports.BroadcastChannel = void 0; exports.clearNodeFolder = clearNodeFolder; exports.enforceOptions = enforceOptions; @@ -14,7 +14,18 @@ var _methodChooser = require("./method-chooser.js"); var _options = require("./options.js"); +/** + * Contains all open channels, + * used in tests to ensure everything is closed. + */ +var OPEN_BROADCAST_CHANNELS = new Set(); +exports.OPEN_BROADCAST_CHANNELS = OPEN_BROADCAST_CHANNELS; +var lastId = 0; + var BroadcastChannel = function BroadcastChannel(name, options) { + // identifier of the channel to debug stuff + this.id = lastId++; + OPEN_BROADCAST_CHANNELS.add(this); this.name = name; if (ENFORCED_OPTIONS) { @@ -155,6 +166,7 @@ BroadcastChannel.prototype = { return; } + OPEN_BROADCAST_CHANNELS["delete"](this); this.closed = true; var awaitPrepare = this._prepP ? this._prepP : _util.PROMISE_RESOLVED_VOID; this._onML = null; @@ -310,6 +322,12 @@ Object.defineProperty(exports, "BroadcastChannel", { return _broadcastChannel.BroadcastChannel; } }); +Object.defineProperty(exports, "OPEN_BROADCAST_CHANNELS", { + enumerable: true, + get: function get() { + return _broadcastChannel.OPEN_BROADCAST_CHANNELS; + } +}); Object.defineProperty(exports, "beLeader", { enumerable: true, get: function get() { @@ -361,15 +379,14 @@ var LeaderElection = function LeaderElection(broadcastChannel, options) { this.isDead = false; this.token = (0, _util.randomToken)(); /** - * _isApplying - * Only set when a leader application is - * running at the moment. - * @type {Promise | null} + * Apply Queue, + * used to ensure we do not run applyOnce() + * in parallel. */ - this._isApl = false; // _isApplying + this._aplQ = _util.PROMISE_RESOLVED_VOID; // amount of unfinished applyOnce() calls - this._reApply = false; // things to clean up + this._aplQC = 0; // things to clean up this._unl = []; // _unloads @@ -406,6 +423,11 @@ var LeaderElection = function LeaderElection(broadcastChannel, options) { }; LeaderElection.prototype = { + /** + * Returns true if the instance is leader, + * false if not. + * @async + */ applyOnce: function applyOnce() { var _this2 = this; @@ -414,82 +436,113 @@ LeaderElection.prototype = { } if (this.isDead) { - return _util.PROMISE_RESOLVED_FALSE; - } // do nothing if already running + return (0, _util.sleep)(0, false); + } + /** + * Already applying more then once, + * -> wait for the apply queue to be finished. + */ - if (this._isApl) { - this._reApply = true; - return (0, _util.sleep)(0, false); + if (this._aplQC > 1) { + return this._aplQ; } + /** + * Add a new apply-run + */ - var stopCriteria = false; - var recieved = []; - var handleMessage = function handleMessage(msg) { - if (msg.context === 'leader' && msg.token != _this2.token) { - recieved.push(msg); + var applyRun = function applyRun() { + /** + * Optimization shortcuts. + * Directly return if a previous run + * has already elected a leader. + */ + if (_this2.isLeader) { + return _util.PROMISE_RESOLVED_TRUE; + } - if (msg.action === 'apply') { - // other is applying - if (msg.token > _this2.token) { - // other has higher token, stop applying - stopCriteria = true; - } - } + var stopCriteria = false; + var stopCriteriaPromiseResolve; + /** + * Resolves when a stop criteria is reached. + * Uses as a performance shortcut so we do not + * have to await the responseTime when it is already clear + * that the election failed. + */ - if (msg.action === 'tell') { - // other is already leader + var stopCriteriaPromise = new Promise(function (res) { + stopCriteriaPromiseResolve = function stopCriteriaPromiseResolve() { stopCriteria = true; - _this2.hasLeader = true; + res(); + }; + }); + var recieved = []; + + var handleMessage = function handleMessage(msg) { + if (msg.context === 'leader' && msg.token != _this2.token) { + recieved.push(msg); + + if (msg.action === 'apply') { + // other is applying + if (msg.token > _this2.token) { + /** + * other has higher token + * -> stop applying and let other become leader. + */ + stopCriteriaPromiseResolve(); + } + } + + if (msg.action === 'tell') { + // other is already leader + stopCriteriaPromiseResolve(); + _this2.hasLeader = true; + } } - } - }; + }; - this.broadcastChannel.addEventListener('internal', handleMessage); + _this2.broadcastChannel.addEventListener('internal', handleMessage); - var applyPromise = _sendMessage(this, 'apply') // send out that this one is applying - .then(function () { - return (0, _util.sleep)(_this2._options.responseTime); - }) // let others time to respond - .then(function () { - if (stopCriteria) { - return Promise.reject(new Error()); - } else { + var applyPromise = _sendMessage(_this2, 'apply') // send out that this one is applying + .then(function () { + return Promise.race([(0, _util.sleep)(_this2._options.responseTime / 2), stopCriteriaPromise.then(function () { + return Promise.reject(new Error()); + })]); + }) // send again in case another instance was just created + .then(function () { return _sendMessage(_this2, 'apply'); - } - }).then(function () { - return (0, _util.sleep)(_this2._options.responseTime); - }) // let others time to respond - .then(function () { - if (stopCriteria) { - return Promise.reject(new Error()); - } else { - return _sendMessage(_this2); - } - }).then(function () { - return beLeader(_this2); - }) // no one disagreed -> this one is now leader - .then(function () { - return true; - })["catch"](function () { - return false; - }) // apply not successfull - .then(function (success) { - _this2.broadcastChannel.removeEventListener('internal', handleMessage); + }) // let others time to respond + .then(function () { + return Promise.race([(0, _util.sleep)(_this2._options.responseTime / 2), stopCriteriaPromise.then(function () { + return Promise.reject(new Error()); + })]); + })["catch"](function () {}).then(function () { + _this2.broadcastChannel.removeEventListener('internal', handleMessage); + + if (!stopCriteria) { + // no stop criteria -> own is leader + return beLeader(_this2).then(function () { + return true; + }); + } else { + // other is leader + return false; + } + }); - _this2._isApl = false; + return applyPromise; + }; - if (!success && _this2._reApply) { - _this2._reApply = false; - return _this2.applyOnce(); - } else { - return success; - } + this._aplQC = this._aplQC + 1; + this._aplQ = this._aplQ.then(function () { + return applyRun(); + }).then(function () { + _this2._aplQC = _this2._aplQC - 1; + }); + return this._aplQ.then(function () { + return _this2.isLeader; }); - - this._isApl = applyPromise; - return applyPromise; }, awaitLeadership: function awaitLeadership() { if ( @@ -566,6 +619,7 @@ function _awaitLeadershipOnce(leaderElector) { }); // try on fallbackInterval var interval = setInterval(function () { + console.log('applyOnce via fallbackInterval'); leaderElector.applyOnce().then(function () { if (leaderElector.isLeader) { finish(); @@ -1501,7 +1555,7 @@ function fillOptionsWithDefaults() { Object.defineProperty(exports, "__esModule", { value: true }); -exports.isNode = exports.PROMISE_RESOLVED_VOID = exports.PROMISE_RESOLVED_FALSE = void 0; +exports.isNode = exports.PROMISE_RESOLVED_VOID = exports.PROMISE_RESOLVED_TRUE = exports.PROMISE_RESOLVED_FALSE = void 0; exports.isPromise = isPromise; exports.microSeconds = microSeconds; exports.randomInt = randomInt; @@ -1521,6 +1575,8 @@ function isPromise(obj) { var PROMISE_RESOLVED_FALSE = Promise.resolve(false); exports.PROMISE_RESOLVED_FALSE = PROMISE_RESOLVED_FALSE; +var PROMISE_RESOLVED_TRUE = Promise.resolve(true); +exports.PROMISE_RESOLVED_TRUE = PROMISE_RESOLVED_TRUE; var PROMISE_RESOLVED_VOID = Promise.resolve(); exports.PROMISE_RESOLVED_VOID = PROMISE_RESOLVED_VOID;