<commit_before>/*
 *
 * Copyright 2015, Google Inc.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *     * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following disclaimer
 * in the documentation and/or other materials provided with the
 * distribution.
 *     * Neither the name of Google Inc. nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 */

'use strict';

var assert = require('assert');
var grpc = require('bindings')('grpc.node');

/**
 * This is used for testing functions with multiple asynchronous calls that
 * can happen in different orders. This should be passed the number of async
 * function invocations that can occur last, and each of those should call this
 * function's return value
 * @param {function()} done The function that should be called when a test is
 *     complete.
 * @param {number} count The number of calls to the resulting function if the
 *     test passes.
 * @return {function()} The function that should be called at the end of each
 *     sequence of asynchronous functions.
 */
function multiDone(done, count) {
  return function() {
    count -= 1;
    if (count <= 0) {
      done();
    }
  };
}

describe('end-to-end', function() {
  var server;
  var channel;
  before(function() {
    server = new grpc.Server();
    var port_num = server.addHttp2Port('0.0.0.0:0');
    server.start();
    channel = new grpc.Channel('localhost:' + port_num);
  });
  after(function() {
    server.shutdown();
  });
  it('should start and end a request without error', function(complete) {
    var done = multiDone(complete, 2);
    var deadline = new Date();
    deadline.setSeconds(deadline.getSeconds() + 3);
    var status_text = 'xyz';
    var call = new grpc.Call(channel,
                             'dummy_method',
                             Infinity);
    var client_batch = {};
    client_batch[grpc.opType.SEND_INITIAL_METADATA] = {};
    client_batch[grpc.opType.SEND_CLOSE_FROM_CLIENT] = true;
    client_batch[grpc.opType.RECV_INITIAL_METADATA] = true;
    client_batch[grpc.opType.RECV_STATUS_ON_CLIENT] = true;
    call.startBatch(client_batch, function(err, response) {
      assert.ifError(err);
      assert.deepEqual(response, {
        'send metadata': true,
        'client close': true,
        'metadata': {},
        'status': {
          'code': grpc.status.OK,
          'details': status_text,
          'metadata': {}
        }
      });
      done();
    });

    server.requestCall(function(err, call_details) {
      var new_call = call_details['new call'];
      assert.notEqual(new_call, null);
      var server_call = new_call.call;
      assert.notEqual(server_call, null);
      var server_batch = {};
      server_batch[grpc.opType.SEND_INITIAL_METADATA] = {};
      server_batch[grpc.opType.SEND_STATUS_FROM_SERVER] = {
        'metadata': {},
        'code': grpc.status.OK,
        'details': status_text
      };
      server_batch[grpc.opType.RECV_CLOSE_ON_SERVER] = true;
      server_call.startBatch(server_batch, function(err, response) {
        assert.ifError(err);
        assert.deepEqual(response, {
          'send metadata': true,
          'send status': true,
          'cancelled': false
        });
       done();
      });
    });
  });
  it('should successfully send and receive metadata', function(complete) {
    var done = multiDone(complete, 2);
    var deadline = new Date();
    deadline.setSeconds(deadline.getSeconds() + 3);
    var status_text = 'xyz';
    var call = new grpc.Call(channel,
                             'dummy_method',
                             Infinity);
    var client_batch = {};
    client_batch[grpc.opType.SEND_INITIAL_METADATA] = {
      'client_key': ['client_value']
    };
    client_batch[grpc.opType.SEND_CLOSE_FROM_CLIENT] = true;
    client_batch[grpc.opType.RECV_INITIAL_METADATA] = true;
    client_batch[grpc.opType.RECV_STATUS_ON_CLIENT] = true;
    call.startBatch(client_batch, function(err, response) {
      assert.ifError(err);
      assert(response['send metadata']);
      assert(response['client close']);
      assert(response.hasOwnProperty('metadata'));
      assert.strictEqual(response.metadata.server_key[0].toString(),
                         'server_value');
      assert.deepEqual(response.status, {'code': grpc.status.OK,
                                         'details': status_text,
                                         'metadata': {}});
      done();
    });

    server.requestCall(function(err, call_details) {
      var new_call = call_details['new call'];
      assert.notEqual(new_call, null);
      assert.strictEqual(new_call.metadata.client_key[0].toString(),
                         'client_value');
      var server_call = new_call.call;
      assert.notEqual(server_call, null);
      var server_batch = {};
      server_batch[grpc.opType.SEND_INITIAL_METADATA] = {
        'server_key': ['server_value']
      };
      server_batch[grpc.opType.SEND_STATUS_FROM_SERVER] = {
        'metadata': {},
        'code': grpc.status.OK,
        'details': status_text
      };
      server_batch[grpc.opType.RECV_CLOSE_ON_SERVER] = true;
      server_call.startBatch(server_batch, function(err, response) {
        assert.ifError(err);
        assert.deepEqual(response, {
          'send metadata': true,
          'send status': true,
          'cancelled': false
        });
       done();
      });
    });
  });
  it('should send and receive data without error', function(complete) {
    var req_text = 'client_request';
    var reply_text = 'server_response';
    var done = multiDone(complete, 2);
    var deadline = new Date();
    deadline.setSeconds(deadline.getSeconds() + 3);
    var status_text = 'success';
    var call = new grpc.Call(channel,
                             'dummy_method',
                             Infinity);
    var client_batch = {};
    client_batch[grpc.opType.SEND_INITIAL_METADATA] = {};
    client_batch[grpc.opType.SEND_MESSAGE] = new Buffer(req_text);
    client_batch[grpc.opType.SEND_CLOSE_FROM_CLIENT] = true;
    client_batch[grpc.opType.RECV_INITIAL_METADATA] = true;
    client_batch[grpc.opType.RECV_MESSAGE] = true;
    client_batch[grpc.opType.RECV_STATUS_ON_CLIENT] = true;
    call.startBatch(client_batch, function(err, response) {
      assert.ifError(err);
      assert(response['send metadata']);
      assert(response['client close']);
      assert.deepEqual(response.metadata, {});
      assert(response['send message']);
      assert.strictEqual(response.read.toString(), reply_text);
      assert.deepEqual(response.status, {'code': grpc.status.OK,
                                         'details': status_text,
                                         'metadata': {}});
      done();
    });

    server.requestCall(function(err, call_details) {
      var new_call = call_details['new call'];
      assert.notEqual(new_call, null);
      var server_call = new_call.call;
      assert.notEqual(server_call, null);
      var server_batch = {};
      server_batch[grpc.opType.SEND_INITIAL_METADATA] = {};
      server_batch[grpc.opType.RECV_MESSAGE] = true;
      server_call.startBatch(server_batch, function(err, response) {
        assert.ifError(err);
        assert(response['send metadata']);
        assert.strictEqual(response.read.toString(), req_text);
        var response_batch = {};
        response_batch[grpc.opType.SEND_MESSAGE] = new Buffer(reply_text);
        response_batch[grpc.opType.SEND_STATUS_FROM_SERVER] = {
          'metadata': {},
          'code': grpc.status.OK,
          'details': status_text
        };
        response_batch[grpc.opType.RECV_CLOSE_ON_SERVER] = true;
        server_call.startBatch(response_batch, function(err, response) {
          assert(response['send status']);
          assert(!response.cancelled);
          done();
        });
      });
    });
  });
  it('should send multiple messages', function(complete) {
    var done = multiDone(complete, 2);
    var requests = ['req1', 'req2'];
    var deadline = new Date();
    deadline.setSeconds(deadline.getSeconds() + 3);
    var status_text = 'xyz';
    var call = new grpc.Call(channel,
                             'dummy_method',
                             Infinity);
    var client_batch = {};
    client_batch[grpc.opType.SEND_INITIAL_METADATA] = {};
    client_batch[grpc.opType.SEND_MESSAGE] = new Buffer(requests[0]);
    client_batch[grpc.opType.RECV_INITIAL_METADATA] = true;
    call.startBatch(client_batch, function(err, response) {
      assert.ifError(err);
      assert.deepEqual(response, {
        'send metadata': true,
        'send message': true,
        'metadata': {}
      });
      var req2_batch = {};
      req2_batch[grpc.opType.SEND_MESSAGE] = new Buffer(requests[1]);
      req2_batch[grpc.opType.SEND_CLOSE_FROM_CLIENT] = true;
      req2_batch[grpc.opType.RECV_STATUS_ON_CLIENT] = true;
      call.startBatch(req2_batch, function(err, resp) {
        assert.ifError(err);
        assert.deepEqual(resp, {
          'send message': true,
          'client close': true,
          'status': {
            'code': grpc.status.OK,
            'details': status_text,
            'metadata': {}
          }
        });
        done();
      });
    });

    server.requestCall(function(err, call_details) {
      var new_call = call_details['new call'];
      assert.notEqual(new_call, null);
      var server_call = new_call.call;
      assert.notEqual(server_call, null);
      var server_batch = {};
      server_batch[grpc.opType.SEND_INITIAL_METADATA] = {};
      server_batch[grpc.opType.RECV_MESSAGE] = true;
      server_call.startBatch(server_batch, function(err, response) {
        assert.ifError(err);
        assert(response['send metadata']);
        assert.strictEqual(response.read.toString(), requests[0]);
        var end_batch = {};
        end_batch[grpc.opType.RECV_CLOSE_ON_SERVER] = true;
        end_batch[grpc.opType.SEND_STATUS_FROM_SERVER] = {
          'metadata': {},
          'code': grpc.status.OK,
          'details': status_text
        };
        end_batch[grpc.opType.RECV_MESSAGE] = true;
        server_call.startBatch(end_batch, function(err, response) {
          assert.ifError(err);
          assert(response['send status']);
          assert(!response.cancelled);
          assert.strictEqual(response.read.toString(), requests[1]);
          done();
        });
      });
    });
  });
});
<commit_msg>Simplified some tests<commit_after>/*
 *
 * Copyright 2015, Google Inc.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *     * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following disclaimer
 * in the documentation and/or other materials provided with the
 * distribution.
 *     * Neither the name of Google Inc. nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 */

'use strict';

var assert = require('assert');
var grpc = require('bindings')('grpc.node');

/**
 * This is used for testing functions with multiple asynchronous calls that
 * can happen in different orders. This should be passed the number of async
 * function invocations that can occur last, and each of those should call this
 * function's return value
 * @param {function()} done The function that should be called when a test is
 *     complete.
 * @param {number} count The number of calls to the resulting function if the
 *     test passes.
 * @return {function()} The function that should be called at the end of each
 *     sequence of asynchronous functions.
 */
function multiDone(done, count) {
  return function() {
    count -= 1;
    if (count <= 0) {
      done();
    }
  };
}

describe('end-to-end', function() {
  var server;
  var channel;
  before(function() {
    server = new grpc.Server();
    var port_num = server.addHttp2Port('0.0.0.0:0');
    server.start();
    channel = new grpc.Channel('localhost:' + port_num);
  });
  after(function() {
    server.shutdown();
  });
  it('should start and end a request without error', function(complete) {
    var done = multiDone(complete, 2);
    var deadline = new Date();
    deadline.setSeconds(deadline.getSeconds() + 3);
    var status_text = 'xyz';
    var call = new grpc.Call(channel,
                             'dummy_method',
                             Infinity);
    var client_batch = {};
    client_batch[grpc.opType.SEND_INITIAL_METADATA] = {};
    client_batch[grpc.opType.SEND_CLOSE_FROM_CLIENT] = true;
    client_batch[grpc.opType.RECV_INITIAL_METADATA] = true;
    client_batch[grpc.opType.RECV_STATUS_ON_CLIENT] = true;
    call.startBatch(client_batch, function(err, response) {
      assert.ifError(err);
      assert.deepEqual(response, {
        'send metadata': true,
        'client close': true,
        'metadata': {},
        'status': {
          'code': grpc.status.OK,
          'details': status_text,
          'metadata': {}
        }
      });
      done();
    });

    server.requestCall(function(err, call_details) {
      var new_call = call_details['new call'];
      assert.notEqual(new_call, null);
      var server_call = new_call.call;
      assert.notEqual(server_call, null);
      var server_batch = {};
      server_batch[grpc.opType.SEND_INITIAL_METADATA] = {};
      server_batch[grpc.opType.SEND_STATUS_FROM_SERVER] = {
        'metadata': {},
        'code': grpc.status.OK,
        'details': status_text
      };
      server_batch[grpc.opType.RECV_CLOSE_ON_SERVER] = true;
      server_call.startBatch(server_batch, function(err, response) {
        assert.ifError(err);
        assert.deepEqual(response, {
          'send metadata': true,
          'send status': true,
          'cancelled': false
        });
       done();
      });
    });
  });
  it('should successfully send and receive metadata', function(complete) {
    var done = multiDone(complete, 2);
    var deadline = new Date();
    deadline.setSeconds(deadline.getSeconds() + 3);
    var status_text = 'xyz';
    var call = new grpc.Call(channel,
                             'dummy_method',
                             Infinity);
    var client_batch = {};
    client_batch[grpc.opType.SEND_INITIAL_METADATA] = {
      'client_key': ['client_value']
    };
    client_batch[grpc.opType.SEND_CLOSE_FROM_CLIENT] = true;
    client_batch[grpc.opType.RECV_INITIAL_METADATA] = true;
    client_batch[grpc.opType.RECV_STATUS_ON_CLIENT] = true;
    call.startBatch(client_batch, function(err, response) {
      assert.ifError(err);
      assert.deepEqual(response,{
        'send metadata': true,
        'client close': true,
        metadata: {server_key: ['server_value']},
        status: {'code': grpc.status.OK,
                 'details': status_text,
                 'metadata': {}}
      });
      done();
    });

    server.requestCall(function(err, call_details) {
      var new_call = call_details['new call'];
      assert.notEqual(new_call, null);
      assert.strictEqual(new_call.metadata.client_key[0],
                         'client_value');
      var server_call = new_call.call;
      assert.notEqual(server_call, null);
      var server_batch = {};
      server_batch[grpc.opType.SEND_INITIAL_METADATA] = {
        'server_key': ['server_value']
      };
      server_batch[grpc.opType.SEND_STATUS_FROM_SERVER] = {
        'metadata': {},
        'code': grpc.status.OK,
        'details': status_text
      };
      server_batch[grpc.opType.RECV_CLOSE_ON_SERVER] = true;
      server_call.startBatch(server_batch, function(err, response) {
        assert.ifError(err);
        assert.deepEqual(response, {
          'send metadata': true,
          'send status': true,
          'cancelled': false
        });
       done();
      });
    });
  });
  it('should send and receive data without error', function(complete) {
    var req_text = 'client_request';
    var reply_text = 'server_response';
    var done = multiDone(complete, 2);
    var deadline = new Date();
    deadline.setSeconds(deadline.getSeconds() + 3);
    var status_text = 'success';
    var call = new grpc.Call(channel,
                             'dummy_method',
                             Infinity);
    var client_batch = {};
    client_batch[grpc.opType.SEND_INITIAL_METADATA] = {};
    client_batch[grpc.opType.SEND_MESSAGE] = new Buffer(req_text);
    client_batch[grpc.opType.SEND_CLOSE_FROM_CLIENT] = true;
    client_batch[grpc.opType.RECV_INITIAL_METADATA] = true;
    client_batch[grpc.opType.RECV_MESSAGE] = true;
    client_batch[grpc.opType.RECV_STATUS_ON_CLIENT] = true;
    call.startBatch(client_batch, function(err, response) {
      assert.ifError(err);
      assert(response['send metadata']);
      assert(response['client close']);
      assert.deepEqual(response.metadata, {});
      assert(response['send message']);
      assert.strictEqual(response.read.toString(), reply_text);
      assert.deepEqual(response.status, {'code': grpc.status.OK,
                                         'details': status_text,
                                         'metadata': {}});
      done();
    });

    server.requestCall(function(err, call_details) {
      var new_call = call_details['new call'];
      assert.notEqual(new_call, null);
      var server_call = new_call.call;
      assert.notEqual(server_call, null);
      var server_batch = {};
      server_batch[grpc.opType.SEND_INITIAL_METADATA] = {};
      server_batch[grpc.opType.RECV_MESSAGE] = true;
      server_call.startBatch(server_batch, function(err, response) {
        assert.ifError(err);
        assert(response['send metadata']);
        assert.strictEqual(response.read.toString(), req_text);
        var response_batch = {};
        response_batch[grpc.opType.SEND_MESSAGE] = new Buffer(reply_text);
        response_batch[grpc.opType.SEND_STATUS_FROM_SERVER] = {
          'metadata': {},
          'code': grpc.status.OK,
          'details': status_text
        };
        response_batch[grpc.opType.RECV_CLOSE_ON_SERVER] = true;
        server_call.startBatch(response_batch, function(err, response) {
          assert(response['send status']);
          assert(!response.cancelled);
          done();
        });
      });
    });
  });
  it('should send multiple messages', function(complete) {
    var done = multiDone(complete, 2);
    var requests = ['req1', 'req2'];
    var deadline = new Date();
    deadline.setSeconds(deadline.getSeconds() + 3);
    var status_text = 'xyz';
    var call = new grpc.Call(channel,
                             'dummy_method',
                             Infinity);
    var client_batch = {};
    client_batch[grpc.opType.SEND_INITIAL_METADATA] = {};
    client_batch[grpc.opType.SEND_MESSAGE] = new Buffer(requests[0]);
    client_batch[grpc.opType.RECV_INITIAL_METADATA] = true;
    call.startBatch(client_batch, function(err, response) {
      assert.ifError(err);
      assert.deepEqual(response, {
        'send metadata': true,
        'send message': true,
        'metadata': {}
      });
      var req2_batch = {};
      req2_batch[grpc.opType.SEND_MESSAGE] = new Buffer(requests[1]);
      req2_batch[grpc.opType.SEND_CLOSE_FROM_CLIENT] = true;
      req2_batch[grpc.opType.RECV_STATUS_ON_CLIENT] = true;
      call.startBatch(req2_batch, function(err, resp) {
        assert.ifError(err);
        assert.deepEqual(resp, {
          'send message': true,
          'client close': true,
          'status': {
            'code': grpc.status.OK,
            'details': status_text,
            'metadata': {}
          }
        });
        done();
      });
    });

    server.requestCall(function(err, call_details) {
      var new_call = call_details['new call'];
      assert.notEqual(new_call, null);
      var server_call = new_call.call;
      assert.notEqual(server_call, null);
      var server_batch = {};
      server_batch[grpc.opType.SEND_INITIAL_METADATA] = {};
      server_batch[grpc.opType.RECV_MESSAGE] = true;
      server_call.startBatch(server_batch, function(err, response) {
        assert.ifError(err);
        assert(response['send metadata']);
        assert.strictEqual(response.read.toString(), requests[0]);
        var end_batch = {};
        end_batch[grpc.opType.RECV_CLOSE_ON_SERVER] = true;
        end_batch[grpc.opType.SEND_STATUS_FROM_SERVER] = {
          'metadata': {},
          'code': grpc.status.OK,
          'details': status_text
        };
        end_batch[grpc.opType.RECV_MESSAGE] = true;
        server_call.startBatch(end_batch, function(err, response) {
          assert.ifError(err);
          assert(response['send status']);
          assert(!response.cancelled);
          assert.strictEqual(response.read.toString(), requests[1]);
          done();
        });
      });
    });
  });
});
<|endoftext|>


<commit_before>'use strict';

/**
 * @license
 * Copyright 2015 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.
 */

goog.provide('tachyfont.TachyFontSet');

goog.require('goog.Promise');
goog.require('goog.array');
goog.require('goog.log');
goog.require('goog.style');
goog.require('tachyfont.IncrementalFontUtils');
goog.require('tachyfont.chainedPromises');

/**
 * Manage a group of TachyFonts.
 *
 * @param {string} familyName The font family name for this set.
 * @constructor
 */
tachyfont.TachyFontSet = function(familyName) {
  /**
   * The TachyFonts managed in this set.
   *
   * @type {!Array.<tachyfont.TachyFont>}
   */
  this.fonts = [];

  /** @type {!Object} */
  this.fontIdToIndex = {};

  /**
   * Map of CSS family spec to TachyFont family.
   *
   * @type {!Object.<string, string>}
   */
  this.cssFamilyToTachyFontFamily = {};

  /** @type {string} */
  this.familyName = familyName;

  /**
   * Sigh, for really slow sites do not set the CSS until the page is loaded.
   *
   * @type {boolean}
   */
  this.domContentLoaded = false;

  /**
   * The number of chars that need to be loaded.
   *
   * TODO(bstell): this is not fully used yet.
   *
   * @private {number}
   */
  this.pendingChars_ = 0;

  /**
   * Do not need to scan the DOM if there have been mutation events before
   * DOMContentLoaded.
   *
   * @type {boolean}
   */
  this.hadMutationEvents = false;

  /**
   * The updateFont operation takes time so serialize them.
   *
   * @type {tachyfont.chainedPromises}
   */
  this.finishPrecedingUpdateFont = new tachyfont.chainedPromises();
  if (goog.DEBUG) {
    this.finishPrecedingUpdateFont.setDebugMessage('finishPrecedingUpdateFont');
  }

  /**
   * The timerID from setTimeout.
   *
   * @private {?number} The timerID from setTimeout.
   */
  this.pendingUpdateRequest_ = null;
};

/*
 * There are multiple issues for the 'character detection', 'character data
 * fetching', and 'CSS update' logic to manage:
 *
 *   1. Character Detection
 *      * Use a Mutation Observer to record the characters as they are added to
 *        the DOM.
 *        * Behavior of simple web pages:
 *          * Load the DOM in a single operation and fire DOMContentLoaded
 *          * Note: the mutation event can happen before or after the
 *            DOMContentLoaded event.
 *        * Behavior of complex web pages:
 *          * Slowly load chunks of characters into the DOM both before and
 *            after DOMContentLoaded.
 *          * Mutation events happen multiple times before DOMContentLoaded and
 *            multiple times after DOMContentLoaded.
 *        * Mutation events can happen before or after DOMContentLoaded so at
 *          DOMContentLoaded:
 *          * If there have been mutation events then immediately update the
 *            fonts.
 *          * If there have not been mutation events then wait for mutation
 *            event which will follow soon.  We could scan the DOM to do the
 *            update immediately on DOMContentLoaded but scanning the DOM is
 *            expensive.
 *
 *   2. Character Data Fetching
 *      We want to have all the character data as close to DOMContentLoaded as
 *      possible. Because of round trip latency, we want to avoid lots of small
 *      fetches.
 *      * Before and after DOMContentLoaded: to reduce  the number of fetches
 *        only fetch every few seconds.
 *      * On DOMContentLoaded if there have been mutation events then fetch
 *        immediately. If there have not been any mutation events then wait to
 *        fetch the data on the soon to follow mutation event.
 *
 *   3. CSS Updating
 *      * It is _very_ CPU/time expensive to ship multiple multi-megabyte fonts
 *        from Javascript to the browser native code.
 *        * During the transfer the heavy CPU usage makes the page unresponsive.
 *        * Currently during the transfer the text becomes blank. The user sees
 *          an unpleasant 'flash' of blank text between the time when the
 *          fallback glyphs are cleared and the new glyphs are available for
 *          display.
 *        * The 'flash' can perhaps be solved by rendering to an off screen area
 *          and only switching the CSS once the transfer is complete. Note that
 *          this will not do anything about the CPU usage.
 *      * If on start up the font is already persisted then update the CSS
 *        immediately as it is very likely the font already has the UI text. The
 *        positive of this is that the UI shows the correct glyphs as soon as
 *        possible. The negative of this is there may be a ransom note effect
 *        and the CSS update makes the page unresponsived during the data
 *        transfer.
 *      * Before DOMContentLoaded do not display updates to the font as this
 *        would make the page unresponsive (even if the flashing is fixed).
 *      * At DOMContentLoaded display the font (this is the main goal)
 *      * After DOMContentLoaded only fetch characters and set the font every
 *        few seconds to balance keeping the display correct vs CPU loading.
 */

/**
 * Maximum time in milliseconds to wait before doing a character update.
 *
 * @type {number}
 */
tachyfont.TachyFontSet.TIMEOUT = 3000;

/**
 * Add a TachyFont.
 *
 * @param {tachyfont.TachyFont} font The TachyFont to add to the set.
 */
tachyfont.TachyFontSet.prototype.addFont = function(font) {
  this.fonts.push(font);
};

/**
 * For the node and sub-nodes remove TachyFont from input fields.
 *
 * @param {Node} node The starting point for walking the node/sub-nodes.
 */
tachyfont.TachyFontSet.prototype.recursivelyRemoveTachyFontFromInputFields =
    function(node) {
  this.removeTachyFontFromInputField(node);
  var children = node.childNodes;
  for (var i = 0; i < children.length; i++) {
    this.recursivelyRemoveTachyFontFromInputFields(children[i]);
  }
};

/**
 * Remove TachyFont from an input field.
 *
 * @param {Node} node The node to work on.
 */
tachyfont.TachyFontSet.prototype.removeTachyFontFromInputField =
    function(node) {
  if (node.nodeName != 'INPUT') {
    return;
  }

  var cssFamily = goog.style.getComputedStyle(/** @type {Element} */ (node),
      'font-family');
  if (goog.DEBUG) {
    goog.log.log(tachyfont.logger, goog.log.Level.FINER,
        'INPUT css family: ' + cssFamily);
  }

  var families = cssFamily.split(',');
  // Remove the TachyFont if it is in the family list.
  goog.array.removeAllIf(families, function(val, index, array) {
    // Handle extra quotes.
    val = tachyfont.IncrementalFontUtils.trimFamilyName(val);
    return this.familyName == val;
  }, this);
  var newCssFamily = families.join(', ');
  node.style.fontFamily = newCssFamily;
  if (goog.DEBUG) {
    goog.log.log(tachyfont.logger, goog.log.Level.FINER, 'newCssFamily: ' +
        goog.style.getComputedStyle(/** @type {Element} */ (node),
            'font-family'));
  }
};

/**
 * For the node and sub-nodes record the needed text for each TachyFont.
 *
 * @param {Node} node The starting point for walking the node/sub-nodes.
 */
tachyfont.TachyFontSet.prototype.recursivelyAddTextToFontGroups =
    function(node) {
  this.addTextToFontGroups(node);
  var children = node.childNodes;
  for (var i = 0; i < children.length; i++) {
    this.recursivelyAddTextToFontGroups(children[i]);
  }
};

/**
 * Record the needed text for each TachyFont.
 *
 * @param {Node} node The text node.
 * @return {boolean} True if text was added.
 */
tachyfont.TachyFontSet.prototype.addTextToFontGroups = function(node) {
  if (node.nodeName != '#text') {
    return false;
  }

  var text = node.nodeValue.trim();
  if (!text) {
    return false;
  }

  var parentNode = /** @type {Element} */ (node.parentNode);
  // <title> text does not have a parentNode.
  if (!parentNode) {
    return false;
  }
  var parentName = node.parentNode.nodeName;
  if (parentName == 'SCRIPT' || parentName == 'STYLE') {
    return false;
  }
  var cssFamily = goog.style.getComputedStyle(parentNode, 'font-family');
  var weight = goog.style.getComputedStyle(parentNode, 'font-weight');
  // TODO(bstell): add support for slant, width, etc.
  if (goog.DEBUG) {
    goog.log.fine(tachyfont.logger, cssFamily + '/' + weight + ': "' +
        text + '"');
  }

  // Convert the cssFamily to a family (empty string if not supported)
  var family = this.cssFamilyToTachyFontFamily[cssFamily];
  if (family == undefined) {
    var families = cssFamily.split(',');
    for (var i = 0; i < families.length; i++) {
      var aFamily = tachyfont.IncrementalFontUtils.trimFamilyName(families[i]);
      if (aFamily == this.familyName) {
        this.cssFamilyToTachyFontFamily[cssFamily] = this.familyName;
        break;
      }
    }
    family = this.cssFamilyToTachyFontFamily[cssFamily];
  }
  if (!family) {
    if (goog.DEBUG) {
      goog.log.log(tachyfont.logger, goog.log.Level.FINER,
          'cssFamily \'' + cssFamily + '\' not supported');
    }
    return false;
  }

  // Normalize the weight; eg, 'normal' -> '400'
  weight = tachyfont.cssWeightToNumber[weight] || weight;
  var fontId = tachyfont.fontId(family, weight);

  // Look for this in the font set.
  var index = this.fontIdToIndex[fontId];
  if (index == undefined) {
    if (goog.DEBUG) {
      goog.log.log(tachyfont.logger, goog.log.Level.FINER,
          'did not find = ' + fontId);
    }
    return false;
  }

  var tachyFont = this.fonts[index];
  // Handle UTF16.
  var charArray = tachyfont.stringToChars(text);
  var textAdded = false;
  // Tell the font it needs these characters.
  var charlist = tachyFont.incrfont.charsToLoad;
  for (var i = 0; i < charArray.length; i++) {
    var c = charArray[i];
    if (charlist[c] != 1) {
      textAdded = true;
      this.pendingChars_ += 1;
    }
    charlist[c] = 1;
  }
  return textAdded;
};

/**
 * Request an update of a group of TachyFonts
 */
tachyfont.TachyFontSet.prototype.requestUpdateFonts = function() {
  // Set a pending request.
  if (this.pendingUpdateRequest_ == null) {
    this.pendingUpdateRequest_ = setTimeout(function() {
      if (goog.DEBUG) {
        goog.log.info(tachyfont.logger, 'requestUpdateFonts: updateFonts');
      }
      this.updateFonts(false);
    }.bind(this), tachyfont.TachyFontSet.TIMEOUT);
  }
};

/**
 * Update a group of TachyFonts
 *
 * @param {boolean} allowEarlyUse Allow the font to be used before the page has
 *     finished loading.
 * @return {goog.Promise}
 *
 */
tachyfont.TachyFontSet.prototype.updateFonts = function(allowEarlyUse) {
  if (goog.DEBUG) {
    goog.log.fine(tachyfont.logger, 'updateFonts');
  }
  // Clear any pending request.
  if (this.pendingUpdateRequest_ != null) {
    if (goog.DEBUG) {
      goog.log.info(tachyfont.logger, 'clear pendingUpdateRequest_');
    }
    clearTimeout(this.pendingUpdateRequest_);
    this.pendingUpdateRequest_ = null;
  }
  var msg;
  if (goog.DEBUG) {
    goog.log.log(tachyfont.logger, goog.log.Level.FINER,
        'updateFonts: wait for preceding update');
    msg = 'updateFonts';
  }
  var allUpdated = this.finishPrecedingUpdateFont.getChainedPromise(msg);
  allUpdated.getPrecedingPromise().
      then(function() {
        if (goog.DEBUG) {
          goog.log.log(tachyfont.logger, goog.log.Level.FINER,
              'updateFonts: done waiting for preceding update');
        }
        var updatingFonts = [];
        for (var i = 0; i < this.fonts.length; i++) {
          var fontObj = this.fonts[i].incrfont;
          var load = fontObj.loadChars();
          updatingFonts.push(load);
        }
        return goog.Promise.all(updatingFonts);
      }.bind(this)).
      thenCatch(function(err) {
        if (goog.DEBUG) {
          debugger;
        }
      }).
      then(function(/*loadResults*/) {
        var fontsData = [];
        for (var i = 0; i < this.fonts.length; i++) {
          var fontObj = this.fonts[i].incrfont;
          var needToSetFont = fontObj.needToSetFont;
          // It takes significant time to pass the font data from Javascript to
          // the browser. So unless specifically told to do so, do not update
          // the font before the page loads.
          if (!this.domContentLoaded) {
            if (!allowEarlyUse) {
              needToSetFont = false;
            }
          }
          // TODO(bstell): check the font has loaded char data. If no char data
          // was ever loaded then don't waste CPU and time loading a useless
          // font.
          var fontData;
          if (needToSetFont) {
            fontData = fontObj.getBase;
          } else {
            fontData = goog.Promise.resolve(null);
          }
          fontsData.push(fontData);
        }
        if (goog.DEBUG) {
          goog.log.log(tachyfont.logger, goog.log.Level.FINER,
              'updateFonts: wait for font bases');
        }
        return goog.Promise.all(fontsData);
      }.bind(this)).
      then(function(loadResults) {
        if (goog.DEBUG) {
          goog.log.log(tachyfont.logger, goog.log.Level.FINER,
              'updateFonts: got font bases');
        }
        var allCssSet = [];
        for (var i = 0; i < loadResults.length; i++) {
          var loadResult = loadResults[i];
          if (loadResult == null) {
            allCssSet.push(goog.Promise.resolve(null));
            continue;
          }
          var fileInfo = loadResult[0];
          var fontData = loadResult[1];
          var fontObj = this.fonts[i].incrfont;
          if (goog.DEBUG) {
            goog.log.fine(tachyfont.logger, 'updateFonts: setFont: ');
          }
          var cssSetResult = fontObj.setFont(fontData, fileInfo).
              then(function(cssSetResult) {
                tachyfont.IncrementalFontUtils.setVisibility(fontObj.style,
                fontObj.fontInfo, true);
              });
          allCssSet.push(cssSetResult);
        }
        return goog.Promise.all(allCssSet);
      }.bind(this)).
      then(function(setResults) {
        if (goog.DEBUG) {
          goog.log.fine(tachyfont.logger, 'updateFonts: updated all fonts');
        }
        allUpdated.resolve();
      }.bind(this)).
      thenCatch(function(e) {
        debugger;
        allUpdated.reject();
        if (goog.DEBUG) {
          goog.log.error(tachyfont.logger, 'failed to load all fonts' +
              e.stack);
          debugger;
        }
      });
  return allUpdated.getPromise();
};
<commit_msg>remove a debugger statement<commit_after>'use strict';

/**
 * @license
 * Copyright 2015 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.
 */

goog.provide('tachyfont.TachyFontSet');

goog.require('goog.Promise');
goog.require('goog.array');
goog.require('goog.log');
goog.require('goog.style');
goog.require('tachyfont.IncrementalFontUtils');
goog.require('tachyfont.chainedPromises');

/**
 * Manage a group of TachyFonts.
 *
 * @param {string} familyName The font family name for this set.
 * @constructor
 */
tachyfont.TachyFontSet = function(familyName) {
  /**
   * The TachyFonts managed in this set.
   *
   * @type {!Array.<tachyfont.TachyFont>}
   */
  this.fonts = [];

  /** @type {!Object} */
  this.fontIdToIndex = {};

  /**
   * Map of CSS family spec to TachyFont family.
   *
   * @type {!Object.<string, string>}
   */
  this.cssFamilyToTachyFontFamily = {};

  /** @type {string} */
  this.familyName = familyName;

  /**
   * Sigh, for really slow sites do not set the CSS until the page is loaded.
   *
   * @type {boolean}
   */
  this.domContentLoaded = false;

  /**
   * The number of chars that need to be loaded.
   *
   * TODO(bstell): this is not fully used yet.
   *
   * @private {number}
   */
  this.pendingChars_ = 0;

  /**
   * Do not need to scan the DOM if there have been mutation events before
   * DOMContentLoaded.
   *
   * @type {boolean}
   */
  this.hadMutationEvents = false;

  /**
   * The updateFont operation takes time so serialize them.
   *
   * @type {tachyfont.chainedPromises}
   */
  this.finishPrecedingUpdateFont = new tachyfont.chainedPromises();
  if (goog.DEBUG) {
    this.finishPrecedingUpdateFont.setDebugMessage('finishPrecedingUpdateFont');
  }

  /**
   * The timerID from setTimeout.
   *
   * @private {?number} The timerID from setTimeout.
   */
  this.pendingUpdateRequest_ = null;
};

/*
 * There are multiple issues for the 'character detection', 'character data
 * fetching', and 'CSS update' logic to manage:
 *
 *   1. Character Detection
 *      * Use a Mutation Observer to record the characters as they are added to
 *        the DOM.
 *        * Behavior of simple web pages:
 *          * Load the DOM in a single operation and fire DOMContentLoaded
 *          * Note: the mutation event can happen before or after the
 *            DOMContentLoaded event.
 *        * Behavior of complex web pages:
 *          * Slowly load chunks of characters into the DOM both before and
 *            after DOMContentLoaded.
 *          * Mutation events happen multiple times before DOMContentLoaded and
 *            multiple times after DOMContentLoaded.
 *        * Mutation events can happen before or after DOMContentLoaded so at
 *          DOMContentLoaded:
 *          * If there have been mutation events then immediately update the
 *            fonts.
 *          * If there have not been mutation events then wait for mutation
 *            event which will follow soon.  We could scan the DOM to do the
 *            update immediately on DOMContentLoaded but scanning the DOM is
 *            expensive.
 *
 *   2. Character Data Fetching
 *      We want to have all the character data as close to DOMContentLoaded as
 *      possible. Because of round trip latency, we want to avoid lots of small
 *      fetches.
 *      * Before and after DOMContentLoaded: to reduce  the number of fetches
 *        only fetch every few seconds.
 *      * On DOMContentLoaded if there have been mutation events then fetch
 *        immediately. If there have not been any mutation events then wait to
 *        fetch the data on the soon to follow mutation event.
 *
 *   3. CSS Updating
 *      * It is _very_ CPU/time expensive to ship multiple multi-megabyte fonts
 *        from Javascript to the browser native code.
 *        * During the transfer the heavy CPU usage makes the page unresponsive.
 *        * Currently during the transfer the text becomes blank. The user sees
 *          an unpleasant 'flash' of blank text between the time when the
 *          fallback glyphs are cleared and the new glyphs are available for
 *          display.
 *        * The 'flash' can perhaps be solved by rendering to an off screen area
 *          and only switching the CSS once the transfer is complete. Note that
 *          this will not do anything about the CPU usage.
 *      * If on start up the font is already persisted then update the CSS
 *        immediately as it is very likely the font already has the UI text. The
 *        positive of this is that the UI shows the correct glyphs as soon as
 *        possible. The negative of this is there may be a ransom note effect
 *        and the CSS update makes the page unresponsived during the data
 *        transfer.
 *      * Before DOMContentLoaded do not display updates to the font as this
 *        would make the page unresponsive (even if the flashing is fixed).
 *      * At DOMContentLoaded display the font (this is the main goal)
 *      * After DOMContentLoaded only fetch characters and set the font every
 *        few seconds to balance keeping the display correct vs CPU loading.
 */

/**
 * Maximum time in milliseconds to wait before doing a character update.
 *
 * @type {number}
 */
tachyfont.TachyFontSet.TIMEOUT = 3000;

/**
 * Add a TachyFont.
 *
 * @param {tachyfont.TachyFont} font The TachyFont to add to the set.
 */
tachyfont.TachyFontSet.prototype.addFont = function(font) {
  this.fonts.push(font);
};

/**
 * For the node and sub-nodes remove TachyFont from input fields.
 *
 * @param {Node} node The starting point for walking the node/sub-nodes.
 */
tachyfont.TachyFontSet.prototype.recursivelyRemoveTachyFontFromInputFields =
    function(node) {
  this.removeTachyFontFromInputField(node);
  var children = node.childNodes;
  for (var i = 0; i < children.length; i++) {
    this.recursivelyRemoveTachyFontFromInputFields(children[i]);
  }
};

/**
 * Remove TachyFont from an input field.
 *
 * @param {Node} node The node to work on.
 */
tachyfont.TachyFontSet.prototype.removeTachyFontFromInputField =
    function(node) {
  if (node.nodeName != 'INPUT') {
    return;
  }

  var cssFamily = goog.style.getComputedStyle(/** @type {Element} */ (node),
      'font-family');
  if (goog.DEBUG) {
    goog.log.log(tachyfont.logger, goog.log.Level.FINER,
        'INPUT css family: ' + cssFamily);
  }

  var families = cssFamily.split(',');
  // Remove the TachyFont if it is in the family list.
  goog.array.removeAllIf(families, function(val, index, array) {
    // Handle extra quotes.
    val = tachyfont.IncrementalFontUtils.trimFamilyName(val);
    return this.familyName == val;
  }, this);
  var newCssFamily = families.join(', ');
  node.style.fontFamily = newCssFamily;
  if (goog.DEBUG) {
    goog.log.log(tachyfont.logger, goog.log.Level.FINER, 'newCssFamily: ' +
        goog.style.getComputedStyle(/** @type {Element} */ (node),
            'font-family'));
  }
};

/**
 * For the node and sub-nodes record the needed text for each TachyFont.
 *
 * @param {Node} node The starting point for walking the node/sub-nodes.
 */
tachyfont.TachyFontSet.prototype.recursivelyAddTextToFontGroups =
    function(node) {
  this.addTextToFontGroups(node);
  var children = node.childNodes;
  for (var i = 0; i < children.length; i++) {
    this.recursivelyAddTextToFontGroups(children[i]);
  }
};

/**
 * Record the needed text for each TachyFont.
 *
 * @param {Node} node The text node.
 * @return {boolean} True if text was added.
 */
tachyfont.TachyFontSet.prototype.addTextToFontGroups = function(node) {
  if (node.nodeName != '#text') {
    return false;
  }

  var text = node.nodeValue.trim();
  if (!text) {
    return false;
  }

  var parentNode = /** @type {Element} */ (node.parentNode);
  // <title> text does not have a parentNode.
  if (!parentNode) {
    return false;
  }
  var parentName = node.parentNode.nodeName;
  if (parentName == 'SCRIPT' || parentName == 'STYLE') {
    return false;
  }
  var cssFamily = goog.style.getComputedStyle(parentNode, 'font-family');
  var weight = goog.style.getComputedStyle(parentNode, 'font-weight');
  // TODO(bstell): add support for slant, width, etc.
  if (goog.DEBUG) {
    goog.log.fine(tachyfont.logger, cssFamily + '/' + weight + ': "' +
        text + '"');
  }

  // Convert the cssFamily to a family (empty string if not supported)
  var family = this.cssFamilyToTachyFontFamily[cssFamily];
  if (family == undefined) {
    var families = cssFamily.split(',');
    for (var i = 0; i < families.length; i++) {
      var aFamily = tachyfont.IncrementalFontUtils.trimFamilyName(families[i]);
      if (aFamily == this.familyName) {
        this.cssFamilyToTachyFontFamily[cssFamily] = this.familyName;
        break;
      }
    }
    family = this.cssFamilyToTachyFontFamily[cssFamily];
  }
  if (!family) {
    if (goog.DEBUG) {
      goog.log.log(tachyfont.logger, goog.log.Level.FINER,
          'cssFamily \'' + cssFamily + '\' not supported');
    }
    return false;
  }

  // Normalize the weight; eg, 'normal' -> '400'
  weight = tachyfont.cssWeightToNumber[weight] || weight;
  var fontId = tachyfont.fontId(family, weight);

  // Look for this in the font set.
  var index = this.fontIdToIndex[fontId];
  if (index == undefined) {
    if (goog.DEBUG) {
      goog.log.log(tachyfont.logger, goog.log.Level.FINER,
          'did not find = ' + fontId);
    }
    return false;
  }

  var tachyFont = this.fonts[index];
  // Handle UTF16.
  var charArray = tachyfont.stringToChars(text);
  var textAdded = false;
  // Tell the font it needs these characters.
  var charlist = tachyFont.incrfont.charsToLoad;
  for (var i = 0; i < charArray.length; i++) {
    var c = charArray[i];
    if (charlist[c] != 1) {
      textAdded = true;
      this.pendingChars_ += 1;
    }
    charlist[c] = 1;
  }
  return textAdded;
};

/**
 * Request an update of a group of TachyFonts
 */
tachyfont.TachyFontSet.prototype.requestUpdateFonts = function() {
  // Set a pending request.
  if (this.pendingUpdateRequest_ == null) {
    this.pendingUpdateRequest_ = setTimeout(function() {
      if (goog.DEBUG) {
        goog.log.info(tachyfont.logger, 'requestUpdateFonts: updateFonts');
      }
      this.updateFonts(false);
    }.bind(this), tachyfont.TachyFontSet.TIMEOUT);
  }
};

/**
 * Update a group of TachyFonts
 *
 * @param {boolean} allowEarlyUse Allow the font to be used before the page has
 *     finished loading.
 * @return {goog.Promise}
 *
 */
tachyfont.TachyFontSet.prototype.updateFonts = function(allowEarlyUse) {
  if (goog.DEBUG) {
    goog.log.fine(tachyfont.logger, 'updateFonts');
  }
  // Clear any pending request.
  if (this.pendingUpdateRequest_ != null) {
    if (goog.DEBUG) {
      goog.log.info(tachyfont.logger, 'clear pendingUpdateRequest_');
    }
    clearTimeout(this.pendingUpdateRequest_);
    this.pendingUpdateRequest_ = null;
  }
  var msg;
  if (goog.DEBUG) {
    goog.log.log(tachyfont.logger, goog.log.Level.FINER,
        'updateFonts: wait for preceding update');
    msg = 'updateFonts';
  }
  var allUpdated = this.finishPrecedingUpdateFont.getChainedPromise(msg);
  allUpdated.getPrecedingPromise().
      then(function() {
        if (goog.DEBUG) {
          goog.log.log(tachyfont.logger, goog.log.Level.FINER,
              'updateFonts: done waiting for preceding update');
        }
        var updatingFonts = [];
        for (var i = 0; i < this.fonts.length; i++) {
          var fontObj = this.fonts[i].incrfont;
          var load = fontObj.loadChars();
          updatingFonts.push(load);
        }
        return goog.Promise.all(updatingFonts);
      }.bind(this)).
      thenCatch(function(err) {
        if (goog.DEBUG) {
          debugger;
        }
      }).
      then(function(/*loadResults*/) {
        var fontsData = [];
        for (var i = 0; i < this.fonts.length; i++) {
          var fontObj = this.fonts[i].incrfont;
          var needToSetFont = fontObj.needToSetFont;
          // It takes significant time to pass the font data from Javascript to
          // the browser. So unless specifically told to do so, do not update
          // the font before the page loads.
          if (!this.domContentLoaded) {
            if (!allowEarlyUse) {
              needToSetFont = false;
            }
          }
          // TODO(bstell): check the font has loaded char data. If no char data
          // was ever loaded then don't waste CPU and time loading a useless
          // font.
          var fontData;
          if (needToSetFont) {
            fontData = fontObj.getBase;
          } else {
            fontData = goog.Promise.resolve(null);
          }
          fontsData.push(fontData);
        }
        if (goog.DEBUG) {
          goog.log.log(tachyfont.logger, goog.log.Level.FINER,
              'updateFonts: wait for font bases');
        }
        return goog.Promise.all(fontsData);
      }.bind(this)).
      then(function(loadResults) {
        if (goog.DEBUG) {
          goog.log.log(tachyfont.logger, goog.log.Level.FINER,
              'updateFonts: got font bases');
        }
        var allCssSet = [];
        for (var i = 0; i < loadResults.length; i++) {
          var loadResult = loadResults[i];
          if (loadResult == null) {
            allCssSet.push(goog.Promise.resolve(null));
            continue;
          }
          var fileInfo = loadResult[0];
          var fontData = loadResult[1];
          var fontObj = this.fonts[i].incrfont;
          if (goog.DEBUG) {
            goog.log.fine(tachyfont.logger, 'updateFonts: setFont: ');
          }
          var cssSetResult = fontObj.setFont(fontData, fileInfo).
              then(function(cssSetResult) {
                tachyfont.IncrementalFontUtils.setVisibility(fontObj.style,
                fontObj.fontInfo, true);
              });
          allCssSet.push(cssSetResult);
        }
        return goog.Promise.all(allCssSet);
      }.bind(this)).
      then(function(setResults) {
        if (goog.DEBUG) {
          goog.log.fine(tachyfont.logger, 'updateFonts: updated all fonts');
        }
        allUpdated.resolve();
      }.bind(this)).
      thenCatch(function(e) {
        if (goog.DEBUG) {
          goog.log.error(tachyfont.logger, 'failed to load all fonts' +
              e.stack);
          debugger;
        }
        allUpdated.reject();
      });
  return allUpdated.getPromise();
};
<|endoftext|>


<commit_before>var buster = require('buster'),
  report = require('../../lib/endpoint/report');

buster.testCase('report', {
  'should have endpoint name': function () {
    assert.equals(report.name, 'report');
  },
  'should have required params': function () {
    assert.equals(report.params.required, ['key', 'userIp', 'id']);
  },
  'should have optional params': function () {
    assert.equals(report.params.optional, ['userAgent', 'userSessionId', 'content']);
  },
  'should return endpoint name and eventName param as path': function () {
    assert.equals(report.path({ eventName: 'someeventname' }), 'report/someeventname');
  },
  'should pass result to callback when success handler is called': function (done) {
    function cb(err, result) {
      assert.isNull(err),
      assert.equals(result.foo, 'bar');
      done();
    }
    report.handlers[200]({ foo: 'bar' }, cb);
  },
  'should pass result to callback when search modified handler is called': function (done) {
    function cb(err, result) {
      assert.isNull(err),
      assert.equals(result.foo, 'bar');
      done();
    }
    report.handlers[206]({ foo: 'bar' }, cb);
  },
  'should pass error and result to callback when validation error handler is called': function (done) {
    function cb(err, result) {
      assert.equals(err.message, 'Validation error');
      assert.equals(result.foo, 'bar');
      done();
    }
    report.handlers[400]({ foo: 'bar' }, cb);
  }
});<commit_msg>Clean up lint violations.<commit_after>var buster = require('buster'),
  report = require('../../lib/endpoint/report');

buster.testCase('report', {
  'should have endpoint name': function () {
    assert.equals(report.name, 'report');
  },
  'should have required params': function () {
    assert.equals(report.params.required, ['key', 'userIp', 'id']);
  },
  'should have optional params': function () {
    assert.equals(report.params.optional, ['userAgent', 'userSessionId', 'content']);
  },
  'should return endpoint name and eventName param as path': function () {
    assert.equals(report.path({ eventName: 'someeventname' }), 'report/someeventname');
  },
  'should pass result to callback when success handler is called': function (done) {
    function cb(err, result) {
      assert.isNull(err);
      assert.equals(result.foo, 'bar');
      done();
    }
    report.handlers[200]({ foo: 'bar' }, cb);
  },
  'should pass result to callback when search modified handler is called': function (done) {
    function cb(err, result) {
      assert.isNull(err);
      assert.equals(result.foo, 'bar');
      done();
    }
    report.handlers[206]({ foo: 'bar' }, cb);
  },
  'should pass error and result to callback when validation error handler is called': function (done) {
    function cb(err, result) {
      assert.equals(err.message, 'Validation error');
      assert.equals(result.foo, 'bar');
      done();
    }
    report.handlers[400]({ foo: 'bar' }, cb);
  }
});<|endoftext|>


<commit_before>CKEDITOR.editorConfig = function( config )
{
    config.toolbar_Silki =
        [ [ "Bold", "Italic" ],
          [ "H2", "H3", "H4", "-", "Pre" ],
          [ "NumberedList", "BulletedList", "-", "Outdent", "Indent", "Blockquote" ],
          [ "Link", "Unlink", "Anchor" ],
          [ "Image", "Table", "HorizontalRule", "SpecialChar" ],
          [ "Undo", "Redo", "Maximize" ] ];

    config.toolbar = "Silki";

    config.skin = "Silki";

    config.linkShowTargetTab = false;
    config.linkShowAdvancedTab = false;
};

<commit_msg>Remove Anchor from ckeditor tab for now<commit_after>CKEDITOR.editorConfig = function( config )
{
    config.toolbar_Silki =
        [ [ "Bold", "Italic" ],
          [ "H2", "H3", "H4", "-", "Pre" ],
          [ "NumberedList", "BulletedList", "-", "Outdent", "Indent", "Blockquote" ],
          [ "Link", "Unlink" ],
          [ "Image", "Table", "HorizontalRule", "SpecialChar" ],
          [ "Undo", "Redo", "Maximize" ] ];

    config.toolbar = "Silki";

    config.skin = "Silki";

    config.linkShowTargetTab = false;
    config.linkShowAdvancedTab = false;
};

<|endoftext|>


<commit_before>Application.Controllers.controller('notesController',
    ["$scope", "mcapi", "User", "recent", "Project", notesController]);

function notesController($scope, mcapi, User, recent, Project) {

    $scope.add_notes = function () {
        $scope.note = {
            'item_id': $scope.doc.id,
            'item_type': 'datafile',
            'creator': User.u(),
            'project_id': $scope.project.id,
            'note': $scope.model.new_note,
            'title': $scope.model.title
        };
        $scope.model = {
            new_note: "",
            title: ""
        };
        mcapi('/notes')
            .success(function (note) {
                $scope.project.notes.push(note);
                recent.gotoLast($scope.project.id);
            }).post($scope.note);
    };

    $scope.editNotes = function (index) {
        $scope.edit_index = index;
    };

    $scope.saveNotes = function (note) {
        var i = _.indexOf($scope.project.notes, function (item) {
            return item.id === note.id;
        });
        if (i !== -1) {
            mcapi('/notes')
                .success(function (note) {
                    $scope.project.notes[i] = note;
                }).put({'title': note.title, 'note': note.note, 'id': note.id});
        }
        $scope.edit_index = -1;
    };

    function init() {
        $scope.model = {
            new_note: "",
            title: ""
        };
    }

    init();
}

Application.Directives.directive('notes', notesDirective);
function notesDirective() {
    return {
        restrict: "A",
        scope: {
            edit: "=",
            doc: '=',
            update: '@',
            project: "="
        },
        controller: "notesController",
        templateUrl: 'application/core/projects/project/files/notes.html'
    };
}
<commit_msg>change to notes directive<commit_after>Application.Controllers.controller('notesController',
    ["$scope", "mcapi", "User", "recent", "Project", notesController]);

function notesController($scope, mcapi, User, recent, Project) {

    $scope.add_notes = function () {
        $scope.note = {
            'item_id': $scope.doc.id,
            'item_type': 'datafile',
            'creator': User.u(),
            'project_id': $scope.project.id,
            'note': $scope.model.new_note,
            'title': $scope.model.title
        };
        $scope.model = {
            new_note: "",
            title: ""
        };
        mcapi('/notes')
            .success(function (note) {
                $scope.project.notes.push(note);
                //recent.gotoLast($scope.project.id);
            }).post($scope.note);
    };

    $scope.editNotes = function (index) {
        $scope.edit_index = index;
    };

    $scope.saveNotes = function (note) {
        var i = _.indexOf($scope.project.notes, function (item) {
            return item.id === note.id;
        });
        if (i !== -1) {
            mcapi('/notes')
                .success(function (note) {
                    $scope.project.notes[i] = note;
                }).put({'title': note.title, 'note': note.note, 'id': note.id});
        }
        $scope.edit_index = -1;
    };

    function init() {
        $scope.model = {
            new_note: "",
            title: ""
        };
    }

    init();
}

Application.Directives.directive('notes', notesDirective);
function notesDirective() {
    return {
        restrict: "A",
        scope: {
            edit: "=",
            doc: '=',
            update: '@',
            project: "="
        },
        controller: "notesController",
        templateUrl: 'application/core/projects/project/files/notes.html'
    };
}
<|endoftext|>


<commit_before>const _ = require('lodash');
const Promise = require('bluebird');
const debug = require('debug')('lib:parse');
const moment = require('moment');
const nconf = require('nconf');
const xpath = require('xpath');
const xmldom = require('xmldom');
const JSDOM = require('jsdom').JSDOM;

const mongo = require('./mongo');
const sequence = require('../parsers/components/utils/sequence');
const summarize = require('../parsers/components/utils/summarize');
const parserDebug = require('../parsers/components/utils/parserDebug');
const echoes = require('./echoes');
const utils = require('./utils');
const aggregated = require('./aggregated');
const glue = require('./glue');

function checkMetadata(impression, repeat) {
    if(_.isUndefined(impression.id))
        throw new Error("impression missing");

    return mongo
        .readOne(nconf.get('schema').metadata, { id: impression.id })
        .then(function(i) {
            if( _.get(i, 'id') === impression.id && !repeat) {
                debug("metadata [%s] already exists: skipping", i.id);
                return null;
            }

            if( _.get(i, 'id') === impression.id && repeat) {
                debug("metadata [%s] exists, but repeat is requested", i.id);
                return Promise.all([
                    mongo.remove(nconf.get('schema').metadata, { id: impression.id }),
                    mongo.remove(nconf.get('schema').summary, { id: impression.id })
                ])
                .return(impression);
            }

            /* else if _.isUndefined(i) is returned the impression */
            return impression;
        });
}

function initialize(impression) {
    const xmlerr = { warning: 0, error: 0, fatal: 0 };
    var domOptions = {
          errorHandler:{
              warning: function() { xmlerr.warning += 1; },
              error: function() { xmlerr.error += 1; },
              fatalError: function() { xmlerr.fatal += 1;  },
          }
    };
    /* this is the envelope with get appended the metadata from the various parsers */
    return {
        impression,
        xmlerr,
        xpath,
        jsdom: new JSDOM(impression.html).window.document,
        dom: new xmldom.DOMParser(domOptions).parseFromString(impression.html),
    };
}

function finalize(envelopes) {
    if(!_.size(envelopes))
        return { metadata: [], summary: [], statistics: [], parsers: [], errors: [] };

    /* here is build the new complex object which travels in the pipeline,
     * metadata & parsers are the internal content, not meant to be public */

    const errors = _.reduce(envelopes, function(memo, e) {
        if(!_.size(e.errors))
            return memo;
        var r = {
            errors: e.errors,
            when:  new Date(),
            id: e.impression.id,
            timelineId: e.impression.timelineId
        }
        memo.push(r);
        return memo;
    }, []);

    const removef = ['dom', 'jsdom', 'xpath', 'xmlerr', 'impression', 'errors'];
    const impressionFields =
        ['id', 'timelineId', 'userId', 'impressionOrder', 'impressionTime'];

    const metadata = _.map(envelopes, function(e) {
        return _.extend(
            _.pick(e.impression, impressionFields),
            _.omit(e, removef),
            { when: new Date() }
        );
    });
    const parsers = _.map(metadata, parserDebug);

    /* summary is the format used for user-facing API */
    const summary = _.map(metadata, summarize);
    /* statistics are the public information on how fbtrex is doing */
    const statistics = _.map(summary, aggregated.computeStats);

    return {
        metadata,
        parsers,
        summary,
        statistics,
        errors
    };
}

function logSummary(blobs) {
    _.each(blobs.summary, function(e) {
        echoes.echo(
            _.extend({'index': 'parserv' },
            _.pick(e, ['errors', 'type', 'publicationTime', 'postId',
                       'permaLink', 'author', 'textlength', 'impressionTime',
                       'impressionOrder', 'pseudo', 'timeline', 'regexp' ])
            )
        );
    });

    /* the `fulldump` is set by executers or by `parsers/precise.js` */
    if(nconf.get('fulldump'))
        _.times(_.size(blobs.metadata), function(o, i) {
            let s = _.nth(blobs.summary, i);
            console.log(JSON.stringify(s, undefined, 1));
            console.log("\x1b[36m");
            let m = _.nth(blobs.metadata, i);
            console.log(JSON.stringify(m, undefined, 1));
        });

    /* this is dumped even without fulldump */
    const E = "\x1b[47m\x1b[31m";
    _.each(blobs.errors, function(o) {
        console.log(E, JSON.stringify(o, undefined, 2));
    });
}

function mark(blobs) {
    return Promise.map(blobs.metadata, function(e) {
        return mongo
            .readOne(nconf.get('schema').htmls, { id: e.id })
            .then(function(existing) {
                existing.processed = true;
                return mongo
                    .updateOne(nconf.get('schema').htmls, { _id: existing._id }, existing);
            });
    }, { concurrency: 1});
}

function save(blobs) {
    let chain = [];
    debug("Received %d metadata and %d errors", _.size(blobs.metadata), _.size(blobs.errors));

    if(_.size(blobs.metadata))
        chain.push(
            mongo.writeMany(nconf.get('schema').metadata, blobs.metadata)  );

    if(_.size(blobs.metadata) && !nconf.get('onlymetadata') ) {
        chain.push(
            mongo.writeMany(nconf.get('schema').summary, blobs.summary)  );
        chain.push(
            aggregated.updateHourly(blobs.statistics)   );
        /*
         - still unclear how and what we should records
        chain.push(
            mongo.writeMany(nconf.get('schema').parsers, blobs.parsers)   );
         */
    }

    if(_.size(blobs.errors) && !nconf.get('onlymetadata') )
        chain.push(
            mongo.writeMany(nconf.get('schema').errors, blobs.errors)  );

    return Promise
        .all(chain)
        .return({
            metadata: _.size(blobs.metadata),
            errors: _.size(blobs.errors)
        })
        .catch(function(error) {
            console.log("error in saving:", error.message);
            return null;
        });
}

function postIdCount(e) {
    /* this function creates certain conditional fields in metadata:
     * - postId is build at the end of `sequence`
     * - postCount { personal, global } gets created always */

    // this is slowing too much the parsing, I'm going to removed it
    // --- sadClaudio

    if(!e.postId)
        return e;

    e.postCount = { personal: null, global: null };

    return Promise.all([
        mongo.count(nconf.get('schema').metadata,
            { linkedtime: { postId: e.postId } }),
        mongo.count(nconf.get('schema').metadata,
            { linkedtime: { postId: e.postId }, userId: e.userId })
    ])
    .then(function(results) {
        e.postCount.global = results[0];
        e.postCount.personal = results[1];
        return e;
    });
};

function semanticIdCount(e) {
    /* this function creates certain conditional fields in metadata:
     * * if there is no .texts or zero-length texts, it returns
     * - semanticId gets created always
     * - semanticCount { personal, global } gets created always
     * - semantic: true, gets created when the semanticCount.global is zero  */

    if(!e.fullTextSize)
        return e;

    e.semanticId = utils.hash({ text: e.fullText });
    e.semanticCount = { personal: null, global: null };

    return Promise.all([
        mongo.count(nconf.get('schema').metadata, { semanticId: e.semanticId }),
        mongo.count(nconf.get('schema').metadata, { semanticId: e.semanticId, userId: e.userId })
    ])
    .then(function(results) {
        e.semanticCount.global = results[0];
        e.semanticCount.personal = results[1];

        /* only the first metadata with a due semanticId is marked with `semantic`,
         * this will be processed later by lib/semantic.js */
        if(!e.semanticCount.global)
            e.semantic = true;
        return e;
    });
};

function mergeHTMLImpression(html) {
    return mongo
        .readOne(nconf.get('schema').impressions, { id: html.impressionId })
        .then(function(impression) {
            _.unset(impression, 'id');
            _.unset(impression, 'htmlId');
            return _.merge(html, impression);
        });
}

function parseHTML(htmlfilter, repeat) {
    return mongo
        .read(nconf.get('schema').htmls, htmlfilter)
        .then(function(found) {
            if(_.size(found) > 0)
                return found;
            if(nconf.get('retrive') != true)
                return [];
            return glue.retrive(htmlfilter)
                .then(glue.writers)
                .tap(function(x) {
                    if(x && x[2] && htmlfilter.id == x[2][0].id)
                        debug("Successfully retrived remote content");
                    else
                        debug("Failure in retriving remote content!");
                })
                .then(function() {
                    return mongo
                        .read(nconf.get('schema').htmls, htmlfilter);
                });
        })
        .catch(function(error) {
            debug("Managed error in retrieving content: %s", error);
            // console.error(error.stack);
            return [];
        })
        .map(mergeHTMLImpression, { concurrency: 1 })
        .then(_.compact)
        .then(function(impressions) {
            return _.orderBy(impressions, { impressionOrder: -1 });
        })
        .tap(function(impressions) {
            if(_.size(impressions)) {
                const firstT = moment(_.first(impressions).impressionTime).format("DD/MMM/YY HH:mm");
                const lastT = moment(_.last(impressions).impressionTime).format("DD/MMM/YY HH:mm");
                const humanized = moment
                    .duration( _.last(impressions).impressionTime - _.first(impressions).impressionTime )
                    .humanize();
                debug("Processing %d impressionsOrder %s [%s] %s %s [%s] %s",
                    /*                                    ^^^^^^^^^^^^^ conditionals */
                    _.size(impressions),
                    _.first(impressions).impressionOrder, firstT,
                    _.size(impressions) > 1 ? "-" : "",
                    _.size(impressions) > 1 ? _.last(impressions).impressionOrder : "",
                    _.size(impressions) > 1 ? "[" + lastT + "]" : "",
                    _.size(impressions) > 1 ? "(" + humanized + ")" : ""
                );
            }
        })
        .map(function(e) {
            return checkMetadata(e, repeat);
        }, { concurrency: 1 })
        .then(_.compact)
        .map(initialize)
        .map(sequence, { concurrency: 1 })
        /* this is the function processing the parsers
         * it is call of every .id which should be analyzed */
        .catch(function(error) {
            debug("[E] Unmanaged error in parser sequence: %s", error.message);
            console.log(error.stack);
            return null;
        })
        .then(function(metadata) {
            if(!metadata)
                return [];

            debug("⁂  completed %d metadata", _.size(metadata));
            if(!_.isUndefined(nconf.get('verbose')))
                debug("⁂  %s", JSON.stringify(metadata, undefined, 2));

            return metadata;
        })
        // .map(postIdCount)
        .map(semanticIdCount)
        .then(finalize)
        .tap(logSummary)
        .tap(mark)
        .then(save)
        .catch(function(error) {
            console.log("[error after parsing]", error.message);
            console.log(error.stack);
            return null;
        });
}

module.exports = {
    checkMetadata: checkMetadata,
    initialize: initialize,
    mergeHTMLImpression: mergeHTMLImpression,
    finalize: finalize,
    logSummary: logSummary,
    save: save,
    postIdCount: postIdCount,
    semanticIdCount: semanticIdCount,
    mark: mark,
    parseHTML: parseHTML,
};
<commit_msg>removed also semanticIdCount for performance reasons and because was not really used<commit_after>const _ = require('lodash');
const Promise = require('bluebird');
const debug = require('debug')('lib:parse');
const moment = require('moment');
const nconf = require('nconf');
const xpath = require('xpath');
const xmldom = require('xmldom');
const JSDOM = require('jsdom').JSDOM;

const mongo = require('./mongo');
const sequence = require('../parsers/components/utils/sequence');
const summarize = require('../parsers/components/utils/summarize');
const parserDebug = require('../parsers/components/utils/parserDebug');
const echoes = require('./echoes');
const utils = require('./utils');
const aggregated = require('./aggregated');
const glue = require('./glue');

function checkMetadata(impression, repeat) {
    if(_.isUndefined(impression.id))
        throw new Error("impression missing");

    return mongo
        .readOne(nconf.get('schema').metadata, { id: impression.id })
        .then(function(i) {
            if( _.get(i, 'id') === impression.id && !repeat) {
                debug("metadata [%s] already exists: skipping", i.id);
                return null;
            }

            if( _.get(i, 'id') === impression.id && repeat) {
                debug("metadata [%s] exists, but repeat is requested", i.id);
                return Promise.all([
                    mongo.remove(nconf.get('schema').metadata, { id: impression.id }),
                    mongo.remove(nconf.get('schema').summary, { id: impression.id })
                ])
                .return(impression);
            }

            /* else if _.isUndefined(i) is returned the impression */
            return impression;
        });
}

function initialize(impression) {
    const xmlerr = { warning: 0, error: 0, fatal: 0 };
    var domOptions = {
          errorHandler:{
              warning: function() { xmlerr.warning += 1; },
              error: function() { xmlerr.error += 1; },
              fatalError: function() { xmlerr.fatal += 1;  },
          }
    };
    /* this is the envelope with get appended the metadata from the various parsers */
    return {
        impression,
        xmlerr,
        xpath,
        jsdom: new JSDOM(impression.html).window.document,
        dom: new xmldom.DOMParser(domOptions).parseFromString(impression.html),
    };
}

function finalize(envelopes) {
    if(!_.size(envelopes))
        return { metadata: [], summary: [], statistics: [], parsers: [], errors: [] };

    /* here is build the new complex object which travels in the pipeline,
     * metadata & parsers are the internal content, not meant to be public */

    const errors = _.reduce(envelopes, function(memo, e) {
        if(!_.size(e.errors))
            return memo;
        var r = {
            errors: e.errors,
            when:  new Date(),
            id: e.impression.id,
            timelineId: e.impression.timelineId
        }
        memo.push(r);
        return memo;
    }, []);

    const removef = ['dom', 'jsdom', 'xpath', 'xmlerr', 'impression', 'errors'];
    const impressionFields =
        ['id', 'timelineId', 'userId', 'impressionOrder', 'impressionTime'];

    const metadata = _.map(envelopes, function(e) {
        return _.extend(
            _.pick(e.impression, impressionFields),
            _.omit(e, removef),
            { when: new Date() }
        );
    });
    const parsers = _.map(metadata, parserDebug);

    /* summary is the format used for user-facing API */
    const summary = _.map(metadata, summarize);
    /* statistics are the public information on how fbtrex is doing */
    const statistics = _.map(summary, aggregated.computeStats);

    return {
        metadata,
        parsers,
        summary,
        statistics,
        errors
    };
}

function logSummary(blobs) {
    _.each(blobs.summary, function(e) {
        echoes.echo(
            _.extend({'index': 'parserv' },
            _.pick(e, ['errors', 'type', 'publicationTime', 'postId',
                       'permaLink', 'author', 'textlength', 'impressionTime',
                       'impressionOrder', 'pseudo', 'timeline', 'regexp' ])
            )
        );
    });

    /* the `fulldump` is set by executers or by `parsers/precise.js` */
    if(nconf.get('fulldump'))
        _.times(_.size(blobs.metadata), function(o, i) {
            let s = _.nth(blobs.summary, i);
            console.log(JSON.stringify(s, undefined, 1));
            console.log("\x1b[36m");
            let m = _.nth(blobs.metadata, i);
            console.log(JSON.stringify(m, undefined, 1));
        });

    /* this is dumped even without fulldump */
    const E = "\x1b[47m\x1b[31m";
    _.each(blobs.errors, function(o) {
        console.log(E, JSON.stringify(o, undefined, 2));
    });
}

function mark(blobs) {
    return Promise.map(blobs.metadata, function(e) {
        return mongo
            .readOne(nconf.get('schema').htmls, { id: e.id })
            .then(function(existing) {
                existing.processed = true;
                return mongo
                    .updateOne(nconf.get('schema').htmls, { _id: existing._id }, existing);
            });
    }, { concurrency: 1});
}

function save(blobs) {
    let chain = [];
    debug("Received %d metadata and %d errors", _.size(blobs.metadata), _.size(blobs.errors));

    if(_.size(blobs.metadata))
        chain.push(
            mongo.writeMany(nconf.get('schema').metadata, blobs.metadata)  );

    if(_.size(blobs.metadata) && !nconf.get('onlymetadata') ) {
        chain.push(
            mongo.writeMany(nconf.get('schema').summary, blobs.summary)  );
        chain.push(
            aggregated.updateHourly(blobs.statistics)   );
        /*
         - still unclear how and what we should records
        chain.push(
            mongo.writeMany(nconf.get('schema').parsers, blobs.parsers)   );
         */
    }

    if(_.size(blobs.errors) && !nconf.get('onlymetadata') )
        chain.push(
            mongo.writeMany(nconf.get('schema').errors, blobs.errors)  );

    return Promise
        .all(chain)
        .return({
            metadata: _.size(blobs.metadata),
            errors: _.size(blobs.errors)
        })
        .catch(function(error) {
            console.log("error in saving:", error.message);
            return null;
        });
}

function postIdCount(e) {
    /* this function creates certain conditional fields in metadata:
     * - postId is build at the end of `sequence`
     * - postCount { personal, global } gets created always */

    // this is slowing too much the parsing, I'm going to removed it
    // --- sadClaudio

    if(!e.postId)
        return e;

    e.postCount = { personal: null, global: null };

    return Promise.all([
        mongo.count(nconf.get('schema').metadata,
            { linkedtime: { postId: e.postId } }),
        mongo.count(nconf.get('schema').metadata,
            { linkedtime: { postId: e.postId }, userId: e.userId })
    ])
    .then(function(results) {
        e.postCount.global = results[0];
        e.postCount.personal = results[1];
        return e;
    });
};

function semanticIdCount(e) {
    /* this function creates certain conditional fields in metadata:
     * * if there is no .texts or zero-length texts, it returns
     * - semanticId gets created always
     * - semanticCount { personal, global } gets created always
     * - semantic: true, gets created when the semanticCount.global is zero  */

    if(!e.fullTextSize)
        return e;

    e.semanticId = utils.hash({ text: e.fullText });
    e.semanticCount = { personal: null, global: null };

    return Promise.all([
        mongo.count(nconf.get('schema').metadata, { semanticId: e.semanticId }),
        mongo.count(nconf.get('schema').metadata, { semanticId: e.semanticId, userId: e.userId })
    ])
    .then(function(results) {
        e.semanticCount.global = results[0];
        e.semanticCount.personal = results[1];

        /* only the first metadata with a due semanticId is marked with `semantic`,
         * this will be processed later by lib/semantic.js */
        if(!e.semanticCount.global)
            e.semantic = true;
        return e;
    });
};

function mergeHTMLImpression(html) {
    return mongo
        .readOne(nconf.get('schema').impressions, { id: html.impressionId })
        .then(function(impression) {
            _.unset(impression, 'id');
            _.unset(impression, 'htmlId');
            return _.merge(html, impression);
        });
}

function parseHTML(htmlfilter, repeat) {
    return mongo
        .read(nconf.get('schema').htmls, htmlfilter)
        .then(function(found) {
            if(_.size(found) > 0)
                return found;
            if(nconf.get('retrive') != true)
                return [];
            return glue.retrive(htmlfilter)
                .then(glue.writers)
                .tap(function(x) {
                    if(x && x[2] && htmlfilter.id == x[2][0].id)
                        debug("Successfully retrived remote content");
                    else
                        debug("Failure in retriving remote content!");
                })
                .then(function() {
                    return mongo
                        .read(nconf.get('schema').htmls, htmlfilter);
                });
        })
        .catch(function(error) {
            debug("Managed error in retrieving content: %s", error);
            // console.error(error.stack);
            return [];
        })
        .map(mergeHTMLImpression, { concurrency: 1 })
        .then(_.compact)
        .then(function(impressions) {
            return _.orderBy(impressions, { impressionOrder: -1 });
        })
        .tap(function(impressions) {
            if(_.size(impressions)) {
                const firstT = moment(_.first(impressions).impressionTime).format("DD/MMM/YY HH:mm");
                const lastT = moment(_.last(impressions).impressionTime).format("DD/MMM/YY HH:mm");
                const humanized = moment
                    .duration( _.last(impressions).impressionTime - _.first(impressions).impressionTime )
                    .humanize();
                debug("Processing %d impressionsOrder %s [%s] %s %s [%s] %s",
                    /*                                    ^^^^^^^^^^^^^ conditionals */
                    _.size(impressions),
                    _.first(impressions).impressionOrder, firstT,
                    _.size(impressions) > 1 ? "-" : "",
                    _.size(impressions) > 1 ? _.last(impressions).impressionOrder : "",
                    _.size(impressions) > 1 ? "[" + lastT + "]" : "",
                    _.size(impressions) > 1 ? "(" + humanized + ")" : ""
                );
            }
        })
        .map(function(e) {
            return checkMetadata(e, repeat);
        }, { concurrency: 1 })
        .then(_.compact)
        .map(initialize)
        .map(sequence, { concurrency: 1 })
        /* this is the function processing the parsers
         * it is call of every .id which should be analyzed */
        .catch(function(error) {
            debug("[E] Unmanaged error in parser sequence: %s", error.message);
            console.log(error.stack);
            return null;
        })
        .then(function(metadata) {
            if(!metadata)
                return [];

            debug("⁂  completed %d metadata", _.size(metadata));
            if(!_.isUndefined(nconf.get('verbose')))
                debug("⁂  %s", JSON.stringify(metadata, undefined, 2));

            return metadata;
        })
        //.map(postIdCount)
        //.map(semanticIdCount)
        .then(finalize)
        .tap(logSummary)
        .tap(mark)
        .then(save)
        .catch(function(error) {
            console.log("[error after parsing]", error.message);
            console.log(error.stack);
            return null;
        });
}

module.exports = {
    checkMetadata: checkMetadata,
    initialize: initialize,
    mergeHTMLImpression: mergeHTMLImpression,
    finalize: finalize,
    logSummary: logSummary,
    save: save,
    postIdCount: postIdCount,
    semanticIdCount: semanticIdCount,
    mark: mark,
    parseHTML: parseHTML,
};
<|endoftext|>


<commit_before>/* eslint-env jest */
jest.mock('request-promise')

/**
 * These tests are run against the "real" cli by actually starting a subprocess.
 * This means that no mocks are available.
 */

const runCli = require('./helpers/cli').realCli

const containsMissingError = expect.stringContaining('ERR! Missing')
const scriptIds = require('./fixtures/script-ids')

const expectNoSuccess = function (msg) {
  return (...args) => {
    console.log(...args)
    throw new Error(msg)
  }
}

describe('CLI Only', () => {
  const request = require('request-promise')
  beforeEach(() => {
    request.mockClear()
  })

  const required = {
    'api key': '--api-key=asdf',
    'script id': '--script-id=' + scriptIds.SCRIPT_ID_SUCCESS,
    'version name': '--version-name=2.0.0'
  }
  for (let pair of Object.entries(required)) {
    const missing = pair[0]
    const args = Object.values(required).filter(x => x !== pair[1])

    it(`fails if ${missing} is missing`, () => {
      return runCli(args)
        .then(expectNoSuccess('should error'), ({ stderr, code }) => {
          expect(code).not.toBe(0)
          expect(stderr).toEqual(containsMissingError)
          expect(request).not.toHaveBeenCalled()
        })
    })
  }
})

describe('CLI with environment variables', () => {
  const request = require('request-promise')
  beforeEach(() => {
    request.mockClear()
  })

  it('fails if version is missing (given SF_APIKEY and SF_SCRIPTID)', () => {
    return runCli([], {
      SF_APIKEY: 'asdff',
      SF_SCRIPTID: scriptIds.SCRIPT_ID_SUCCESS
    }).then(expectNoSuccess('should error'))
      .catch(({ stderr, code }) => {
        expect(code).not.toBe(0)
        expect(stderr).toEqual(containsMissingError)
        expect(request).not.toHaveBeenCalled()
      })
  })

  it('fails if api key is missing (given SF_SCRIPTID, --version-name)', () => {
    return runCli(['--version-name=2.0.0'], {
      SF_SCRIPTID: scriptIds.SCRIPT_ID_SUCCESS
    }).then(expectNoSuccess('should error'), ({ stderr, code }) => {
      expect(code).not.toBe(0)
      console.error(stderr)
      expect(stderr).toEqual(containsMissingError)
      expect(request).not.toHaveBeenCalled()
    })
  })

  it('fails if script id is missing (given SF_APIKEY, --version-name)', () => {
    return runCli(['--version-name=2.0.0'], {
      SF_APIKEY: 'asdfsdf'
    }).then(expectNoSuccess('should error'), ({ stderr, code }) => {
      expect(code).not.toBe(0)
      expect(stderr).toEqual(containsMissingError)
      expect(request).not.toHaveBeenCalled()
    })
  })
})
<commit_msg>fix(test): fix tests<commit_after>/* eslint-env jest */
jest.mock('request-promise')

/**
 * These tests are run against the "real" cli by actually starting a subprocess.
 * This means that no mocks are available.
 */

const runCli = require('./helpers/cli').realCli

const containsMissingError = expect.stringContaining('ERR! Missing')
const scriptIds = require('./fixtures/script-ids')

const expectNoSuccess = function (msg) {
  return (...args) => {
    console.log(...args)
    throw new Error(msg)
  }
}

describe('CLI Only', () => {
  const request = require('request-promise')
  beforeEach(() => {
    request.mockClear()
  })

  const required = {
    'api key': '--api-key=asdf',
    'script id': '--script-id=' + scriptIds.SCRIPT_ID_SUCCESS,
    'version name': '--version-name=2.0.0'
  }
  for (let pair of Object.entries(required)) {
    const missing = pair[0]
    const args = Object.values(required).filter(x => x !== pair[1])

    it(`fails if ${missing} is missing`, () => {
      return runCli(args)
        .then(expectNoSuccess('should error'), ({ stderr, code }) => {
          expect(code).not.toBe(0)
          expect(stderr).toEqual(containsMissingError)
          expect(request).not.toHaveBeenCalled()
        })
    })
  }
})

describe('CLI with environment variables', () => {
  const request = require('request-promise')
  beforeEach(() => {
    request.mockClear()
  })

  it('fails if version is missing (given SF_APIKEY and SF_SCRIPTID)', () => {
    return runCli([], {
      SF_APIKEY: 'asdff',
      SF_SCRIPTID: scriptIds.SCRIPT_ID_SUCCESS
    }).then(expectNoSuccess('should error'))
      .catch(({ stderr, code }) => {
        expect(code).not.toBe(0)
        expect(request).not.toHaveBeenCalled()
      })
  })

  it('fails if api key is missing (given SF_SCRIPTID, --version-name)', () => {
    return runCli(['--version-name=2.0.0'], {
      SF_SCRIPTID: scriptIds.SCRIPT_ID_SUCCESS
    }).then(expectNoSuccess('should error'), ({ stderr, code }) => {
      expect(code).not.toBe(0)
      expect(request).not.toHaveBeenCalled()
    })
  })

  it('fails if script id is missing (given SF_APIKEY, --version-name)', () => {
    return runCli(['--version-name=2.0.0'], {
      SF_APIKEY: 'asdfsdf'
    }).then(expectNoSuccess('should error'), ({ stderr, code }) => {
      expect(code).not.toBe(0)
      expect(request).not.toHaveBeenCalled()
    })
  })
})
<|endoftext|>