<commit_before>const { COMMANDS } = require("./constants");
const Variable = require("./variable");
const MAGIC = "FG";
const ENCODING = "binary";

const PRIMITIVE_TYPES = {
  5: "BOOLEAN",
  2: "INT",
  3: "FLOAT",
  4: "DOUBLE",
  6: "STRING"
};

// Holds a buffer and a pointer. Consumers can consume bytesoff the end of the
// file. When we want to run in the browser, we can refactor this class to use a
// typed array: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Typed_arrays
class MakiFile {
  constructor(buffer) {
    this._buffer = buffer;
    this._i = 0;
  }

  readUInt32LE() {
    const int = this._buffer.readUInt32LE(this._i);
    this._i += 4;
    return int;
  }

  peakUInt32LE() {
    return this._buffer.readUInt32LE(this._i);
  }

  readUInt16LE() {
    const int = this._buffer.readUInt16LE(this._i);
    this._i += 2;
    return int;
  }

  readUInt8() {
    const int = this._buffer.readUInt8(this._i);
    this._i++;
    return int;
  }

  readStringOfLength(length) {
    const str = this._buffer.toString(ENCODING, this._i, this._i + length);
    this._i += length;
    return str;
  }

  readString() {
    return this.readStringOfLength(this.readUInt16LE());
  }

  getPosition() {
    return this._i;
  }
}

function readMagic(makiFile) {
  const magic = makiFile.readStringOfLength(MAGIC.length);
  if (magic !== MAGIC) {
    throw new Error("Magic number does not mach. Is this a maki file?");
  }
  return magic;
}

function readVersion(makiFile) {
  // No idea what we're actually expecting here.
  return makiFile.readUInt16LE();
}

function readClasses(makiFile) {
  let count = makiFile.readUInt32LE();
  const classes = [];
  while (count--) {
    let identifier = "";
    let chunks = 4;
    while (chunks--) {
      identifier += makiFile
        .readUInt32LE()
        .toString(16)
        .padStart(8, "0");
    }
    classes.push(identifier);
  }
  return classes;
}

function readMethods(makiFile) {
  let count = makiFile.readUInt32LE();
  const methods = [];
  while (count--) {
    const classCode = makiFile.readUInt16LE();
    // Offset into our parsed types
    const typeOffset = classCode & 0xff;
    // This is probably the second half of a uint32
    makiFile.readUInt16LE();
    const name = makiFile.readString();
    methods.push({ name, typeOffset });
  }
  return methods;
}

function readVariables({ makiFile, classes }) {
  let count = makiFile.readUInt32LE();
  const variables = [];
  while (count--) {
    const typeOffset = makiFile.readUInt8();
    const object = makiFile.readUInt8();
    const subClass = makiFile.readUInt16LE();
    const uinit1 = makiFile.readUInt16LE();
    const uinit2 = makiFile.readUInt16LE();
    const uinit3 = makiFile.readUInt16LE();
    const uinit4 = makiFile.readUInt16LE();
    const global = makiFile.readUInt8();
    const system = makiFile.readUInt8();
    const props = {
      typeOffset,
      object,
      subClass,
      uinit1,
      uinit2,
      uinit3,
      uinit4,
      global,
      system
    };

    if (object) {
      const klass = classes[typeOffset];
      if (klass == null) {
        throw new Error("Invalid type");
      }
      variables.push(
        new Variable({ ...props, type: klass, typeName: "OBJECT" })
      );
    } else if (subClass) {
      const variable = variables[typeOffset];
      if (variable == null) {
        throw new Error("Invalid type");
      }
      variables.push(
        new Variable({ ...props, type: variable, typeName: "SUBCLASS" })
      );
    } else {
      const typeName = PRIMITIVE_TYPES[typeOffset];
      if (typeName == null) {
        throw new Error("Invalid type");
      }
      let value = null;

      switch (typeName) {
        // BOOLEAN
        case PRIMITIVE_TYPES[5]:
          value = uinit1;
          break;
        // INT
        case PRIMITIVE_TYPES[2]:
          value = uinit1;
          break;
        case PRIMITIVE_TYPES[3]:
        case PRIMITIVE_TYPES[4]:
          const exponent = (uinit2 & 0xff80) >> 7;
          const mantisse = ((0x80 | (uinit2 & 0x7f)) << 16) | uinit1;
          value = mantisse * 2.0 ** (exponent - 0x96);
          break;
        case PRIMITIVE_TYPES[6]:
          // This will likely get set by constants later on.
          break;
        default:
          throw new Error("Invalid primitive type");
      }
      const variable = new Variable({ ...props, type: typeName, typeName });
      variable.setValue(value);
      variables.push(variable);
    }
  }
  return variables;
}

function readConstants({ makiFile, variables }) {
  let count = makiFile.readUInt32LE();
  while (count--) {
    const i = makiFile.readUInt32LE();
    const variable = variables[i];
    const value = makiFile.readString();
    // TODO: Don't mutate
    variable.setValue(value);
  }
}

function readBindings(makiFile) {
  let count = makiFile.readUInt32LE();
  const bindings = [];
  while (count--) {
    const variableOffset = makiFile.readUInt32LE();
    const methodOffset = makiFile.readUInt32LE();
    const binaryOffset = makiFile.readUInt32LE();
    bindings.push({ variableOffset, binaryOffset, methodOffset });
  }
  return bindings;
}

function decodeCode({ makiFile, classes, variables, methods, bindings }) {
  const length = makiFile.readUInt32LE();
  const start = makiFile.getPosition();

  function getPos() {
    return makiFile.getPosition() - start;
  }
  const localFunctions = {};
  const results = [];
  while (makiFile.getPosition() < start + length) {
    const command = parseComand({
      makiFile,
      length,
      pos: getPos(),
      classes,
      variables,
      methods,
      localFunctions
    });
    results.push(command);
  }
  // TODO: Don't mutate
  Object.values(localFunctions).forEach(localFunction => {
    bindings.push(localFunction);
  });

  bindings.sort((a, b) => {
    return a.binaryOffset - b.binaryOffset;
  });

  return results;
}

// TODO: Refactor this to consume bytes directly off the end of MakiFile
function parseComand({ makiFile, length, pos, localFunctions }) {
  const command = {};
  const opcode = makiFile.readUInt8();
  command.offset = pos;
  command.pos = pos;
  command.opcode = opcode;
  command.arguments = [];
  command.command = COMMANDS[opcode];

  if (command.command == null) {
    throw new Error(`Unknown opcode "${opcode}"`);
  }

  if (command.command.arg == null) {
    return command;
  }

  const argValue = makiFile.readUInt32LE();
  const argType = command.command.arg;
  let arg = null;
  switch (argType) {
    case "var": {
      arg = argValue;
      break;
    }
    case "line": {
      arg = argValue + 5;
      break;
    }
    case "objFunc": {
      // TODO: ClassesOffset
      arg = argValue;
      break;
    }
    case "func": {
      // Note in the perl code here: "todo, something strange going on here..."
      const variable = argValue + 5;
      const offset = variable + pos;
      arg = {
        name: `func${offset}`,
        code: [],
        offset
      };
      if (localFunctions[offset] == null) {
        localFunctions[offset] = {
          function: arg,
          offset
        };
      }
      break;
    }
    case "obj": {
      // Classes Offset
      arg = argValue;
      break;
    }
    default:
      throw new Error("Invalid argType");
  }

  command.arguments = [arg];

  // From perl: look forward for a stack protection block
  // (why do I have to look FORWARD. stupid nullsoft)
  if (
    // Is there another UInt32 to read?
    length > pos + 5 + 4 &&
    makiFile.peakUInt32LE() >= 0xffff0000
  ) {
    makiFile.readUInt32LE();
  }

  if (opcode === 112) {
    makiFile.readUInt8();
  }
  return command;
}

function parse(buffer) {
  const makiFile = new MakiFile(buffer);

  const magic = readMagic(makiFile);
  readVersion(makiFile);
  makiFile.readUInt32LE(); // Not sure what we are skipping over here. Just some UInt 32.
  const classes = readClasses(makiFile);
  const methods = readMethods(makiFile);
  const variables = readVariables({ makiFile: makiFile, classes });
  readConstants({ makiFile: makiFile, variables });
  const bindings = readBindings(makiFile);
  const commands = decodeCode({
    makiFile,
    classes,
    variables,
    methods,
    bindings
  });

  // Map binary offsets to command indexes.
  // Some bindings/functions ask us to jump to a place in the binary data and
  // start executing. However, we want to do all the parsing up front, and just
  // return a list of commands. This map allows anything that mentions a binary
  // offset to find the command they should jump to.
  const offsetToCommand = {};
  commands.forEach((command, i) => {
    if (command.offset != null) {
      offsetToCommand[command.offset] = i;
    }
  });

  const resolvedBindings = bindings.map(binding => {
    const { binaryOffset, ...rest } = binding;
    return {
      commandOffset: offsetToCommand[binaryOffset],
      ...rest
    };
  });
  return {
    magic,
    classes,
    methods,
    variables,
    bindings: resolvedBindings,
    commands
  };
}

module.exports = parse;
<commit_msg>Start writing MakiFile to use a Uint8Array under the hood<commit_after>const { COMMANDS } = require("./constants");
const Variable = require("./variable");
const MAGIC = "FG";
const ENCODING = "binary";

const PRIMITIVE_TYPES = {
  5: "BOOLEAN",
  2: "INT",
  3: "FLOAT",
  4: "DOUBLE",
  6: "STRING"
};

// Holds a buffer and a pointer. Consumers can consume bytesoff the end of the
// file. When we want to run in the browser, we can refactor this class to use a
// typed array: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Typed_arrays
class MakiFile {
  constructor(buffer) {
    this._arr = new Uint8Array(buffer);
    this._buffer = buffer;
    this._i = 0;
  }

  readUInt32LE() {
    const int = this.peakUInt32LE();
    this._i += 4;
    return int;
  }

  peakUInt32LE() {
    const int = this._buffer.readUInt32LE(this._i);
    const arrInt = this.__readUInt32LE__arr();
    if (int !== arrInt) {
      throw new Error("Array value does not equal buffer value");
    }
    return int;
  }

  __readUInt32LE__arr() {
    const offset = this._i >>> 0;

    return (
      (this._arr[offset] |
        (this._arr[offset + 1] << 8) |
        (this._arr[offset + 2] << 16)) +
      this._arr[offset + 3] * 0x1000000
    );
  }

  readUInt16LE() {
    const int = this._buffer.readUInt16LE(this._i);
    const arrInt = this.__readUInt16LE__arr();
    if (int !== arrInt) {
      throw new Error("Array value does not equal buffer value");
    }
    this._i += 2;
    return int;
  }

  __readUInt16LE__arr() {
    const offset = this._i >>> 0;
    return this._arr[offset] | (this._arr[offset + 1] << 8);
  }

  readUInt8() {
    const int = this._buffer.readUInt8(this._i);
    const arrInt = this._arr[this._i];
    if (int !== arrInt) {
      throw new Error("Array value does not equal buffer value");
    }
    this._i++;
    return int;
  }

  readStringOfLength(length) {
    const str = this._buffer.toString(ENCODING, this._i, this._i + length);
    const arrStr = this.__readString__arr(length);
    if (str !== arrStr) {
      throw new Error("Array value does not equal buffer value");
    }
    this._i += length;
    return str;
  }

  readString() {
    return this.readStringOfLength(this.readUInt16LE());
  }

  getPosition() {
    return this._i;
  }

  __readString__arr(length) {
    let ret = "";
    const end = Math.min(this._arr.length, this._i + length);

    for (var i = this._i; i < end; ++i) {
      ret += String.fromCharCode(this._arr[i]);
    }
    return ret;
  }
}

function readMagic(makiFile) {
  const magic = makiFile.readStringOfLength(MAGIC.length);
  if (magic !== MAGIC) {
    throw new Error("Magic number does not mach. Is this a maki file?");
  }
  return magic;
}

function readVersion(makiFile) {
  // No idea what we're actually expecting here.
  return makiFile.readUInt16LE();
}

function readClasses(makiFile) {
  let count = makiFile.readUInt32LE();
  const classes = [];
  while (count--) {
    let identifier = "";
    let chunks = 4;
    while (chunks--) {
      identifier += makiFile
        .readUInt32LE()
        .toString(16)
        .padStart(8, "0");
    }
    classes.push(identifier);
  }
  return classes;
}

function readMethods(makiFile) {
  let count = makiFile.readUInt32LE();
  const methods = [];
  while (count--) {
    const classCode = makiFile.readUInt16LE();
    // Offset into our parsed types
    const typeOffset = classCode & 0xff;
    // This is probably the second half of a uint32
    makiFile.readUInt16LE();
    const name = makiFile.readString();
    methods.push({ name, typeOffset });
  }
  return methods;
}

function readVariables({ makiFile, classes }) {
  let count = makiFile.readUInt32LE();
  const variables = [];
  while (count--) {
    const typeOffset = makiFile.readUInt8();
    const object = makiFile.readUInt8();
    const subClass = makiFile.readUInt16LE();
    const uinit1 = makiFile.readUInt16LE();
    const uinit2 = makiFile.readUInt16LE();
    const uinit3 = makiFile.readUInt16LE();
    const uinit4 = makiFile.readUInt16LE();
    const global = makiFile.readUInt8();
    const system = makiFile.readUInt8();

    if (object) {
      const klass = classes[typeOffset];
      if (klass == null) {
        throw new Error("Invalid type");
      }
      variables.push(new Variable({ type: klass, typeName: "OBJECT" }));
    } else if (subClass) {
      const variable = variables[typeOffset];
      if (variable == null) {
        throw new Error("Invalid type");
      }
      variables.push(new Variable({ type: variable, typeName: "SUBCLASS" }));
    } else {
      const typeName = PRIMITIVE_TYPES[typeOffset];
      if (typeName == null) {
        throw new Error("Invalid type");
      }
      let value = null;

      switch (typeName) {
        // BOOLEAN
        case PRIMITIVE_TYPES[5]:
          value = uinit1;
          break;
        // INT
        case PRIMITIVE_TYPES[2]:
          value = uinit1;
          break;
        case PRIMITIVE_TYPES[3]:
        case PRIMITIVE_TYPES[4]:
          const exponent = (uinit2 & 0xff80) >> 7;
          const mantisse = ((0x80 | (uinit2 & 0x7f)) << 16) | uinit1;
          value = mantisse * 2.0 ** (exponent - 0x96);
          break;
        case PRIMITIVE_TYPES[6]:
          // This will likely get set by constants later on.
          break;
        default:
          throw new Error("Invalid primitive type");
      }
      const variable = new Variable({ type: typeName, typeName });
      variable.setValue(value);
      variables.push(variable);
    }
  }
  return variables;
}

function readConstants({ makiFile, variables }) {
  let count = makiFile.readUInt32LE();
  while (count--) {
    const i = makiFile.readUInt32LE();
    const variable = variables[i];
    const value = makiFile.readString();
    // TODO: Don't mutate
    variable.setValue(value);
  }
}

function readBindings(makiFile) {
  let count = makiFile.readUInt32LE();
  const bindings = [];
  while (count--) {
    const variableOffset = makiFile.readUInt32LE();
    const methodOffset = makiFile.readUInt32LE();
    const binaryOffset = makiFile.readUInt32LE();
    bindings.push({ variableOffset, binaryOffset, methodOffset });
  }
  return bindings;
}

function decodeCode({ makiFile, classes, variables, methods, bindings }) {
  const length = makiFile.readUInt32LE();
  const start = makiFile.getPosition();

  function getPos() {
    return makiFile.getPosition() - start;
  }
  const localFunctions = {};
  const results = [];
  while (makiFile.getPosition() < start + length) {
    const command = parseComand({
      makiFile,
      length,
      pos: getPos(),
      classes,
      variables,
      methods,
      localFunctions
    });
    results.push(command);
  }
  // TODO: Don't mutate
  Object.values(localFunctions).forEach(localFunction => {
    bindings.push(localFunction);
  });

  bindings.sort((a, b) => {
    return a.binaryOffset - b.binaryOffset;
  });

  return results;
}

// TODO: Refactor this to consume bytes directly off the end of MakiFile
function parseComand({ makiFile, length, pos, localFunctions }) {
  const command = {};
  const opcode = makiFile.readUInt8();
  command.offset = pos;
  command.pos = pos;
  command.opcode = opcode;
  command.arguments = [];
  command.command = COMMANDS[opcode];

  if (command.command == null) {
    throw new Error(`Unknown opcode "${opcode}"`);
  }

  if (command.command.arg == null) {
    return command;
  }

  const argValue = makiFile.readUInt32LE();
  const argType = command.command.arg;
  let arg = null;
  switch (argType) {
    case "var": {
      arg = argValue;
      break;
    }
    case "line": {
      arg = argValue + 5;
      break;
    }
    case "objFunc": {
      // TODO: ClassesOffset
      arg = argValue;
      break;
    }
    case "func": {
      // Note in the perl code here: "todo, something strange going on here..."
      const variable = argValue + 5;
      const offset = variable + pos;
      arg = {
        name: `func${offset}`,
        code: [],
        offset
      };
      if (localFunctions[offset] == null) {
        localFunctions[offset] = {
          function: arg,
          offset
        };
      }
      break;
    }
    case "obj": {
      // Classes Offset
      arg = argValue;
      break;
    }
    default:
      throw new Error("Invalid argType");
  }

  command.arguments = [arg];

  // From perl: look forward for a stack protection block
  // (why do I have to look FORWARD. stupid nullsoft)
  if (
    // Is there another UInt32 to read?
    length > pos + 5 + 4 &&
    makiFile.peakUInt32LE() >= 0xffff0000
  ) {
    makiFile.readUInt32LE();
  }

  if (opcode === 112) {
    makiFile.readUInt8();
  }
  return command;
}

function parse(buffer) {
  const makiFile = new MakiFile(buffer);

  const magic = readMagic(makiFile);
  readVersion(makiFile);
  makiFile.readUInt32LE(); // Not sure what we are skipping over here. Just some UInt 32.
  const classes = readClasses(makiFile);
  const methods = readMethods(makiFile);
  const variables = readVariables({ makiFile: makiFile, classes });
  readConstants({ makiFile: makiFile, variables });
  const bindings = readBindings(makiFile);
  const commands = decodeCode({
    makiFile,
    classes,
    variables,
    methods,
    bindings
  });

  // Map binary offsets to command indexes.
  // Some bindings/functions ask us to jump to a place in the binary data and
  // start executing. However, we want to do all the parsing up front, and just
  // return a list of commands. This map allows anything that mentions a binary
  // offset to find the command they should jump to.
  const offsetToCommand = {};
  commands.forEach((command, i) => {
    if (command.offset != null) {
      offsetToCommand[command.offset] = i;
    }
  });

  const resolvedBindings = bindings.map(binding => {
    const { binaryOffset, ...rest } = binding;
    return {
      commandOffset: offsetToCommand[binaryOffset],
      ...rest
    };
  });
  return {
    magic,
    classes,
    methods,
    variables,
    bindings: resolvedBindings,
    commands
  };
}

module.exports = parse;
<|endoftext|>


<commit_before>girderTest.importPlugin('candela');
girderTest.startApp();

describe('Test the candela UI.', function () {
    it('register a user', girderTest.createUser(
        'johndoe', 'john.doe@email.com', 'John', 'Doe', 'password!'
    ));

    it('uploads the data file', function () {
        runs(function () {
            expect($('#g-user-action-menu.open').length).toBe(0);
            $('.g-user-text>a:first').click();
        });
        girderTest.waitForLoad();

        runs(function () {
            expect($('#g-user-action-menu.open').length).toBe(1);
            $('a.g-my-folders').click();
        });
        girderTest.waitForLoad();

        runs(function () {
            $('a.g-folder-list-link:last').click();
        });
        girderTest.waitForLoad();

        waitsFor(function () {
            return $('ol.breadcrumb>li.active').text() === 'Public' &&
                   $('.g-empty-parent-message:visible').length === 1;
        }, 'descending into Public folder');

        girderTest.binaryUpload('clients/web/test/testFile.csv');

        runs(function () {
            $('.g-item-list-link:first').click();
        });
    });

    it('sets up candela inputs and renders the visualization', function () {
        waitsFor(function () {
            return $('.g-item-candela-component').length === 1;
        }, 'the candela component selector to appear');

        runs(function () {
            expect($('.g-item-candela-component option').length).toBeGreaterThan(10);
            $('.g-item-candela-component').val('BarChart').change();
        });

        waitsFor(function () {
            var inputs = $('.g-candela-inputs-container').children().eq(1).children().children();
            return inputs.length > 3;
        }, 'the bar chart options to be available');

        runs(function () {
            var inputs = $('.g-candela-inputs-container').children().eq(1).children().children();
            expect(inputs.eq(0).find('label').text()).toBe('Width');
            expect(inputs.eq(1).find('label').text()).toBe('Height');
            expect(inputs.eq(2).find('label').text()).toBe('X');
            var values = [];
            for (var i = 0; i < 4; i += 1) {
                values.push(inputs.eq(2).find('option').eq(i).text());
            }
            values.sort();
            expect(values).toEqual(['(none)', 'a_b', 'c', 'id']);
            $('.g-candela-update-vis').click();
        });

        waitsFor(function () {
            return $('.g-candela-vis').find('canvas').length === 1;
        }, 'the vis canvas to be drawn');

        runs(function () {
            $('.g-item-candela-component').val('TreeHeatmap').change();
        });

        waitsFor(function () {
            var inputs = $('.g-candela-inputs-container').children().eq(1).children().children();
            return inputs.length === 9;
        }, 'the visualization type to change to TreeHeatmap');

        runs(function () {
            var inputs = $('.g-candela-inputs-container').children().eq(1).children().children();
            expect(inputs.eq(2).find('label').text()).toBe('Identifier column');
            expect(inputs.eq(3).find('label').text()).toBe('Color scale');
            expect(inputs.eq(3).find('option').eq(1).text()).toBe('row');
        });

        runs(function () {
            $('.g-item-candela-component').val('Histogram').change();
        });

        waitsFor(function () {
            var inputs = $('.g-candela-inputs-container').children().eq(1).children().children();
            return inputs.length > 3;
        }, 'the visualization type to change to BulletChart');

        runs(function () {
            var inputs = $('.g-candela-inputs-container').children().eq(1).children().children();
            expect(inputs.eq(2).find('label').text()).toBe('X');
        });
    });

    it('uploads a bad data file', function () {
        runs(function () {
            expect($('#g-user-action-menu.open').length).toBe(0);
            $('.g-user-text>a:first').click();
        });
        girderTest.waitForLoad();

        runs(function () {
            expect($('#g-user-action-menu.open').length).toBe(1);
            $('a.g-my-folders').click();
        });
        girderTest.waitForLoad();

        runs(function () {
            $('a.g-folder-list-link:last').click();
        });
        girderTest.waitForLoad();

        waitsFor(function () {
            return $('ol.breadcrumb>li.active').text() === 'Public' &&
                   $('.g-item-list-link').length === 1;
        }, 'descending into Public folder');

        girderTest.binaryUpload('clients/web/test/testFileBad.csv');

        runs(function () {
            $('.g-item-list-link').eq(1).click();
        });

        waitsFor(function () {
            return $('.alert-danger').length === 1;
        }, 'the error message to appear');

        runs(function () {
            expect($('.alert-danger').text()).toContain('An error occurred while attempting to read and parse the data file.');
        });
    });
});
<commit_msg>Resolve a race condition in the candela test<commit_after>girderTest.importPlugin('candela');
girderTest.startApp();

describe('Test the candela UI.', function () {
    it('register a user', girderTest.createUser(
        'johndoe', 'john.doe@email.com', 'John', 'Doe', 'password!'
    ));

    it('uploads the data file', function () {
        runs(function () {
            expect($('#g-user-action-menu.open').length).toBe(0);
            $('.g-user-text>a:first').click();
        });
        girderTest.waitForLoad();

        runs(function () {
            expect($('#g-user-action-menu.open').length).toBe(1);
            $('a.g-my-folders').click();
        });
        girderTest.waitForLoad();

        waitsFor(function () {
            return $('a.g-folder-list-link').length === 2;
        }, 'Public and Private folders to appear');

        runs(function () {
            $('a.g-folder-list-link:last').click();
        });
        girderTest.waitForLoad();

        waitsFor(function () {
            return $('ol.breadcrumb>li.active').text() === 'Public' &&
                   $('.g-empty-parent-message:visible').length === 1;
        }, 'descending into Public folder');

        girderTest.binaryUpload('clients/web/test/testFile.csv');

        runs(function () {
            $('.g-item-list-link:first').click();
        });
    });

    it('sets up candela inputs and renders the visualization', function () {
        waitsFor(function () {
            return $('.g-item-candela-component').length === 1;
        }, 'the candela component selector to appear');

        runs(function () {
            expect($('.g-item-candela-component option').length).toBeGreaterThan(10);
            $('.g-item-candela-component').val('BarChart').change();
        });

        waitsFor(function () {
            var inputs = $('.g-candela-inputs-container').children().eq(1).children().children();
            return inputs.length > 3;
        }, 'the bar chart options to be available');

        runs(function () {
            var inputs = $('.g-candela-inputs-container').children().eq(1).children().children();
            expect(inputs.eq(0).find('label').text()).toBe('Width');
            expect(inputs.eq(1).find('label').text()).toBe('Height');
            expect(inputs.eq(2).find('label').text()).toBe('X');
            var values = [];
            for (var i = 0; i < 4; i += 1) {
                values.push(inputs.eq(2).find('option').eq(i).text());
            }
            values.sort();
            expect(values).toEqual(['(none)', 'a_b', 'c', 'id']);
            $('.g-candela-update-vis').click();
        });

        waitsFor(function () {
            return $('.g-candela-vis').find('canvas').length === 1;
        }, 'the vis canvas to be drawn');

        runs(function () {
            $('.g-item-candela-component').val('TreeHeatmap').change();
        });

        waitsFor(function () {
            var inputs = $('.g-candela-inputs-container').children().eq(1).children().children();
            return inputs.length === 9;
        }, 'the visualization type to change to TreeHeatmap');

        runs(function () {
            var inputs = $('.g-candela-inputs-container').children().eq(1).children().children();
            expect(inputs.eq(2).find('label').text()).toBe('Identifier column');
            expect(inputs.eq(3).find('label').text()).toBe('Color scale');
            expect(inputs.eq(3).find('option').eq(1).text()).toBe('row');
        });

        runs(function () {
            $('.g-item-candela-component').val('Histogram').change();
        });

        waitsFor(function () {
            var inputs = $('.g-candela-inputs-container').children().eq(1).children().children();
            return inputs.length > 3;
        }, 'the visualization type to change to BulletChart');

        runs(function () {
            var inputs = $('.g-candela-inputs-container').children().eq(1).children().children();
            expect(inputs.eq(2).find('label').text()).toBe('X');
        });
    });

    it('uploads a bad data file', function () {
        runs(function () {
            expect($('#g-user-action-menu.open').length).toBe(0);
            $('.g-user-text>a:first').click();
        });
        girderTest.waitForLoad();

        runs(function () {
            expect($('#g-user-action-menu.open').length).toBe(1);
            $('a.g-my-folders').click();
        });
        girderTest.waitForLoad();

        runs(function () {
            $('a.g-folder-list-link:last').click();
        });
        girderTest.waitForLoad();

        waitsFor(function () {
            return $('ol.breadcrumb>li.active').text() === 'Public' &&
                   $('.g-item-list-link').length === 1;
        }, 'descending into Public folder');

        girderTest.binaryUpload('clients/web/test/testFileBad.csv');

        runs(function () {
            $('.g-item-list-link').eq(1).click();
        });

        waitsFor(function () {
            return $('.alert-danger').length === 1;
        }, 'the error message to appear');

        runs(function () {
            expect($('.alert-danger').text()).toContain('An error occurred while attempting to read and parse the data file.');
        });
    });
});
<|endoftext|>


<commit_before>/** @jsx jsx */

import React from 'react'; // eslint-disable-line no-unused-vars
import { jsx, Global } from '@emotion/core';
import { globalStyles, gridSize } from '@arch-ui/theme';
import { SkipNavContent } from '@reach/skip-nav';

import Layout from '../templates/layout';
import { Container } from '../components/Container';

export default () => (
  <Layout>
    {() => (
      <>
        <Global styles={globalStyles} />
        <NotFound />
      </>
    )}
  </Layout>
);

const layoutGutter = gridSize * 4;

const NotFound = () => (

    <SkipNavContent />
    <Container
      css={{
        paddingTop: layoutGutter,
        paddingBottom: layoutGutter,
      }}
    >

        Page Not Found

We couldn't find what you were looking for.

    </Container>

);
<commit_msg>[website] Add mq 404 h1 (#1240)<commit_after>/** @jsx jsx */

import React from 'react'; // eslint-disable-line no-unused-vars
import { jsx, Global } from '@emotion/core';
import { globalStyles, gridSize } from '@arch-ui/theme';
import { SkipNavContent } from '@reach/skip-nav';

import Layout from '../templates/layout';
import { Container } from '../components/Container';
import { mq } from '../utils/media';

export default () => (
  <Layout>
    {() => (
      <>
        <Global styles={globalStyles} />
        <NotFound />
      </>
    )}
  </Layout>
);

const layoutGutter = gridSize * 4;

const NotFound = () => (

    <SkipNavContent />
    <Container
      css={{
        paddingTop: layoutGutter,
        paddingBottom: layoutGutter,
      }}
    >

        Page Not Found

We couldn't find what you were looking for.

    </Container>

);
<|endoftext|>


<commit_before>angular.module('NgModelOptionsOnInvalid').directive('ngModelOptionsOnInvalid', function () {
  return {
    require: ['ngModel', '^form'],
    controller: function ($element, $scope, $attrs) {
      var
        ngModelOptionsController = $element.controller('ngModel'),
        formController = $element.controller('form');

      var unwatch = $scope.$watch(formController.$name + '.' + $attrs.name + '.$valid', function (newVal) {
        if (newVal === false) {

        // TODO: get the options from the attr

        // var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
        // $options = copy($scope.$eval($attrs.ngModelOptions))
        // $options.updateOn.replace(DEFAULT_REGEXP, function() {
        //   $options.updateOnDefault = true;
        //   return ' ';
        // })
        // ngModelOptionsController.$$setOptions($options);

          ngModelOptionsController.$$setOptions({
            updateOn: 'default'
          });

          unwatch();
        }
      });
    }
  };
});
<commit_msg>Initialize module<commit_after>angular.module('NgModelOptionsOnInvalid', []).directive('ngModelOptionsOnInvalid', function () {
  return {
    require: ['ngModel', '^form'],
    controller: function ($element, $scope, $attrs) {
      var
        ngModelOptionsController = $element.controller('ngModel'),
        formController = $element.controller('form');

      var unwatch = $scope.$watch(formController.$name + '.' + $attrs.name + '.$valid', function (newVal) {
        if (newVal === false) {

        // TODO: get the options from the attr

        // var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
        // $options = copy($scope.$eval($attrs.ngModelOptions))
        // $options.updateOn.replace(DEFAULT_REGEXP, function() {
        //   $options.updateOnDefault = true;
        //   return ' ';
        // })
        // ngModelOptionsController.$$setOptions($options);

          ngModelOptionsController.$$setOptions({
            updateOn: 'default'
          });

          unwatch();
        }
      });
    }
  };
});
<|endoftext|>


<commit_before>import React, { Component } from 'react';
import { Modal } from 'react-native';
import { Content, Card, CardItem, Button, Text, Body } from 'native-base';
import KnownLanguages from './knownLanguages';

// Temporary array with languages
const LANGUAGES = [{ name: 'Swedish', id: 1 }, { name: 'English', id: 2 },
  { name: 'Portuguese', id: 3 }, { name: 'Dutch', id: 4 },
  { name: 'Swahili', id: 5 }, { name: 'Norwegian', id: 6 },
  { name: 'German', id: 7 }, { name: 'Danish', id: 8 },
  { name: 'Finish', id: 9 }];

export default class LanguagePicker extends Component {

  // Constructor setting intitial state
  constructor(props) {
    super(props);
    this.state = {
      modalVisible: false,
      myLanguages: [],
    };
  }

  // Get a list with the name of all selected languages
  getMyLanguageNames() {
    return Array.from(
      LANGUAGES.filter(languageObject =>
        this.state.myLanguages.includes(languageObject.id),
      ),
      languageObject => languageObject.name);
  }

  // Display the modal
  setModalVisible(visible) {
    this.setState({
      modalVisible: visible,
      myLanguages: [1, 6],
    });
  }

  // Render the component
  render() {
    return (
      <Content>
        <Modal
          visible={this.state.modalVisible}
          animationType={'fade'}
          transparent
          onRequestClose={() => { this.setModalVisible(false); }}
        >
          <Card bordered="true">
            <CardItem bordered="false">
              <Body >
                <Button block onPress={() => this.setModalVisible(false)}>
                  <Text >Done</Text>
                </Button>
              </Body>
            </CardItem>
          </Card>
        </Modal>

        <Card bordered="true">
          <CardItem
            bordered="true"
            onPress={() => this.setModalVisible(true)}
          >
            <Body >
              <KnownLanguages languages={this.getMyLanguageNames()} />
            </Body>
          </CardItem>
        </Card>
      </Content>
    );
  }
}
<commit_msg>LanguagePicker modal displaying language list<commit_after>import React, { Component } from 'react';
import { Modal } from 'react-native';
import { Content, Card, CardItem, Button, Text, Body, List, Right, Radio } from 'native-base';
import KnownLanguages from './knownLanguages';

// Temporary array with languages
const LANGUAGES = [{ name: 'Swedish', id: 1 }, { name: 'English', id: 2 },
  { name: 'Portuguese', id: 3 }, { name: 'Dutch', id: 4 },
  { name: 'Swahili', id: 5 }, { name: 'Norwegian', id: 6 },
  { name: 'German', id: 7 }, { name: 'Danish', id: 8 },
  { name: 'Finish', id: 9 }];

export default class LanguagePicker extends Component {

  // Constructor setting intitial state
  constructor(props) {
    super(props);
    this.state = {
      modalVisible: false,
      myLanguages: [],
    };
  }

  // Get a list with the name of all selected languages
  getMyLanguageNames() {
    return Array.from(
      LANGUAGES.filter(languageObject =>
        this.state.myLanguages.includes(languageObject.id),
      ),
      languageObject => languageObject.name);
  }

  // Display the modal
  setModalVisible(visible) {
    this.setState({
      modalVisible: visible,
      myLanguages: [1, 6],
    });
  }

  // Add or remove language as a known language
  languageSelection(language) {
    alert(`Language: ${language.name}`);
  }

  // Render the component
  render() {
    return (
      <Content>
        <Modal
          visible={this.state.modalVisible}
          animationType={'fade'}
          transparent
          onRequestClose={() => { this.setModalVisible(false); }}
        >
          <Card bordered="true">
            <CardItem
              bordered="false"
              style={{ flex: 1, alignItems: 'flex-start' }}
            >
              <List
                dataArray={LANGUAGES}
                renderRow={rowData =>
                  <Card bordered="true">
                    <CardItem bordered="true" >
                      <Text>{rowData.name}</Text>
                      <Right>
                        <Radio
                          selected={this.state.myLanguages.includes(rowData.id)}
                          onPress={() => this.languageSelection(rowData)}
                        />
                      </Right>
                    </CardItem>
                  </Card>
                    }
              />
            </CardItem>
            <CardItem bordered="false">
              <Body >
                <Button block onPress={() => this.setModalVisible(false)}>
                  <Text >Done</Text>
                </Button>
              </Body>
            </CardItem>
          </Card>
        </Modal>

        <Card bordered="true">
          <CardItem
            bordered="true"
            onPress={() => this.setModalVisible(true)}
          >
            <Body >
              <KnownLanguages languages={this.getMyLanguageNames()} />
            </Body>
          </CardItem>
        </Card>
      </Content>
    );
  }
}
<|endoftext|>


<commit_before>/*
 * Paper.js
 *
 * This file is part of Paper.js, a JavaScript Vector Graphics Library,
 * based on Scriptographer.org and designed to be largely API compatible.
 * http://paperjs.org/
 * http://scriptographer.org/
 *
 * Distributed under the MIT license. See LICENSE file for details.
 *
 * Copyright (c) 2011, Juerg Lehni & Jonathan Puckey
 * http://lehni.org/ & http://jonathanpuckey.com/
 *
 * All rights reserved.
 */

var Document = this.Document = Base.extend({
	beans: true,

	initialize: function(canvas) {
		if (canvas && canvas instanceof HTMLCanvasElement) {
			this.canvas = canvas;
			this._size = Size.create(canvas.offsetWidth,
					canvas.offsetHeight);
		} else {
			this._size = Size.read(arguments) || new Size(1024, 768);
			this.canvas = CanvasProvider.getCanvas(this._size);
		}
		// TODO: currently we don't do anything with Document#bounds.. What
		// does it mean to change the bounds of the document? (JP)
		this.bounds = Rectangle.create(0, 0, this._size.width,
				this._size.height);
		this.context = this.canvas.getContext('2d');
		paper.documents.push(this);
		this.activate();
		this.layers = [];
		this.activeLayer = new Layer();
		this.setCurrentStyle(null);
		this.symbols = [];
		this.activeView = new DocumentView(this);
		this.views = [this.activeView];
		this._selectedItems = {};
		this._selectedItemCount = 0;
	},

	getSize: function() {
		return this._size;
	},

	setSize: function(size) {
		size = Size.read(arguments);
		if (this.canvas) {
			this.canvas.width = size.width;
			this.canvas.height = size.height;
		}
		this._size = size;
		this.bounds.setSize(size);
	},

	getCurrentStyle: function() {
		return this._currentStyle;
	},

	setCurrentStyle: function(style) {
		this._currentStyle = PathStyle.create(this, style);
	},

	activate: function() {
		var index = paper.documents.indexOf(this);
		if (index != -1) {
			paper.document = this;
			return true;
		}
		return false;
	},

	getSelectedItems: function() {
		// TODO: return groups if their children are all selected,
		// and filter out their children from the list.
		// TODO: the order of these items should be that of their
		// drawing order.
		var items = [];
		Base.each(this._selectedItems, function(item) {
			items.push(item);
		});
		return items;
	},

	// TODO: implement setSelectedItems?

	_selectItem: function(item, select) {
		if (select) {
			this._selectedItemCount++;
			this._selectedItems[item.getId()] = item;
		} else {
			this._selectedItemCount--;
			delete this._selectedItems[item.getId()];
		}
	},

	/**
	 * Selects all items in the document.
	 */
	selectAll: function() {
		// TODO: is using for var i in good practice?
		// or should we use Base.each? (JP)
		for (var i = 0, l = this.layers.length; i < l; i++)
			this.layers[i].setSelected(true);
	},

	/**
	 * Deselects all selected items in the document.
	 */
	deselectAll: function() {
		// TODO: is using for var i in good practice?
		// or should we use Base.each? (JP)
		for (var i in this._selectedItems)
			this._selectedItems[i].setSelected(false);
	},

	draw: function() {
		if (this.canvas) {
			var ctx = this.context;
			ctx.save();

			// TODO: Remove dirty rectangle test code once it's actually
			// implemented.
			var testDirtyRects = false;
			if (testDirtyRects) {
				var left = this.size.width / 8,
					top = this.size.height / 8;

				function clear(rect) {
					ctx.clearRect(rect.x, rect.y, rect.width, rect.height);

					if (true) {
						ctx.moveTo(rect.x, rect.y);
						ctx.lineTo(rect.x + rect.width, rect.y);
						ctx.lineTo(rect.x + rect.width, rect.y + rect.height);
						ctx.lineTo(rect.x, rect.y + rect.height);
					}
				}

				ctx.beginPath();

				clear(Rectangle.create(left, top, 2 * left, 2 * top));
				clear(Rectangle.create(3 * left, 3 * top, 2 * left, 2 * top));

//				clear(Rectangle.create(left, top, 4 * left, 4 * top));

				ctx.closePath();
				ctx.clip();
			} else {
				// Initial tests conclude that clearing the canvas using clearRect
				// is always faster than setting canvas.width = canvas.width
				// http://jsperf.com/clearrect-vs-setting-width/7
				ctx.clearRect(0, 0, this.size.width + 1, this.size.height + 1);
			}

			var param = { offset: new Point(0, 0) };
			for (var i = 0, l = this.layers.length; i < l; i++)
				Item.draw(this.layers[i], ctx, param);
			ctx.restore();

			// Draw the selection of the selected items in the document:
			if (this._selectedItemCount > 0) {
				ctx.save();
				ctx.strokeWidth = 1;
				// TODO: use Layer#color
				ctx.strokeStyle = ctx.fillStyle = '#4f7aff';
				param = { selection: true };
				Base.each(this._selectedItems, function(item) {
					item.draw(ctx, param);
				});
				ctx.restore();
			}
		}
	},

	redraw: function() {
		this.draw();
	}
});
<commit_msg>Document: when the canvas element that is passed to the constructor has a resize attribute, resize the document to the size of the window whenever the user resizes the window.<commit_after>/*
 * Paper.js
 *
 * This file is part of Paper.js, a JavaScript Vector Graphics Library,
 * based on Scriptographer.org and designed to be largely API compatible.
 * http://paperjs.org/
 * http://scriptographer.org/
 *
 * Distributed under the MIT license. See LICENSE file for details.
 *
 * Copyright (c) 2011, Juerg Lehni & Jonathan Puckey
 * http://lehni.org/ & http://jonathanpuckey.com/
 *
 * All rights reserved.
 */

var Document = this.Document = Base.extend({
	beans: true,

	initialize: function(canvas) {
		if (canvas && canvas instanceof HTMLCanvasElement) {
			this.canvas = canvas;
			if (canvas.getAttribute('resize')) {
				// If the canvas has a fullscreen attribute,
				// resize the canvas to fill the window and resize it again
				// whenever the user resizes the window.
				// TODO: set the following styles on the body tag:
				// body {
				//	background: black;
				//	margin: 0;
				//	overflow: hidden;
				// }
				this._size = Element.getScrollBounds().size;
				this.canvas.width = this._size.width;
				this.canvas.height = this._size.height;
				var that = this;
				Event.add(window, {
					resize: function(event) {
						that.setSize(Element.getScrollBounds().size);
						that.redraw();
					}
				});
			} else {
				this._size = Size.create(canvas.offsetWidth,
						canvas.offsetHeight);
			}
		} else {
			this._size = Size.read(arguments) || new Size(1024, 768);
			this.canvas = CanvasProvider.getCanvas(this._size);
		}
		// TODO: currently we don't do anything with Document#bounds.. What
		// does it mean to change the bounds of the document? (JP)
		this.bounds = Rectangle.create(0, 0, this._size.width,
				this._size.height);
		this.context = this.canvas.getContext('2d');
		paper.documents.push(this);
		this.activate();
		this.layers = [];
		this.activeLayer = new Layer();
		this.setCurrentStyle(null);
		this.symbols = [];
		this.activeView = new DocumentView(this);
		this.views = [this.activeView];
		this._selectedItems = {};
		this._selectedItemCount = 0;
	},

	getSize: function() {
		return this._size;
	},

	setSize: function(size) {
		size = Size.read(arguments);
		if (this.canvas) {
			this.canvas.width = size.width;
			this.canvas.height = size.height;
		}
		this._size = size;
		this.bounds.setSize(size);
	},

	getCurrentStyle: function() {
		return this._currentStyle;
	},

	setCurrentStyle: function(style) {
		this._currentStyle = PathStyle.create(this, style);
	},

	activate: function() {
		var index = paper.documents.indexOf(this);
		if (index != -1) {
			paper.document = this;
			return true;
		}
		return false;
	},

	getSelectedItems: function() {
		// TODO: return groups if their children are all selected,
		// and filter out their children from the list.
		// TODO: the order of these items should be that of their
		// drawing order.
		var items = [];
		Base.each(this._selectedItems, function(item) {
			items.push(item);
		});
		return items;
	},

	// TODO: implement setSelectedItems?

	_selectItem: function(item, select) {
		if (select) {
			this._selectedItemCount++;
			this._selectedItems[item.getId()] = item;
		} else {
			this._selectedItemCount--;
			delete this._selectedItems[item.getId()];
		}
	},

	/**
	 * Selects all items in the document.
	 */
	selectAll: function() {
		// TODO: is using for var i in good practice?
		// or should we use Base.each? (JP)
		for (var i = 0, l = this.layers.length; i < l; i++)
			this.layers[i].setSelected(true);
	},

	/**
	 * Deselects all selected items in the document.
	 */
	deselectAll: function() {
		// TODO: is using for var i in good practice?
		// or should we use Base.each? (JP)
		for (var i in this._selectedItems)
			this._selectedItems[i].setSelected(false);
	},

	draw: function() {
		if (this.canvas) {
			var ctx = this.context;
			ctx.save();

			// TODO: Remove dirty rectangle test code once it's actually
			// implemented.
			var testDirtyRects = false;
			if (testDirtyRects) {
				var left = this.size.width / 8,
					top = this.size.height / 8;

				function clear(rect) {
					ctx.clearRect(rect.x, rect.y, rect.width, rect.height);

					if (true) {
						ctx.moveTo(rect.x, rect.y);
						ctx.lineTo(rect.x + rect.width, rect.y);
						ctx.lineTo(rect.x + rect.width, rect.y + rect.height);
						ctx.lineTo(rect.x, rect.y + rect.height);
					}
				}

				ctx.beginPath();

				clear(Rectangle.create(left, top, 2 * left, 2 * top));
				clear(Rectangle.create(3 * left, 3 * top, 2 * left, 2 * top));

//				clear(Rectangle.create(left, top, 4 * left, 4 * top));

				ctx.closePath();
				ctx.clip();
			} else {
				// Initial tests conclude that clearing the canvas using clearRect
				// is always faster than setting canvas.width = canvas.width
				// http://jsperf.com/clearrect-vs-setting-width/7
				ctx.clearRect(0, 0, this.size.width + 1, this.size.height + 1);
			}

			var param = { offset: new Point(0, 0) };
			for (var i = 0, l = this.layers.length; i < l; i++)
				Item.draw(this.layers[i], ctx, param);
			ctx.restore();

			// Draw the selection of the selected items in the document:
			if (this._selectedItemCount > 0) {
				ctx.save();
				ctx.strokeWidth = 1;
				// TODO: use Layer#color
				ctx.strokeStyle = ctx.fillStyle = '#4f7aff';
				param = { selection: true };
				Base.each(this._selectedItems, function(item) {
					item.draw(ctx, param);
				});
				ctx.restore();
			}
		}
	},

	redraw: function() {
		this.draw();
	}
});
<|endoftext|>


<commit_before>(function() {
  angular.module("viewpoint.core", [
    'ngTouch',
    'ngSanitize',
    'ngMessages',
    'ngAria',
    'environment',
    'ngLodash',
    'ngAutodisable',
    'ui.router',
    'zttDesignLite'
  ]);
})();
<commit_msg>Inclui autocomplete como dependencia<commit_after>(function() {
  angular.module("viewpoint.core", [
    'ngTouch',
    'ngSanitize',
    'ngMessages',
    'ngAria',
    'angucomplete-alt',
    'environment',
    'ngLodash',
    'ngAutodisable',
    'ui.router',
    'zttDesignLite'
  ]);
})();
<|endoftext|>


<commit_before><commit_msg>促销、发货统计、详情页认证<commit_after><|endoftext|>


<commit_before>'use strict'

const C = require('../constants/constants')
const DistributedStateRegistry = require('../cluster/distributed-state-registry')
const SocketWrapper = require('../message/socket-wrapper')

class SubscriptionRegistry {

  /**
   * A generic mechanism to handle subscriptions from sockets to topics.
   * A bit like an event-hub, only that it registers SocketWrappers rather
   * than functions
   *
   * @constructor
   *
   * @param {Object} options deepstream options
   * @param {String} topic one of C.TOPIC
   * @param {[String]} clusterTopic A unique cluster topic, if not created uses format: topic_SUBSCRIPTIONS
   */
  constructor(options, topic, clusterTopic) {
    this._delayedBroadcasts = {}
    this._delay = -1
    if (options.broadcastTimeout !== undefined) {
      this._delay = options.broadcastTimeout
    }
    this._subscriptions = {}
    this._options = options
    this._topic = topic
    this._subscriptionListener = null
    this._unsubscribeAllFunctions = []
    this._constants = {
      MULTIPLE_SUBSCRIPTIONS: C.EVENT.MULTIPLE_SUBSCRIPTIONS,
      SUBSCRIBE: C.ACTIONS.SUBSCRIBE,
      UNSUBSCRIBE: C.ACTIONS.UNSUBSCRIBE,
      NOT_SUBSCRIBED: C.EVENT.NOT_SUBSCRIBED
    }

    this._setupRemoteComponents(clusterTopic)
  }

  /**
   * Setup all the remote components and actions required to deal with the subscription
   * via the cluster.
   */
  _setupRemoteComponents(clusterTopic) {
    this._clusterSubscriptions = new DistributedStateRegistry(clusterTopic || `${this._topic}_${C.TOPIC.SUBSCRIPTIONS}`, this._options)
    this._clusterSubscriptions.on('add', this._onClusterSubscriptionAdded.bind(this))
    this._clusterSubscriptions.on('remove', this._onClusterSubscriptionRemoved.bind(this))
  }

  /**
   * Return all the servers that have this subscription.
   *
   * @param  {String} subscriptionName the subscriptionName to look for
   *
   * @public
   * @return {Array}  An array of all the servernames with this subscription
   */
  getAllServers(subscriptionName) {
    return this._clusterSubscriptions.getAllServers(subscriptionName)
  }

  /**
   * Return all the servers that have this subscription excluding the current
   * server name
   *
   * @param  {String} subscriptionName the subscriptionName to look for
   *
   * @public
   * @return {Array}  An array of all the servernames with this subscription
   */
  getAllRemoteServers(subscriptionName) {
    const serverNames = this._clusterSubscriptions.getAllServers(subscriptionName)
    const localServerIndex = serverNames.indexOf(this._options.serverName)
    if (localServerIndex > -1) {
      serverNames.splice(serverNames.indexOf(this._options.serverName), 1)
    }
    return serverNames
  }

  /**
   * Returns a list of all the topic this registry
   * currently has subscribers for
   *
   * @public
   * @returns {Array} names
   */
  getNames() {
    return this._clusterSubscriptions.getAll()
  }

  /**
   * Returns true if the subscription exists somewhere
   * in the cluster
   *
   * @public
   * @returns {Array} names
   */
  hasName(subscriptionName) {
    return this._clusterSubscriptions.getAll().indexOf(subscriptionName) !== -1
  }

  /**
  * This method allows you to customise the SubscriptionRegistry so that it can send custom events and ack messages back.
  * For example, when using the C.ACTIONS.LISTEN, you would override SUBSCRIBE with C.ACTIONS.SUBSCRIBE and UNSUBSCRIBE with UNSUBSCRIBE
  *
  * @param {string} name The name of the the variable to override. This can be either MULTIPLE_SUBSCRIPTIONS, SUBSCRIBE, UNSUBSCRIBE, NOT_SUBSCRIBED
  * @param {string} value The value to override with.
  *
  * @public
  * @returns {void}
  */
  setAction(name, value) {
    this._constants[name.toUpperCase()] = value
  }

  /**
   * Broadcasts the enqueued messages for the timed out subscription room.
   *
   * @param   {Object} delayedBroadcasts holds information of what messages to send and where
   *
   * @public
   * @returns {void}
   */
  onBroadcastTimeout(delayedBroadcasts) {
    const sockets = this._subscriptions[delayedBroadcasts.name]
    if (sockets) {
      // sort vector of unique senders by uuid. doing so in combination with
      // the sorting of the sockets in this subscription name means we can
      // simplify comparisons
      delayedBroadcasts.uniqueSendersVector.sort((a, b) => {
        if (a.sender < b.sender) {
          return -1
        } else if (a.sender > b.sender) {
          return 1
        }
        return 0
      })

      // for all unique senders and their gaps, build their special messages
      for (const uniqueSender of delayedBroadcasts.uniqueSendersVector) {
        uniqueSender.message = delayedBroadcasts.sharedMessages
          .substring(0, uniqueSender.gaps[0].start)
        let lastStop = uniqueSender.gaps[0].stop
        for (let j = 1; j < uniqueSender.gaps.length; j++) {
          uniqueSender.message += delayedBroadcasts.sharedMessages
            .substring(lastStop, uniqueSender.gaps[j].start)
          lastStop = uniqueSender.gaps[j].stop
        }
        uniqueSender.message += delayedBroadcasts.sharedMessages
          .substring(lastStop, delayedBroadcasts.sharedMessages.length)
      }

      // for all sockets in this subscription name, send either sharedMessage or this socket's
      // specialized message. only sockets that sent something will have a special message, all
      // other sockets are only listeners and receive the exact same (sharedMessage) message.
      const preparedMessage = SocketWrapper.prepareMessage(delayedBroadcasts.sharedMessages)
      let j = 0
      for (const socket of sockets) {
        // since both uniqueSendersVector and sockets are sorted by uuid, we can efficiently determine
        // if this socket is a sender in this subscription name or not as well as look up the eventual
        // specialized message for this socket.
        if (j < delayedBroadcasts.uniqueSendersVector.length &&
          delayedBroadcasts.uniqueSendersVector[j].sender === socket.uuid) {
          if (delayedBroadcasts.uniqueSendersVector[j].message.length) {
            socket.sendNative(delayedBroadcasts.uniqueSendersVector[j].message)
          }
          j++
        } else {
          // since we know when a socket is a sender and when it is a listener we can use the optimized prepared
          // message for listeners
          socket.sendPrepared(preparedMessage)
        }
      }
      SocketWrapper.finalizeMessage(preparedMessage)
    }

    // delete this delayed broadcast
    delete this._delayedBroadcasts[delayedBroadcasts.name]
  }

  /**
   * Enqueues a message string to be broadcast to all subscribers. Broadcasts will potentially
   * be reordered in relation to *other* subscription names, but never in relation to the same
   * subscription name. Each broadcast is given 'broadcastTimeout' ms to coalesce into one big
   * broadcast.
   *
   * @param   {String} name      the name/topic the subscriber was previously registered for
   * @param   {String} msgString the message as string
   * @param   {[SocketWrapper]} sender an optional socketWrapper that shouldn't receive the message
   *
   * @public
   * @returns {void}
   */
  sendToSubscribers(name, msgString, sender) {
    if (!this._subscriptions[name]) {
      return
    }

    // not all messages are valid, this should be fixed elsewhere!
    if (msgString.charAt(msgString.length - 1) !== C.MESSAGE_SEPERATOR) {
      msgString += C.MESSAGE_SEPERATOR
    }

    // if not already a delayed broadcast, create it
    let delayedBroadcasts = this._delayedBroadcasts[name]
    if (delayedBroadcasts === undefined) {
      this._delayedBroadcasts[name] = delayedBroadcasts = {
        uniqueSendersVector: [],
        uniqueSendersMap: {},
        timer: null,
        name,
        sharedMessages: ''
      }
    }

    // append this message to the sharedMessage, the message that
    // is shared in the broadcast to every listener-only
    const start = delayedBroadcasts.sharedMessages.length
    delayedBroadcasts.sharedMessages += msgString
    const stop = delayedBroadcasts.sharedMessages.length

    // uniqueSendersMap maps from uuid to offset in uniqueSendersVector
    // each uniqueSender has a vector of "gaps" in relation to sharedMessage
    // senders should not receive what they sent themselves, so a gap is inserted
    // for every send from this sender
    let pos
    if (sender && sender.uuid !== undefined) {
      pos = delayedBroadcasts.uniqueSendersMap[sender.uuid]
      if (pos !== undefined) {
        delayedBroadcasts.uniqueSendersVector[pos].gaps.push({ start, stop })
      } else {
        pos = delayedBroadcasts.uniqueSendersVector.length
        delayedBroadcasts.uniqueSendersMap[sender.uuid] = pos
        delayedBroadcasts.uniqueSendersVector[pos] = {
          sender: sender.uuid,
          message: null,
          gaps: [{ start, stop }]
        }
      }
    }

    // reuse the same timer if already started
    if (!delayedBroadcasts.timer) {
      if (this._delay !== -1) {
        delayedBroadcasts.timer = setTimeout(this.onBroadcastTimeout.bind(this), this._delay, delayedBroadcasts)
      } else {
        this.onBroadcastTimeout(delayedBroadcasts)
      }
    }
  }

  /**
   * Adds a SocketWrapper as a subscriber to a topic
   *
   * @param   {String} name
   * @param   {SocketWrapper} socketWrapper
   *
   * @public
   * @returns {void}
   */
  subscribe(name, socketWrapper) {
    if (this._subscriptions[name] === undefined) {
      this._subscriptions[name] = []
    }

    if (this._subscriptions[name].indexOf(socketWrapper) !== -1) {
      const msg = `repeat supscription to "${name}" by ${socketWrapper.user}`
      this._options.logger.log(C.LOG_LEVEL.WARN, this._constants.MULTIPLE_SUBSCRIPTIONS, msg)
      socketWrapper.sendError(this._topic, this._constants.MULTIPLE_SUBSCRIPTIONS, name)
      return
    }

    if (!this.isLocalSubscriber(socketWrapper)) {
      const unsubscribeAllFn = this.unsubscribeAll.bind(this, socketWrapper)
      this._unsubscribeAllFunctions.push({
        socketWrapper,
        fn: unsubscribeAllFn
      })
      socketWrapper.once('close', unsubscribeAllFn)
    }

    // insert socket in vector, sorted by uuid
    const sorted = this._subscriptions[name]
    let index = 0
    for (; index < sorted.length; index++) {
      if (sorted[index].uuid > socketWrapper.uuid) {
        break
      }
    }
    sorted.splice(index, 0, socketWrapper)

    if (this._subscriptionListener) {
      this._subscriptionListener.onSubscriptionMade(
        name,
        socketWrapper,
        this._subscriptions[name].length
      )
    }

    this._clusterSubscriptions.add(name)

    const logMsg = `for ${this._topic}:${name} by ${socketWrapper.user}`
    this._options.logger.log(C.LOG_LEVEL.DEBUG, this._constants.SUBSCRIBE, logMsg)
    socketWrapper.sendMessage(this._topic, C.ACTIONS.ACK, [this._constants.SUBSCRIBE, name])
  }

  /**
   * Removes a SocketWrapper from the list of subscriptions for a topic
   *
   * @param   {String} name
   * @param   {SocketWrapper} socketWrapper
   * @param   {Boolean} silent supresses logs and unsubscribe ACK messages
   *
   * @public
   * @returns {void}
   */
  unsubscribe(name, socketWrapper, silent) {
    let msg
    let i

    if (this._subscriptions[name] === undefined ||
      this._subscriptions[name].indexOf(socketWrapper) === -1) {
      msg = `${socketWrapper.user} is not subscribed to ${name}`
      this._options.logger.log(C.LOG_LEVEL.WARN, this._constants.NOT_SUBSCRIBED, msg)
      socketWrapper.sendError(this._topic, this._constants.NOT_SUBSCRIBED, name)
      return
    }

    if (this._subscriptions[name].length === 1) {
      this._clusterSubscriptions.remove(name)
      delete this._subscriptions[name]
    } else {
      this._subscriptions[name].splice(this._subscriptions[name].indexOf(socketWrapper), 1)
    }

    if (this._subscriptionListener) {
      const allServerNames = this._clusterSubscriptions.getAllServers(name)
      const indexOfCurrentNode = allServerNames.indexOf(this._options.serverName)
      if (indexOfCurrentNode > -1) {
        allServerNames.splice(indexOfCurrentNode, 1)
      }
      this._subscriptionListener.onSubscriptionRemoved(
        name,
        socketWrapper,
        this._subscriptions[name] ? this._subscriptions[name].length : 0,
        allServerNames.length
      )
    }

    if (!silent) {
      const logMsg = `for ${this._topic}:${name} by ${socketWrapper.user}`
      this._options.logger.log(C.LOG_LEVEL.DEBUG, this._constants.UNSUBSCRIBE, logMsg)
      socketWrapper.sendMessage(this._topic, C.ACTIONS.ACK, [this._constants.UNSUBSCRIBE, name])
    }

    if (!this.isLocalSubscriber(socketWrapper)) {
      for (i = 0; i < this._unsubscribeAllFunctions.length; i++) {
        if (this._unsubscribeAllFunctions[i].socketWrapper === socketWrapper) {
          socketWrapper.removeListener('close', this._unsubscribeAllFunctions[i].fn)
          this._unsubscribeAllFunctions.splice(i, 1)
        }
      }
    }
  }

  /**
   * Removes the SocketWrapper from all subscriptions. This is also called
   * when the socket closes
   *
   * @param   {SocketWrapper} socketWrapper
   *
   * @public
   * @returns {void}
   */
  unsubscribeAll(socketWrapper) {
    let name
    let index

    for (name in this._subscriptions) {
      index = this._subscriptions[name].indexOf(socketWrapper)

      if (index !== -1) {
        this.unsubscribe(name, socketWrapper)
      }
    }
  }

  /**
   * Returns true if socketWrapper is subscribed to any of the events in
   * this registry. This is useful to bind events on close only once
   *
   * @param {SocketWrapper} socketWrapper
   *
   * @public
   * @returns {Boolean} isLocalSubscriber
   */
  isLocalSubscriber(socketWrapper) {
    for (const name in this._subscriptions) {
      if (this._subscriptions[name].indexOf(socketWrapper) !== -1) {
        return true
      }
    }

    return false
  }

  /**
   * Returns an array of SocketWrappers that are subscribed
   * to <name> or null if there are no subscribers
   *
   * @param   {String} name
   *
   * @public
   * @returns {Array} SocketWrapper[]
   */
  getLocalSubscribers(name) {
    return this._subscriptions[name] || []
  }

  /**
   * Returns a random SocketWrapper out of the array
   * of SocketWrappers that are subscribed to <name>
   *
   * @param   {String} name
   *
   * @public
   * @returns {SocketWrapper}
   */
  getRandomLocalSubscriber(name) {
    const subscribers = this.getLocalSubscribers(name)

    if (subscribers.length > 0) {
      return subscribers[Math.floor(Math.random() * subscribers.length)]
    }
    return null
  }

  /**
   * Returns true if there are SocketWrappers that
   * are subscribed to <name> or false if there
   * aren't any subscribers
   *
   * @param   {String}  name
   *
   * @public
   * @returns {Boolean} hasLocalSubscribers
   */
  hasLocalSubscribers(name) {
    const subscriptions = this._subscriptions[name]
    return !!subscriptions && subscriptions.length !== 0
  }

  /**
   * Allows to set a subscriptionListener after the class had been instantiated
   *
   * @param {SubscriptionListener} subscriptionListener - a class exposing a onSubscriptionMade and onSubscriptionRemoved method
   *
   * @public
   * @returns {void}
   */
  setSubscriptionListener(subscriptionListener) {
    this._subscriptionListener = subscriptionListener
  }

  /**
   * Called when a subscription has been added to the cluster
   * This can be invoked locally or remotely, so we check if it
   * is a local invocation and ignore it if so in favour of the
   * call done from subscribe
   * @param  {String} name the name that was added
   */
  _onClusterSubscriptionAdded(name) {
    if (this._subscriptionListener && !this._subscriptions[name]) {
      this._subscriptionListener.onSubscriptionMade(name, null, 1)
    }
  }

  /**
   * Called when a subscription has been removed from the cluster
   * This can be invoked locally or remotely, so we check if it
   * is a local invocation and ignore it if so in favour of the
   * call done from unsubscribe
   * @param  {String} name the name that was removed
   */
  _onClusterSubscriptionRemoved(name) {
    if (this._subscriptionListener && !this._subscriptions[name]) {
      this._subscriptionListener.onSubscriptionRemoved(name, null, 0, 0)
    }
  }

}

module.exports = SubscriptionRegistry
<commit_msg>refactor(subscription-registry): making add/remove state registry calls symmetrical<commit_after>'use strict'

const C = require('../constants/constants')
const DistributedStateRegistry = require('../cluster/distributed-state-registry')
const SocketWrapper = require('../message/socket-wrapper')

class SubscriptionRegistry {

  /**
   * A generic mechanism to handle subscriptions from sockets to topics.
   * A bit like an event-hub, only that it registers SocketWrappers rather
   * than functions
   *
   * @constructor
   *
   * @param {Object} options deepstream options
   * @param {String} topic one of C.TOPIC
   * @param {[String]} clusterTopic A unique cluster topic, if not created uses format: topic_SUBSCRIPTIONS
   */
  constructor(options, topic, clusterTopic) {
    this._delayedBroadcasts = {}
    this._delay = -1
    if (options.broadcastTimeout !== undefined) {
      this._delay = options.broadcastTimeout
    }
    this._subscriptions = {}
    this._options = options
    this._topic = topic
    this._subscriptionListener = null
    this._unsubscribeAllFunctions = []
    this._constants = {
      MULTIPLE_SUBSCRIPTIONS: C.EVENT.MULTIPLE_SUBSCRIPTIONS,
      SUBSCRIBE: C.ACTIONS.SUBSCRIBE,
      UNSUBSCRIBE: C.ACTIONS.UNSUBSCRIBE,
      NOT_SUBSCRIBED: C.EVENT.NOT_SUBSCRIBED
    }

    this._setupRemoteComponents(clusterTopic)
  }

  /**
   * Setup all the remote components and actions required to deal with the subscription
   * via the cluster.
   */
  _setupRemoteComponents(clusterTopic) {
    this._clusterSubscriptions = new DistributedStateRegistry(clusterTopic || `${this._topic}_${C.TOPIC.SUBSCRIPTIONS}`, this._options)
    this._clusterSubscriptions.on('add', this._onClusterSubscriptionAdded.bind(this))
    this._clusterSubscriptions.on('remove', this._onClusterSubscriptionRemoved.bind(this))
  }

  /**
   * Return all the servers that have this subscription.
   *
   * @param  {String} subscriptionName the subscriptionName to look for
   *
   * @public
   * @return {Array}  An array of all the servernames with this subscription
   */
  getAllServers(subscriptionName) {
    return this._clusterSubscriptions.getAllServers(subscriptionName)
  }

  /**
   * Return all the servers that have this subscription excluding the current
   * server name
   *
   * @param  {String} subscriptionName the subscriptionName to look for
   *
   * @public
   * @return {Array}  An array of all the servernames with this subscription
   */
  getAllRemoteServers(subscriptionName) {
    const serverNames = this._clusterSubscriptions.getAllServers(subscriptionName)
    const localServerIndex = serverNames.indexOf(this._options.serverName)
    if (localServerIndex > -1) {
      serverNames.splice(serverNames.indexOf(this._options.serverName), 1)
    }
    return serverNames
  }

  /**
   * Returns a list of all the topic this registry
   * currently has subscribers for
   *
   * @public
   * @returns {Array} names
   */
  getNames() {
    return this._clusterSubscriptions.getAll()
  }

  /**
   * Returns true if the subscription exists somewhere
   * in the cluster
   *
   * @public
   * @returns {Array} names
   */
  hasName(subscriptionName) {
    return this._clusterSubscriptions.getAll().indexOf(subscriptionName) !== -1
  }

  /**
  * This method allows you to customise the SubscriptionRegistry so that it can send custom events and ack messages back.
  * For example, when using the C.ACTIONS.LISTEN, you would override SUBSCRIBE with C.ACTIONS.SUBSCRIBE and UNSUBSCRIBE with UNSUBSCRIBE
  *
  * @param {string} name The name of the the variable to override. This can be either MULTIPLE_SUBSCRIPTIONS, SUBSCRIBE, UNSUBSCRIBE, NOT_SUBSCRIBED
  * @param {string} value The value to override with.
  *
  * @public
  * @returns {void}
  */
  setAction(name, value) {
    this._constants[name.toUpperCase()] = value
  }

  /**
   * Broadcasts the enqueued messages for the timed out subscription room.
   *
   * @param   {Object} delayedBroadcasts holds information of what messages to send and where
   *
   * @public
   * @returns {void}
   */
  onBroadcastTimeout(delayedBroadcasts) {
    const sockets = this._subscriptions[delayedBroadcasts.name]
    if (sockets) {
      // sort vector of unique senders by uuid. doing so in combination with
      // the sorting of the sockets in this subscription name means we can
      // simplify comparisons
      delayedBroadcasts.uniqueSendersVector.sort((a, b) => {
        if (a.sender < b.sender) {
          return -1
        } else if (a.sender > b.sender) {
          return 1
        }
        return 0
      })

      // for all unique senders and their gaps, build their special messages
      for (const uniqueSender of delayedBroadcasts.uniqueSendersVector) {
        uniqueSender.message = delayedBroadcasts.sharedMessages
          .substring(0, uniqueSender.gaps[0].start)
        let lastStop = uniqueSender.gaps[0].stop
        for (let j = 1; j < uniqueSender.gaps.length; j++) {
          uniqueSender.message += delayedBroadcasts.sharedMessages
            .substring(lastStop, uniqueSender.gaps[j].start)
          lastStop = uniqueSender.gaps[j].stop
        }
        uniqueSender.message += delayedBroadcasts.sharedMessages
          .substring(lastStop, delayedBroadcasts.sharedMessages.length)
      }

      // for all sockets in this subscription name, send either sharedMessage or this socket's
      // specialized message. only sockets that sent something will have a special message, all
      // other sockets are only listeners and receive the exact same (sharedMessage) message.
      const preparedMessage = SocketWrapper.prepareMessage(delayedBroadcasts.sharedMessages)
      let j = 0
      for (const socket of sockets) {
        // since both uniqueSendersVector and sockets are sorted by uuid, we can efficiently determine
        // if this socket is a sender in this subscription name or not as well as look up the eventual
        // specialized message for this socket.
        if (j < delayedBroadcasts.uniqueSendersVector.length &&
          delayedBroadcasts.uniqueSendersVector[j].sender === socket.uuid) {
          if (delayedBroadcasts.uniqueSendersVector[j].message.length) {
            socket.sendNative(delayedBroadcasts.uniqueSendersVector[j].message)
          }
          j++
        } else {
          // since we know when a socket is a sender and when it is a listener we can use the optimized prepared
          // message for listeners
          socket.sendPrepared(preparedMessage)
        }
      }
      SocketWrapper.finalizeMessage(preparedMessage)
    }

    // delete this delayed broadcast
    delete this._delayedBroadcasts[delayedBroadcasts.name]
  }

  /**
   * Enqueues a message string to be broadcast to all subscribers. Broadcasts will potentially
   * be reordered in relation to *other* subscription names, but never in relation to the same
   * subscription name. Each broadcast is given 'broadcastTimeout' ms to coalesce into one big
   * broadcast.
   *
   * @param   {String} name      the name/topic the subscriber was previously registered for
   * @param   {String} msgString the message as string
   * @param   {[SocketWrapper]} sender an optional socketWrapper that shouldn't receive the message
   *
   * @public
   * @returns {void}
   */
  sendToSubscribers(name, msgString, sender) {
    if (!this._subscriptions[name]) {
      return
    }

    // not all messages are valid, this should be fixed elsewhere!
    if (msgString.charAt(msgString.length - 1) !== C.MESSAGE_SEPERATOR) {
      msgString += C.MESSAGE_SEPERATOR
    }

    // if not already a delayed broadcast, create it
    let delayedBroadcasts = this._delayedBroadcasts[name]
    if (delayedBroadcasts === undefined) {
      this._delayedBroadcasts[name] = delayedBroadcasts = {
        uniqueSendersVector: [],
        uniqueSendersMap: {},
        timer: null,
        name,
        sharedMessages: ''
      }
    }

    // append this message to the sharedMessage, the message that
    // is shared in the broadcast to every listener-only
    const start = delayedBroadcasts.sharedMessages.length
    delayedBroadcasts.sharedMessages += msgString
    const stop = delayedBroadcasts.sharedMessages.length

    // uniqueSendersMap maps from uuid to offset in uniqueSendersVector
    // each uniqueSender has a vector of "gaps" in relation to sharedMessage
    // senders should not receive what they sent themselves, so a gap is inserted
    // for every send from this sender
    let pos
    if (sender && sender.uuid !== undefined) {
      pos = delayedBroadcasts.uniqueSendersMap[sender.uuid]
      if (pos !== undefined) {
        delayedBroadcasts.uniqueSendersVector[pos].gaps.push({ start, stop })
      } else {
        pos = delayedBroadcasts.uniqueSendersVector.length
        delayedBroadcasts.uniqueSendersMap[sender.uuid] = pos
        delayedBroadcasts.uniqueSendersVector[pos] = {
          sender: sender.uuid,
          message: null,
          gaps: [{ start, stop }]
        }
      }
    }

    // reuse the same timer if already started
    if (!delayedBroadcasts.timer) {
      if (this._delay !== -1) {
        delayedBroadcasts.timer = setTimeout(this.onBroadcastTimeout.bind(this), this._delay, delayedBroadcasts)
      } else {
        this.onBroadcastTimeout(delayedBroadcasts)
      }
    }
  }

  /**
   * Adds a SocketWrapper as a subscriber to a topic
   *
   * @param   {String} name
   * @param   {SocketWrapper} socketWrapper
   *
   * @public
   * @returns {void}
   */
  subscribe(name, socketWrapper) {
    if (this._subscriptions[name] === undefined) {
      this._subscriptions[name] = []
    }

    if (this._subscriptions[name].indexOf(socketWrapper) !== -1) {
      const msg = `repeat supscription to "${name}" by ${socketWrapper.user}`
      this._options.logger.log(C.LOG_LEVEL.WARN, this._constants.MULTIPLE_SUBSCRIPTIONS, msg)
      socketWrapper.sendError(this._topic, this._constants.MULTIPLE_SUBSCRIPTIONS, name)
      return
    }

    if (!this.isLocalSubscriber(socketWrapper)) {
      const unsubscribeAllFn = this.unsubscribeAll.bind(this, socketWrapper)
      this._unsubscribeAllFunctions.push({
        socketWrapper,
        fn: unsubscribeAllFn
      })
      socketWrapper.once('close', unsubscribeAllFn)
    }

    // insert socket in vector, sorted by uuid
    const sorted = this._subscriptions[name]
    let index = 0
    for (; index < sorted.length; index++) {
      if (sorted[index].uuid > socketWrapper.uuid) {
        break
      }
    }
    sorted.splice(index, 0, socketWrapper)

    if (this._subscriptionListener) {
      this._subscriptionListener.onSubscriptionMade(
        name,
        socketWrapper,
        this._subscriptions[name].length
      )
    }

    this._clusterSubscriptions.add(name)

    const logMsg = `for ${this._topic}:${name} by ${socketWrapper.user}`
    this._options.logger.log(C.LOG_LEVEL.DEBUG, this._constants.SUBSCRIBE, logMsg)
    socketWrapper.sendMessage(this._topic, C.ACTIONS.ACK, [this._constants.SUBSCRIBE, name])
  }

  /**
   * Removes a SocketWrapper from the list of subscriptions for a topic
   *
   * @param   {String} name
   * @param   {SocketWrapper} socketWrapper
   * @param   {Boolean} silent supresses logs and unsubscribe ACK messages
   *
   * @public
   * @returns {void}
   */
  unsubscribe(name, socketWrapper, silent) {
    let msg
    let i

    if (this._subscriptions[name] === undefined ||
      this._subscriptions[name].indexOf(socketWrapper) === -1) {
      msg = `${socketWrapper.user} is not subscribed to ${name}`
      this._options.logger.log(C.LOG_LEVEL.WARN, this._constants.NOT_SUBSCRIBED, msg)
      socketWrapper.sendError(this._topic, this._constants.NOT_SUBSCRIBED, name)
      return
    }

    this._clusterSubscriptions.remove(name)

    if (this._subscriptions[name].length === 1) {
      delete this._subscriptions[name]
    } else {
      this._subscriptions[name].splice(this._subscriptions[name].indexOf(socketWrapper), 1)
    }

    if (this._subscriptionListener) {
      const allServerNames = this._clusterSubscriptions.getAllServers(name)
      const indexOfCurrentNode = allServerNames.indexOf(this._options.serverName)
      if (indexOfCurrentNode > -1) {
        allServerNames.splice(indexOfCurrentNode, 1)
      }
      this._subscriptionListener.onSubscriptionRemoved(
        name,
        socketWrapper,
        this._subscriptions[name] ? this._subscriptions[name].length : 0,
        allServerNames.length
      )
    }

    if (!silent) {
      const logMsg = `for ${this._topic}:${name} by ${socketWrapper.user}`
      this._options.logger.log(C.LOG_LEVEL.DEBUG, this._constants.UNSUBSCRIBE, logMsg)
      socketWrapper.sendMessage(this._topic, C.ACTIONS.ACK, [this._constants.UNSUBSCRIBE, name])
    }

    if (!this.isLocalSubscriber(socketWrapper)) {
      for (i = 0; i < this._unsubscribeAllFunctions.length; i++) {
        if (this._unsubscribeAllFunctions[i].socketWrapper === socketWrapper) {
          socketWrapper.removeListener('close', this._unsubscribeAllFunctions[i].fn)
          this._unsubscribeAllFunctions.splice(i, 1)
        }
      }
    }
  }

  /**
   * Removes the SocketWrapper from all subscriptions. This is also called
   * when the socket closes
   *
   * @param   {SocketWrapper} socketWrapper
   *
   * @public
   * @returns {void}
   */
  unsubscribeAll(socketWrapper) {
    let name
    let index

    for (name in this._subscriptions) {
      index = this._subscriptions[name].indexOf(socketWrapper)

      if (index !== -1) {
        this.unsubscribe(name, socketWrapper)
      }
    }
  }

  /**
   * Returns true if socketWrapper is subscribed to any of the events in
   * this registry. This is useful to bind events on close only once
   *
   * @param {SocketWrapper} socketWrapper
   *
   * @public
   * @returns {Boolean} isLocalSubscriber
   */
  isLocalSubscriber(socketWrapper) {
    for (const name in this._subscriptions) {
      if (this._subscriptions[name].indexOf(socketWrapper) !== -1) {
        return true
      }
    }

    return false
  }

  /**
   * Returns an array of SocketWrappers that are subscribed
   * to <name> or null if there are no subscribers
   *
   * @param   {String} name
   *
   * @public
   * @returns {Array} SocketWrapper[]
   */
  getLocalSubscribers(name) {
    return this._subscriptions[name] || []
  }

  /**
   * Returns a random SocketWrapper out of the array
   * of SocketWrappers that are subscribed to <name>
   *
   * @param   {String} name
   *
   * @public
   * @returns {SocketWrapper}
   */
  getRandomLocalSubscriber(name) {
    const subscribers = this.getLocalSubscribers(name)

    if (subscribers.length > 0) {
      return subscribers[Math.floor(Math.random() * subscribers.length)]
    }
    return null
  }

  /**
   * Returns true if there are SocketWrappers that
   * are subscribed to <name> or false if there
   * aren't any subscribers
   *
   * @param   {String}  name
   *
   * @public
   * @returns {Boolean} hasLocalSubscribers
   */
  hasLocalSubscribers(name) {
    const subscriptions = this._subscriptions[name]
    return !!subscriptions && subscriptions.length !== 0
  }

  /**
   * Allows to set a subscriptionListener after the class had been instantiated
   *
   * @param {SubscriptionListener} subscriptionListener - a class exposing a onSubscriptionMade and onSubscriptionRemoved method
   *
   * @public
   * @returns {void}
   */
  setSubscriptionListener(subscriptionListener) {
    this._subscriptionListener = subscriptionListener
  }

  /**
   * Called when a subscription has been added to the cluster
   * This can be invoked locally or remotely, so we check if it
   * is a local invocation and ignore it if so in favour of the
   * call done from subscribe
   * @param  {String} name the name that was added
   */
  _onClusterSubscriptionAdded(name) {
    if (this._subscriptionListener && !this._subscriptions[name]) {
      this._subscriptionListener.onSubscriptionMade(name, null, 1)
    }
  }

  /**
   * Called when a subscription has been removed from the cluster
   * This can be invoked locally or remotely, so we check if it
   * is a local invocation and ignore it if so in favour of the
   * call done from unsubscribe
   * @param  {String} name the name that was removed
   */
  _onClusterSubscriptionRemoved(name) {
    if (this._subscriptionListener && !this._subscriptions[name]) {
      this._subscriptionListener.onSubscriptionRemoved(name, null, 0, 0)
    }
  }

}

module.exports = SubscriptionRegistry
<|endoftext|>


<commit_before>import * from "../actions/actions";

const initialState = {
  focusedStory: undefined,
  stories: []
}

function updateMapState (state = initialState, action) {
  switch (action.type) {
    case SET_FOCUSED_STORY:
      return Object.assign({}, state, {
        focusedStory: action.storyId;
      });
    case SET_STORIES:
      return Object.assign({}, state, {
        stories: action.stories;
      });
    default:
      return state;
  }
}
<commit_msg>export the reducer<commit_after>import * from "../actions/actions";

const initialState = {
  focusedStory: undefined,
  stories: []
}

function updateMapState (state = initialState, action) {
  switch (action.type) {
    case SET_FOCUSED_STORY:
      return Object.assign({}, state, {
        focusedStory: action.storyId;
      });
    case SET_STORIES:
      return Object.assign({}, state, {
        stories: action.stories;
      });
    default:
      return state;
  }
}

export default updateMapState;
<|endoftext|>


<commit_before>// Copyright 2019 Google LLC
//
// 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
//
//     https://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.
//

'use strict';

module.exports = {
  opts: {
    readme: './README.md',
    package: './package.json',
    template: './node_modules/jsdoc-fresh',
    recurse: true,
    verbose: true,
    destination: './docs/'
  },
  plugins: [
    'plugins/markdown',
    'jsdoc-region-tag'
  ],
  source: {
    excludePattern: '(^|\\/|\\\\)[._]',
    include: [
      'build/src'
    ],
    includePattern: '\\.js$'
  },
  templates: {
    copyright: 'Copyright 2018 Google, LLC.',
    includeDate: false,
    sourceFiles: false,
    systemName: '@google-cloud/recommender',
    theme: 'lumen'
  },
  markdown: {
    idInHeadings: true
  }
};
<commit_msg>chore: update jsdoc.js (#32)<commit_after>// Copyright 2019 Google LLC
//
// 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
//
//     https://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.
//

'use strict';

module.exports = {
  opts: {
    readme: './README.md',
    package: './package.json',
    template: './node_modules/jsdoc-fresh',
    recurse: true,
    verbose: true,
    destination: './docs/'
  },
  plugins: [
    'plugins/markdown',
    'jsdoc-region-tag'
  ],
  source: {
    excludePattern: '(^|\\/|\\\\)[._]',
    include: [
      'build/src'
    ],
    includePattern: '\\.js$'
  },
  templates: {
    copyright: 'Copyright 2019 Google, LLC.',
    includeDate: false,
    sourceFiles: false,
    systemName: '@google-cloud/recommender',
    theme: 'lumen',
    default: {
      "outputSourceFiles": false
    }
  },
  markdown: {
    idInHeadings: true
  }
};
<|endoftext|>


<commit_before>function runTest()
{
    FBTest.sysout("generateCurlCommand.START");

    var HOST = FW.FBL.makeURI(basePath).host;
    var REQUEST_URL = basePath + "lib/netUtils/server.php";
    var REFERRER_URL = basePath + "lib/netUtils/generateCurlCommand.html";

    var EXPECTED_GET_RESULT = "curl '" + REQUEST_URL + "?param1=12345&param2=Test%20&%20Test&param3=$&+,/:;=?@%20#%{}|\\^~[]%C2%B4%27' -H 'Host: " + HOST + "' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:22.0) Gecko/20100101 Firefox/22.0' -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' -H 'Accept-Language: en-US,en;q=0.5' -H 'Accept-Encoding: gzip, deflate' -H 'Referer: " + REFERRER_URL + "' -H 'Connection: keep-alive'";
    var EXPECTED_POST_RESULT = "curl '" + REQUEST_URL + "' -H 'Host: " + HOST + "' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:22.0) Gecko/20100101 Firefox/22.0' -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' -H 'Accept-Language: en-US,en;q=0.5' -H 'Accept-Encoding: gzip, deflate' -H 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8' -H 'Referer: " + REFERRER_URL + "' -H 'Connection: keep-alive' -H 'Pragma: no-cache' -H 'Cache-Control: no-cache' --data $'param1=12345&param2=Test & Test&param3=$&+,/:;=?@ #%{}|\\\\^~[]\\xb4\\''";
    var EXPECTED_HEAD_RESULT = "curl '" + REQUEST_URL + "' -X HEAD -H 'Host: " + HOST + "' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:22.0) Gecko/20100101 Firefox/22.0' -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' -H 'Accept-Language: en-US,en;q=0.5' -H 'Accept-Encoding: gzip, deflate' -H 'Referer: " + REFERRER_URL + "' -H 'Connection: keep-alive'";
    var EXPECTED_PUT_RESULT = "curl '" + REQUEST_URL + "' -X PUT -H 'Host: " + HOST + "' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:22.0) Gecko/20100101 Firefox/22.0' -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' -H 'Accept-Language: en-US,en;q=0.5' -H 'Accept-Encoding: gzip, deflate' -H 'Content-Type: application/json; charset=UTF-8' -H 'Referer: " + REFERRER_URL + "' -H 'Connection: keep-alive' --data '{\"a\":\"1\", \"b\":\"2\", \"c\":\"3\"}'";
    var EXPECTED_DELETE_RESULT = "curl '" + REQUEST_URL + "' -X DELETE -H 'Host: " + HOST + "' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:22.0) Gecko/20100101 Firefox/22.0' -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' -H 'Accept-Language: en-US,en;q=0.5' -H 'Accept-Encoding: gzip, deflate' -H 'Referer: " + REFERRER_URL + "' -H 'Connection: keep-alive'";

    FBTest.setPref("net.curlAddCompressedArgument", false);
    FBTest.openNewTab(basePath + "lib/netUtils/generateCurlCommand.html", function(win)
    {
        FBTest.enableNetPanel(function (win)
        {
            var tasks = new FBTest.TaskList();
            tasks.push(submitAndVerify, "GET", EXPECTED_GET_RESULT, win, "Generated cURL command from a GET request result should be correct");
            tasks.push(submitAndVerify, "POST", EXPECTED_POST_RESULT, win, "Generated cURL command from a POST request result should be correct");
            tasks.push(submitAndVerify, "HEAD", EXPECTED_HEAD_RESULT, win, "Generated cURL command from a HEAD request result should be correct");
            tasks.push(submitAndVerify, "PUT", EXPECTED_PUT_RESULT, win, "Generated cURL command from a PUT request result should be correct");
            tasks.push(submitAndVerify, "DELETE", EXPECTED_DELETE_RESULT, win, "Generated cURL command from a DELETE request result should be correct");

            tasks.run(function ()
            {
                FBTest.cleanUpTestTabs();
                FBTest.testDone("generateCurlCommand.DONE");
            });
        });
    });
}

function submitAndVerify(taskCallback, method, expectedResult, win, whatToExpectText)
{
    FBTest.progress("Test XHR " + method + " request");

    FBTest.selectPanel("net").clear();
    onRequestDisplayed(function(netRow)
    {
        var file = FW.Firebug.getRepObject(netRow);
        var generatedCommand = FW.Firebug.NetMonitor.Utils.generateCurlCommand(file);
        FBTest.compare(generatedCommand, expectedResult, whatToExpectText);

        taskCallback();
    });

    win.wrappedJSObject.submitForm(method, true);
}

function onRequestDisplayed(callback)
{
    // Create listener for mutation events.
    var doc = FBTest.getPanelDocument();
    var recognizer = new MutationRecognizer(doc.defaultView, "tr",
        {"class": "netRow category-xhr loaded"});

    // Wait for a XHR log to appear in the Net panel.
    recognizer.onRecognizeAsync(callback);
}<commit_msg>6616: Net Panel: Copy request as a cURL command - Fix failing test (user agent header)<commit_after>function runTest()
{
    FBTest.sysout("generateCurlCommand.START");

    var HOST = FW.FBL.makeURI(basePath).host;
    var REQUEST_URL = basePath + "lib/netUtils/server.php";
    var REFERRER_URL = basePath + "lib/netUtils/generateCurlCommand.html";

    var EXPECTED_GET_RESULT = "curl '" + REQUEST_URL + "?param1=12345&param2=Test%20&%20Test&param3=$&+,/:;=?@%20#%{}|\\^~[]%C2%B4%27' -H 'Host: " + HOST + "' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:22.0) Gecko/20100101 Firefox/22.0' -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' -H 'Accept-Language: en-US,en;q=0.5' -H 'Accept-Encoding: gzip, deflate' -H 'Referer: " + REFERRER_URL + "' -H 'Connection: keep-alive'";
    var EXPECTED_POST_RESULT = "curl '" + REQUEST_URL + "' -H 'Host: " + HOST + "' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:22.0) Gecko/20100101 Firefox/22.0' -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' -H 'Accept-Language: en-US,en;q=0.5' -H 'Accept-Encoding: gzip, deflate' -H 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8' -H 'Referer: " + REFERRER_URL + "' -H 'Connection: keep-alive' -H 'Pragma: no-cache' -H 'Cache-Control: no-cache' --data $'param1=12345&param2=Test & Test&param3=$&+,/:;=?@ #%{}|\\\\^~[]\\xb4\\''";
    var EXPECTED_HEAD_RESULT = "curl '" + REQUEST_URL + "' -X HEAD -H 'Host: " + HOST + "' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:22.0) Gecko/20100101 Firefox/22.0' -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' -H 'Accept-Language: en-US,en;q=0.5' -H 'Accept-Encoding: gzip, deflate' -H 'Referer: " + REFERRER_URL + "' -H 'Connection: keep-alive'";
    var EXPECTED_PUT_RESULT = "curl '" + REQUEST_URL + "' -X PUT -H 'Host: " + HOST + "' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:22.0) Gecko/20100101 Firefox/22.0' -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' -H 'Accept-Language: en-US,en;q=0.5' -H 'Accept-Encoding: gzip, deflate' -H 'Content-Type: application/json; charset=UTF-8' -H 'Referer: " + REFERRER_URL + "' -H 'Connection: keep-alive' --data '{\"a\":\"1\", \"b\":\"2\", \"c\":\"3\"}'";
    var EXPECTED_DELETE_RESULT = "curl '" + REQUEST_URL + "' -X DELETE -H 'Host: " + HOST + "' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:22.0) Gecko/20100101 Firefox/22.0' -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' -H 'Accept-Language: en-US,en;q=0.5' -H 'Accept-Encoding: gzip, deflate' -H 'Referer: " + REFERRER_URL + "' -H 'Connection: keep-alive'";

    FBTest.setPref("net.curlAddCompressedArgument", false);
    FBTest.openNewTab(basePath + "lib/netUtils/generateCurlCommand.html", function(win)
    {
        FBTest.enableNetPanel(function (win)
        {
            var tasks = new FBTest.TaskList();
            tasks.push(submitAndVerify, "GET", EXPECTED_GET_RESULT, win, "Generated cURL command from a GET request result should be correct");
            tasks.push(submitAndVerify, "POST", EXPECTED_POST_RESULT, win, "Generated cURL command from a POST request result should be correct");
            tasks.push(submitAndVerify, "HEAD", EXPECTED_HEAD_RESULT, win, "Generated cURL command from a HEAD request result should be correct");
            tasks.push(submitAndVerify, "PUT", EXPECTED_PUT_RESULT, win, "Generated cURL command from a PUT request result should be correct");
            tasks.push(submitAndVerify, "DELETE", EXPECTED_DELETE_RESULT, win, "Generated cURL command from a DELETE request result should be correct");

            tasks.run(function ()
            {
                FBTest.cleanUpTestTabs();
                FBTest.testDone("generateCurlCommand.DONE");
            });
        });
    });
}

function submitAndVerify(taskCallback, method, expectedResult, win, whatToExpectText)
{
    FBTest.progress("Test XHR " + method + " request");
    FBTest.selectPanel("net").clear();

    function replaceUserAgentHeader(str)
    {
        var replaceWithStr = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:22.0) Gecko/20100101 Firefox/22.0";
        return str.replace(/(-H 'User-Agent: ).+?(')/i, "$1" + replaceWithStr + "$2");
    }

    onRequestDisplayed(function(netRow)
    {
        var file = FW.Firebug.getRepObject(netRow);
        var generatedCommand = FW.Firebug.NetMonitor.Utils.generateCurlCommand(file);
        FBTest.compare(replaceUserAgentHeader(generatedCommand), expectedResult, whatToExpectText);

        taskCallback();
    });

    win.wrappedJSObject.submitForm(method, true);
}

function onRequestDisplayed(callback)
{
    // Create listener for mutation events.
    var doc = FBTest.getPanelDocument();
    var recognizer = new MutationRecognizer(doc.defaultView, "tr",
        {"class": "netRow category-xhr loaded"});

    // Wait for a XHR log to appear in the Net panel.
    recognizer.onRecognizeAsync(callback);
}<|endoftext|>