<commit_before>
/**
 * socket.io
 * Copyright(c) 2011 LearnBoost <dev@learnboost.com>
 * MIT Licensed
 */

(function (exports, io) {

  /**
   * Expose constructor.
   */

  exports.SocketNamespace = SocketNamespace;

  /**
   * Socket namespace constructor.
   *
   * @constructor
   * @api public
   */

  function SocketNamespace (socket, name) {
    this.socket = socket;
    this.name = name || '';
    this.flags = {};
    this.json = new Flag(this, 'json');
    this.ackPackets = 0;
    this.acks = {};
  };

  /**
   * Apply EventEmitter mixin.
   */

  io.util.mixin(SocketNamespace, io.EventEmitter);

  /**
   * Copies emit since we override it
   *
   * @api private
   */

  SocketNamespace.prototype.$emit = io.EventEmitter.prototype.emit;

  /**
   * Sends a packet.
   *
   * @api private
   */

  SocketNamespace.prototype.packet = function (packet) {
    packet.endpoint = this.name;
    this.socket.packet(packet);
    this.flags = {};
    return this;
  };

  /**
   * Sends a message
   *
   * @api public
   */

  SocketNamespace.prototype.send = function (data, fn) {
    var packet = {
        type: this.flags.json ? 'json' : 'message'
      , data: data
    };

    if ('function' == typeof fn) {
      packet.id = ++this.ackPackets;
      packet.ack = fn.length ? 'data' : true;
      this.acks[packet.id] = fn;
    }

    return this.packet(packet);
  };

  /**
   * Emits an event
   *
   * @api public
   */

  SocketNamespace.prototype.emit = function (name) {
    var args = Array.prototype.slice.call(arguments, 1)
      , lastArg = args[args.length - 1]
      , packet = {
            type: 'event'
          , name: name
        };

    if ('function' == typeof lastArg) {
      packet.id = ++this.ackPackets;
      packet.ack = lastArg.length ? 'data' : true;
      this.acks[packet.id] = lastArg;
      args = args.slice(0, args.length - 1);
    }

    packet.args = args;

    return this.packet(packet);
  };

  /**
   * Disconnects the namespace
   *
   * @api private
   */

  SocketNamespace.prototype.disconnect = function () {
    if (this.name === '') {
      this.socket.disconnect();
    } else {
      this.packet({ type: 'disconnect' });
      this.$emit('disconnect');
    }

    return this;
  };

  /**
   * Handles a packet
   *
   * @api private
   */

  SocketNamespace.prototype.onPacket = function (packet) {
    var self = this;

    function ack () {
      self.packet({
          type: 'ack'
        , args: io.util.toArray(arguments)
        , ackId: packet.id
      });
    };

    switch (packet.type) {
      case 'connect':
        this.$emit('connect');
        break;

      case 'disconnect':
        if (this.name === '') {
          this.socket.onDisconnect(packet.reason || 'booted');
        } else {
          this.$emit('disconnect', packet.reason);
        }
        break;

      case 'message':
      case 'json':
        var params = ['message', packet.data];

        if (packet.ack == 'data') {
          params.push(ack);
        } else if (packet.ack) {
          this.packet({ type: 'ack', ackId: packet.id });
        }

        this.$emit.apply(this, params);
        break;

      case 'event':
        var params = [packet.name].concat(packet.args);

        if (packet.ack == 'data')
          params.push(ack);

        this.$emit.apply(this, params);
        break;

      case 'ack':
        if (this.acks[packet.ackId]) {
          this.acks[packet.ackId].apply(this, packet.args);
          delete this.acks[packet.ackId];
        }

      case 'error':
        if (packet.advice){
          this.socket.onError(packet);
        } else {
          this.$emit('error', packet.reason);
        }
    }
  };

  /**
   * Flag interface.
   *
   * @api private
   */

  function Flag (nsp, name) {
    this.namespace = nsp;
    this.name = name;
  };

  /**
   * Send a message
   *
   * @api public
   */

  Flag.prototype.send = function () {
    this.namespace.flags[this.name] = true;
    this.namespace.send.apply(this.namespace, arguments);
  };

  /**
   * Emit an event
   *
   * @api public
   */

  Flag.prototype.emit = function () {
    this.namespace.flags[this.name] = true;
    this.namespace.emit.apply(this.namespace, arguments);
  };

})(
    'undefined' != typeof io ? io : module.exports
  , 'undefined' != typeof io ? io : module.parent.exports
);
<commit_msg>added missing breaks; the `ack` packet also emitted the onerror<commit_after>
/**
 * socket.io
 * Copyright(c) 2011 LearnBoost <dev@learnboost.com>
 * MIT Licensed
 */

(function (exports, io) {

  /**
   * Expose constructor.
   */

  exports.SocketNamespace = SocketNamespace;

  /**
   * Socket namespace constructor.
   *
   * @constructor
   * @api public
   */

  function SocketNamespace (socket, name) {
    this.socket = socket;
    this.name = name || '';
    this.flags = {};
    this.json = new Flag(this, 'json');
    this.ackPackets = 0;
    this.acks = {};
  };

  /**
   * Apply EventEmitter mixin.
   */

  io.util.mixin(SocketNamespace, io.EventEmitter);

  /**
   * Copies emit since we override it
   *
   * @api private
   */

  SocketNamespace.prototype.$emit = io.EventEmitter.prototype.emit;

  /**
   * Sends a packet.
   *
   * @api private
   */

  SocketNamespace.prototype.packet = function (packet) {
    packet.endpoint = this.name;
    this.socket.packet(packet);
    this.flags = {};
    return this;
  };

  /**
   * Sends a message
   *
   * @api public
   */

  SocketNamespace.prototype.send = function (data, fn) {
    var packet = {
        type: this.flags.json ? 'json' : 'message'
      , data: data
    };

    if ('function' == typeof fn) {
      packet.id = ++this.ackPackets;
      packet.ack = fn.length ? 'data' : true;
      this.acks[packet.id] = fn;
    }

    return this.packet(packet);
  };

  /**
   * Emits an event
   *
   * @api public
   */

  SocketNamespace.prototype.emit = function (name) {
    var args = Array.prototype.slice.call(arguments, 1)
      , lastArg = args[args.length - 1]
      , packet = {
            type: 'event'
          , name: name
        };

    if ('function' == typeof lastArg) {
      packet.id = ++this.ackPackets;
      packet.ack = lastArg.length ? 'data' : true;
      this.acks[packet.id] = lastArg;
      args = args.slice(0, args.length - 1);
    }

    packet.args = args;

    return this.packet(packet);
  };

  /**
   * Disconnects the namespace
   *
   * @api private
   */

  SocketNamespace.prototype.disconnect = function () {
    if (this.name === '') {
      this.socket.disconnect();
    } else {
      this.packet({ type: 'disconnect' });
      this.$emit('disconnect');
    }

    return this;
  };

  /**
   * Handles a packet
   *
   * @api private
   */

  SocketNamespace.prototype.onPacket = function (packet) {
    var self = this;

    function ack () {
      self.packet({
          type: 'ack'
        , args: io.util.toArray(arguments)
        , ackId: packet.id
      });
    };

    switch (packet.type) {
      case 'connect':
        this.$emit('connect');
        break;

      case 'disconnect':
        if (this.name === '') {
          this.socket.onDisconnect(packet.reason || 'booted');
        } else {
          this.$emit('disconnect', packet.reason);
        }
        break;

      case 'message':
      case 'json':
        var params = ['message', packet.data];

        if (packet.ack == 'data') {
          params.push(ack);
        } else if (packet.ack) {
          this.packet({ type: 'ack', ackId: packet.id });
        }

        this.$emit.apply(this, params);
        break;

      case 'event':
        var params = [packet.name].concat(packet.args);

        if (packet.ack == 'data')
          params.push(ack);

        this.$emit.apply(this, params);
        break;

      case 'ack':
        if (this.acks[packet.ackId]) {
          this.acks[packet.ackId].apply(this, packet.args);
          delete this.acks[packet.ackId];
        }
        break;

      case 'error':
        if (packet.advice){
          this.socket.onError(packet);
        } else {
          this.$emit('error', packet.reason);
        }
        break;
    }
  };

  /**
   * Flag interface.
   *
   * @api private
   */

  function Flag (nsp, name) {
    this.namespace = nsp;
    this.name = name;
  };

  /**
   * Send a message
   *
   * @api public
   */

  Flag.prototype.send = function () {
    this.namespace.flags[this.name] = true;
    this.namespace.send.apply(this.namespace, arguments);
  };

  /**
   * Emit an event
   *
   * @api public
   */

  Flag.prototype.emit = function () {
    this.namespace.flags[this.name] = true;
    this.namespace.emit.apply(this.namespace, arguments);
  };

})(
    'undefined' != typeof io ? io : module.exports
  , 'undefined' != typeof io ? io : module.parent.exports
);
<|endoftext|>


<commit_before>// Copyright 2011 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

var capServer = new CapServer(newUUIDv4());

var resolver = function(instID) {
  if (instID in instToTabID) {
    return launchedInstances[instToTabID[instID]].tunnel.sendInterface;
  }
  else if (instID === capServer.instanceID) {
    return capServer.publicInterface;
  }
  else {
    // TODO(arjun): this is terrible; Joe told me to do this
    return currentStation.tunnel.sendInterface;
  }
};

capServer.setResolver(resolver);

// { tabID : { callbackName : [ rcList, info ] }}
var offerMap = Object.create(null);
var acceptMap = Object.create(null);

//
// Station setup
//
var stations = (function() {
  var stations = localStorage.stations ?
    JSON.parse(localStorage.stations) : {};

  function set(name, stationURL) {
    stations[name] = stationURL;
    localStorage.stations = JSON.stringify(stations);
  }

  function get(name) {
    return stations[name];
  }

  function names() {
    return Object.keys(stations);
  }

  return Object.freeze({
    set: set,
    get: get,
    names: names,
  });
})();

var MAKESTATION = 'http://localhost:9001/belay/generate';

var makeStation = function(name, k) {
  capServer.restore(MAKESTATION).get(
    function(stationCap) {
      // TODO(mzero): should be defensive about stationCap being a Cap
      stations.set(name, stationCap.serialize());
      if (k) k();
    },
    function(_) {
      console.log('Failed to gen station');
    });
};

var launchStation = function(name) {
  launch(stations.get(name));
}

var instToTabID = Object.create(null);
/* Maps tabIDs to
  { instID: Str,
    outpostData: Any,    station-determined; background is transparent
    tunnel: CapTunnel,
    url: Str,
    station: ``Station''
   }
*/
var launchedInstances = Object.create(null);
var launchedStations = Object.create(null);

var makeSetNewInstHandler = function(station) {
  return capServer.grant(function(handler) {
    station.newInstHandler = handler;
  });
};

var makeCloseInstHandler = function(station) {
  return capServer.grant(function(handler) {
    station.closeInstHandler = handler;
  });
};

var makeCloseInstanceCap = function(instID) {
  return capServer.grant(function() {
    var tabID = instToTabID[instID];
    delete instToTabID[instID];
    delete launchedInstances[tabID];
    // TODO(arjun): assume success; closing caps should be revoked when the
    // instance is closed.
    chrome.tabs.remove(tabID);
  });
};

// Capability returned to station to launch instances in new windows.
var makeLaunchHandler = function(station) {
  return capServer.grant({
    post: function(args, sk, fk) {
      var width = typeof args.width === 'number' ? args.width : undefined;
      var height = typeof args.height === 'number' ? args.height: undefined;
      chrome.windows.create({ url: args.url,
                              width: width,
                              height: height },
                            function(wnd) {
        var tab = wnd.tabs[0];
        var tunnel = makeTunnel(tabPorts.getTabPort(tab.id));
        launchedInstances[tab.id] = { instID: args.instID,
                                      tunnel: tunnel,
                                      url: args.url,
                                      outpostData: args.outpostData,
                                      station: station };
        instToTabID[args.instID] = tab.id;
        sk(makeCloseInstanceCap(args.instID));
        // chrome.tabs.onUpdated fires on the next turn, sending the
        // outpost message.
      });
    }
  });
};

var instanceRequest = function(data, tabID) {
  var launchData = capServer.dataPostProcess(data);
  var station = currentStation;

  station.newInstHandler.post({
    launchData: launchData,
    relaunch: capServer.grant(function(args) {
      chrome.tabs.update(tabID, { url: args.url }, function(tab) {
        var tunnel = makeTunnel(tabPorts.refreshTabPort(tab.id));
        launchedInstances[tab.id] = { instID: args.instID,
                                      tunnel: tunnel,
                                      url: args.url,
                                      outpostData: args.outpostData,
                                      station: station };
        instToTabID[args.instID] = tab.id;
        // chrome.tabs.onUpdated _does not fire_ on the next turn.
        tunnel.sendOutpost(capServer.dataPreProcess(args.outpostData));
      });
      return makeCloseInstanceCap(args.instID);
    })
  });
};

var makeTunnel = function(port) {
  var tunnel = new CapTunnel(port);
  tunnel.setLocalResolver(resolver);
  return tunnel;
};

// opens a page, and sends info over
var launch = function(url) {
  capServer.restore(url).get(
    function(data) {
      var page = data.page;
      var info = data.info;
      chrome.tabs.create({ url: page.html },
        function(tab) {
          var tunnel = makeTunnel(tabPorts.getTabPort(tab.id));
          launchedStations[tab.id] = {
            url: url, html: page.html, info: info, tunnel: tunnel };
        });
    },
    function(_) {
      console.log('Launch failed.');
    });
};

// Also runs on first-load
var reloadInstance = function(tabID, info, tab) {
  var instance = launchedInstances[tabID];
  console.assert(instance);
  instance.tunnel.sendOutpost(capServer.dataPreProcess(instance.outpostData));
};

var currentStation = false;

var handleClosedStation = function(tabID) {
  var station = launchedStations[tabID];

  if (currentStation === station) {
    currentStation = false;
  }

  closeInstancesOfStation(station);
  delete launchedStations[tabID];
}

var closeInstancesOfStation = function(station) {
  Object.keys(launchedInstances).forEach(function(instTabID) {
    var instance = launchedInstances[instTabID];
    if (instance.station === station) {
      delete instToTabID[instance.instID];
      delete launchedInstances[instTabID];
      chrome.tabs.remove(Number(instTabID));
    }
  });
}

chrome.tabs.onRemoved.addListener(function (tabID, removeInfo) {
  if (tabID in launchedInstances) {
    var station = launchedInstances[tabID].station;
    var instID = launchedInstances[tabID].instID;
    delete instToTabID[instID];
    delete launchedInstances[tabID];
    station.closeInstHandler.put(instID);
  }
  else if (tabID in launchedStations) {
    handleClosedStation(tabID);
  }
});

// NOTE(jpolitz): This event is called twice on page load, and twice
// on page refresh.  We only handle 'complete' events, so we can be
// sure that the receiving tab is correctly set up.
chrome.tabs.onUpdated.addListener(function(tabID, info, tab) {
  if (info.status !== 'complete') return;
  var tabInfo = launchedStations[tabID];
  if (tabInfo === undefined) {
    if (tabID in launchedInstances) {
      reloadInstance(tabID, info, tab);
    }
    return;
  }

  // TODO(arjun): hack--currentStation is the last loaded station
  currentStation = tabInfo;

  closeInstancesOfStation(tabInfo);

  tabInfo.tunnel = makeTunnel(tabPorts.refreshTabPort(tabID));

  var sendToTunnel = function(info) {
    tabInfo.tunnel.sendOutpost(capServer.dataPreProcess({
      info: info,
      browserID: capServer.instanceID,
      services: {
        highlightByRC: capServer.grant(highlighting.highlightByRC),
        unhighlight: capServer.grant(highlighting.unhighlight)
      },
      launch: makeLaunchHandler(tabInfo),
      setNewInstHandler: makeSetNewInstHandler(tabInfo),
      setCloseInstHandler: makeCloseInstHandler(tabInfo)
    }));
  };

  if (tabInfo.info) {
    sendToTunnel(tabInfo.info);
    tabInfo.info = undefined;
  } else {
    capServer.restore(tabInfo.url).get(
      function(data) {
        if (data.page.html === info.html || info.url === undefined) {
          sendToTunnel(data.info);
        }
      });
  }
});

//
// Ports to pages
//

var tabPorts = (function() {
  var ports = Object.create(null);

  var getTabPort = function(tabID) {
    if (!(tabID in ports)) ports[tabID] = new PortQueue();
    return ports[tabID];
  };

  var refreshTabPort = function(tabID) {
    if (tabID in ports) delete ports[tabID];
    return getTabPort(tabID);
  };

  var makeRelayPort = function(tabID) {
    var extPort = {
      postMessage: function(message, ports) {
        if (ports && ports.length > 0) { throw 'TabPort: Can\'t send ports'; }
        chrome.tabs.sendRequest(tabID, message);
      },
      onmessage: function() { throw 'ExtPort: onmessage not set.'; }
    };
    return extPort;
  };

  chrome.extension.onRequest.addListener(
    function(message, sender, sendResponse) {
      var tabID = sender.tab.id;
      var port = getTabPort(tabID);
      if (message.type === 'init') {
        if (!port.hasPort()) port.setPort(makeRelayPort(tabID));
      }
      else if (message.type === 'instanceRequest') {
        instanceRequest(message.gen, tabID);
      }
      else port.onmessage({ data: message });
    });

  return Object.freeze({
      getTabPort: getTabPort,
      refreshTabPort: refreshTabPort
  });
})();

// highlighting draggable/droppable elements
var highlighting = (function() {

  var ports = Object.create(null);

  var highlightByRC = function(args) {
    args.type = 'highlight';
    Object.keys(ports).forEach(function(tabId) {
      ports[tabId].postMessage(args);
    });
  };

  var unhighlight = function() {
    Object.keys(ports).forEach(function(tabId) {
      ports[tabId].postMessage({ type: 'unhighlight' });
    });
  };

  var registerHighlighter = function(port) {
    var tabId = port.sender.tab.id;

    console.assert(!(tabId in ports)); // sanity check
    ports[tabId] = port;

    port.onDisconnect.addListener(function() {
      delete ports[tabId];
    });
  };

  return {
    registerHighlighter: registerHighlighter,
    highlightByRC: highlightByRC,
    unhighlight: unhighlight
  };

})();

chrome.extension.onConnect.addListener(function(port) {
  if (port.name === 'highlight') {
    highlighting.registerHighlighter(port);
  }
});
<commit_msg>Bug: outpost was sent twice.<commit_after>// Copyright 2011 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

var capServer = new CapServer(newUUIDv4());

var resolver = function(instID) {
  if (instID in instToTabID) {
    return launchedInstances[instToTabID[instID]].tunnel.sendInterface;
  }
  else if (instID === capServer.instanceID) {
    return capServer.publicInterface;
  }
  else {
    // TODO(arjun): this is terrible; Joe told me to do this
    return currentStation.tunnel.sendInterface;
  }
};

capServer.setResolver(resolver);

// { tabID : { callbackName : [ rcList, info ] }}
var offerMap = Object.create(null);
var acceptMap = Object.create(null);

//
// Station setup
//
var stations = (function() {
  var stations = localStorage.stations ?
    JSON.parse(localStorage.stations) : {};

  function set(name, stationURL) {
    stations[name] = stationURL;
    localStorage.stations = JSON.stringify(stations);
  }

  function get(name) {
    return stations[name];
  }

  function names() {
    return Object.keys(stations);
  }

  return Object.freeze({
    set: set,
    get: get,
    names: names,
  });
})();

var MAKESTATION = 'http://localhost:9001/belay/generate';

var makeStation = function(name, k) {
  capServer.restore(MAKESTATION).get(
    function(stationCap) {
      // TODO(mzero): should be defensive about stationCap being a Cap
      stations.set(name, stationCap.serialize());
      if (k) k();
    },
    function(_) {
      console.log('Failed to gen station');
    });
};

var launchStation = function(name) {
  launch(stations.get(name));
}

var instToTabID = Object.create(null);
/* Maps tabIDs to
  { instID: Str,
    outpostData: Any,    station-determined; background is transparent
    tunnel: CapTunnel,
    url: Str,
    station: ``Station''
   }
*/
var launchedInstances = Object.create(null);
var launchedStations = Object.create(null);

var makeSetNewInstHandler = function(station) {
  return capServer.grant(function(handler) {
    station.newInstHandler = handler;
  });
};

var makeCloseInstHandler = function(station) {
  return capServer.grant(function(handler) {
    station.closeInstHandler = handler;
  });
};

var makeCloseInstanceCap = function(instID) {
  return capServer.grant(function() {
    var tabID = instToTabID[instID];
    delete instToTabID[instID];
    delete launchedInstances[tabID];
    // TODO(arjun): assume success; closing caps should be revoked when the
    // instance is closed.
    chrome.tabs.remove(tabID);
  });
};

// Capability returned to station to launch instances in new windows.
var makeLaunchHandler = function(station) {
  return capServer.grant({
    post: function(args, sk, fk) {
      var width = typeof args.width === 'number' ? args.width : undefined;
      var height = typeof args.height === 'number' ? args.height: undefined;
      chrome.windows.create({ url: args.url,
                              width: width,
                              height: height },
                            function(wnd) {
        var tab = wnd.tabs[0];
        var tunnel = makeTunnel(tabPorts.getTabPort(tab.id));
        launchedInstances[tab.id] = { instID: args.instID,
                                      tunnel: tunnel,
                                      url: args.url,
                                      outpostData: args.outpostData,
                                      station: station };
        instToTabID[args.instID] = tab.id;
        sk(makeCloseInstanceCap(args.instID));
        // chrome.tabs.onUpdated fires on the next turn, sending the
        // outpost message.
      });
    }
  });
};

var instanceRequest = function(data, tabID) {
  var launchData = capServer.dataPostProcess(data);
  var station = currentStation;

  station.newInstHandler.post({
    launchData: launchData,
    relaunch: capServer.grant(function(args) {
      chrome.tabs.update(tabID, { url: args.url }, function(tab) {
        var tunnel = makeTunnel(tabPorts.refreshTabPort(tab.id));
        launchedInstances[tab.id] = { instID: args.instID,
                                      tunnel: tunnel,
                                      url: args.url,
                                      outpostData: args.outpostData,
                                      station: station };
        instToTabID[args.instID] = tab.id;
      });
      return makeCloseInstanceCap(args.instID);
    })
  });
};

var makeTunnel = function(port) {
  var tunnel = new CapTunnel(port);
  tunnel.setLocalResolver(resolver);
  return tunnel;
};

// opens a page, and sends info over
var launch = function(url) {
  capServer.restore(url).get(
    function(data) {
      var page = data.page;
      var info = data.info;
      chrome.tabs.create({ url: page.html },
        function(tab) {
          var tunnel = makeTunnel(tabPorts.getTabPort(tab.id));
          launchedStations[tab.id] = {
            url: url, html: page.html, info: info, tunnel: tunnel };
        });
    },
    function(_) {
      console.log('Launch failed.');
    });
};

// Also runs on first-load
var reloadInstance = function(tabID, info, tab) {
  var instance = launchedInstances[tabID];
  console.assert(instance);
  instance.tunnel.sendOutpost(capServer.dataPreProcess(instance.outpostData));
};

var currentStation = false;

var handleClosedStation = function(tabID) {
  var station = launchedStations[tabID];

  if (currentStation === station) {
    currentStation = false;
  }

  closeInstancesOfStation(station);
  delete launchedStations[tabID];
}

var closeInstancesOfStation = function(station) {
  Object.keys(launchedInstances).forEach(function(instTabID) {
    var instance = launchedInstances[instTabID];
    if (instance.station === station) {
      delete instToTabID[instance.instID];
      delete launchedInstances[instTabID];
      chrome.tabs.remove(Number(instTabID));
    }
  });
}

chrome.tabs.onRemoved.addListener(function (tabID, removeInfo) {
  if (tabID in launchedInstances) {
    var station = launchedInstances[tabID].station;
    var instID = launchedInstances[tabID].instID;
    delete instToTabID[instID];
    delete launchedInstances[tabID];
    station.closeInstHandler.put(instID);
  }
  else if (tabID in launchedStations) {
    handleClosedStation(tabID);
  }
});

// NOTE(jpolitz): This event is called twice on page load, and twice
// on page refresh.  We only handle 'complete' events, so we can be
// sure that the receiving tab is correctly set up.
chrome.tabs.onUpdated.addListener(function(tabID, info, tab) {
  if (info.status !== 'complete') return;
  var tabInfo = launchedStations[tabID];
  if (tabInfo === undefined) {
    if (tabID in launchedInstances) {
      reloadInstance(tabID, info, tab);
    }
    return;
  }

  // TODO(arjun): hack--currentStation is the last loaded station
  currentStation = tabInfo;

  closeInstancesOfStation(tabInfo);

  tabInfo.tunnel = makeTunnel(tabPorts.refreshTabPort(tabID));

  var sendToTunnel = function(info) {
    tabInfo.tunnel.sendOutpost(capServer.dataPreProcess({
      info: info,
      browserID: capServer.instanceID,
      services: {
        highlightByRC: capServer.grant(highlighting.highlightByRC),
        unhighlight: capServer.grant(highlighting.unhighlight)
      },
      launch: makeLaunchHandler(tabInfo),
      setNewInstHandler: makeSetNewInstHandler(tabInfo),
      setCloseInstHandler: makeCloseInstHandler(tabInfo)
    }));
  };

  if (tabInfo.info) {
    sendToTunnel(tabInfo.info);
    tabInfo.info = undefined;
  } else {
    capServer.restore(tabInfo.url).get(
      function(data) {
        if (data.page.html === info.html || info.url === undefined) {
          sendToTunnel(data.info);
        }
      });
  }
});

//
// Ports to pages
//

var tabPorts = (function() {
  var ports = Object.create(null);

  var getTabPort = function(tabID) {
    if (!(tabID in ports)) ports[tabID] = new PortQueue();
    return ports[tabID];
  };

  var refreshTabPort = function(tabID) {
    if (tabID in ports) delete ports[tabID];
    return getTabPort(tabID);
  };

  var makeRelayPort = function(tabID) {
    var extPort = {
      postMessage: function(message, ports) {
        if (ports && ports.length > 0) { throw 'TabPort: Can\'t send ports'; }
        chrome.tabs.sendRequest(tabID, message);
      },
      onmessage: function() { throw 'ExtPort: onmessage not set.'; }
    };
    return extPort;
  };

  chrome.extension.onRequest.addListener(
    function(message, sender, sendResponse) {
      var tabID = sender.tab.id;
      var port = getTabPort(tabID);
      if (message.type === 'init') {
        if (!port.hasPort()) port.setPort(makeRelayPort(tabID));
      }
      else if (message.type === 'instanceRequest') {
        instanceRequest(message.gen, tabID);
      }
      else port.onmessage({ data: message });
    });

  return Object.freeze({
      getTabPort: getTabPort,
      refreshTabPort: refreshTabPort
  });
})();

// highlighting draggable/droppable elements
var highlighting = (function() {

  var ports = Object.create(null);

  var highlightByRC = function(args) {
    args.type = 'highlight';
    Object.keys(ports).forEach(function(tabId) {
      ports[tabId].postMessage(args);
    });
  };

  var unhighlight = function() {
    Object.keys(ports).forEach(function(tabId) {
      ports[tabId].postMessage({ type: 'unhighlight' });
    });
  };

  var registerHighlighter = function(port) {
    var tabId = port.sender.tab.id;

    console.assert(!(tabId in ports)); // sanity check
    ports[tabId] = port;

    port.onDisconnect.addListener(function() {
      delete ports[tabId];
    });
  };

  return {
    registerHighlighter: registerHighlighter,
    highlightByRC: highlightByRC,
    unhighlight: unhighlight
  };

})();

chrome.extension.onConnect.addListener(function(port) {
  if (port.name === 'highlight') {
    highlighting.registerHighlighter(port);
  }
});
<|endoftext|>


<commit_before>'use strict';

(function() {

    angular
        .module('simpleArmoryApp')
        .factory('PlannerService', PlannerService);

    function PlannerService($http, $log, LoginService, $routeParams, $q) {
        //  cache results
        var parsedStepsObject;
        LoginService.onLogin(function() {
            parsedStepsObject = undefined;
        });

        return {
            getSteps: function(items) {
                if (parsedStepsObject) {
                    return $q.when(parsedStepsObject);
                }

                var profile;
                return LoginService.getProfile($routeParams)
                    .then(function(p) {
                        profile = p;
                        $log.log('Parsing planner.json...');
                        return $http.get('data/planner.json', { cache: true});
                    })
                    .then(function(data) {
                        parsedStepsObject = parseStepsObject(data.data.steps, items);
                        return parsedStepsObject;
                    });
            }
        };

        // gotta love recursion
        function parseStepsObject(steps, items) {
            var neededSteps = [];
            angular.forEach(steps, function(step) {
                if (step.steps) {
                    var neededChildSteps = parseStepsObject(step.steps, items);

                    // if we have child steps and we found ones that were needed, then we can
                    // go ahead and add ourself as a step and our children too
                    if (neededChildSteps.length > 0) {
                        neededSteps.push(step);
                        neededSteps = neededSteps.concat(neededChildSteps);
                        if (step.finalStep) {
                            neededSteps.push({'title':step.finalStep, 'hearth':true});
                        }
                    }
                }
                else if (!checkStepCompleted(step, items)) {
                    neededSteps.push(step);
                }
            });

            return neededSteps;
        }

        function checkStepCompleted(step, items) {
            var completed = true;
            var showAll = false; // used for debugging
            var neededBosses = [];

            // check to see if we've finished all the bosses
            if (step.bosses) {
                angular.forEach(step.bosses, function(boss) {
                    if (boss.isAlliance && items.isAlliance && items.lookup[boss.ID] === undefined) {
                        neededBosses.push(boss);
                        completed = false;
                    }
                    else if (boss.isHorde && !items.isAlliance && items.lookup[boss.ID] === undefined) {
                        neededBosses.push(boss);
                        completed = false;
                    }
                    else if ((boss.ID !== undefined && items.lookup[boss.ID] === undefined) || showAll) {
                        neededBosses.push(boss);
                        completed = false;
                    }
                });
            }

            // reset bosses array to the ones we need
            step.bosses = neededBosses;

            return completed;
        }
    }

})();<commit_msg>Fix in the planner logic where mounts were being added for horde mounts being added to alliance characters in certain circumstances.  Addressing: https://github.com/kevinclement/SimpleArmory/issues/282<commit_after>'use strict';

(function() {

    angular
        .module('simpleArmoryApp')
        .factory('PlannerService', PlannerService);

    function PlannerService($http, $log, LoginService, $routeParams, $q) {
        //  cache results
        var parsedStepsObject;
        LoginService.onLogin(function() {
            parsedStepsObject = undefined;
        });

        return {
            getSteps: function(items) {
                if (parsedStepsObject) {
                    return $q.when(parsedStepsObject);
                }

                var profile;
                return LoginService.getProfile($routeParams)
                    .then(function(p) {
                        profile = p;
                        $log.log('Parsing planner.json...');
                        return $http.get('data/planner.json', { cache: true});
                    })
                    .then(function(data) {
                        parsedStepsObject = parseStepsObject(data.data.steps, items);
                        return parsedStepsObject;
                    });
            }
        };

        // gotta love recursion
        function parseStepsObject(steps, items) {
            var neededSteps = [];
            angular.forEach(steps, function(step) {
                if (step.steps) {
                    var neededChildSteps = parseStepsObject(step.steps, items);

                    // if we have child steps and we found ones that were needed, then we can
                    // go ahead and add ourself as a step and our children too
                    if (neededChildSteps.length > 0) {
                        neededSteps.push(step);
                        neededSteps = neededSteps.concat(neededChildSteps);
                        if (step.finalStep) {
                            neededSteps.push({'title':step.finalStep, 'hearth':true});
                        }
                    }
                }
                else if (!checkStepCompleted(step, items)) {
                    neededSteps.push(step);
                }
            });

            return neededSteps;
        }

        function checkStepCompleted(step, items) {
            var completed = true;
            var showAll = false; // used for debugging
            var neededBosses = [];

            // check to see if we've finished all the bosses
            if (step.bosses) {
                angular.forEach(step.bosses, function(boss) {

                    var bossIsNeutral = !boss.isAlliance && !boss.isHorde;
                    var character = items; // aliasing for clarity
                    var characterNeedsBoss = function(id){ return !character.lookup[id]; };
                    var addBoss = function(boss) {
                            neededBosses.push(boss);
                            completed = false;
                        };

                    if (showAll) { addBoss(boss); return; }
                    if (boss.ID === undefined) { return; } // continue the loop, bad boss data
                    if (!characterNeedsBoss(boss.ID)) { return; }

                    if ( bossIsNeutral || (boss.isAlliance && character.isAlliance) || (boss.isHorde && character.isHorde)) {
                        addBoss(boss);
                        return;
                    }

                });
            }

            // reset bosses array to the ones we need
            step.bosses = neededBosses;

            return completed;
        }
    }

})();
<|endoftext|>


<commit_before>'use strict';

logViewerApp.controller('LogviewerCtrl', [
    '$location', '$window', '$document', '$rootScope', '$scope',
    '$timeout', '$resource', 'ThTextLogStepModel', 'ThJobDetailModel',
    'ThJobModel', 'thNotify', 'dateFilter', 'ThResultSetModel',
    'thDateFormat', 'thReftestStatus',
    function Logviewer(
        $location, $window, $document, $rootScope, $scope,
        $timeout, $resource, ThTextLogStepModel, ThJobDetailModel,
        ThJobModel, thNotify, dateFilter, ThResultSetModel,
        thDateFormat, thReftestStatus) {

        const query_string = $location.search();
        $scope.css = '';
        $rootScope.urlBasePath = $location.absUrl().split('logviewer')[0];
        $rootScope.logOffset = 7;

        if (query_string.repo !== "") {
            $rootScope.repoName = query_string.repo;
        }

        if (query_string.job_id !== "") {
            $scope.job_id= query_string.job_id;
        }

        $scope.loading = false;
        $scope.jobExists = true;
        $scope.showSuccessful = true;

        $scope.$watch('steps', () => {
            if (!$scope.steps) {
                return;
            }

            $scope.showSuccessful = !$scope.hasFailedSteps();
        });

        $scope.logPostMessage = (values) => {
            const { lineNumber, highlightStart } = values;

            if (lineNumber && !highlightStart) {
                values.highlightStart = lineNumber;
                values.highlightEnd = lineNumber;
            }

            updateQuery(values);

            // Add offset to lineNumber to see above the failure line
            if (lineNumber) {
                values.lineNumber -= $rootScope.logOffset;
            }

            $document[0].getElementById('logview').contentWindow.postMessage(values, "*");
        };

        $scope.hasFailedSteps = () => {
            const steps = $scope.steps;

            if (!steps) {
                return false;
            }

            for (let i = 0; i < steps.length; i++) {
                // We only recently generated step results as part of ingestion,
                // so we have to check the results property is present.
                // TODO: Remove this when the old data has expired, so long as
                // other data submitters also provide a step result.
                if ('result' in steps[i] && steps[i].result !== 'success' &&
                    steps[i].result !== 'skipped') {
                    return true;
                }
            }

            return false;
        };

        // Get the css class for the result, step buttons and other general use
        $scope.getShadingClass = (result) => {
            return "result-status-shading-" + result;
        };

        // @@@ it may be possible to do this with the angular date filter?
        $scope.formatTime = (startedStr, finishedStr) => {
            if (!startedStr || !finishedStr) {
                return '';
            }

            const sec = Math.abs(new Date(startedStr) - new Date(finishedStr)) / 1000.0;
            const h = Math.floor(sec / 3600);
            const m = Math.floor(sec % 3600 / 60);
            const s = Math.floor(sec % 3600 % 60);
            const secStng = sec.toString();
            const ms = secStng.substr(secStng.indexOf(".") + 1, 2);

            return ((h > 0 ? h + 'h ' : '') + (m > 0 ? m + 'm ' : '') +
                   (s > 0 ? s + 's ' : '') + (ms > 0 ? ms + 'ms ' : '00ms'));
        };

        $scope.displayTime = (started, finished) => {
            const start = started ? started.substr(started.indexOf(' ') + 1, 8) : '?';
            const end = finished ? finished.substr(finished.indexOf(' ') + 1, 8) : '?';

            return start + '-' + end;
        };

        $scope.init = () => {
            $scope.logProperties = [];

            ThJobModel.get_list($scope.repoName, {
                project_specific_id: $scope.job_id
            }).then(function(jobList) {
                if (jobList.length > 0) {
                    $scope.job_id = jobList[0]['id'];
                }
                ThJobModel.get($scope.repoName, $scope.job_id).then(job => {
                    // set the title of the browser window/tab
                    $scope.logViewerTitle = job.get_title();

                    if (job.logs && job.logs.length) {
                        $scope.rawLogURL = job.logs[0].url;
                    }

                    // set the result value and shading color class
                    $scope.result = {label: 'Result', value: job.result};
                    $scope.resultStatusShading = $scope.getShadingClass(job.result);

                    // other properties, in order of appearance
                    $scope.logProperties = [
                        {label: 'Job', value: $scope.logViewerTitle},
                        {label: 'Machine', value: job.machine_name},
                        {label: 'Start', value: dateFilter(job.start_timestamp * 1000, thDateFormat)},
                        {label: 'End', value: dateFilter(job.end_timestamp * 1000, thDateFormat)}
                    ];

                    // Test to disable successful steps checkbox on taskcluster jobs
                    $scope.isTaskClusterLog = (job.build_system_type === 'taskcluster');

                    // Test to expose the reftest button in the logviewer actionbar
                    $scope.isReftest = () => {
                        if (job.job_group_name) {
                            return thReftestStatus(job);
                        }
                    };

                    // get the revision and linkify it
                    ThResultSetModel.getResultSet($scope.repoName, job.result_set_id).then(data => {
                        const revision = data.data.revision;

                        $scope.logProperties.push({label: 'Revision', value: revision});
                    });

                    ThJobDetailModel.getJobDetails({job_guid: job.job_guid}).then(jobDetails => {
                        $scope.job_details = jobDetails;
                    });
                }, () => {
                    $scope.loading = false;
                    $scope.jobExists = false;
                    thNotify.send('The job does not exist or has expired', 'danger', true);
                });
            });
        };

        $scope.logviewerInit = () => {
            // Listen for messages from child frame
            setLogListener();

            ThTextLogStepModel.query({
                project: $rootScope.repoName,
                jobId: $scope.job_id
            }, textLogSteps => {
                let shouldPost = true;
                const allErrors = _.flatten(textLogSteps.map(s => s.errors));
                const q = $location.search();
                $scope.steps = textLogSteps;

                // add an ordering to each step
                textLogSteps.forEach((step, i) => {step.order = i;});

                // load the first failure step line else load the head
                if (allErrors.length) {
                    $scope.css = $scope.css + errorLinesCss(allErrors);

                    if (!q.lineNumber) {
                        $scope.logPostMessage({ lineNumber: allErrors[0].line_number + 1, customStyle: $scope.css });
                        shouldPost = false;
                    }
                } else if (!q.lineNumber) {
                    for (let i = 0; i < $scope.steps.length; i++) {
                        let step = $scope.steps[i];

                        if (step.result !== "success") {
                            $scope.logPostMessage({
                                lineNumber: step.started_line_number + 1,
                                highlightStart: step.started_line_number + 1,
                                highlightEnd: step.finished_line_number + 1,
                                customStyle: $scope.css
                            });

                            break;
                        }
                    }
                }

                if (shouldPost) {
                    $scope.logPostMessage({ customStyle: $scope.css });
                }
            });
        };

        $scope.setDisplayedStep = (step) => {
            const highlightStart = step.started_line_number + 1 || step.line_number + 1;
            const highlightEnd = step.finished_line_number + 1 || step.line_number + 1;
            $scope.displayedStep = step;

            $scope.logPostMessage({ lineNumber: highlightStart, highlightStart, highlightEnd });
        };

        function errorLinesCss(errors) {
            return errors
              .map(({ line_number }) => `a[id="${line_number + 1}"]+span`)
              .join(',')
              .concat('{background:#fbe3e3;color:#a94442}');
        }

        function logCss() {
            const hideToolbar = '#toolbar{display:none}';
            const body = 'html,body{background:#f8f8f8;color:#333;font-size:12px}';
            const highlight = '#log .highlight a,#log .highlight span{background:#f8eec7!important}';
            const hover = '#log .line:hover{background:transparent}#log .line a:hover,#log .highlight a:hover{background:#f8eec7;color:#000}';
            const stripe = '.lazy-list .line:nth-child(2n){background:#fff!important}.lazy-list .line:nth-child(2n+1){background:#f8f8f8!important}';
            const linePadding = '#log .line{padding:0 15px 0 35px}';
            const lineNumber = '#log .line a,#log .highlight a{color:rgba(0,0,0,.3)}';
            const font = '#log{font-family:monospace}';

            return hideToolbar + body + highlight + hover + stripe + lineNumber + linePadding + font;
        }

        /** utility functions **/

        function updateQuery(values) {
            const data = typeof values === 'string' ? JSON.parse(values) : values;
            const { lineNumber, highlightStart, highlightEnd } = data;

            if (highlightStart !== highlightEnd) {
                $location.search('lineNumber', `${highlightStart}-${highlightEnd}`).replace();
            }
            else if (highlightStart) {
                $location.search('lineNumber', highlightStart).replace();
            } else {
                $location.search('lineNumber', lineNumber).replace();
            }
        }

        function setLogListener() {
            let workerReady = false;

            $window.addEventListener('message', (e) => {
                // Send initial css when child frame loads URL successfully
                if (!workerReady) {
                    workerReady = true;

                    $scope.css = $scope.css + logCss();
                    $scope.logPostMessage({ customStyle: $scope.css });
                }

                $timeout(updateQuery(e.data));
            });
        }
    }
]);
<commit_msg>Bug 1360676 - Only add line highlight to unknown steps if the job itself is marked as a failure (#2435) r=camd<commit_after>'use strict';

logViewerApp.controller('LogviewerCtrl', [
    '$location', '$window', '$document', '$rootScope', '$scope',
    '$timeout', '$resource', 'ThTextLogStepModel', 'ThJobDetailModel',
    'ThJobModel', 'thNotify', 'dateFilter', 'ThResultSetModel',
    'thDateFormat', 'thReftestStatus',
    function Logviewer(
        $location, $window, $document, $rootScope, $scope,
        $timeout, $resource, ThTextLogStepModel, ThJobDetailModel,
        ThJobModel, thNotify, dateFilter, ThResultSetModel,
        thDateFormat, thReftestStatus) {

        const query_string = $location.search();
        $scope.css = '';
        $rootScope.urlBasePath = $location.absUrl().split('logviewer')[0];
        $rootScope.logOffset = 7;

        if (query_string.repo !== "") {
            $rootScope.repoName = query_string.repo;
        }

        if (query_string.job_id !== "") {
            $scope.job_id= query_string.job_id;
        }

        $scope.loading = false;
        $scope.jobExists = true;
        $scope.showSuccessful = true;

        $scope.$watch('steps', () => {
            if (!$scope.steps) {
                return;
            }

            $scope.showSuccessful = !$scope.hasFailedSteps();
        });

        $scope.logPostMessage = (values) => {
            const { lineNumber, highlightStart } = values;

            if (lineNumber && !highlightStart) {
                values.highlightStart = lineNumber;
                values.highlightEnd = lineNumber;
            }

            updateQuery(values);

            // Add offset to lineNumber to see above the failure line
            if (lineNumber) {
                values.lineNumber -= $rootScope.logOffset;
            }

            $document[0].getElementById('logview').contentWindow.postMessage(values, "*");
        };

        $scope.hasFailedSteps = () => {
            const steps = $scope.steps;

            if (!steps) {
                return false;
            }

            for (let i = 0; i < steps.length; i++) {
                // We only recently generated step results as part of ingestion,
                // so we have to check the results property is present.
                // TODO: Remove this when the old data has expired, so long as
                // other data submitters also provide a step result.
                if ('result' in steps[i] && steps[i].result !== 'success' &&
                    steps[i].result !== 'skipped') {
                    return true;
                }
            }

            return false;
        };

        // Get the css class for the result, step buttons and other general use
        $scope.getShadingClass = (result) => {
            return "result-status-shading-" + result;
        };

        // @@@ it may be possible to do this with the angular date filter?
        $scope.formatTime = (startedStr, finishedStr) => {
            if (!startedStr || !finishedStr) {
                return '';
            }

            const sec = Math.abs(new Date(startedStr) - new Date(finishedStr)) / 1000.0;
            const h = Math.floor(sec / 3600);
            const m = Math.floor(sec % 3600 / 60);
            const s = Math.floor(sec % 3600 % 60);
            const secStng = sec.toString();
            const ms = secStng.substr(secStng.indexOf(".") + 1, 2);

            return ((h > 0 ? h + 'h ' : '') + (m > 0 ? m + 'm ' : '') +
                   (s > 0 ? s + 's ' : '') + (ms > 0 ? ms + 'ms ' : '00ms'));
        };

        $scope.displayTime = (started, finished) => {
            const start = started ? started.substr(started.indexOf(' ') + 1, 8) : '?';
            const end = finished ? finished.substr(finished.indexOf(' ') + 1, 8) : '?';

            return start + '-' + end;
        };

        $scope.init = () => {
            $scope.logProperties = [];

            ThJobModel.get_list($scope.repoName, {
                project_specific_id: $scope.job_id
            }).then(function(jobList) {
                if (jobList.length > 0) {
                    $scope.job_id = jobList[0]['id'];
                }
                ThJobModel.get($scope.repoName, $scope.job_id).then(job => {
                    // set the title of the browser window/tab
                    $scope.logViewerTitle = job.get_title();

                    if (job.logs && job.logs.length) {
                        $scope.rawLogURL = job.logs[0].url;
                    }

                    // set the result value and shading color class
                    $scope.result = {label: 'Result', value: job.result};
                    $scope.resultStatusShading = $scope.getShadingClass(job.result);

                    // other properties, in order of appearance
                    $scope.logProperties = [
                        {label: 'Job', value: $scope.logViewerTitle},
                        {label: 'Machine', value: job.machine_name},
                        {label: 'Start', value: dateFilter(job.start_timestamp * 1000, thDateFormat)},
                        {label: 'End', value: dateFilter(job.end_timestamp * 1000, thDateFormat)}
                    ];

                    // Test to disable successful steps checkbox on taskcluster jobs
                    $scope.isTaskClusterLog = (job.build_system_type === 'taskcluster');

                    // Test to expose the reftest button in the logviewer actionbar
                    $scope.isReftest = () => {
                        if (job.job_group_name) {
                            return thReftestStatus(job);
                        }
                    };

                    // get the revision and linkify it
                    ThResultSetModel.getResultSet($scope.repoName, job.result_set_id).then(data => {
                        const revision = data.data.revision;

                        $scope.logProperties.push({label: 'Revision', value: revision});
                    });

                    ThJobDetailModel.getJobDetails({job_guid: job.job_guid}).then(jobDetails => {
                        $scope.job_details = jobDetails;
                    });
                }, () => {
                    $scope.loading = false;
                    $scope.jobExists = false;
                    thNotify.send('The job does not exist or has expired', 'danger', true);
                });
            });
        };

        $scope.logviewerInit = () => {
            // Listen for messages from child frame
            setLogListener();

            ThTextLogStepModel.query({
                project: $rootScope.repoName,
                jobId: $scope.job_id
            }, textLogSteps => {
                let shouldPost = true;
                const allErrors = _.flatten(textLogSteps.map(s => s.errors));
                const q = $location.search();
                $scope.steps = textLogSteps;

                // add an ordering to each step
                textLogSteps.forEach((step, i) => {step.order = i;});

                // load the first failure step line else load the head
                if (allErrors.length) {
                    $scope.css = $scope.css + errorLinesCss(allErrors);

                    if (!q.lineNumber) {
                        $scope.logPostMessage({ lineNumber: allErrors[0].line_number + 1, customStyle: $scope.css });
                        shouldPost = false;
                    }
                } else if (!q.lineNumber) {
                    for (let i = 0; i < $scope.steps.length; i++) {
                        let step = $scope.steps[i];

                        if (step.result !== "success" && ($scope.result &&
                            $scope.result.value !== "success" && step.result !== "unknown")) {
                            $scope.logPostMessage({
                                lineNumber: step.started_line_number + 1,
                                highlightStart: step.started_line_number + 1,
                                highlightEnd: step.finished_line_number + 1,
                                customStyle: $scope.css
                            });

                            break;
                        }
                    }
                }

                if (shouldPost) {
                    $scope.logPostMessage({ customStyle: $scope.css });
                }
            });
        };

        $scope.setDisplayedStep = (step) => {
            const highlightStart = step.started_line_number + 1 || step.line_number + 1;
            const highlightEnd = step.finished_line_number + 1 || step.line_number + 1;
            $scope.displayedStep = step;

            $scope.logPostMessage({ lineNumber: highlightStart, highlightStart, highlightEnd });
        };

        function errorLinesCss(errors) {
            return errors
              .map(({ line_number }) => `a[id="${line_number + 1}"]+span`)
              .join(',')
              .concat('{background:#fbe3e3;color:#a94442}');
        }

        function logCss() {
            const hideToolbar = '#toolbar{display:none}';
            const body = 'html,body{background:#f8f8f8;color:#333;font-size:12px}';
            const highlight = '#log .highlight a,#log .highlight span{background:#f8eec7!important}';
            const hover = '#log .line:hover{background:transparent}#log .line a:hover,#log .highlight a:hover{background:#f8eec7;color:#000}';
            const stripe = '.lazy-list .line:nth-child(2n){background:#fff!important}.lazy-list .line:nth-child(2n+1){background:#f8f8f8!important}';
            const linePadding = '#log .line{padding:0 15px 0 35px}';
            const lineNumber = '#log .line a,#log .highlight a{color:rgba(0,0,0,.3)}';
            const font = '#log{font-family:monospace}';

            return hideToolbar + body + highlight + hover + stripe + lineNumber + linePadding + font;
        }

        /** utility functions **/

        function updateQuery(values) {
            const data = typeof values === 'string' ? JSON.parse(values) : values;
            const { lineNumber, highlightStart, highlightEnd } = data;

            if (highlightStart !== highlightEnd) {
                $location.search('lineNumber', `${highlightStart}-${highlightEnd}`).replace();
            }
            else if (highlightStart) {
                $location.search('lineNumber', highlightStart).replace();
            } else {
                $location.search('lineNumber', lineNumber).replace();
            }
        }

        function setLogListener() {
            let workerReady = false;

            $window.addEventListener('message', (e) => {
                // Send initial css when child frame loads URL successfully
                if (!workerReady) {
                    workerReady = true;

                    $scope.css = $scope.css + logCss();
                    $scope.logPostMessage({ customStyle: $scope.css });
                }

                $timeout(updateQuery(e.data));
            });
        }
    }
]);
<|endoftext|>


<commit_before>// Content marker for forum topic lists, blog entries and so on
//
// Redis keys:
//
// - `marker_cut:<user_id>:<section_id>` (key) - contain timestamp of read cut
// - `marker_marks:<user_id>` (zset) - contain `_id` of read content and timestamp as index
// - `marker_pos:<user_id>` (hash) - content postinion
//   - <content_id> (JSON)
//     - `current`
//     - `max` - last read
//     - `ts` - last update
// - `marker_pos_updates` (zset) - last update info for `marker_pos:*`
// - `marker_cut_updates` (zset) - last update info for `marker_cut:*`
// - `marker_marks_items` (set) - items list for `marker_marks:*`
//
'use strict';

const Promise = require('bluebird');
const _       = require('lodash');

module.exports = function (N, collectionName) {

  let gcHandlers = {};

  function Marker() {
  }

  // Recalculate category cut
  //
  // - type (String) - content type
  // - userId (ObjectId)
  // - categoryId (ObjectId)
  // - currentCut (Number)
  //
  Marker.gc = Promise.coroutine(function* (type, userId, categoryId, currentCut) {
    if (!gcHandlers[type]) return;

    let contentInfo = yield gcHandlers[type](userId, categoryId, currentCut);

    if (!contentInfo.length) return;

    contentInfo = _.sortBy(contentInfo, 'lastPostTs');

    let marks = yield Marker.info(userId, contentInfo);

    let updatedCut = currentCut;
    let mark;

    for (let i = 0; i < contentInfo.length; i++) {
      mark = marks[contentInfo[i].contentId];

      if (mark.isNew || mark.next !== -1) break;

      updatedCut = +contentInfo[i].lastPostTs;
    }

    if (updatedCut !== currentCut) {
      yield Marker.markAll(userId, categoryId, updatedCut);
    }
  });

  // Add handler to load content data for `.gc()`
  //
  // - type (String) - content type
  // - handler (Function) - `function (userId, categoryId, currentCut, callback)`
  //   - userId (ObjectId)
  //   - categoryId (ObjectId)
  //   - currentCut (Number)
  //   - callback (Function) - `function (err, contentInfo)`
  //     - err
  //     - contentInfo ([Object])
  //       - contentId (ObjectId)
  //       - categoryId (ObjectId)
  //       - lastPostNumber (Number) - last post number in thread (post hid)
  //       - lastPostTs (Number)
  //
  Marker.registerGc = function (type, handler) {
    gcHandlers[type] = handler;
  };

  // Mark content as read
  //
  // - userId (ObjectId)
  // - contentId (ObjectId)
  // - categoryId (ObjectId)
  // - type (String) - content type
  //
  Marker.mark = Promise.coroutine(function* (userId, contentId, categoryId, type) {
    if (!userId || String(userId) === '000000000000000000000000') {
      return;
    }

    let res = yield Marker.cuts(userId, [ categoryId ]);

    // Don't mark old content
    if (contentId.getTimestamp() < res[categoryId]) return;

    yield Promise.all([
      N.redis.saddAsync('marker_marks_items', String(userId)),
      N.redis.zaddAsync('marker_marks:' + userId, +contentId.getTimestamp(), String(contentId))
    ]);
    yield Marker.gc(type, userId, categoryId, res[categoryId]);
  });

  // Mark all topics before now as read
  //
  // - userId (ObjectId)
  // - categoryId (ObjectId)
  // - ts (Number) - optional, cut off timestamp, `Date.now()` by default
  //
  Marker.markAll = Promise.coroutine(function* (userId, categoryId, ts) {
    if (!userId || String(userId) === '000000000000000000000000') {
      return;
    }

    let now = Date.now();

    if (!ts) {
      ts = now;
    }

    // If `ts` bigger than now plus one hour or more - stop here
    if (ts > 1000 * 60 * 60 + Date.now()) return;

    yield N.redis.zaddAsync('marker_cut_updates', now, userId + ':' + categoryId);
    yield N.redis.setAsync('marker_cut:' + userId + ':' + categoryId, ts);
  });

  // Remove extra position markers if user have more than limit
  //
  const limitPositionMarkers = Promise.coroutine(function* (userId) {
    let maxItems = 1000;
    let gcThreshold = maxItems + Math.round(maxItems * 0.10) + 1;

    // Get position records count
    let cnt = yield N.redis.hlenAsync('marker_pos:' + userId);

    // If count less than limit - skip
    if (cnt <= gcThreshold) return Promise.resolve();

    let items = yield N.redis.hgetallAsync('marker_pos:' + userId);
    let query = N.redis.multi();

    _(items)
      .mapValues((json, id) => {
        let result = { ts: -1 };

        if (json) {
          try {
            result = JSON.parse(json);
          } catch (__) {}
        }

        result.id = id;

        return result;
      })
      .sortBy('ts')
      .take(_.values(items).length - maxItems)
      .forEach(item => {
        query.hdel('marker_pos:' + userId, item.id);
        query.zrem('marker_pos_updates', userId + ':' + item.id);
      });

    return query.execAsync();
  });

  // Set current scroll position in topic
  //
  // - userId (ObjectId)
  // - contentId (ObjectId)
  // - position (Number) - post number in thread (post hid)
  // - max (Number) - last read post in thread
  // - categoryId (ObjectId)
  // - type (String) - content type
  //
  Marker.setPos = Promise.coroutine(function* (userId, contentId, position, max, categoryId, type) {
    if (!userId || String(userId) === '000000000000000000000000') {
      return;
    }

    let now = Date.now();
    let posJson = yield N.redis.hgetAsync('marker_pos:' + userId, String(contentId));
    let pos;
    let maxUpdated = false;

    if (posJson) {
      try {
        pos = JSON.parse(posJson);
      } catch (__) {}
    }

    pos = pos || { max: 1, current: position, ts: +now };

    pos.current = position;
    pos.ts = +now;

    if (pos.max < max) {
      pos.max = max;
      maxUpdated = true;
    }

    yield N.redis.zaddAsync('marker_pos_updates', now, userId + ':' + contentId);
    yield N.redis.hsetAsync('marker_pos:' + userId, String(contentId), JSON.stringify(pos));
    yield limitPositionMarkers(userId);

    if (!maxUpdated) return;

    let res = yield Marker.cuts(userId, [ categoryId ]);

    yield Marker.gc(type, userId, categoryId, res[categoryId]);
  });

  // Get cuts ts for categories
  //
  // - userId (ObjectId)
  // - categoriesIds ([ObjectId])
  //
  // returns (Hash) - key is `categoryId` value is number
  //
  Marker.cuts = Promise.coroutine(function* (userId, categoriesIds) {
    if (categoriesIds.length === 0) {
      return [];
    }

    if (!userId || String(userId) === '000000000000000000000000') {
      let now = Date.now();

      return categoriesIds.reduce((acc, id) => {
        acc[String(id)] = now;
        return acc;
      }, {});
    }

    let content_read_marks_expire = yield N.settings.get('content_read_marks_expire');

    let defaultCut = Date.now() - (content_read_marks_expire * 24 * 60 * 60 * 1000);
    let result = categoriesIds.reduce((acc, id) => {
      acc[String(id)] = defaultCut;
      return acc;
    }, {});

    let cutKeys = Object.keys(result).map(id => 'marker_cut:' + userId + ':' + id);

    let res = yield N.redis.mgetAsync(cutKeys);

    Object.keys(result).forEach((id, i) => {
      result[id] = +res[i] || defaultCut;
    });

    return result;
  });

  // Build content info
  //
  // - userId (ObjectId)
  // - contentInfo ([Object])
  //   - categoryId (ObjectId)
  //   - contentId (ObjectId)
  //   - lastPostNumber (Number) - last post number in thread (post hid)
  //   - lastPostTs (Number)
  //
  // returns (Hash) - key is `contentId` value is object
  //
  // - isNew (Boolean) - is topic already opened by user (or older than 30 days)
  // - next (Number) - hid of first unread post in topic or `-1` if not set
  // - position (Number) - last read post position or `-1` if not set
  //
  Marker.info = Promise.coroutine(function* (userId, contentInfo) {
    let result = {};

    contentInfo.forEach(item => {
      result[item.contentId] = { isNew: false, next: -1, position: -1 };
    });

    if (!userId || String(userId) === '000000000000000000000000' || contentInfo.length === 0) {
      return result;
    }

    // Fetch cuts
    let cuts = yield Marker.cuts(userId, _.map(contentInfo, 'categoryId'));

    // Set `isNew` flag by cut
    contentInfo.forEach(item => {
      if (item.contentId.getTimestamp() > cuts[item.categoryId]) {
        result[item.contentId].isNew = true;
      }
    });

    // Unset `isNew` flag by markers
    //
    let newCandidates = [];
    let query = N.redis.multi();

    _.forEach(result, (v, id) => {
      if (v.isNew) {
        query.zscore('marker_marks:' + userId, id);
        newCandidates.push(id);
      }
    });

    let res = yield query.execAsync();

    _.forEach(newCandidates, (id, n) => {
      if (res[n] !== null) {
        result[id].isNew = false;
      }
    });

    // Fill position info
    //
    let contentIds = _.keys(result);
    let max;

    query = N.redis.multi();

    contentIds.forEach(id => query.hget('marker_pos:' + userId, id));

    let posInfo = yield query.execAsync();

    posInfo = posInfo.map(json => {
      let result;

      if (json) {
        try {
          result = JSON.parse(json);
        } catch (__) {}
      }

      return result;
    });

    _.forEach(contentInfo, item => {
      max = (posInfo[contentIds.indexOf(String(item.contentId))] || {}).max || -1;
      result[item.contentId].position = (posInfo[contentIds.indexOf(String(item.contentId))] || {}).current || -1;

      if (max === -1 || item.lastPostTs < cuts[item.categoryId]) {
        result[item.contentId].next = -1;
      } else if (item.lastPostNumber > max) {
        result[item.contentId].next = +max + 1;
      }
    });

    return result;
  });

  // Cleanup deprecated markers (older than 30 days)
  //
  // - callback (Function) - `function (err)`
  //
  Marker.cleanup = Promise.coroutine(function* () {
    let content_read_marks_expire = yield N.settings.get('content_read_marks_expire');
    let lastTs = Date.now() - (content_read_marks_expire * 24 * 60 * 60 * 1000);

    // Cleanup position markers
    //
    N.redis.zrangebyscoreAsync('marker_pos_updates', '-inf', lastTs).then(items => {
      let query = N.redis.multi();

      items.forEach(item => {
        let parts = item.split(':');

        query.hdel('marker_pos:' + parts[0], parts[1]);
        query.zrem('marker_pos_updates', item);
      });

      return query.execAsync();
    });

    // Cleanup cut markers
    //
    N.redis.zrangebyscoreAsync('marker_cut_updates', '-inf', lastTs).then(items => {
      let query = N.redis.multi();

      items.forEach(item => {
        query.del('marker_cut:' + item);
        query.zrem('marker_cut_updates', item);
      });

      return query.execAsync();
    });

    // Cleanup read markers
    //
    N.redis.smembersAsync('marker_marks_items').then(items => {
      let query = N.redis.multi();

      items.forEach(function (item) {
        query.zremrangebyscore('marker_marks:' + item, '-inf', lastTs);
        query.zcard('marker_marks:' + item);
      });

      return query.execAsync().then(res => {
        let query = N.redis.multi();

        items.forEach((item, i) => {
          if (res[i * 2 + 1] === 0) {
            query.srem('marker_marks_items', item);
          }
        });

        return query.execAsync();
      });
    });
  });

  N.wire.on('init:models', function emit_init_Marker(__, callback) {
    N.wire.emit('init:models.' + collectionName, Marker, callback);
  });

  N.wire.on('init:models.' + collectionName, function init_model_Marker() {
    N.models[collectionName] = Marker;
  });
};
<commit_msg>fixed `Marker` doc<commit_after>// Content marker for forum topic lists, blog entries and so on
//
// Redis data:
//
// - `marker_marks:<user_id>`            (zset) - contain `_id` of read content and timestamp as score
// - `marker_marks_items`                (set)  - `user_id` list, used to search all `marker_marks:*` sets
// - `marker_pos:<user_id>`              (hash) - content position
//   - <content_id> (JSON)
//     - `current`
//     - `max` - last read
//     - `ts` - last update
// - `marker_pos_updates`                (zset) - last update info for `marker_pos:*`
// - `marker_cut:<user_id>:<section_id>` (key)  - contain timestamp of read cut
// - `marker_cut_updates`                (zset) - last update info for `marker_cut:*`
//
// GC logic:
//
// - remove old `marker_pos:*` using `marker_pos_updates`
// - remove old `marker_cut:*` using `marker_cut_updates`
// - for each user (stored in `marker_marks_items`) remove old `marker_marks:<user_id>`
//
'use strict';

const Promise = require('bluebird');
const _       = require('lodash');

module.exports = function (N, collectionName) {

  let gcHandlers = {};

  function Marker() {
  }

  // Recalculate category cut
  //
  // - type (String) - content type
  // - userId (ObjectId)
  // - categoryId (ObjectId)
  // - currentCut (Number)
  //
  Marker.gc = Promise.coroutine(function* (type, userId, categoryId, currentCut) {
    if (!gcHandlers[type]) return;

    let contentInfo = yield gcHandlers[type](userId, categoryId, currentCut);

    if (!contentInfo.length) return;

    contentInfo = _.sortBy(contentInfo, 'lastPostTs');

    let marks = yield Marker.info(userId, contentInfo);

    let updatedCut = currentCut;
    let mark;

    for (let i = 0; i < contentInfo.length; i++) {
      mark = marks[contentInfo[i].contentId];

      if (mark.isNew || mark.next !== -1) break;

      updatedCut = +contentInfo[i].lastPostTs;
    }

    if (updatedCut !== currentCut) {
      yield Marker.markAll(userId, categoryId, updatedCut);
    }
  });

  // Add handler to load content data for `.gc()`
  //
  // - type (String) - content type
  // - handler (Function) - `function (userId, categoryId, currentCut, callback)`
  //   - userId (ObjectId)
  //   - categoryId (ObjectId)
  //   - currentCut (Number)
  //   - callback (Function) - `function (err, contentInfo)`
  //     - err
  //     - contentInfo ([Object])
  //       - contentId (ObjectId)
  //       - categoryId (ObjectId)
  //       - lastPostNumber (Number) - last post number in thread (post hid)
  //       - lastPostTs (Number)
  //
  Marker.registerGc = function (type, handler) {
    gcHandlers[type] = handler;
  };

  // Mark content as read
  //
  // - userId (ObjectId)
  // - contentId (ObjectId)
  // - categoryId (ObjectId)
  // - type (String) - content type
  //
  Marker.mark = Promise.coroutine(function* (userId, contentId, categoryId, type) {
    if (!userId || String(userId) === '000000000000000000000000') {
      return;
    }

    let res = yield Marker.cuts(userId, [ categoryId ]);

    // Don't mark old content
    if (contentId.getTimestamp() < res[categoryId]) return;

    yield Promise.all([
      N.redis.saddAsync('marker_marks_items', String(userId)),
      N.redis.zaddAsync('marker_marks:' + userId, +contentId.getTimestamp(), String(contentId))
    ]);
    yield Marker.gc(type, userId, categoryId, res[categoryId]);
  });

  // Mark all topics before now as read
  //
  // - userId (ObjectId)
  // - categoryId (ObjectId)
  // - ts (Number) - optional, cut off timestamp, `Date.now()` by default
  //
  Marker.markAll = Promise.coroutine(function* (userId, categoryId, ts) {
    if (!userId || String(userId) === '000000000000000000000000') {
      return;
    }

    let now = Date.now();

    if (!ts) {
      ts = now;
    }

    // If `ts` bigger than now plus one hour or more - stop here
    if (ts > 1000 * 60 * 60 + Date.now()) return;

    yield N.redis.zaddAsync('marker_cut_updates', now, userId + ':' + categoryId);
    yield N.redis.setAsync('marker_cut:' + userId + ':' + categoryId, ts);
  });

  // Remove extra position markers if user have more than limit
  //
  const limitPositionMarkers = Promise.coroutine(function* (userId) {
    let maxItems = 1000;
    let gcThreshold = maxItems + Math.round(maxItems * 0.10) + 1;

    // Get position records count
    let cnt = yield N.redis.hlenAsync('marker_pos:' + userId);

    // If count less than limit - skip
    if (cnt <= gcThreshold) return Promise.resolve();

    let items = yield N.redis.hgetallAsync('marker_pos:' + userId);
    let query = N.redis.multi();

    _(items)
      .mapValues((json, id) => {
        let result = { ts: -1 };

        if (json) {
          try {
            result = JSON.parse(json);
          } catch (__) {}
        }

        result.id = id;

        return result;
      })
      .sortBy('ts')
      .take(_.values(items).length - maxItems)
      .forEach(item => {
        query.hdel('marker_pos:' + userId, item.id);
        query.zrem('marker_pos_updates', userId + ':' + item.id);
      });

    return query.execAsync();
  });

  // Set current scroll position in topic
  //
  // - userId (ObjectId)
  // - contentId (ObjectId)
  // - position (Number) - post number in thread (post hid)
  // - max (Number) - last read post in thread
  // - categoryId (ObjectId)
  // - type (String) - content type
  //
  Marker.setPos = Promise.coroutine(function* (userId, contentId, position, max, categoryId, type) {
    if (!userId || String(userId) === '000000000000000000000000') {
      return;
    }

    let now = Date.now();
    let posJson = yield N.redis.hgetAsync('marker_pos:' + userId, String(contentId));
    let pos;
    let maxUpdated = false;

    if (posJson) {
      try {
        pos = JSON.parse(posJson);
      } catch (__) {}
    }

    pos = pos || { max: 1, current: position, ts: +now };

    pos.current = position;
    pos.ts = +now;

    if (pos.max < max) {
      pos.max = max;
      maxUpdated = true;
    }

    yield N.redis.zaddAsync('marker_pos_updates', now, userId + ':' + contentId);
    yield N.redis.hsetAsync('marker_pos:' + userId, String(contentId), JSON.stringify(pos));
    yield limitPositionMarkers(userId);

    if (!maxUpdated) return;

    let res = yield Marker.cuts(userId, [ categoryId ]);

    yield Marker.gc(type, userId, categoryId, res[categoryId]);
  });

  // Get cuts ts for categories
  //
  // - userId (ObjectId)
  // - categoriesIds ([ObjectId])
  //
  // returns (Hash) - key is `categoryId` value is number
  //
  Marker.cuts = Promise.coroutine(function* (userId, categoriesIds) {
    if (categoriesIds.length === 0) {
      return [];
    }

    if (!userId || String(userId) === '000000000000000000000000') {
      let now = Date.now();

      return categoriesIds.reduce((acc, id) => {
        acc[String(id)] = now;
        return acc;
      }, {});
    }

    let content_read_marks_expire = yield N.settings.get('content_read_marks_expire');

    let defaultCut = Date.now() - (content_read_marks_expire * 24 * 60 * 60 * 1000);
    let result = categoriesIds.reduce((acc, id) => {
      acc[String(id)] = defaultCut;
      return acc;
    }, {});

    let cutKeys = Object.keys(result).map(id => 'marker_cut:' + userId + ':' + id);

    let res = yield N.redis.mgetAsync(cutKeys);

    Object.keys(result).forEach((id, i) => {
      result[id] = +res[i] || defaultCut;
    });

    return result;
  });

  // Build content info
  //
  // - userId (ObjectId)
  // - contentInfo ([Object])
  //   - categoryId (ObjectId)
  //   - contentId (ObjectId)
  //   - lastPostNumber (Number) - last post number in thread (post hid)
  //   - lastPostTs (Number)
  //
  // returns (Hash) - key is `contentId` value is object
  //
  // - isNew (Boolean) - is topic already opened by user (or older than 30 days)
  // - next (Number) - hid of first unread post in topic or `-1` if not set
  // - position (Number) - last read post position or `-1` if not set
  //
  Marker.info = Promise.coroutine(function* (userId, contentInfo) {
    let result = {};

    contentInfo.forEach(item => {
      result[item.contentId] = { isNew: false, next: -1, position: -1 };
    });

    if (!userId || String(userId) === '000000000000000000000000' || contentInfo.length === 0) {
      return result;
    }

    // Fetch cuts
    let cuts = yield Marker.cuts(userId, _.map(contentInfo, 'categoryId'));

    // Set `isNew` flag by cut
    contentInfo.forEach(item => {
      if (item.contentId.getTimestamp() > cuts[item.categoryId]) {
        result[item.contentId].isNew = true;
      }
    });

    // Unset `isNew` flag by markers
    //
    let newCandidates = [];
    let query = N.redis.multi();

    _.forEach(result, (v, id) => {
      if (v.isNew) {
        query.zscore('marker_marks:' + userId, id);
        newCandidates.push(id);
      }
    });

    let res = yield query.execAsync();

    _.forEach(newCandidates, (id, n) => {
      if (res[n] !== null) {
        result[id].isNew = false;
      }
    });

    // Fill position info
    //
    let contentIds = _.keys(result);
    let max;

    query = N.redis.multi();

    contentIds.forEach(id => query.hget('marker_pos:' + userId, id));

    let posInfo = yield query.execAsync();

    posInfo = posInfo.map(json => {
      let result;

      if (json) {
        try {
          result = JSON.parse(json);
        } catch (__) {}
      }

      return result;
    });

    _.forEach(contentInfo, item => {
      max = (posInfo[contentIds.indexOf(String(item.contentId))] || {}).max || -1;
      result[item.contentId].position = (posInfo[contentIds.indexOf(String(item.contentId))] || {}).current || -1;

      if (max === -1 || item.lastPostTs < cuts[item.categoryId]) {
        result[item.contentId].next = -1;
      } else if (item.lastPostNumber > max) {
        result[item.contentId].next = +max + 1;
      }
    });

    return result;
  });

  // Cleanup deprecated markers (older than 30 days):
  //
  // - remove old `marker_pos:*` using `marker_pos_updates`
  // - remove old `marker_cut:*` using `marker_cut_updates`
  // - for each user (stored in `marker_marks_items`) remove old `marker_marks:<user_id>`
  //
  Marker.cleanup = Promise.coroutine(function* () {
    let content_read_marks_expire = yield N.settings.get('content_read_marks_expire');
    let lastTs = Date.now() - (content_read_marks_expire * 24 * 60 * 60 * 1000);

    // Cleanup position markers
    //
    let posItems = yield N.redis.zrangebyscoreAsync('marker_pos_updates', '-inf', lastTs);
    let posQuery = N.redis.multi();

    posItems.forEach(item => {
      let parts = item.split(':');

      posQuery.hdel(`marker_pos:${parts[0]}`, parts[1]);
      posQuery.zrem('marker_pos_updates', item);
    });

    yield posQuery.execAsync();

    // Cleanup cut markers
    //
    let cutItems = yield N.redis.zrangebyscoreAsync('marker_cut_updates', '-inf', lastTs);
    let cutQuery = N.redis.multi();

    cutItems.forEach(item => {
      cutQuery.del('marker_cut:' + item);
      cutQuery.zrem('marker_cut_updates', item);
    });

    yield cutQuery.execAsync();

    // Cleanup read markers
    //
    // TODO: cut by script if number of active users per month became too big
    //
    let marksItems = yield N.redis.smembersAsync('marker_marks_items');
    let marksQuery = N.redis.multi();

    marksItems.forEach(item => {
      // Drop elements and count the rest
      marksQuery.zremrangebyscore('marker_marks:' + item, '-inf', lastTs);
      marksQuery.zcard('marker_marks:' + item);
    });

    let res = yield marksQuery.execAsync();
    let query = N.redis.multi();

    marksItems.forEach((item, i) => {
      if (res[i * 2 + 1] === 0) { // zcard result
        query.srem('marker_marks_items', item);
      }
    });

    yield query.execAsync();
  });

  N.wire.on('init:models', function emit_init_Marker(__, callback) {
    N.wire.emit('init:models.' + collectionName, Marker, callback);
  });

  N.wire.on('init:models.' + collectionName, function init_model_Marker() {
    N.models[collectionName] = Marker;
  });
};
<|endoftext|>


<commit_before>import {
  select
} from 'd3-selection'

import {
  scaleTime,
  scaleLinear,
  scaleLog
} from 'd3-scale'

import {
  axisBottom,
  axisLeft
} from 'd3-axis'

import {
  timeMinute
} from 'd3-time'

import {
  timeFormat
} from 'd3-time-format'

import {
  color
} from 'd3-color'

import {
  max
} from 'd3-array'

import {
  drag
} from 'd3-drag'

import 'd3-selection-multi'

async function heatmap(el) {
  let height = 395
  let margin = {
    left: 1,
    top: 1,
    right: 1
  }

  let offset = {
    left: 60,
    bottom: 20
  }

  let $el = select(el)
  let width = $el.node().offsetWidth - margin.left - margin.right - offset.left - 1

  let url = new URL(el.dataset.source)
  url.searchParams.append('tbs', Math.floor(width / 8) + 1)

  let response = await fetch(url),
    json = await response.json(),
    data = json['values']

  let bucketWidth = width / json['time']['size']
  let bucketHeight = height / json['latency']['size']

  let scaleX = scaleTime()
    .range([0, width])
    .domain(json['time']['range'].map((x) => Date.parse(x)))
  let scaleY = scaleLinear()
    .range([height, 0])
    .domain(json['latency']['range'].map((x) => x / 1000000))
  let scaleZ = scaleLog()
    .range(['#CFE9FF', '#0275D8'])
    .domain([1, max(data, (x) => x.v)])

  let svg = $el.append('svg')
      .attr('width', $el.node().offsetWidth)
      .attr('height', height + offset.bottom)
    .append('g')
      .attr('transform', `translate(${margin.left + offset.left}, ${margin.top})`)

  let axisX = axisBottom(scaleX)
    .tickFormat(timeFormat('%H:%M'))
    .ticks(timeMinute.every(10))
  let axisY = axisLeft(scaleY)
    .tickFormat((x) => `${x} ms`)
    .ticks(3)

  svg.append('g')
    .attr('transform', `translate(0,${height})`)
    .call(axisX)

  svg.append('g')
    .attr('transform', 'translate(0, 0)')
    .call(axisY)

  let heatmap = svg
    .append('g')
    .classed('hm-data', true)

  let group = undefined
  let lastY = 0

  for(let {x, y, v} of data) {
    if(!group || y != lastY) {
      group = heatmap.append('g')
      group.classed(`hm-row-${y}`, true)
      lastY = y
    }

    group
      .append('rect')
      .attrs({
        x: Math.round(1 + x * bucketWidth),
        y: height - bucketHeight - y * bucketHeight,
        width: Math.round(bucketWidth),
        height: bucketHeight,
        fill: scaleZ(v),
        title: v,
      })
  }

  let hm_back = heatmap
    .append('rect')
    .attr('width', width)
    .attr('height', height)
    .attr('fill', 'transparent')

  function dragstart(e) {
    console.log('start', e)
  }

  function dragging(e) {
    console.log('drag', e)
  }

  function dragstop(e) {
    console.log('end', e)
  }

  hm_back.call(drag()
    .on('start', dragstart)
    .on('drag', dragging)
    .on('end', dragstop));
}

function initialize() {
  document.querySelectorAll('#heatmap').forEach((el) => heatmap(el))
}

if (document.readyState === "complete" ||
    document.readyState === "loaded" ||
    document.readyState === "interactive") {
  initialize()
} else {
  document.addEventListener('DOMContentLoaded', initialize)
}
<commit_msg>Do not force axis tick count for heatmap<commit_after>import {
  select
} from 'd3-selection'

import {
  scaleTime,
  scaleLinear,
  scaleLog
} from 'd3-scale'

import {
  axisBottom,
  axisLeft
} from 'd3-axis'

import {
  timeMinute
} from 'd3-time'

import {
  timeFormat
} from 'd3-time-format'

import {
  color
} from 'd3-color'

import {
  max
} from 'd3-array'

import {
  drag
} from 'd3-drag'

import 'd3-selection-multi'

async function heatmap(el) {
  let height = 395
  let margin = {
    left: 1,
    top: 1,
    right: 1
  }

  let offset = {
    left: 60,
    bottom: 20
  }

  let $el = select(el)
  let width = $el.node().offsetWidth - margin.left - margin.right - offset.left - 1

  let url = new URL(el.dataset.source)
  url.searchParams.append('tbs', Math.floor(width / 8) + 1)

  let response = await fetch(url),
    json = await response.json(),
    data = json['values']

  let bucketWidth = width / json['time']['size']
  let bucketHeight = height / json['latency']['size']

  let scaleX = scaleTime()
    .range([0, width])
    .domain(json['time']['range'].map((x) => Date.parse(x)))
  let scaleY = scaleLinear()
    .range([height, 0])
    .domain(json['latency']['range'].map((x) => x / 1000000))
  let scaleZ = scaleLog()
    .range(['#CFE9FF', '#0275D8'])
    .domain([1, max(data, (x) => x.v)])

  let svg = $el.append('svg')
      .attr('width', $el.node().offsetWidth)
      .attr('height', height + offset.bottom)
    .append('g')
      .attr('transform', `translate(${margin.left + offset.left}, ${margin.top})`)

  let axisX = axisBottom(scaleX)
    .tickFormat(timeFormat('%H:%M'))
  let axisY = axisLeft(scaleY)
    .tickFormat((x) => `${x} ms`)

  svg.append('g')
    .attr('transform', `translate(0,${height})`)
    .call(axisX)

  svg.append('g')
    .attr('transform', 'translate(0, 0)')
    .call(axisY)

  let heatmap = svg
    .append('g')
    .classed('hm-data', true)

  let group = undefined
  let lastY = 0

  for(let {x, y, v} of data) {
    if(!group || y != lastY) {
      group = heatmap.append('g')
      group.classed(`hm-row-${y}`, true)
      lastY = y
    }

    group
      .append('rect')
      .attrs({
        x: Math.round(1 + x * bucketWidth),
        y: height - bucketHeight - y * bucketHeight,
        width: Math.round(bucketWidth),
        height: bucketHeight,
        fill: scaleZ(v),
        title: v,
      })
  }

  let hm_back = heatmap
    .append('rect')
    .attr('width', width)
    .attr('height', height)
    .attr('fill', 'transparent')

  function dragstart(e) {
    console.log('start', e)
  }

  function dragging(e) {
    console.log('drag', e)
  }

  function dragstop(e) {
    console.log('end', e)
  }

  hm_back.call(drag()
    .on('start', dragstart)
    .on('drag', dragging)
    .on('end', dragstop));
}

function initialize() {
  document.querySelectorAll('#heatmap').forEach((el) => heatmap(el))
}

if (document.readyState === "complete" ||
    document.readyState === "loaded" ||
    document.readyState === "interactive") {
  initialize()
} else {
  document.addEventListener('DOMContentLoaded', initialize)
}
<|endoftext|>