bucket.js

'use strict';

const constants = require('./constants');
const utils = require('./utils');


/**
 * @typedef {array} Bucket~contact
 * @property {string} 0 - Node identity key
 * @property {object} 1 - Contact information (varies by plugins)
 */

/**
 * Represents a column of the routing table holding up to K contacts
 */
class Bucket extends Map {

  /**
   * @constructor
   */
  constructor() {
    super();
  }

  /**
   * @property {number} length - The number of contacts in the bucket
   */
  get length() {
    return super.size;
  }

  /**
   * @property {object} head - The contact at the bucket head
   */
  get head() {
    return [...super.entries()].shift();
  }

  /**
   * @property {object} tail - The contact at the bucket tail
   */
  get tail() {
    return [...super.entries()].pop();
  }

  /**
   * Sets the contact to the node ID in the bucket if it is not full; if the
   * bucket already contains the contact, move it to the tail - otherwise we
   * place it at the head
   * @param {string} nodeId - The identity key for the contact
   * @param {object} contact - The address information for the contact
   * @returns {number} index
   */
  set(nodeId, contact) {
    if (this.has(nodeId)) {
      super.delete(nodeId);
      super.set(nodeId, contact);
    } else if (this.size < constants.K) {
      let bucketEntries = [...this.entries()];

      super.clear();
      super.set(nodeId, contact);

      for (let [nodeId, contact] of bucketEntries) {
        super.set(nodeId, contact);
      }
    }

    return this.indexOf(nodeId);
  }

  /**
   * Returns the index of the given node id
   * @param {string} key - Node identity key for getting index
   * @returns {number}
   */
  indexOf(key) {
    let isMissing = -1;
    let index = isMissing;

    for (let nodeId of this.keys()) {
      index++;

      if (key !== nodeId) {
        continue;
      }

      return index;
    }

    return isMissing;
  }

  /**
   * Returns an array of contacts in the bucket that are closest to the given
   * key
   * @param {string|buffer} key - Reference key for finding other contacts
   * @param {number} [count=constants.K] - Max results to return
   * @param {boolean} [exclusive=false] - Exclude result matching the key exactly
   * @returns {array}
   */
  getClosestToKey(key, count = constants.K, exclusive = false) {
    let contacts = [];

    for (let [identity, contact] of this.entries()) {
      contacts.push({
        contact, identity, distance: utils.getDistance(identity, key)
      });
    }

    return new Map(contacts.sort((a, b) => {
      return utils.compareKeyBuffers(
        Buffer.from(a.distance, 'hex'),
        Buffer.from(b.distance, 'hex')
      );
    }).filter((result) => {
      if (exclusive) {
        return result.identity !== key.toString('hex');
      } else {
        return true;
      }
    }).map((obj) => [obj.identity, obj.contact]).splice(0, count));
  }
}

module.exports = Bucket;