<commit_before>/**
 * Create a pool of objects to improve the performance and avoid the garbage collector
 * @class
 * @memberof PQ.utils
 * @param objectConstructor {*}
 * @param args {array}
 * @param [num] {number}
 */
function Pool(objectConstructor, args, num){

    /**
     * All objects in this pool
     * @member {Array}
     */
    this.poolItems = [];

    /**
     * The 'constructor' for all objects in this pool
     * @member {*|Object}
     */
    this.objectConstructor = objectConstructor || Object;

    /**
     * Params to create the object
     * @member {array}
     */
    this.args = args || [];

    /**
     * Number of items to create
     * @member {number}
     */
    this.num = num;

    if(num)this.generateObjects(num);
    return this;
}

Pool.prototype.constructor = Pool;

/**
 * Get the pool length
 * @returns {Number}
 */
Pool.prototype.getLength = function(){
    return this.poolItems.length;
};

/**
 * Return an instance of the objects in the pool
 * @returns {*}
 */
Pool.prototype.getObject = function(){
    if(this.num && this.poolItems.length <= this.num*0.10){
        this.generateObjects(this.num);
    }else if(this.poolItems.length <= 0){
        return null;
    }

    var obj = this.poolItems.pop();
    if(obj.onPoolExit){
        obj.onPoolExit(this);
    }
    return obj;
};

/**
 * Put an instance in the pool list
 * @param obj {*}
 * @returns {Pool}
 */
Pool.prototype.putObject = function(obj){
    this.poolItems.unshift(obj);
    if(obj.onPoolEnter){
        obj.onPoolEnter(this);
    }
    return this;
};

/**
 * Generate and sotrage all the objects
 * @param num {number}
 * @returns {Pool}
 */
Pool.prototype.generateObjects = function(num){
    for(var i = 0; i < num; i++){
        this.poolItems.push(this.createNewObject());
    }
    return this;
};

/**
 * Create a new instances of the pool object with the arguments
 * @returns {*}
 */
Pool.prototype.createNewObject = function(){
    var scope = this;
    var obj;

    try {
        obj = new (Function.prototype.bind.apply(this.objectConstructor, ([null]).concat(this.args)))();
    }catch(e){
        obj = applyFix(this.objectConstructor, this.args);
    }

    obj.returnToPool = function(){
        scope.putObject(this);
        return this;
    };

    return obj;
};

module.exports = Pool;

function applyFix(obj, args){
    var ev = "Function('obj',";
    var fn = "\"return new obj(";

    for(var i = 0; i < args.length; i++){
        ev += "'a"+i+"',";
        fn += "a"+i;
        if(i !== args.length-1){
            fn += ",";
        }
    }

    fn += ")\"";
    ev += fn + ")";

    return (eval(ev)).apply(this, ([obj]).concat(args));
}<commit_msg>jshint ignore added<commit_after>/**
 * Create a pool of objects to improve the performance and avoid the garbage collector
 * @class
 * @memberof PQ.utils
 * @param objectConstructor {*}
 * @param args {array}
 * @param [num] {number}
 */
function Pool(objectConstructor, args, num){

    /**
     * All objects in this pool
     * @member {Array}
     */
    this.poolItems = [];

    /**
     * The 'constructor' for all objects in this pool
     * @member {*|Object}
     */
    this.objectConstructor = objectConstructor || Object;

    /**
     * Params to create the object
     * @member {array}
     */
    this.args = args || [];

    /**
     * Number of items to create
     * @member {number}
     */
    this.num = num;

    if(num)this.generateObjects(num);
    return this;
}

Pool.prototype.constructor = Pool;

/**
 * Get the pool length
 * @returns {Number}
 */
Pool.prototype.getLength = function(){
    return this.poolItems.length;
};

/**
 * Return an instance of the objects in the pool
 * @returns {*}
 */
Pool.prototype.getObject = function(){
    if(this.num && this.poolItems.length <= this.num*0.10){
        this.generateObjects(this.num);
    }else if(this.poolItems.length <= 0){
        return null;
    }

    var obj = this.poolItems.pop();
    if(obj.onPoolExit){
        obj.onPoolExit(this);
    }
    return obj;
};

/**
 * Put an instance in the pool list
 * @param obj {*}
 * @returns {Pool}
 */
Pool.prototype.putObject = function(obj){
    this.poolItems.unshift(obj);
    if(obj.onPoolEnter){
        obj.onPoolEnter(this);
    }
    return this;
};

/**
 * Generate and sotrage all the objects
 * @param num {number}
 * @returns {Pool}
 */
Pool.prototype.generateObjects = function(num){
    for(var i = 0; i < num; i++){
        this.poolItems.push(this.createNewObject());
    }
    return this;
};

/**
 * Create a new instances of the pool object with the arguments
 * @returns {*}
 */
Pool.prototype.createNewObject = function(){
    var scope = this;
    var obj;

    try {
        obj = new (Function.prototype.bind.apply(this.objectConstructor, ([null]).concat(this.args)))();
    }catch(e){
        obj = applyFix(this.objectConstructor, this.args);
    }

    obj.returnToPool = function(){
        scope.putObject(this);
        return this;
    };

    return obj;
};

module.exports = Pool;

// jshint ignore:start
function applyFix(obj, args){
    var ev = "Function('obj',";
    var fn = "\"return new obj(";

    for(var i = 0; i < args.length; i++){
        ev += "'a"+i+"',";
        fn += "a"+i;
        if(i !== args.length-1){
            fn += ",";
        }
    }

    fn += ")\"";
    ev += fn + ")";

    return (eval(ev)).apply(this, ([obj]).concat(args));
}
// jshint ignore:end<|endoftext|>


<commit_before>import React from 'react';
import Layout from './components/layout/Layout';
import Flex from './components/layout/Flex';
import Fixed from './components/layout/Fixed';
import Calendar from './components/calendar/Calendar';
import TabPane from './components/tab-pane/TabPane';
import Tab from './components/tab-pane/Tab';
import SplitPane from './components/split-pane/SplitPane';
import AutoSuggest from './components/auto-suggest/AutoSuggest';
import moment from 'moment';

let Application = React.createClass({

    autoSuggested(suggestion) {
        console.info('Application.autoSuggested', suggestion);
    },

    dateSelected(date) {
        console.info('Application.dateSelected', date);
    },

    render: function () {
        return (
                <Layout type="rows">

                    <Fixed className="header">

                        <Layout type="columns">
                            <Fixed className="logo">∞</Fixed>
                            <Flex></Flex>
                            <Fixed>
                                <AutoSuggest onSuggestion={this.autoSuggested} />
                            </Fixed>
                        </Layout>

                    </Fixed>

                    <Flex className="xxx">

                        <Layout type="columns">

                            <Fixed className="Sidebar">
                                <Layout type="rows">
                                    <Fixed>

Movies

News

Music

                                    </Fixed>
                                    <Flex></Flex>
                                    <Fixed>

Settings

Logout

                                    </Fixed>
                                </Layout>
                            </Fixed>

                            <Flex className="content">
                                <SplitPane orientation="horizontal" minSize="420">

                                    <TabPane orientation="vertical" className="my-tabs">
                                        <Tab id="A" name="A">some stuff</Tab>
                                        <Tab id="B" name="B" active="true">whatevs</Tab>
                                        <Tab id="C" name="C">innit</Tab>
                                    </TabPane>

                                    <SplitPane orientation="vertical" minSize="10">

                                            Bacon ipsum dolor amet pancetta filet mignon short ribs corned beef.

                                        <Calendar date={moment().add(2, 'd')} onSelect={this.dateSelected} />

                                    </SplitPane>

                                </SplitPane>
                            </Flex>

                        </Layout>

                    </Flex>
                </Layout>
        )
    }
});

React.render(<Application />, document.body);

<commit_msg>tab pane demo<commit_after>import React from 'react';
import Layout from './components/layout/Layout';
import Flex from './components/layout/Flex';
import Fixed from './components/layout/Fixed';
import Calendar from './components/calendar/Calendar';
import TabPane from './components/tab-pane/TabPane';
import Tab from './components/tab-pane/Tab';
import SplitPane from './components/split-pane/SplitPane';
import AutoSuggest from './components/auto-suggest/AutoSuggest';
import moment from 'moment';

let Application = React.createClass({

    autoSuggested(suggestion) {
        console.info('Application.autoSuggested', suggestion);
    },

    dateSelected(date) {
        console.info('Application.dateSelected', date);
    },

    render: function () {
        return (
                <Layout type="rows">

                    <Fixed className="header">

                        <Layout type="columns">
                            <Fixed className="logo">∞</Fixed>
                            <Flex></Flex>
                            <Fixed>
                                <AutoSuggest onSuggestion={this.autoSuggested} />
                            </Fixed>
                        </Layout>

                    </Fixed>

                    <Flex className="xxx">

                        <Layout type="columns">

                            <Fixed className="Sidebar">
                                <Layout type="rows">
                                    <Fixed>

Movies

News

Music

                                    </Fixed>
                                    <Flex></Flex>
                                    <Fixed>

Settings

Logout

                                    </Fixed>
                                </Layout>
                            </Fixed>

                            <Flex className="content">
                                <SplitPane orientation="horizontal" minSize="420">

                                    <TabPane orientation="horizontal" className="my-tabs">
                                        <Tab id="A" name="A">some stuff</Tab>
                                        <Tab id="B" name="B" active="true">whatevs</Tab>
                                        <Tab id="C" name="C">innit</Tab>
                                    </TabPane>

                                    <SplitPane orientation="vertical" minSize="10">

                                            Bacon ipsum dolor amet pancetta filet mignon short ribs corned beef.

                                        <Calendar date={moment().add(2, 'd')} onSelect={this.dateSelected} />

                                    </SplitPane>

                                </SplitPane>
                            </Flex>

                        </Layout>

                    </Flex>
                </Layout>
        )
    }
});

React.render(<Application />, document.body);

<|endoftext|>


<commit_before>var util2 = require('lib/util2');

var myutil = {};

myutil.defun = function(fn, fargs, fbody) {
  if (!fbody) {
    fbody = fargs;
    fargs = [];
  }
  return new Function('return function ' + fn + '(' + fargs.join(', ') + ') {' + fbody + '}')();
};

myutil.slog = function() {
  var args = [];
  for (var i = 0, ii = arguments.length; i < ii; ++i) {
    args[i] = util2.toString(arguments[i]);
  }
  return args.join(' ');
};

myutil.toObject = function(key, value) {
  if (typeof key === 'object') {
    return key;
  }
  var obj = {};
  obj[key] = value;
  return obj;
};

myutil.flag = function(flags) {
  if (typeof flags === 'boolean') {
    return flags;
  }
  for (var i = 1, ii = arguments.length; i < ii; ++i) {
    if (flags[arguments[i]]) {
      return true;
    }
  }
  return false;
};

myutil.toFlags = function(flags) {
  if (typeof flags === 'string') {
    flags = myutil.toObject(flags, true);
  } else {
    flags = !!flags;
  }
  return flags;
};

module.exports = myutil;
<commit_msg>Add myutil shadow for copying onto undefined fields<commit_after>var util2 = require('lib/util2');

var myutil = {};

myutil.shadow = function(a, b) {
  for (var k in a) {
    if (typeof b === 'undefined') {
      b[k] = a[k];
    }
  }
  return b;
};

myutil.defun = function(fn, fargs, fbody) {
  if (!fbody) {
    fbody = fargs;
    fargs = [];
  }
  return new Function('return function ' + fn + '(' + fargs.join(', ') + ') {' + fbody + '}')();
};

myutil.slog = function() {
  var args = [];
  for (var i = 0, ii = arguments.length; i < ii; ++i) {
    args[i] = util2.toString(arguments[i]);
  }
  return args.join(' ');
};

myutil.toObject = function(key, value) {
  if (typeof key === 'object') {
    return key;
  }
  var obj = {};
  obj[key] = value;
  return obj;
};

myutil.flag = function(flags) {
  if (typeof flags === 'boolean') {
    return flags;
  }
  for (var i = 1, ii = arguments.length; i < ii; ++i) {
    if (flags[arguments[i]]) {
      return true;
    }
  }
  return false;
};

myutil.toFlags = function(flags) {
  if (typeof flags === 'string') {
    flags = myutil.toObject(flags, true);
  } else {
    flags = !!flags;
  }
  return flags;
};

module.exports = myutil;
<|endoftext|>


<commit_before>jQuery.noConflict();
(function($) {
  $.plugin('card_editor', (function() {
    var baseUrl = "/api/v2/projects/";
    var editingCard;
    var editingCardId;
    var project;
    var number;
    function card_uri() {
      return baseUrl + project + "/cards/" + number;
    }
    function ajaxErrorHandler(x) {
      console.log(x);
      $this.card_editor('status', 'error');
    }

    var userIconCache = {};

    function ajaxUpdateUserIcon(url, elements) {
      $.ajax({
        url: url,
        dataType: 'xml',
        success: function(xmlDoc) {
          var icon_path = $(xmlDoc).find('icon_path').text();
          if (icon_path) {
            userIconCache[url] = icon_path;
            updateUserIcon(url, elements);
          }
        },
        failure: ajaxErrorHandler
      });
    }

    function updateUserIcon(url, elements) {
      if (userIconCache[url]) {
        elements.css('background-image', 'url(' + userIconCache[url] + ')');
      } else {
        ajaxUpdateUserIcon(url, elements);
      }
    }

    function currentUser() {
      var currentUserId = $('#current-user a').attr('href').split('/')[3];
      return "/api/v2/users/" + currentUserId + ".xml";
    }

    var actionImage = (function() {
      var actionImages = {
        "add": '/images/icon-source-add.png',
        "edit": '/images/icon-edit-pencil.gif',
        "remove": '/images/icon-source-delete.png',
        "move": '/images/icon-source-modify.png'
      };

      return function (actionElement) {
        return $('<img/>').attr('src', actionImages[actionElement.data('data').type]);
      };
    })();

    var user;

    return {
      init: function(card) {
        project = card.project;
        number = card.number;
        this.card_editor('status', 'loading');
        var $this = this.addClass('card_editor');
        $.ajax({
          url: card_uri() + '.xml',
          dataType: 'xml',
          success: function(xmlDoc) {
            $this.card_editor('initWikiMate', xmlDoc);
            $this.card_editor('status', 'idle');
          },
          failure: ajaxErrorHandler
        });
        return this;
      },

      initWikiMate: function(cardDoc) {
        editingCard = $(cardDoc);
        editingCardId = editingCard.find('card id').text();
        user = editingCard.find('card modified_by').attr('url');
        var story = window.wiki_parser.parse(editingCard.find('card description').text());
        var $this = this;
        this.empty().wikimate({story: story, change: function(event, action) {
          $this.card_editor('update', event, action);
        }});
        user = currentUser();
        $.ajax({
          url: '/projects/' + project + '/cards/history?id=' + editingCardId,
          dataType: 'html',
          success: function(xmlDoc) {
            var changes = $(xmlDoc).find('.card-event').filter(function(_, card_event) {
              return $(card_event).find('.change').text().trim().match(/Description changed/);
            });
            var journal = _.map(story, function(item) { return {id: item.id, type: 'add', item: item}; });
            var userIcons = changes.map(function(i, change) {
              return $(change).find('.user-icon img');
            }).toArray();

            $this.wikimate('journal', journal, function(actionElement) {
              var img = userIcons.pop();
              if (img) {
                actionElement.css('background-image', 'url(' + $(img).prop('src') + ')');
              } else {
                updateUserIcon(user, actionElement);
              }
              actionElement.html(actionImage(actionElement));
            });
            userIcons = [];
          },
          failure: ajaxErrorHandler
        });
      },

      update: function(event, action) {
        this.card_editor('status', 'updating');
        var desc = wiki_parser.dump(this.wikimate('story'));

        var $this = this;
        $.ajax({
          url: card_uri() + ".xml",
          dataType: 'xml',
          data: {card: {description: desc}},
          type: 'PUT',
          success: function(r) {
            $this.card_editor('status', 'idle');
          },
          failure: ajaxErrorHandler
        });
        return this;
      },

      renderWiki: function(content, onSuccess) {
        var paramStr = $.param({
          content_provider: {id: editingCardId, type: 'card'},
          content: content
        });
        $.ajax({
          url: baseUrl + project + "/render?" + paramStr,
          dataType: 'html',
          success: onSuccess,
          failure: ajaxErrorHandler
        });
      },

      status: function(newStatus) {
        switch(newStatus) {
          case 'loading':
          case 'updating':
            this.css('border-left', '2px solid yellow');
            break;
          case 'error':
            this.css('border-left', '2px solid red');
            break;
          default:
            this.css('border-left', '2px solid orange');
        }
      }
    };
  })());

  var match = window.location.href.match(/\/projects\/([\da-z_]+)\/cards\/(\d+)[^\/]*$/);
  if (match) {
    $('#content').card_editor({project: match[1], number: match[2]});
  } else {
    if (console) {
      console.log("Not on Mingle Card show page: ");
      console.log(window.location.href);
    }
  }
})(jQuery);
<commit_msg>step back, removed action bar, focus on editor now<commit_after>jQuery.noConflict();
(function($) {
  $.plugin('card_editor', (function() {
    var baseUrl = "/api/v2/projects/";
    var editingCard;
    var editingCardId;
    var project;
    var number;
    function card_uri() {
      return baseUrl + project + "/cards/" + number;
    }
    function ajaxErrorHandler(x) {
      console.log(x);
      $this.card_editor('status', 'error');
    }

    return {
      init: function(card) {
        project = card.project;
        number = card.number;
        this.card_editor('status', 'loading');
        var $this = this.addClass('card_editor');
        $.ajax({
          url: card_uri() + '.xml',
          dataType: 'xml',
          success: function(xmlDoc) {
            $this.card_editor('initWikiMate', xmlDoc);
            $this.card_editor('status', 'idle');
          },
          failure: ajaxErrorHandler
        });
        return this;
      },

      initWikiMate: function(cardDoc) {
        editingCard = $(cardDoc);
        editingCardId = editingCard.find('card id').text();
        var story = window.wiki_parser.parse(editingCard.find('card description').text());
        var $this = this;
        this.empty().wikimate({story: story, change: function(event, action) {
          $this.card_editor('update', event, action);
        }});
      },

      update: function(event, action) {
        this.card_editor('status', 'updating');
        var desc = wiki_parser.dump(this.wikimate('story'));

        var $this = this;
        $.ajax({
          url: card_uri() + ".xml",
          dataType: 'xml',
          data: {card: {description: desc}},
          type: 'PUT',
          success: function(r) {
            $this.card_editor('status', 'idle');
          },
          failure: ajaxErrorHandler
        });
        return this;
      },

      renderWiki: function(content, onSuccess) {
        var paramStr = $.param({
          content_provider: {id: editingCardId, type: 'card'},
          content: content
        });
        $.ajax({
          url: baseUrl + project + "/render?" + paramStr,
          dataType: 'html',
          success: onSuccess,
          failure: ajaxErrorHandler
        });
      },

      status: function(newStatus) {
        switch(newStatus) {
          case 'loading':
          case 'updating':
            this.css('border-left', '2px solid yellow');
            break;
          case 'error':
            this.css('border-left', '2px solid red');
            break;
          default:
            this.css('border-left', '2px solid orange');
        }
      }
    };
  })());

  var match = window.location.href.match(/\/projects\/([\da-z_]+)\/cards\/(\d+)[^\/]*$/);
  if (match) {
    $('#content').card_editor({project: match[1], number: match[2]});
  } else {
    if (console) {
      console.log("Not on Mingle Card show page: ");
      console.log(window.location.href);
    }
  }
})(jQuery);
<|endoftext|>


<commit_before>import GlslCanvas from 'glslCanvas';
import { subscribeInteractiveDom } from '../tools/interactiveDom';
import MediaCapture from '../tools/mediaCapture';
import MenuItem from '../ui/MenuItem';
// 3er Parties
import { saveAs } from '../vendor/FileSaver.min.js';

var CONTROLS_CLASSNAME = 'ge_control';

export default class Shader {
    constructor (main) {
        this.main = main;
        this.options = main.options;
        this.frag = "";

        // DOM CONTAINER
        this.el = document.createElement('div');
        this.el.setAttribute('class', 'ge_canvas_container');
        // CREATE AND START GLSLCANVAS
        this.el_canvas = document.createElement('canvas');
        this.el_canvas.setAttribute('class', 'ge_canvas');
        this.el_canvas.setAttribute('width', (this.options.canvas_width || this.options.canvas_size || '250') / window.devicePixelRatio);
        this.el_canvas.setAttribute('height', (this.options.canvas_height || this.options.canvas_size || '250') / window.devicePixelRatio);
        this.el_canvas.setAttribute('data-fragment', this.options.frag);
        this.el.appendChild(this.el_canvas);
        let glslcanvas = new GlslCanvas(this.el_canvas, { premultipliedAlpha: false, preserveDrawingBuffer: true, backgroundColor: 'rgba(1,1,1,1)' });

        this.canvas = glslcanvas;

        if (this.options.imgs.length > 0) {
            for (let i in this.options.imgs) {
                this.canvas.setUniform('u_tex' + i, this.options.imgs[i]);
            }
        }

        // Media Capture
        this.media_capture = new MediaCapture();
        this.media_capture.setCanvas(this.el_canvas);
        this.canvas.on('render', () => {
            this.media_capture.completeScreenshot();
        })

        // CONTROLS
        this.control_pannel = document.createElement('ul');
        this.control_pannel.className = CONTROLS_CLASSNAME;
        this.el.appendChild(this.control_pannel);
        this.controls = {};
        // play/stop
        this.controls.playPause = new MenuItem(this.control_pannel, 'ge_control_element', '▐ ▌', (event) => {
            event.stopPropagation();
            event.preventDefault();
            if (glslcanvas.paused) {
                glslcanvas.play();
                this.controls.playPause.name = '▐ ▌';//'Pause';
            } else {
                glslcanvas.pause();
                this.controls.playPause.name = ' ▶ ';//'Play';
            }
        });
        // rec
        this.isCapturing = false;
        let rec = new MenuItem(this.control_pannel, 'ge_control_element', '⬤', (event) => {
            event.stopPropagation();
            event.preventDefault();
            if (this.isCapturing) {
                this.stopVideoCapture();
            } else {
                this.startVideoCapture();
            }
        });
        this.controls.rec = rec;
        this.controls.rec.button.style.color = 'red';
        this.controls.rec.button.style.transform = 'translate(0px,-2px)';
        // present mode (only if there is a presentation.html file to point to)
        this.controls.presentationMode = new MenuItem(this.control_pannel, 'ge_control_element', '⬔', (event) => {
            event.stopPropagation();
            event.preventDefault();
            if (main.pWindowOpen) {
                main.togglePresentationWindow(false);
            } else {
                main.togglePresentationWindow(true);
            }
        });
        this.controls.presentationMode.button.style.fontSize = '22px';

        this.el_control = this.el.getElementsByClassName(CONTROLS_CLASSNAME)[0];
        this.el_control.addEventListener('mouseenter', (event) => { this.showControls(); });
        this.el_control.addEventListener('mouseleave', (event) => { this.hideControls(); });
        this.el_canvas.addEventListener('mousemove', (event) => {
            if (event.offsetY>this.el_canvas.clientHeight*.66) {
                this.showControls();
            }
            else {
                this.hideControls();
            }
        })
        this.hideControls();

        // ========== EVENTS
        // Draggable/resizable/snappable
        if (main.options.canvas_draggable || main.options.canvas_resizable || main.options.canvas_snapable) {
            subscribeInteractiveDom(this.el, {
                                                move: main.options.canvas_draggable,
                                                resize: main.options.canvas_resizable,
                                                snap: main.options.canvas_snapable
                                            });

            if (main.options.canvas_size === 'halfscreen') {
                this.el.snapRight();
            }

            this.el.on('move', (event) => {
                event.el.style.width = event.el.clientWidth+'px';
                event.el.style.height = event.el.clientHeight+'px';
            })
            this.el.on('resize', (event) => {
                glslcanvas.canvas.style.width = event.el.clientWidth+'px';
                glslcanvas.canvas.style.height = event.el.clientHeight+'px';
                glslcanvas.resize();
            })
        }

        // If there is a menu offset the editor to come after it
        if (main.menu) {
            this.el.style.top = (main.menu.el.clientHeight || main.menu.el.offsetHeight || main.menu.el.scrollHeight) + "px";
        }

        // Add all this to the main container
        main.container.appendChild(this.el);
        glslcanvas.resize();
    }

    hideControls () {
        if (this.el_control.className === CONTROLS_CLASSNAME) {
            this.el_control.className = CONTROLS_CLASSNAME+' '+CONTROLS_CLASSNAME+'_hidden';
        }
    }

    showControls () {
        if (this.el_control.className === CONTROLS_CLASSNAME+' '+CONTROLS_CLASSNAME+'_hidden') {
            this.el_control.className = CONTROLS_CLASSNAME;
        }
    }

    requestRedraw() {
        this.canvas.forceRender = true;
        this.canvas.render();
    }

    screenshot () {
        this.requestRedraw();
        return this.media_capture.screenshot();
    }

    startVideoCapture () {
        this.requestRedraw();
        if (this.media_capture.startVideoCapture()) {
            this.isCapturing = true;
            this.controls.rec.name = '■';
            this.controls.rec.button.style.color = 'white';
            this.controls.rec.button.style.transform = 'translate(0px,2px)';
            this.controls.rec.button.style.fontSize = '28px';
        }
    }

    stopVideoCapture () {
        if (this.isCapturing) {
            this.isCapturing = false;
            this.controls.rec.name = '⬤';
            this.controls.rec.button.style.color = 'red';
            this.controls.rec.button.style.fontSize = '14px';
            this.controls.rec.button.style.transform = 'translate(0px,-2px)';
            this.media_capture.stopVideoCapture().then((video) => {
                saveAs(video.blob, `${+new Date()}.webm`);
            });
        }
    }

    openWindow() {
        this.originalSize = {width: this.canvas.canvas.clientWidth, height: this.canvas.canvas.clientHeight};
        this.presentationWindow = window.open("", "_blank", "presentationWindow");
        this.setUpPresentationWindow();
    }

    closeWindow() {
        if (this.presentationWindow) {
            this.presentationWindow.close();
        }
    }

    setCanvasSize(w, h) {
        this.canvas.canvas.style.width = w + 'px';
        this.canvas.canvas.style.height = h + 'px';
    }

    setUpPresentationWindow() {
        this.presentationWindow.document.body.appendChild(this.canvas.canvas);
        var d = this.presentationWindow.document;
        var div = d.createElement("div");
        div.appendChild(d.createTextNode("Projector mode | "));
        var span = this.presentationWindow.document.createElement("span");
        div.appendChild(span);
        span.appendChild(d.createTextNode("If the canvas doesn't update, drag this window and reveal the editor"));
        d.body.appendChild(div);

        d.title = "GLSL Editor"
        d.body.style.padding = "0";
        d.body.style.margin = "0";
  			d.body.style.background = "#272822";
  			d.body.style.overflow = "hidden";

        div.style.position = "absolute";
        div.style.width = "100%";
        div.style.background = "rgba(0, 0, 0, 0.5)";
        div.style.position = "absolute";
        div.style.top = "0";
        div.style.left = "0";
        div.style.right = "0";
        div.style.padding = "16px";
        div.style.color = "#ffffff";
        div.style.fontSize = "14px";
        div.style.fontFamily = "Helvetica, Geneva, sans-serif";
        div.style.fontWeight = "400";
        div.style.letterWpacing = "0.1em";
        div.style.textAlign = "center";
        div.style.opacity = "1";
        div.style.zIndex = "9999";
        div.style.setProperty("-webkit-transition", "opacity 1.5s");
        div.style.setProperty("-moz-transition", "opacity 1.5s");
        div.style.setProperty("transition", "opacity 1.5s");

        span.style.color = "rgba(255, 255, 255, 0.5)";

        setTimeout(()=>{
            div.style.opacity = 0;
        }, 4000);

        this.setCanvasSize(this.presentationWindow.innerWidth, this.presentationWindow.innerHeight);
        this.presentationWindow.addEventListener('resize', this.onPresentationWindowResize.bind(this));
        this.presentationWindow.addEventListener("unload", this.onPresentationWindowClose.bind(this));
    }

    onPresentationWindowClose() {
        this.el.appendChild(this.canvas.canvas);
        this.setCanvasSize(this.originalSize.width, this.originalSize.height);
        this.canvas.resize();

        this.main.onClosePresentationWindow();
        this.main.menu.onClosePresentationWindow();
        this.presentationWindow = null;
    }

    onPresentationWindowResize() {
      if (this.presentationWindow) {
        this.setCanvasSize(this.presentationWindow.innerWidth, this.presentationWindow.innerHeight);
        this.canvas.resize();
      }
    }
}
<commit_msg>Fix typo in css property name<commit_after>import GlslCanvas from 'glslCanvas';
import { subscribeInteractiveDom } from '../tools/interactiveDom';
import MediaCapture from '../tools/mediaCapture';
import MenuItem from '../ui/MenuItem';
// 3er Parties
import { saveAs } from '../vendor/FileSaver.min.js';

var CONTROLS_CLASSNAME = 'ge_control';

export default class Shader {
    constructor (main) {
        this.main = main;
        this.options = main.options;
        this.frag = "";

        // DOM CONTAINER
        this.el = document.createElement('div');
        this.el.setAttribute('class', 'ge_canvas_container');
        // CREATE AND START GLSLCANVAS
        this.el_canvas = document.createElement('canvas');
        this.el_canvas.setAttribute('class', 'ge_canvas');
        this.el_canvas.setAttribute('width', (this.options.canvas_width || this.options.canvas_size || '250') / window.devicePixelRatio);
        this.el_canvas.setAttribute('height', (this.options.canvas_height || this.options.canvas_size || '250') / window.devicePixelRatio);
        this.el_canvas.setAttribute('data-fragment', this.options.frag);
        this.el.appendChild(this.el_canvas);
        let glslcanvas = new GlslCanvas(this.el_canvas, { premultipliedAlpha: false, preserveDrawingBuffer: true, backgroundColor: 'rgba(1,1,1,1)' });

        this.canvas = glslcanvas;

        if (this.options.imgs.length > 0) {
            for (let i in this.options.imgs) {
                this.canvas.setUniform('u_tex' + i, this.options.imgs[i]);
            }
        }

        // Media Capture
        this.media_capture = new MediaCapture();
        this.media_capture.setCanvas(this.el_canvas);
        this.canvas.on('render', () => {
            this.media_capture.completeScreenshot();
        })

        // CONTROLS
        this.control_pannel = document.createElement('ul');
        this.control_pannel.className = CONTROLS_CLASSNAME;
        this.el.appendChild(this.control_pannel);
        this.controls = {};
        // play/stop
        this.controls.playPause = new MenuItem(this.control_pannel, 'ge_control_element', '▐ ▌', (event) => {
            event.stopPropagation();
            event.preventDefault();
            if (glslcanvas.paused) {
                glslcanvas.play();
                this.controls.playPause.name = '▐ ▌';//'Pause';
            } else {
                glslcanvas.pause();
                this.controls.playPause.name = ' ▶ ';//'Play';
            }
        });
        // rec
        this.isCapturing = false;
        let rec = new MenuItem(this.control_pannel, 'ge_control_element', '⬤', (event) => {
            event.stopPropagation();
            event.preventDefault();
            if (this.isCapturing) {
                this.stopVideoCapture();
            } else {
                this.startVideoCapture();
            }
        });
        this.controls.rec = rec;
        this.controls.rec.button.style.color = 'red';
        this.controls.rec.button.style.transform = 'translate(0px,-2px)';
        // present mode (only if there is a presentation.html file to point to)
        this.controls.presentationMode = new MenuItem(this.control_pannel, 'ge_control_element', '⬔', (event) => {
            event.stopPropagation();
            event.preventDefault();
            if (main.pWindowOpen) {
                main.togglePresentationWindow(false);
            } else {
                main.togglePresentationWindow(true);
            }
        });
        this.controls.presentationMode.button.style.fontSize = '22px';

        this.el_control = this.el.getElementsByClassName(CONTROLS_CLASSNAME)[0];
        this.el_control.addEventListener('mouseenter', (event) => { this.showControls(); });
        this.el_control.addEventListener('mouseleave', (event) => { this.hideControls(); });
        this.el_canvas.addEventListener('mousemove', (event) => {
            if (event.offsetY>this.el_canvas.clientHeight*.66) {
                this.showControls();
            }
            else {
                this.hideControls();
            }
        })
        this.hideControls();

        // ========== EVENTS
        // Draggable/resizable/snappable
        if (main.options.canvas_draggable || main.options.canvas_resizable || main.options.canvas_snapable) {
            subscribeInteractiveDom(this.el, {
                                                move: main.options.canvas_draggable,
                                                resize: main.options.canvas_resizable,
                                                snap: main.options.canvas_snapable
                                            });

            if (main.options.canvas_size === 'halfscreen') {
                this.el.snapRight();
            }

            this.el.on('move', (event) => {
                event.el.style.width = event.el.clientWidth+'px';
                event.el.style.height = event.el.clientHeight+'px';
            })
            this.el.on('resize', (event) => {
                glslcanvas.canvas.style.width = event.el.clientWidth+'px';
                glslcanvas.canvas.style.height = event.el.clientHeight+'px';
                glslcanvas.resize();
            })
        }

        // If there is a menu offset the editor to come after it
        if (main.menu) {
            this.el.style.top = (main.menu.el.clientHeight || main.menu.el.offsetHeight || main.menu.el.scrollHeight) + "px";
        }

        // Add all this to the main container
        main.container.appendChild(this.el);
        glslcanvas.resize();
    }

    hideControls () {
        if (this.el_control.className === CONTROLS_CLASSNAME) {
            this.el_control.className = CONTROLS_CLASSNAME+' '+CONTROLS_CLASSNAME+'_hidden';
        }
    }

    showControls () {
        if (this.el_control.className === CONTROLS_CLASSNAME+' '+CONTROLS_CLASSNAME+'_hidden') {
            this.el_control.className = CONTROLS_CLASSNAME;
        }
    }

    requestRedraw() {
        this.canvas.forceRender = true;
        this.canvas.render();
    }

    screenshot () {
        this.requestRedraw();
        return this.media_capture.screenshot();
    }

    startVideoCapture () {
        this.requestRedraw();
        if (this.media_capture.startVideoCapture()) {
            this.isCapturing = true;
            this.controls.rec.name = '■';
            this.controls.rec.button.style.color = 'white';
            this.controls.rec.button.style.transform = 'translate(0px,2px)';
            this.controls.rec.button.style.fontSize = '28px';
        }
    }

    stopVideoCapture () {
        if (this.isCapturing) {
            this.isCapturing = false;
            this.controls.rec.name = '⬤';
            this.controls.rec.button.style.color = 'red';
            this.controls.rec.button.style.fontSize = '14px';
            this.controls.rec.button.style.transform = 'translate(0px,-2px)';
            this.media_capture.stopVideoCapture().then((video) => {
                saveAs(video.blob, `${+new Date()}.webm`);
            });
        }
    }

    openWindow() {
        this.originalSize = {width: this.canvas.canvas.clientWidth, height: this.canvas.canvas.clientHeight};
        this.presentationWindow = window.open("", "_blank", "presentationWindow");
        this.setUpPresentationWindow();
    }

    closeWindow() {
        if (this.presentationWindow) {
            this.presentationWindow.close();
        }
    }

    setCanvasSize(w, h) {
        this.canvas.canvas.style.width = w + 'px';
        this.canvas.canvas.style.height = h + 'px';
    }

    setUpPresentationWindow() {
        this.presentationWindow.document.body.appendChild(this.canvas.canvas);
        var d = this.presentationWindow.document;
        var div = d.createElement("div");
        div.appendChild(d.createTextNode("Projector mode | "));
        var span = this.presentationWindow.document.createElement("span");
        div.appendChild(span);
        span.appendChild(d.createTextNode("If the canvas doesn't update, drag this window and reveal the editor"));
        d.body.appendChild(div);

        d.title = "GLSL Editor"
        d.body.style.padding = "0";
        d.body.style.margin = "0";
  			d.body.style.background = "#272822";
  			d.body.style.overflow = "hidden";

        div.style.position = "absolute";
        div.style.width = "100%";
        div.style.background = "rgba(0, 0, 0, 0.5)";
        div.style.position = "absolute";
        div.style.top = "0";
        div.style.left = "0";
        div.style.right = "0";
        div.style.padding = "16px";
        div.style.color = "#ffffff";
        div.style.fontSize = "14px";
        div.style.fontFamily = "Helvetica, Geneva, sans-serif";
        div.style.fontWeight = "400";
        div.style.letterSpacing = "0.1em";
        div.style.textAlign = "center";
        div.style.opacity = "1";
        div.style.zIndex = "9999";
        div.style.setProperty("-webkit-transition", "opacity 1.5s");
        div.style.setProperty("-moz-transition", "opacity 1.5s");
        div.style.setProperty("transition", "opacity 1.5s");

        span.style.color = "rgba(255, 255, 255, 0.5)";

        setTimeout(()=>{
            div.style.opacity = 0;
        }, 4000);

        this.setCanvasSize(this.presentationWindow.innerWidth, this.presentationWindow.innerHeight);
        this.presentationWindow.addEventListener('resize', this.onPresentationWindowResize.bind(this));
        this.presentationWindow.addEventListener("unload", this.onPresentationWindowClose.bind(this));
    }

    onPresentationWindowClose() {
        this.el.appendChild(this.canvas.canvas);
        this.setCanvasSize(this.originalSize.width, this.originalSize.height);
        this.canvas.resize();

        this.main.onClosePresentationWindow();
        this.main.menu.onClosePresentationWindow();
        this.presentationWindow = null;
    }

    onPresentationWindowResize() {
      if (this.presentationWindow) {
        this.setCanvasSize(this.presentationWindow.innerWidth, this.presentationWindow.innerHeight);
        this.canvas.resize();
      }
    }
}
<|endoftext|>


<commit_before>'use strict';

const Lf = require('lovefield');
const Qs = require('qs');

const Chrometools = require('./chrometools.js');
const Track = require('./track.js');
const Trackcache = require('./trackcache.js');
const Playlist = require('./playlist.js');

const Reporting = require('./reporting.js');

const GM_BASE_URL = 'https://play.google.com/music/';
const GM_SERVICE_URL = `${GM_BASE_URL}services/`;

function authedGMRequest(endpoint, data, userIndex, method, callback, onError) {
  // Call an endpoint and callback with it's parsed response.

  chrome.cookies.get({name: 'xt', url: 'https://play.google.com/music'}, Chrometools.unlessError(cookie => {
    if (cookie === null) {
      // TODO alert user somehow
      console.error('unable to get xt cookie');
      Reporting.Raven.captureMessage('unable to get xt cookie');
    } else {
      let format = '';
      let payload = {json: JSON.stringify(data)};
      if (data.constructor === Array || method === 'get') {
        format = 'format=jsarray&';
        if (method !== 'get') {
          payload = JSON.stringify(data);
        }
      }

      const url = `${GM_SERVICE_URL}${endpoint}?${format}${Qs.stringify({u: userIndex, xt: cookie.value})}`;

      // TODO this is stupid
      let dataType = 'json';
      if (method === 'get') {
        dataType = 'html';
      }

      if (typeof onError === 'undefined') {
        onError = res => {  // eslint-disable-line no-param-reassign
          console.error('request to failed:', url, data, res);
          Reporting.Raven.captureMessage(`request to ${url} failed`, {
            extra: {url, data, res},
          });
        };
      }

      // TODO jquery should be injected with browserify?
      $[method](
        url,
        payload,
        res => {
          callback(res);
        },

        dataType
      )
      .fail(onError);
    }
  }));
}

exports.getTrackChanges = function getTrackChanges(userIndex, sinceTimestamp, callback) {
  // Callback {newTimestamp: 1234, upsertedTracks: [{}], deletedIds: ['']}
  // timestamps are in microseconds.
  const payload = {
    lastUpdated: sinceTimestamp,
    tier: 1, // TODO need proper tier?
    requestCause: 1,
    sessionId: '',
  };

  console.log('getTrackChanges', sinceTimestamp);

  // want arg to window.parent['slat_process']
  authedGMRequest('streamingloadalltracks', payload, userIndex, 'get', response => {
    const result = {newTimestamp: null, upsertedTracks: [], deletedIds: []};

    const parser = new DOMParser();
    const doc = parser.parseFromString(response, 'application/xml');

    // console.log(doc);
    const scripts = doc.getElementsByTagName('script');

    // console.log(scripts);
    const startMark = '\nwindow.parent[\'slat_process\'](';
    const endMark = '000]';
    for (let i = 0; i < scripts.length; i++) {
      let code = scripts[i].innerHTML;
      let start = code.indexOf(startMark);
      if (start === -1) {
        continue;
      }

      // console.log('chunk', i);
      // console.log('to slice', JSON.stringify(code));
      let end = code.lastIndexOf(endMark);

      // console.log(start, end);

      start = start + startMark.length;
      end = end + endMark.length;
      code = code.slice(start, end);

      // console.log('to eval', JSON.stringify(code));
      const parsed = eval(code);

      // console.log(parsed);

      // we just want the last chunk's timestamp
      result.newTimestamp = parsed[1];

      for (let j = 0; j < parsed[0].length; j++) {
        const gtrack = parsed[0][j];
        if (gtrack[19] === 1) {
          result.deletedIds.push(gtrack[0]);
        } else {
          result.upsertedTracks.push(Track.fromJsproto(gtrack));
        }
      }

      console.log(parsed[0].length, 'tracks');
    }

    console.log(result);
    callback(result);
  });
};

exports.updatePlaylist = function updatePlaylist(userIndex, id, title, playlist, callback) {
  // Callback no args after updating an existing playlist.
  const lastSync = new Date().toLocaleString();
  const description = Playlist.toString(playlist);
  const syncMsg = `Synced ${lastSync} by Autoplaylists for Google Music™ to contain: ${description}.`;

  const payload = [['', 1], [id, null, title, syncMsg]];
  console.log('updatePlaylist', playlist);

  authedGMRequest('editplaylist', payload, userIndex, 'post', response => {
    console.log(response);
    callback();
  });
};

exports.createRemotePlaylist = function createRemotePlaylist(userIndex, title, callback) {
  // Callback a playlist id for a new, empty playlist.
  const payload = [['', 1], [false, title, null, []]];

  console.log('createRemotePlaylist', title);

  // response:
  // [[0,2,0] ,["id","some long base64 string",null,timestamp]]
  authedGMRequest('createplaylist', payload, userIndex, 'post', response => {
    console.log(response);
    callback(response[1][0]);
  });
};

exports.deleteRemotePlaylist = function deleteRemotePlaylist(userIndex, remoteId, callback) {
  // Callback no args after deleting a playlist.

  const payload = {
    id: remoteId,
    requestCause: 1,
    requestType: 1,
    sessionId: '',
  };

  console.log('deleteRemotePlaylist', remoteId);

  authedGMRequest('deleteplaylist', payload, userIndex, 'post', response => {
    console.log('delete playlist response', response);
    callback();
  });
};

function addTracks(userIndex, playlistId, tracks, callback, onError) {
  // Append these tracks and callback the api response, or null if adding 0 tracks.

  if (tracks.length === 0) {
    console.log('skipping add of 0 tracks');
    return callback(null);
  }

  console.log('adding', tracks.length, 'tracks. first 5 are', JSON.stringify(tracks.slice(0, 5), null, 2));

  // [["<sessionid>",1],["<listid>",[["<store id or songid>",tracktype]]]]
  const payload = [['', 1],
    [
      // Google always sends [id, type] pairs, but that's caused problems for me around AA and store ids and types.
      // Just sending an id seems to work, so maybe that'll fix everything?
      playlistId, tracks.map(t => [t.id]),
    ],
  ];
  authedGMRequest('addtrackstoplaylist', payload, userIndex, 'post', response => {
    console.log('add response', JSON.stringify(response, null, 2));
    if (response.length <= 1 || response[1].length <= 0 || response[1][0] === 0) {
      // I used to think a [0] response array of 0, 2, 0 signaled errors,
      // but I've seen some successful responses with that recently.
      // 0 instead of the update timestamp seems a better indicator of errors.
      let responseArray = null;
      if (response.length > 0) {
        responseArray = JSON.stringify(response[0]);
      }

      // These events are really common and filling up the sentry quota.
      // Only sending 1 in 1k ought to bring them under the quota.
      if(Math.random() < 0.001) {
        Reporting.Raven.captureMessage('probable error from addTracks', {
          tags: {playlistId, responseArray},
          extra: {response, playlistId, tracks},
        });
      }
    }
    callback(response);
  }, onError);
}

function deleteEntries(userIndex, playlistId, entries, callback, onError) {
  // Delete entries with id and entryId keys; callback the api response.
  console.log('deleting', entries.length, 'entries. first 5 are', JSON.stringify(entries.slice(0, 5), null, 2));
  const payload = {
    songIds: entries.map(entry => entry.id),

    entryIds: entries.map(entry => entry.entryId),

    listId: playlistId,
    sessionId: '',
  };
  authedGMRequest('deletesong', payload, userIndex, 'post', response => {
    console.log('delete response', JSON.stringify(response, null, 2));
    callback(response);
  }, onError);
}

function loadPlaylistContents(db, userIndex, playlistId, callback, onError) {
  // Callback a list of objects with entryId and track keys.

  const payload = [['', 1], [playlistId]];
  authedGMRequest('loaduserplaylist', payload, userIndex, 'post', response => {
    if (response.length < 2) {
      return onError(`unexpected loadPlaylistContents response: ${JSON.stringify(response, null, 2)}`);
    }

    const contents = [];

    if (response[1].length !== 0) {
      const gentries = response[1][0];
      console.log('playlist', playlistId, 'has', gentries.length, 'entries. first 3:',
                  JSON.stringify(gentries.slice(0, 3), null, 2));

      for (let i = 0; i < gentries.length; i++) {
        const gentry = gentries[i];
        const entryId = gentry[43];
        const track = Track.fromJsproto(gentry);
        contents.push({entryId, track});
      }
    }

    callback(contents);
  }, onError);
}

exports.setPlaylistContents = function setPlaylistContents(db, userIndex, playlistId, tracks, callback, onError) {
  // Update a remote playlist to contain only the given tracks, in any order.

  // This requires multiple requests:
  // 1) get playlist tracks
  // 2) delete current - desired
  // 3) add desired - current

  loadPlaylistContents(db, userIndex, playlistId, contents => {
    if (contents.length !== 0) {
      const idsToAdd = {};
      for (let i = 0; i < tracks.length; i++) {
        const track = tracks[i];
        idsToAdd[track.id] = track;
      }

      const deleteCandidates = {};
      for (let i = 0; i < contents.length; i++) {
        const remoteTrack = contents[i].track;
        const entryId = contents[i].entryId;

        if (!(remoteTrack.id in idsToAdd)) {
          deleteCandidates[remoteTrack.id] = entryId;
        } else {
          delete idsToAdd[remoteTrack.id];
        }
      }

      const tracksToAdd = [];
      for (const id in idsToAdd) {
        tracksToAdd.push(idsToAdd[id]);
      }

      const entriesToDelete = [];
      const deleteCandidateIds = Object.keys(deleteCandidates);
      if (deleteCandidateIds.length > 0) {
        // Don't delete tracks that may be currently playing.
        // This requires a query against the track cache, since the lastPlayed
        // for remote tracks is set to 0 for any recent plays!?
        // FIXME pull this out to Trackcache.
        const track = db.getSchema().table('Track');
        db.select().from(track).where(
          // We don't know if these entries name a library or store id.
          Lf.op.or(track.id.in(deleteCandidateIds),
                   track.storeId.in(deleteCandidateIds))
        ).exec().then(rows => {
          const nowMillis = new Date().getTime();
          const delayMillis = 0;

          rows.forEach(row => {
            if (delayMillis + row.lastPlayed / 1000 > nowMillis - row.durationMillis) {
              console.info('not deleting', row, 'since it may be playing.',
                           delayMillis + row.lastPlayed / 1000, nowMillis - row.durationMillis
                          );
              if (row.id in deleteCandidates) {
                delete deleteCandidates[row.id];
              } else {
                delete deleteCandidates[row.storeId];
              }
            }
          });

          for (const deleteId in deleteCandidates) {
            entriesToDelete.push({id: deleteId, entryId: deleteCandidates[deleteId]});
          }

          if (entriesToDelete.length > 0) {
            deleteEntries(userIndex, playlistId, entriesToDelete, deleteResponse => { // eslint-disable-line no-unused-vars
              // We log the delete response inside deleteEntries and have no other need for it here.
              addTracks(userIndex, playlistId, tracksToAdd, callback);
            }, onError);
          } else {
            console.log('no need to delete post-filter; adding');
            addTracks(userIndex, playlistId, tracksToAdd, callback, onError);
          }
        }).catch(e => {
          console.error(e);
          Reporting.Raven.captureMessage(JSON.stringify(e), {
            tags: {playlistId},
            extra: {deleteCandidates, deleteCandidateIds, tracksToAdd},
          });
        });
      } else {
        console.log('no need to delete pre-filter; adding');
        addTracks(userIndex, playlistId, tracksToAdd, callback, onError);
      }
    } else {
      console.log('adding to empty');
      addTracks(userIndex, playlistId, tracks, callback, onError);
    }
  }, onError);
};

exports.setPlaylistOrder = function setPlaylistOrder(db, userIndex, playlist, callback, onError) {
  // Set the remote ordering of a playlist according to playlist's sort order.
  // This trusts that the remote contents are already correct.

  // This approach handles the maybe-playing tracks that wouldn't be in our tracks
  // if we queried them locally.

  loadPlaylistContents(db, userIndex, playlist.remoteId, contents => {
    if (contents.length !== 0) {
      // Reordering calls deal in entry ids, not track ids.
      const currentOrdering = [];
      const desiredOrdering = [];
      const idToEntryId = {};

      for (let i = 0; i < contents.length; i++) {
        idToEntryId[contents[i].track.id] = contents[i].entryId;
        currentOrdering.push(contents[i].entryId);
      }

      const remoteTracks = contents.map(c => c.track);

      Trackcache.orderTracks(db, playlist, remoteTracks, orderedTracks => {
        for (let i = 0; i < orderedTracks.length; i++) {
          const track = orderedTracks[i];
          desiredOrdering.push(idToEntryId[track.id]);
        }

        // It's ridiculous that javascript doesn't have a builtin for this.
        // Thankfully we have simple items and can get away with this hack.
        if (JSON.stringify(currentOrdering) !== JSON.stringify(desiredOrdering)) {
          // The two empty strings are sentinels for "first track" and "last track".
          // This lets us send our entire reordering at once without calculating the relative movements.
          // I'm not sure if the interface was intended to be used this way, but it seems to work.
          const payload = [['', 1], [desiredOrdering, '', '']];
          authedGMRequest('changeplaylisttrackorder', payload, userIndex, 'post', response => {
            // TODO These should all be checked for errors.
            // It looks like responses will have [[0, 1, 1], [call-specific response]] on success.
            callback(response);
          }, onError);
        } else {
          // Avoid triggering a ui refresh on noop reorderings.
          console.log('no need to reorder playlist', playlist.title);
          callback(null);
        }
      }, onError);
    } else {
      console.log('no need to reorder empty playlist', playlist.title);
      callback(null);
    }
  }, onError);
};
<commit_msg>call onError when xt cookie unavailable<commit_after>'use strict';

const Lf = require('lovefield');
const Qs = require('qs');

const Chrometools = require('./chrometools.js');
const Track = require('./track.js');
const Trackcache = require('./trackcache.js');
const Playlist = require('./playlist.js');

const Reporting = require('./reporting.js');

const GM_BASE_URL = 'https://play.google.com/music/';
const GM_SERVICE_URL = `${GM_BASE_URL}services/`;

function authedGMRequest(endpoint, data, userIndex, method, callback, onError) {
  // Call an endpoint and callback with it's parsed response.

  chrome.cookies.get({name: 'xt', url: 'https://play.google.com/music'}, Chrometools.unlessError(cookie => {
    if (cookie === null) {
      // TODO alert user somehow
      console.error('unable to get xt cookie');
      Reporting.Raven.captureMessage('unable to get xt cookie');
      onError(new Error('unable to get xt cookie'));
    } else {
      let format = '';
      let payload = {json: JSON.stringify(data)};
      if (data.constructor === Array || method === 'get') {
        format = 'format=jsarray&';
        if (method !== 'get') {
          payload = JSON.stringify(data);
        }
      }

      const url = `${GM_SERVICE_URL}${endpoint}?${format}${Qs.stringify({u: userIndex, xt: cookie.value})}`;

      // TODO this is stupid
      let dataType = 'json';
      if (method === 'get') {
        dataType = 'html';
      }

      if (typeof onError === 'undefined') {
        onError = res => {  // eslint-disable-line no-param-reassign
          console.error('request to failed:', url, data, res);
          Reporting.Raven.captureMessage(`request to ${url} failed`, {
            extra: {url, data, res},
          });
        };
      }

      // TODO jquery should be injected with browserify?
      $[method](
        url,
        payload,
        res => {
          callback(res);
        },

        dataType
      )
      .fail(onError);
    }
  }));
}

exports.getTrackChanges = function getTrackChanges(userIndex, sinceTimestamp, callback) {
  // Callback {newTimestamp: 1234, upsertedTracks: [{}], deletedIds: ['']}
  // timestamps are in microseconds.
  const payload = {
    lastUpdated: sinceTimestamp,
    tier: 1, // TODO need proper tier?
    requestCause: 1,
    sessionId: '',
  };

  console.log('getTrackChanges', sinceTimestamp);

  // want arg to window.parent['slat_process']
  authedGMRequest('streamingloadalltracks', payload, userIndex, 'get', response => {
    const result = {newTimestamp: null, upsertedTracks: [], deletedIds: []};

    const parser = new DOMParser();
    const doc = parser.parseFromString(response, 'application/xml');

    // console.log(doc);
    const scripts = doc.getElementsByTagName('script');

    // console.log(scripts);
    const startMark = '\nwindow.parent[\'slat_process\'](';
    const endMark = '000]';
    for (let i = 0; i < scripts.length; i++) {
      let code = scripts[i].innerHTML;
      let start = code.indexOf(startMark);
      if (start === -1) {
        continue;
      }

      // console.log('chunk', i);
      // console.log('to slice', JSON.stringify(code));
      let end = code.lastIndexOf(endMark);

      // console.log(start, end);

      start = start + startMark.length;
      end = end + endMark.length;
      code = code.slice(start, end);

      // console.log('to eval', JSON.stringify(code));
      const parsed = eval(code);

      // console.log(parsed);

      // we just want the last chunk's timestamp
      result.newTimestamp = parsed[1];

      for (let j = 0; j < parsed[0].length; j++) {
        const gtrack = parsed[0][j];
        if (gtrack[19] === 1) {
          result.deletedIds.push(gtrack[0]);
        } else {
          result.upsertedTracks.push(Track.fromJsproto(gtrack));
        }
      }

      console.log(parsed[0].length, 'tracks');
    }

    console.log(result);
    callback(result);
  });
};

exports.updatePlaylist = function updatePlaylist(userIndex, id, title, playlist, callback) {
  // Callback no args after updating an existing playlist.
  const lastSync = new Date().toLocaleString();
  const description = Playlist.toString(playlist);
  const syncMsg = `Synced ${lastSync} by Autoplaylists for Google Music™ to contain: ${description}.`;

  const payload = [['', 1], [id, null, title, syncMsg]];
  console.log('updatePlaylist', playlist);

  authedGMRequest('editplaylist', payload, userIndex, 'post', response => {
    console.log(response);
    callback();
  });
};

exports.createRemotePlaylist = function createRemotePlaylist(userIndex, title, callback) {
  // Callback a playlist id for a new, empty playlist.
  const payload = [['', 1], [false, title, null, []]];

  console.log('createRemotePlaylist', title);

  // response:
  // [[0,2,0] ,["id","some long base64 string",null,timestamp]]
  authedGMRequest('createplaylist', payload, userIndex, 'post', response => {
    console.log(response);
    callback(response[1][0]);
  });
};

exports.deleteRemotePlaylist = function deleteRemotePlaylist(userIndex, remoteId, callback) {
  // Callback no args after deleting a playlist.

  const payload = {
    id: remoteId,
    requestCause: 1,
    requestType: 1,
    sessionId: '',
  };

  console.log('deleteRemotePlaylist', remoteId);

  authedGMRequest('deleteplaylist', payload, userIndex, 'post', response => {
    console.log('delete playlist response', response);
    callback();
  });
};

function addTracks(userIndex, playlistId, tracks, callback, onError) {
  // Append these tracks and callback the api response, or null if adding 0 tracks.

  if (tracks.length === 0) {
    console.log('skipping add of 0 tracks');
    return callback(null);
  }

  console.log('adding', tracks.length, 'tracks. first 5 are', JSON.stringify(tracks.slice(0, 5), null, 2));

  // [["<sessionid>",1],["<listid>",[["<store id or songid>",tracktype]]]]
  const payload = [['', 1],
    [
      // Google always sends [id, type] pairs, but that's caused problems for me around AA and store ids and types.
      // Just sending an id seems to work, so maybe that'll fix everything?
      playlistId, tracks.map(t => [t.id]),
    ],
  ];
  authedGMRequest('addtrackstoplaylist', payload, userIndex, 'post', response => {
    console.log('add response', JSON.stringify(response, null, 2));
    if (response.length <= 1 || response[1].length <= 0 || response[1][0] === 0) {
      // I used to think a [0] response array of 0, 2, 0 signaled errors,
      // but I've seen some successful responses with that recently.
      // 0 instead of the update timestamp seems a better indicator of errors.
      let responseArray = null;
      if (response.length > 0) {
        responseArray = JSON.stringify(response[0]);
      }

      // These events are really common and filling up the sentry quota.
      // Only sending 1 in 1k ought to bring them under the quota.
      if(Math.random() < 0.001) {
        Reporting.Raven.captureMessage('probable error from addTracks', {
          tags: {playlistId, responseArray},
          extra: {response, playlistId, tracks},
        });
      }
    }
    callback(response);
  }, onError);
}

function deleteEntries(userIndex, playlistId, entries, callback, onError) {
  // Delete entries with id and entryId keys; callback the api response.
  console.log('deleting', entries.length, 'entries. first 5 are', JSON.stringify(entries.slice(0, 5), null, 2));
  const payload = {
    songIds: entries.map(entry => entry.id),

    entryIds: entries.map(entry => entry.entryId),

    listId: playlistId,
    sessionId: '',
  };
  authedGMRequest('deletesong', payload, userIndex, 'post', response => {
    console.log('delete response', JSON.stringify(response, null, 2));
    callback(response);
  }, onError);
}

function loadPlaylistContents(db, userIndex, playlistId, callback, onError) {
  // Callback a list of objects with entryId and track keys.

  const payload = [['', 1], [playlistId]];
  authedGMRequest('loaduserplaylist', payload, userIndex, 'post', response => {
    if (response.length < 2) {
      return onError(`unexpected loadPlaylistContents response: ${JSON.stringify(response, null, 2)}`);
    }

    const contents = [];

    if (response[1].length !== 0) {
      const gentries = response[1][0];
      console.log('playlist', playlistId, 'has', gentries.length, 'entries. first 3:',
                  JSON.stringify(gentries.slice(0, 3), null, 2));

      for (let i = 0; i < gentries.length; i++) {
        const gentry = gentries[i];
        const entryId = gentry[43];
        const track = Track.fromJsproto(gentry);
        contents.push({entryId, track});
      }
    }

    callback(contents);
  }, onError);
}

exports.setPlaylistContents = function setPlaylistContents(db, userIndex, playlistId, tracks, callback, onError) {
  // Update a remote playlist to contain only the given tracks, in any order.

  // This requires multiple requests:
  // 1) get playlist tracks
  // 2) delete current - desired
  // 3) add desired - current

  loadPlaylistContents(db, userIndex, playlistId, contents => {
    if (contents.length !== 0) {
      const idsToAdd = {};
      for (let i = 0; i < tracks.length; i++) {
        const track = tracks[i];
        idsToAdd[track.id] = track;
      }

      const deleteCandidates = {};
      for (let i = 0; i < contents.length; i++) {
        const remoteTrack = contents[i].track;
        const entryId = contents[i].entryId;

        if (!(remoteTrack.id in idsToAdd)) {
          deleteCandidates[remoteTrack.id] = entryId;
        } else {
          delete idsToAdd[remoteTrack.id];
        }
      }

      const tracksToAdd = [];
      for (const id in idsToAdd) {
        tracksToAdd.push(idsToAdd[id]);
      }

      const entriesToDelete = [];
      const deleteCandidateIds = Object.keys(deleteCandidates);
      if (deleteCandidateIds.length > 0) {
        // Don't delete tracks that may be currently playing.
        // This requires a query against the track cache, since the lastPlayed
        // for remote tracks is set to 0 for any recent plays!?
        // FIXME pull this out to Trackcache.
        const track = db.getSchema().table('Track');
        db.select().from(track).where(
          // We don't know if these entries name a library or store id.
          Lf.op.or(track.id.in(deleteCandidateIds),
                   track.storeId.in(deleteCandidateIds))
        ).exec().then(rows => {
          const nowMillis = new Date().getTime();
          const delayMillis = 0;

          rows.forEach(row => {
            if (delayMillis + row.lastPlayed / 1000 > nowMillis - row.durationMillis) {
              console.info('not deleting', row, 'since it may be playing.',
                           delayMillis + row.lastPlayed / 1000, nowMillis - row.durationMillis
                          );
              if (row.id in deleteCandidates) {
                delete deleteCandidates[row.id];
              } else {
                delete deleteCandidates[row.storeId];
              }
            }
          });

          for (const deleteId in deleteCandidates) {
            entriesToDelete.push({id: deleteId, entryId: deleteCandidates[deleteId]});
          }

          if (entriesToDelete.length > 0) {
            deleteEntries(userIndex, playlistId, entriesToDelete, deleteResponse => { // eslint-disable-line no-unused-vars
              // We log the delete response inside deleteEntries and have no other need for it here.
              addTracks(userIndex, playlistId, tracksToAdd, callback);
            }, onError);
          } else {
            console.log('no need to delete post-filter; adding');
            addTracks(userIndex, playlistId, tracksToAdd, callback, onError);
          }
        }).catch(e => {
          console.error(e);
          Reporting.Raven.captureMessage(JSON.stringify(e), {
            tags: {playlistId},
            extra: {deleteCandidates, deleteCandidateIds, tracksToAdd},
          });
        });
      } else {
        console.log('no need to delete pre-filter; adding');
        addTracks(userIndex, playlistId, tracksToAdd, callback, onError);
      }
    } else {
      console.log('adding to empty');
      addTracks(userIndex, playlistId, tracks, callback, onError);
    }
  }, onError);
};

exports.setPlaylistOrder = function setPlaylistOrder(db, userIndex, playlist, callback, onError) {
  // Set the remote ordering of a playlist according to playlist's sort order.
  // This trusts that the remote contents are already correct.

  // This approach handles the maybe-playing tracks that wouldn't be in our tracks
  // if we queried them locally.

  loadPlaylistContents(db, userIndex, playlist.remoteId, contents => {
    if (contents.length !== 0) {
      // Reordering calls deal in entry ids, not track ids.
      const currentOrdering = [];
      const desiredOrdering = [];
      const idToEntryId = {};

      for (let i = 0; i < contents.length; i++) {
        idToEntryId[contents[i].track.id] = contents[i].entryId;
        currentOrdering.push(contents[i].entryId);
      }

      const remoteTracks = contents.map(c => c.track);

      Trackcache.orderTracks(db, playlist, remoteTracks, orderedTracks => {
        for (let i = 0; i < orderedTracks.length; i++) {
          const track = orderedTracks[i];
          desiredOrdering.push(idToEntryId[track.id]);
        }

        // It's ridiculous that javascript doesn't have a builtin for this.
        // Thankfully we have simple items and can get away with this hack.
        if (JSON.stringify(currentOrdering) !== JSON.stringify(desiredOrdering)) {
          // The two empty strings are sentinels for "first track" and "last track".
          // This lets us send our entire reordering at once without calculating the relative movements.
          // I'm not sure if the interface was intended to be used this way, but it seems to work.
          const payload = [['', 1], [desiredOrdering, '', '']];
          authedGMRequest('changeplaylisttrackorder', payload, userIndex, 'post', response => {
            // TODO These should all be checked for errors.
            // It looks like responses will have [[0, 1, 1], [call-specific response]] on success.
            callback(response);
          }, onError);
        } else {
          // Avoid triggering a ui refresh on noop reorderings.
          console.log('no need to reorder playlist', playlist.title);
          callback(null);
        }
      }, onError);
    } else {
      console.log('no need to reorder empty playlist', playlist.title);
      callback(null);
    }
  }, onError);
};
<|endoftext|>


<commit_before>angular.module('RPC', []).factory('$rpc', function ($rootScope, $log, $q) {
    var invocationCounter = 0;
    var endCounter = 0;
    var serverChannels = {};
    var clientChannels = {};
    var deferreds = [];
    var baseURL;
    var rpcMaster;
    var knownTemplates = {};

    var serverRunDate;  // used for invalidating the cache
    var serverRunDateDeferred = $q.defer();
    serverRunDateDeferred.promise.then(function (date) {
        serverRunDate = new Date(date);
    });

    var callEnded = function (Id) {
        if (deferreds[Id]) {
            delete deferreds[Id];
            endCounter++;
            rpc.onEnd(endCounter);
            if (endCounter == invocationCounter) {
                rpc.onBatchEnd(endCounter);
                invocationCounter = 0;
                endCounter = 0;
            }
        }else {
            $log.warn("Deferred Id " + Id + " was resolved/rejected more than once, this should not occur.");
        }
    };

    /**
     * Generates a 'safe' key for storing cache in client's local storage
     * @param name
     * @returns {string}
     */
    function getCacheKey(name) {
        return 'SIORPC:' + baseURL + '/' + name;
    }

    function cacheIt(key, data) {
        try{
            localStorage[key] = JSON.stringify(data);
        }catch(e){
            $log.warn("Error raised when writing to local storage: " + e); // probably quoata exceeded
        }
    }

    var _loadChannel = function (name, handshakeData, deferred) {
        if (!serverChannels.hasOwnProperty(name)) {
            serverChannels[name] = {};
        }
        var channel = serverChannels[name];
        channel._loadDef = deferred;
        channel._handshake = handshakeData;
        serverRunDateDeferred.promise.then(function () {

            var cacheKey = getCacheKey(name);
            var cached = localStorage[cacheKey];
            if (cached) {
                cached = JSON.parse(cached);
                if (serverRunDate < new Date(cached.cDate)) {
                    if (handshakeData) {
                        rpcMaster.emit('authenticate', {name: name, handshake: handshakeData});
                        channel.cached = cached;
                    } else {
                        connectToServerChannel(channel, name);
                        registerRemoteFunctions(cached, false); // will register functions from cached manifest
                    }
                } else {
                    //cache has been invalidated
                    delete localStorage[cacheKey];
                    rpcMaster.emit('load channel', {name: name, handshake: handshakeData});
                }
            } else {
                rpcMaster.emit('load channel', {name: name, handshake: handshakeData});
            }
        });

        return channel._loadDef.promise;
    };

    var registerRemoteFunctions = function (data, storeInCache) {
        var channel = serverChannels[data.name];
        var remoteMethodInvocation = function (fnName) {
            channel[fnName] = function () {
                invocationCounter++;
                channel._socket.emit('call',
                    {Id: invocationCounter, fnName: fnName, args: Array.prototype.slice.call(arguments, 0)}
                );
                if (invocationCounter == 1) {
                    rpc.onBatchStarts(invocationCounter);
                }
                rpc.onCall(invocationCounter);
                deferreds[invocationCounter] = $q.defer();
                return deferreds[invocationCounter].promise;
            };
        };

        if (data.fnNames) {
            if (data.tplId) {
                //store the template
                knownTemplates[data.tplId] = data.fnNames;
            }
            data.fnNames.forEach(remoteMethodInvocation);   //initialize from incoming data
        } else {
            knownTemplates[data.tplId].forEach(remoteMethodInvocation); //this has to be initialized from known template
        }

        channel._loadDef.resolve(channel);
        if (storeInCache !== false) {
            $rootScope.$apply();
            data.cDate = new Date();    // here we make a note of when the channel cache was saved
            cacheIt(getCacheKey(data.name), data)
        }

    };

    /**
     *
     * @param {Object} channel
     * @param {String} name
     */
    var connectToServerChannel = function (channel, name) {

        channel._socket = io.connect(baseURL + '/rpc-' + name)
            .on('resolve', function (data) {
                deferreds[data.Id].resolve(data.value);
                callEnded(data.Id);
            })
            .on('reject', function (data) {
                if (data && data.Id) {
                    deferreds[data.Id].reject(data.reason);
                    $log.error("Call " + data.Id + " is rejected, reason ", data.reason);

                    callEnded(data.Id);
                } else {
                    $log.error("Unknown error occured on RPC socket connection");
                }
            })
            .on('connect_failed', function (reason) {
                $log.error('unable to connect to namespace ', reason);
                channel._loadDef.reject(reason);
            })
            .on('disconnect', function (data) {
                delete serverChannels[name];
                $log.warn("Server channel " + name + " disconnected.");
            })
            .on('reconnect', function () {
                $log.info('reconnected channel' + name);
                rpc.loadChannel(name, channel._handshake, true);
            });
    };

    /**
     * connects to remote server which exposes RPC calls
     * @param {String} url to connect to, for example http://localhost:8080
     * @param {Object} handshake for global authorization
     * returns {Socket} master socket
     */
    var connect = function (url, handshake) {
        if (!rpcMaster && url) {
            baseURL = url;
            rpcMaster = io.connect(url + '/rpc-master', handshake)
                .on('serverRunDate', function (runDate) {
                    serverRunDateDeferred.resolve(runDate);
                    $rootScope.$apply();
                })
                .on('authenticated', function (data) {
                    var name = data.name;
                    var channel = serverChannels[name];
                    connectToServerChannel(channel, name);
                    registerRemoteFunctions(channel.cached, false); // will register functions from cached manifest
                })
                .on('channelFns', function (data, storeInCache) {
                    var name = data.name;
                    var channel = serverChannels[name];
                    connectToServerChannel(channel, name);
                    registerRemoteFunctions(data, storeInCache);
                })
                .on('channelDoesNotExist', function (data) {

                    var channel = serverChannels[data.name];
                    channel._loadDef.reject();
                    $log.warn("no channel under name: " + data.name);
                    $rootScope.$apply();

                })
                .on('client channel created', function (name) {

                    var channel = clientChannels[name];
                    var socket = io.connect(baseURL + '/rpcC-' + name + '/' + rpcMaster.io.engine.id);  //rpcC stands for rpc Client
                    channel._socket = socket;
                    socket.on('call', function (data) {
                        var exposed = channel.fns;
                        if (exposed.hasOwnProperty(data.fnName) && typeof exposed[data.fnName] === 'function') {

                            var retVal = exposed[data.fnName].apply(this, data.args);
                            $q.when(retVal).then(function (retVal) {
								if (retVal instanceof Error) {
									// when synchronously returned Error
									socket.emit('reject', { Id: data.Id, reason: retVal.toString() });
								} else {
									socket.emit('resolve', { Id: data.Id, value: retVal });
								}
                            }, function (error) {
								if (error instanceof Error) {
									error = error.toString();
								}
								socket.emit('reject', { Id: data.Id, reason: error });
                            });

                        } else {
                            socket.emit('reject', {Id: data.Id, reason: 'no such function has been exposed: ' + data.fnName });
                        }
                    });
                    channel.deferred.resolve(channel);

                });
            return rpcMaster;

        } else {
            $log.warn("ignoring connect command, either url of master null or already connected");
        }
    };
    var rpc = {
        connect: connect,
        loadAllChannels: function () {
            if (rpcMaster) {
                rpcMaster.__channelListLoad = $q.defer();
                rpcMaster.emit('load channelList');
                rpcMaster
                    .on('channels', function (data) {
                        var name = data.list.pop();
                        while(name) {
                            serverChannels[name] = {};
                            _loadChannel(name);
                            name = data.list.pop();
                        }
                        rpcMaster.__channelListLoad.resolve(serverChannels);
                        $rootScope.$apply();

                    });
                return rpcMaster.__channelListLoad.promise;
            } else {
                $log.error("no connection to master");
            }

        },
        /**
         * for a particular channel this will connect and prepared the channel for use, if called more than once for one
         * channel, it will return it's instance
         * @param {string} name
         * @param {*} [handshakeData] custom param for authentication
         * @param {Boolean} force pass true when you want to load the channnel again even if it exists already-used on reconnection
         * @returns {promise}
         */
        loadChannel: function (name, handshakeData, force) {
            if (serverChannels.hasOwnProperty(name) && !force) {
                return serverChannels[name]._loadDef.promise;
            } else {
                var def = $q.defer();
                _loadChannel(name, handshakeData, def);
                return def.promise;
            }
        },
        /**
         * @param name {string}
         * @param toExpose {Object} object with functions as values
         * @returns {Promise} a promise saying that server is connected and can call the client
         */
        expose: function (name, toExpose) { //
            if (!clientChannels.hasOwnProperty(name)) {
                clientChannels[name] = {};
            }
            var channel = clientChannels[name];
            channel.fns = toExpose;
            channel.deferred = $q.defer();
            var fnNames = [];
            for(var fn in toExpose)
            {
				if (fn === '_socket') {
					throw new Error('Failed to expose channel, _socket property is reserved for socket namespace');
				}
                fnNames.push(fn);
            }

			rpcMaster.emit('expose channel', {name: name, fns: fnNames});

            return channel.deferred.promise;
        }
    };
    var nop = angular.noop;
	//These are internal callbacks of socket.io-rpc, use them if you want to implement something like a global loader indicator
    rpc.onBatchStarts = nop;	//called when invocation counter equals 1
    rpc.onBatchEnd = nop;		//called when invocation counter equals endCounter
    rpc.onCall = nop;			//called when invocation counter equals endCounter
    rpc.onEnd = nop;			//called when one call is returned
    rpc.auth = {};
    return rpc;
}).directive('rpcController', function ($controller, $q, $rpc) {
    return {
		scope: true,
		compile: function compile(tEl, tAttrs) {
			return {
				pre: function (scope, iElement, attr, controller) {
                    if (!attr.rpcChannel) {
                        throw new Error("No channel name defined on rpc-controller element: " + iElement[0].outerHTML);
                    }
                    var ctrlName = attr.rpcController;
                    var instantiate = function (promise) {
                        promise.then(function (channel) {
                            var localInj = {
                                $scope: scope
                            };
                            localInj[attr.rpcChannel] = channel;
                            var ctrl = $controller(ctrlName, localInj);
                            iElement.children().data('$ngControllerController', ctrl);
                        }, function (err) {
                            $log.error("Cannot instantiate rpc-controller - channel failed to load");
                        });
                    };
                    if (attr.rpcAuth) {
						var authGetter = $rpc.auth[attr.rpcAuth];
						if (typeof authGetter !== 'function') {
							throw new Error('no auth getter function found under ' + attr.rpcAuth);
						}
                        $q.when(authGetter()).then(function (handshake) {

                            var promise = $rpc.loadChannel(attr.rpcChannel, handshake);
                            instantiate(promise);
                        });
					} else {
                        var promise = $rpc.loadChannel(attr.rpcChannel);
                        instantiate(promise);
                    }

                }
			};
		}
	}

});
<commit_msg>hopefully now properly handling channel load promises<commit_after>angular.module('RPC', []).factory('$rpc', function ($rootScope, $log, $q) {
    var invocationCounter = 0;
    var endCounter = 0;
    var serverChannels = {};
    var clientChannels = {};
    var deferreds = [];
    var baseURL;
    var rpcMaster;
    var knownTemplates = {};

    var serverRunDate;  // used for invalidating the cache
    var serverRunDateDeferred = $q.defer();
    serverRunDateDeferred.promise.then(function (date) {
        serverRunDate = new Date(date);
    });

    var callEnded = function (Id) {
        if (deferreds[Id]) {
            delete deferreds[Id];
            endCounter++;
            rpc.onEnd(endCounter);
            if (endCounter == invocationCounter) {
                rpc.onBatchEnd(endCounter);
                invocationCounter = 0;
                endCounter = 0;
            }
        }else {
            $log.warn("Deferred Id " + Id + " was resolved/rejected more than once, this should not occur.");
        }
    };

    /**
     * Generates a 'safe' key for storing cache in client's local storage
     * @param name
     * @returns {string}
     */
    function getCacheKey(name) {
        return 'SIORPC:' + baseURL + '/' + name;
    }

    function cacheIt(key, data) {
        try{
            localStorage[key] = JSON.stringify(data);
        }catch(e){
            $log.warn("Error raised when writing to local storage: " + e); // probably quoata exceeded
        }
    }

    var _loadChannel = function (name, handshakeData, deferred) {
        if (!serverChannels.hasOwnProperty(name)) {
            serverChannels[name] = {};
        }
        var channel = serverChannels[name];
        channel._loadDef = deferred;
        channel._handshake = handshakeData;
        serverRunDateDeferred.promise.then(function () {

            var cacheKey = getCacheKey(name);
            var cached = localStorage[cacheKey];
            if (cached) {
                cached = JSON.parse(cached);
                if (serverRunDate < new Date(cached.cDate)) {
                    if (handshakeData) {
                        rpcMaster.emit('authenticate', {name: name, handshake: handshakeData});
                        channel.cached = cached;
                    } else {
                        connectToServerChannel(channel, name);
                        registerRemoteFunctions(cached, false); // will register functions from cached manifest
                    }
                } else {
                    //cache has been invalidated
                    delete localStorage[cacheKey];
                    rpcMaster.emit('load channel', {name: name, handshake: handshakeData});
                }
            } else {
                rpcMaster.emit('load channel', {name: name, handshake: handshakeData});
            }
        });

        return channel._loadDef.promise;
    };

    var registerRemoteFunctions = function (data, storeInCache) {
        var channel = serverChannels[data.name];
        var remoteMethodInvocation = function (fnName) {
            channel[fnName] = function () {
                invocationCounter++;
                channel._socket.emit('call',
                    {Id: invocationCounter, fnName: fnName, args: Array.prototype.slice.call(arguments, 0)}
                );
                if (invocationCounter == 1) {
                    rpc.onBatchStarts(invocationCounter);
                }
                rpc.onCall(invocationCounter);
                deferreds[invocationCounter] = $q.defer();
                return deferreds[invocationCounter].promise;
            };
        };

        if (data.fnNames) {
            if (data.tplId) {
                //store the template
                knownTemplates[data.tplId] = data.fnNames;
            }
            data.fnNames.forEach(remoteMethodInvocation);   //initialize from incoming data
        } else {
            knownTemplates[data.tplId].forEach(remoteMethodInvocation); //this has to be initialized from known template
        }

        channel._loadDef.resolve(channel);
        if (storeInCache !== false) {
            $rootScope.$apply();
            data.cDate = new Date();    // here we make a note of when the channel cache was saved
            cacheIt(getCacheKey(data.name), data)
        }

    };

    /**
     *
     * @param {Object} channel
     * @param {String} name
     */
    var connectToServerChannel = function (channel, name) {

        channel._socket = io.connect(baseURL + '/rpc-' + name)
            .on('resolve', function (data) {
                deferreds[data.Id].resolve(data.value);
                callEnded(data.Id);
            })
            .on('reject', function (data) {
                if (data && data.Id) {
                    deferreds[data.Id].reject(data.reason);
                    $log.error("Call " + data.Id + " is rejected, reason ", data.reason);

                    callEnded(data.Id);
                } else {
                    $log.error("Unknown error occured on RPC socket connection");
                }
            })
            .on('connect_failed', function (reason) {
                $log.error('unable to connect to namespace ', reason);
                channel._loadDef.reject(reason);
            })
            .on('disconnect', function (data) {
                var reconDfd = $q.defer();
                channel._loadDef = reconDfd;
                $log.warn("Server channel " + name + " disconnected.");

                channel._socket.on('reconnect', function () {
                    $log.info('reconnected channel' + name);
                    _loadChannel(name, channel._handshake, reconDfd);
                });
            });
    };

    /**
     * connects to remote server which exposes RPC calls
     * @param {String} url to connect to, for example http://localhost:8080
     * @param {Object} handshake for global authorization
     * returns {Socket} master socket
     */
    var connect = function (url, handshake) {
        if (!rpcMaster && url) {
            baseURL = url;
            rpcMaster = io.connect(url + '/rpc-master', handshake)
                .on('serverRunDate', function (runDate) {
                    serverRunDateDeferred.resolve(runDate);
                    $rootScope.$apply();
                })
                .on('authenticated', function (data) {
                    var name = data.name;
                    var channel = serverChannels[name];
                    connectToServerChannel(channel, name);
                    registerRemoteFunctions(channel.cached, false); // will register functions from cached manifest
                })
                .on('channelFns', function (data, storeInCache) {
                    var name = data.name;
                    var channel = serverChannels[name];
                    connectToServerChannel(channel, name);
                    registerRemoteFunctions(data, storeInCache);
                })
                .on('channelDoesNotExist', function (data) {

                    var channel = serverChannels[data.name];
                    channel._loadDef.reject();
                    $log.warn("no channel under name: " + data.name);
                    $rootScope.$apply();

                })
                .on('client channel created', function (name) {

                    var channel = clientChannels[name];
                    var socket = io.connect(baseURL + '/rpcC-' + name + '/' + rpcMaster.io.engine.id);  //rpcC stands for rpc Client
                    channel._socket = socket;
                    socket.on('call', function (data) {
                        var exposed = channel.fns;
                        if (exposed.hasOwnProperty(data.fnName) && typeof exposed[data.fnName] === 'function') {

                            var retVal = exposed[data.fnName].apply(this, data.args);
                            $q.when(retVal).then(function (retVal) {
								if (retVal instanceof Error) {
									// when synchronously returned Error
									socket.emit('reject', { Id: data.Id, reason: retVal.toString() });
								} else {
									socket.emit('resolve', { Id: data.Id, value: retVal });
								}
                            }, function (error) {
								if (error instanceof Error) {
									error = error.toString();
								}
								socket.emit('reject', { Id: data.Id, reason: error });
                            });

                        } else {
                            socket.emit('reject', {Id: data.Id, reason: 'no such function has been exposed: ' + data.fnName });
                        }
                    });
                    channel.deferred.resolve(channel);

                });
            return rpcMaster;

        } else {
            $log.warn("ignoring connect command, either url of master null or already connected");
        }
    };
    var rpc = {
        connect: connect,
        loadAllChannels: function () {
            if (rpcMaster) {
                rpcMaster.__channelListLoad = $q.defer();
                rpcMaster.emit('load channelList');
                rpcMaster
                    .on('channels', function (data) {
                        var name = data.list.pop();
                        while(name) {
                            serverChannels[name] = {};
                            _loadChannel(name);
                            name = data.list.pop();
                        }
                        rpcMaster.__channelListLoad.resolve(serverChannels);
                        $rootScope.$apply();

                    });
                return rpcMaster.__channelListLoad.promise;
            } else {
                $log.error("no connection to master");
            }

        },
        /**
         * for a particular channel this will connect and prepared the channel for use, if called more than once for one
         * channel, it will return it's instance
         * @param {string} name
         * @param {*} [handshakeData] custom param for authentication
         * @param {Boolean} force pass true when you want to load the channnel again even if it exists already-used on reconnection
         * @returns {promise}
         */
        loadChannel: function (name, handshakeData, force) {
            if (serverChannels.hasOwnProperty(name) && !force) {
                return serverChannels[name]._loadDef.promise;
            } else {
                var def = $q.defer();
                _loadChannel(name, handshakeData, def);
                return def.promise;
            }
        },
        /**
         * @param name {string}
         * @param toExpose {Object} object with functions as values
         * @returns {Promise} a promise saying that server is connected and can call the client
         */
        expose: function (name, toExpose) { //
            if (!clientChannels.hasOwnProperty(name)) {
                clientChannels[name] = {};
            }
            var channel = clientChannels[name];
            channel.fns = toExpose;
            channel.deferred = $q.defer();
            var fnNames = [];
            for(var fn in toExpose)
            {
				if (fn === '_socket') {
					throw new Error('Failed to expose channel, _socket property is reserved for socket namespace');
				}
                fnNames.push(fn);
            }

			rpcMaster.emit('expose channel', {name: name, fns: fnNames});

            return channel.deferred.promise;
        }
    };
    var nop = angular.noop;
	//These are internal callbacks of socket.io-rpc, use them if you want to implement something like a global loader indicator
    rpc.onBatchStarts = nop;	//called when invocation counter equals 1
    rpc.onBatchEnd = nop;		//called when invocation counter equals endCounter
    rpc.onCall = nop;			//called when invocation counter equals endCounter
    rpc.onEnd = nop;			//called when one call is returned
    rpc.auth = {};
    return rpc;
}).directive('rpcController', function ($controller, $q, $rpc) {
    return {
		scope: true,
		compile: function compile(tEl, tAttrs) {
			return {
				pre: function (scope, iElement, attr, controller) {
                    if (!attr.rpcChannel) {
                        throw new Error("No channel name defined on rpc-controller element: " + iElement[0].outerHTML);
                    }
                    var ctrlName = attr.rpcController;
                    var instantiate = function (promise) {
                        promise.then(function (channel) {
                            var localInj = {
                                $scope: scope
                            };
                            localInj[attr.rpcChannel] = channel;
                            var ctrl = $controller(ctrlName, localInj);
                            iElement.children().data('$ngControllerController', ctrl);
                        }, function (err) {
                            $log.error("Cannot instantiate rpc-controller - channel failed to load");
                        });
                    };
                    if (attr.rpcAuth) {
						var authGetter = $rpc.auth[attr.rpcAuth];
						if (typeof authGetter !== 'function') {
							throw new Error('no auth getter function found under ' + attr.rpcAuth);
						}
                        $q.when(authGetter()).then(function (handshake) {

                            var promise = $rpc.loadChannel(attr.rpcChannel, handshake);
                            instantiate(promise);
                        });
					} else {
                        var promise = $rpc.loadChannel(attr.rpcChannel);
                        instantiate(promise);
                    }

                }
			};
		}
	}

});
<|endoftext|>