| 'use strict' |
| |
| var transport = require('../spdy-transport') |
| var utils = transport.utils |
| |
| var assert = require('assert') |
| var debug = require('debug')('spdy:priority') |
| |
| function PriorityNode (tree, options) { |
| this.tree = tree |
| |
| this.id = options.id |
| this.parent = options.parent |
| this.weight = options.weight |
| |
| // To be calculated in `addChild` |
| this.priorityFrom = 0 |
| this.priorityTo = 1 |
| this.priority = 1 |
| |
| this.children = { |
| list: [], |
| weight: 0 |
| } |
| |
| if (this.parent !== null) { |
| this.parent.addChild(this) |
| } |
| } |
| |
| function compareChildren (a, b) { |
| return a.weight === b.weight ? a.id - b.id : a.weight - b.weight |
| } |
| |
| PriorityNode.prototype.toJSON = function toJSON () { |
| return { |
| parent: this.parent, |
| weight: this.weight, |
| exclusive: this.exclusive |
| } |
| } |
| |
| PriorityNode.prototype.getPriority = function getPriority () { |
| return this.priority |
| } |
| |
| PriorityNode.prototype.getPriorityRange = function getPriorityRange () { |
| return { from: this.priorityFrom, to: this.priorityTo } |
| } |
| |
| PriorityNode.prototype.addChild = function addChild (child) { |
| child.parent = this |
| utils.binaryInsert(this.children.list, child, compareChildren) |
| this.children.weight += child.weight |
| |
| this._updatePriority(this.priorityFrom, this.priorityTo) |
| } |
| |
| PriorityNode.prototype.remove = function remove () { |
| assert(this.parent, 'Can\'t remove root node') |
| |
| this.parent.removeChild(this) |
| this.tree._removeNode(this) |
| |
| // Move all children to the parent |
| for (var i = 0; i < this.children.list.length; i++) { |
| this.parent.addChild(this.children.list[i]) |
| } |
| } |
| |
| PriorityNode.prototype.removeChild = function removeChild (child) { |
| this.children.weight -= child.weight |
| var index = utils.binarySearch(this.children.list, child, compareChildren) |
| if (index !== -1 && this.children.list.length >= index) { |
| this.children.list.splice(index, 1) |
| } |
| } |
| |
| PriorityNode.prototype.removeChildren = function removeChildren () { |
| var children = this.children.list |
| this.children.list = [] |
| this.children.weight = 0 |
| return children |
| } |
| |
| PriorityNode.prototype._updatePriority = function _updatePriority (from, to) { |
| this.priority = to - from |
| this.priorityFrom = from |
| this.priorityTo = to |
| |
| var weight = 0 |
| for (var i = 0; i < this.children.list.length; i++) { |
| var node = this.children.list[i] |
| var nextWeight = weight + node.weight |
| |
| node._updatePriority( |
| from + this.priority * (weight / this.children.weight), |
| from + this.priority * (nextWeight / this.children.weight) |
| ) |
| weight = nextWeight |
| } |
| } |
| |
| function PriorityTree (options) { |
| this.map = {} |
| this.list = [] |
| this.defaultWeight = options.defaultWeight || 16 |
| |
| this.count = 0 |
| this.maxCount = options.maxCount |
| |
| // Root |
| this.root = this.add({ |
| id: 0, |
| parent: null, |
| weight: 1 |
| }) |
| } |
| module.exports = PriorityTree |
| |
| PriorityTree.create = function create (options) { |
| return new PriorityTree(options) |
| } |
| |
| PriorityTree.prototype.add = function add (options) { |
| if (options.id === options.parent) { |
| return this.addDefault(options.id) |
| } |
| |
| var parent = options.parent === null ? null : this.map[options.parent] |
| if (parent === undefined) { |
| return this.addDefault(options.id) |
| } |
| |
| debug('add node=%d parent=%d weight=%d exclusive=%d', |
| options.id, |
| options.parent === null ? -1 : options.parent, |
| options.weight || this.defaultWeight, |
| options.exclusive ? 1 : 0) |
| |
| var children |
| if (options.exclusive) { |
| children = parent.removeChildren() |
| } |
| |
| var node = new PriorityNode(this, { |
| id: options.id, |
| parent: parent, |
| weight: options.weight || this.defaultWeight |
| }) |
| this.map[options.id] = node |
| |
| if (options.exclusive) { |
| for (var i = 0; i < children.length; i++) { |
| node.addChild(children[i]) |
| } |
| } |
| |
| this.count++ |
| if (this.count > this.maxCount) { |
| debug('hit maximum remove id=%d', this.list[0].id) |
| this.list.shift().remove() |
| } |
| |
| // Root node is not subject to removal |
| if (node.parent !== null) { |
| this.list.push(node) |
| } |
| |
| return node |
| } |
| |
| // Only for testing, should use `node`'s methods |
| PriorityTree.prototype.get = function get (id) { |
| return this.map[id] |
| } |
| |
| PriorityTree.prototype.addDefault = function addDefault (id) { |
| debug('creating default node') |
| return this.add({ id: id, parent: 0, weight: this.defaultWeight }) |
| } |
| |
| PriorityTree.prototype._removeNode = function _removeNode (node) { |
| delete this.map[node.id] |
| var index = utils.binarySearch(this.list, node, compareChildren) |
| this.list.splice(index, 1) |
| this.count-- |
| } |