IndexedDB:使用promises升级?

Tom*_*ver 5 javascript promise indexeddb

刚刚开始我的第一个使用IndexedDb的项目,我很难在第一次使用时创建一个用于打开和升级数据库的系统.我想使用promises(当前的angularJs$q服务,但我很灵活)给我一些关于捕获任何错误的保证,并减少关于失败模式的心理开销推理.我的要求是:

  • 使用者调用某些函数来打开和升级返回promise的数据库
  • 该功能按顺序执行所有必需的升级/迁移.如果没有发生错误,则通过与数据库的连接解决promise
  • 如果在任何阶段发生任何错误,则保证拒绝承诺错误
  • 添加新的迁移/升级步骤就像定义执行升级的功能一样简单,所有其他并发问题都由"框架"处理.

到目前为止我遇到的问题:

  • onupgraderequired如果DB不需要升级(这样一个承诺,得到了在升级完成解决将永远不会得到解决,如果DB不需要升级回调不叫,并调用代码不知道这是否会是这样接线回调时)
  • 如果一次升级依赖于另一次升级(例如,填充刚创建的商店),则必须等到其onsuccess回调被调用 - 因此每次升级都需要顺序链接
  • 看来,在链中的前任解决之后执行的承诺的延迟足以在再次需要之前将"事务"标记为非活动状态(我认为它们被安排为'nextTick',这可能是相同的机制这使交易失效).
  • 更新如果一个升级依赖于另一个升级,则在onsuccess调用第一个回调时,该versionchange事务不再处于活动状态.

我目前的结论是,API基本上对基于承诺的方法持敌对态度.我最好的尝试在下面(简化了一点以便于阅读).我哪里错了?

var newPromise = function(withDeferred) {
    var deferred = $q.defer();
    try {
        withDeferred(deferred);
    } catch (err) {
        deferred.reject(err); 
    }
    return deferred.promise;
};

var newTransactionPromise = function(getTransaction) {
    return newPromise(function(deferred) {
        var transaction = getTransaction();

        transaction.oncomplete = function(ev) { deferred.resolve(); };
        transaction.onabort = function(ev) { deferred.reject(transaction.error); };
    });
};

var migrations = [
    function(db) {
        return newTransactionPromise(function() {
            // throws: The database is not running a version change transaction.
            return db
                .createObjectStore("entries", { keyPath: 'id', autoIncrement: true })
                .transaction;
        });
    },
    function(db) {
        return newTransactionPromise(function()
        {
            var entryStore = db.transaction("entries", "readwrite").objectStore("entries");
            entryStore.add({ description: "First task" });
            return entryStore.transaction;
        });
    }
];

var upgradeAndOpen = function() {
    return newPromise(function(deferred) {
        var latest_version = migrations.length;
        var request = indexedDB.open("caesium", latest_version);

        request.onupgradeneeded = function(event) {
            try {
                // create an already resolved promise to start a chain
                var setupDeferred = $q.defer(); 
                setupDeferred.resolve();
                var setupComplete = setupDeferred.promise;

                for (var v = event.oldVersion; v < latest_version; v++)
                {
                    // Problem: the versionchange transaction will be 'inactive' before this promise is scheduled
                    var nextMigration = migrations[v].bind(this, request.result);
                    setupComplete = setupComplete.then(nextMigration);
                }

                setupComplete["catch"](deferred.reject);
            } catch (err) {
                deferred.reject(err);
            }
        };

        request.onerror = function(event) { deferred.reject(request.error); };
        request.onsuccess = function(event) { deferred.resolve(request.result); };
    });
};

upgradeAndOpen()["catch"](function(err) { $scope.status = err; });
Run Code Online (Sandbox Code Playgroud)

Tom*_*ver 1

我终于找到了一种方法来避免这个 API 的所有麻烦,并找到了一个解决方案,该解决方案公开了一个干净的基于承诺的接口,并推广到任意数量的数据库迁移。关键问题:

  • 架构更改只能事务期间执行versionchange但是数据更改不能在事务期间执行versionchange,因此我们必须区分数据迁移和模式迁移,并通过不同的事务以不同的方式执行它们。 更新数据更改可以在事务期间执行versionchange,但不能通过通常的db.transaction('readwrite', ...).objectstore(...)方法执行 - 这会引发异常。而是使用对交易的引用versionchange
  • 为了允许模式创建和填充的任意交错,我们必须将它们视为单独的迁移步骤,并且只有在上一步的事务成功后才尝试执行一个步骤。
  • 规范明确禁止显式事务管理(https://dvcs.w3.org/hg/IndexedDB/raw-file/tip/Overview.html#transaction-concept ),这限制了事务可重用的程度因为一旦事件循环完成它们就会被标记为非活动状态
  • 因此该方法.open(dbName, version)只允许一笔versionchange交易,一旦成功就消失了。versionchange该方法是创建交易的唯一方法
  • 因此,多个迁移步骤需要多次连续调用.open(dbName, version)
  • versionchange当其他数据库连接打开时,事务会阻塞,因此在尝试链中的下一次迁移之前必须关闭每个连接。

我为协商所有这些问题而想出的代码如下。

var newPromise = function(withDeferred) {
    var deferred = $q.defer();
    try {
        withDeferred(deferred);
    } catch (err) {
        deferred.reject(err); 
    }
    return deferred.promise;
};

var newTransactionPromise = function(getTransaction) {
    return newPromise(function(deferred) {
        var transaction = getTransaction();

        transaction.oncomplete = function(ev) { deferred.resolve(); };
        transaction.onabort = function(ev) { deferred.reject(transaction.error); };
    });
};

var newMigrationPromise = function(dbName, version, migration) {
    return newPromise(function(deferred) {
        var request = indexedDB.open(dbName, version);

        // NB: caller must ensure upgrade callback always called
        request.onupgradeneeded = function(event) {
            var db = request.result;
            newTransactionPromise(
                function() {
                    migration(db, request.transaction);
                    return request.transaction;
                })
                .then(function() { db.close(); })
                .then(deferred.resolve, deferred.reject);
        };

        request.onerror = function(ev) { deferred.reject(request.error); };
    });
};

var migrations = [
    function(db, transaction) {
        db.createObjectStore("entries", { keyPath: 'id', autoIncrement: true });
    },
    function(db, transactionn) {
        db.createObjectStore("entries2", { keyPath: 'id', autoIncrement: true });
    },
    function(db, transaction) {
        var entryStore = transaction.objectStore("entries");

        entryStore.add({description: "First task"});
    }
];

var upgradeAndOpen = function() {
    return open()
        .then(function(db) {
            var version = db.version;
            db.close(); // this connection will block the upgrade (AFAICT)
            return version;
        })
        .then(function(version) {
            return newPromise(function(deferred) {
                // version when created but empty is v1
                // so after the first migration (index: 0) the version must be 2
                var migrationsPerformed = version - 1;
                var migrationCount = migrations.length;

                var previousMigration = newPromise(function(deferred) { deferred.resolve(); });

                for (var index = migrationsPerformed; index < migrationCount; index++)
                {
                    var performNextMigration = newMigrationPromise.bind(this, "caesium", index+2, migrations[index]);
                    previousMigration = previousMigration.then(performNextMigration);
                }

                previousMigration.then(deferred.resolve, deferred.reject);
            });
        })
        .then(open);
};

var open = function() {
    return newPromise(function(deferred) {
        var request = indexedDB.open("caesium");

        request.onsuccess = function(ev) { deferred.resolve(request.result); };
        request.onerror = function(ev) { deferred.reject(request.error); };
    });
};

upgradeAndOpen()
    .then(function() { $scope.status = "completed"; }, function(err) { $scope.status = err; });
Run Code Online (Sandbox Code Playgroud)