<commit_before>/*  Copyright (c) 2012 Sven "FuzzYspo0N" Bergström,
                  2013 Robert XD Hawkins

    written by : http://underscorediscovery.com
    written for : http://buildnewgames.com/real-time-multiplayer/

    modified for collective behavior experiments on Amazon Mechanical Turk

    MIT Licensed.
*/

/*
   THE FOLLOWING FUNCTIONS MAY NEED TO BE CHANGED
*/

// A window global for our game root variable.
var game = {};
// A window global for our id, which we can use to look ourselves up
var my_id = null;
// Keeps track of whether player is paying attention...
var visible;
var active_keys = [];
var speed_change = "none";

// what happens when you press 'left'?
left_turn = function() {
    var self = game.get_player(my_id);
    self.angle = (Number(self.angle) - 5) % 360;
};

// what happens when you press 'left'?
right_turn = function() {
    var self = game.get_player(my_id);
    self.angle = (Number(self.angle) + 5) % 360;
};

// Function that gets called client-side when server disconnects someone
client_ondisconnect = function(data) {
    // Redirect to exit survey
    console.log("server booted")
    var URL = 'http://projects.csail.mit.edu/ci/turk/forms/end.html?id=' + my_id;
    window.location.replace(URL);
};

/*
Note: If you add some new variable to your game that must be shared
  across server and client, add it both here and the server_send_update
  function in game.core.js to make sure it syncs

Explanation: This function is at the center of the problem of
  networking -- everybody has different INSTANCES of the game. The
  server has its own, and both players have theirs too. This can get
  confusing because the server will update a variable, and the variable
  of the same name won't change in the clients (because they have a
  different instance of it). To make sure everybody's on the same page,
  the server regularly sends news about its variables to the clients so
  that they can update their variables to reflect changes.
*/
client_onserverupdate_received = function(data){

    // Update client versions of variables with data received from
    // server_send_update function in game.core.js
    if(data.players) {
        _.map(_.zip(data.players, game.players),
            function(z){
                z[1].id = z[0].id
                if (z[0].player == null) {
                    z[1].player = null
                } else {
                    var s_player = z[0].player
                    var l_player = z[1].player
                    if(z[0].id != my_id | l_player.angle == null)
			l_player.angle = s_player.angle
                    l_player.curr_background = s_player.cbg
		    l_player.total_points = s_player.tot
                    l_player.pos = game.pos(s_player.pos)
                    l_player.speed = s_player.speed
                }
            })
    }

    game.condition = data.cond;
    game.draw_enabled = data.de;
    game.good2write = data.g2w;

};

// This is where clients parse socket.io messages from the server. If
// you want to add another event (labeled 'x', say), just add another
// case here, then call

//          this.instance.player_host.send("s.x. <data>")

// The corresponding function where the server parses messages from
// clients, look for "server_onMessage" in game.server.js.
client_onMessage = function(data) {

    var commands = data.split('.');
    var command = commands[0];
    var subcommand = commands[1] || null;
    var commanddata = commands[2] || null;

    switch(command) {
    case 's': //server message

        switch(subcommand) {
	case 'end' :
	    // Redirect to exit survey
	    console.log("received end message...")
	    var URL = 'http://projects.csail.mit.edu/ci/turk/forms/end.html?id=' + my_id;
	    window.location.replace(URL); break;
        case 'alert' : // Not in database, so you can't play...
            alert('You did not enter an ID');
            window.location.replace('http://nodejs.org'); break;
        case 'join' : //join a game requested
            var num_players = commanddata;
            client_onjoingame(num_players); break;
        case 'add_player' : // New player joined... Need to add them to our list.
            console.log("adding player" + commanddata)
            game.players.push({id: commanddata, player: new game_player(game)}); break;
        case 'begin_game' :
            game.hidden_enabled = true;
            client_newgame(); break;
        case 'blink' : //blink title
            flashTitle("GO!");  break;
        }
        break;
    }
};

// Restarts things on the client side. Necessary for iterated games.
client_newgame = function() {
    // Initiate countdown (with timeouts)
    game.get_player(my_id).angle = null;
    client_countdown();
};

client_countdown = function() {
    var player = game.get_player(my_id);
    player.message = 'Begin in 3...';
    setTimeout(function(){player.message = 'Begin in 2...';},
               1000);
    setTimeout(function(){player.message = 'Begin in 1...';},
               2000);
    setTimeout(function(){
        player.message = 'GO';
        game.start_time = new Date();},
	3000);
    setTimeout(function(){player.message = '';},
               4000);
}

client_update = function() {
    var player = game.get_player(my_id);

    //Clear the screen area
    game.ctx.clearRect(0,0,485,280);

    // Alter speeds
    if (speed_change != "none") {
        player.speed = speed_change == "up" ? game.max_speed : game.min_speed;
        game.socket.send("s." + String(player.speed).replace(/\./g,'-'));
        speed_change = "none"
    }

    // Turn if key is still being held... Don't do anything if both are held
    if (active_keys.length == 1) {
        if(_.contains(active_keys, 'right')) right_turn();
        if(_.contains(active_keys, 'left')) left_turn() ;
    }

    // Draw visibility radius
    //if(player.pos) draw_visibility_radius(game, player)

    //Draw opponent next (but only those in radius)
    _.map(game.get_others(my_id), function(p){
        draw_player(game, p.player)
	draw_label(game, p.player, "Player " + p.id.slice(0,4))
    })

    // Draw points scoreboard
    $("#cumulative_bonus").html("Total bonus so far: $" + (player.total_points).fixed(2));

    $("#curr_bonus").html("Current Score: " + Math.floor(player.curr_background*100) + "%");

    // Draw time remaining
    var time_remaining = game.round_length - Math.floor((new Date() - game.start_time) / (1000 * 60))
    $("#time").html(game.good2write ? "Time remaining: " + time_remaining + " minutes" : "You are in the waiting room.");

    //And then we draw ourself so we're always in front
    if(player.pos) {
	draw_player(game, player)
	draw_label(game, player, "YOU");
    }

};

var percentColors = [
    { pct: 0.0, color: { r: 0xff, g: 0x00, b: 0 } },
    { pct: 0.5, color: { r: 0xff, g: 0xff, b: 0xff } },
    { pct: 1.0, color: { r: 0x00, g: 0xff, b: 0 } } ];

var getColorForPercentage = function(pct) {
    for (var i = 0; i < percentColors.length; i++) {
        if (pct <= percentColors[i].pct) {
            var lower = percentColors[i - 1] || { pct: 0.1, color: { r: 0x0, g: 0x00, b: 0 } };
            var upper = percentColors[i];
            var range = upper.pct - lower.pct;
            var rangePct = (pct - lower.pct) / range;
            var pctLower = 1 - rangePct;
            var pctUpper = rangePct;
            var color = {
                r: Math.floor(lower.color.r * pctLower + upper.color.r * pctUpper),
                g: Math.floor(lower.color.g * pctLower + upper.color.g * pctUpper),
                b: Math.floor(lower.color.b * pctLower + upper.color.b * pctUpper)
            };
            return 'rgb(' + [color.r, color.g, color.b].join(',') + ')';
        }
    }
}

/*
  The following code should NOT need to be changed
*/

// When loading the page, we store references to our
// drawing canvases, and initiate a game instance.
window.onload = function(){
    //Create our game client instance.
    game = new game_core();

    //Connect to the socket.io server!
    client_connect_to_server(game);

    //Fetch the viewport
    game.viewport = document.getElementById('viewport');

    //Adjust its size
    game.viewport.width = game.world.width;
    game.viewport.height = game.world.height;

    // Keep track of which keys are being pressed. Keys fire continuously while held.
    KeyboardJS.on('left',
	function(){ // Only notify on FIRST press
	    if(!_.contains(active_keys, 'left')) {
		active_keys.push('left');
	    }
	},
	function(){ // Only notify on first release
	    if(_.contains(active_keys, 'left')) {
		game.socket.send('a.' + game.get_player(my_id).angle)
	    }
	    active_keys = _.without(active_keys, 'left');});
    KeyboardJS.on('right',
        function(){ // Only notify on first press
	    if(!_.contains(active_keys, 'right')) {
		active_keys.push('right');
	    }
	},
        function(){ // Only notify on first release
	    if(_.contains(active_keys, 'right')) {
		game.socket.send('a.' + game.get_player(my_id).angle)
	    }
	    active_keys = _.without(active_keys, 'right');})
    KeyboardJS.on('space',
        function(){speed_change = "up"},
        function(){speed_change = "down"})

    //Fetch the rendering contexts
    game.ctx = game.viewport.getContext('2d');

    //Set the draw style for the font
    game.ctx.font = '11px "Helvetica"';

    //Finally, start the loop
    game.update();
};

// Associates callback functions corresponding to different socket messages
client_connect_to_server = function(game) {
    //Store a local reference to our connection to the server
    game.socket = io.connect();

    //When we connect, we are not 'connected' until we have a server id
    //and are placed in a game by the server. The server sends us a message for that.
    game.socket.on('connect', function(){
//        game.state = 'connecting';
    }.bind(game));

    game.socket.on('ping', function(data){
	    game.socket.send('pong.' + data.sendTime + "." + data.tick_num)})
    //Sent when we are disconnected (network, server down, etc)
    game.socket.on('disconnect', client_ondisconnect.bind(game));
    //Sent each tick of the server simulation. This is our authoritive update
    game.socket.on('onserverupdate', client_onserverupdate_received);
    //Handle when we connect to the server, showing state and storing id's.
    game.socket.on('onconnected', client_onconnected.bind(game));
    //On message from the server, we parse the commands and send it to the handlers
    game.socket.on('message', client_onMessage.bind(game));
};

client_onconnected = function(data) {
    //The server responded that we are now in a game,
    //this lets us store the information about ourselves
    // so that we remember who we are.
    my_id = data.id;
    game.players[0].id = my_id;
    game.get_player(my_id).online = true;
};

client_onjoingame = function(num_players) {
    // Need client to know how many players there are, so they can set up the appropriate data structure
    _.map(_.range(num_players - 1), function(i){game.players.unshift({id: null, player: new game_player(game)})});
    // Set self color, leave others default white
    game.get_player(my_id).color = game.self_color;
    // Start 'em moving
    game.get_player(my_id).speed = game.min_speed;
};

// Automatically registers whether user has switched tabs...
(function() {
    document.hidden = hidden = "hidden";

    // Standards:
    if (hidden in document)
        document.addEventListener("visibilitychange", onchange);
    else if ((hidden = "mozHidden") in document)
        document.addEventListener("mozvisibilitychange", onchange);
    else if ((hidden = "webkitHidden") in document)
        document.addEventListener("webkitvisibilitychange", onchange);
    else if ((hidden = "msHidden") in document)
        document.addEventListener("msvisibilitychange", onchange);
    // IE 9 and lower:
    else if ('onfocusin' in document)
        document.onfocusin = document.onfocusout = onchange;
    // All others:
    else
        window.onpageshow = window.onpagehide = window.onfocus
             = window.onblur = onchange;
})();

function onchange (evt) {
    var v = 'visible', h = 'hidden',
    evtMap = {
        focus:v, focusin:v, pageshow:v, blur:h, focusout:h, pagehide:h
    };
    evt = evt || window.event;
    if (evt.type in evtMap) {
        document.body.className = evtMap[evt.type];
    } else {
        document.body.className = evt.target.hidden ? "hidden" : "visible";
    }
    visible = document.body.className;
    game.socket.send("h." + document.body.className);
};

// Flashes title to notify user that game has started
(function () {

    var original = document.title;
    var timeout;

    window.flashTitle = function (newMsg, howManyTimes) {
        function step() {
            document.title = (document.title == original) ? newMsg : original;
            if (visible == "hidden") {
                timeout = setTimeout(step, 500);
            } else {
                document.title = original;
            }
        };
        cancelFlashTitle(timeout);
        step();
    };

window.cancelFlashTitle = function (timeout) {
    clearTimeout(timeout);
    document.title = original;
};

}());
<commit_msg>Changed timer display<commit_after>/*  Copyright (c) 2012 Sven "FuzzYspo0N" Bergström,
                  2013 Robert XD Hawkins

    written by : http://underscorediscovery.com
    written for : http://buildnewgames.com/real-time-multiplayer/

    modified for collective behavior experiments on Amazon Mechanical Turk

    MIT Licensed.
*/

/*
   THE FOLLOWING FUNCTIONS MAY NEED TO BE CHANGED
*/

// A window global for our game root variable.
var game = {};
// A window global for our id, which we can use to look ourselves up
var my_id = null;
// Keeps track of whether player is paying attention...
var visible;
var active_keys = [];
var speed_change = "none";

// what happens when you press 'left'?
left_turn = function() {
    var self = game.get_player(my_id);
    self.angle = (Number(self.angle) - 5) % 360;
};

// what happens when you press 'left'?
right_turn = function() {
    var self = game.get_player(my_id);
    self.angle = (Number(self.angle) + 5) % 360;
};

// Function that gets called client-side when server disconnects someone
client_ondisconnect = function(data) {
    // Redirect to exit survey
    console.log("server booted")
    var URL = 'http://projects.csail.mit.edu/ci/turk/forms/end.html?id=' + my_id;
    window.location.replace(URL);
};

/*
Note: If you add some new variable to your game that must be shared
  across server and client, add it both here and the server_send_update
  function in game.core.js to make sure it syncs

Explanation: This function is at the center of the problem of
  networking -- everybody has different INSTANCES of the game. The
  server has its own, and both players have theirs too. This can get
  confusing because the server will update a variable, and the variable
  of the same name won't change in the clients (because they have a
  different instance of it). To make sure everybody's on the same page,
  the server regularly sends news about its variables to the clients so
  that they can update their variables to reflect changes.
*/
client_onserverupdate_received = function(data){

    // Update client versions of variables with data received from
    // server_send_update function in game.core.js
    if(data.players) {
        _.map(_.zip(data.players, game.players),
            function(z){
                z[1].id = z[0].id
                if (z[0].player == null) {
                    z[1].player = null
                } else {
                    var s_player = z[0].player
                    var l_player = z[1].player
                    if(z[0].id != my_id | l_player.angle == null)
			l_player.angle = s_player.angle
                    l_player.curr_background = s_player.cbg
		    l_player.total_points = s_player.tot
                    l_player.pos = game.pos(s_player.pos)
                    l_player.speed = s_player.speed
                }
            })
    }

    game.condition = data.cond;
    game.draw_enabled = data.de;
    game.good2write = data.g2w;

};

// This is where clients parse socket.io messages from the server. If
// you want to add another event (labeled 'x', say), just add another
// case here, then call

//          this.instance.player_host.send("s.x. <data>")

// The corresponding function where the server parses messages from
// clients, look for "server_onMessage" in game.server.js.
client_onMessage = function(data) {

    var commands = data.split('.');
    var command = commands[0];
    var subcommand = commands[1] || null;
    var commanddata = commands[2] || null;

    switch(command) {
    case 's': //server message

        switch(subcommand) {
	case 'end' :
	    // Redirect to exit survey
	    console.log("received end message...")
	    var URL = 'http://projects.csail.mit.edu/ci/turk/forms/end.html?id=' + my_id;
	    window.location.replace(URL); break;
        case 'alert' : // Not in database, so you can't play...
            alert('You did not enter an ID');
            window.location.replace('http://nodejs.org'); break;
        case 'join' : //join a game requested
            var num_players = commanddata;
            client_onjoingame(num_players); break;
        case 'add_player' : // New player joined... Need to add them to our list.
            console.log("adding player" + commanddata)
            game.players.push({id: commanddata, player: new game_player(game)}); break;
        case 'begin_game' :
            game.hidden_enabled = true;
            client_newgame(); break;
        case 'blink' : //blink title
            flashTitle("GO!");  break;
        }
        break;
    }
};

// Restarts things on the client side. Necessary for iterated games.
client_newgame = function() {
    // Initiate countdown (with timeouts)
    game.get_player(my_id).angle = null;
    client_countdown();
};

client_countdown = function() {
    var player = game.get_player(my_id);
    player.message = 'Begin in 3...';
    setTimeout(function(){player.message = 'Begin in 2...';},
               1000);
    setTimeout(function(){player.message = 'Begin in 1...';},
               2000);
    setTimeout(function(){
        player.message = 'GO';
        game.start_time = new Date();},
	3000);
    setTimeout(function(){player.message = '';},
               4000);
}

client_update = function() {
    var player = game.get_player(my_id);

    //Clear the screen area
    game.ctx.clearRect(0,0,485,280);

    // Alter speeds
    if (speed_change != "none") {
        player.speed = speed_change == "up" ? game.max_speed : game.min_speed;
        game.socket.send("s." + String(player.speed).replace(/\./g,'-'));
        speed_change = "none"
    }

    // Turn if key is still being held... Don't do anything if both are held
    if (active_keys.length == 1) {
        if(_.contains(active_keys, 'right')) right_turn();
        if(_.contains(active_keys, 'left')) left_turn() ;
    }

    // Draw visibility radius
    //if(player.pos) draw_visibility_radius(game, player)

    //Draw opponent next (but only those in radius)
    _.map(game.get_others(my_id), function(p){
        draw_player(game, p.player)
	draw_label(game, p.player, "Player " + p.id.slice(0,4))
    })

    // Draw points scoreboard
    $("#cumulative_bonus").html("Total bonus so far: $" + (player.total_points).fixed(2));

    $("#curr_bonus").html("Current Score: " + Math.floor(player.curr_background*100) + "%");

    var time_remaining = game.round_length - Math.floor((new Date() - game.start_time) / (1000 * 60))
    // Draw time remaining
    if(time_remaining > 1) {
	$("#time").html(game.good2write ? "Time remaining: " + time_remaining + " minutes" : "You are in the waiting room.");
    } else {
	var time_remaining = game.round_length - Math.floor((new Date() - game.start_time) / (1000 * 60)*6)/6
	time_remaining = Math.max(Math.floor(time_remaining*6)*10, 10)
	$("#time").html(game.good2write ? "Time remaining: " + time_remaining + " seconds" : "You are in the waiting room.");
    }

    //And then we draw ourself so we're always in front
    if(player.pos) {
	draw_player(game, player)
	draw_label(game, player, "YOU");
    }

};

var percentColors = [
    { pct: 0.0, color: { r: 0xff, g: 0x00, b: 0 } },
    { pct: 0.5, color: { r: 0xff, g: 0xff, b: 0xff } },
    { pct: 1.0, color: { r: 0x00, g: 0xff, b: 0 } } ];

var getColorForPercentage = function(pct) {
    for (var i = 0; i < percentColors.length; i++) {
        if (pct <= percentColors[i].pct) {
            var lower = percentColors[i - 1] || { pct: 0.1, color: { r: 0x0, g: 0x00, b: 0 } };
            var upper = percentColors[i];
            var range = upper.pct - lower.pct;
            var rangePct = (pct - lower.pct) / range;
            var pctLower = 1 - rangePct;
            var pctUpper = rangePct;
            var color = {
                r: Math.floor(lower.color.r * pctLower + upper.color.r * pctUpper),
                g: Math.floor(lower.color.g * pctLower + upper.color.g * pctUpper),
                b: Math.floor(lower.color.b * pctLower + upper.color.b * pctUpper)
            };
            return 'rgb(' + [color.r, color.g, color.b].join(',') + ')';
        }
    }
}

/*
  The following code should NOT need to be changed
*/

// When loading the page, we store references to our
// drawing canvases, and initiate a game instance.
window.onload = function(){
    //Create our game client instance.
    game = new game_core();

    //Connect to the socket.io server!
    client_connect_to_server(game);

    //Fetch the viewport
    game.viewport = document.getElementById('viewport');

    //Adjust its size
    game.viewport.width = game.world.width;
    game.viewport.height = game.world.height;

    // Keep track of which keys are being pressed. Keys fire continuously while held.
    KeyboardJS.on('left',
	function(){ // Only notify on FIRST press
	    if(!_.contains(active_keys, 'left')) {
		active_keys.push('left');
	    }
	},
	function(){ // Only notify on first release
	    if(_.contains(active_keys, 'left')) {
		game.socket.send('a.' + game.get_player(my_id).angle)
	    }
	    active_keys = _.without(active_keys, 'left');});
    KeyboardJS.on('right',
        function(){ // Only notify on first press
	    if(!_.contains(active_keys, 'right')) {
		active_keys.push('right');
	    }
	},
        function(){ // Only notify on first release
	    if(_.contains(active_keys, 'right')) {
		game.socket.send('a.' + game.get_player(my_id).angle)
	    }
	    active_keys = _.without(active_keys, 'right');})
    KeyboardJS.on('space',
        function(){speed_change = "up"},
        function(){speed_change = "down"})

    //Fetch the rendering contexts
    game.ctx = game.viewport.getContext('2d');

    //Set the draw style for the font
    game.ctx.font = '11px "Helvetica"';

    //Finally, start the loop
    game.update();
};

// Associates callback functions corresponding to different socket messages
client_connect_to_server = function(game) {
    //Store a local reference to our connection to the server
    game.socket = io.connect();

    //When we connect, we are not 'connected' until we have a server id
    //and are placed in a game by the server. The server sends us a message for that.
    game.socket.on('connect', function(){
//        game.state = 'connecting';
    }.bind(game));

    game.socket.on('ping', function(data){
	    game.socket.send('pong.' + data.sendTime + "." + data.tick_num)})
    //Sent when we are disconnected (network, server down, etc)
    game.socket.on('disconnect', client_ondisconnect.bind(game));
    //Sent each tick of the server simulation. This is our authoritive update
    game.socket.on('onserverupdate', client_onserverupdate_received);
    //Handle when we connect to the server, showing state and storing id's.
    game.socket.on('onconnected', client_onconnected.bind(game));
    //On message from the server, we parse the commands and send it to the handlers
    game.socket.on('message', client_onMessage.bind(game));
};

client_onconnected = function(data) {
    //The server responded that we are now in a game,
    //this lets us store the information about ourselves
    // so that we remember who we are.
    my_id = data.id;
    game.players[0].id = my_id;
    game.get_player(my_id).online = true;
};

client_onjoingame = function(num_players) {
    // Need client to know how many players there are, so they can set up the appropriate data structure
    _.map(_.range(num_players - 1), function(i){game.players.unshift({id: null, player: new game_player(game)})});
    // Set self color, leave others default white
    game.get_player(my_id).color = game.self_color;
    // Start 'em moving
    game.get_player(my_id).speed = game.min_speed;
};

// Automatically registers whether user has switched tabs...
(function() {
    document.hidden = hidden = "hidden";

    // Standards:
    if (hidden in document)
        document.addEventListener("visibilitychange", onchange);
    else if ((hidden = "mozHidden") in document)
        document.addEventListener("mozvisibilitychange", onchange);
    else if ((hidden = "webkitHidden") in document)
        document.addEventListener("webkitvisibilitychange", onchange);
    else if ((hidden = "msHidden") in document)
        document.addEventListener("msvisibilitychange", onchange);
    // IE 9 and lower:
    else if ('onfocusin' in document)
        document.onfocusin = document.onfocusout = onchange;
    // All others:
    else
        window.onpageshow = window.onpagehide = window.onfocus
             = window.onblur = onchange;
})();

function onchange (evt) {
    var v = 'visible', h = 'hidden',
    evtMap = {
        focus:v, focusin:v, pageshow:v, blur:h, focusout:h, pagehide:h
    };
    evt = evt || window.event;
    if (evt.type in evtMap) {
        document.body.className = evtMap[evt.type];
    } else {
        document.body.className = evt.target.hidden ? "hidden" : "visible";
    }
    visible = document.body.className;
    game.socket.send("h." + document.body.className);
};

// Flashes title to notify user that game has started
(function () {

    var original = document.title;
    var timeout;

    window.flashTitle = function (newMsg, howManyTimes) {
        function step() {
            document.title = (document.title == original) ? newMsg : original;
            if (visible == "hidden") {
                timeout = setTimeout(step, 500);
            } else {
                document.title = original;
            }
        };
        cancelFlashTitle(timeout);
        step();
    };

window.cancelFlashTitle = function (timeout) {
    clearTimeout(timeout);
    document.title = original;
};

}());
<|endoftext|>


<commit_before>/**
 * Implement Gatsby's Node APIs in this file.
 *
 * See: https://www.gatsbyjs.org/docs/node-apis/
 */

// You can delete this file if you're not using it
//
//
const { createFilePath } = require(`gatsby-source-filesystem`);
const path = require('path');

const draftFilter = `
    filter: {
      frontmatter: { draft: { ne: true }}
    }
  `;

const listPosts = `
  query IndexQuery {
    allMarkdownRemark(
      sort: { order: DESC, fields: [fields___date] }
      ${process.env.NODE_ENV === 'production' ? draftFilter : ''}
    ) {
      edges {
        node {
          id
          html
          excerpt
          fields {
            path
            slug
            rawDate: date
            date(formatString: "DD MMM YYYY")
          }
          frontmatter {
            title
            draft
            category
          }
        }
      }
    }
  }
`;

exports.createPages = async ({ actions, graphql }) => {
  const { createPage } = actions;
  const blogPostTemplate = path.resolve(`src/templates/post.js`);

  const query = graphql(listPosts);

  return query.then(result => {
    if (result.errors) {
      return Promise.reject(result);
    }

    const posts = result.data.allMarkdownRemark.edges;
    posts.forEach(({ node }, index) => {
      createPage({
        path: node.fields.path,
        component: blogPostTemplate,
        context: { slug: node.fields.slug },
      });
    });
    return [];
  });
};

exports.createSchemaCustomization = ({ actions }) => {
  const { createTypes } = actions;
  const typeDefs = `
    type MarkdownRemark implements Node @dontInfer {
      frontmatter: Frontmatter!
      fields: MarkdownRemarkFields
    }

    type MarkdownRemarkFields {
      slug: String!
      path: String!
      date: Date @dateformat
    }

    type Frontmatter @infer {
      title: String
      category: String
      draft: Boolean
      excerpt: String
      featured: File @fileByRelativePath
    }
  `;
  createTypes(typeDefs);
};

exports.onCreateNode = ({ node, getNode, actions }) => {
  const { createNodeField } = actions;
  if (node.internal.type === `MarkdownRemark`) {
    const filename = createFilePath({ node, getNode, basePath: `pages` });
    // get the date and title from the file name
    const [, date, slug] = filename.match(/^\/([\d]{4}-[\d]{2}-[\d]{2})-{1}(.+)\/$/);
    const date_parts = date.split('-');
    const path = '/' + date_parts.concat([slug]).join('/');
    createNodeField({ node, name: `slug`, value: slug });
    createNodeField({ node, name: `date`, value: date });
    createNodeField({ node, name: `path`, value: path });
  }
};
<commit_msg>remove the trailing slash<commit_after>/**
 * Implement Gatsby's Node APIs in this file.
 *
 * See: https://www.gatsbyjs.org/docs/node-apis/
 */

// You can delete this file if you're not using it
//
//
const { createFilePath } = require(`gatsby-source-filesystem`);
const path = require('path');

const draftFilter = `
    filter: {
      frontmatter: { draft: { ne: true }}
    }
  `;

const listPosts = `
  query IndexQuery {
    allMarkdownRemark(
      sort: { order: DESC, fields: [fields___date] }
      ${process.env.NODE_ENV === 'production' ? draftFilter : ''}
    ) {
      edges {
        node {
          id
          html
          excerpt
          fields {
            path
            slug
            rawDate: date
            date(formatString: "DD MMM YYYY")
          }
          frontmatter {
            title
            draft
            category
          }
        }
      }
    }
  }
`;

exports.createPages = async ({ actions, graphql }) => {
  const { createPage } = actions;
  const blogPostTemplate = path.resolve(`src/templates/post.js`);

  const query = graphql(listPosts);

  return query.then(result => {
    if (result.errors) {
      return Promise.reject(result);
    }

    const posts = result.data.allMarkdownRemark.edges;
    posts.forEach(({ node }, index) => {
      createPage({
        path: node.fields.path,
        component: blogPostTemplate,
        context: { slug: node.fields.slug },
      });
    });
    return [];
  });
};

exports.createSchemaCustomization = ({ actions }) => {
  const { createTypes } = actions;
  const typeDefs = `
    type MarkdownRemark implements Node @dontInfer {
      frontmatter: Frontmatter!
      fields: MarkdownRemarkFields
    }

    type MarkdownRemarkFields {
      slug: String!
      path: String!
      date: Date @dateformat
    }

    type Frontmatter @infer {
      title: String
      category: String
      draft: Boolean
      excerpt: String
      featured: File @fileByRelativePath
    }
  `;
  createTypes(typeDefs);
};

exports.onCreateNode = ({ node, getNode, actions }) => {
  const { createNodeField } = actions;
  if (node.internal.type === `MarkdownRemark`) {
    const filename = createFilePath({ node, getNode, basePath: `pages`, trailingSlash: false });
    console.log(filename);
    // get the date and title from the file name
    const [, date, slug] = filename.match(/^\/([\d]{4}-[\d]{2}-[\d]{2})-{1}(.+)$/);
    const date_parts = date.split('-');
    const path = '/' + date_parts.concat([slug]).join('/');
    createNodeField({ node, name: `slug`, value: slug });
    createNodeField({ node, name: `date`, value: date });
    createNodeField({ node, name: `path`, value: path });
  }
};
<|endoftext|>


<commit_before>const path = require('path');
const _ = require('lodash');
const webpackLodashPlugin = require('lodash-webpack-plugin');
const crypto = require('crypto');
const redirects = require('./redirects.js');

/**
 * Generate node edges
 *
 * @param {any} { node, boundActionCreators, getNode }
 */
exports.onCreateNode = ({ node, boundActionCreators, getNode }) => {
  const { createNodeField } = boundActionCreators;

  /**
   * Add slug edge
   */
  let slug;
  if (node.internal.type === 'MarkdownRemark') {
    const fileNode = getNode(node.parent);
    const parsedFilePath = path.parse(fileNode.relativePath);
    if (
      Object.prototype.hasOwnProperty.call(node, 'frontmatter') &&
      Object.prototype.hasOwnProperty.call(node.frontmatter, 'slug')
    ) {
      slug = `/${node.frontmatter.slug}`;
    } else if (parsedFilePath.name !== 'index' && parsedFilePath.dir !== '') {
      slug = `/${parsedFilePath.name}/`;
    } else {
      slug = `/${parsedFilePath.dir}/`;
    }
    if (
      Object.prototype.hasOwnProperty.call(node, 'frontmatter') &&
      Object.prototype.hasOwnProperty.call(node.frontmatter, 'slug')
    ) {
      slug = `/${_.kebabCase(node.frontmatter.slug)}`;
    }
    createNodeField({ node, name: 'slug', value: _.kebabCase(slug).toLowerCase() });

    /**
     * Add permalink edge
     *
     * If there is a path frontmatter - that overrides all. Otherwise, we'll use the nested directory structure to build the permalink.
     */
    let permalink;
    if (
      Object.prototype.hasOwnProperty.call(node, 'frontmatter') &&
      Object.prototype.hasOwnProperty.call(node.frontmatter, 'path')
    ) {
      permalink = `/${node.frontmatter.path}${slug}`;
    } else if (parsedFilePath.dir !== '') {
      permalink = `/${parsedFilePath.dir}${slug}`;
    } else {
      permalink = slug;
    }
    createNodeField({ node, name: 'permalink', value: permalink.toLowerCase() });

    /**
     * Check if doc is "ui" or "for developers" and add a field slug to represent this.
     */
    let docType;
    if (permalink.match(/ui\/[^/]+/)) {
      docType = 'ui';
    } else if (permalink.match(/for-developers\/[^/]+/)) {
      docType = 'for-developers';
    } else if (permalink.match(/glossary\/[^/]+/)) {
      docType = 'glossary';
    } else if (permalink.match(/release-notes\/[^/]+/)) {
      docType = 'release-notes';
    }

    createNodeField({ node, name: 'docType', value: docType });

    let cat;
    if (
      Object.prototype.hasOwnProperty.call(node, 'frontmatter') &&
      Object.prototype.hasOwnProperty.call(node.frontmatter, 'category')
    ) {
      cat = node.frontmatter.category;
    } else {
      // remove docType prefix
      cat = parsedFilePath.dir.replace(`${docType}`, '');
      cat = cat.split('/');
      cat = (cat.length > 1 && cat[1].length) ? cat[1] : 'uncategorized';
    }

    createNodeField({ node, name: 'category', value: cat });

    let group = 'ungrouped';
    if (
      Object.prototype.hasOwnProperty.call(node, 'frontmatter') &&
      Object.prototype.hasOwnProperty.call(node.frontmatter, 'group')
    ) {
      group = node.frontmatter.group;
    }
    createNodeField({ node, name: 'group', value: group });

    let title;
    if (
      Object.prototype.hasOwnProperty.call(node, 'frontmatter') &&
      Object.prototype.hasOwnProperty.call(node.frontmatter, 'title')
    ) {
      title = node.frontmatter.title;
    } else {
      title = parsedFilePath.name.replace('-', '');
    }
    createNodeField({ node, name: 'title', value: title });
  }
};

exports.createPages = ({ graphql, boundActionCreators }) => {
  const { createPage, createRedirect } = boundActionCreators;

  // Redirect /ui/ to /docs/ as it's not actually nested like /docs/for-developers/
  createRedirect({
    fromPath: '/ui/',
    isPermanent: true,
    redirectInBrowser: true,
    toPath: '/',
  });

  createRedirect({
    fromPath: '/ui',
    isPermanent: true,
    redirectInBrowser: true,
    toPath: '/',
  });

  redirects.forEach((redirect) => {
    createRedirect({
      fromPath: redirect.from,
      isPermanent: true,
      redirectInBrowser: true,
      toPath: redirect.to,
    });
  });

  return new Promise((resolve, reject) => {
    const docsPage = path.resolve('src/templates/doc.jsx');
    const categoryPage = path.resolve('src/templates/category.jsx');

    resolve(graphql(`
        {
          allMarkdownRemark {
            edges {
              node {
                id
                fileAbsolutePath
                fields {
                  permalink
                  slug
                  category
                  docType
                }
              }
            }
          }
        }
      `).then((result) => {
      if (result.errors) {
        /* eslint no-console: "off" */
        console.log(result.errors);
        reject(result.errors);
      }

      const helpCategorySet = new Set();
      const developerCategorySet = new Set();

      result.data.allMarkdownRemark.edges.forEach((edge) => {
        const {
          category,
          docType,
        } = edge.node.fields;

        // aggregate "ui" categories
        if (docType === 'ui') {
          helpCategorySet.add(category);
        }

        // aggregate "for-developers" categories
        if (docType === 'for-developers') {
          developerCategorySet.add(category);
        }

        // Create docs pages
        const { permalink } = edge.node.fields;

        createPage({
          path: permalink,
          component: docsPage,
          context: {
            slug: edge.node.fields.slug,
            id: edge.node.id,
          },
        });
      });

      const categoryList = Array.from(developerCategorySet);
      categoryList.forEach((category, i) => {
        // Create "for-developer" category nodes.
        const cat = {
          id: `${i}`,
          slug: category,
          parent: '__SOURCE__',
          children: [],
          internal: {
            type: 'forDeveloperCategories',
          },
        };
        // Get content digest of node. (Required field)
        const contentDigest = crypto
          .createHash('md5')
          .update(JSON.stringify(cat))
          .digest('hex');

        // add it to contentNode
        cat.internal.contentDigest = contentDigest;

        // Create "/for-developers/<category-slug>" pages.
        createPage({
          path: `/for-developers/${_.kebabCase(category)}/`,
          component: categoryPage,
          context: {
            docType: 'for-developers',
            category,
          },
        });
      });

      const helpCategoryList = Array.from(helpCategorySet);
      helpCategoryList.forEach((category, i) => {
        // Create "ui" category nodes.
        const cat = {
          id: `${i}`,
          slug: category,
          parent: '__SOURCE__',
          children: [],
          internal: {
            type: 'helpSupportCategories',
          },
        };

        // Get content digest of node. (Required field)
        const contentDigest = crypto
          .createHash('md5')
          .update(JSON.stringify(cat))
          .digest('hex');

        // add it to userNode
        cat.internal.contentDigest = contentDigest;

        // Create "/ui/<category-slug>" pages.
        createPage({
          path: `/ui/${_.kebabCase(category)}/`,
          component: categoryPage,
          context: {
            docType: 'ui',
            category,
          },
        });
      });
    }));
  });
};

exports.onCreatePage = async ({ page, boundActionCreators }) => {
  const { createPage } = boundActionCreators;

  return new Promise((resolve) => {
    if (page.path.match(/^\/404/)) {
      // It's assumed that `landingPage.js` exists in the `/layouts/` directory
      page.layout = 'noHeaderFooter';

      // Update the page.
      createPage(page);
    }

    resolve();
  });
};

exports.modifyWebpackConfig = ({ config, stage }) => {
  if (stage === 'build-javascript') {
    config.plugin('Lodash', webpackLodashPlugin, null);
  }
};

// SASS Sourcemaps
exports.modifyWebpackConfig = ({ config }) => {
  const newConfig = { ...config };
  const loadersArr = ['style', 'css?sourceMap', 'sass', 'sass?sourceMap'];
  if (config._loaders.sass.config.loaders) {
    newConfig._loaders.sass.config.loaders = loadersArr;
  }
  return newConfig;
};
<commit_msg>remove redirects<commit_after>const path = require('path');
const _ = require('lodash');
const webpackLodashPlugin = require('lodash-webpack-plugin');
const crypto = require('crypto');
const redirects = require('./redirects.js');

/**
 * Generate node edges
 *
 * @param {any} { node, boundActionCreators, getNode }
 */
exports.onCreateNode = ({ node, boundActionCreators, getNode }) => {
  const { createNodeField } = boundActionCreators;

  /**
   * Add slug edge
   */
  let slug;
  if (node.internal.type === 'MarkdownRemark') {
    const fileNode = getNode(node.parent);
    const parsedFilePath = path.parse(fileNode.relativePath);
    if (
      Object.prototype.hasOwnProperty.call(node, 'frontmatter') &&
      Object.prototype.hasOwnProperty.call(node.frontmatter, 'slug')
    ) {
      slug = `/${node.frontmatter.slug}`;
    } else if (parsedFilePath.name !== 'index' && parsedFilePath.dir !== '') {
      slug = `/${parsedFilePath.name}/`;
    } else {
      slug = `/${parsedFilePath.dir}/`;
    }
    if (
      Object.prototype.hasOwnProperty.call(node, 'frontmatter') &&
      Object.prototype.hasOwnProperty.call(node.frontmatter, 'slug')
    ) {
      slug = `/${_.kebabCase(node.frontmatter.slug)}`;
    }
    createNodeField({ node, name: 'slug', value: _.kebabCase(slug).toLowerCase() });

    /**
     * Add permalink edge
     *
     * If there is a path frontmatter - that overrides all. Otherwise, we'll use the nested directory structure to build the permalink.
     */
    let permalink;
    if (
      Object.prototype.hasOwnProperty.call(node, 'frontmatter') &&
      Object.prototype.hasOwnProperty.call(node.frontmatter, 'path')
    ) {
      permalink = `/${node.frontmatter.path}${slug}`;
    } else if (parsedFilePath.dir !== '') {
      permalink = `/${parsedFilePath.dir}${slug}`;
    } else {
      permalink = slug;
    }
    createNodeField({ node, name: 'permalink', value: permalink.toLowerCase() });

    /**
     * Check if doc is "ui" or "for developers" and add a field slug to represent this.
     */
    let docType;
    if (permalink.match(/ui\/[^/]+/)) {
      docType = 'ui';
    } else if (permalink.match(/for-developers\/[^/]+/)) {
      docType = 'for-developers';
    } else if (permalink.match(/glossary\/[^/]+/)) {
      docType = 'glossary';
    } else if (permalink.match(/release-notes\/[^/]+/)) {
      docType = 'release-notes';
    }

    createNodeField({ node, name: 'docType', value: docType });

    let cat;
    if (
      Object.prototype.hasOwnProperty.call(node, 'frontmatter') &&
      Object.prototype.hasOwnProperty.call(node.frontmatter, 'category')
    ) {
      cat = node.frontmatter.category;
    } else {
      // remove docType prefix
      cat = parsedFilePath.dir.replace(`${docType}`, '');
      cat = cat.split('/');
      cat = (cat.length > 1 && cat[1].length) ? cat[1] : 'uncategorized';
    }

    createNodeField({ node, name: 'category', value: cat });

    let group = 'ungrouped';
    if (
      Object.prototype.hasOwnProperty.call(node, 'frontmatter') &&
      Object.prototype.hasOwnProperty.call(node.frontmatter, 'group')
    ) {
      group = node.frontmatter.group;
    }
    createNodeField({ node, name: 'group', value: group });

    let title;
    if (
      Object.prototype.hasOwnProperty.call(node, 'frontmatter') &&
      Object.prototype.hasOwnProperty.call(node.frontmatter, 'title')
    ) {
      title = node.frontmatter.title;
    } else {
      title = parsedFilePath.name.replace('-', '');
    }
    createNodeField({ node, name: 'title', value: title });
  }
};

exports.createPages = ({ graphql, boundActionCreators }) => {
  const { createPage } = boundActionCreators;

  return new Promise((resolve, reject) => {
    const docsPage = path.resolve('src/templates/doc.jsx');
    const categoryPage = path.resolve('src/templates/category.jsx');

    resolve(graphql(`
        {
          allMarkdownRemark {
            edges {
              node {
                id
                fileAbsolutePath
                fields {
                  permalink
                  slug
                  category
                  docType
                }
              }
            }
          }
        }
      `).then((result) => {
      if (result.errors) {
        /* eslint no-console: "off" */
        console.log(result.errors);
        reject(result.errors);
      }

      const helpCategorySet = new Set();
      const developerCategorySet = new Set();

      result.data.allMarkdownRemark.edges.forEach((edge) => {
        const {
          category,
          docType,
        } = edge.node.fields;

        // aggregate "ui" categories
        if (docType === 'ui') {
          helpCategorySet.add(category);
        }

        // aggregate "for-developers" categories
        if (docType === 'for-developers') {
          developerCategorySet.add(category);
        }

        // Create docs pages
        const { permalink } = edge.node.fields;

        createPage({
          path: permalink,
          component: docsPage,
          context: {
            slug: edge.node.fields.slug,
            id: edge.node.id,
          },
        });
      });

      const categoryList = Array.from(developerCategorySet);
      categoryList.forEach((category, i) => {
        // Create "for-developer" category nodes.
        const cat = {
          id: `${i}`,
          slug: category,
          parent: '__SOURCE__',
          children: [],
          internal: {
            type: 'forDeveloperCategories',
          },
        };
        // Get content digest of node. (Required field)
        const contentDigest = crypto
          .createHash('md5')
          .update(JSON.stringify(cat))
          .digest('hex');

        // add it to contentNode
        cat.internal.contentDigest = contentDigest;

        // Create "/for-developers/<category-slug>" pages.
        createPage({
          path: `/for-developers/${_.kebabCase(category)}/`,
          component: categoryPage,
          context: {
            docType: 'for-developers',
            category,
          },
        });
      });

      const helpCategoryList = Array.from(helpCategorySet);
      helpCategoryList.forEach((category, i) => {
        // Create "ui" category nodes.
        const cat = {
          id: `${i}`,
          slug: category,
          parent: '__SOURCE__',
          children: [],
          internal: {
            type: 'helpSupportCategories',
          },
        };

        // Get content digest of node. (Required field)
        const contentDigest = crypto
          .createHash('md5')
          .update(JSON.stringify(cat))
          .digest('hex');

        // add it to userNode
        cat.internal.contentDigest = contentDigest;

        // Create "/ui/<category-slug>" pages.
        createPage({
          path: `/ui/${_.kebabCase(category)}/`,
          component: categoryPage,
          context: {
            docType: 'ui',
            category,
          },
        });
      });
    }));
  });
};

exports.onCreatePage = async ({ page, boundActionCreators }) => {
  const { createPage } = boundActionCreators;

  return new Promise((resolve) => {
    if (page.path.match(/^\/404/)) {
      // It's assumed that `landingPage.js` exists in the `/layouts/` directory
      page.layout = 'noHeaderFooter';

      // Update the page.
      createPage(page);
    }

    resolve();
  });
};

exports.modifyWebpackConfig = ({ config, stage }) => {
  if (stage === 'build-javascript') {
    config.plugin('Lodash', webpackLodashPlugin, null);
  }
};

// SASS Sourcemaps
exports.modifyWebpackConfig = ({ config }) => {
  const newConfig = { ...config };
  const loadersArr = ['style', 'css?sourceMap', 'sass', 'sass?sourceMap'];
  if (config._loaders.sass.config.loaders) {
    newConfig._loaders.sass.config.loaders = loadersArr;
  }
  return newConfig;
};
<|endoftext|>


<commit_before>'use strict'

const EventEmitter = require('events').EventEmitter
const qs = require('querystring')
const util = require('util')
const uuid = require('uuid')
const _ = require('lodash/fp')

const BN = require('./bn')
const E = require('./error')

const mapValuesWithKey = _.mapValues.convert({ 'cap': false })

const _request = require('./request')
const logs = require('./logs')

const NETWORK_DOWN_COUNT_THRESHOLD = 3
const DISPENSE_TIMEOUT = 120000
const NETWORK_TIMEOUT = 5000
const LOGS_SYNC_INTERVAL = 5000

let serialNumber = 0
let networkDownCount = 0
const pid = uuid.v4()

// TODO: need to pass global options to request
const Trader = function (protocol, clientCert, connectionInfo) {
  if (!(this instanceof Trader)) return new Trader(protocol, clientCert, connectionInfo)
  EventEmitter.call(this)

  const globalOptions = {
    protocol,
    connectionInfo,
    clientCert
  }

  this.request = options => _request.request(this.configVersion, globalOptions, options)
  this.globalOptions = globalOptions
  this.balanceRetries = 0
  this.pollRetries = 0
  this.state = { state: 'initial', isIdle: false }
  this.configVersion = null
  this.dispenseIntervalPointer = null
  this.terms = false

  // Start logs sync
  setInterval(this.syncLogs.bind(this), LOGS_SYNC_INTERVAL)
  this.syncLogs()
}

util.inherits(Trader, EventEmitter)

/**
 * Synchronize logs with the server
 *
 * @name syncLogs
 * @function
 *
 * @returns {null}
 */
Trader.prototype.syncLogs = function syncLogs () {
  // Get last seen timestamp from server
  this.request({ path: '/logs', method: 'get' })
    .then(data => data.body)
  // Delete log files that are two or more days old
    .then((it) => {
      const twoDaysAgo = (() => {
        let date = new Date()

	// Notice that `setDate()` can take negative values. So if you'd take
	// -2 days on the 1st of April you'd get the 30th of March. Several
	// examples can be seen at: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setDate#Using_setDate()
        date.setDate(date.getDate() - 2)
	return date
      })()

      return logs.removeLogFiles(twoDaysAgo).then(() => it)
    })
  // Load unseen logs to send
    .then(logs.queryNewestLogs)
  // Send unsaved logs to server
    .then(logs => {
      if (logs.length === 0) return
      return this.request({
        path: '/logs',
        method: 'POST',
        body: { logs }
      })
    })
    .catch(err => console.log(`Sync logs error: ${err}`))
}

Trader.prototype.clearConfigVersion = function clearConfigVersion () {
  this.configVersion = null
}

Trader.prototype.setConfigVersion = function setConfigVersion () {
  if (!this.latestConfigVersion) throw new Error('We don\'t have a configVersion')
  this.configVersion = this.latestConfigVersion
}

Trader.prototype.verifyUser = function verifyUser (idRec, cb) {
  console.log(idRec)
  return this.request({
    path: '/verify_user',
    method: 'POST',
    body: idRec
  })
}

Trader.prototype.rates = function rates (cryptoCode) {
  if (this._rates) return this._rates[cryptoCode]
}

Trader.prototype.verifyTransaction = function verifyTransaction (idRec) {
  console.log(idRec)

  return this.request({
    path: '/verify_transaction',
    method: 'POST',
    body: idRec
  }).catch(err => console.log(err))
}

Trader.prototype.reportEvent = function reportEvent (eventType, note) {
  const rec = {
    eventType: eventType,
    note: note,
    deviceTime: Date.now()
  }

  return this.request({
    path: '/event',
    method: 'POST',
    body: rec
  }).catch(err => console.log(err.stack))
}

Trader.prototype.poll = function poll () {
  const stateRec = this.state

  const path = '/poll?state=' + stateRec.state + '&idle=' + stateRec.isIdle + '&pid=' + pid + '&sn=' + serialNumber.toString()
  serialNumber++

  return this.request({
    path,
    method: 'GET',
    noRetry: true
  })
    .then(r => this.pollHandler(r.body))
    .catch(err => this.pollError(err))
}

function massage (tx) {
  if (!tx) return tx

  if (tx.direction === 'cashIn') {
    return _.assign(tx, {
      cryptoAtoms: BN(tx.cryptoAtoms),
      fiat: BN(tx.fiat),
      cashInFee: BN(tx.cashInFee),
      commissionPercentage: BN(tx.commissionPercentage),
      minimumTx: BN(tx.minimumTx)
    })
  }

  return _.assign(tx, {
    cryptoAtoms: BN(tx.cryptoAtoms),
    commissionPercentage: BN(tx.commissionPercentage),
    fiat: BN(tx.fiat)
  })
}

Trader.prototype.postTx = function postTx (tx) {
  // Don't retry because that's handled at a higher level.

  const requestId = uuid.v4()

  return this.request({
    path: '/tx?rid=' + requestId,
    method: 'POST',
    body: tx,
    noRetry: true
  })
    .then(r => massage(r.body))
    .catch(err => {
      if (err.statusCode === 409) {
        const errorType = _.get('response.body.errorType', err, 'ratchet')

        console.log(`Encountered ${errorType} error, txId: %s ${tx.id}`)
        if (errorType === 'stale') throw new E.StaleError('Stale error')
        throw new E.RatchetError('Ratchet error')
      }

      if (err.statusCode >= 500) throw err
      if (isNetworkError(err)) return
      throw err
    })
}

function isNetworkError (err) {
  switch (err.name) {
    case 'RequestError':
    case 'ReadError':
    case 'ParseError':
      return true
    default:
      return false
  }
}

function stateChange (state, isIdle) {
  this.state = { state, isIdle }

  const rec = { state, isIdle, uuid: uuid.v4() }

  // Don't retry because we're only interested in the current state
  // and things could get confused.
  return this.request({
    path: '/state',
    method: 'POST',
    body: rec,
    noRetry: true
  }).catch(() => {})
}

// At machine startup two stateChanges occur virtually at the same time: 'idleState' and 'chooseCoin'
// It was frequently seen on the server requests arriving out of order, the throttle mitigates this issue
// This is particularlly important at startup because of the 'machine stuck' notification
Trader.prototype.stateChange = _.throttle(1000, stateChange)

Trader.prototype.phoneCode = function phoneCode (phone) {
  return this.request({
    path: '/phone_code',
    method: 'POST',
    body: { phone }
  })
    .then(r => r.body)
    .catch(err => {
      if (err && err.statusCode === 401) {
        const badNumberErr = new Error('Bad phone number')
        badNumberErr.name = 'BadNumberError'
        throw badNumberErr
      }

      throw err
    })
}

Trader.prototype.fetchPhoneTx = function fetchPhoneTx (phone) {
  return this.request({
    path: '/tx?' + qs.stringify({ phone }),
    method: 'GET'
  })
    .then(r => massage(r.body))
}

Trader.prototype.updateCustomer = function updateCustomer (customerId, customerPatch) {
  return this.request({
    path: `/customer/${customerId}`,
    body: customerPatch,
    method: 'PATCH'
  })
    .then(r => r.body)
}

Trader.prototype.waitForOneDispense = function waitForOneDispense (tx, status) {
  return this.request({
    path: `/tx/${tx.id}?status=${status}`,
    method: 'GET',
    noRetry: true
  })
    .then(r => massage(r.body))
}

Trader.prototype.cancelDispense = function cancelDispense () {
  this.cancelDispenseFlag = true
}

Trader.prototype.waitForDispense = function waitForDispense (tx, status) {
  let processing = false
  let timedout = false
  const t0 = Date.now()
  this.cancelDispenseFlag = false

  clearInterval(this.dispenseIntervalPointer)

  return new Promise((resolve, reject) => {
    let lastGood = Date.now()

    this.dispenseIntervalPointer = setInterval(() => {
      if (processing) return

      processing = true

      if (this.cancelDispenseFlag) {
        this.cancelDispenseFlag = false
        timedout = true
        clearInterval(this.dispenseIntervalPointer)
        return resolve()
      }

      if (Date.now() - t0 > DISPENSE_TIMEOUT) {
        timedout = true
        clearInterval(this.dispenseIntervalPointer)

        const err = new Error('Dispense timeout')
        err.networkDown = Date.now() - lastGood > NETWORK_TIMEOUT

        return reject(err)
      }

      this.waitForOneDispense(tx, status)
        .then(newTx => {
          processing = false

          if (!newTx || timedout) return

          clearInterval(this.dispenseIntervalPointer)

          return resolve(newTx)
        })
        .catch(err => {
          processing = false

          if (err.statusCode === 304) {
            lastGood = Date.now()
          }

          if (timedout || isNetworkError(err)) return

          clearInterval(this.dispenseIntervalPointer)
          return reject(err)
        })
    }, 1000)
  })
}

function toBN (obj) {
  try {
    return BN(obj)
  } catch (__) {
    return obj
  }
}

Trader.prototype.pollHandler = function pollHandler (res) {
  this.idVerificationLimit = res.idVerificationLimit
  this.idVerificationEnabled = res.idVerificationEnabled
  this.smsVerificationActive = res.smsVerificationActive
  this.smsVerificationThreshold = res.smsVerificationThreshold
  this.hardLimitVerificationActive = res.hardLimitVerificationActive
  this.hardLimitVerificationThreshold = (this.hardLimitVerificationActive)
    ? BN(res.hardLimitVerificationThreshold)
    : BN(Infinity)
  this.idCardDataVerificationActive = res.idCardDataVerificationActive
  this.idCardDataVerificationThreshold = res.idCardDataVerificationThreshold
  this.idCardPhotoVerificationActive = res.idCardPhotoVerificationActive
  this.idCardPhotoVerificationThreshold = res.idCardPhotoVerificationThreshold
  this.sanctionsVerificationActive = res.sanctionsVerificationActive
  this.sanctionsVerificationThreshold = res.sanctionsVerificationThreshold
  this.crossRefVerificationActive = res.crossRefVerificationActive
  this.crossRefVerificationThreshold = res.crossRefVerificationThreshold
  this.frontCameraVerificationActive = res.frontCameraVerificationActive
  this.frontCameraVerificationThreshold = res.frontCameraVerificationThreshold
  this.locale = res.locale
  this.hasLightning = res.hasLightning
  this.terms = res.terms || false
  this.operatorInfo = res.operatorInfo

  networkDownCount = 0

  if (res.cassettes) {
    const mapper = (v, k) => k === 'denomination' ? BN(v) : v
    this.cassettes = _.map(mapValuesWithKey(mapper), res.cassettes.cassettes)
    this.virtualCassettes = _.map(BN, res.cassettes.virtualCassettes)
  }

  this.twoWayMode = res.twoWayMode
  this.zeroConfLimit = res.zeroConfLimit
  this._rates = _.mapValues(_.mapValues(toBN), res.rates)
  this.coins = _.filter(coin => isActiveCoin(res, coin), _.map(_.mapValues(toBN), res.coins))
  this.balances = _.mapValues(toBN, res.balances)
  this.latestConfigVersion = res.configVersion

  if (_.isEmpty(this.coins)) {
    return this.emit('networkDown')
  }

  if (res.reboot) this.emit('reboot')
  this.emit('pollUpdate', isNewState(this))
  this.emit('networkUp')
}

Trader.prototype.pollError = function pollError (err) {
  if (isNetworkError(err)) {
    networkDownCount++

    if (networkDownCount > NETWORK_DOWN_COUNT_THRESHOLD) {
      return this.emit('networkDown')
    }

    console.log('WARNING: Ephemeral network error [%s]', err.message)

    return
  }

  if (err.statusCode === 403) return this.emit('unpair')

  console.log(err)
  this.emit('networkDown')
}

let oldState = {}
function isNewState (res) {
  const pare = r => ({
    twoWayMode: r.twoWayMode,
    locale: r.locale,
    coins: _.map('cryptoCode', r.coins)
  })

  if (_.isEqual(pare(res), oldState)) return false

  oldState = pare(res)
  return true
}

function isActiveCoin (res, coin) {
  return !_.isNil(res.rates[coin.cryptoCode]) && !_.isNil(res.balances[coin.cryptoCode])
}

module.exports = Trader
<commit_msg>Show stack trace on sync logs error<commit_after>'use strict'

const EventEmitter = require('events').EventEmitter
const qs = require('querystring')
const util = require('util')
const uuid = require('uuid')
const _ = require('lodash/fp')

const BN = require('./bn')
const E = require('./error')

const mapValuesWithKey = _.mapValues.convert({ 'cap': false })

const _request = require('./request')
const logs = require('./logs')

const NETWORK_DOWN_COUNT_THRESHOLD = 3
const DISPENSE_TIMEOUT = 120000
const NETWORK_TIMEOUT = 5000
const LOGS_SYNC_INTERVAL = 5000

let serialNumber = 0
let networkDownCount = 0
const pid = uuid.v4()

// TODO: need to pass global options to request
const Trader = function (protocol, clientCert, connectionInfo) {
  if (!(this instanceof Trader)) return new Trader(protocol, clientCert, connectionInfo)
  EventEmitter.call(this)

  const globalOptions = {
    protocol,
    connectionInfo,
    clientCert
  }

  this.request = options => _request.request(this.configVersion, globalOptions, options)
  this.globalOptions = globalOptions
  this.balanceRetries = 0
  this.pollRetries = 0
  this.state = { state: 'initial', isIdle: false }
  this.configVersion = null
  this.dispenseIntervalPointer = null
  this.terms = false

  // Start logs sync
  setInterval(this.syncLogs.bind(this), LOGS_SYNC_INTERVAL)
  this.syncLogs()
}

util.inherits(Trader, EventEmitter)

/**
 * Synchronize logs with the server
 *
 * @name syncLogs
 * @function
 *
 * @returns {null}
 */
Trader.prototype.syncLogs = function syncLogs () {
  // Get last seen timestamp from server
  this.request({ path: '/logs', method: 'get' })
    .then(data => data.body)
  // Delete log files that are two or more days old
    .then((it) => {
      const twoDaysAgo = (() => {
        let date = new Date()

	// Notice that `setDate()` can take negative values. So if you'd take
	// -2 days on the 1st of April you'd get the 30th of March. Several
	// examples can be seen at: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setDate#Using_setDate()
        date.setDate(date.getDate() - 2)
	return date
      })()

      return logs.removeLogFiles(twoDaysAgo).then(() => it)
    })
  // Load unseen logs to send
    .then(logs.queryNewestLogs)
  // Send unsaved logs to server
    .then(logs => {
      if (logs.length === 0) return
      return this.request({
        path: '/logs',
        method: 'POST',
        body: { logs }
      })
    })
    .catch(err => console.log('Sync logs error:', err))
}

Trader.prototype.clearConfigVersion = function clearConfigVersion () {
  this.configVersion = null
}

Trader.prototype.setConfigVersion = function setConfigVersion () {
  if (!this.latestConfigVersion) throw new Error('We don\'t have a configVersion')
  this.configVersion = this.latestConfigVersion
}

Trader.prototype.verifyUser = function verifyUser (idRec, cb) {
  console.log(idRec)
  return this.request({
    path: '/verify_user',
    method: 'POST',
    body: idRec
  })
}

Trader.prototype.rates = function rates (cryptoCode) {
  if (this._rates) return this._rates[cryptoCode]
}

Trader.prototype.verifyTransaction = function verifyTransaction (idRec) {
  console.log(idRec)

  return this.request({
    path: '/verify_transaction',
    method: 'POST',
    body: idRec
  }).catch(err => console.log(err))
}

Trader.prototype.reportEvent = function reportEvent (eventType, note) {
  const rec = {
    eventType: eventType,
    note: note,
    deviceTime: Date.now()
  }

  return this.request({
    path: '/event',
    method: 'POST',
    body: rec
  }).catch(err => console.log(err.stack))
}

Trader.prototype.poll = function poll () {
  const stateRec = this.state

  const path = '/poll?state=' + stateRec.state + '&idle=' + stateRec.isIdle + '&pid=' + pid + '&sn=' + serialNumber.toString()
  serialNumber++

  return this.request({
    path,
    method: 'GET',
    noRetry: true
  })
    .then(r => this.pollHandler(r.body))
    .catch(err => this.pollError(err))
}

function massage (tx) {
  if (!tx) return tx

  if (tx.direction === 'cashIn') {
    return _.assign(tx, {
      cryptoAtoms: BN(tx.cryptoAtoms),
      fiat: BN(tx.fiat),
      cashInFee: BN(tx.cashInFee),
      commissionPercentage: BN(tx.commissionPercentage),
      minimumTx: BN(tx.minimumTx)
    })
  }

  return _.assign(tx, {
    cryptoAtoms: BN(tx.cryptoAtoms),
    commissionPercentage: BN(tx.commissionPercentage),
    fiat: BN(tx.fiat)
  })
}

Trader.prototype.postTx = function postTx (tx) {
  // Don't retry because that's handled at a higher level.

  const requestId = uuid.v4()

  return this.request({
    path: '/tx?rid=' + requestId,
    method: 'POST',
    body: tx,
    noRetry: true
  })
    .then(r => massage(r.body))
    .catch(err => {
      if (err.statusCode === 409) {
        const errorType = _.get('response.body.errorType', err, 'ratchet')

        console.log(`Encountered ${errorType} error, txId: %s ${tx.id}`)
        if (errorType === 'stale') throw new E.StaleError('Stale error')
        throw new E.RatchetError('Ratchet error')
      }

      if (err.statusCode >= 500) throw err
      if (isNetworkError(err)) return
      throw err
    })
}

function isNetworkError (err) {
  switch (err.name) {
    case 'RequestError':
    case 'ReadError':
    case 'ParseError':
      return true
    default:
      return false
  }
}

function stateChange (state, isIdle) {
  this.state = { state, isIdle }

  const rec = { state, isIdle, uuid: uuid.v4() }

  // Don't retry because we're only interested in the current state
  // and things could get confused.
  return this.request({
    path: '/state',
    method: 'POST',
    body: rec,
    noRetry: true
  }).catch(() => {})
}

// At machine startup two stateChanges occur virtually at the same time: 'idleState' and 'chooseCoin'
// It was frequently seen on the server requests arriving out of order, the throttle mitigates this issue
// This is particularlly important at startup because of the 'machine stuck' notification
Trader.prototype.stateChange = _.throttle(1000, stateChange)

Trader.prototype.phoneCode = function phoneCode (phone) {
  return this.request({
    path: '/phone_code',
    method: 'POST',
    body: { phone }
  })
    .then(r => r.body)
    .catch(err => {
      if (err && err.statusCode === 401) {
        const badNumberErr = new Error('Bad phone number')
        badNumberErr.name = 'BadNumberError'
        throw badNumberErr
      }

      throw err
    })
}

Trader.prototype.fetchPhoneTx = function fetchPhoneTx (phone) {
  return this.request({
    path: '/tx?' + qs.stringify({ phone }),
    method: 'GET'
  })
    .then(r => massage(r.body))
}

Trader.prototype.updateCustomer = function updateCustomer (customerId, customerPatch) {
  return this.request({
    path: `/customer/${customerId}`,
    body: customerPatch,
    method: 'PATCH'
  })
    .then(r => r.body)
}

Trader.prototype.waitForOneDispense = function waitForOneDispense (tx, status) {
  return this.request({
    path: `/tx/${tx.id}?status=${status}`,
    method: 'GET',
    noRetry: true
  })
    .then(r => massage(r.body))
}

Trader.prototype.cancelDispense = function cancelDispense () {
  this.cancelDispenseFlag = true
}

Trader.prototype.waitForDispense = function waitForDispense (tx, status) {
  let processing = false
  let timedout = false
  const t0 = Date.now()
  this.cancelDispenseFlag = false

  clearInterval(this.dispenseIntervalPointer)

  return new Promise((resolve, reject) => {
    let lastGood = Date.now()

    this.dispenseIntervalPointer = setInterval(() => {
      if (processing) return

      processing = true

      if (this.cancelDispenseFlag) {
        this.cancelDispenseFlag = false
        timedout = true
        clearInterval(this.dispenseIntervalPointer)
        return resolve()
      }

      if (Date.now() - t0 > DISPENSE_TIMEOUT) {
        timedout = true
        clearInterval(this.dispenseIntervalPointer)

        const err = new Error('Dispense timeout')
        err.networkDown = Date.now() - lastGood > NETWORK_TIMEOUT

        return reject(err)
      }

      this.waitForOneDispense(tx, status)
        .then(newTx => {
          processing = false

          if (!newTx || timedout) return

          clearInterval(this.dispenseIntervalPointer)

          return resolve(newTx)
        })
        .catch(err => {
          processing = false

          if (err.statusCode === 304) {
            lastGood = Date.now()
          }

          if (timedout || isNetworkError(err)) return

          clearInterval(this.dispenseIntervalPointer)
          return reject(err)
        })
    }, 1000)
  })
}

function toBN (obj) {
  try {
    return BN(obj)
  } catch (__) {
    return obj
  }
}

Trader.prototype.pollHandler = function pollHandler (res) {
  this.idVerificationLimit = res.idVerificationLimit
  this.idVerificationEnabled = res.idVerificationEnabled
  this.smsVerificationActive = res.smsVerificationActive
  this.smsVerificationThreshold = res.smsVerificationThreshold
  this.hardLimitVerificationActive = res.hardLimitVerificationActive
  this.hardLimitVerificationThreshold = (this.hardLimitVerificationActive)
    ? BN(res.hardLimitVerificationThreshold)
    : BN(Infinity)
  this.idCardDataVerificationActive = res.idCardDataVerificationActive
  this.idCardDataVerificationThreshold = res.idCardDataVerificationThreshold
  this.idCardPhotoVerificationActive = res.idCardPhotoVerificationActive
  this.idCardPhotoVerificationThreshold = res.idCardPhotoVerificationThreshold
  this.sanctionsVerificationActive = res.sanctionsVerificationActive
  this.sanctionsVerificationThreshold = res.sanctionsVerificationThreshold
  this.crossRefVerificationActive = res.crossRefVerificationActive
  this.crossRefVerificationThreshold = res.crossRefVerificationThreshold
  this.frontCameraVerificationActive = res.frontCameraVerificationActive
  this.frontCameraVerificationThreshold = res.frontCameraVerificationThreshold
  this.locale = res.locale
  this.hasLightning = res.hasLightning
  this.terms = res.terms || false
  this.operatorInfo = res.operatorInfo

  networkDownCount = 0

  if (res.cassettes) {
    const mapper = (v, k) => k === 'denomination' ? BN(v) : v
    this.cassettes = _.map(mapValuesWithKey(mapper), res.cassettes.cassettes)
    this.virtualCassettes = _.map(BN, res.cassettes.virtualCassettes)
  }

  this.twoWayMode = res.twoWayMode
  this.zeroConfLimit = res.zeroConfLimit
  this._rates = _.mapValues(_.mapValues(toBN), res.rates)
  this.coins = _.filter(coin => isActiveCoin(res, coin), _.map(_.mapValues(toBN), res.coins))
  this.balances = _.mapValues(toBN, res.balances)
  this.latestConfigVersion = res.configVersion

  if (_.isEmpty(this.coins)) {
    return this.emit('networkDown')
  }

  if (res.reboot) this.emit('reboot')
  this.emit('pollUpdate', isNewState(this))
  this.emit('networkUp')
}

Trader.prototype.pollError = function pollError (err) {
  if (isNetworkError(err)) {
    networkDownCount++

    if (networkDownCount > NETWORK_DOWN_COUNT_THRESHOLD) {
      return this.emit('networkDown')
    }

    console.log('WARNING: Ephemeral network error [%s]', err.message)

    return
  }

  if (err.statusCode === 403) return this.emit('unpair')

  console.log(err)
  this.emit('networkDown')
}

let oldState = {}
function isNewState (res) {
  const pare = r => ({
    twoWayMode: r.twoWayMode,
    locale: r.locale,
    coins: _.map('cryptoCode', r.coins)
  })

  if (_.isEqual(pare(res), oldState)) return false

  oldState = pare(res)
  return true
}

function isActiveCoin (res, coin) {
  return !_.isNil(res.rates[coin.cryptoCode]) && !_.isNil(res.balances[coin.cryptoCode])
}

module.exports = Trader
<|endoftext|>


<commit_before>import { StateAndEffect } from './sideEffects';

const REPLAY_HAPPENED = Symbol('replayHappened');

export function enableEffects(applyMiddleware, opts = {}) {
  var DEBUG = opts.debug || true

  return (...middlewares) => {
    return nextStoreEnhancer => (defaultReducer, initialState) => {
      // Here we will collect all actions that need to be runned after original
      // dispatch has runned. In case next dispatch produce aditional effects
      // they will be enqued and performed in order in which they appeared.
      // Listeners will be notified after all effects has been performed.
      let pendingEffects = [];

      let currentReducer = defaultReducer;
      let store;

      /*
      const listeners = [];
      let pendingListen = false;

      function subscribe(listener) {
        function abordableListener() {
          if (pendingEffects.length === 0) {
            if (DEBUG) { console.log('injected listener will be called'); }
            listener();
            pendingListen = false;
          } else {
            if (DEBUG) { console.warn('skip listeners we have pending effects: ', pendingEffects); }
            pendingListen = true;
          }
        }
        listeners.push(listener);

        const originalUnsubscribe = store.subscribe(abordableListener);

        return function unsubscribe() {
          originalUnsubscribe();
          const index = listeners.indexOf(listener);
          listeners.splice(index, 1);
        };
      }

      function notifyListenersIfNeeded() {
        if (pendingListen && (pendingEffects.length === 0)) {
          if (DEBUG) { console.log('performSideEffectsMiddleware call pending listeners'); }
          pendingListen = false;
          listeners.slice().forEach(listener => listener());
        }
      }
      */

      function unliftReducer(reducer) {
        return (state, action) => {
          const newState = reducer(state, action);
          if (newState instanceof StateAndEffect) {
            if (!action[REPLAY_HAPPENED]) {
              if (DEBUG) { console.log('adding side effect ', newState.effects); }
              pendingEffects.push(...(newState.effects));
              action[REPLAY_HAPPENED] = true;
            }
            return newState.state;
          }
          return newState;
        };
      }

      function getReducer() {
        return currentReducer;
      }
      function replaceReducer(nextReducer) {
        currentReducer = nextReducer;
        store.replaceReducer(unliftReducer(currentReducer));
      }

      function performSideEffectsMiddleware() { // this function can take {dispatch, getState} as argument
        return next => action => {
          if (DEBUG) { console.log('performSideEffectsMiddleware dispatch happened with:', JSON.stringify(action), action); }
          // we dispatch normaly action to next middleware
          const ret = next(action);
          // after dispatch we check if we have effects to run and if yes than
          // we will dispatch them. That mean that they will go back trough
          // this function
          while (pendingEffects.length > 0) {
            if (DEBUG) { console.log('performSideEffectsMiddleware removing side effect '); }
            next(pendingEffects.shift());
          }

          // After we run effects maybe we discarded norification for listeners
          // so we will try run them again
          // notifyListenersIfNeeded();

          // we will return same thing to keep compatibility
          return ret;
        };
      }

      store = applyMiddleware(performSideEffectsMiddleware, ...middlewares)(nextStoreEnhancer)(unliftReducer(currentReducer), initialState);

      return {
        ...store,
    //    subscribe,
        getReducer,
        replaceReducer
      };
    };
  };
}
<commit_msg>subscribe just exports the store subscribe<commit_after>import { StateAndEffect } from './sideEffects';

const REPLAY_HAPPENED = Symbol('replayHappened');

export function enableEffects(applyMiddleware, opts = {}) {
  var DEBUG = opts.debug || true

  return (...middlewares) => {
    return nextStoreEnhancer => (defaultReducer, initialState) => {
      // Here we will collect all actions that need to be runned after original
      // dispatch has runned. In case next dispatch produce aditional effects
      // they will be enqued and performed in order in which they appeared.
      // Listeners will be notified after all effects has been performed.
      let pendingEffects = [];

      let currentReducer = defaultReducer;
      let store;

      function subscribe(listener) { return store.subscribe(listener) }

      function unliftReducer(reducer) {
        return (state, action) => {
          const newState = reducer(state, action);
          if (newState instanceof StateAndEffect) {
            if (!action[REPLAY_HAPPENED]) {
              if (DEBUG) { console.log('adding side effect ', newState.effects); }
              pendingEffects.push(...(newState.effects));
              action[REPLAY_HAPPENED] = true;
            }
            return newState.state;
          }
          return newState;
        };
      }

      function getReducer() {
        return currentReducer;
      }
      function replaceReducer(nextReducer) {
        currentReducer = nextReducer;
        store.replaceReducer(unliftReducer(currentReducer));
      }

      function performSideEffectsMiddleware() { // this function can take {dispatch, getState} as argument
        return next => action => {
          if (DEBUG) { console.log('performSideEffectsMiddleware dispatch happened with:', JSON.stringify(action), action); }
          // we dispatch normaly action to next middleware
          const ret = next(action);
          // after dispatch we check if we have effects to run and if yes than
          // we will dispatch them. That mean that they will go back trough
          // this function
          while (pendingEffects.length > 0) {
            if (DEBUG) { console.log('performSideEffectsMiddleware removing side effect '); }
            next(pendingEffects.shift());
          }

          // we will return same thing to keep compatibility
          return ret;
        };
      }

      store = applyMiddleware(performSideEffectsMiddleware, ...middlewares)(nextStoreEnhancer)(unliftReducer(currentReducer), initialState);

      return {
        ...store,
        subscribe,
        getReducer,
        replaceReducer
      };
    };
  };
}
<|endoftext|>


<commit_before>var _ = require('lodash'),
	acorn = require('acorn'),
	async = require('async'),
	chalk = require('chalk'),
	constants = require('./constants'),
	fs = require('fs-extra'),
	path = require('path'),
	tiapp = require('tiapp.xml'),
	readline = require('readline'),
	server = require('./server'),
	spinner = require('char-spinner'),
	titanium = require('./titanium'),
	request = require('request');

var DEFAULTS = {
	PROJECT: '_tmp',
	ID: 'triple.tmpapp'
};

// some colors
var error = chalk.bold.red,
	warn = chalk.bold.yellow;

var tabCompletionNS = {};

module.exports = function(opts, callback) {
	var buffer = [];

	callback = arguments[arguments.length-1];
	if (!opts || _.isFunction(opts)) {
		opts = {};
	}

	if (opts.verbose) {
		titanium.verbose = true;
	}

	var rl = readline.createInterface({
		input: process.stdin,
		output: process.stdout,
		completer: tabCompletion(rl)
	});

	async.series([

		// make sure we're logged in
		function(cb) {
			titanium.loggedIn(function(err, loggedIn) {
				if (err) {
					return cb('error checking status: ' + err);
				} else if (!loggedIn) {
					return cb('You must be logged in to use triple. Use `titanium login`.');
				}
				return cb();
			});
		},

		// setup titanium completion database
		function (cb) {
			titanium.sdk(function(err,sdk, version, info){
				var cacheFile = path.join(sdk.path,'triple.cache');
				if (fs.existsSync(cacheFile)) {
					return fs.readFile(cacheFile, function(err,contents){
						contents && (tabCompletionSetup(JSON.parse(contents.toString())));
						cb(err);
					});
				}
				var interval = spinner();
				fs.readFile(path.join(sdk.path,'api.jsca'), function(err,buf){
					if (err) {
						clearInterval(interval);
						console.warn(warn('warn: could not find api.jsca file, auto-complete disabled'));
						return cb();
					}
					var tabCompletionDB = [];
					JSON.parse(buf.toString()).types.forEach(function(entry){
						tabCompletionDB.push(entry.name);
						entry.properties && entry.properties.forEach(function(prop){
							tabCompletionDB.push(entry.name+'.'+prop.name);
						});
						entry.functions && entry.functions.forEach(function(fn){
							tabCompletionDB.push(entry.name+'.'+fn.name);
						});
					});
					fs.writeFile(cacheFile, JSON.stringify(tabCompletionDB), function(err){
						clearInterval(interval);
						tabCompletionSetup(tabCompletionDB);
						cb(err);
					});
				});
			});
		},

		// create the repl project
		function(cb) {
			if (!fs.existsSync(DEFAULTS.PROJECT)) {
				console.log('[creating app]');
				var interval = spinner();
				var createOpts = _.defaults(opts.create || {}, {
					name: DEFAULTS.PROJECT,
					id: DEFAULTS.ID
				});
				titanium.create(createOpts, function(err, results) {
					clearInterval(interval);
					cb(err, results);
				});
			} else {
				cb();
			}
		},

		// prep app
		function(cb) {
			var resources = path.join(DEFAULTS.PROJECT, 'Resources'),
				app = path.join(__dirname, '..', 'app');

			fs.readdirSync(app).forEach(function(file) {
				copy(path.join(app, file), path.join(resources, file));
			});
			copy(path.join(__dirname, 'constants.js'), path.join(resources, 'constants.js'));

			cb();
		},

		// update tiapp.xml
		function(cb) {
			if(opts.module) {
				tiappXml = path.join(DEFAULTS.PROJECT, 'tiapp.xml'),
				tiappLoaded = tiapp.load(path.resolve(tiappXml));

				opts.module.forEach(function(id) {
					console.log('[Injecting module: %s]', id);
					tiappLoaded.setModule(id);
				});
				tiappLoaded.write();
			}

			cb();
		},

		// // start up repl server
		function(cb) {
			server.on('end', function(data) {
				return cb(data);
			});
			server.listen(constants.PORT, cb);
		},

		// build the repl project
		function(cb) {
			console.log('[launching app]');
			var interval = spinner();
			var buildOpts = _.defaults(opts.build || {}, {
				projectDir: DEFAULTS.PROJECT,
				platform: 'ios',
				iosVersion: '7.1',
				noSimFocus: true,
				server: server
			});
			titanium.build(buildOpts);
			server.on('ready', function() {
				clearInterval(interval);
				cb();
			});
		},

		// prompt
		function(cb) {

			// clear spinner and prompt
			process.stdout.write('\r \r');
			rl.setPrompt(constants.PROMPT);
			rl.prompt();

			// handle each line of input
			rl.on('line', function(line) {
				var match;

				// skip empty lines
				if (!line) {
					rl.prompt();
					return;

				// handle triple commands
				} else if (match = line.match(/^\s*(\.\w*)(?:\s|$)/)) {
					var args = match.input.trim().split(/\s+/).slice(1);

					// don't put triple commands on history
					rl.history.shift();

					// execute the command
					switch(match[1].substring(1)) {
						case 'break':
							buffer = [];
							rl.setPrompt(constants.PROMPT);
							break;
						case 'clear':
							server.emit('message', constants.CLEAR_MESSAGE);
							break;
						case 'load':
							load(args, rl);
							break;
						case 'exit':
							return cb();
						case 'save':
							var file = args[0];
							if (!file) {
								console.error(error('must specify path for save'));
							} else {
								fs.ensureDir(path.dirname(file));
								fs.writeFileSync(file, rl.history.slice(0).reverse().join('\n'));
							}
							break;
						default:
							console.error(error('invalid command "' + match[1] + '"'));
							break;
					}
					rl.prompt();

				// process code
				} else {
					try {

						// assemble code from buffer
						buffer.push(line);
						var code = buffer.join('\n');

						// validate the buffered code
						acorn.parse(code);

						// send code to server
						server.emit('message', code);

						// reset buffer and prompt
						buffer = [];
						rl.setPrompt(constants.PROMPT);
					} catch (e) {

						// prompt for multi-line statement
						rl.setPrompt(constants.CONTINUE_PROMPT);
						rl.prompt();
					}
				}

			});

			// handle CTRL+C
			rl.on('SIGINT', function() {

				// If we're in a multi-line statement, break out of it
				if (buffer.length) {
					buffer = [];
					console.log('\n(^C again to quit)');
					rl.setPrompt(constants.PROMPT);
					rl.prompt();

				// quit
				} else {
					cb();
				}
			});

			// do we have a file to load?
			if (opts.args && opts.args.length) {
				load(opts.args, rl);
			}

		}

	], callback);
};

function copy(src, dst) {
	fs.writeFileSync(dst, fs.readFileSync(src));
}

function sendLoadCommand(rl, location, content, delay) {
	try {
		acorn.parse(content);
	} catch (err) {
		console.error(error('error parsing "' + location + '"'));
		console.error(err);
		return rl.prompt();
	}

	var tasks = [];
	content.split('\n').forEach(function(line){
		line = line.trim();
		if (line) {
			tasks.push(function(next){
				setTimeout(function(){
					rl.write(line + '\n');
					next();
				}, delay || 25);
			});
		}
	});
	return async.series(tasks);
}

function load(tokens, rl) {
	var location = tokens[0],
		delay = tokens[1] || 0;

	// looks like a url, fetch it
	if (/^https?:\/\//.test(location)) {
		var interval = spinner();
		return request(location, function(err,content) {
			clearInterval(interval);
			if (err) {
				console.error(error('error loading "' + location + '"'));
				console.error(error(err));
				rl.prompt();
				return;
			}
			sendLoadCommand(rl, location, content.body, delay);
		});

	// probably a file
	} else {
		if (fs.existsSync(location)) {
			return sendLoadCommand(rl, location, fs.readFileSync(location).toString(), delay);
		}
	}

	console.error(error('not sure how to load "' + location + '". does it exist?'));
	rl.prompt();
}

function tabCompletionSetup(tabCompletionDB) {
	if (tabCompletionDB && tabCompletionDB.length) {
		tabCompletionDB.forEach(function(name){
			var tokens = name.split('.'),
				obj = tabCompletionNS;
			for (var c=0;c<tokens.length;c++){
				var cname = tokens[c];
				var entry = obj[cname];
				if (!entry) {
					obj[cname] = entry = {};
				}
				obj = entry;
			}
		});
		tabCompletionDB = null;
		tabCompletionNS.Ti = tabCompletionNS.Titanium;
	}
}

var simpleExpressionRE = /(([a-zA-Z_$](?:\w|\$)*)\.)*([a-zA-Z_$](?:\w|\$)*)\.?$/;
function tabCompletion(rl) {
	return function(line) {

		var match = line.match(simpleExpressionRE);
		line = match[0];

		var tokens = line.split('.'),
			obj = tabCompletionNS,
			hits = [];

		for (var c=0;c<tokens.length;c++) {
			var token = tokens[c];
			if (token) {
				var entry = obj[token];
				if (!entry) {
					hits = Object.keys(obj).filter(function(e){
						return e.indexOf(token)===0;
					});
					obj = null;
					line = token;
					break;
				}
				obj = entry;
			}
		}

		if (obj) {
			hits = Object.keys(obj);
		}

		return [!hits.length && !line ? Object.keys(tabCompletionNS) : hits, line];
	};
}
<commit_msg>cleanup, add require re<commit_after>var _ = require('lodash'),
	acorn = require('acorn'),
	async = require('async'),
	chalk = require('chalk'),
	constants = require('./constants'),
	fs = require('fs-extra'),
	path = require('path'),
	tiapp = require('tiapp.xml'),
	readline = require('readline'),
	server = require('./server'),
	spinner = require('char-spinner'),
	titanium = require('./titanium'),
	request = require('request');

var DEFAULTS = {
	PROJECT: '_tmp',
	ID: 'triple.tmpapp'
};

var error = chalk.bold.red,
	warn = chalk.bold.yellow.
	requireRE = /\brequire\s*\(['"](([\w\.\/-]+\/)?([\w\.\/-]*))/,
	simpleExpressionRE = /(([a-zA-Z_$](?:\w|\$)*)\.)*([a-zA-Z_$](?:\w|\$)*)\.?$/,
	tabCompletionNS = {};

module.exports = function(opts, callback) {
	var buffer = [];

	callback = arguments[arguments.length-1];
	if (!opts || _.isFunction(opts)) {
		opts = {};
	}

	if (opts.verbose) {
		titanium.verbose = true;
	}

	var rl = readline.createInterface({
		input: process.stdin,
		output: process.stdout,
		completer: tabCompletion(rl)
	});

	async.series([

		// make sure we're logged in
		function(cb) {
			titanium.loggedIn(function(err, loggedIn) {
				if (err) {
					return cb('error checking status: ' + err);
				} else if (!loggedIn) {
					return cb('You must be logged in to use triple. Use `titanium login`.');
				}
				return cb();
			});
		},

		// setup titanium completion database
		function (cb) {
			titanium.sdk(function(err,sdk, version, info){
				var cacheFile = path.join(sdk.path,'triple.cache');
				if (fs.existsSync(cacheFile)) {
					return fs.readFile(cacheFile, function(err,contents){
						contents && (tabCompletionSetup(JSON.parse(contents.toString())));
						cb(err);
					});
				}
				var interval = spinner();
				fs.readFile(path.join(sdk.path,'api.jsca'), function(err,buf){
					if (err) {
						clearInterval(interval);
						console.warn(warn('warn: could not find api.jsca file, auto-complete disabled'));
						return cb();
					}
					var tabCompletionDB = [];
					JSON.parse(buf.toString()).types.forEach(function(entry){
						tabCompletionDB.push(entry.name);
						entry.properties && entry.properties.forEach(function(prop){
							tabCompletionDB.push(entry.name+'.'+prop.name);
						});
						entry.functions && entry.functions.forEach(function(fn){
							tabCompletionDB.push(entry.name+'.'+fn.name);
						});
					});
					fs.writeFile(cacheFile, JSON.stringify(tabCompletionDB), function(err){
						clearInterval(interval);
						tabCompletionSetup(tabCompletionDB);
						cb(err);
					});
				});
			});
		},

		// create the repl project
		function(cb) {
			if (!fs.existsSync(DEFAULTS.PROJECT)) {
				console.log('[creating app]');
				var interval = spinner();
				var createOpts = _.defaults(opts.create || {}, {
					name: DEFAULTS.PROJECT,
					id: DEFAULTS.ID
				});
				titanium.create(createOpts, function(err, results) {
					clearInterval(interval);
					cb(err, results);
				});
			} else {
				cb();
			}
		},

		// prep app
		function(cb) {
			var resources = path.join(DEFAULTS.PROJECT, 'Resources'),
				app = path.join(__dirname, '..', 'app');

			fs.readdirSync(app).forEach(function(file) {
				copy(path.join(app, file), path.join(resources, file));
			});
			copy(path.join(__dirname, 'constants.js'), path.join(resources, 'constants.js'));

			cb();
		},

		// update tiapp.xml
		function(cb) {
			if(opts.module) {
				tiappXml = path.join(DEFAULTS.PROJECT, 'tiapp.xml'),
				tiappLoaded = tiapp.load(path.resolve(tiappXml));

				opts.module.forEach(function(id) {
					console.log('[Injecting module: %s]', id);
					tiappLoaded.setModule(id);
				});
				tiappLoaded.write();
			}

			cb();
		},

		// // start up repl server
		function(cb) {
			server.on('end', function(data) {
				return cb(data);
			});
			server.listen(constants.PORT, cb);
		},

		// build the repl project
		function(cb) {
			console.log('[launching app]');
			var interval = spinner();
			var buildOpts = _.defaults(opts.build || {}, {
				projectDir: DEFAULTS.PROJECT,
				platform: 'ios',
				iosVersion: '7.1',
				noSimFocus: true,
				server: server
			});
			titanium.build(buildOpts);
			server.on('ready', function() {
				clearInterval(interval);
				cb();
			});
		},

		// prompt
		function(cb) {

			// clear spinner and prompt
			process.stdout.write('\r \r');
			rl.setPrompt(constants.PROMPT);
			rl.prompt();

			// handle each line of input
			rl.on('line', function(line) {
				var match;

				// skip empty lines
				if (!line) {
					rl.prompt();
					return;

				// handle triple commands
				} else if (match = line.match(/^\s*(\.\w*)(?:\s|$)/)) {
					var args = match.input.trim().split(/\s+/).slice(1);

					// don't put triple commands on history
					rl.history.shift();

					// execute the command
					switch(match[1].substring(1)) {
						case 'break':
							buffer = [];
							rl.setPrompt(constants.PROMPT);
							break;
						case 'clear':
							server.emit('message', constants.CLEAR_MESSAGE);
							break;
						case 'load':
							load(args, rl);
							break;
						case 'exit':
							return cb();
						case 'save':
							var file = args[0];
							if (!file) {
								console.error(error('must specify path for save'));
							} else {
								fs.ensureDir(path.dirname(file));
								fs.writeFileSync(file, rl.history.slice(0).reverse().join('\n'));
							}
							break;
						default:
							console.error(error('invalid command "' + match[1] + '"'));
							break;
					}
					rl.prompt();

				// process code
				} else {
					try {

						// assemble code from buffer
						buffer.push(line);
						var code = buffer.join('\n');

						// validate the buffered code
						acorn.parse(code);

						// send code to server
						server.emit('message', code);

						// reset buffer and prompt
						buffer = [];
						rl.setPrompt(constants.PROMPT);
					} catch (e) {

						// prompt for multi-line statement
						rl.setPrompt(constants.CONTINUE_PROMPT);
						rl.prompt();
					}
				}

			});

			// handle CTRL+C
			rl.on('SIGINT', function() {

				// If we're in a multi-line statement, break out of it
				if (buffer.length) {
					buffer = [];
					console.log('\n(^C again to quit)');
					rl.setPrompt(constants.PROMPT);
					rl.prompt();

				// quit
				} else {
					cb();
				}
			});

			// do we have a file to load?
			if (opts.args && opts.args.length) {
				load(opts.args, rl);
			}

		}

	], callback);
};

function copy(src, dst) {
	fs.writeFileSync(dst, fs.readFileSync(src));
}

function sendLoadCommand(rl, location, content, delay) {
	try {
		acorn.parse(content);
	} catch (err) {
		console.error(error('error parsing "' + location + '"'));
		console.error(err);
		return rl.prompt();
	}

	var tasks = [];
	content.split('\n').forEach(function(line){
		line = line.trim();
		if (line) {
			tasks.push(function(next){
				setTimeout(function(){
					rl.write(line + '\n');
					next();
				}, delay || 25);
			});
		}
	});
	return async.series(tasks);
}

function load(tokens, rl) {
	var location = tokens[0],
		delay = tokens[1] || 0;

	// looks like a url, fetch it
	if (/^https?:\/\//.test(location)) {
		var interval = spinner();
		return request(location, function(err,content) {
			clearInterval(interval);
			if (err) {
				console.error(error('error loading "' + location + '"'));
				console.error(error(err));
				rl.prompt();
				return;
			}
			sendLoadCommand(rl, location, content.body, delay);
		});

	// probably a file
	} else {
		if (fs.existsSync(location)) {
			return sendLoadCommand(rl, location, fs.readFileSync(location).toString(), delay);
		}
	}

	console.error(error('not sure how to load "' + location + '". does it exist?'));
	rl.prompt();
}

function tabCompletionSetup(tabCompletionDB) {
	if (tabCompletionDB && tabCompletionDB.length) {
		tabCompletionDB.forEach(function(name){
			var tokens = name.split('.'),
				obj = tabCompletionNS;
			for (var c=0;c<tokens.length;c++){
				var cname = tokens[c];
				var entry = obj[cname];
				if (!entry) {
					obj[cname] = entry = {};
				}
				obj = entry;
			}
		});
		tabCompletionDB = null;
		tabCompletionNS.Ti = tabCompletionNS.Titanium;
	}
}

function tabCompletion(rl) {
	return function(line) {

		var match = line.match(simpleExpressionRE);
		line = match[0];

		var tokens = line.split('.'),
			obj = tabCompletionNS,
			hits = [];

		for (var c=0;c<tokens.length;c++) {
			var token = tokens[c];
			if (token) {
				var entry = obj[token];
				if (!entry) {
					hits = Object.keys(obj).filter(function(e){
						return e.indexOf(token)===0;
					});
					obj = null;
					line = token;
					break;
				}
				obj = entry;
			}
		}

		if (obj) {
			hits = Object.keys(obj);
		}

		return [!hits.length && !line ? Object.keys(tabCompletionNS) : hits, line];
	};
}
<|endoftext|>