Skip to content

Commit

Permalink
5.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
pubkey committed Mar 22, 2023
1 parent 063a2cb commit 69c76a4
Show file tree
Hide file tree
Showing 33 changed files with 3,138 additions and 2,391 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

## X.X.X (comming soon)

## 5.0.0 (23 March 2023)

- Use [Web Locks API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Locks_API) for leader election if possible.
- `LeaderElector.hasLeader` is now a function that returns a `Promise<boolean>`.

Expand Down
5 changes: 3 additions & 2 deletions dist/es5node/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Object.defineProperty(exports, "OPEN_BROADCAST_CHANNELS", {
Object.defineProperty(exports, "beLeader", {
enumerable: true,
get: function get() {
return _leaderElection.beLeader;
return _leaderElectionUtil.beLeader;
}
});
Object.defineProperty(exports, "clearNodeFolder", {
Expand All @@ -40,4 +40,5 @@ Object.defineProperty(exports, "enforceOptions", {
}
});
var _broadcastChannel = require("./broadcast-channel.js");
var _leaderElection = require("./leader-election.js");
var _leaderElection = require("./leader-election.js");
var _leaderElectionUtil = require("./leader-election-util.js");
49 changes: 49 additions & 0 deletions dist/es5node/leader-election-util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"use strict";

Object.defineProperty(exports, "__esModule", {
value: true
});
exports.beLeader = beLeader;
exports.sendLeaderMessage = sendLeaderMessage;
var _unload = require("unload");
/**
* sends and internal message over the broadcast-channel
*/
function sendLeaderMessage(leaderElector, action) {
var msgJson = {
context: 'leader',
action: action,
token: leaderElector.token
};
return leaderElector.broadcastChannel.postInternal(msgJson);
}
function beLeader(leaderElector) {
leaderElector.isLeader = true;
leaderElector._hasLeader = true;
var unloadFn = (0, _unload.add)(function () {
return leaderElector.die();
});
leaderElector._unl.push(unloadFn);
var isLeaderListener = function isLeaderListener(msg) {
if (msg.context === 'leader' && msg.action === 'apply') {
sendLeaderMessage(leaderElector, 'tell');
}
if (msg.context === 'leader' && msg.action === 'tell' && !leaderElector._dpLC) {
/**
* another instance is also leader!
* This can happen on rare events
* like when the CPU is at 100% for long time
* or the tabs are open very long and the browser throttles them.
* @link https://github.com/pubkey/broadcast-channel/issues/414
* @link https://github.com/pubkey/broadcast-channel/issues/385
*/
leaderElector._dpLC = true;
leaderElector._dpL(); // message the lib user so the app can handle the problem
sendLeaderMessage(leaderElector, 'tell'); // ensure other leader also knows the problem
}
};

leaderElector.broadcastChannel.addEventListener('internal', isLeaderListener);
leaderElector._lstns.push(isLeaderListener);
return sendLeaderMessage(leaderElector, 'tell');
}
88 changes: 88 additions & 0 deletions dist/es5node/leader-election-web-lock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"use strict";

Object.defineProperty(exports, "__esModule", {
value: true
});
exports.LeaderElectionWebLock = void 0;
var _util = require("./util.js");
var _leaderElectionUtil = require("./leader-election-util.js");
/**
* A faster version of the leader elector that uses the WebLock API
* @link https://developer.mozilla.org/en-US/docs/Web/API/Web_Locks_API
*/
var LeaderElectionWebLock = function LeaderElectionWebLock(broadcastChannel, options) {
var _this = this;
this.broadcastChannel = broadcastChannel;
broadcastChannel._befC.push(function () {
return _this.die();
});
this._options = options;
this.isLeader = false;
this.isDead = false;
this.token = (0, _util.randomToken)();
this._lstns = [];
this._unl = [];
this._dpL = function () {}; // onduplicate listener
this._dpLC = false; // true when onduplicate called

this._wKMC = {}; // stuff for cleanup
};
exports.LeaderElectionWebLock = LeaderElectionWebLock;
LeaderElectionWebLock.prototype = {
hasLeader: function hasLeader() {
return navigator.locks.query().then(function (locks) {
if (locks.held && locks.held.length > 0) {
return true;
} else {
return false;
}
});
},
awaitLeadership: function awaitLeadership() {
var _this2 = this;
if (!this._wLMP) {
this._wKMC.c = new AbortController();
var returnPromise = new Promise(function (res, rej) {
_this2._wKMC.res = res;
_this2._wKMC.rej = rej;
});
this._wLMP = new Promise(function (res) {
var lockId = 'pubkey-bc||' + _this2.broadcastChannel.method.type + '||' + _this2.broadcastChannel.name;
navigator.locks.request(lockId, {
signal: _this2._wKMC.c.signal
}, function () {
(0, _leaderElectionUtil.beLeader)(_this2);
res();
return returnPromise;
});
});
}
return this._wLMP;
},
set onduplicate(_fn) {
// Do nothing because there are no duplicates in the WebLock version
},
die: function die() {
var _this3 = this;
var ret = (0, _leaderElectionUtil.sendLeaderMessage)(this, 'death');
this._lstns.forEach(function (listener) {
return _this3.broadcastChannel.removeEventListener('internal', listener);
});
this._lstns = [];
this._unl.forEach(function (uFn) {
return uFn.remove();
});
this._unl = [];
if (this.isLeader) {
this.isLeader = false;
}
this.isDead = true;
if (this._wKMC.res) {
this._wKMC.res();
}
if (this._wKMC.c) {
this._wKMC.c.abort();
}
return ret;
}
};
71 changes: 16 additions & 55 deletions dist/es5node/leader-election.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.beLeader = beLeader;
exports.createLeaderElection = createLeaderElection;
var _util = require("./util.js");
var _unload = require("unload");
var _leaderElectionUtil = require("./leader-election-util.js");
var _leaderElectionWebLock = require("./leader-election-web-lock.js");
var LeaderElection = function LeaderElection(broadcastChannel, options) {
var _this = this;
this.broadcastChannel = broadcastChannel;
this._options = options;
this.isLeader = false;
this.hasLeader = false;
this._hasLeader = false;
this.isDead = false;
this.token = (0, _util.randomToken)();

Expand All @@ -39,17 +39,20 @@ var LeaderElection = function LeaderElection(broadcastChannel, options) {
var hasLeaderListener = function hasLeaderListener(msg) {
if (msg.context === 'leader') {
if (msg.action === 'death') {
_this.hasLeader = false;
_this._hasLeader = false;
}
if (msg.action === 'tell') {
_this.hasLeader = true;
_this._hasLeader = true;
}
}
};
this.broadcastChannel.addEventListener('internal', hasLeaderListener);
this._lstns.push(hasLeaderListener);
};
LeaderElection.prototype = {
hasLeader: function hasLeader() {
return Promise.resolve(this._hasLeader);
},
/**
* Returns true if the instance is leader,
* false if not.
Expand Down Expand Up @@ -115,7 +118,7 @@ LeaderElection.prototype = {
if (msg.action === 'tell') {
// other is already leader
stopCriteriaPromiseResolve();
_this2.hasLeader = true;
_this2._hasLeader = true;
}
}
};
Expand All @@ -132,15 +135,15 @@ LeaderElection.prototype = {
* run in the background.
*/
var waitForAnswerTime = isFromFallbackInterval ? _this2._options.responseTime * 4 : _this2._options.responseTime;
return _sendMessage(_this2, 'apply') // send out that this one is applying
return (0, _leaderElectionUtil.sendLeaderMessage)(_this2, 'apply') // send out that this one is applying
.then(function () {
return Promise.race([(0, _util.sleep)(waitForAnswerTime), stopCriteriaPromise.then(function () {
return Promise.reject(new Error());
})]);
})
// send again in case another instance was just created
.then(function () {
return _sendMessage(_this2, 'apply');
return (0, _leaderElectionUtil.sendLeaderMessage)(_this2, 'apply');
})
// let others time to respond
.then(function () {
Expand All @@ -151,7 +154,7 @@ LeaderElection.prototype = {
_this2.broadcastChannel.removeEventListener('internal', handleMessage);
if (!stopCriteria) {
// no stop criteria -> own is leader
return beLeader(_this2).then(function () {
return (0, _leaderElectionUtil.beLeader)(_this2).then(function () {
return true;
});
} else {
Expand Down Expand Up @@ -191,11 +194,11 @@ LeaderElection.prototype = {
});
this._unl = [];
if (this.isLeader) {
this.hasLeader = false;
this._hasLeader = false;
this.isLeader = false;
}
this.isDead = true;
return _sendMessage(this, 'death');
return (0, _leaderElectionUtil.sendLeaderMessage)(this, 'death');
}
};

Expand Down Expand Up @@ -251,7 +254,7 @@ function _awaitLeadershipOnce(leaderElector) {
// try when other leader dies
var whenDeathListener = function whenDeathListener(msg) {
if (msg.context === 'leader' && msg.action === 'death') {
leaderElector.hasLeader = false;
leaderElector._hasLeader = false;
leaderElector.applyOnce().then(function () {
if (leaderElector.isLeader) {
finish();
Expand All @@ -263,48 +266,6 @@ function _awaitLeadershipOnce(leaderElector) {
leaderElector._lstns.push(whenDeathListener);
});
}

/**
* sends and internal message over the broadcast-channel
*/
function _sendMessage(leaderElector, action) {
var msgJson = {
context: 'leader',
action: action,
token: leaderElector.token
};
return leaderElector.broadcastChannel.postInternal(msgJson);
}
function beLeader(leaderElector) {
leaderElector.isLeader = true;
leaderElector.hasLeader = true;
var unloadFn = (0, _unload.add)(function () {
return leaderElector.die();
});
leaderElector._unl.push(unloadFn);
var isLeaderListener = function isLeaderListener(msg) {
if (msg.context === 'leader' && msg.action === 'apply') {
_sendMessage(leaderElector, 'tell');
}
if (msg.context === 'leader' && msg.action === 'tell' && !leaderElector._dpLC) {
/**
* another instance is also leader!
* This can happen on rare events
* like when the CPU is at 100% for long time
* or the tabs are open very long and the browser throttles them.
* @link https://github.com/pubkey/broadcast-channel/issues/414
* @link https://github.com/pubkey/broadcast-channel/issues/385
*/
leaderElector._dpLC = true;
leaderElector._dpL(); // message the lib user so the app can handle the problem
_sendMessage(leaderElector, 'tell'); // ensure other leader also knows the problem
}
};

leaderElector.broadcastChannel.addEventListener('internal', isLeaderListener);
leaderElector._lstns.push(isLeaderListener);
return _sendMessage(leaderElector, 'tell');
}
function fillOptionsWithDefaults(options, channel) {
if (!options) options = {};
options = JSON.parse(JSON.stringify(options));
Expand All @@ -321,7 +282,7 @@ function createLeaderElection(channel, options) {
throw new Error('BroadcastChannel already has a leader-elector');
}
options = fillOptionsWithDefaults(options, channel);
var elector = new LeaderElection(channel, options);
var elector = (0, _util.supportsWebLockAPI)() ? new _leaderElectionWebLock.LeaderElectionWebLock(channel, options) : new LeaderElection(channel, options);
channel._befC.push(function () {
return elector.die();
});
Expand Down
10 changes: 7 additions & 3 deletions dist/es5node/method-chooser.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ function chooseMethod(options) {
var useMethod = chooseMethods.find(function (method) {
return method.canBeUsed();
});
if (!useMethod) throw new Error("No usable method found in " + JSON.stringify(METHODS.map(function (m) {
return m.type;
})));else return useMethod;
if (!useMethod) {
throw new Error("No usable method found in " + JSON.stringify(METHODS.map(function (m) {
return m.type;
})));
} else {
return useMethod;
}
}
13 changes: 13 additions & 0 deletions dist/es5node/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ exports.microSeconds = microSeconds;
exports.randomInt = randomInt;
exports.randomToken = randomToken;
exports.sleep = sleep;
exports.supportsWebLockAPI = supportsWebLockAPI;
/**
* returns true if the given object is a promise
*/
Expand Down Expand Up @@ -59,4 +60,16 @@ function microSeconds() {
additional = 0;
return ms * 1000;
}
}

/**
* Check if WebLock API is supported.
* @link https://developer.mozilla.org/en-US/docs/Web/API/Web_Locks_API
*/
function supportsWebLockAPI() {
if (typeof navigator !== 'undefined' && typeof navigator.locks !== 'undefined' && typeof navigator.locks.request === 'function') {
return true;
} else {
return false;
}
}
3 changes: 2 additions & 1 deletion dist/esbrowser/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { BroadcastChannel, clearNodeFolder, enforceOptions, OPEN_BROADCAST_CHANNELS } from './broadcast-channel.js';
export { createLeaderElection, beLeader } from './leader-election.js';
export { createLeaderElection } from './leader-election.js';
export { beLeader } from './leader-election-util.js';
Loading

0 comments on commit 69c76a4

Please sign in to comment.