如何在没有事务自动提交的情况下使用 IndexedDB 的承诺?
How to use promises with IndexedDB without transactions auto-committing?
有没有什么方法可以在不自动提交事务的情况下使用带有承诺的 IndexedDB 和 async/await?我知道您不能在交易过程中执行诸如获取网络数据之类的操作,但是我在网上找到的有关该主题的所有内容都表明,如果您只是将 IndexedDB 包装在 promises 中,它应该仍然可以工作。
然而,在我的测试 (Firefox 73) 中,我发现简单地将请求的 onsuccess
方法包装在 Promise 中就足以导致事务在 Promise 执行之前自动提交,而相同的代码使用原始 IndexedDB API 时有效。我能做些什么?
这是我的代码的一个简化的最小示例。
const {log, error, trace, assert} = console;
const VERSION = 1;
const OBJ_STORE_NAME = 'test';
const DATA_KEY = 'data';
const META_KEY = 'last-updated';
function open_db(name, version) {
return new Promise((resolve, reject) => {
const req = indexedDB.open(name, version);
req.onerror = reject;
req.onupgradeneeded = e => {
const db = e.target.result;
for (const name of db.objectStoreNames) {db.deleteObjectStore(name);}
db.createObjectStore(OBJ_STORE_NAME);
};
req.onsuccess = e => resolve(e.target.result);
});
}
function idbreq(objs, method, ...args) {
return new Promise((resolve, reject) => {
const req = objs[method](...args);
req.onsuccess = e => resolve(req.result);
req.onerror = e => reject(req.error);
});
}
async function update_db(db) {
const new_time = (new Date).toISOString();
const new_data = 42; // simplified for sake of example
const [old_data, last_time] = await (() => {
const t = db.transaction([OBJ_STORE_NAME], 'readonly');
t.onabort = e => error('trans1 abort', e);
t.onerror = e => error('trans1 error', e);
t.oncomplete = e => log('trans1 complete', e);
const obj_store = t.objectStore(OBJ_STORE_NAME);
return Promise.all([
idbreq(obj_store, 'get', DATA_KEY),
idbreq(obj_store, 'get', META_KEY),
]);
})();
log('fetched data from db');
// do stuff with data before writing it back
(async () => {
log('beginning write callback');
const t = db.transaction([OBJ_STORE_NAME], 'readwrite');
t.onabort = e => error('trans2 abort', e);
t.onerror = e => error('trans2 error', e);
t.oncomplete = e => log('trans2 complete', e);
const obj_store = t.objectStore(OBJ_STORE_NAME);
// This line works when using onsuccess directly, but simply wrapping it in a Promise causes the
// transaction to autocommit before the rest of the code executes, resulting in an error.
obj_store.get(META_KEY).onsuccess = ({result: last_time2}) => {
log('last_time', last_time, 'last_time2', last_time2, 'new_time', new_time);
// Check if some other transaction updated db in the mean time so we don't overwrite newer data
if (!last_time2 || last_time2 < new_time) {
obj_store.put(new_time, META_KEY);
obj_store.put(new_data, DATA_KEY);
}
log('finished write callback');
};
// This version of the above code using a Promise wrapper results in an error
// idbreq(obj_store, 'get', META_KEY).then(last_time2 => {
// log('last_time', last_time, 'last_time2', last_time2, 'new_time', new_time);
// if (!last_time2 || last_time2 < new_time) {
// obj_store.put(new_time, META_KEY);
// obj_store.put(new_data, DATA_KEY);
// }
// log('finished write callback');
// });
// Ideally, I'd be able to use await like a civilized person, but the above example
// shows that IndexedDB breaks when simply using promises, even without await.
// const last_time2 = await idbreq(obj_store, 'get', META_KEY);
// log('last_time', last_time, 'last_time2', last_time2, 'new_time', new_time);
// if (!last_time2 || last_time2 < new_time) {
// obj_store.put(new_time, META_KEY);
// obj_store.put(new_data, DATA_KEY);
// }
// log('finished write callback');
})();
return [last_time, new_time];
}
open_db('test').then(update_db).then(([prev, new_]) => log(`updated db timestamp from ${prev} to ${new_}`));
围绕交易而不是单个请求编排承诺。
如果这会导致您的设计出现问题,并且您仍想使用 indexedDB,那么请围绕它进行设计。重新评估您是否需要事务安全性,或者您是否需要实际为多个请求重用一个事务,而不是创建多个事务,每个事务只有几个请求。
与使用大量请求生成少量事务相比,使用少量请求生成大量事务几乎没有开销。唯一真正关心的是一致性。
任何 await 都是伪装的 yield。当没有请求挂起时,indexedDB 事务超时。收益会导致时间间隔,因此交易会超时。
事实证明,问题出在我的代码中完全不同的部分。
在我的顶级代码结束时,我有
.catch(e => {
error('caught error', e);
alert(e);
});
我不确定细节,但显示警报似乎会导致所有事务自动提交,而承诺仍在等待中,导致我在用户单击 "ok" 时看到的错误警报弹出窗口和未决承诺继续。从我的全局错误处理程序中删除 alert
调用解决了这个问题。
有没有什么方法可以在不自动提交事务的情况下使用带有承诺的 IndexedDB 和 async/await?我知道您不能在交易过程中执行诸如获取网络数据之类的操作,但是我在网上找到的有关该主题的所有内容都表明,如果您只是将 IndexedDB 包装在 promises 中,它应该仍然可以工作。
然而,在我的测试 (Firefox 73) 中,我发现简单地将请求的 onsuccess
方法包装在 Promise 中就足以导致事务在 Promise 执行之前自动提交,而相同的代码使用原始 IndexedDB API 时有效。我能做些什么?
这是我的代码的一个简化的最小示例。
const {log, error, trace, assert} = console;
const VERSION = 1;
const OBJ_STORE_NAME = 'test';
const DATA_KEY = 'data';
const META_KEY = 'last-updated';
function open_db(name, version) {
return new Promise((resolve, reject) => {
const req = indexedDB.open(name, version);
req.onerror = reject;
req.onupgradeneeded = e => {
const db = e.target.result;
for (const name of db.objectStoreNames) {db.deleteObjectStore(name);}
db.createObjectStore(OBJ_STORE_NAME);
};
req.onsuccess = e => resolve(e.target.result);
});
}
function idbreq(objs, method, ...args) {
return new Promise((resolve, reject) => {
const req = objs[method](...args);
req.onsuccess = e => resolve(req.result);
req.onerror = e => reject(req.error);
});
}
async function update_db(db) {
const new_time = (new Date).toISOString();
const new_data = 42; // simplified for sake of example
const [old_data, last_time] = await (() => {
const t = db.transaction([OBJ_STORE_NAME], 'readonly');
t.onabort = e => error('trans1 abort', e);
t.onerror = e => error('trans1 error', e);
t.oncomplete = e => log('trans1 complete', e);
const obj_store = t.objectStore(OBJ_STORE_NAME);
return Promise.all([
idbreq(obj_store, 'get', DATA_KEY),
idbreq(obj_store, 'get', META_KEY),
]);
})();
log('fetched data from db');
// do stuff with data before writing it back
(async () => {
log('beginning write callback');
const t = db.transaction([OBJ_STORE_NAME], 'readwrite');
t.onabort = e => error('trans2 abort', e);
t.onerror = e => error('trans2 error', e);
t.oncomplete = e => log('trans2 complete', e);
const obj_store = t.objectStore(OBJ_STORE_NAME);
// This line works when using onsuccess directly, but simply wrapping it in a Promise causes the
// transaction to autocommit before the rest of the code executes, resulting in an error.
obj_store.get(META_KEY).onsuccess = ({result: last_time2}) => {
log('last_time', last_time, 'last_time2', last_time2, 'new_time', new_time);
// Check if some other transaction updated db in the mean time so we don't overwrite newer data
if (!last_time2 || last_time2 < new_time) {
obj_store.put(new_time, META_KEY);
obj_store.put(new_data, DATA_KEY);
}
log('finished write callback');
};
// This version of the above code using a Promise wrapper results in an error
// idbreq(obj_store, 'get', META_KEY).then(last_time2 => {
// log('last_time', last_time, 'last_time2', last_time2, 'new_time', new_time);
// if (!last_time2 || last_time2 < new_time) {
// obj_store.put(new_time, META_KEY);
// obj_store.put(new_data, DATA_KEY);
// }
// log('finished write callback');
// });
// Ideally, I'd be able to use await like a civilized person, but the above example
// shows that IndexedDB breaks when simply using promises, even without await.
// const last_time2 = await idbreq(obj_store, 'get', META_KEY);
// log('last_time', last_time, 'last_time2', last_time2, 'new_time', new_time);
// if (!last_time2 || last_time2 < new_time) {
// obj_store.put(new_time, META_KEY);
// obj_store.put(new_data, DATA_KEY);
// }
// log('finished write callback');
})();
return [last_time, new_time];
}
open_db('test').then(update_db).then(([prev, new_]) => log(`updated db timestamp from ${prev} to ${new_}`));
围绕交易而不是单个请求编排承诺。
如果这会导致您的设计出现问题,并且您仍想使用 indexedDB,那么请围绕它进行设计。重新评估您是否需要事务安全性,或者您是否需要实际为多个请求重用一个事务,而不是创建多个事务,每个事务只有几个请求。
与使用大量请求生成少量事务相比,使用少量请求生成大量事务几乎没有开销。唯一真正关心的是一致性。
任何 await 都是伪装的 yield。当没有请求挂起时,indexedDB 事务超时。收益会导致时间间隔,因此交易会超时。
事实证明,问题出在我的代码中完全不同的部分。
在我的顶级代码结束时,我有
.catch(e => {
error('caught error', e);
alert(e);
});
我不确定细节,但显示警报似乎会导致所有事务自动提交,而承诺仍在等待中,导致我在用户单击 "ok" 时看到的错误警报弹出窗口和未决承诺继续。从我的全局错误处理程序中删除 alert
调用解决了这个问题。