UNPKG

ndn-js

Version:

A JavaScript client library for Named Data Networking

945 lines (878 loc) 160 kB
/* Minimalistic IndexedDB Wrapper with Bullet Proof Transactions ============================================================= By David Fahlander, david.fahlander@gmail.com Version 1.1 - May 26, 2015. Tested successfully on Chrome, IE, Firefox and Opera. Official Website: https://github.com/dfahlander/Dexie.js/wiki/Dexie.js Licensed under the Apache License Version 2.0, January 2004, http://www.apache.org/licenses/ */ (function (global, publish, undefined) { "use strict"; function extend(obj, extension) { if (typeof extension !== 'object') extension = extension(); // Allow to supply a function returning the extension. Useful for simplifying private scopes. Object.keys(extension).forEach(function (key) { obj[key] = extension[key]; }); return obj; } function derive(Child) { return { from: function (Parent) { Child.prototype = Object.create(Parent.prototype); Child.prototype.constructor = Child; return { extend: function (extension) { extend(Child.prototype, typeof extension !== 'object' ? extension(Parent.prototype) : extension); } }; } }; } function override(origFunc, overridedFactory) { return overridedFactory(origFunc); } function Dexie(dbName, options) { /// <param name="options" type="Object" optional="true">Specify only if you wich to control which addons that should run on this instance</param> var addons = (options && options.addons) || Dexie.addons; // Resolve all external dependencies: var deps = Dexie.dependencies; var indexedDB = deps.indexedDB, IDBKeyRange = deps.IDBKeyRange, IDBTransaction = deps.IDBTransaction; var DOMError = deps.DOMError, TypeError = deps.TypeError, Error = deps.Error; var globalSchema = this._dbSchema = {}; var versions = []; var dbStoreNames = []; var allTables = {}; var notInTransFallbackTables = {}; ///<var type="IDBDatabase" /> var idbdb = null; // Instance of IDBDatabase var db_is_blocked = true; var dbOpenError = null; var isBeingOpened = false; var READONLY = "readonly", READWRITE = "readwrite"; var db = this; var pausedResumeables = []; var autoSchema = false; var hasNativeGetDatabaseNames = !!getNativeGetDatabaseNamesFn(); function init() { // If browser (not node.js or other), subscribe to versionchange event and reload page db.on("versionchange", function (ev) { // Default behavior for versionchange event is to close database connection. // Caller can override this behavior by doing db.on("versionchange", function(){ return false; }); // Let's not block the other window from making it's delete() or open() call. db.close(); db.on('error').fire(new Error("Database version changed by other database connection.")); // In many web applications, it would be recommended to force window.reload() // when this event occurs. Do do that, subscribe to the versionchange event // and call window.location.reload(true); }); } // // // // ------------------------- Versioning Framework--------------------------- // // // this.version = function (versionNumber) { /// <param name="versionNumber" type="Number"></param> /// <returns type="Version"></returns> if (idbdb) throw new Error("Cannot add version when database is open"); this.verno = Math.max(this.verno, versionNumber); var versionInstance = versions.filter(function (v) { return v._cfg.version === versionNumber; })[0]; if (versionInstance) return versionInstance; versionInstance = new Version(versionNumber); versions.push(versionInstance); versions.sort(lowerVersionFirst); return versionInstance; }; function Version(versionNumber) { this._cfg = { version: versionNumber, storesSource: null, dbschema: {}, tables: {}, contentUpgrade: null }; this.stores({}); // Derive earlier schemas by default. } extend(Version.prototype, { stores: function (stores) { /// <summary> /// Defines the schema for a particular version /// </summary> /// <param name="stores" type="Object"> /// Example: <br/> /// {users: "id++,first,last,&amp;username,*email", <br/> /// passwords: "id++,&amp;username"}<br/> /// <br/> /// Syntax: {Table: "[primaryKey][++],[&amp;][*]index1,[&amp;][*]index2,..."}<br/><br/> /// Special characters:<br/> /// "&amp;" means unique key, <br/> /// "*" means value is multiEntry, <br/> /// "++" means auto-increment and only applicable for primary key <br/> /// </param> this._cfg.storesSource = this._cfg.storesSource ? extend(this._cfg.storesSource, stores) : stores; // Derive stores from earlier versions if they are not explicitely specified as null or a new syntax. var storesSpec = {}; versions.forEach(function (version) { // 'versions' is always sorted by lowest version first. extend(storesSpec, version._cfg.storesSource); }); var dbschema = (this._cfg.dbschema = {}); this._parseStoresSpec(storesSpec, dbschema); // Update the latest schema to this version // Update API globalSchema = db._dbSchema = dbschema; removeTablesApi([allTables, db, notInTransFallbackTables]); setApiOnPlace([notInTransFallbackTables], tableNotInTransaction, Object.keys(dbschema), READWRITE, dbschema); setApiOnPlace([allTables, db, this._cfg.tables], db._transPromiseFactory, Object.keys(dbschema), READWRITE, dbschema, true); dbStoreNames = Object.keys(dbschema); return this; }, upgrade: function (upgradeFunction) { /// <param name="upgradeFunction" optional="true">Function that performs upgrading actions.</param> var self = this; fakeAutoComplete(function () { upgradeFunction(db._createTransaction(READWRITE, Object.keys(self._cfg.dbschema), self._cfg.dbschema));// BUGBUG: No code completion for prev version's tables wont appear. }); this._cfg.contentUpgrade = upgradeFunction; return this; }, _parseStoresSpec: function (stores, outSchema) { Object.keys(stores).forEach(function (tableName) { if (stores[tableName] !== null) { var instanceTemplate = {}; var indexes = parseIndexSyntax(stores[tableName]); var primKey = indexes.shift(); if (primKey.multi) throw new Error("Primary key cannot be multi-valued"); if (primKey.keyPath && primKey.auto) setByKeyPath(instanceTemplate, primKey.keyPath, 0); indexes.forEach(function (idx) { if (idx.auto) throw new Error("Only primary key can be marked as autoIncrement (++)"); if (!idx.keyPath) throw new Error("Index must have a name and cannot be an empty string"); setByKeyPath(instanceTemplate, idx.keyPath, idx.compound ? idx.keyPath.map(function () { return ""; }) : ""); }); outSchema[tableName] = new TableSchema(tableName, primKey, indexes, instanceTemplate); } }); } }); function runUpgraders(oldVersion, idbtrans, reject, openReq) { if (oldVersion === 0) { //globalSchema = versions[versions.length - 1]._cfg.dbschema; // Create tables: Object.keys(globalSchema).forEach(function (tableName) { createTable(idbtrans, tableName, globalSchema[tableName].primKey, globalSchema[tableName].indexes); }); // Populate data var t = db._createTransaction(READWRITE, dbStoreNames, globalSchema); t.idbtrans = idbtrans; t.idbtrans.onerror = eventRejectHandler(reject, ["populating database"]); t.on('error').subscribe(reject); Promise.newPSD(function () { Promise.PSD.trans = t; try { db.on("populate").fire(t); } catch (err) { openReq.onerror = idbtrans.onerror = function (ev) { ev.preventDefault(); }; // Prohibit AbortError fire on db.on("error") in Firefox. try { idbtrans.abort(); } catch (e) { } idbtrans.db.close(); reject(err); } }); } else { // Upgrade version to version, step-by-step from oldest to newest version. // Each transaction object will contain the table set that was current in that version (but also not-yet-deleted tables from its previous version) var queue = []; var oldVersionStruct = versions.filter(function (version) { return version._cfg.version === oldVersion; })[0]; if (!oldVersionStruct) throw new Error("Dexie specification of currently installed DB version is missing"); globalSchema = db._dbSchema = oldVersionStruct._cfg.dbschema; var anyContentUpgraderHasRun = false; var versToRun = versions.filter(function (v) { return v._cfg.version > oldVersion; }); versToRun.forEach(function (version) { /// <param name="version" type="Version"></param> var oldSchema = globalSchema; var newSchema = version._cfg.dbschema; adjustToExistingIndexNames(oldSchema, idbtrans); adjustToExistingIndexNames(newSchema, idbtrans); globalSchema = db._dbSchema = newSchema; { var diff = getSchemaDiff(oldSchema, newSchema); diff.add.forEach(function (tuple) { queue.push(function (idbtrans, cb) { createTable(idbtrans, tuple[0], tuple[1].primKey, tuple[1].indexes); cb(); }); }); diff.change.forEach(function (change) { if (change.recreate) { throw new Error("Not yet support for changing primary key"); } else { queue.push(function (idbtrans, cb) { var store = idbtrans.objectStore(change.name); change.add.forEach(function (idx) { addIndex(store, idx); }); change.change.forEach(function (idx) { store.deleteIndex(idx.name); addIndex(store, idx); }); change.del.forEach(function (idxName) { store.deleteIndex(idxName); }); cb(); }); } }); if (version._cfg.contentUpgrade) { queue.push(function (idbtrans, cb) { anyContentUpgraderHasRun = true; var t = db._createTransaction(READWRITE, [].slice.call(idbtrans.db.objectStoreNames, 0), newSchema); t.idbtrans = idbtrans; var uncompletedRequests = 0; t._promise = override(t._promise, function (orig_promise) { return function (mode, fn, writeLock) { ++uncompletedRequests; function proxy(fn) { return function () { fn.apply(this, arguments); if (--uncompletedRequests === 0) cb(); // A called db operation has completed without starting a new operation. The flow is finished, now run next upgrader. } } return orig_promise.call(this, mode, function (resolve, reject, trans) { arguments[0] = proxy(resolve); arguments[1] = proxy(reject); fn.apply(this, arguments); }, writeLock); }; }); idbtrans.onerror = eventRejectHandler(reject, ["running upgrader function for version", version._cfg.version]); t.on('error').subscribe(reject); version._cfg.contentUpgrade(t); if (uncompletedRequests === 0) cb(); // contentUpgrade() didnt call any db operations at all. }); } if (!anyContentUpgraderHasRun || !hasIEDeleteObjectStoreBug()) { // Dont delete old tables if ieBug is present and a content upgrader has run. Let tables be left in DB so far. This needs to be taken care of. queue.push(function (idbtrans, cb) { // Delete old tables deleteRemovedTables(newSchema, idbtrans); cb(); }); } } }); // Now, create a queue execution engine var runNextQueuedFunction = function () { try { if (queue.length) queue.shift()(idbtrans, runNextQueuedFunction); else createMissingTables(globalSchema, idbtrans); // At last, make sure to create any missing tables. (Needed by addons that add stores to DB without specifying version) } catch (err) { openReq.onerror = idbtrans.onerror = function (ev) { ev.preventDefault(); }; // Prohibit AbortError fire on db.on("error") in Firefox. try { idbtrans.abort(); } catch(e) {} idbtrans.db.close(); reject(err); } }; runNextQueuedFunction(); } } function getSchemaDiff(oldSchema, newSchema) { var diff = { del: [], // Array of table names add: [], // Array of [tableName, newDefinition] change: [] // Array of {name: tableName, recreate: newDefinition, del: delIndexNames, add: newIndexDefs, change: changedIndexDefs} }; for (var table in oldSchema) { if (!newSchema[table]) diff.del.push(table); } for (var table in newSchema) { var oldDef = oldSchema[table], newDef = newSchema[table]; if (!oldDef) diff.add.push([table, newDef]); else { var change = { name: table, def: newSchema[table], recreate: false, del: [], add: [], change: [] }; if (oldDef.primKey.src !== newDef.primKey.src) { // Primary key has changed. Remove and re-add table. change.recreate = true; diff.change.push(change); } else { var oldIndexes = oldDef.indexes.reduce(function (prev, current) { prev[current.name] = current; return prev; }, {}); var newIndexes = newDef.indexes.reduce(function (prev, current) { prev[current.name] = current; return prev; }, {}); for (var idxName in oldIndexes) { if (!newIndexes[idxName]) change.del.push(idxName); } for (var idxName in newIndexes) { var oldIdx = oldIndexes[idxName], newIdx = newIndexes[idxName]; if (!oldIdx) change.add.push(newIdx); else if (oldIdx.src !== newIdx.src) change.change.push(newIdx); } if (change.recreate || change.del.length > 0 || change.add.length > 0 || change.change.length > 0) { diff.change.push(change); } } } } return diff; } function createTable(idbtrans, tableName, primKey, indexes) { /// <param name="idbtrans" type="IDBTransaction"></param> var store = idbtrans.db.createObjectStore(tableName, primKey.keyPath ? { keyPath: primKey.keyPath, autoIncrement: primKey.auto } : { autoIncrement: primKey.auto }); indexes.forEach(function (idx) { addIndex(store, idx); }); return store; } function createMissingTables(newSchema, idbtrans) { Object.keys(newSchema).forEach(function (tableName) { if (!idbtrans.db.objectStoreNames.contains(tableName)) { createTable(idbtrans, tableName, newSchema[tableName].primKey, newSchema[tableName].indexes); } }); } function deleteRemovedTables(newSchema, idbtrans) { for (var i = 0; i < idbtrans.db.objectStoreNames.length; ++i) { var storeName = idbtrans.db.objectStoreNames[i]; if (newSchema[storeName] === null || newSchema[storeName] === undefined) { idbtrans.db.deleteObjectStore(storeName); } } } function addIndex(store, idx) { store.createIndex(idx.name, idx.keyPath, { unique: idx.unique, multiEntry: idx.multi }); } // // // Dexie Protected API // // this._allTables = allTables; this._tableFactory = function createTable(mode, tableSchema, transactionPromiseFactory) { /// <param name="tableSchema" type="TableSchema"></param> if (mode === READONLY) return new Table(tableSchema.name, transactionPromiseFactory, tableSchema, Collection); else return new WriteableTable(tableSchema.name, transactionPromiseFactory, tableSchema); }; this._createTransaction = function (mode, storeNames, dbschema, parentTransaction) { return new Transaction(mode, storeNames, dbschema, parentTransaction); }; function tableNotInTransaction(mode, storeNames) { throw new Error("Table " + storeNames[0] + " not part of transaction. Original Scope Function Source: " + Dexie.Promise.PSD.trans.scopeFunc.toString()); } this._transPromiseFactory = function transactionPromiseFactory(mode, storeNames, fn) { // Last argument is "writeLocked". But this doesnt apply to oneshot direct db operations, so we ignore it. if (db_is_blocked && (!Promise.PSD || !Promise.PSD.letThrough)) { // Database is paused. Wait til resumed. var blockedPromise = new Promise(function (resolve, reject) { pausedResumeables.push({ resume: function () { var p = db._transPromiseFactory(mode, storeNames, fn); blockedPromise.onuncatched = p.onuncatched; p.then(resolve, reject); } }); }); return blockedPromise; } else { var trans = db._createTransaction(mode, storeNames, globalSchema); return trans._promise(mode, function (resolve, reject) { // An uncatched operation will bubble to this anonymous transaction. Make sure // to continue bubbling it up to db.on('error'): trans.error(function (err) { db.on('error').fire(err); }); fn(function (value) { // Instead of resolving value directly, wait with resolving it until transaction has completed. // Otherwise the data would not be in the DB if requesting it in the then() operation. // Specifically, to ensure that the following expression will work: // // db.friends.put({name: "Arne"}).then(function () { // db.friends.where("name").equals("Arne").count(function(count) { // assert (count === 1); // }); // }); // trans.complete(function () { resolve(value); }); }, reject, trans); }); } }; this._whenReady = function (fn) { if (db_is_blocked && (!Promise.PSD || !Promise.PSD.letThrough)) { return new Promise(function (resolve, reject) { fakeAutoComplete(function () { new Promise(function () { fn(resolve, reject); }); }); pausedResumeables.push({ resume: function () { fn(resolve, reject); } }); }); } return new Promise(fn); }; // // // // // Dexie API // // // this.verno = 0; this.open = function () { return new Promise(function (resolve, reject) { if (idbdb || isBeingOpened) throw new Error("Database already opened or being opened"); var req, dbWasCreated = false; function openError(err) { try { req.transaction.abort(); } catch (e) { } /*if (dbWasCreated) { // Workaround for issue with some browsers. Seem not to be needed though. // Unit test "Issue#100 - not all indexes are created" works without it on chrome,FF,opera and IE. idbdb.close(); indexedDB.deleteDatabase(db.name); }*/ isBeingOpened = false; dbOpenError = err; db_is_blocked = false; reject(dbOpenError); pausedResumeables.forEach(function (resumable) { // Resume all stalled operations. They will fail once they wake up. resumable.resume(); }); pausedResumeables = []; } try { dbOpenError = null; isBeingOpened = true; // Make sure caller has specified at least one version if (versions.length === 0) { autoSchema = true; } // Multiply db.verno with 10 will be needed to workaround upgrading bug in IE: // IE fails when deleting objectStore after reading from it. // A future version of Dexie.js will stopover an intermediate version to workaround this. // At that point, we want to be backward compatible. Could have been multiplied with 2, but by using 10, it is easier to map the number to the real version number. if (!indexedDB) throw new Error("indexedDB API not found. If using IE10+, make sure to run your code on a server URL (not locally). If using Safari, make sure to include indexedDB polyfill."); req = autoSchema ? indexedDB.open(dbName) : indexedDB.open(dbName, Math.round(db.verno * 10)); req.onerror = eventRejectHandler(openError, ["opening database", dbName]); req.onblocked = function (ev) { db.on("blocked").fire(ev); }; req.onupgradeneeded = trycatch (function (e) { if (autoSchema && !db._allowEmptyDB) { // Unless an addon has specified db._allowEmptyDB, lets make the call fail. // Caller did not specify a version or schema. Doing that is only acceptable for opening alread existing databases. // If onupgradeneeded is called it means database did not exist. Reject the open() promise and make sure that we // do not create a new database by accident here. req.onerror = function (event) { event.preventDefault(); }; // Prohibit onabort error from firing before we're done! req.transaction.abort(); // Abort transaction (would hope that this would make DB disappear but it doesnt.) // Close database and delete it. req.result.close(); var delreq = indexedDB.deleteDatabase(dbName); // The upgrade transaction is atomic, and javascript is single threaded - meaning that there is no risk that we delete someone elses database here! delreq.onsuccess = delreq.onerror = function () { openError(new Error("Database '" + dbName + "' doesnt exist")); }; } else { if (e.oldVersion === 0) dbWasCreated = true; req.transaction.onerror = eventRejectHandler(openError); var oldVer = e.oldVersion > Math.pow(2, 62) ? 0 : e.oldVersion; // Safari 8 fix. runUpgraders(oldVer / 10, req.transaction, openError, req); } }, openError); req.onsuccess = trycatch(function (e) { isBeingOpened = false; idbdb = req.result; if (autoSchema) readGlobalSchema(); else if (idbdb.objectStoreNames.length > 0) adjustToExistingIndexNames(globalSchema, idbdb.transaction(safariMultiStoreFix(idbdb.objectStoreNames), READONLY)); idbdb.onversionchange = db.on("versionchange").fire; // Not firing it here, just setting the function callback to any registered subscriber. if (!hasNativeGetDatabaseNames) { // Update localStorage with list of database names globalDatabaseList(function (databaseNames) { if (databaseNames.indexOf(dbName) === -1) return databaseNames.push(dbName); }); } // Now, let any subscribers to the on("ready") fire BEFORE any other db operations resume! // If an the on("ready") subscriber returns a Promise, we will wait til promise completes or rejects before Promise.newPSD(function () { Promise.PSD.letThrough = true; // Set a Promise-Specific Data property informing that onready is firing. This will make db._whenReady() let the subscribers use the DB but block all others (!). Quite cool ha? try { var res = db.on.ready.fire(); if (res && typeof res.then === 'function') { // If on('ready') returns a promise, wait for it to complete and then resume any pending operations. res.then(resume, function (err) { idbdb.close(); idbdb = null; openError(err); }); } else { asap(resume); // Cannot call resume directly because then the pauseResumables would inherit from our PSD scope. } } catch (e) { openError(e); } function resume() { db_is_blocked = false; pausedResumeables.forEach(function (resumable) { // If anyone has made operations on a table instance before the db was opened, the operations will start executing now. resumable.resume(); }); pausedResumeables = []; resolve(); } }); }, openError); } catch (err) { openError(err); } }); }; this.close = function () { if (idbdb) { idbdb.close(); idbdb = null; db_is_blocked = true; dbOpenError = null; } }; this.delete = function () { var args = arguments; return new Promise(function (resolve, reject) { if (args.length > 0) throw new Error("Arguments not allowed in db.delete()"); function doDelete() { db.close(); var req = indexedDB.deleteDatabase(dbName); req.onsuccess = function () { if (!hasNativeGetDatabaseNames) { globalDatabaseList(function(databaseNames) { var pos = databaseNames.indexOf(dbName); if (pos >= 0) return databaseNames.splice(pos, 1); }); } resolve(); }; req.onerror = eventRejectHandler(reject, ["deleting", dbName]); req.onblocked = function() { db.on("blocked").fire(); }; } if (isBeingOpened) { pausedResumeables.push({ resume: doDelete }); } else { doDelete(); } }); }; this.backendDB = function () { return idbdb; }; this.isOpen = function () { return idbdb !== null; }; this.hasFailed = function () { return dbOpenError !== null; }; this.dynamicallyOpened = function() { return autoSchema; } /*this.dbg = function (collection, counter) { if (!this._dbgResult || !this._dbgResult[counter]) { if (typeof collection === 'string') collection = this.table(collection).toCollection().limit(100); if (!this._dbgResult) this._dbgResult = []; var db = this; new Promise(function () { Promise.PSD.letThrough = true; db._dbgResult[counter] = collection.toArray(); }); } return this._dbgResult[counter]._value; }*/ // // Properties // this.name = dbName; // db.tables - an array of all Table instances. // TODO: Change so that tables is a simple member and make sure to update it whenever allTables changes. Object.defineProperty(this, "tables", { get: function () { /// <returns type="Array" elementType="WriteableTable" /> return Object.keys(allTables).map(function (name) { return allTables[name]; }); } }); // // Events // this.on = events(this, "error", "populate", "blocked", { "ready": [promisableChain, nop], "versionchange": [reverseStoppableEventChain, nop] }); // Handle on('ready') specifically: If DB is already open, trigger the event immediately. Also, default to unsubscribe immediately after being triggered. this.on.ready.subscribe = override(this.on.ready.subscribe, function (origSubscribe) { return function (subscriber, bSticky) { function proxy () { if (!bSticky) db.on.ready.unsubscribe(proxy); return subscriber.apply(this, arguments); } origSubscribe.call(this, proxy); if (db.isOpen()) { if (db_is_blocked) { pausedResumeables.push({ resume: proxy }); } else { proxy(); } } }; }); fakeAutoComplete(function () { db.on("populate").fire(db._createTransaction(READWRITE, dbStoreNames, globalSchema)); db.on("error").fire(new Error()); }); this.transaction = function (mode, tableInstances, scopeFunc) { /// <summary> /// /// </summary> /// <param name="mode" type="String">"r" for readonly, or "rw" for readwrite</param> /// <param name="tableInstances">Table instance, Array of Table instances, String or String Array of object stores to include in the transaction</param> /// <param name="scopeFunc" type="Function">Function to execute with transaction</param> // Let table arguments be all arguments between mode and last argument. tableInstances = [].slice.call(arguments, 1, arguments.length - 1); // Let scopeFunc be the last argument scopeFunc = arguments[arguments.length - 1]; var parentTransaction = Promise.PSD && Promise.PSD.trans; // Check if parent transactions is bound to this db instance, and if caller wants to reuse it if (!parentTransaction || parentTransaction.db !== db || mode.indexOf('!') !== -1) parentTransaction = null; var onlyIfCompatible = mode.indexOf('?') !== -1; mode = mode.replace('!', '').replace('?', ''); // // Get storeNames from arguments. Either through given table instances, or through given table names. // var tables = Array.isArray(tableInstances[0]) ? tableInstances.reduce(function (a, b) { return a.concat(b); }) : tableInstances; var error = null; var storeNames = tables.map(function (tableInstance) { if (typeof tableInstance === "string") { return tableInstance; } else { if (!(tableInstance instanceof Table)) error = error || new TypeError("Invalid type. Arguments following mode must be instances of Table or String"); return tableInstance.name; } }); // // Resolve mode. Allow shortcuts "r" and "rw". // if (mode == "r" || mode == READONLY) mode = READONLY; else if (mode == "rw" || mode == READWRITE) mode = READWRITE; else error = new Error("Invalid transaction mode: " + mode); if (parentTransaction) { // Basic checks if (!error) { if (parentTransaction && parentTransaction.mode === READONLY && mode === READWRITE) { if (onlyIfCompatible) parentTransaction = null; // Spawn new transaction instead. else error = error || new Error("Cannot enter a sub-transaction with READWRITE mode when parent transaction is READONLY"); } if (parentTransaction) { storeNames.forEach(function (storeName) { if (!parentTransaction.tables.hasOwnProperty(storeName)) { if (onlyIfCompatible) parentTransaction = null; // Spawn new transaction instead. else error = error || new Error("Table " + storeName + " not included in parent transaction. Parent Transaction function: " + parentTransaction.scopeFunc.toString()); } }); } } } if (parentTransaction) { // If this is a sub-transaction, lock the parent and then launch the sub-transaction. return parentTransaction._promise(mode, enterTransactionScope, "lock"); } else { // If this is a root-level transaction, wait til database is ready and then launch the transaction. return db._whenReady(enterTransactionScope); } function enterTransactionScope(resolve, reject) { // Our transaction. To be set later. var trans = null; try { // Throw any error if any of the above checks failed. // Real error defined some lines up. We throw it here from within a Promise to reject Promise // rather than make caller need to both use try..catch and promise catching. The reason we still // throw here rather than do Promise.reject(error) is that we like to have the stack attached to the // error. Also because there is a catch() clause bound to this try() that will bubble the error // to the parent transaction. if (error) throw error; // // Create Transaction instance // trans = db._createTransaction(mode, storeNames, globalSchema, parentTransaction); // Provide arguments to the scope function (for backward compatibility) var tableArgs = storeNames.map(function (name) { return trans.tables[name]; }); tableArgs.push(trans); // If transaction completes, resolve the Promise with the return value of scopeFunc. var returnValue; var uncompletedRequests = 0; // Create a new PSD frame to hold Promise.PSD.trans. Must not be bound to the current PSD frame since we want // it to pop before then() callback is called of our returned Promise. Promise.newPSD(function () { // Let the transaction instance be part of a Promise-specific data (PSD) value. Promise.PSD.trans = trans; trans.scopeFunc = scopeFunc; // For Error ("Table " + storeNames[0] + " not part of transaction") when it happens. This may help localizing the code that started a transaction used on another place. if (parentTransaction) { // Emulate transaction commit awareness for inner transaction (must 'commit' when the inner transaction has no more operations ongoing) trans.idbtrans = parentTransaction.idbtrans; trans._promise = override(trans._promise, function (orig) { return function (mode, fn, writeLock) { ++uncompletedRequests; function proxy(fn2) { return function (val) { var retval; // _rootExec needed so that we do not loose any IDBTransaction in a setTimeout() call. Promise._rootExec(function () { retval = fn2(val); // _tickFinalize makes sure to support lazy micro tasks executed in Promise._rootExec(). // We certainly do not want to copy the bad pattern from IndexedDB but instead allow // execution of Promise.then() callbacks until the're all done. Promise._tickFinalize(function () { if (--uncompletedRequests === 0 && trans.active) { trans.active = false; trans.on.complete.fire(); // A called db operation has completed without starting a new operation. The flow is finished } }); }); return retval; } } return orig.call(this, mode, function (resolve2, reject2, trans) { return fn(proxy(resolve2), proxy(reject2), trans); }, writeLock); }; }); } trans.complete(function () { resolve(returnValue); }); // If transaction fails, reject the Promise and bubble to db if noone catched this rejection. trans.error(function (e) { if (trans.idbtrans) trans.idbtrans.onerror = preventDefault; // Prohibit AbortError from firing. try {trans.abort();} catch(e2){} if (parentTransaction) { parentTransaction.active = false; parentTransaction.on.error.fire(e); // Bubble to parent transaction } var catched = reject(e); if (!parentTransaction && !catched) { db.on.error.fire(e);// If not catched, bubble error to db.on("error"). } }); // Finally, call the scope function with our table and transaction arguments. Promise._rootExec(function() { returnValue = scopeFunc.apply(trans, tableArgs); // NOTE: returnValue is used in trans.on.complete() not as a returnValue to this func. }); }); if (!trans.idbtrans || (parentTransaction && uncompletedRequests === 0)) { trans._nop(); // Make sure transaction is being used so that it will resolve. } } catch (e) { // If exception occur, abort the transaction and reject Promise. if (trans && trans.idbtrans) trans.idbtrans.onerror = preventDefault; // Prohibit AbortError from firing. if (trans) trans.abort(); if (parentTransaction) parentTransaction.on.error.fire(e); asap(function () { // Need to use asap(=setImmediate/setTimeout) before calling reject because we are in the Promise constructor and reject() will always return false if so. if (!reject(e)) db.on("error").fire(e); // If not catched, bubble exception to db.on("error"); }); } } }; this.table = function (tableName) { /// <returns type="WriteableTable"></returns> if (!autoSchema && !allTables.hasOwnProperty(tableName)) { throw new Error("Table does not exist"); return { AN_UNKNOWN_TABLE_NAME_WAS_SPECIFIED: 1 }; } return allTables[tableName]; }; // // // // Table Class // // // function Table(name, transactionPromiseFactory, tableSchema, collClass) { /// <param name="name" type="String"></param> this.name = name; this.schema = tableSchema; this.hook = allTables[name] ? allTables[name].hook : events(null, { "creating": [hookCreatingChain, nop], "reading": [pureFunctionChain, mirror], "updating": [hookUpdatingChain, nop], "deleting": [nonStoppableEventChain, nop] }); this._tpf = transactionPromiseFactory; this._collClass = collClass || Collection; } extend(Table.prototype, function () { function failReadonly() { throw new Error("Current Transaction is READONLY"); } return { // // Table Protected Methods // _trans: function getTransaction(mode, fn, writeLocked) { return this._tpf(mode, [this.name], fn, writeLocked); }, _idbstore: function getIDBObjectStore(mode, fn, writeLocked) { var self = this; return this._tpf(mode, [this.name], function (resolve, reject, trans) { fn(resolve, reject, trans.idbtrans.objectStore(self.name), trans); }, writeLocked); }, // // Table Public Methods // get: function (key, cb) { var self = this; fakeAutoComplete(function () { cb(self.schema.instanceTemplate); }); return this._idbstore(READONLY, function (resolve, reject, idbstore) { var req = idbstore.get(key); req.onerror = eventRejectHandler(reject, ["getting", key, "from", self.name]); req.onsuccess = function () { resolve(self.hook.reading.fire(req.result)); }; }).then(cb); }, where: function (indexName) { return new WhereClause(this, indexName); }, count: function (cb) { return this.toCollection().count(cb); }, offset: function (offset) { return this.toCollection().offset(offset); }, limit: function (numRows) { return this.toCollection().limit(numRows); }, reverse: function () { return this.toCollection().reverse(); }, filter: function (filterFunction) { return this.toCollection().and(filterFunction); }, each: function (fn) { var self = this; fakeAutoComplete(function () { fn(self.schema.instanceTemplate); }); return this._idbstore(READONLY, function (resolve, reject, idbstore) { var req = idbstore.openCursor(); req.onerror = eventRejectHandler(reject, ["calling", "Table.each()", "on", self.name]); iterate(req, null, fn, resolve, reject, self.hook.reading.fire); }); }, toArray: function (cb) { var self = this; fakeAutoComplete(function () { cb([self.schema.insta