dnssd
Version:
Bonjour/Avahi-like service discovery in pure JavaScript
405 lines (320 loc) • 13.1 kB
JavaScript
'use strict';
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
var EventEmitter = require('./EventEmitter');
var TimerContainer = require('./TimerContainer');
var filename = require('path').basename(__filename);
var debug = require('./debug')('dnssd:' + filename);
var ONE_SECOND = 1000;
/**
* @class
* @extends EventEmitter
*
* ExpiringRecordCollection is a set collection for resource records or
* query records. Uniqueness is determined by a record's hash property,
* which is a hash of a records name, type, class, and rdata. Records
* are evicted from the collection as their TTLs expire.
*
* Since there may be several records with the same name, type, and class,
* but different rdata, within a record set (e.g. PTR records for a service
* type), related records are tracked in this._related.
*
* This collection emits 'reissue' and 'expired' events as records TTLs
* decrease towards expiration. Reissues are emitted at 80%, 85%, 90% and 95%
* of each records TTL. Re-adding a record refreshes the TTL.
*
* @emits 'expired'
* @emits 'reissue'
*/
var ExpiringRecordCollection = function (_EventEmitter) {
_inherits(ExpiringRecordCollection, _EventEmitter);
/**
* @param {ResourceRecord[]} [records] - optional starting records
* @param {string} [description] - optional description for debugging
*/
function ExpiringRecordCollection(records, description) {
_classCallCheck(this, ExpiringRecordCollection);
// make debugging easier, who owns this / what it is
var _this = _possibleConstructorReturn(this, (ExpiringRecordCollection.__proto__ || Object.getPrototypeOf(ExpiringRecordCollection)).call(this));
_this._desc = description;
_this._records = {}; // record.hash: record
_this._related = {}; // record.namehash: Set() of record hashes
_this._insertionTime = {}; // record.hash: Date.now()
_this._timerContainers = {}; // record.hash: new TimerContainer()
_this.size = 0;
if (records) _this.addEach(records);
return _this;
}
/**
* Adds record. Re-added records refresh TTL expiration timers.
* @param {ResourceRecord} record
*/
_createClass(ExpiringRecordCollection, [{
key: 'add',
value: function add(record) {
var id = record.hash;
var group = record.namehash;
// expire TTL=0 goodbye records instead
if (record.ttl === 0) return this.setToExpire(record);
debug.v('#add(): %s', record);
debug.v(' to: ' + this._desc);
// only increment size if the record is new
if (!this._records[id]) this.size++;
// keep track of related records (same name, type, and class)
if (!this._related[group]) this._related[group] = new Set();
// remove any old timers
if (this._timerContainers[id]) this._timerContainers[id].clear();
this._records[id] = record;
this._related[group].add(id);
this._insertionTime[id] = Date.now();
this._timerContainers[id] = new TimerContainer();
// do reissue/expired timers
this._schedule(record);
}
}, {
key: 'addEach',
value: function addEach(records) {
var _this2 = this;
records.forEach(function (record) {
return _this2.add(record);
});
}
}, {
key: 'has',
value: function has(record) {
return Object.hasOwnProperty.call(this._records, record.hash);
}
/**
* Checks if a record was added to the collection within a given range
*
* @param {ResourceRecord} record
* @param {number} range - in *seconds*
* @return {boolean}
*/
}, {
key: 'hasAddedWithin',
value: function hasAddedWithin(record, range) {
var then = this._insertionTime[record.hash];
return Number(parseFloat(then)) === then && range * ONE_SECOND >= Date.now() - then;
}
/**
* Returns a *clone* of originally added record that matches requested record.
* The clone's TTL is reduced to the current TTL. A clone is used so the
* original record's TTL isn't modified.
*
* @param {ResourceRecord} record
* @return {ResourceRecord|undefined}
*/
}, {
key: 'get',
value: function get(record) {
if (!this.has(record)) return undefined;
var then = this._insertionTime[record.hash];
var elapsed = ~~((Date.now() - then) / ONE_SECOND);
var clone = record.clone();
clone.ttl -= elapsed;
return clone;
}
/**
* @emits 'expired' w/ the expiring record
*/
}, {
key: 'delete',
value: function _delete(record) {
if (!this.has(record)) return;
var id = record.hash;
var group = record.namehash;
this.size--;
this._timerContainers[id].clear();
delete this._records[id];
delete this._insertionTime[id];
delete this._timerContainers[id];
if (this._related[group]) this._related[group].delete(id);
debug.v('deleting: %s', record);
debug.v(' from: ' + this._desc);
this.emit('expired', record);
}
/**
* Deletes all records, clears all timers, resets size to 0
*/
}, {
key: 'clear',
value: function clear() {
debug.v('#clear()');
this.removeAllListeners();
Object.values(this._timerContainers).forEach(function (timers) {
return timers.clear();
});
this.size = 0;
this._records = {};
this._related = {};
this._insertionTime = {};
this._timerContainers = {};
}
/**
* Sets record to be deleted in 1s, but doesn't immediately delete it
*/
}, {
key: 'setToExpire',
value: function setToExpire(record) {
var _this3 = this;
// can't expire unknown records
if (!this.has(record)) return;
// don't reset expire timer if this gets called again, say due to
// repeated goodbyes. only one timer (expire) would be set in this case
if (this._timerContainers[record.hash].count() === 1) return;
debug.v('#setToExpire(): %s', record);
debug.v(' on: ' + this._desc);
this._timerContainers[record.hash].clear();
this._timerContainers[record.hash].set(function () {
return _this3.delete(record);
}, ONE_SECOND);
}
/**
* Flushes any other records that have the same name, class, and type
* from the collection *if* the records have been in the collection
* longer than 1s.
*/
}, {
key: 'flushRelated',
value: function flushRelated(record) {
var _this4 = this;
// only flush records that have cache-flush bit set
if (!record.isUnique) return;
this._getRelatedRecords(record.namehash).forEach(function (related) {
// can't flush itself
if (related.equals(record)) return;
// only flush records added more than 1s ago
if (!_this4.hasAddedWithin(related, 1)) _this4.setToExpire(related);
});
}
/**
* Records with original TTLs (not reduced ttl clones)
*/
}, {
key: 'toArray',
value: function toArray() {
return Object.values(this._records);
}
/**
* Checks if collection contains any other records with the same name, type,
* and class but different rdata. Non-unique records always return false & a
* record can't conflict with itself
*
* @param {ResourceRecord} record
* @return {boolean}
*/
}, {
key: 'hasConflictWith',
value: function hasConflictWith(record) {
if (!record.isUnique) return false;
return !!this._getRelatedRecords(record.namehash).filter(function (related) {
return !related.equals(record);
}).length;
}
/**
* Finds any records in collection that matches name, type, and class of a
* given query. Rejects any records with a TTL below the cutoff percentage.
* Returns clones of records to prevent changes to original objects.
*
* @param {QueryRecord} query
* @param {number} [cutoff] - percentage, 0.0 - 1.0
* @return {ResourceRecords[]}
*/
}, {
key: 'find',
value: function find(query) {
var cutoff = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0.25;
debug.v('#find(): "' + query.name + '" type: ' + query.qtype);
debug.v(' in: ' + this._desc);
return this._filterTTL(this._getRelatedRecords(query.namehash), cutoff);
}
/**
* Gets all any records in collection with a TTL above the cutoff percentage.
* Returns clones of records to prevent changes to original objects.
*
* @param {number} [cutoff] - percentage, 0.0 - 1.0
* @return {ResouceRecords[]}
*/
}, {
key: 'getAboveTTL',
value: function getAboveTTL() {
var cutoff = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0.25;
debug.v('#getAboveTTL(): %' + cutoff * 100);
return this._filterTTL(this.toArray(), cutoff);
}
/**
* Gets records that have same name, type, and class.
*/
}, {
key: '_getRelatedRecords',
value: function _getRelatedRecords(namehash) {
var _this5 = this;
return this._related[namehash] && this._related[namehash].size ? [].concat(_toConsumableArray(this._related[namehash])).map(function (id) {
return _this5._records[id];
}) : [];
}
/**
* Filters given records by their TTL.
* Returns clones of records to prevent changes to original objects.
*
* @param {ResouceRecords[]} records
* @param {number} cutoff - percentage, 0.0 - 1.0
* @return {ResouceRecords[]}
*/
}, {
key: '_filterTTL',
value: function _filterTTL(records, cutoff) {
var _this6 = this;
return records.reduce(function (result, record) {
var then = _this6._insertionTime[record.hash];
var elapsed = ~~((Date.now() - then) / ONE_SECOND);
var percent = (record.ttl - elapsed) / record.ttl;
debug.v('└── %s @ %d%', record, ~~(percent * 100));
if (percent >= cutoff) {
var clone = record.clone();
clone.ttl -= elapsed;
result.push(clone);
}
return result;
}, []);
}
/**
* Sets expiration/reissue timers for a record.
*
* Sets expiration at end of TTL.
* Sets reissue events at 80%, 85%, 90%, 95% of records TTL, plus a random
* extra 0-2%. (see rfc)
*
* @emits 'reissue' w/ the record that needs to be refreshed
*
* @param {ResouceRecords} record
*/
}, {
key: '_schedule',
value: function _schedule(record) {
var _this7 = this;
var id = record.hash;
var ttl = record.ttl * ONE_SECOND;
var expired = function expired() {
return _this7.delete(record);
};
var reissue = function reissue() {
return _this7.emit('reissue', record);
};
var random = function random(min, max) {
return Math.random() * (max - min) + min;
};
this._timerContainers[id].setLazy(reissue, ttl * random(0.80, 0.82));
this._timerContainers[id].setLazy(reissue, ttl * random(0.85, 0.87));
this._timerContainers[id].setLazy(reissue, ttl * random(0.90, 0.92));
this._timerContainers[id].setLazy(reissue, ttl * random(0.95, 0.97));
this._timerContainers[id].set(expired, ttl);
}
}]);
return ExpiringRecordCollection;
}(EventEmitter);
module.exports = ExpiringRecordCollection;