<commit_before>(function(global) {
  var connections = {};

  // to avoid collisions like '__proto__'
  var protectName = function(name) {
    return '$' + name;
  };
  var unprotectName = function(protected_name) {
    return protected_name.substring(1);
  };

  var keyInRange = function(range, key, keyOpen) {
    var lowerOpen = keyOpen || range.lowerOpen;
    var upperOpen = keyOpen || range.upperOpen;
    return ((lowerOpen && indexedDB.cmp(key, range.lower) > 0) ||
            (!lowerOpen && indexedDB.cmp(key, range.lower) >= 0)) &&
           ((upperOpen && indexedDB.cmp(key, range.upper) < 0) ||
            (!lowerOpen && indexedDB.cmp(key, range.upper) <= 0))
  };
  var rangesIntersect = function(range1, range2) {
    var lower1Open = range1.lowerOpen || range2.upperOpen;
    var upper1Open = range1.upperOpen || range2.lowerOpen;
    return ((lower1Open && indexedDB.cmp(range1.lower, range2.upper) < 0) ||
            (!lower1Open && indexedDB.cmp(range1.lower, range2.upper) <= 0)) &&
           ((upper1Open && indexedDB.cmp(range1.upper, range2.lower) > 0) ||
            (!upper1Open && indexedDB.cmp(range1.upper, range2.lower) >= 0));
  }
  var rangesTouch = function(range1, range2) {
    var lower1Open = range1.lowerOpen && range2.upperOpen;
    var upper1Open = range1.upperOpen && range2.lowerOpen;
    return ((lower1Open && indexedDB.cmp(range1.lower, range2.upper) < 0) ||
            (!lower1Open && indexedDB.cmp(range1.lower, range2.upper) <= 0)) &&
           ((upper1Open && indexedDB.cmp(range1.upper, range2.lower) > 0) ||
            (!upper1Open && indexedDB.cmp(range1.upper, range2.lower) >= 0));
  }
  var rangeInRange = function(outsideRange, range2) {
    var lower1Open = outsideRange.lowerOpen && !range2.lowerOpen;
    var upper1Open = outsideRange.upperOpen && !range2.upperOpen;
    return ((lower1Open && indexedDB.cmp(outsideRange.lower, range2.lower) < 0) ||
            (!lower1Open && indexedDB.cmp(outsideRange.lower, range2.lower) <= 0)) &&
           ((upper1Open && indexedDB.cmp(outsideRange.upper, range2.upper) > 0) ||
            (!upper1Open && indexedDB.cmp(outsideRange.upper, range2.upper) >= 0));
  }
  var unionRanges = function (range1, range2) {
    var lower;
    var lowerOpen; 
    var upper;
    var upperOpen;
    if (range1.lower == range2.lower) {
      lower = range1.lower;
      lowerOpen = range1.lowerOpen || range2.lowerOpen;
    } else if (range1.lower < range2.lower) {
      lower = range1.lower;
      lowerOpen = range1.lowerOpen;
    } else {
      lower = range2.lower;
      lowerOpen = range2.lowerOpen;
    }
    if (range1.upper == range2.upper) {
      upper = range1.upper;
      upperOpen = range1.upperOpen || range2.upperOpen;
    } else if (range1.upper > range2.upper) {
      upper = range1.upper;
      upperOpen = range1.upperOpen;
    } else {
      upper = range2.upper;
      upperOpen = range2.upperOpen;
    }
    return IDBKeyRange.bound(lower, upper, lowerOpen, upperOpen);
  } 

  var filterForRange = function(range) {
    return function(element) {
      if (element.type == "clear") {
        return true;
      }
      if (element.key instanceof IDBKeyRange) {
        return rangesIntersect(element.key, range);
      } else {
        return keyInRange(range, element.key, false);
      }
    };
  }

  // returns if we should add the new change
  var cullChangesForNewChange = function(changeInfo, newChange) {
    if (changeInfo.changes.length == 0) {
      return true;
    }
    if (newChange.type === 'clear') {
      changeInfo.changes = [];
      changeInfo.valueChanges = [];
      changeInfo.index = new Map();
    }
    if (newChange.type === 'add') {
      // if this was successful, we don't have anything before us to cull.
      return true;
    }
    if (newChange.type === 'put') {
      if (changeInfo.index.has(newChange.key)) {
        var oldChange = changeInfo.index.get(newChange.key);
        if (oldChange.type === 'add') {
          newChange.type = 'add';
        }
        var index = changeInfo.changes.indexOf(oldChange);
        changeInfo.changes.splice(index, 1);
        changeInfo.valueChanges.splice(index, 1);
      }
      return true;
    }
    if (newChange.type !== 'delete') {
      console.log('Error, unrecognized type: ', newChange.type);
      return false;
    }
    for (var i = changeInfo.changes.length-1; i >= 0; i--) {
      var change = changeInfo.changes[i];
      switch(change.type) {
        case 'clear':
          return false;
        case 'delete':
          if (rangeInRange(change.key, newChange.key)) {
            // previous delete already does us
            return false;
          }
          if (rangesTouch(change.key, newChange.key)) {
            // we intersect with an old change, so expand the old change with the new one.
            change.key = unionRanges(change.key, newChange.key);
            return false;
          }
          break;
        case 'add':
        case 'put':
          if (keyInRange(newChange.key, change.key, false)) {
            changeInfo.changes.splice(i, 1);
            changeInfo.valueChanges.splice(i, 1); 
          }
          break;
      }
    }
    return true;
  };

  // name assumed to be protected
  var addOpenDatabase = function(db, name){
    db._listeners = {};
    db._openTransactions = 0;
    db._closePending = false;
    connections[name] = connections[name] || [];
    connections[name].push(db);
  };
  var closeDatabase = function(db) {
    for (var osName in db._listeners) {
      var list = db._listeners[osName];
      for (var i = 0; i < list.length; i++) {
        list[i].alive = false;
      }
    }
    db._listeners = {};
    var list = connections[protectName(db.name)];
    if (!list) {
      console.log('Cannot find db connection for name ' + db.name);
      return;
    }
    var index = list.indexOf(db);
    list.splice(index, 1);
  };

  // os store names assumed to be unprotected,
  // returns control object
  var addObserver = function(db, objectStoresAndRanges, fcn, options) {
    var osToRange = {};
    if (options.onlyExternal) {
      console.log('External changes (multiple browsing contexts) are not supported.' +
                  'Observer is effectively a no-op until that is done.  Sorry!');
    }
    for (var i = 0; i < objectStoresAndRanges.length; i++) {
      var nameAndRange = objectStoresAndRanges[i];
      osToRange[protectName(nameAndRange.name)] = nameAndRange.range;
    }
    var listener = { db: db, fcn: fcn, ranges: osToRange, alive: true, options: options };

    var osNames = [];
    for (var i = 0; i < objectStoresAndRanges.length; i++) {
      var nameAndRange = objectStoresAndRanges[i];
      osNames.push(nameAndRange.name);
      var name = protectName(nameAndRange.name);
      db._listeners[name] = db._listeners[name] || [];
      db._listeners[name].push(listener);
    }
    // let the observer load initial state.
    var txn = db.transaction(osNames, 'readonly');
    fcn(null,
      { initializing: true, db: db, objectStoreName: null, isExternalChange: false, transaction: txn});
    var control = {
      isAlive: function() {
        return listener.alive;
      },
      stop: function() {
        for (var osName in listener.ranges) {
          var list = db._listeners[osName];
          if (!list) {
            console.log('could not find list for object store ' + osName);
            continue;
          }
          var index = list.indexOf(listener);
          if (index === -1) {
            console.log('could not find listener in list for object store ' + osName);
            return;
          }
          list.splice(index, 1);
        }
        listener.alive = false;
      }
    };
    return control;
  };

  // protected name
  var hasListeners = function(dbname, osName) { 
    var dbs = connections[dbname];
    if (!dbs) {
      return false;
    }
    for (var i = 0; i < dbs.length; i++) {
      var listeners = dbs[i]._listeners;
      if (listeners && listeners[osName] && listeners[osName].length > 0) {
        return true;
      }
    }
    return false;
  };
  // protected name
  var hasListenersForValues = function(dbname, osName) {
    var dbs = connections[dbname];
    if (!dbs) {
      return false;
    }
    for (var i = 0; i < dbs.length; i++) {
      var listeners = dbs[i]._listeners;
      if (listeners && listeners[osName]) {
        var list = listeners[osName];
        for (var i = 0; i < list.length; i++) {
          if (list[i].options.includeValues) {
            return true;
          }
        }
      }
    }
    return false;
  };
  
  var pushOperation = function(objectStore, changesMap, type, keyOrRange, value) { 
    var name = protectName(objectStore.name);
    if (!hasListeners(objectStore.transaction.db.name, name)) {
      return;
    }
    if (!changesMap[name]) {
      changesMap[name] = { changes: [], valueChanges: [], index: new Map() };
    }
    var changeInfo = changesMap[name];
    var operation = { type: type };
    if (keyOrRange) {
      operation.key = keyOrRange;
    }
    var shouldAdd = cullChangesForNewChange(changeInfo, operation);
    if (!shouldAdd) {
      return;
    }
    if (keyOrRange && !(keyOrRange instanceof IDBKeyRange)) {
      changeInfo.index.set(
          keyOrRange, operation);
    }
    if (hasListenersForValues(objectStore.transaction.db.name, name)) {
      var valueOperation = { type: operation.type };
      if (keyOrRange) {
        valueOperation.key = operation.key;
      }
      if (value) {
        valueOperation.value = value;
      }
      changeInfo.valueChanges.push(valueOperation);
    }
    changeInfo.changes.push(operation);
  };

  var getListeners = function(dbName, objectStoreName) {
    if (!connections[dbName]) {
      return [];
    }
    var listeners = [];
    connections[dbName].forEach(function(db) {
      if (!db._listeners[objectStoreName]) {
        return;
      }
      listeners = listeners.concat(db._listeners[objectStoreName]);
    });
    return listeners;
  };

  var $open = IDBFactory.prototype.open;
  IDBFactory.prototype.open = function(name /*, version*/) {
    var request = $open.apply(this, arguments);
    request.addEventListener('success', function() {
      var connection = request.result;
      addOpenDatabase(connection, name);
    });
    return request;
  };

  var $close = IDBDatabase.prototype.close;
  IDBDatabase.prototype.close = function() {
    $close.apply(this, arguments);
    if (this._openTransactions === 0) {
      closeDatabase(this);
    } else {
      this._closePending = true;
    }
  };

  IDBDatabase.prototype.observe = function(namesOrNamesAndRanges, listenerFunction, options) {
    var sanitizedNamesAndRanges = [];
    if (typeof namesOrNamesAndRanges === 'string') {
      sanitizedNamesAndRanges = [{ name: namesOrNamesAndRanges }];
    } else if (typeof namesOrNamesAndRanges === 'object') {
      if (Array.isArray(namesOrNamesAndRanges)) {
        for (var i = 0; i < namesOrNamesAndRanges.length; i++) {
          var argEntry = namesOrNamesAndRanges[i];
          if (typeof argEntry === "string") {
            argEntry = { name: argEntry };
          }
          if (!argEntry.name) {
            console.log('No name provided for namesAndRanges array entry: ', argEntry);
            continue;
          }
          var entry = {
            name: argEntry.name,
            range: argEntry.range
          }
          sanitizedNamesAndRanges.push(entry);
        }
      } else {
        if (!namesOrNamesAndRanges.name) {
          console.log('No name provided for namesAndRanges: ', namesOrNamesAndRanges);
          return null;
        }
        var entry = {
          name: namesOrNamesAndRanges.name,
          range: namesOrNamesAndRanges.range
        };  
        sanitizedNamesAndRanges.push(entry);
      }
    } else {
      console.log('unknown namesOrNamesAndRanges argument: ', namesOrNamesAndRanges);
      return null;
    }
    if (sanitizedNamesAndRanges.length == 0) {
      console.log('could not parse namesOrNamesAndRanges argument');
      return null;
    }
    var sanatizedOptions = {
      includeValues: options ? !!options.includeValues : false,
      includeTransaction: options ? !!options.includeTransaction : false,
      excludeChanges: options ? !!options.excludeChanges : false,
      onlyExternal: options ? !!options.onlyExternal : false
    }
    return addObserver(this, sanitizedNamesAndRanges, listenerFunction, sanatizedOptions);
  };

  var $transaction = IDBDatabase.prototype.transaction;
  IDBDatabase.prototype.transaction = function(scope, mode) {
    var tx = $transaction.apply(this, arguments);
    if (mode !== 'readwrite') return tx;
    tx._changes = [];
    tx.db._openTransactions += 1;
    tx.addEventListener('complete', function() {
      var changeMap = tx._changes;
      tx._changes = [];
      for (var objectStoreName in changeMap) {
        var unprotectedName = unprotectName(objectStoreName);
        var listeners = getListeners(tx.db.name, objectStoreName);
        var changesRecord = changeMap[objectStoreName];
        for (var i = 0; i < listeners.length; i++) {
          var listener = listeners[i];
          var metadata = {
            initializing: false,
            db: listener.db,
            objectStoreName: unprotectedName,
            isExternalChange: false
          };
          var changes = changesRecord.changes;
          if (listener.options.includeValues) {
            changes = changesRecord.valueChanges;
          }
          var range = listener.ranges[objectStoreName];
          if (range) {
            changes = changes.filter(filterForRange(range));
          }
          if (changes.length == 0) {
            continue;
          }
          if (listener.options.excludeChanges) {
            changes = null;
          }
          if (listener.options.includeTransaction) {
            var osNames = Object.keys(listener.ranges).map(function(value) {
              return unprotectName(value);
            });
            metadata.transaction = tx.db.transaction(osNames, 'readonly');
          }
          listener.fcn(changes, metadata);
        }
      }
      tx.db._openTransactions -= 1;
      if (tx.db._closePending) {
        closeDatabase(tx.db);
      }
    });
    tx.addEventListener('abort', function() {
      tx.db._openTransactions -= 1;
      if (tx.db._closePending) {
        closeDatabase(tx.db);
      }<commit_msg>Switched to using Map instead of a js object<commit_after>(function(global) {
  var connections = new Map();

  // to avoid collisions like '__proto__'

  var keyInRange = function(range, key, keyOpen) {
    var lowerOpen = keyOpen || range.lowerOpen;
    var upperOpen = keyOpen || range.upperOpen;
    return ((lowerOpen && indexedDB.cmp(key, range.lower) > 0) ||
            (!lowerOpen && indexedDB.cmp(key, range.lower) >= 0)) &&
           ((upperOpen && indexedDB.cmp(key, range.upper) < 0) ||
            (!lowerOpen && indexedDB.cmp(key, range.upper) <= 0))
  };
  var rangesIntersect = function(range1, range2) {
    var lower1Open = range1.lowerOpen || range2.upperOpen;
    var upper1Open = range1.upperOpen || range2.lowerOpen;
    return ((lower1Open && indexedDB.cmp(range1.lower, range2.upper) < 0) ||
            (!lower1Open && indexedDB.cmp(range1.lower, range2.upper) <= 0)) &&
           ((upper1Open && indexedDB.cmp(range1.upper, range2.lower) > 0) ||
            (!upper1Open && indexedDB.cmp(range1.upper, range2.lower) >= 0));
  }
  var rangesTouch = function(range1, range2) {
    var lower1Open = range1.lowerOpen && range2.upperOpen;
    var upper1Open = range1.upperOpen && range2.lowerOpen;
    return ((lower1Open && indexedDB.cmp(range1.lower, range2.upper) < 0) ||
            (!lower1Open && indexedDB.cmp(range1.lower, range2.upper) <= 0)) &&
           ((upper1Open && indexedDB.cmp(range1.upper, range2.lower) > 0) ||
            (!upper1Open && indexedDB.cmp(range1.upper, range2.lower) >= 0));
  }
  var rangeInRange = function(outsideRange, range2) {
    var lower1Open = outsideRange.lowerOpen && !range2.lowerOpen;
    var upper1Open = outsideRange.upperOpen && !range2.upperOpen;
    return ((lower1Open && indexedDB.cmp(outsideRange.lower, range2.lower) < 0) ||
            (!lower1Open && indexedDB.cmp(outsideRange.lower, range2.lower) <= 0)) &&
           ((upper1Open && indexedDB.cmp(outsideRange.upper, range2.upper) > 0) ||
            (!upper1Open && indexedDB.cmp(outsideRange.upper, range2.upper) >= 0));
  }
  var unionRanges = function (range1, range2) {
    var lower;
    var lowerOpen; 
    var upper;
    var upperOpen;
    if (range1.lower == range2.lower) {
      lower = range1.lower;
      lowerOpen = range1.lowerOpen || range2.lowerOpen;
    } else if (range1.lower < range2.lower) {
      lower = range1.lower;
      lowerOpen = range1.lowerOpen;
    } else {
      lower = range2.lower;
      lowerOpen = range2.lowerOpen;
    }
    if (range1.upper == range2.upper) {
      upper = range1.upper;
      upperOpen = range1.upperOpen || range2.upperOpen;
    } else if (range1.upper > range2.upper) {
      upper = range1.upper;
      upperOpen = range1.upperOpen;
    } else {
      upper = range2.upper;
      upperOpen = range2.upperOpen;
    }
    return IDBKeyRange.bound(lower, upper, lowerOpen, upperOpen);
  } 

  var filterForRange = function(range) {
    return function(element) {
      if (element.type == "clear") {
        return true;
      }
      if (element.key instanceof IDBKeyRange) {
        return rangesIntersect(element.key, range);
      } else {
        return keyInRange(range, element.key, false);
      }
    };
  }

  // returns if we should add the new change
  var cullChangesForNewChange = function(changeInfo, newChange) {
    if (changeInfo.changes.length == 0) {
      return true;
    }
    if (newChange.type === 'clear') {
      changeInfo.changes = [];
      changeInfo.valueChanges = [];
      changeInfo.index = new Map();
    }
    if (newChange.type === 'add') {
      // if this was successful, we don't have anything before us to cull.
      return true;
    }
    if (newChange.type === 'put') {
      if (changeInfo.index.has(newChange.key)) {
        var oldChange = changeInfo.index.get(newChange.key);
        if (oldChange.type === 'add') {
          newChange.type = 'add';
        }
        var index = changeInfo.changes.indexOf(oldChange);
        changeInfo.changes.splice(index, 1);
        changeInfo.valueChanges.splice(index, 1);
      }
      return true;
    }
    if (newChange.type !== 'delete') {
      console.log('Error, unrecognized type: ', newChange.type);
      return false;
    }
    for (var i = changeInfo.changes.length-1; i >= 0; i--) {
      var change = changeInfo.changes[i];
      switch(change.type) {
        case 'clear':
          return false;
        case 'delete':
          if (rangeInRange(change.key, newChange.key)) {
            // previous delete already does us
            return false;
          }
          if (rangesTouch(change.key, newChange.key)) {
            // we intersect with an old change, so expand the old change with the new one.
            change.key = unionRanges(change.key, newChange.key);
            return false;
          }
          break;
        case 'add':
        case 'put':
          if (keyInRange(newChange.key, change.key, false)) {
            changeInfo.changes.splice(i, 1);
            changeInfo.valueChanges.splice(i, 1); 
          }
          break;
      }
    }
    return true;
  };

  var addOpenDatabase = function(db, name){
    db._listeners = new Map();
    db._openTransactions = 0;
    db._closePending = false;
    if (!connections.has(name)) {
      connections.set(name, []);
    }
    connections.get(name).push(db);
  };

  var closeDatabase = function(db) {
    db._listeners.forEach(function(list, osName) {
      for (var i = 0; i < list.length; i++) {
        list[i].alive = false;
      }
    }, this);
    
    db._listeners.clear();
    var list = connections.get(db.name);
    if (!list) {
      console.log('Cannot find db connection for name ' + db.name);
      return;
    }
    var index = list.indexOf(db);
    list.splice(index, 1);
  };

  // returns control object
  var addObserver = function(db, objectStoresAndRanges, fcn, options) {
    var osToRange = new Map();
    if (options.onlyExternal) {
      console.error('External changes (multiple browsing contexts) are not supported.' +
                    'Observer is effectively a no-op until that is done.  Sorry!');
    }
    for (var i = 0; i < objectStoresAndRanges.length; i++) {
      var nameAndRange = objectStoresAndRanges[i];
      osToRange.set(nameAndRange.name, nameAndRange.range);
    }
    var listener = { db: db, fcn: fcn, ranges: osToRange, alive: true, options: options };

    var osNames = [];
    for (var i = 0; i < objectStoresAndRanges.length; i++) {
      var nameAndRange = objectStoresAndRanges[i];
      osNames.push(nameAndRange.name);
      var name = nameAndRange.name;
      if (!db._listeners.has(name)) {
        db._listeners.set(name, []);
      }
      db._listeners.get(name).push(listener);
    }
    // let the observer load initial state.
    var txn = db.transaction(osNames, 'readonly');
    fcn(null,
      { initializing: true, db: db, objectStoreName: null, isExternalChange: false, transaction: txn});
    var control = {
      isAlive: function() {
        return listener.alive;
      },
      stop: function() {
        listener.ranges.forEach(function(range, osName) {
          var list = db._listeners.get(osName);
          if (!list) {
            console.error('could not find list for object store ' + osName);
            return;
          }
          var index = list.indexOf(listener);
          if (index === -1) {
            console.error('could not find listener in list for object store ' + osName);
            return;
          }
          list.splice(index, 1);
          db._listeners.set(osName, list);
        }, this);
        listener.alive = false;
      }
    };
    return control;
  };

  var hasListeners = function(dbname, osName) {
    var dbs = connections.get(dbname);
    if (!dbs) {
      return false;
    }
    for (var i = 0; i < dbs.length; i++) {
      var listeners = dbs[i]._listeners;
      if (listeners && listeners.has(osName) && listeners.get(osName).length > 0) {
        return true;
      }
    }
    return false;
  };
  // protected name
  var hasListenersForValues = function(dbname, osName) {
    var dbs = connections.get(dbname);
    if (!dbs) {
      return false;
    }
    for (var i = 0; i < dbs.length; i++) {
      var listeners = dbs[i]._listeners;
      if (listeners && listeners.has(osName)) {
        var list = listeners.get(osName);
        for (var i = 0; i < list.length; i++) {
          if (list[i].options.includeValues) {
            return true;
          }
        }
      }
    }
    return false;
  };
  
  var pushOperation = function(objectStore, changesMap, type, keyOrRange, value) { 
    var name = objectStore.name;
    if (!hasListeners(objectStore.transaction.db.name, name)) {
      return;
    }
    if (!changesMap[name]) {
      changesMap[name] = { changes: [], valueChanges: [], index: new Map() };
    }
    var changeInfo = changesMap[name];
    var operation = { type: type };
    if (keyOrRange) {
      operation.key = keyOrRange;
    }
    var shouldAdd = cullChangesForNewChange(changeInfo, operation);
    if (!shouldAdd) {
      return;
    }
    if (keyOrRange && !(keyOrRange instanceof IDBKeyRange)) {
      changeInfo.index.set(
          keyOrRange, operation);
    }
    if (hasListenersForValues(objectStore.transaction.db.name, name)) {
      var valueOperation = { type: operation.type };
      if (keyOrRange) {
        valueOperation.key = operation.key;
      }
      if (value) {
        valueOperation.value = value;
      }
      changeInfo.valueChanges.push(valueOperation);
    }
    changeInfo.changes.push(operation);
  };

  var getListeners = function(dbName, objectStoreName) {
    if (!connections.has(dbName)) {
      return [];
    }
    var listeners = [];
    connections.get(dbName).forEach(function(db) {
      if (!db._listeners.has(objectStoreName)) {
        return;
      }
      listeners = listeners.concat(db._listeners.get(objectStoreName));
    });
    return listeners;
  };

  var $open = IDBFactory.prototype.open;
  IDBFactory.prototype.open = function(name /*, version*/) {
    var request = $open.apply(this, arguments);
    request.addEventListener('success', function() {
      var connection = request.result;
      addOpenDatabase(connection, name);
    });
    return request;
  };

  var $close = IDBDatabase.prototype.close;
  IDBDatabase.prototype.close = function() {
    $close.apply(this, arguments);
    if (this._openTransactions === 0) {
      closeDatabase(this);
    } else {
      this._closePending = true;
    }
  };

  IDBDatabase.prototype.observe = function(namesOrNamesAndRanges, listenerFunction, options) {
    var sanitizedNamesAndRanges = [];
    if (typeof namesOrNamesAndRanges === 'string') {
      sanitizedNamesAndRanges = [{ name: namesOrNamesAndRanges }];
    } else if (typeof namesOrNamesAndRanges === 'object') {
      if (Array.isArray(namesOrNamesAndRanges)) {
        for (var i = 0; i < namesOrNamesAndRanges.length; i++) {
          var argEntry = namesOrNamesAndRanges[i];
          if (typeof argEntry === "string") {
            argEntry = { name: argEntry };
          }
          if (!argEntry.name) {
            console.log('No name provided for namesAndRanges array entry: ', argEntry);
            continue;
          }
          var entry = {
            name: argEntry.name,
            range: argEntry.range
          }
          sanitizedNamesAndRanges.push(entry);
        }
      } else {
        if (!namesOrNamesAndRanges.name) {
          console.log('No name provided for namesAndRanges: ', namesOrNamesAndRanges);
          return null;
        }
        var entry = {
          name: namesOrNamesAndRanges.name,
          range: namesOrNamesAndRanges.range
        };  
        sanitizedNamesAndRanges.push(entry);
      }
    } else {
      console.log('unknown namesOrNamesAndRanges argument: ', namesOrNamesAndRanges);
      return null;
    }
    if (sanitizedNamesAndRanges.length == 0) {
      console.log('could not parse namesOrNamesAndRanges argument');
      return null;
    }
    var sanatizedOptions = {
      includeValues: options ? !!options.includeValues : false,
      includeTransaction: options ? !!options.includeTransaction : false,
      excludeChanges: options ? !!options.excludeChanges : false,
      onlyExternal: options ? !!options.onlyExternal : false
    }
    return addObserver(this, sanitizedNamesAndRanges, listenerFunction, sanatizedOptions);
  };

  var $transaction = IDBDatabase.prototype.transaction;
  IDBDatabase.prototype.transaction = function(scope, mode) {
    var tx = $transaction.apply(this, arguments);
    if (mode !== 'readwrite') return tx;
    tx._changes = [];
    tx.db._openTransactions += 1;
    tx.addEventListener('complete', function() {
      var changeMap = tx._changes;
      tx._changes = [];
      for (var objectStoreName in changeMap) {
        var listeners = getListeners(tx.db.name, objectStoreName);
        var changesRecord = changeMap[objectStoreName];
        for (var i = 0; i < listeners.length; i++) {
          var listener = listeners[i];
          var metadata = {
            initializing: false,
            db: listener.db,
            objectStoreName: objectStoreName,
            isExternalChange: false
          };
          var changes = changesRecord.changes;
          if (listener.options.includeValues) {
            changes = changesRecord.valueChanges;
          }
          var range = listener.ranges[objectStoreName];
          if (range) {
            changes = changes.filter(filterForRange(range));
          }
          if (changes.length == 0) {
            continue;
          }
          if (listener.options.excludeChanges) {
            changes = null;
          }
          if (listener.options.includeTransaction) {
            var osNames = Object.keys(listener.ranges);
            metadata.transaction = tx.db.transaction(osNames, 'readonly');
          }
          listener.fcn(changes, metadata);
        }
      }
      tx.db._openTransactions -= 1;
      if (tx.db._closePending) {
        closeDatabase(tx.db);
      }
    });
    tx.addEventListener('abort', function() {
      tx.db._openTransactions -= 1;
      if (tx.db._closePending) {
        closeDatabase(tx.db);
      }
{'fraction_non_alphanumeric': 0.12658756393750653, 'fraction_numerical': 0.00831622533839034, 'mean_word_length': 2.3653395784543325, 'pattern_counts': {'":': 0, '<': 37, '<?xml version=': 0, '>': 25, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6381729', 'n_tokens_mistral': 8757, 'n_tokens_neox': 8592, 'n_words': 1924}