<commit_before>/**********************************/
/* Signalfire Javascript Library  */
/* Version: 0.0.2                 */
/*                                */
/* Copyright 2013 Travis Wimer    */
/* http://traviswimer.com         */
/*                                */
/* Released under the MIT license */
/**********************************/

var io = require('socket.io');

module.exports = function(){

	// Is used to give each peer a unique ID
	var peerIdCounter = 0;

	// create next ID and return it
	var incrementPeerIdCounter = function(){
		peerIdCounter++;
		return peerIdCounter;
	};

/////////////////
// Peer Object //
/*****************************************************************************/
	var createPeer = function(socket){

	////////////////////////////////
	// Peer Object Initialization //
	//////////////////////////////////////////////////
	//                                              //

		// The Peer object
		var peerObject = {
			'socket': socket
		};

		// listen for client sending offer
		peerObject.socket.on('clientSendingOffer', receiveOfferFromClient.bind(peerObject));

		// detect receiving an answer from clietn
		peerObject.socket.on('clientSendingAnswer', receiveAnswerFromClient.bind(peerObject));

		// listen for ice candidates
		peerObject.socket.on('clientSendingIce', receiveIceCandidate.bind(peerObject));

	//                                              //
	//////////////////////////////////////////////////

	/////////////////////////////////////
	// Peer Object's Private Variables //
	//////////////////////////////////////////////////
	//                                              //

		var id = incrementPeerIdCounter();

		// holds the peerConnect offer for this peer
		var offer;

		// list of peers that can connect
		var connectingPeers = {};

	//                                              //
	//////////////////////////////////////////////////

	//////////////////////////////////
	// Peer Object's Public Methods //
	//////////////////////////////////////////////////
	//                                              //
		// Initiates signaling process
		peerObject.connectToPeer = function(peer){
			// answering peer starts waiting for an offer
			peer.acceptOfferFrom(this);

			// This peer will make an offer
			this.makeOfferRequest(peer);
		};

		// Request a peerConnection offer from the current peer
		peerObject.makeOfferRequest = function(peer){

			// add peer to list of accepted requests
			connectingPeers[peer.getPeerId()] = peer;

			// If the peer disconnects, remove them
			removePeerOnDisconnect(peer);

			// ask client for an offer
			this.socket.emit('serverRequestingOffer', {peerId:peer.getPeerId()});
		};

		// Accept peerConnection offers from the specified peer
		peerObject.acceptOfferFrom = function(peer){

			// add peer to list of accepted requests
			connectingPeers[peer.getPeerId()] = peer;

			// If the peer disconnects, remove them
			removePeerOnDisconnect(peer);

		};

		// getter for the peer ID
		peerObject.getPeerId = function(){
			return id;
		};

		// getter for the peer offer
		peerObject.getPeerOffer = function(){
			return offer;
		};

	//                                              //
	//////////////////////////////////////////////////

	///////////////////////////////////
	// Peer Object's Private Methods //
	//////////////////////////////////////////////////
	//                                              //

		// Handle an offer sent from the client
		function receiveOfferFromClient(data){
			offer = data.offer;

			sendOfferToClient.call(this, data.peerId);
		}

		// Send offer to the other peer
		function sendOfferToClient(peerId){

			// If peer exists, send offer
			if(connectingPeers[peerId]){
				connectingPeers[peerId].socket.emit(
					'serverSendingOffer',
					{
						peerId: this.getPeerId(),
						offer: offer
					}
				);
			}
		}

		// Send answer to initiating peer
		function receiveAnswerFromClient(data){
			var peerId = data.peerId;
			data.peerId = this.getPeerId();

			// If peer exists, send answer
			if(connectingPeers[peerId]){
				connectingPeers[peerId].socket.emit('serverSendingAnswer', data);
			}
		}

		//Handle ICE candidates received
		function receiveIceCandidate(data){
			var iceInfo = {
				peerId: this.getPeerId(),
				candidate: data.candidate
			};

			// If peer exists, send ICE candidate
			if(connectingPeers[data.peerId]){
				connectingPeers[data.peerId].socket.emit('serverSendingIce', iceInfo);
			}
		}

		// Removes peers that disconnect from the list
		function removePeerOnDisconnect(peer){
			peer.socket.on('disconnect', function () {
				var remotePeerId=peer.getPeerId();
				if(connectingPeers[remotePeerId]){
					delete connectingPeers[remotePeerId];
				}
			});
		}

	//                                              //
	//////////////////////////////////////////////////

		// Return an instance of the Peer class
		return peerObject;
	};

/*____________________________________________________________________________*/

/////////////////////////////
// Module's Public Methods //
/*****************************************************************************/

	// Initiates listening for signaling requests
	///////////////////////////////////////////////
	var listen = function(port, successCallback, failCallback){
		if(typeof port != 'number'){
			return false;
		}

		// Handle socket requests
		/////////////////////////////
		var server = io.listen(port);

		server.sockets.on('connection', function(socket){
			var peer;
			try{
				peer = createPeer(socket);
			}catch(e){
				failCallback(e);
			}finally{
				successCallback(peer);
			}
		});

		return server;
	};

	// Return the public methods
	return {
		listen:listen
	};

/*____________________________________________________________________________*/

}();<commit_msg>Providing port that is not a number will call the failback<commit_after>/**********************************/
/* Signalfire Javascript Library  */
/* Version: 0.0.2                 */
/*                                */
/* Copyright 2013 Travis Wimer    */
/* http://traviswimer.com         */
/*                                */
/* Released under the MIT license */
/**********************************/

var io = require('socket.io');

module.exports = function(){

	// Is used to give each peer a unique ID
	var peerIdCounter = 0;

	// create next ID and return it
	var incrementPeerIdCounter = function(){
		peerIdCounter++;
		return peerIdCounter;
	};

/////////////////
// Peer Object //
/*****************************************************************************/
	var createPeer = function(socket){

	////////////////////////////////
	// Peer Object Initialization //
	//////////////////////////////////////////////////
	//                                              //

		// The Peer object
		var peerObject = {
			'socket': socket
		};

		// listen for client sending offer
		peerObject.socket.on('clientSendingOffer', receiveOfferFromClient.bind(peerObject));

		// detect receiving an answer from clietn
		peerObject.socket.on('clientSendingAnswer', receiveAnswerFromClient.bind(peerObject));

		// listen for ice candidates
		peerObject.socket.on('clientSendingIce', receiveIceCandidate.bind(peerObject));

	//                                              //
	//////////////////////////////////////////////////

	/////////////////////////////////////
	// Peer Object's Private Variables //
	//////////////////////////////////////////////////
	//                                              //

		var id = incrementPeerIdCounter();

		// holds the peerConnect offer for this peer
		var offer;

		// list of peers that can connect
		var connectingPeers = {};

	//                                              //
	//////////////////////////////////////////////////

	//////////////////////////////////
	// Peer Object's Public Methods //
	//////////////////////////////////////////////////
	//                                              //
		// Initiates signaling process
		peerObject.connectToPeer = function(peer){
			// answering peer starts waiting for an offer
			peer.acceptOfferFrom(this);

			// This peer will make an offer
			this.makeOfferRequest(peer);
		};

		// Request a peerConnection offer from the current peer
		peerObject.makeOfferRequest = function(peer){

			// add peer to list of accepted requests
			connectingPeers[peer.getPeerId()] = peer;

			// If the peer disconnects, remove them
			removePeerOnDisconnect(peer);

			// ask client for an offer
			this.socket.emit('serverRequestingOffer', {peerId:peer.getPeerId()});
		};

		// Accept peerConnection offers from the specified peer
		peerObject.acceptOfferFrom = function(peer){

			// add peer to list of accepted requests
			connectingPeers[peer.getPeerId()] = peer;

			// If the peer disconnects, remove them
			removePeerOnDisconnect(peer);

		};

		// getter for the peer ID
		peerObject.getPeerId = function(){
			return id;
		};

		// getter for the peer offer
		peerObject.getPeerOffer = function(){
			return offer;
		};

	//                                              //
	//////////////////////////////////////////////////

	///////////////////////////////////
	// Peer Object's Private Methods //
	//////////////////////////////////////////////////
	//                                              //

		// Handle an offer sent from the client
		function receiveOfferFromClient(data){
			offer = data.offer;

			sendOfferToClient.call(this, data.peerId);
		}

		// Send offer to the other peer
		function sendOfferToClient(peerId){

			// If peer exists, send offer
			if(connectingPeers[peerId]){
				connectingPeers[peerId].socket.emit(
					'serverSendingOffer',
					{
						peerId: this.getPeerId(),
						offer: offer
					}
				);
			}
		}

		// Send answer to initiating peer
		function receiveAnswerFromClient(data){
			var peerId = data.peerId;
			data.peerId = this.getPeerId();

			// If peer exists, send answer
			if(connectingPeers[peerId]){
				connectingPeers[peerId].socket.emit('serverSendingAnswer', data);
			}
		}

		//Handle ICE candidates received
		function receiveIceCandidate(data){
			var iceInfo = {
				peerId: this.getPeerId(),
				candidate: data.candidate
			};

			// If peer exists, send ICE candidate
			if(connectingPeers[data.peerId]){
				connectingPeers[data.peerId].socket.emit('serverSendingIce', iceInfo);
			}
		}

		// Removes peers that disconnect from the list
		function removePeerOnDisconnect(peer){
			peer.socket.on('disconnect', function () {
				var remotePeerId=peer.getPeerId();
				if(connectingPeers[remotePeerId]){
					delete connectingPeers[remotePeerId];
				}
			});
		}

	//                                              //
	//////////////////////////////////////////////////

		// Return an instance of the Peer class
		return peerObject;
	};

/*____________________________________________________________________________*/

/////////////////////////////
// Module's Public Methods //
/*****************************************************************************/

	// Initiates listening for signaling requests
	///////////////////////////////////////////////
	var listen = function(port, successCallback, failCallback){
		if(typeof port != 'number'){
			failCallback("invalid port number");
		}

		// Handle socket requests
		/////////////////////////////
		var server = io.listen(port);

		server.sockets.on('connection', function(socket){
			var peer;
			try{
				peer = createPeer(socket);
			}catch(e){
				failCallback(e);
			}finally{
				successCallback(peer);
			}
		});

		return server;
	};

	// Return the public methods
	return {
		listen:listen
	};

/*____________________________________________________________________________*/

}();<|endoftext|>


<commit_before>"use strict";

var _ = require("lodash"),
    accounting = require("accounting"),
    logger = require("./logger")();

// Represents the various categories of an online item
exports.categories = Object.freeze({
    DIGITAL_MUSIC: "Digital Music",
    MUSIC: "Music",
    VIDEO_GAMES: "Video Games",
    MOBILE_APPS: "Mobile Apps",
    MOVIES_TV: "Movies & TV",
    CAMERA_VIDEO: "Camera & Video",
    TOYS_GAMES: "Toys & Games",
    BOOKS: "Books",
    KINDLE_BOOKS: "Kindle Books",
    HOUSEHOLD: "Household",
    HEALTH_PERSONAL_CARE: "Health & Personal Care",
    TELEVISION_VIDEO: "Television & Video",
    HOME_AUDIO: "Home Audio",
    LUGGAGE: "Luggage",
    OTHER: "Other"
});

/**
 * Finds content on a page, returning either the text or null.
 *
 * @param  {Object} $         jQuery-like object
 * @param  {Array} selectors  An array of selectors to search with
 * @return {String}           The content found (or null)
 */
exports.findContentOnPage = function($, selectors) {
    var i, content;

    logger.log("selectors: " + selectors);

    // loop until we find the content, or we exhaust our selectors
    for (i = 0; i < selectors.length; i++) {
        content = $(selectors[i]);
        if (!_.isEmpty(content)) {
            logger.log("found content with selector: " + selectors[i]);
            return content.text().trim();
        }
    }

    // if we've not found anything, return null to signify that
    return null;
};

var DOLLAR_PREFIX = "$";
var EURO_TEXT_PREFIX = "eur";
var EURO_SYMBOL_PREFIX = "€";
var YEN_TEXT_PREFIX = "円";
var YEN_SYMBOL_PREFIX = "￥";

exports.processPrice = function(priceString) {
    var price;

    priceString = priceString.toLowerCase();

    logger.log("price string (lowercased): " + priceString);

    // currency specific processing
    if (_.includes(priceString, DOLLAR_PREFIX)) {
        logger.log("found dollar in price, converting to number...");
        price = accounting.unformat(priceString);
    } else if (_.includes(priceString, EURO_TEXT_PREFIX) ||
        _.includes(priceString, EURO_SYMBOL_PREFIX)) {
        logger.log("found euro in price, converting to number...");
        price = accounting.unformat(priceString, ",");
    } else if (_.includes(priceString, YEN_TEXT_PREFIX) ||
        _.includes(priceString, YEN_SYMBOL_PREFIX)) {
        logger.log("found yen in price, converting to number...");
        price = accounting.unformat(priceString);
    } else {
        // unknown price type!!
        logger.error("unknown price type, unable to process, returning -1");
        price = -1;
    }

    logger.log("price (post-process): " + price);
    return price;
};
<commit_msg>Add support for GBP prices<commit_after>"use strict";

var _ = require("lodash"),
    accounting = require("accounting"),
    logger = require("./logger")();

// Represents the various categories of an online item
exports.categories = Object.freeze({
    DIGITAL_MUSIC: "Digital Music",
    MUSIC: "Music",
    VIDEO_GAMES: "Video Games",
    MOBILE_APPS: "Mobile Apps",
    MOVIES_TV: "Movies & TV",
    CAMERA_VIDEO: "Camera & Video",
    TOYS_GAMES: "Toys & Games",
    BOOKS: "Books",
    KINDLE_BOOKS: "Kindle Books",
    HOUSEHOLD: "Household",
    HEALTH_PERSONAL_CARE: "Health & Personal Care",
    TELEVISION_VIDEO: "Television & Video",
    HOME_AUDIO: "Home Audio",
    LUGGAGE: "Luggage",
    OTHER: "Other"
});

/**
 * Finds content on a page, returning either the text or null.
 *
 * @param  {Object} $         jQuery-like object
 * @param  {Array} selectors  An array of selectors to search with
 * @return {String}           The content found (or null)
 */
exports.findContentOnPage = function($, selectors) {
    var i, content;

    logger.log("selectors: " + selectors);

    // loop until we find the content, or we exhaust our selectors
    for (i = 0; i < selectors.length; i++) {
        content = $(selectors[i]);
        if (!_.isEmpty(content)) {
            logger.log("found content with selector: " + selectors[i]);
            return content.text().trim();
        }
    }

    // if we've not found anything, return null to signify that
    return null;
};

var DOLLAR_PREFIX = "$";
var EURO_TEXT_PREFIX = "eur";
var EURO_SYMBOL_PREFIX = "€";
var YEN_TEXT_PREFIX = "円";
var YEN_SYMBOL_PREFIX = "￥";
var GBP_SYMBOL_PREFIX = "£";

exports.processPrice = function(priceString) {
    var price;

    priceString = priceString.toLowerCase();

    logger.log("price string (lowercased): " + priceString);

    // currency specific processing
    if (_.includes(priceString, DOLLAR_PREFIX)) {
        logger.log("found dollar in price, converting to number...");
        price = accounting.unformat(priceString);
    } else if (_.includes(priceString, EURO_TEXT_PREFIX) ||
        _.includes(priceString, EURO_SYMBOL_PREFIX)) {
        logger.log("found euro in price, converting to number...");
        price = accounting.unformat(priceString, ",");
    } else if (_.includes(priceString, YEN_TEXT_PREFIX) ||
        _.includes(priceString, YEN_SYMBOL_PREFIX)) {
        logger.log("found yen in price, converting to number...");
        price = accounting.unformat(priceString);
    } else if (_.includes(priceString, GBP_SYMBOL_PREFIX) ||
        _.includes(priceString, GBP_SYMBOL_PREFIX)) {
        logger.log("found gbp in price, converting to number...");
        price = accounting.unformat(priceString);
    } else {
        // unknown price type!!
        logger.error("unknown price type, unable to process, returning -1");
        price = -1;
    }

    logger.log("price (post-process): " + price);
    return price;
};
<|endoftext|>


<commit_before>var vscode = require('vscode');

function sortActiveSelection(algorithm, removeDuplicateValues) {
  var textEditor = vscode.window.activeTextEditor;
  var selection = textEditor.selection;
  if (selection.isSingleLine) {
    return;
  }
  sortLines(textEditor, selection.start.line, selection.end.line, algorithm, removeDuplicateValues);
}

function sortLines(textEditor, startLine, endLine, algorithm, removeDuplicateValues) {
  var lines = [];
  for (var i = startLine; i <= endLine; i++) {
    lines.push(textEditor.document.lineAt(i).text);
  }
  lines.sort(algorithm);

  if (removeDuplicateValues) {
    lines = getUniqueArray(lines);
  }

  textEditor.edit(function (editBuilder) {
    var range = new vscode.Range(startLine, 0, endLine, textEditor.document.lineAt(endLine).text.length);
    editBuilder.replace(range, lines.join("\n"));
  });
}

function getUniqueArray(lines) {
  var unique = [];
  for (var i = 0; i < lines.length; ++i) {
    if (unique.length === 0 || (unique[unique.length - 1] !== lines[i])) {
      unique.push(lines[i]);
    }
  }
  return unique;
}

function reverseCompare(a, b) {
  return a < b ? 1 : -1;
}

function caseInsensitiveCompare(a, b) {
  return a.localeCompare(b, {sensitivity: 'base'});
}

function lineLengthCompare(a, b) {
  return a.length > b.length ? 1 : -1;
}

function lineLengthReverseCompare(a, b) {
  return a.length > b.length ? -1 : 1;
}

var intlCollator = new Intl.Collator(undefined, {numeric:true});
function naturalCompare(a, b) {
  return intlCollator.compare(a, b);
}

function shuffleCompare(a, b) {
  return Math.random() > 0.5 ? 1 : -1;
}

exports.sortNormal = sortActiveSelection.bind(null, undefined, false);
exports.sortReverse = sortActiveSelection.bind(null, reverseCompare, false);
exports.sortCaseInsensitive = sortActiveSelection.bind(null, caseInsensitiveCompare, false);
exports.sortLineLength = sortActiveSelection.bind(null, lineLengthCompare, false);
exports.sortLineLengthReverse = sortActiveSelection.bind(null, lineLengthReverseCompare, false);
exports.sortNatural = sortActiveSelection.bind(null, naturalCompare, false);
exports.sortUnique = sortActiveSelection.bind(null, undefined, true);
exports.sortShuffle = sortActiveSelection.bind(null, shuffleCompare, false);
<commit_msg>Lazy loaded Intl.Collator<commit_after>var vscode = require('vscode');

function sortActiveSelection(algorithm, removeDuplicateValues) {
  var textEditor = vscode.window.activeTextEditor;
  var selection = textEditor.selection;
  if (selection.isSingleLine) {
    return;
  }
  sortLines(textEditor, selection.start.line, selection.end.line, algorithm, removeDuplicateValues);
}

function sortLines(textEditor, startLine, endLine, algorithm, removeDuplicateValues) {
  var lines = [];
  for (var i = startLine; i <= endLine; i++) {
    lines.push(textEditor.document.lineAt(i).text);
  }
  lines.sort(algorithm);

  if (removeDuplicateValues) {
    lines = getUniqueArray(lines);
  }

  textEditor.edit(function (editBuilder) {
    var range = new vscode.Range(startLine, 0, endLine, textEditor.document.lineAt(endLine).text.length);
    editBuilder.replace(range, lines.join("\n"));
  });
}

function getUniqueArray(lines) {
  var unique = [];
  for (var i = 0; i < lines.length; ++i) {
    if (unique.length === 0 || (unique[unique.length - 1] !== lines[i])) {
      unique.push(lines[i]);
    }
  }
  return unique;
}

function reverseCompare(a, b) {
  return a < b ? 1 : -1;
}

function caseInsensitiveCompare(a, b) {
  return a.localeCompare(b, {sensitivity: 'base'});
}

function lineLengthCompare(a, b) {
  return a.length > b.length ? 1 : -1;
}

function lineLengthReverseCompare(a, b) {
  return a.length > b.length ? -1 : 1;
}

var intlCollator;
function naturalCompare(a, b) {
  if (!intlCollator) {
    intlCollator = new Intl.Collator(undefined, {numeric:true});
  }
  return intlCollator.compare(a, b);
}

function shuffleCompare(a, b) {
  return Math.random() > 0.5 ? 1 : -1;
}

exports.sortNormal = sortActiveSelection.bind(null, undefined, false);
exports.sortReverse = sortActiveSelection.bind(null, reverseCompare, false);
exports.sortCaseInsensitive = sortActiveSelection.bind(null, caseInsensitiveCompare, false);
exports.sortLineLength = sortActiveSelection.bind(null, lineLengthCompare, false);
exports.sortLineLengthReverse = sortActiveSelection.bind(null, lineLengthReverseCompare, false);
exports.sortNatural = sortActiveSelection.bind(null, naturalCompare, false);
exports.sortUnique = sortActiveSelection.bind(null, undefined, true);
exports.sortShuffle = sortActiveSelection.bind(null, shuffleCompare, false);
<|endoftext|>


<commit_before><commit_msg>reordered<commit_after>/* FileSaver.js
 * A saveAs() FileSaver implementation.
 * 1.1.20150716
 *
 * By Eli Grey, http://eligrey.com
 * License: X11/MIT
 *   See https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md
 */

/*global self */
/*jslint bitwise: true, indent: 4, laxbreak: true, laxcomma: true, smarttabs: true, plusplus: true */

/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */

var saveAs = saveAs || (function(view) {
    "use strict";
    // IE <10 is explicitly unsupported
    if (typeof navigator !== "undefined" && /MSIE [1-9]\./.test(navigator.userAgent)) {
        return;
    }
    var
          doc = view.document
          // only get URL when necessary in case Blob.js hasn't overridden it yet
        , get_URL = function() {
            return view.URL || view.webkitURL || view;
        }
        , save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a")
        , can_use_save_link = "download" in save_link
        , click = function(node) {
            var event = new MouseEvent("click");
            node.dispatchEvent(event);
        }
        , webkit_req_fs = view.webkitRequestFileSystem
        , req_fs = view.requestFileSystem || webkit_req_fs || view.mozRequestFileSystem
        , throw_outside = function(ex) {
            (view.setImmediate || view.setTimeout)(function() {
                throw ex;
            }, 0);
        }
        , force_saveable_type = "application/octet-stream"
        , fs_min_size = 0
        // See https://code.google.com/p/chromium/issues/detail?id=375297#c7 and
        // https://github.com/eligrey/FileSaver.js/commit/485930a#commitcomment-8768047
        // for the reasoning behind the timeout and revocation flow
        , arbitrary_revoke_timeout = 500 // in ms
        , revoke = function(file) {
            var revoker = function() {
                if (typeof file === "string") { // file is an object URL
                    get_URL().revokeObjectURL(file);
                } else { // file is a File
                    file.remove();
                }
            };
            if (view.chrome) {
                revoker();
            } else {
                setTimeout(revoker, arbitrary_revoke_timeout);
            }
        }
        , dispatch = function(filesaver, event_types, event) {
            event_types = [].concat(event_types);
            var i = event_types.length;
            while (i--) {
                var listener = filesaver["on" + event_types[i]];
                if (typeof listener === "function") {
                    try {
                        listener.call(filesaver, event || filesaver);
                    } catch (ex) {
                        throw_outside(ex);
                    }
                }
            }
        }
        , auto_bom = function(blob) {
            // prepend BOM for UTF-8 XML and text/* types (including HTML)
            if (/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) {
                return new Blob(["\ufeff", blob], {type: blob.type});
            }
            return blob;
        }
        , FileSaver = function(blob, name, no_auto_bom) {
            if (!no_auto_bom) {
                blob = auto_bom(blob);
            }
            // First try a.download, then web filesystem, then object URLs
            var
                  filesaver = this
                , type = blob.type
                , blob_changed = false
                , object_url
                , target_view
                , dispatch_all = function() {
                    dispatch(filesaver, "writestart progress write writeend".split(" "));
                }
                // on any filesys errors revert to saving with object URLs
                , fs_error = function() {
                    // don't create more object URLs than needed
                    if (blob_changed || !object_url) {
                        object_url = get_URL().createObjectURL(blob);
                    }
                    if (target_view) {
                        target_view.location.href = object_url;
                    } else {
                        var new_tab = view.open(object_url, "_blank");
                        if (new_tab == undefined && typeof safari !== "undefined") {
                            //Apple do not allow window.open, see http://bit.ly/1kZffRI
                            view.location.href = object_url
                        }
                    }
                    filesaver.readyState = filesaver.DONE;
                    dispatch_all();
                    revoke(object_url);
                }
                , abortable = function(func) {
                    return function() {
                        if (filesaver.readyState !== filesaver.DONE) {
                            return func.apply(this, arguments);
                        }
                    };
                }
                , create_if_not_found = {create: true, exclusive: false}
                , slice
            ;
            filesaver.readyState = filesaver.INIT;
            if (!name) {
                name = "download";
            }
            if (can_use_save_link) {
                object_url = get_URL().createObjectURL(blob);
                save_link.href = object_url;
                save_link.download = name;
                setTimeout(function() {
                    click(save_link);
                    dispatch_all();
                    revoke(object_url);
                    filesaver.readyState = filesaver.DONE;
                });
                return;
            }
            // Object and web filesystem URLs have a problem saving in Google Chrome when
            // viewed in a tab, so I force save with application/octet-stream
            // http://code.google.com/p/chromium/issues/detail?id=91158
            // Update: Google errantly closed 91158, I submitted it again:
            // https://code.google.com/p/chromium/issues/detail?id=389642
            if (view.chrome && type && type !== force_saveable_type) {
                slice = blob.slice || blob.webkitSlice;
                blob = slice.call(blob, 0, blob.size, force_saveable_type);
                blob_changed = true;
            }
            // Since I can't be sure that the guessed media type will trigger a download
            // in WebKit, I append .download to the filename.
            // https://bugs.webkit.org/show_bug.cgi?id=65440
            if (webkit_req_fs && name !== "download") {
                name += ".download";
            }
            if (type === force_saveable_type || webkit_req_fs) {
                target_view = view;
            }
            if (!req_fs) {
                fs_error();
                return;
            }
            fs_min_size += blob.size;
            req_fs(view.TEMPORARY, fs_min_size, abortable(function(fs) {
                fs.root.getDirectory("saved", create_if_not_found, abortable(function(dir) {
                    var save = function() {
                        dir.getFile(name, create_if_not_found, abortable(function(file) {
                            file.createWriter(abortable(function(writer) {
                                writer.onwriteend = function(event) {
                                    target_view.location.href = file.toURL();
                                    filesaver.readyState = filesaver.DONE;
                                    dispatch(filesaver, "writeend", event);
                                    revoke(file);
                                };
                                writer.onerror = function() {
                                    var error = writer.error;
                                    if (error.code !== error.ABORT_ERR) {
                                        fs_error();
                                    }
                                };
                                "writestart progress write abort".split(" ").forEach(function(event) {
                                    writer["on" + event] = filesaver["on" + event];
                                });
                                writer.write(blob);
                                filesaver.abort = function() {
                                    writer.abort();
                                    filesaver.readyState = filesaver.DONE;
                                };
                                filesaver.readyState = filesaver.WRITING;
                            }), fs_error);
                        }), fs_error);
                    };
                    dir.getFile(name, {create: false}, abortable(function(file) {
                        // delete file if it already exists
                        file.remove();
                        save();
                    }), abortable(function(ex) {
                        if (ex.code === ex.NOT_FOUND_ERR) {
                            save();
                        } else {
                            fs_error();
                        }
                    }));
                }), fs_error);
            }), fs_error);
        }
        , FS_proto = FileSaver.prototype
        , saveAs = function(blob, name, no_auto_bom) {
            return new FileSaver(blob, name, no_auto_bom);
        }
    ;
    // IE 10+ (native saveAs)
    if (typeof navigator !== "undefined" && navigator.msSaveOrOpenBlob) {
        return function(blob, name, no_auto_bom) {
            if (!no_auto_bom) {
                blob = auto_bom(blob);
            }
            return navigator.msSaveOrOpenBlob(blob, name || "download");
        };
    }

    FS_proto.abort = function() {
        var filesaver = this;
        filesaver.readyState = filesaver.DONE;
        dispatch(filesaver, "abort");
    };
    FS_proto.readyState = FS_proto.INIT = 0;
    FS_proto.WRITING = 1;
    FS_proto.DONE = 2;

    FS_proto.error =
    FS_proto.onwritestart =
    FS_proto.onprogress =
    FS_proto.onwrite =
    FS_proto.onabort =
    FS_proto.onerror =
    FS_proto.onwriteend =
        null;

    return saveAs;
}(
       typeof self !== "undefined" && self
    || typeof window !== "undefined" && window
    || this.content
));
// `self` is undefined in Firefox for Android content script context
// while `this` is nsIContentFrameMessageManager
// with an attribute `content` that corresponds to the window

if (typeof module !== "undefined" && module.exports) {
  module.exports.saveAs = saveAs;
} else if ((typeof define !== "undefined" && define !== null) && (define.amd != null)) {
  define([], function() {
    return saveAs;
  });
}<|endoftext|>


<commit_before>'use strict';
/*jslint node: true, stupid: true */

var fs = require('fs');
var mustache = require('mustache');
var yaml = require('js-yaml');
var colors = require('colors');
var marked = require('./markdown_renderer');
var init = require('./holograph_init');
var extend = require('extend');

function showError(message) {
    if (message) {
        console.log(message);
        console.log('Build failed (╯°□°）╯︵ ┻━┻)'.red);
        process.exit(1);
    }
}

function readFile(file) {
    try {
        var comment = fs.readFileSync(file, 'utf8');
    } catch(err) {
        showError(err.message);
    }
    return comment;
}

function extractPalette(source, config) {
    try {
        var template = fs.readFileSync(config.documentation_assets + '/_swatches.html', 'utf8');
    } catch (err) {
        return;
    }

    // fetch palettes
    var match;
    var palettes = {};
    var pattern = /^(.*?)[:=](.*?);?\s*\/\/\s*hg-palette:(.*?)$/mg;
    while ((match = pattern.exec(source)) !== null) {
        var paletteName = match[3].trim();
        var colourVariable = match[1].trim();
        var colourValue = match[2].trim();
        if (!palettes[paletteName]) {
            palettes[paletteName] = [];
        }
        palettes[paletteName].push({
            'name': colourVariable,
            'value': colourValue
        });
    }

    // convert to array
    var paletteList = [];
    for (var palette in palettes) {
        paletteList.push({
            'title': palette,
            'colours': palettes[palette]
        });
    };

    // create markup
    var content = mustache.render(template, {
        palettes: paletteList
    });
    return content;
}

function prepareCategories(results, config) {
    var doc = /\/\*doc\w*\n([\s\S]*?)\*\//mg;
    var pages = {};

    results.forEach(function(file) {
        var text = readFile(file);
        var matches = [];
        while (matches = doc.exec(text)) {
            var content = marked(matches[1]);
            if (!(pages.hasOwnProperty(content.meta.category))) {
                pages[content.meta.category] = [];
            }
            content.html += extractPalette(text, config);
            pages[content.meta.category].push(content);
        }
    });
    return pages;
}

function preparePageLinks(current, pages, index_title) {
    var links = [{
        link: 'index.html',
        title: index_title,
        selected: current === 'index' ? 'selected' : ''
    }];
    var category;

    for (category in pages) {
        if (pages.hasOwnProperty(category)) {
            links.push({
                link: category + '.html',
                title: category,
                selected: category === current ? 'selected' : ''
            });
        }
    }

    return links;
}

function generatePage(config, page) {
    var blocks = [];
    var rawContent = fs.readFileSync(config.documentation_assets + '/_header.html', 'utf8');

    page.forEach(function (block) {
        rawContent += '
'+block.meta.title+'
';
        rawContent += block.html;

        blocks.push({
            name: block.meta.name,
            title: block.meta.title
        });
    });

    rawContent += fs.readFileSync(config.documentation_assets +'/_footer.html', 'utf8');

    return {
        content: rawContent,
        blocks: blocks
    }
}

function processFiles(results, config, cb) {
    var pages = prepareCategories(results, config);
    var category, content;

    for (category in pages) {
        if (pages.hasOwnProperty(category)) {
            content = generatePage(config, pages[category])
            fs.writeFile(
                config.destination + '/' + category + '.html',
                mustache.render(
                    content.content,
                    {
                        title: category,
                        config: config,
                        categories: preparePageLinks(category, pages, config.index_title),
                        blocks: content.blocks
                    }
                )
            );

            if (category === config.index) {
                fs.writeFile(
                    config.destination + '/index.html',
                    mustache.render(
                        content.content,
                        {
                            title: 'index',
                            config: config,
                            categories: preparePageLinks('index', pages, config.index_title),
                            blocks: content.blocks
                        }
                    )
                )
            }
        }
    }

    if (cb) cb();
}

function maybeThrowError(err) {
    if (err) { throw err; }
}

function holograph(config, callback) {
    init(config, function(err, results) {
        if(err) { callback(err); }
        processFiles(results, config, callback);
    });
}

function run() {
    var config = {
        index_title: 'Home'
    };

    fs.readFile('holograph_config.yml', 'utf8',
        function(err, data) {
            if (err) {
                if (err.code === 'ENOENT') {
                    showError('Could not find holograph_config.yml in the current directory.');
                }
            }

            holograph(extend(config, yaml.safeLoad(data)), function(err, result) {
                showError(err);
                console.log('Build successful \\o\/'.green);
            });
        }
    );
}

module.exports = {
    holograph: holograph,
    run: run,
    prepareCategories: prepareCategories
};
<commit_msg>Improve URLs<commit_after>'use strict';
/*jslint node: true, stupid: true */

var fs = require('fs');
var mustache = require('mustache');
var yaml = require('js-yaml');
var colors = require('colors');
var marked = require('./markdown_renderer');
var init = require('./holograph_init');
var extend = require('extend');

function showError(message) {
    if (message) {
        console.log(message);
        console.log('Build failed (╯°□°）╯︵ ┻━┻)'.red);
        process.exit(1);
    }
}

function readFile(file) {
    try {
        var comment = fs.readFileSync(file, 'utf8');
    } catch(err) {
        showError(err.message);
    }
    return comment;
}

function extractPalette(source, config) {
    try {
        var template = fs.readFileSync(config.documentation_assets + '/_swatches.html', 'utf8');
    } catch (err) {
        return;
    }

    // fetch palettes
    var match;
    var palettes = {};
    var pattern = /^(.*?)[:=](.*?);?\s*\/\/\s*hg-palette:(.*?)$/mg;
    while ((match = pattern.exec(source)) !== null) {
        var paletteName = match[3].trim();
        var colourVariable = match[1].trim();
        var colourValue = match[2].trim();
        if (!palettes[paletteName]) {
            palettes[paletteName] = [];
        }
        palettes[paletteName].push({
            'name': colourVariable,
            'value': colourValue
        });
    }

    // convert to array
    var paletteList = [];
    for (var palette in palettes) {
        paletteList.push({
            'title': palette,
            'colours': palettes[palette]
        });
    };

    // create markup
    var content = mustache.render(template, {
        palettes: paletteList
    });
    return content;
}

function prepareCategories(results, config) {
    var doc = /\/\*doc\w*\n([\s\S]*?)\*\//mg;
    var pages = {};

    results.forEach(function(file) {
        var text = readFile(file);
        var matches = [];
        while (matches = doc.exec(text)) {
            var content = marked(matches[1]);
            if (!(pages.hasOwnProperty(content.meta.category))) {
                pages[content.meta.category] = [];
            }
            content.html += extractPalette(text, config);
            pages[content.meta.category].push(content);
        }
    });
    return pages;
}

function preparePageLinks(current, pages, index_title) {
    var links = [{
        link: 'index.html',
        title: index_title,
        selected: current === 'index' ? 'selected' : ''
    }];
    var category;

    for (category in pages) {
        if (pages.hasOwnProperty(category)) {
            links.push({
                link: category.replace(/\s+/g, '').toLowerCase() + '.html',
                title: category,
                selected: category === current ? 'selected' : ''
            });
        }
    }

    return links;
}

function generatePage(config, page) {
    var blocks = [];
    var rawContent = fs.readFileSync(config.documentation_assets + '/_header.html', 'utf8');

    page.forEach(function (block) {
        rawContent += '
'+block.meta.title+'
';
        rawContent += block.html;

        blocks.push({
            name: block.meta.name,
            title: block.meta.title
        });
    });

    rawContent += fs.readFileSync(config.documentation_assets +'/_footer.html', 'utf8');

    return {
        content: rawContent,
        blocks: blocks
    }
}

function processFiles(results, config, cb) {
    var pages = prepareCategories(results, config);
    var category, content;

    for (category in pages) {
        if (pages.hasOwnProperty(category)) {
            content = generatePage(config, pages[category])
            fs.writeFile(
                config.destination + '/' + category + '.html',
                mustache.render(
                    content.content,
                    {
                        title: category,
                        config: config,
                        categories: preparePageLinks(category, pages, config.index_title),
                        blocks: content.blocks
                    }
                )
            );

            if (category === config.index) {
                fs.writeFile(
                    config.destination + '/index.html',
                    mustache.render(
                        content.content,
                        {
                            title: 'index',
                            config: config,
                            categories: preparePageLinks('index', pages, config.index_title),
                            blocks: content.blocks
                        }
                    )
                )
            }
        }
    }

    if (cb) cb();
}

function maybeThrowError(err) {
    if (err) { throw err; }
}

function holograph(config, callback) {
    init(config, function(err, results) {
        if(err) { callback(err); }
        processFiles(results, config, callback);
    });
}

function run() {
    var config = {
        index_title: 'Home'
    };

    fs.readFile('holograph_config.yml', 'utf8',
        function(err, data) {
            if (err) {
                if (err.code === 'ENOENT') {
                    showError('Could not find holograph_config.yml in the current directory.');
                }
            }

            holograph(extend(config, yaml.safeLoad(data)), function(err, result) {
                showError(err);
                console.log('Build successful \\o\/'.green);
            });
        }
    );
}

module.exports = {
    holograph: holograph,
    run: run,
    prepareCategories: prepareCategories
};
<|endoftext|>


<commit_before>// https://github.khronos.org/KTX-Specification

const VERSION = [ 0xAB, 0x4B, 0x54, 0x58, 0x20, 0x32, 0x30, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A ];
const VERSION_OFFSET = 0;
const VERSION_LENGTH = VERSION.length;
const HEADER_OFFSET = VERSION_OFFSET + VERSION_LENGTH;
const HEADER_LENGTH = 9 * 4; // 9 uint32s
const INDEX_OFFSET = HEADER_OFFSET + HEADER_LENGTH;
const INDEX_LENGTH = 4 * 4 + 2 * 8; // 4 uint32s and 2 uint64s
const LEVEL_INDEX_OFFSET = INDEX_OFFSET + INDEX_LENGTH;

class Ktx2Image
{
    constructor()
    {
        this.vkFormat = 0;
        this.typeSize = 0;
        this.width = 0;
        this.height = 0;
        this.pixelDepth = 0;
        this.layerCount = 0;
        this.faceCount = 0;
        this.levelCount = 0;
        this.suporcompressionScheme = 0;

        this.dfdByteOffset = 0;
        this.dfdByteLength = 0;
        this.kvdByteOffset = 0;
        this.kvdByteLength = 0;
        this.sgdByteOffset = 0;
        this.sgdByteLength = 0;

        this.levels = [];
    }

    initialize(arrayBuffer)
    {
        const version = new DataView(arrayBuffer, VERSION_OFFSET, VERSION_LENGTH);
        if (! this.checkVersion(version))
        {
            console.error("Invalid KTX2 version identifier");
            return;
        }

        const header = new DataView(arrayBuffer, HEADER_OFFSET, HEADER_LENGTH);
        this.parseHeader(header);

        const fileIndex = new DataView(arrayBuffer, INDEX_OFFSET, INDEX_LENGTH);
        this.parseIndex(fileIndex);

        const levelIndexLength = this.levelCount * 3 * 8; // 3 uint64s per level
        const levelIndex = new DataView(arrayBuffer, LEVEL_INDEX_OFFSET, levelIndexLength);
        this.parseLevelIndex(levelIndex);

        this.parseLevelData(arrayBuffer);
    }

    checkVersion(version)
    {
        for (let i = 0; i < VERSION_LENGTH; i++)
        {
            if (version.getUint8(i) != VERSION[i])
            {
                return false;
            }
        }

        return true;
    }

    parseHeader(header)
    {
        let offset = 0;
        const getNext = () =>
        {
            const result = header.getUint32(offset, true);
            offset += 4;
            return result;
        };

        this.vkFormat = getNext();
        this.typeSize = getNext();
        this.width = getNext();
        this.height = getNext();
        this.pixelDepth = getNext();
        this.layerCount = getNext();
        this.faceCount = getNext();
        this.levelCount = getNext();
        this.suporcompressionScheme = getNext();
    }

    parseIndex(fileIndex)
    {
        let offset = 0;
        const getNext32 = () =>
        {
            const result = fileIndex.getUint32(offset, true);
            offset += 4;
            return result;
        };
        const getNext64 = () =>
        {
            const result = this.getUint64(fileIndex, offset, true);
            offset += 8;
            return result;
        };

        this.dfdByteOffset = getNext32();
        this.dfdByteLength = getNext32();
        this.kvdByteOffset = getNext32();
        this.kvdByteLength = getNext32();
        this.sgdByteOffset = getNext64();
        this.sgdByteLength = getNext64();
    }

    parseLevelIndex(levelIndex)
    {
        let offset = 0;
        const getNext = () =>
        {
            const result = this.getUint64(levelIndex, offset, true);
            offset += 8;
            return result;
        };

        for (let i = 0; i < this.levelCount; i++)
        {
            const level = {};
            level.byteOffset = getNext();
            level.byteLength = getNext();
            level.uncompressedByteLength = getNext();

            this.levels.push(level);
        }
    }

    parseLevelData(arrayBuffer)
    {
        let miplevel = 0;
        for (let level of this.levels)
        {
            level.miplevel = miplevel++;
            level.width = this.width / (miplevel * miplevel);
            level.height = this.height / (miplevel * miplevel);

            level.faces = [];
            for (let i = 0; i < this.faceCount; i++)
            {
                const face = {};

                if (this.vkFormat == VK_FORMAT.R16G16B16A16_SFLOAT)
                {
                    face.data = new Uint16Array(arrayBuffer, level.byteOffset, level.byteLength / this.typeSize);
                }

                level.faces.push(face);
            }
        }
    }

    // https://stackoverflow.com/questions/53103695
    getUint64(view, byteOffset, littleEndian)
    {
        // we should actually be able to use BigInt, but we can't create a Uint8Array with BigInt offset/length

        const left =  view.getUint32(byteOffset, littleEndian);
        const right = view.getUint32(byteOffset + 4, littleEndian);
        const combined = littleEndian ? left + (2 ** 32 * right) : (2 ** 32 * left) + right;

        if (! Number.isSafeInteger(combined))
        {
            console.warn("ktx2image: " + combined + " exceeds MAX_SAFE_INTEGER. Precision may be lost.");
        }

        return combined;
    }
}

// https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkFormat.html
const VK_FORMAT =
{
    R16G16B16A16_SFLOAT: 97
};

export { Ktx2Image };
<commit_msg>fix face data parsing<commit_after>// https://github.khronos.org/KTX-Specification

const VERSION = [ 0xAB, 0x4B, 0x54, 0x58, 0x20, 0x32, 0x30, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A ];
const VERSION_OFFSET = 0;
const VERSION_LENGTH = VERSION.length;
const HEADER_OFFSET = VERSION_OFFSET + VERSION_LENGTH;
const HEADER_LENGTH = 9 * 4; // 9 uint32s
const INDEX_OFFSET = HEADER_OFFSET + HEADER_LENGTH;
const INDEX_LENGTH = 4 * 4 + 2 * 8; // 4 uint32s and 2 uint64s
const LEVEL_INDEX_OFFSET = INDEX_OFFSET + INDEX_LENGTH;

class Ktx2Image
{
    constructor()
    {
        this.vkFormat = 0;
        this.typeSize = 0;
        this.width = 0;
        this.height = 0;
        this.pixelDepth = 0;
        this.layerCount = 0;
        this.faceCount = 0;
        this.levelCount = 0;
        this.suporcompressionScheme = 0;

        this.dfdByteOffset = 0;
        this.dfdByteLength = 0;
        this.kvdByteOffset = 0;
        this.kvdByteLength = 0;
        this.sgdByteOffset = 0;
        this.sgdByteLength = 0;

        this.levels = [];
    }

    initialize(arrayBuffer)
    {
        const version = new DataView(arrayBuffer, VERSION_OFFSET, VERSION_LENGTH);
        if (! this.checkVersion(version))
        {
            console.error("Invalid KTX2 version identifier");
            return;
        }

        const header = new DataView(arrayBuffer, HEADER_OFFSET, HEADER_LENGTH);
        this.parseHeader(header);

        const fileIndex = new DataView(arrayBuffer, INDEX_OFFSET, INDEX_LENGTH);
        this.parseIndex(fileIndex);

        const levelIndexLength = this.levelCount * 3 * 8; // 3 uint64s per level
        const levelIndex = new DataView(arrayBuffer, LEVEL_INDEX_OFFSET, levelIndexLength);
        this.parseLevelIndex(levelIndex);

        this.parseLevelData(arrayBuffer);
    }

    checkVersion(version)
    {
        for (let i = 0; i < VERSION_LENGTH; i++)
        {
            if (version.getUint8(i) != VERSION[i])
            {
                return false;
            }
        }

        return true;
    }

    parseHeader(header)
    {
        let offset = 0;
        const getNext = () =>
        {
            const result = header.getUint32(offset, true);
            offset += 4;
            return result;
        };

        this.vkFormat = getNext();
        this.typeSize = getNext();
        this.width = getNext();
        this.height = getNext();
        this.pixelDepth = getNext();
        this.layerCount = getNext();
        this.faceCount = getNext();
        this.levelCount = getNext();
        this.suporcompressionScheme = getNext();
    }

    parseIndex(fileIndex)
    {
        let offset = 0;
        const getNext32 = () =>
        {
            const result = fileIndex.getUint32(offset, true);
            offset += 4;
            return result;
        };
        const getNext64 = () =>
        {
            const result = this.getUint64(fileIndex, offset, true);
            offset += 8;
            return result;
        };

        this.dfdByteOffset = getNext32();
        this.dfdByteLength = getNext32();
        this.kvdByteOffset = getNext32();
        this.kvdByteLength = getNext32();
        this.sgdByteOffset = getNext64();
        this.sgdByteLength = getNext64();
    }

    parseLevelIndex(levelIndex)
    {
        let offset = 0;
        const getNext = () =>
        {
            const result = this.getUint64(levelIndex, offset, true);
            offset += 8;
            return result;
        };

        for (let i = 0; i < this.levelCount; i++)
        {
            const level = {};
            level.byteOffset = getNext();
            level.byteLength = getNext();
            level.uncompressedByteLength = getNext();

            this.levels.push(level);
        }
    }

    parseLevelData(arrayBuffer)
    {
        let miplevel = 0;
        for (let level of this.levels)
        {
            level.miplevel = miplevel++;
            level.width = this.width / (miplevel * miplevel);
            level.height = this.height / (miplevel * miplevel);

            level.faces = [];
            for (let i = 0; i < this.faceCount; i++)
            {
                const face = {};

                const faceLength = level.byteLength / 6;
                const faceOffset = level.byteOffset + faceLength * i;

                if (this.vkFormat == VK_FORMAT.R16G16B16A16_SFLOAT)
                {
                    face.data = new Uint16Array(arrayBuffer, faceOffset, faceLength / this.typeSize);
                }

                level.faces.push(face);
            }
        }
    }

    // https://stackoverflow.com/questions/53103695
    getUint64(view, byteOffset, littleEndian)
    {
        // we should actually be able to use BigInt, but we can't create a Uint8Array with BigInt offset/length

        const left =  view.getUint32(byteOffset, littleEndian);
        const right = view.getUint32(byteOffset + 4, littleEndian);
        const combined = littleEndian ? left + (2 ** 32 * right) : (2 ** 32 * left) + right;

        if (! Number.isSafeInteger(combined))
        {
            console.warn("ktx2image: " + combined + " exceeds MAX_SAFE_INTEGER. Precision may be lost.");
        }

        return combined;
    }
}

// https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkFormat.html
const VK_FORMAT =
{
    R16G16B16A16_SFLOAT: 97
};

export { Ktx2Image };
<|endoftext|>


<commit_before>var events = require('events');
var fs = require('fs');
var Q = require('q');
var util = require('util');
var zephyr = require('zephyr');

var conf = require('./config.js');
var db = require('./db.js');
var error = require('./error.js');
var innerdemon = require('./innerdemon.js');
var message = require('./message.js');
var Principal = require('./principal.js').Principal;
var renew = require('./renew.js');
var sequtil = require('./sequtil.js');
var zutil = require('./zutil.js');

var MAX_SUBSCRIBE_CHUNK = 20;

function subscriptionToZephyrTriple(sub) {
  return [sub.classKey, sub.instanceKey, sub.recipient];
}

function Subscriber() {
  events.EventEmitter.call(this);

  // Dedicated database pool, so queries don't starve us out.
  this.dbPool_ = db.createPool();

  // A queue to ensure we process messages serially.
  this.queue_ = new sequtil.JobQueue(this.processMessage_.bind(this));
  // Active inner demons.
  this.innerDemons_ = { };
  // Ticket renewal timer
  this.renewTimer_ = null;
  // State saving timer.
  this.stateTimer_ = null;
  // Throttler and serializer for state saving.
  this.stateSaver_ = null;

  // Listen for notices.
  zephyr.on('notice', this.onNotice_.bind(this));
}
util.inherits(Subscriber, events.EventEmitter);

Subscriber.prototype.addJob_ = function(msg) {
  var count = this.queue_.addJob(msg);
  if (count > 1)
    console.log('Messages stalled; queue size is', count);
}

Subscriber.prototype.onNotice_ = function(notice) {
  var msg = message.noticeToMessage(notice);
  if (msg)
    this.addJob_(msg);
};

Subscriber.prototype.processMessage_ = function(msg) {
  // Save to the database.
  return this.dbPool_.getConnection().then(function (conn) {
    return conn.saveMessage(
      msg
    ).finally(conn.end.bind(conn)).then(function(ret) {
      // We didn't save the message. Don't do anything.
      if (!ret)
        return;

      msg.id = ret.id;
      this.emit('message', msg, ret.userIds);
    }.bind(this));
  }.bind(this), function(err) {
    // If we failed to save a message, scream loudly, but don't crash.
    //
    // TODO(davidben): Keep the message in the queue! Do an
    // exponential backoff sort of thing.
    console.error("Failed to save message!", err, msg);
  }.bind(this));
};

Subscriber.prototype.spawnInnerDemon_ = function(principal, sessionState) {
  if (this.innerDemons_[principal])
    return this.innerDemons_[principal];

  console.log('Spawning inner demon for %s', principal);
  this.innerDemons_[principal] =
    new innerdemon.InnerDemon(principal, sessionState);

  this.innerDemons_[principal].on('message', function(msg) {
    var recipient = msg.isOutgoing ? msg.sender : msg.recipient;
    // The zephyrds actually compare recipients case-insensitively,
    // but not when subscribing. Since cross-realm messages come from
    // other zephyrds which are sensible, I believe this is only
    // relevant for personals.
    //
    // zephyrd uses strcasecmp which is locale-specific and
    // toLowerCase is Unicode-aware, but UCS-2 only per ES5.1, but
    // whatever. This is all just a sanity check anyway.
    if (recipient.toLowerCase() != principal.toLowerCase()) {
      console.error('Got message with bad recipient!', principal, msg);
    }
    // This nonsense is really not worth having separate recepient and
    // receipientKey, so just normalize the darn thing.
    if (!msg.isOutgoing)
      msg.recipient = principal;
    this.addJob_(msg);
  }.bind(this));

  this.innerDemons_[principal].on('exit', function(code) {
    console.log('Demon for %s exited with code %d', principal, code);
    delete this.innerDemons_[principal];
  }.bind(this));

  return this.innerDemons_[principal];
};

Subscriber.prototype.refreshPrivateSubs = function(user,
                                                   creds,
                                                   knownGoodCreds) {
  var innerDemon = this.spawnInnerDemon_(user.principal);
  return db.getUserPrivateSubscriptions(user).then(function(subs) {
    return innerDemon.subscribeTo(subs.map(subscriptionToZephyrTriple),
                                  creds, knownGoodCreds);
  }).then(function() {
    // Inner demon state changed. Subject to our throttling, save
    // state.
    if (this.stateSaver_)
      this.stateSaver_.request();
  }.bind(this));
};

Subscriber.prototype.subscribeTo_ = function(subs) {
  // Identical to zephyr.subscribeToSansDefaults, but chunking by 500
  // to avoid zhm bugs.
  console.log('Subscribing to %d subs', subs.length);
  var promise = Q();
  for (var i = 0; i < subs.length; i += MAX_SUBSCRIBE_CHUNK) {
    promise = promise.then(function(i) {
      console.log('Subscribed to %d of %d', i, subs.length);
      return Q.nfcall(
        zephyr.subscribeToSansDefaults,
        subs.slice(i, i + MAX_SUBSCRIBE_CHUNK)
      ).then(null, function(err) {
        // TODO(davidben): No, seriously, fix this.
        console.log('Failed to subscribe. Retrying', err);
        return Q.nfcall(
          zephyr.subscribeToSansDefaults,
          subs.slice(i, i + MAX_SUBSCRIBE_CHUNK));
      });
    }.bind(null, i));
  }
  return promise;
};

Subscriber.prototype.refreshPublicSubs_ = function() {
  return db.getAllPublicSubscriptions().then(function(subs) {
    return this.subscribeTo_(subs.map(subscriptionToZephyrTriple));
  }.bind(this));
};

Subscriber.prototype.renewTickets_ = function() {
  console.log('Renewing tickets...');
  renew.renewTickets().then(function() {
    console.log('Renewed. Refreshing public subs...');
    // Also refresh subs. Not necessary, but good to have new keys.
    return this.refreshPublicSubs_();
  }.bind(this)).then(function() {
    console.log('Refreshed.');
  }, function(err) {
    console.error('Error refreshing tickets', err);
  }).done();
};

Subscriber.prototype.restoreInnerDemonState_ = function() {
  return Q.nfcall(
    fs.readFile, conf.get('demonStateFile')
  ).then(function(data) {
    return JSON.parse(data);
  }, function(err) {
    if (err.code == 'ENOENT')
      return { };
    throw err;
  }).then(function(state) {
    return Q.all(Object.keys(state).map(function(principal) {
      if (typeof state[principal] === 'string') {
        state[principal] = {
          sessionState: state[principal],
          lastGoodTicket: null
        };
      }
      // Restore inner demon with saved zephyr state.
      var innerDemon =
        this.spawnInnerDemon_(principal, state[principal].sessionState);
      // If we have a valid ticket on file, also try to refresh subs
      // from that.
      if (state[principal].lastGoodTicket &&
          state[principal].lastGoodTicket.endtime > (new Date()).getTime()) {
        return db.getUser(principal).then(function(user) {
          return this.refreshPrivateSubs(user,
                                         state[principal].lastGoodTicket, true);
        }.bind(this)).then(null, function(err) {
          console.log('Failed to use (possible expired) saved creds for',
                      principal, err);
        });
      } else {
        return Q();
      }
    }.bind(this)));
  }.bind(this));
};

// Sort of odd to have this guy wrap all the zephyr functions, but oh
// well. The idea is that is the only guy that actually has to be a
// singleton. The rest can all be split over multiple machines
// assuming we can proxy over the 'message' events.

Subscriber.prototype.start = function() {
  // TODO(davidben): Starting inner demons and the like assume zephyr
  // is up. But it would probably be nice to do this afterwards just
  // to decrease the likelihood that we get one of the inner demons'
  // ports? Dunno if that's a problem, realistically. The code
  // previously tried to do that, but it was racy.

  var renewPromise;
  if (conf.get('daemonKeytab')) {
    renewPromise = renew.renewTickets();
  } else {
    renewPromise = Q();
    console.error('!!!!!!!!!!!!!!!!!!!!!');
    console.error('No daemon keytab configured. Using credential cache.');
    console.error('Configure a daemon principal in production.');
    console.error('!!!!!!!!!!!!!!!!!!!!!');
  }
  return renewPromise.then(function() {
    zephyr.initialize();
    zephyr.openPort();

    // Renew tickets regularly.
    if (conf.get('daemonKeytab')) {
      this.renewTimer_ = setInterval(this.renewTickets_.bind(this),
                                     conf.get('renewTimer'));
    }
    this.restoreInnerDemonState_().then(function() {
      console.log("Restored all inner demon state");

      this.stateSaver_ = new sequtil.Throttler(
        this.saveInnerDemonState_.bind(this),
        conf.get('demonStateThrottle'));

      this.stateTimer_ = setInterval(
        this.stateSaver_.request.bind(this.stateSaver_),
        conf.get('demonStateTimer'));
    }.bind(this), function(err) {
      console.error("Error restoring inner demon state:", err);
    }).done();
    return this.refreshPublicSubs_();
  }.bind(this));
};

Subscriber.prototype.addDefaultUserSubscriptions = function(user) {
  var subs = [{
    class: 'message',
    instance: '*',
    recipient: user.principal
  }];
  // Parse the principal.
  var principal = Principal.fromString(user.principal);
  // If a single-component principal on this realm, automatically
  // subscribe to their personal class.
  //
  // TODO(davidben): Cross-realm?? Does that even happen?
  // TODO(davidben): Instead of this silly thing, use principal.toStringShort()?
  if (principal.name.length == 1 && principal.realm == zephyr.getRealm()) {
    subs.push({
      class: principal.name[0],
      instance: '*',
      recipient: ''
    });
  }

  // Subscribe to personals.
  return db.addUserSubscriptions(user, subs).then(function() {
    // Throw away the return...
  });
};

Subscriber.prototype.addUserSubscriptions = function(user, subs, creds) {
  // ACL checks.
  for (var i = 0; i < subs.length; i++) {
    if (zutil.isPersonal(subs[i].recipient)) {
      if (subs[i].recipient !== user.principal)
        return Q.reject(new error.UserError(403, "Cannot subscribe to triple"));
      if (!creds) {
        return Q.reject(
          new error.UserError(400, "Credentials required for personals"));
      }
    }
  }

  // Add to the database.
  return db.addUserSubscriptions(user, subs).then(function(withKeys) {
    // Bleh. Looping twice is kinda awkward, but this avoids calling
    // downcase everywhere.
    var publicSubs = [];
    for (var i = 0; i < withKeys.length; i++) {
      if (!zutil.isPersonal(withKeys[i].recipient)) {
        publicSubs.push(withKeys[i]);
      }
    }

    // And subscribe to the triples.
    return Q.all([
      // Personals, if any. (Or if we just got creds stapled.)
      creds ? this.refreshPrivateSubs(user, creds) : Q(),
      // Public notices, if any.
      publicSubs.length ?
        this.subscribeTo_(publicSubs.map(subscriptionToZephyrTriple)) : Q()
    ]).then(function() {
      return withKeys;
    });
  }.bind(this));
};

Subscriber.prototype.removeUserSubscription = function(user, sub, creds) {
  // Only remove from the database, not from the subscriber.
  // TODO(davidben): Garbage-collect the zephyr subs.
  // TODO(davidben): We can unsubscribe the inner demons just fine.
  return db.removeUserSubscription(user,
                                   sub.class, sub.instance, sub.recipient);
};

Subscriber.prototype.zwrite = function(user, msg, creds) {
  // We just saw creds. Ought to refresh with them.
  this.refreshPrivateSubs(user, creds, false).then(null, function(err) {
    console.log('Failed to refresh subs for', user.principal, err);
  }).done();
  // Oh, and do what the user actually wanted.
  var innerDemon = this.spawnInnerDemon_(user.principal);
  return innerDemon.zwrite(msg, creds);
};

Subscriber.prototype.needsZephyrCreds = function(user) {
  var innerDemon = this.innerDemons_[user.principal];
  if (!innerDemon)
    return true;
  return innerDemon.needsCredentials();
};

Subscriber.prototype.getInnerDemonState = function() {
  return Q.all(Object.keys(this.innerDemons_).map(function(key) {
    return this.innerDemons_[key].dumpSession().then(function(sessionState) {
      return [key, sessionState];
    }.bind(this));
  }.bind(this))).then(function(tuples) {
    var state = { };
    for (var i = 0; i < tuples.length; i++) {
      state[tuples[i][0]] = tuples[i][1];
    }
    return state;
  });
};

Subscriber.prototype.saveInnerDemonState_ = function() {
  console.log('Querying inner demons for session state');
  return this.getInnerDemonState().then(function(state) {
    console.log('Saving session state');
    return Q.nfcall(fs.writeFile,
                    conf.get('demonStateFile'),
                    JSON.stringify(state),
                    { mode: 0600 });
  }).then(null, function(err) {
    console.error('Error saving inner demon state', err);
  });
};

Subscriber.prototype.shutdown = function() {
  return (
    this.stateSaver_ ? this.stateSaver_.request({noThrottle: true}) : Q()
  ).then(function() {
    if (this.renewTimer_ != null)
      clearInterval(this.renewTimer_);
    if (this.stateTimer__ != null)
      clearInterval(this.stateTimer_);
    console.log('Canceling subscriptions and shutting down inner demons...');
    return Q.all([
      Q.nfcall(zephyr.cancelSubscriptions),
      Q.all(Object.keys(this.innerDemons_).map(function(key) {
        return this.innerDemons_[key].expel();
      }.bind(this)))
    ]);
  }.bind(this));
};

exports.Subscriber = Subscriber;
<commit_msg>Still not enough<commit_after>var events = require('events');
var fs = require('fs');
var Q = require('q');
var util = require('util');
var zephyr = require('zephyr');

var conf = require('./config.js');
var db = require('./db.js');
var error = require('./error.js');
var innerdemon = require('./innerdemon.js');
var message = require('./message.js');
var Principal = require('./principal.js').Principal;
var renew = require('./renew.js');
var sequtil = require('./sequtil.js');
var zutil = require('./zutil.js');

var MAX_SUBSCRIBE_CHUNK = 10;

function subscriptionToZephyrTriple(sub) {
  return [sub.classKey, sub.instanceKey, sub.recipient];
}

function Subscriber() {
  events.EventEmitter.call(this);

  // Dedicated database pool, so queries don't starve us out.
  this.dbPool_ = db.createPool();

  // A queue to ensure we process messages serially.
  this.queue_ = new sequtil.JobQueue(this.processMessage_.bind(this));
  // Active inner demons.
  this.innerDemons_ = { };
  // Ticket renewal timer
  this.renewTimer_ = null;
  // State saving timer.
  this.stateTimer_ = null;
  // Throttler and serializer for state saving.
  this.stateSaver_ = null;

  // Listen for notices.
  zephyr.on('notice', this.onNotice_.bind(this));
}
util.inherits(Subscriber, events.EventEmitter);

Subscriber.prototype.addJob_ = function(msg) {
  var count = this.queue_.addJob(msg);
  if (count > 1)
    console.log('Messages stalled; queue size is', count);
}

Subscriber.prototype.onNotice_ = function(notice) {
  var msg = message.noticeToMessage(notice);
  if (msg)
    this.addJob_(msg);
};

Subscriber.prototype.processMessage_ = function(msg) {
  // Save to the database.
  return this.dbPool_.getConnection().then(function (conn) {
    return conn.saveMessage(
      msg
    ).finally(conn.end.bind(conn)).then(function(ret) {
      // We didn't save the message. Don't do anything.
      if (!ret)
        return;

      msg.id = ret.id;
      this.emit('message', msg, ret.userIds);
    }.bind(this));
  }.bind(this), function(err) {
    // If we failed to save a message, scream loudly, but don't crash.
    //
    // TODO(davidben): Keep the message in the queue! Do an
    // exponential backoff sort of thing.
    console.error("Failed to save message!", err, msg);
  }.bind(this));
};

Subscriber.prototype.spawnInnerDemon_ = function(principal, sessionState) {
  if (this.innerDemons_[principal])
    return this.innerDemons_[principal];

  console.log('Spawning inner demon for %s', principal);
  this.innerDemons_[principal] =
    new innerdemon.InnerDemon(principal, sessionState);

  this.innerDemons_[principal].on('message', function(msg) {
    var recipient = msg.isOutgoing ? msg.sender : msg.recipient;
    // The zephyrds actually compare recipients case-insensitively,
    // but not when subscribing. Since cross-realm messages come from
    // other zephyrds which are sensible, I believe this is only
    // relevant for personals.
    //
    // zephyrd uses strcasecmp which is locale-specific and
    // toLowerCase is Unicode-aware, but UCS-2 only per ES5.1, but
    // whatever. This is all just a sanity check anyway.
    if (recipient.toLowerCase() != principal.toLowerCase()) {
      console.error('Got message with bad recipient!', principal, msg);
    }
    // This nonsense is really not worth having separate recepient and
    // receipientKey, so just normalize the darn thing.
    if (!msg.isOutgoing)
      msg.recipient = principal;
    this.addJob_(msg);
  }.bind(this));

  this.innerDemons_[principal].on('exit', function(code) {
    console.log('Demon for %s exited with code %d', principal, code);
    delete this.innerDemons_[principal];
  }.bind(this));

  return this.innerDemons_[principal];
};

Subscriber.prototype.refreshPrivateSubs = function(user,
                                                   creds,
                                                   knownGoodCreds) {
  var innerDemon = this.spawnInnerDemon_(user.principal);
  return db.getUserPrivateSubscriptions(user).then(function(subs) {
    return innerDemon.subscribeTo(subs.map(subscriptionToZephyrTriple),
                                  creds, knownGoodCreds);
  }).then(function() {
    // Inner demon state changed. Subject to our throttling, save
    // state.
    if (this.stateSaver_)
      this.stateSaver_.request();
  }.bind(this));
};

Subscriber.prototype.subscribeTo_ = function(subs) {
  // Identical to zephyr.subscribeToSansDefaults, but chunking by 500
  // to avoid zhm bugs.
  console.log('Subscribing to %d subs', subs.length);
  var promise = Q();
  for (var i = 0; i < subs.length; i += MAX_SUBSCRIBE_CHUNK) {
    promise = promise.then(function(i) {
      if (i - (i%100) <
          (i + MAX_SUBSCRIBE_CHUNK) - ((i + MAX_SUBSCRIBE_CHUNK) % 100)) {
        console.log('Subscribed to %d of %d', i, subs.length);
      }
      return Q.nfcall(
        zephyr.subscribeToSansDefaults,
        subs.slice(i, i + MAX_SUBSCRIBE_CHUNK)
      ).then(null, function(err) {
        // TODO(davidben): No, seriously, fix this.
        console.log('Failed to subscribe. Retrying', err);
        return Q.nfcall(
          zephyr.subscribeToSansDefaults,
          subs.slice(i, i + MAX_SUBSCRIBE_CHUNK));
      });
    }.bind(null, i));
  }
  return promise;
};

Subscriber.prototype.refreshPublicSubs_ = function() {
  return db.getAllPublicSubscriptions().then(function(subs) {
    return this.subscribeTo_(subs.map(subscriptionToZephyrTriple));
  }.bind(this));
};

Subscriber.prototype.renewTickets_ = function() {
  console.log('Renewing tickets...');
  renew.renewTickets().then(function() {
    console.log('Renewed. Refreshing public subs...');
    // Also refresh subs. Not necessary, but good to have new keys.
    return this.refreshPublicSubs_();
  }.bind(this)).then(function() {
    console.log('Refreshed.');
  }, function(err) {
    console.error('Error refreshing tickets', err);
  }).done();
};

Subscriber.prototype.restoreInnerDemonState_ = function() {
  return Q.nfcall(
    fs.readFile, conf.get('demonStateFile')
  ).then(function(data) {
    return JSON.parse(data);
  }, function(err) {
    if (err.code == 'ENOENT')
      return { };
    throw err;
  }).then(function(state) {
    return Q.all(Object.keys(state).map(function(principal) {
      if (typeof state[principal] === 'string') {
        state[principal] = {
          sessionState: state[principal],
          lastGoodTicket: null
        };
      }
      // Restore inner demon with saved zephyr state.
      var innerDemon =
        this.spawnInnerDemon_(principal, state[principal].sessionState);
      // If we have a valid ticket on file, also try to refresh subs
      // from that.
      if (state[principal].lastGoodTicket &&
          state[principal].lastGoodTicket.endtime > (new Date()).getTime()) {
        return db.getUser(principal).then(function(user) {
          return this.refreshPrivateSubs(user,
                                         state[principal].lastGoodTicket, true);
        }.bind(this)).then(null, function(err) {
          console.log('Failed to use (possible expired) saved creds for',
                      principal, err);
        });
      } else {
        return Q();
      }
    }.bind(this)));
  }.bind(this));
};

// Sort of odd to have this guy wrap all the zephyr functions, but oh
// well. The idea is that is the only guy that actually has to be a
// singleton. The rest can all be split over multiple machines
// assuming we can proxy over the 'message' events.

Subscriber.prototype.start = function() {
  // TODO(davidben): Starting inner demons and the like assume zephyr
  // is up. But it would probably be nice to do this afterwards just
  // to decrease the likelihood that we get one of the inner demons'
  // ports? Dunno if that's a problem, realistically. The code
  // previously tried to do that, but it was racy.

  var renewPromise;
  if (conf.get('daemonKeytab')) {
    renewPromise = renew.renewTickets();
  } else {
    renewPromise = Q();
    console.error('!!!!!!!!!!!!!!!!!!!!!');
    console.error('No daemon keytab configured. Using credential cache.');
    console.error('Configure a daemon principal in production.');
    console.error('!!!!!!!!!!!!!!!!!!!!!');
  }
  return renewPromise.then(function() {
    zephyr.initialize();
    zephyr.openPort();

    // Renew tickets regularly.
    if (conf.get('daemonKeytab')) {
      this.renewTimer_ = setInterval(this.renewTickets_.bind(this),
                                     conf.get('renewTimer'));
    }
    this.restoreInnerDemonState_().then(function() {
      console.log("Restored all inner demon state");

      this.stateSaver_ = new sequtil.Throttler(
        this.saveInnerDemonState_.bind(this),
        conf.get('demonStateThrottle'));

      this.stateTimer_ = setInterval(
        this.stateSaver_.request.bind(this.stateSaver_),
        conf.get('demonStateTimer'));
    }.bind(this), function(err) {
      console.error("Error restoring inner demon state:", err);
    }).done();
    return this.refreshPublicSubs_();
  }.bind(this));
};

Subscriber.prototype.addDefaultUserSubscriptions = function(user) {
  var subs = [{
    class: 'message',
    instance: '*',
    recipient: user.principal
  }];
  // Parse the principal.
  var principal = Principal.fromString(user.principal);
  // If a single-component principal on this realm, automatically
  // subscribe to their personal class.
  //
  // TODO(davidben): Cross-realm?? Does that even happen?
  // TODO(davidben): Instead of this silly thing, use principal.toStringShort()?
  if (principal.name.length == 1 && principal.realm == zephyr.getRealm()) {
    subs.push({
      class: principal.name[0],
      instance: '*',
      recipient: ''
    });
  }

  // Subscribe to personals.
  return db.addUserSubscriptions(user, subs).then(function() {
    // Throw away the return...
  });
};

Subscriber.prototype.addUserSubscriptions = function(user, subs, creds) {
  // ACL checks.
  for (var i = 0; i < subs.length; i++) {
    if (zutil.isPersonal(subs[i].recipient)) {
      if (subs[i].recipient !== user.principal)
        return Q.reject(new error.UserError(403, "Cannot subscribe to triple"));
      if (!creds) {
        return Q.reject(
          new error.UserError(400, "Credentials required for personals"));
      }
    }
  }

  // Add to the database.
  return db.addUserSubscriptions(user, subs).then(function(withKeys) {
    // Bleh. Looping twice is kinda awkward, but this avoids calling
    // downcase everywhere.
    var publicSubs = [];
    for (var i = 0; i < withKeys.length; i++) {
      if (!zutil.isPersonal(withKeys[i].recipient)) {
        publicSubs.push(withKeys[i]);
      }
    }

    // And subscribe to the triples.
    return Q.all([
      // Personals, if any. (Or if we just got creds stapled.)
      creds ? this.refreshPrivateSubs(user, creds) : Q(),
      // Public notices, if any.
      publicSubs.length ?
        this.subscribeTo_(publicSubs.map(subscriptionToZephyrTriple)) : Q()
    ]).then(function() {
      return withKeys;
    });
  }.bind(this));
};

Subscriber.prototype.removeUserSubscription = function(user, sub, creds) {
  // Only remove from the database, not from the subscriber.
  // TODO(davidben): Garbage-collect the zephyr subs.
  // TODO(davidben): We can unsubscribe the inner demons just fine.
  return db.removeUserSubscription(user,
                                   sub.class, sub.instance, sub.recipient);
};

Subscriber.prototype.zwrite = function(user, msg, creds) {
  // We just saw creds. Ought to refresh with them.
  this.refreshPrivateSubs(user, creds, false).then(null, function(err) {
    console.log('Failed to refresh subs for', user.principal, err);
  }).done();
  // Oh, and do what the user actually wanted.
  var innerDemon = this.spawnInnerDemon_(user.principal);
  return innerDemon.zwrite(msg, creds);
};

Subscriber.prototype.needsZephyrCreds = function(user) {
  var innerDemon = this.innerDemons_[user.principal];
  if (!innerDemon)
    return true;
  return innerDemon.needsCredentials();
};

Subscriber.prototype.getInnerDemonState = function() {
  return Q.all(Object.keys(this.innerDemons_).map(function(key) {
    return this.innerDemons_[key].dumpSession().then(function(sessionState) {
      return [key, sessionState];
    }.bind(this));
  }.bind(this))).then(function(tuples) {
    var state = { };
    for (var i = 0; i < tuples.length; i++) {
      state[tuples[i][0]] = tuples[i][1];
    }
    return state;
  });
};

Subscriber.prototype.saveInnerDemonState_ = function() {
  console.log('Querying inner demons for session state');
  return this.getInnerDemonState().then(function(state) {
    console.log('Saving session state');
    return Q.nfcall(fs.writeFile,
                    conf.get('demonStateFile'),
                    JSON.stringify(state),
                    { mode: 0600 });
  }).then(null, function(err) {
    console.error('Error saving inner demon state', err);
  });
};

Subscriber.prototype.shutdown = function() {
  return (
    this.stateSaver_ ? this.stateSaver_.request({noThrottle: true}) : Q()
  ).then(function() {
    if (this.renewTimer_ != null)
      clearInterval(this.renewTimer_);
    if (this.stateTimer__ != null)
      clearInterval(this.stateTimer_);
    console.log('Canceling subscriptions and shutting down inner demons...');
    return Q.all([
      Q.nfcall(zephyr.cancelSubscriptions),
      Q.all(Object.keys(this.innerDemons_).map(function(key) {
        return this.innerDemons_[key].expel();
      }.bind(this)))
    ]);
  }.bind(this));
};

exports.Subscriber = Subscriber;
<|endoftext|>


<commit_before>/**
 * A Slack-LM Bridge.
 * @author Andy Pan
 */

var async = require('async');
var request = require('request');
var PeriodicTask = require('periodic-task');

var Post = require('./post');
var config = require('./server-config/index');

// when not receiving new message for some ticks, go to idle mode
//  to reduce freq of acqiry.
//  once new message for another thershold, go to active mode.
// change interval by modifying task,
//  it will take effect after at least an original interval.
var INTERVAL_NORMAL = 30 * 1000;
var INTERVAL_IDLE = 10 * 60 * 1000;
var idleTicks = 0;
var idleThershold = 40;

var lastSyncTime = 0;
var lastSetInterval = INTERVAL_NORMAL;
var transferredCount = [0, 0];

var startTime = new Date();

var taskFN = function() {
  lastSyncTime = new Date().toISOString();
  console.log('Tick. ' + lastSyncTime);
  async.parallel([
    function(callback) {
      config.slack.fetch(function(resp) {
        // resp.messages[0].ts
        var list = this.decode(resp);

        callback(null, this, list);
      });
    }, function(callback) {
      config.lm.fetch(function(resp) {
        // resp.list.map(function(v){return v.message})
        if (!resp) {
          console.log(' ! Oops! LM connection timeout.');
          callback(null, this);
          return;
        }
        var list = this.decode(resp);
        callback(null, this, list);
      });
    }
  ], function(err, result) {
    function prepareSync(name, data) {
      if(!data[1]) return;
      var len = data[1].length;
      if(len) {
        console.log(' * ' + name + ': ' + len + ' new message(s).');
        console.log('  lastest posts:');
        console.log('  - ' + data[1].join('\n  - '));
      } else {
        console.log(' * ' + name + ': up to date...');
      }
    }
    prepareSync('Slack', result[0]);
    prepareSync('LM   ', result[1]);

    var count = [0, 0];
    if (result[0][1] && result[1][1]
      && ((count[0] = result[0][1].length) + (count[1] = result[1][1].length))
      ) {
      transferredCount[0] += count[0];
      transferredCount[1] += count[1];
      // send post payload
      config.lm.send(result[0][1]);
      config.slack.send(result[1][1]);
      if (idleTicks >= idleThershold) {
        console.log('changed to normal mode');
        task.delay(INTERVAL_NORMAL);
        lastSetInterval = INTERVAL_NORMAL;
      }
      idleTicks = 0;
    } else {
      idleTicks++;
      console.log('idleTicks has been set to ' + idleTicks + '/' + idleThershold);
      if (idleTicks == idleThershold) {
        console.log('changed to idle mode');
        task.delay(INTERVAL_IDLE);
        lastSetInterval = INTERVAL_IDLE;
      }
    }

    console.log();
  })
};

var task = new PeriodicTask(INTERVAL_NORMAL, taskFN);

console.log('Initializing...');
async.parallel([
  function(callback) {
    config.slack.fetch(function(resp) {
      // resp.messages[0].ts
      // /*.map(function(v){return v.ts})*/
        callback(null, this.lastTimestamp);
    });
  }, function(callback) {
    config.lm.fetch(function(resp) {
      if (!resp) {
        // if there is no error, the received data next time will flood
        throw new Error('LM init failed!');
      }
      callback(null, this.lastTimestamp);
    });
  }
], function(err, result) {
  console.log('Initialized.');
  console.log(' * Slack last ts = ' + result[0]);
  console.log(' * LearnMode last ts = ' + result[1]);
  console.log();
  console.log('Now watching with INTERVAL = ' + INTERVAL_NORMAL + '.');
  task.run();
});

module.exports = {
  task: task,
  getInfo: function() {
    // todo
    return {};
  },
  handlers: {
    status: function(req, resp) {
      resp.write('
Uptime: ' + (new Date() - startTime) + 'ms
');
      resp.write('
start from   ' +  startTime.toISOString() + '
');
      resp.write('
current time ' + new Date().toISOString() + '
');

      resp.write('
Status

');
      resp.write('
Last Sync = ' + lastSyncTime + '
');
      resp.write('
Current interval = ' + lastSetInterval + '
');
      resp.write('
Idle Tick = ' + idleTicks + '/' + idleThershold + '
');
      resp.write('
Post transferred = ' + transferredCount[0] + ' to LM + ' + transferredCount[1] + ' to Slack.
');
      resp.write('
');
      resp.end();
    }
  }
};
<commit_msg>modify lastSyncTime's type, small polish of status page<commit_after>/**
 * A Slack-LM Bridge.
 * @author Andy Pan
 */

var async = require('async');
var request = require('request');
var PeriodicTask = require('periodic-task');

var Post = require('./post');
var config = require('./server-config/index');

// when not receiving new message for some ticks, go to idle mode
//  to reduce freq of acqiry.
//  once new message for another thershold, go to active mode.
// change interval by modifying task,
//  it will take effect after at least an original interval.
var INTERVAL_NORMAL = 30 * 1000;
var INTERVAL_IDLE = 10 * 60 * 1000;
var idleTicks = 0;
var idleThershold = 40;

var lastSyncTime = null;
var lastSetInterval = INTERVAL_NORMAL;
var transferredCount = [0, 0];

var startTime = new Date();

var taskFN = function() {
  lastSyncTime = new Date();
  console.log('Tick. ' + lastSyncTime.toISOString());
  async.parallel([
    function(callback) {
      config.slack.fetch(function(resp) {
        // resp.messages[0].ts
        var list = this.decode(resp);

        callback(null, this, list);
      });
    }, function(callback) {
      config.lm.fetch(function(resp) {
        // resp.list.map(function(v){return v.message})
        if (!resp) {
          console.log(' ! Oops! LM connection timeout.');
          callback(null, this);
          return;
        }
        var list = this.decode(resp);
        callback(null, this, list);
      });
    }
  ], function(err, result) {
    function prepareSync(name, data) {
      if(!data[1]) return;
      var len = data[1].length;
      if(len) {
        console.log(' * ' + name + ': ' + len + ' new message(s).');
        console.log('  lastest posts:');
        console.log('  - ' + data[1].join('\n  - '));
      } else {
        console.log(' * ' + name + ': up to date...');
      }
    }
    prepareSync('Slack', result[0]);
    prepareSync('LM   ', result[1]);

    var count = [0, 0];
    if (result[0][1] && result[1][1]
      && ((count[0] = result[0][1].length) + (count[1] = result[1][1].length))
      ) {
      transferredCount[0] += count[0];
      transferredCount[1] += count[1];
      // send post payload
      config.lm.send(result[0][1]);
      config.slack.send(result[1][1]);
      if (idleTicks >= idleThershold) {
        console.log('changed to normal mode');
        task.delay(INTERVAL_NORMAL);
        lastSetInterval = INTERVAL_NORMAL;
      }
      idleTicks = 0;
    } else {
      idleTicks++;
      console.log('idleTicks has been set to ' + idleTicks + '/' + idleThershold);
      if (idleTicks == idleThershold) {
        console.log('changed to idle mode');
        task.delay(INTERVAL_IDLE);
        lastSetInterval = INTERVAL_IDLE;
      }
    }

    console.log();
  })
};

var task = new PeriodicTask(INTERVAL_NORMAL, taskFN);

console.log('Initializing...');
async.parallel([
  function(callback) {
    config.slack.fetch(function(resp) {
      // resp.messages[0].ts
      // /*.map(function(v){return v.ts})*/
        callback(null, this.lastTimestamp);
    });
  }, function(callback) {
    config.lm.fetch(function(resp) {
      if (!resp) {
        // if there is no error, the received data next time will flood
        throw new Error('LM init failed!');
      }
      callback(null, this.lastTimestamp);
    });
  }
], function(err, result) {
  console.log('Initialized.');
  console.log(' * Slack last ts = ' + result[0]);
  console.log(' * LearnMode last ts = ' + result[1]);
  console.log();
  console.log('Now watching with INTERVAL = ' + INTERVAL_NORMAL + '.');
  task.run();
});

module.exports = {
  task: task,
  getInfo: function() {
    // todo
    return {};
  },
  handlers: {
    status: function(req, resp) {
      resp.write('
Uptime: ' + (new Date() - startTime) + 'ms

');
      resp.write('
start from   ' +    startTime.toISOString() + '
');
      resp.write('
current time ' +   new Date().toISOString() + '
');
      resp.write('
Last Sync at ' + lastSyncTime.toISOString() + '
');
      resp.write('
Next Sync at ' +
              new Date(lastSyncTime - (-lastSetInterval)).toISOString() + ' (scheduled)
');
      resp.write('
');

      resp.write('
Status

');
      resp.write('
Current interval = ' + lastSetInterval + '
');
      resp.write('
Idle Tick = ' + idleTicks + '/' + idleThershold + '
');
      resp.write('
Post transferred = ' + transferredCount[0] + ' to LM + ' + transferredCount[1] + ' to Slack
');
      resp.write('
');
      resp.end();
    }
  }
};
<|endoftext|>


<commit_before>/*
  SystemJS map support

  Provides map configuration through
    System.map['jquery'] = 'some/module/map'

  As well as contextual map config through
    System.map['bootstrap'] = {
      jquery: 'some/module/map2'
    }

  Note that this applies for subpaths, just like RequireJS

  jquery      -> 'some/module/map'
  jquery/path -> 'some/module/map/jquery/path'
  bootstrap   -> 'bootstrap'

  Inside any module name of the form 'bootstrap' or 'bootstrap/*'
    jquery    -> 'some/module/map2'
    jquery/p  -> 'some/module/map2/p'

  Maps are carefully applied from most specific contextual map, to least specific global map
*/
(function() {

  System.map = {};

  // return the number of prefix parts (separated by '/') matching the name
  // eg prefixMatchLength('jquery/some/thing', 'jquery') -> 1
  function prefixMatchLength(name, prefix) {
    var prefixParts = prefix.split('/');
    var nameParts = name.split('/');
    if (prefixParts.length > nameParts.length)
      return 0;
    for (var i = 0; i < prefixParts.length; i++)
      if (nameParts[i] != prefixParts[i])
        return 0;
    return prefixParts.length;
  }

  // given a relative-resolved module name and normalized parent name,
  // apply the map configuration
  function applyMap(name, parentName) {

    var curMatch, curMatchLength = 0;
    var curParent, curParentMatchLength = 0;

    // first find most specific contextual match
    if (parentName) {
      for (var p in System.map) {
        var curMap = System.map[p];
        if (typeof curMap != 'object')
          continue;

        // most specific parent match wins first
        if (prefixMatchLength(parentName, p) <= curParentMatchLength)
          continue;

        for (var q in curMap) {
          // most specific name match wins
          if (prefixMatchLength(name, q) <= curMatchLength)
            continue;

          curMatch = q;
          curMatchLength = q.split('/').length;
          curParent = p;
          curParentMatchLength = p.split('/').length;
        }
      }
      if (curMatch) {
        var subPath = name.split('/').splice(curMatchLength).join('/');
        return System.map[curParent][curMatch] + (subPath ? '/' + subPath : '');
      }
    }

    // if we didn't find a contextual match, try the global map
    for (var p in System.map) {
      var curMap = System.map[p];
      if (typeof curMap != 'string')
        continue;

      if (prefixMatchLength(name, p) <= curMatchLength)
        continue;

      curMatch = p;
      curMatchLength = p.split('/').length;
    }

    // return a match if any
    if (!curMatch)
      return name;

    var subPath = name.split('/').splice(curMatchLength).join('/');
    return System.map[curMatch] + (subPath ? '/' + subPath : '');
  }

  var systemNormalize = System.normalize;
  System.normalize = function(name, parentName, parentAddress) {
    return systemNormalize(applyMap(name, parentName), parentName, parentAddress);
  }
})();<commit_msg>Only add map if not there (#10)<commit_after>/*
  SystemJS map support

  Provides map configuration through
    System.map['jquery'] = 'some/module/map'

  As well as contextual map config through
    System.map['bootstrap'] = {
      jquery: 'some/module/map2'
    }

  Note that this applies for subpaths, just like RequireJS

  jquery      -> 'some/module/map'
  jquery/path -> 'some/module/map/jquery/path'
  bootstrap   -> 'bootstrap'

  Inside any module name of the form 'bootstrap' or 'bootstrap/*'
    jquery    -> 'some/module/map2'
    jquery/p  -> 'some/module/map2/p'

  Maps are carefully applied from most specific contextual map, to least specific global map
*/
(function() {

  System.map = System.map || {};

  // return the number of prefix parts (separated by '/') matching the name
  // eg prefixMatchLength('jquery/some/thing', 'jquery') -> 1
  function prefixMatchLength(name, prefix) {
    var prefixParts = prefix.split('/');
    var nameParts = name.split('/');
    if (prefixParts.length > nameParts.length)
      return 0;
    for (var i = 0; i < prefixParts.length; i++)
      if (nameParts[i] != prefixParts[i])
        return 0;
    return prefixParts.length;
  }

  // given a relative-resolved module name and normalized parent name,
  // apply the map configuration
  function applyMap(name, parentName) {

    var curMatch, curMatchLength = 0;
    var curParent, curParentMatchLength = 0;

    // first find most specific contextual match
    if (parentName) {
      for (var p in System.map) {
        var curMap = System.map[p];
        if (typeof curMap != 'object')
          continue;

        // most specific parent match wins first
        if (prefixMatchLength(parentName, p) <= curParentMatchLength)
          continue;

        for (var q in curMap) {
          // most specific name match wins
          if (prefixMatchLength(name, q) <= curMatchLength)
            continue;

          curMatch = q;
          curMatchLength = q.split('/').length;
          curParent = p;
          curParentMatchLength = p.split('/').length;
        }
      }
      if (curMatch) {
        var subPath = name.split('/').splice(curMatchLength).join('/');
        return System.map[curParent][curMatch] + (subPath ? '/' + subPath : '');
      }
    }

    // if we didn't find a contextual match, try the global map
    for (var p in System.map) {
      var curMap = System.map[p];
      if (typeof curMap != 'string')
        continue;

      if (prefixMatchLength(name, p) <= curMatchLength)
        continue;

      curMatch = p;
      curMatchLength = p.split('/').length;
    }

    // return a match if any
    if (!curMatch)
      return name;

    var subPath = name.split('/').splice(curMatchLength).join('/');
    return System.map[curMatch] + (subPath ? '/' + subPath : '');
  }

  var systemNormalize = System.normalize;
  System.normalize = function(name, parentName, parentAddress) {
    return systemNormalize(applyMap(name, parentName), parentName, parentAddress);
  }
})();
<|endoftext|>