tab-elect
Version:
Leader election for browser tabs and workers
152 lines (116 loc) • 3.51 kB
JavaScript
/* eslint-env browser */
module.exports = TabElect
var IdbKvStore = require('idb-kv-store')
var inherits = require('inherits')
var EventEmitter = require('events').EventEmitter
TabElect.SUPPORT = IdbKvStore.INDEXEDDB_SUPPORT && IdbKvStore.BROADCAST_SUPPORT
inherits(TabElect, EventEmitter)
function TabElect (name, opts) {
var self = this
if (!TabElect.SUPPORT) throw new Error('No indexDB or BroadcastChannel support')
if (typeof name === 'undefined') throw new Error('"name" cannot be undefined')
if (!(self instanceof TabElect)) return new TabElect(name, opts)
if (!opts) opts = {}
EventEmitter.call(self)
self.isLeader = false
self.destroyed = false
self._locking = false
self._db = new IdbKvStore('tab-elect-' + name)
self._db.on('remove', onDbRemove)
self._db.on('error', onDbError)
self._db.on('close', onDbClose)
addEventListener('beforeunload', onBeforeUnload)
self.elect()
function onDbRemove (change) {
self._onDbRemove(change)
}
function onDbError (err) {
self._destroy(err)
}
function onDbClose () {
self._destroy(new Error('IDB database unexpectedly closed'))
}
function onBeforeUnload () {
self._destroy()
}
}
TabElect.prototype.elect = function (cb) {
var self = this
cb = cb || noop
if (self.destroyed) throw new Error('Already destroyed')
if (self.isLeader) throw new Error('Already the leader')
if (self._locking) return setTimeout(cb, 0, null, false)
self._db.remove('lock')
.then(function () {
return self._lock(cb)
})
.catch(function (err) {
cb(err)
})
}
TabElect.prototype._lock = function (cb) {
var self = this
cb = cb || noop
if (self.destroyed) return
if (self._locking) return cb(null, false)
self._locking = true
return self._db.add('lock', true)
.then(function () {
self._locking = false
self.isLeader = true
if (self.destroyed) return self._destroyDB()
cb(null, true)
self.emit('elected')
})
.catch(function (err) {
self._locking = false
if (self.destroyed) return self._destroyDB()
// ConstraintError - Add operation failed because key already exists. Someone else is leader
if (err.name === 'ConstraintError') cb(null, false)
else cb(err)
})
}
TabElect.prototype.stepDown = function () {
if (this.destroyed) throw new Error('Already destroyed')
if (!this.isLeader) throw new Error('Can not step down when not the leader')
this._db.remove('lock', noop) // Not much we can do here so ignore it
this._onDepose()
}
TabElect.prototype._onDepose = function () {
this.isLeader = false
this.emit('deposed')
}
TabElect.prototype._onDbRemove = function (change) {
if (this.destroyed || change.key !== 'lock') return
if (this.isLeader) {
// Someone removed our lock so we are not the leader anymore
this._onDepose()
} else {
// The leaders lock has been removed so attempt to elect ourselves
this._lock()
}
}
TabElect.prototype.destroy = function () {
this._destroy()
}
TabElect.prototype._destroy = function (err) {
if (this.destroyed) return
this.destroyed = true
if (err) this.emit('error', err)
this.removeAllListeners()
this._destroyDB()
self.isLeader = false
}
TabElect.prototype._destroyDB = function () {
var self = this
if (!self._db) return
if (self.isLeader) this._db.remove('lock', finished)
else if (!self.locking) finished()
function finished () {
self._db.close()
self._db = null
}
}
function noop () {
// do nothing
}