123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662 |
- (function (root, factory) {
- if (typeof define === 'function' && define.amd) {
- // AMD. Register as an anonymous module.
- define(['backbone', 'underscore'], factory);
- } else if (typeof exports === 'object') {
- // Node. Does not work with strict CommonJS, but
- // only CommonJS-like environments that support module.exports,
- // like Node.
- module.exports = factory(require('backbone'), require('underscore'));
- } else {
- // Browser globals (root is window)
- root.returnExports = factory(root.Backbone, root._);
- }
- }(this, function (Backbone, _) {
- // Generate four random hex digits.
- function S4() {
- return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
- }
- // Generate a pseudo-GUID by concatenating random hexadecimal.
- function guid() {
- return (S4() + S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4() + S4() + S4());
- }
- if ( _(indexedDB).isUndefined() ) { return; }
- // Driver object
- // That's the interesting part.
- // There is a driver for each schema provided. The schema is a te combination of name (for the database), a version as well as migrations to reach that
- // version of the database.
- function Driver(schema, ready, nolog, onerror) {
- this.schema = schema;
- this.ready = ready;
- this.error = null;
- this.transactions = []; // Used to list all transactions and keep track of active ones.
- this.db = null;
- this.nolog = nolog;
- this.onerror = onerror;
- var lastMigrationPathVersion = _.last(this.schema.migrations).version;
- if (!this.nolog) debugLog("opening database " + this.schema.id + " in version #" + lastMigrationPathVersion);
- this.dbRequest = indexedDB.open(this.schema.id,lastMigrationPathVersion); //schema version need to be an unsigned long
- this.launchMigrationPath = function(dbVersion) {
- var transaction = this.dbRequest.transaction;
- var clonedMigrations = _.clone(schema.migrations);
- this.migrate(transaction, clonedMigrations, dbVersion, {
- error: function (event) {
- this.error = "Database not up to date. " + dbVersion + " expected was " + lastMigrationPathVersion;
- }.bind(this)
- });
- };
- this.dbRequest.onblocked = function(event){
- if (!this.nolog) debugLog("connection to database blocked");
- }
- this.dbRequest.onsuccess = function (e) {
- this.db = e.target.result; // Attach the connection ot the queue.
- var currentIntDBVersion = (parseInt(this.db.version) || 0); // we need convert beacuse chrome store in integer and ie10 DP4+ in int;
- var lastMigrationInt = (parseInt(lastMigrationPathVersion) || 0); // And make sure we compare numbers with numbers.
- if (currentIntDBVersion === lastMigrationInt) { //if support new event onupgradeneeded will trigger the ready function
- // No migration to perform!
- this.ready();
- } else if (currentIntDBVersion < lastMigrationInt ) {
- // We need to migrate up to the current migration defined in the database
- this.launchMigrationPath(currentIntDBVersion);
- } else {
- // Looks like the IndexedDB is at a higher version than the current driver schema.
- this.error = "Database version is greater than current code " + currentIntDBVersion + " expected was " + lastMigrationInt;
- }
- }.bind(this);
- this.dbRequest.onerror = function (e) {
- // Failed to open the database
- this.error = "Couldn't not connect to the database"
- if (!this.nolog) debugLog("Couldn't not connect to the database");
- this.onerror();
- }.bind(this);
- this.dbRequest.onabort = function (e) {
- // Failed to open the database
- this.error = "Connection to the database aborted"
- if (!this.nolog) debugLog("Connection to the database aborted");
- this.onerror();
- }.bind(this);
- this.dbRequest.onupgradeneeded = function(iDBVersionChangeEvent){
- this.db =iDBVersionChangeEvent.target.result;
- var newVersion = iDBVersionChangeEvent.newVersion;
- var oldVersion = iDBVersionChangeEvent.oldVersion;
- // Fix Safari 8 and iOS 8 bug
- // at the first connection oldVersion is equal to 9223372036854776000
- // but the real value is 0
- if (oldVersion > 99999999999)
- oldVersion = 0;
- if (!this.nolog) debugLog("onupgradeneeded = " + oldVersion + " => " + newVersion);
- this.launchMigrationPath(oldVersion);
- }.bind(this);
- }
- function debugLog(str) {
- if (typeof window !== "undefined" && typeof window.console !== "undefined" && typeof window.console.log !== "undefined") {
- window.console.log(str);
- }
- else if(console.log !== "undefined") {
- console.log(str)
- }
- }
- // Driver Prototype
- Driver.prototype = {
- // Tracks transactions. Mostly for debugging purposes. TO-IMPROVE
- _track_transaction: function(transaction) {
- this.transactions.push(transaction);
- function removeIt() {
- var idx = this.transactions.indexOf(transaction);
- if (idx !== -1) {this.transactions.splice(idx); }
- };
- transaction.oncomplete = removeIt.bind(this);
- transaction.onabort = removeIt.bind(this);
- transaction.onerror = removeIt.bind(this);
- },
- // Performs all the migrations to reach the right version of the database.
- migrate: function (transaction, migrations, version, options) {
- transaction.onerror = options.error;
- transaction.onabort = options.error;
- if (!this.nolog) debugLog("migrate begin version from #" + version);
- var that = this;
- var migration = migrations.shift();
- if (migration) {
- if (!version || version < migration.version) {
- // We need to apply this migration-
- if (typeof migration.before == "undefined") {
- migration.before = function (next) {
- next();
- };
- }
- if (typeof migration.after == "undefined") {
- migration.after = function (next) {
- next();
- };
- }
- // First, let's run the before script
- if (!this.nolog) debugLog("migrate begin before version #" + migration.version);
- migration.before(function () {
- if (!this.nolog) debugLog("migrate done before version #" + migration.version);
- if (!this.nolog) debugLog("migrate begin migrate version #" + migration.version);
- migration.migrate(transaction, function () {
- if (!this.nolog) debugLog("migrate done migrate version #" + migration.version);
- // Migration successfully appliedn let's go to the next one!
- if (!this.nolog) debugLog("migrate begin after version #" + migration.version);
- migration.after(function () {
- if (!this.nolog) debugLog("migrate done after version #" + migration.version);
- if (!this.nolog) debugLog("Migrated to " + migration.version);
- //last modification occurred, need finish
- if(migrations.length ==0) {
- if (!this.nolog) {
- debugLog("migrate setting transaction.oncomplete to finish version #" + migration.version);
- transaction.oncomplete = function() {
- debugLog("migrate done transaction.oncomplete version #" + migration.version);
- debugLog("Done migrating");
- }
- }
- }
- else
- {
- if (!this.nolog) debugLog("migrate end from version #" + version + " to " + migration.version);
- that.migrate(transaction, migrations, version, options);
- }
- }.bind(this));
- }.bind(this));
- }.bind(this));
- } else {
- // No need to apply this migration
- if (!this.nolog) debugLog("Skipping migration " + migration.version);
- this.migrate(transaction, migrations, version, options);
- }
- }
- },
- // This is the main method, called by the ExecutionQueue when the driver is ready (database open and migration performed)
- execute: function (storeName, method, object, options) {
- if (!this.nolog) debugLog("execute : " + method + " on " + storeName + " for " + object.id);
- switch (method) {
- case "create":
- this.create(storeName, object, options);
- break;
- case "read":
- if (object.id || object.cid) {
- this.read(storeName, object, options); // It's a model
- } else {
- this.query(storeName, object, options); // It's a collection
- }
- break;
- case "update":
- this.update(storeName, object, options); // We may want to check that this is not a collection. TOFIX
- break;
- case "delete":
- if (object.id || object.cid) {
- this.delete(storeName, object, options);
- } else {
- this.clear(storeName, object, options);
- }
- break;
- default:
- // Hum what?
- }
- },
- // Writes the json to the storeName in db. It is a create operations, which means it will fail if the key already exists
- // options are just success and error callbacks.
- create: function (storeName, object, options) {
- var writeTransaction = this.db.transaction([storeName], 'readwrite');
- //this._track_transaction(writeTransaction);
- var store = writeTransaction.objectStore(storeName);
- var json = object.toJSON();
- var idAttribute = _.result(object, 'idAttribute');
- var writeRequest;
- if (json[idAttribute] === undefined && !store.autoIncrement) json[idAttribute] = guid();
- writeTransaction.onerror = function (e) {
- options.error(e);
- };
- writeTransaction.oncomplete = function (e) {
- options.success(json);
- };
- if (!store.keyPath)
- writeRequest = store.add(json, json[idAttribute]);
- else
- writeRequest = store.add(json);
- },
- // Writes the json to the storeName in db. It is an update operation, which means it will overwrite the value if the key already exist
- // options are just success and error callbacks.
- update: function (storeName, object, options) {
- var writeTransaction = this.db.transaction([storeName], 'readwrite');
- //this._track_transaction(writeTransaction);
- var store = writeTransaction.objectStore(storeName);
- var json = object.toJSON();
- var idAttribute = _.result(object, 'idAttribute');
- var writeRequest;
- if (!json[idAttribute]) json[idAttribute] = guid();
- if (!store.keyPath)
- writeRequest = store.put(json, json[idAttribute]);
- else
- writeRequest = store.put(json);
- writeRequest.onerror = function (e) {
- options.error(e);
- };
- writeTransaction.oncomplete = function (e) {
- options.success(json);
- };
- },
- // Reads from storeName in db with json.id if it's there of with any json.xxxx as long as xxx is an index in storeName
- read: function (storeName, object, options) {
- var readTransaction = this.db.transaction([storeName], "readonly");
- this._track_transaction(readTransaction);
- var store = readTransaction.objectStore(storeName);
- var json = object.toJSON();
- var idAttribute = _.result(object, 'idAttribute');
- var getRequest = null;
- if (json[idAttribute]) {
- getRequest = store.get(json[idAttribute]);
- } else if(options.index) {
- var index = store.index(options.index.name);
- getRequest = index.get(options.index.value);
- } else {
- // We need to find which index we have
- var cardinality = 0; // try to fit the index with most matches
- _.each(store.indexNames, function (key, index) {
- index = store.index(key);
- if(typeof index.keyPath === 'string' && 1 > cardinality) {
- // simple index
- if (json[index.keyPath] !== undefined) {
- getRequest = index.get(json[index.keyPath]);
- cardinality = 1;
- }
- } else if(typeof index.keyPath === 'object' && index.keyPath.length > cardinality) {
- // compound index
- var valid = true;
- var keyValue = _.map(index.keyPath, function(keyPart) {
- valid = valid && json[keyPart] !== undefined;
- return json[keyPart];
- });
- if(valid) {
- getRequest = index.get(keyValue);
- cardinality = index.keyPath.length;
- }
- }
- });
- }
- if (getRequest) {
- getRequest.onsuccess = function (event) {
- if (event.target.result) {
- options.success(event.target.result);
- } else {
- options.error("Not Found");
- }
- };
- getRequest.onerror = function () {
- options.error("Not Found"); // We couldn't find the record.
- }
- } else {
- options.error("Not Found"); // We couldn't even look for it, as we don't have enough data.
- }
- },
- // Deletes the json.id key and value in storeName from db.
- delete: function (storeName, object, options) {
- var deleteTransaction = this.db.transaction([storeName], 'readwrite');
- //this._track_transaction(deleteTransaction);
- var store = deleteTransaction.objectStore(storeName);
- var json = object.toJSON();
- var idAttribute = _.result(object, 'idAttribute');
- var deleteRequest = store.delete(json[idAttribute]);
- deleteTransaction.oncomplete = function (event) {
- options.success(null);
- };
- deleteRequest.onerror = function (event) {
- options.error("Not Deleted");
- };
- },
- // Clears all records for storeName from db.
- clear: function (storeName, object, options) {
- var deleteTransaction = this.db.transaction([storeName], "readwrite");
- //this._track_transaction(deleteTransaction);
- var store = deleteTransaction.objectStore(storeName);
- var deleteRequest = store.clear();
- deleteRequest.onsuccess = function (event) {
- options.success(null);
- };
- deleteRequest.onerror = function (event) {
- options.error("Not Cleared");
- };
- },
- // Performs a query on storeName in db.
- // options may include :
- // - conditions : value of an index, or range for an index
- // - range : range for the primary key
- // - limit : max number of elements to be yielded
- // - offset : skipped items.
- query: function (storeName, collection, options) {
- var elements = [];
- var skipped = 0, processed = 0;
- var queryTransaction = this.db.transaction([storeName], "readonly");
- //this._track_transaction(queryTransaction);
- var idAttribute = _.result(collection.model.prototype, 'idAttribute');
- var readCursor = null;
- var store = queryTransaction.objectStore(storeName);
- var index = null,
- lower = null,
- upper = null,
- bounds = null;
- if (options.conditions) {
- // We have a condition, we need to use it for the cursor
- _.each(store.indexNames, function (key) {
- if (!readCursor) {
- index = store.index(key);
- if (options.conditions[index.keyPath] instanceof Array) {
- lower = options.conditions[index.keyPath][0] > options.conditions[index.keyPath][1] ? options.conditions[index.keyPath][1] : options.conditions[index.keyPath][0];
- upper = options.conditions[index.keyPath][0] > options.conditions[index.keyPath][1] ? options.conditions[index.keyPath][0] : options.conditions[index.keyPath][1];
- bounds = IDBKeyRange.bound(lower, upper, true, true);
- if (options.conditions[index.keyPath][0] > options.conditions[index.keyPath][1]) {
- // Looks like we want the DESC order
- readCursor = index.openCursor(bounds, window.IDBCursor.PREV || "prev");
- } else {
- // We want ASC order
- readCursor = index.openCursor(bounds, window.IDBCursor.NEXT || "next");
- }
- } else if (typeof options.conditions[index.keyPath] === 'object' && ('$gt' in options.conditions[index.keyPath] || '$gte' in options.conditions[index.keyPath])) {
- if('$gt' in options.conditions[index.keyPath])
- bounds = IDBKeyRange.lowerBound(options.conditions[index.keyPath]['$gt'], true);
- else
- bounds = IDBKeyRange.lowerBound(options.conditions[index.keyPath]['$gte']);
- readCursor = index.openCursor(bounds, window.IDBCursor.NEXT || "next");
- } else if (typeof options.conditions[index.keyPath] === 'object' && ('$lt' in options.conditions[index.keyPath] || '$lte' in options.conditions[index.keyPath])) {
- if('$lt' in options.conditions[index.keyPath])
- bounds = IDBKeyRange.upperBound(options.conditions[index.keyPath]['$lt'], true);
- else
- bounds = IDBKeyRange.upperBound(options.conditions[index.keyPath]['$lte']);
- readCursor = index.openCursor(bounds, window.IDBCursor.NEXT || "next");
- } else if (options.conditions[index.keyPath] != undefined) {
- bounds = IDBKeyRange.only(options.conditions[index.keyPath]);
- readCursor = index.openCursor(bounds);
- }
- }
- });
- } else if (options.index) {
- index = store.index(options.index.name);
- var excludeLower = !!options.index.excludeLower;
- var excludeUpper = !!options.index.excludeUpper;
- if (index) {
- if (options.index.lower && options.index.upper) {
- bounds = IDBKeyRange.bound(options.index.lower, options.index.upper, excludeLower, excludeUpper);
- } else if (options.index.lower) {
- bounds = IDBKeyRange.lowerBound(options.index.lower, excludeLower);
- } else if (options.index.upper) {
- bounds = IDBKeyRange.upperBound(options.index.upper, excludeUpper);
- } else if (options.index.only) {
- bounds = IDBKeyRange.only(options.index.only);
- }
- if (typeof options.index.order === 'string' && options.index.order.toLowerCase() === 'desc') {
- readCursor = index.openCursor(bounds, window.IDBCursor.PREV || "prev");
- } else {
- readCursor = index.openCursor(bounds, window.IDBCursor.NEXT || "next");
- }
- }
- } else {
- // No conditions, use the index
- if (options.range) {
- lower = options.range[0] > options.range[1] ? options.range[1] : options.range[0];
- upper = options.range[0] > options.range[1] ? options.range[0] : options.range[1];
- bounds = IDBKeyRange.bound(lower, upper);
- if (options.range[0] > options.range[1]) {
- readCursor = store.openCursor(bounds, window.IDBCursor.PREV || "prev");
- } else {
- readCursor = store.openCursor(bounds, window.IDBCursor.NEXT || "next");
- }
- } else {
- readCursor = store.openCursor();
- }
- }
- if (typeof (readCursor) == "undefined" || !readCursor) {
- options.error("No Cursor");
- } else {
- readCursor.onerror = function(e){
- options.error("readCursor error", e);
- };
- // Setup a handler for the cursor’s `success` event:
- readCursor.onsuccess = function (e) {
- var cursor = e.target.result;
- if (!cursor) {
- if (options.addIndividually || options.clear) {
- // nothing!
- // We need to indicate that we're done. But, how?
- collection.trigger("reset");
- } else {
- options.success(elements); // We're done. No more elements.
- }
- }
- else {
- // Cursor is not over yet.
- if (options.limit && processed >= options.limit) {
- // Yet, we have processed enough elements. So, let's just skip.
- if (bounds) {
- if (options.conditions && options.conditions[index.keyPath]) {
- cursor.continue(options.conditions[index.keyPath][1] + 1); /* We need to 'terminate' the cursor cleany, by moving to the end */
- } else if (options.index && (options.index.upper || options.index.lower)) {
- if (typeof options.index.order === 'string' && options.index.order.toLowerCase() === 'desc') {
- cursor.continue(options.index.lower);
- } else {
- cursor.continue(options.index.upper);
- }
- }
- } else {
- cursor.continue(); /* We need to 'terminate' the cursor cleany, by moving to the end */
- }
- }
- else if (options.offset && options.offset > skipped) {
- skipped++;
- cursor.continue(); /* We need to Moving the cursor forward */
- } else {
- // This time, it looks like it's good!
- if (options.addIndividually) {
- collection.add(cursor.value);
- } else if (options.clear) {
- var deleteRequest = store.delete(cursor.value[idAttribute]);
- deleteRequest.onsuccess = function (event) {
- elements.push(cursor.value);
- };
- deleteRequest.onerror = function (event) {
- elements.push(cursor.value);
- };
- } else {
- elements.push(cursor.value);
- }
- processed++;
- cursor.continue();
- }
- }
- };
- }
- },
- close :function(){
- if(this.db){
- this.db.close();
- }
- }
- };
- // ExecutionQueue object
- // The execution queue is an abstraction to buffer up requests to the database.
- // It holds a "driver". When the driver is ready, it just fires up the queue and executes in sync.
- function ExecutionQueue(schema,next,nolog) {
- this.driver = new Driver(schema, this.ready.bind(this), nolog, this.error.bind(this));
- this.started = false;
- this.failed = false;
- this.stack = [];
- this.version = _.last(schema.migrations).version;
- this.next = next;
- }
- // ExecutionQueue Prototype
- ExecutionQueue.prototype = {
- // Called when the driver is ready
- // It just loops over the elements in the queue and executes them.
- ready: function () {
- this.started = true;
- _.each(this.stack, function (message) {
- this.execute(message);
- }.bind(this));
- this.stack = []; // fix memory leak
- this.next();
- },
- error: function() {
- this.failed = true;
- _.each(this.stack, function (message) {
- this.execute(message);
- }.bind(this));
- this.stack = [];
- this.next();
- },
- // Executes a given command on the driver. If not started, just stacks up one more element.
- execute: function (message) {
- if (this.started) {
- try {
- this.driver.execute(message[2].storeName || message[1].storeName, message[0], message[1], message[2]); // Upon messages, we execute the query
- } catch (e) {
- if (e.name === 'InvalidStateError') {
- var f = window.onInvalidStateError;
- if (f) f(e);
- }
- throw e;
- }
- } else if (this.failed) {
- message[2].error();
- } else {
- this.stack.push(message);
- }
- },
- close : function(){
- this.driver.close();
- }
- };
- // Method used by Backbone for sync of data with data store. It was initially designed to work with "server side" APIs, This wrapper makes
- // it work with the local indexedDB stuff. It uses the schema attribute provided by the object.
- // The wrapper keeps an active Executuon Queue for each "schema", and executes querues agains it, based on the object type (collection or
- // single model), but also the method... etc.
- // Keeps track of the connections
- var Databases = {};
- function sync(method, object, options) {
- if(method == "closeall"){
- _.each(Databases,function(database){
- database.close();
- });
- // Clean up active databases object.
- Databases = {};
- return Backbone.$.Deferred().resolve();
- }
- // If a model or a collection does not define a database, fall back on ajaxSync
- if (!object || !_.isObject(object.database)) {
- return Backbone.ajaxSync(method, object, options);
- }
- var schema = object.database;
- if (Databases[schema.id]) {
- if(Databases[schema.id].version != _.last(schema.migrations).version){
- Databases[schema.id].close();
- delete Databases[schema.id];
- }
- }
- var promise;
- if (typeof Backbone.$ === 'undefined' || typeof Backbone.$.Deferred === 'undefined') {
- var noop = function() {};
- var resolve = noop;
- var reject = noop;
- } else {
- var dfd = Backbone.$.Deferred();
- var resolve = dfd.resolve;
- var reject = dfd.reject;
- promise = dfd.promise();
- }
- var success = options.success;
- options.success = function(resp) {
- if (success) success(resp);
- resolve();
- object.trigger('sync', object, resp, options);
- };
- var error = options.error;
- options.error = function(resp) {
- if (error) error(resp);
- reject();
- object.trigger('error', object, resp, options);
- };
- var next = function(){
- Databases[schema.id].execute([method, object, options]);
- };
- if (!Databases[schema.id]) {
- Databases[schema.id] = new ExecutionQueue(schema,next,schema.nolog);
- } else {
- next();
- }
- return promise;
- };
- Backbone.ajaxSync = Backbone.sync;
- Backbone.sync = sync;
- return { sync: sync, debugLog: debugLog};
- }));
|