view browser-couch.js @ 12:9ed45c82a731

Refactoring: made a new ordered dictionary class and made the DB use that.
author Atul Varma <varmaa@toolness.com>
date Fri, 10 Apr 2009 14:29:26 -0700
parents 89c6105c2d1e
children 07e8dc256570
line wrap: on
line source

/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Ubiquity.
 *
 * The Initial Developer of the Original Code is Mozilla.
 * Portions created by the Initial Developer are Copyright (C) 2007
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Atul Varma <atul@mozilla.com>
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

var BrowserCouch = {
  get: function BC_get(name, cb, storage, JSON) {
    var self = this;

    function createDb() {
      cb(new self._DB(name, storage, new self._Dictionary(JSON)));
    }

    if (!storage) {
      if (window.globalStorage || window.localStorage) {
        if (window.globalStorage)
          storage = window.globalStorage[location.hostname];
        else
          storage = window.localStorage;
        if (window.JSON) {
          JSON = window.JSON;
          createDb();
        } else
          self._loadScript(
            "json2.js",
            window,
            function() {
              if (!window.JSON)
                throw new Error('JSON library failed to load');
              JSON = window.JSON;
              createDb();
            });
      } else {
        /* TODO: Consider using JSPersist or something else here. */
        throw new Error('unable to find persistent storage backend');
      }
    } else
      createDb();
  },

  _loadScript: function BC__loadScript(url, window, cb) {
    var doc = window.document;
    var script = doc.createElement("script");
    script.setAttribute("src", url);
    script.addEventListener(
      "load",
      function onLoad() {
        script.removeEventListener("load", onLoad, false);
        cb();
      },
      false
    );
    doc.body.appendChild(script);
  },

  _Dictionary: function BC__Dictionary(JSON) {
    var keysAndValues = [];
    var keyIndex = {};

    function sort() {
      keyIndex = {};
      keysAndValues.sort(function compare(a, b) {
                           if (a[0] < b[0])
                             return -1;
                           if (a[0] > b[0])
                             return 1;
                           return 0;
                         });
      for (var i = 0; i < keysAndValues.length; i++) {
        var tuple = keysAndValues[i];
        keyIndex[tuple[0]] = i;
      }
    }

    this.has = function Dictionary_has(key) {
      return (key in keyIndex);
    };

    this.getNthValue = function Dictionary_getNthValue(index) {
      return keysAndValues[index][1];
    };

    this.getLength = function Dictionary_getLength() {
      return keysAndValues.length;
    };

    this.get = function Dictionary_get(key) {
      var index = keyIndex[key];
      if (index)
        return keysAndValues[index][1];
      else
        throw new Error("key not in dictionary: " + key);
    };

    this.set = function Dictionary_set(key, value) {
      if (key in keyIndex)
        keysAndValues[keyIndex[key]][1] = value;
      else {
        keysAndValues.push([key, value]);
        sort();
      }
    };

    this.delete = function Dictionary_delete(key) {
      delete keysAndValues[keyIndex[key]];
      sort();
    };

    this.clear = function Dictionary_clear() {
      keysAndValues = [];
      keyIndex = {};
    };

    this.toJSON = function Dictionary_toJSON() {
      return JSON.stringify({keysAndValues: keysAndValues,
                             keyIndex: keyIndex});
    };

    this.fromJSON = function Dictionary_fromJSON(string) {
      var obj = JSON.parse(string);
      keysAndValues = obj.keysAndValues;
      keyIndex = obj.keyIndex;
    };
  },

  _DB: function BC__DB(name, storage, dict) {
    var dbName = 'BrowserCouch_DB_' + name;

    if (dbName in storage && storage[dbName].value)
      dict.fromJSON(storage[dbName].value);

    function commitToStorage() {
      storage[dbName].value = dict.toJSON();
    }

    this.wipe = function DB_wipe(cb) {
      dict.clear();
      commitToStorage();
      if (cb)
        cb();
    };

    this.get = function DB_get(id, cb) {
      if (dict.has(id))
        cb(dict.get(id));
      else
        cb(null);
    };

    this.put = function DB_put(document, cb) {
      if (document.constructor.name == "Array") {
        for (var i = 0; i < document.length; i++)
          dict.set(document[i].id, document[i]);
      } else
        dict.set(document.id, document);

      commitToStorage();
      cb();
    };

    this.view = function DB_view(options) {
      // TODO: Add support for worker threads.

      if (!options.map)
        throw new Error('map function not provided');
      if (!options.finished)
        throw new Error('finished callback not provided');

      BrowserCouch._mapReduce(options.map,
                              options.reduce,
                              dict,
                              options.progress,
                              options.finished,
                              options.chunkSize);
    };
  },

  _mapReduce: function BC__mapReduce(map, reduce, dict, progress,
                                     finished, chunkSize) {
    var len = dict.getLength();
    var mapResult = {};

    function emit(key, value) {
      // TODO: This assumes that the key will always be
      // an indexable value. We may have to hash the value,
      // though, if it's e.g. an Object.
      if (!mapResult[key])
        mapResult[key] = [];
      mapResult[key].push(value);
    }

    // Maximum number of items to process before giving the UI a chance
    // to breathe.
    var DEFAULT_CHUNK_SIZE = 1000;

    // If no progress callback is given, we'll automatically give the
    // UI a chance to breathe for this many milliseconds before continuing
    // processing.
    var DEFAULT_UI_BREATHE_TIME = 50;

    if (!chunkSize)
      chunkSize = DEFAULT_CHUNK_SIZE;
    var i = 0;

    function continueMap() {
      var iAtStart = i;

      do {
        map(dict.getNthValue(i), emit);
        i++;
      } while (i - iAtStart < chunkSize &&
               i < len)

      if (i == len)
        doReduce();
      else {
        if (progress)
          progress(i / len, continueMap);
        else
          window.setTimeout(continueMap, DEFAULT_UI_BREATHE_TIME);
      }
    }

    continueMap();

    function doReduce() {
      if (reduce) {
        var keys = [];
        var values = [];

        for (key in mapResult) {
          keys.push(key);
          values.push(mapResult[key]);
        }

        finished(reduce(keys, values));
      } else {
        var result = [];

        for (key in mapResult) {
          var values = mapResult[key];
          for (var i = 0; i < values.length; i++)
            result.push([key, values[i]]);
        }
        finished(result);
      }
    }
  }
};