UNPKG

hypercore-rehoster

Version:

Help keep the hypercores of your choice available

210 lines (178 loc) 5.82 kB
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