hypercore-rehoster
Version:
Help keep the hypercores of your choice available
210 lines (178 loc) • 5.82 kB
JavaScript
const idEnc = require('hypercore-id-encoding')
const RehosterNode = require('./rehoster-node')
const safetyCatch = require('safety-catch')
const ReadyResource = require('ready-resource')
class RehosterNodeManager extends ReadyResource {
// DEVNOTE on design:
// One RehosterNodeManager exists per Rehoster
// All new nodes are added through that object
// Its responsibilities are:
// - Ensure only a single RehosterNode exists per public key
// => keys which are present multiple times, have 1 RehosterNode but multiple RehosterNodeRefs
// (This is crucial for avoiding infinite loops when 2 rehosters rehost each other)
// - Ensure correct ref counting, and close a RehosterNode when it has 0 refs
// - Correctly manage the swarmManager (which does the swarm ref counting for serving/joining)
constructor (swarmManager, corestore, {
shouldRehost,
onNodeDeleted,
onNewNode,
onNodeUpdate,
onFullyDownloaded,
onInvalidKey,
onInvalidValue
}) {
super()
this.swarmManager = swarmManager
this.corestore = corestore
this.shouldRehost = shouldRehost
this.onNodeDeleted = onNodeDeleted
this.onNewNode = onNewNode
this.onNodeUpdate = onNodeUpdate
this.onFullyDownloaded = onFullyDownloaded
this.onInvalidKey = onInvalidKey
this.onInvalidValue = onInvalidValue
this.nodes = new Map()
this._nodeRefs = new Set()
}
async _open () {
await this.swarmManager.ready()
}
async _close () {
// INVARS:
// - All nodes start closing in same tick
// - No new nodes are added when this is closing
// (otherwise lifecycle issues)
const closeProms = []
for (const { node } of this.nodes.values()) {
closeProms.push(node.close())
}
// To stop announcing, which is tracked at
// nodeRef level
for (const nodeRef of [...this._nodeRefs]) {
closeProms.push(nodeRef.close())
}
await Promise.all(closeProms)
}
_getNodeEntry (pubKey) {
return this.nodes.get(idEnc.normalize(pubKey))
}
_setNodeEntry (pubKey, entry) {
return this.nodes.set(idEnc.normalize(pubKey), entry)
}
_deleteNodeEntry (pubKey) {
this.nodes.delete(idEnc.normalize(pubKey))
}
addNode (pubKey, { description, shouldAnnounce = true, isPotentialOnly = false }) {
if (this.closing) return
let nodeEntry = this._getNodeEntry(pubKey)
if (!nodeEntry) {
const node = new RehosterNode({
nodeManager: this,
pubKey,
corestore: this.corestore,
shouldRehost: this.shouldRehost,
onInvalidKey: this.onInvalidKey,
onInvalidValue: this.onInvalidValue,
onNodeUpdate: this.onNodeUpdate,
onFullyDownloaded: this.onFullyDownloaded
})
node.on('error', err => {
this.emit('error', err)
})
nodeEntry = { node, refs: 0 }
this._setNodeEntry(pubKey, nodeEntry)
}
nodeEntry.refs++
const nodeRef = new RehosterNodeRef(nodeEntry.node, this, {
description,
shouldAnnounce,
isPotentialOnly
})
nodeRef.once('ready', () => {
if (isPotentialOnly) {
if (nodeRef.node.core.length === 0) {
// Hold off confirming until we know a core actually lives
// at this key
nodeRef.node.core.on('append', () => {
nodeRef.isPotentialOnly = false
this.onNewNode(nodeRef, nodeEntry.refs)
})
return
}
nodeRef.isPotentialOnly = false
}
this.onNewNode(nodeRef, nodeEntry.refs)
})
return nodeRef
}
// Only RehosterNodeRefs are allowed to call this method
_registerNodeRef (rehosterNodeRef) {
if (this.closing) return
this._nodeRefs.add(rehosterNodeRef)
}
// Only RehosterNodeRefs are allowed to call this method
_removeNode (rehosterNodeRef) {
if (this.closing) return
this._nodeRefs.delete(rehosterNodeRef)
const pubKey = rehosterNodeRef.node.pubKey
const nodeEntry = this._getNodeEntry(rehosterNodeRef.pubKey)
nodeEntry.refs--
if (nodeEntry.refs <= 0) {
this._deleteNodeEntry(pubKey)
nodeEntry.node.close()
.then(() => {
// TODO: there is an edge case, where a node was added
// as potentialOnly, but also as a normal node
// and the potentialOnly one is the last to go.
// In that case it won't call onNodeDeleted even though it should.
// But this shouldn't happen in practice, and the consequence
// is not serious, so not worth tracking the additional state for
if (!rehosterNodeRef.isPotentialOnly) {
this.onNodeDeleted(rehosterNodeRef, nodeEntry.refs)
}
})
.catch(safetyCatch)
} else {
this.onNodeDeleted(rehosterNodeRef, nodeEntry.refs)
}
}
}
class RehosterNodeRef extends ReadyResource {
constructor (node, nodeManager, { description, shouldAnnounce, isPotentialOnly }) {
super()
this.node = node
this.nodeManager = nodeManager
this.shouldAnnounce = shouldAnnounce
this.description = description
this.isPotentialOnly = isPotentialOnly
}
get pubKey () {
return this.node.pubKey
}
get core () {
return this.node.core
}
get discoveryKey () {
return this.core.discoveryKey
}
get swarmManager () {
return this.nodeManager.swarmManager
}
async _open () {
this.nodeManager._registerNodeRef(this)
await this.node.ready()
this._swarmOwnCore()
}
async _close () {
if (this.shouldAnnounce) {
await this.swarmManager.unserve(this.discoveryKey)
}
this.nodeManager._removeNode(this)
}
_swarmOwnCore () {
if (this.shouldAnnounce) {
this.swarmManager.serve(this.discoveryKey)
}
}
}
module.exports = RehosterNodeManager