IndexedDB:承诺升级

IndexedDB: upgrade with promises?

本文关键字:承诺 IndexedDB      更新时间:2023-09-26

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

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

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

  • 如果数据库不需要升级,则不会调用onupgraderequired回调(因此,如果数据库不需升级,则在升级完成时解决的承诺将永远不会得到解决,并且调用代码不知道在连接回调时是否会出现这种情况)
  • 如果一个升级依赖于另一个升级(例如填充您刚刚创建的存储),则必须等待调用其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; });
var open = function(name, ver) {
  return new Promise(function(yes, no) {
     var req = indexedDB.open(name, var);
     req.onupgradedneeded = function(res) {
       no(req);
       req.onsuccess = null; // for clarity
     };
     req.onsuccess = function() {
       yes(res.result);
     };
     req.onblocked = no;
  }
});
open('db name', 3).then(function(db) {
  // use db here
 }, function(req) {
   // version upgrade logic here
   if (req instanceof IDBResult) {
    return new Promise(function(yes, no) {
      req.transaction.createObjectStore('store_3');
      req.onsuccess = function() {
        yes(req.result);
      });
    });
  }
});
       

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

  • 架构更改只能在versionchange事务期间执行
  • ,但数据更改不能在versionchange事务期间执行,因此我们必须区分数据迁移和模式迁移,并以不同的方式使用不同的事务执行它们 update数据更改可以在versionchange事务期间执行,但不能通过通常的db.transaction('readwrite', ...).objectstore(...)方法执行-这会引发异常。而是使用对versionchange事务的引用
  • 为了允许模式创建和填充的任意交错,我们必须将它们视为单独的迁移步骤,并且在前一步骤的事务成功后只尝试一个步骤
  • 规范明确禁止显式事务管理(https://dvcs.w3.org/hg/IndexedDB/raw-file/tip/Overview.html#transaction-概念)这限制了事务在事件循环完成后被标记为非活动时可以重复使用的范围
  • 因此,方法.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; });

闭包库具有IndexedDB的promise包装器。它的开放数据库方法返回一个promise,IDB中的所有内容都用promise包装。

所以这和你的想法很相似。

以下是其中的示例摘录:

  goog.db.openDatabase('mydb', 1, function(ev, db, tx) {
    db.createObjectStore('mystore');
  }).addCallback(function(db) {
    var putTx = db.createTransaction(
        [],
        goog.db.Transaction.TransactionMode.READ_WRITE);
    var store = putTx.objectStore('mystore');
    store.put('value', 'key');
    goog.listen(putTx, goog.db.Transaction.EventTypes.COMPLETE, function() {
      var getTx = db.createTransaction([]);
      var request = getTx.objectStore('mystore').get('key');
      request.addCallback(function(result) {
        ...
      });
  });

我从那里开始,但最终得到的API并没有很好地工作,所以我编写了IndexedDB包装器,再次使用闭包库。

使用promise包装所有内容的问题并不容易使用,您需要跟踪活动事务。正如您所看到的,对于数据库查询,您将有三个级别的promise,1)数据库打开promise 2)事务promise 3)请求promise。

您可以通过在global上始终保留db作为应用程序的引用来消除第一个承诺,但后两个承诺仍然存在。

因此,主要是,我将这三个承诺合并为一个关于我的图书馆的承诺。