我怎么知道 IDBCursor 达到了它的最后一个值?
How can I know that IDBCursor reached it's last value?
我试着承诺 IDBCursor
,像这样:
/**
* @description Allows asynchronous looping over IDBCursor. The cursor request must be provided and must be new and unused!
*/
class IndexedDBAsyncCursor {
/**
*
* @param {IndexedDBAsyncTable} parentTable
* @param {IDBRequest} cursorRequest
*/
constructor(parentTable, cursorRequest) {
this.cursorRequest = cursorRequest;
this.table = parentTable;
/** @type {Promise<IDBCursor>} **/
this.nextValuePromise = null;
/** @type {IDBCursor} **/
this.lastCursor = null;
this.hasNext = true;
this.hookCursorRequest();
}
/**
* @description Starts waiting for the next value
* @private
*/
makeNextValuePromise() {
if (this.nextValuePromise == null) {
this.rejectPromise = null;
this.resolvePromise =null;
this.nextValuePromise = new Promise((resolve, reject) => {
this.rejectPromise = reject;
this.resolvePromise = resolve;
});
}
}
/**
* Adds event listeners on the cursor
* @private
*/
hookCursorRequest() {
this.makeNextValuePromise();
this.cursorRequest.onsuccess = (event) => {
/** @type {IDBCursor} **/
const cursor = event.target.result;
this.lastCursor = cursor;
if (cursor) {
console.log("[IDB CURSOR] Next value: ", cursor);
this.resolvePromise(cursor);
}
else {
this.hasNext = false;
this.resolvePromise(null);
console.log("[IDB CURSOR] End.");
}
};
this.cursorRequest.onerror = (event) => {
this.hasNext = false;
this.rejectPromise(event);
}
}
/**
* @description Resolves with null or an IDBCursor
* @returns {Promise<IDBCursor>}
*/
async next() {
if (!this.hasNext)
return null;
if (this.lastCursor != null) {
this.makeNextValuePromise();
this.lastCursor.continue();
}
const result = await this.nextValuePromise;
this.nextValuePromise = null;
return result;
}
}
预期用途:
const cursor = new IndexedDBAsyncCursor(this, objectStore.openCursor());
/** @type {IDBCursor} **/
var value = null;
while (value = await cursor.next()) {
if (predicate(value)) {
values.push(value.value);
console.log("[IDB] Found value: ",value.value)
if (oneOnly)
break;
}
else {
console.log("[IDB] Value does not match predicate: ",value.value)
}
}
问题是这段代码:
else {
this.hasNext = false;
this.resolvePromise(null);
console.log("[IDB CURSOR] End.");
}
问题是一旦达到最后一个值就不会再次调用onsuccess
。它不再被调用,而我假设它最后一次被调用时使用 null
而不是 IDBCursor
。但是没有这样的事情发生。
如何正确执行此操作?
对这个答案并不完全有信心,但我认为您不能按照游标请求承诺来做。您可以围绕整个游标行走做出承诺,但不是每次迭代。
原因有点复杂,但它与微任务有关,以及事务在未及时检测到调用 cursor.continue
的下一个请求时如何超时。
引用如下:
The Problem Indexed DB transactions compose poorly with Promises.
Transactions are defined as having an active flag that is set when the
transaction is created, and when an IDB event callback from a source
associated with that transaction is run. The active flag is cleared
when the task completes i.e. when control returns from script; for
example, at the end of the callback. Operations within a transaction
(put, get, etc) are only permitted when the flag is true. This implies
that you cannot perform operations within a Promise callback, since it
is by definition not an IDB event callback. Further, transactions
automatically attempt to commit when the flag is cleared and there are
no pending requests. This implies that even if the previous
restriction was lifted, the transaction would commit before any
Promise callback fired. If the active flag mechanism were to be
removed entirely, a new commit model would need to be introduced.
所提供的代码适用于 Chrome,但不适用于 Firefox。仅供参考,这是我用来驱动它的:
indexedDB.deleteDatabase('so');
const open = indexedDB.open('so');
open.onupgradeneeded = e => {
const db = open.result;
const s = db.createObjectStore('s');
for (let i = 0; i < 4; ++i) {
s.put({name: 's' + i, num: i}, i);
}
};
open.onsuccess = async e => {
const db = open.result;
const tx = db.transaction('s');
const objectStore = tx.objectStore('s')
const values = [], oneOnly = false;
const predicate = x => true;
const cursor = new IndexedDBAsyncCursor(this,objectStore.openCursor());
/** @type {IDBCursor} **/
var value = null;
while (value = await cursor.next()) {
if (predicate(value)) {
values.push(value.value);
console.log("[IDB] Found value: ",value.value)
if (oneOnly)
break;
}
else {
console.log("[IDB] Value does not match predicate: ",value.value)
}
}
};
在 Chrome 中,此记录:
[IDB CURSOR] Next value: IDBCursorWithValue {value: {…}, source: IDBObjectStore, direction: "next", key: 0, primaryKey: 0}
[IDB] Found value: {name: "s0", num: 0}
[IDB CURSOR] Next value: IDBCursorWithValue {value: {…}, source: IDBObjectStore, direction: "next", key: 1, primaryKey: 1}
[IDB] Found value: {name: "s1", num: 1}
[IDB CURSOR] Next value: IDBCursorWithValue {value: {…}, source: IDBObjectStore, direction: "next", key: 2, primaryKey: 2}
[IDB] Found value: {name: "s2", num: 2}
[IDB CURSOR] Next value: IDBCursorWithValue {value: {…}, source: IDBObjectStore, direction: "next", key: 3, primaryKey: 3}
[IDB] Found value: {name: "s3", num: 3}
[IDB CURSOR] End.
问题是 Chrome 和 Firefox 在执行微任务(即 Promises 的 "then" 回调)时不一致。 Chrome 是per-spec,微任务在整个任务中执行。 Firefox 尚未修复,微任务稍后执行。更多详情请见 https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
正如 other-Josh 所暗示的,这是一个问题,因为在 Firefox 中,continue()
调用最终发生在“success
”事件处理程序之外,这是不允许的。您可以通过对 async next()
实现进行以下代码更改来查看:
try {
this.lastCursor.continue();
} catch (ex) { console.error(ex.name, ex.message); }
在 Firefox 中,此记录:"TransactionInactiveError A request was placed against a transaction which is currently not active, or which is finished." -- 因为 Firefox 在事件任务之外执行包含下一个调用的微任务,事务不再处于活动状态。
为了在 Firefox 中修复微任务问题之前完成这项工作,有必要重组您的代码以直接在“success
”处理程序中调用 continue()
,无论是否不会使用下一个值,这将需要修改您 track/generate 的承诺。
请注意,即使在 Firefox 中修复了微任务问题,不幸的是,编写的代码仍然很脆弱,因为在 while
循环中执行任何其他异步操作(即引入另一个 await
)可能会推动continue()
调用任务。所以如果你想,例如在迭代时做一个 fetch()
会中断。如上所述直接在“success
”处理程序中执行 continue()
将使它更健壮。
我试着承诺 IDBCursor
,像这样:
/**
* @description Allows asynchronous looping over IDBCursor. The cursor request must be provided and must be new and unused!
*/
class IndexedDBAsyncCursor {
/**
*
* @param {IndexedDBAsyncTable} parentTable
* @param {IDBRequest} cursorRequest
*/
constructor(parentTable, cursorRequest) {
this.cursorRequest = cursorRequest;
this.table = parentTable;
/** @type {Promise<IDBCursor>} **/
this.nextValuePromise = null;
/** @type {IDBCursor} **/
this.lastCursor = null;
this.hasNext = true;
this.hookCursorRequest();
}
/**
* @description Starts waiting for the next value
* @private
*/
makeNextValuePromise() {
if (this.nextValuePromise == null) {
this.rejectPromise = null;
this.resolvePromise =null;
this.nextValuePromise = new Promise((resolve, reject) => {
this.rejectPromise = reject;
this.resolvePromise = resolve;
});
}
}
/**
* Adds event listeners on the cursor
* @private
*/
hookCursorRequest() {
this.makeNextValuePromise();
this.cursorRequest.onsuccess = (event) => {
/** @type {IDBCursor} **/
const cursor = event.target.result;
this.lastCursor = cursor;
if (cursor) {
console.log("[IDB CURSOR] Next value: ", cursor);
this.resolvePromise(cursor);
}
else {
this.hasNext = false;
this.resolvePromise(null);
console.log("[IDB CURSOR] End.");
}
};
this.cursorRequest.onerror = (event) => {
this.hasNext = false;
this.rejectPromise(event);
}
}
/**
* @description Resolves with null or an IDBCursor
* @returns {Promise<IDBCursor>}
*/
async next() {
if (!this.hasNext)
return null;
if (this.lastCursor != null) {
this.makeNextValuePromise();
this.lastCursor.continue();
}
const result = await this.nextValuePromise;
this.nextValuePromise = null;
return result;
}
}
预期用途:
const cursor = new IndexedDBAsyncCursor(this, objectStore.openCursor());
/** @type {IDBCursor} **/
var value = null;
while (value = await cursor.next()) {
if (predicate(value)) {
values.push(value.value);
console.log("[IDB] Found value: ",value.value)
if (oneOnly)
break;
}
else {
console.log("[IDB] Value does not match predicate: ",value.value)
}
}
问题是这段代码:
else {
this.hasNext = false;
this.resolvePromise(null);
console.log("[IDB CURSOR] End.");
}
问题是一旦达到最后一个值就不会再次调用onsuccess
。它不再被调用,而我假设它最后一次被调用时使用 null
而不是 IDBCursor
。但是没有这样的事情发生。
如何正确执行此操作?
对这个答案并不完全有信心,但我认为您不能按照游标请求承诺来做。您可以围绕整个游标行走做出承诺,但不是每次迭代。
原因有点复杂,但它与微任务有关,以及事务在未及时检测到调用 cursor.continue
的下一个请求时如何超时。
引用如下:
The Problem Indexed DB transactions compose poorly with Promises.
Transactions are defined as having an active flag that is set when the transaction is created, and when an IDB event callback from a source associated with that transaction is run. The active flag is cleared when the task completes i.e. when control returns from script; for example, at the end of the callback. Operations within a transaction (put, get, etc) are only permitted when the flag is true. This implies that you cannot perform operations within a Promise callback, since it is by definition not an IDB event callback. Further, transactions automatically attempt to commit when the flag is cleared and there are no pending requests. This implies that even if the previous restriction was lifted, the transaction would commit before any Promise callback fired. If the active flag mechanism were to be removed entirely, a new commit model would need to be introduced.
所提供的代码适用于 Chrome,但不适用于 Firefox。仅供参考,这是我用来驱动它的:
indexedDB.deleteDatabase('so');
const open = indexedDB.open('so');
open.onupgradeneeded = e => {
const db = open.result;
const s = db.createObjectStore('s');
for (let i = 0; i < 4; ++i) {
s.put({name: 's' + i, num: i}, i);
}
};
open.onsuccess = async e => {
const db = open.result;
const tx = db.transaction('s');
const objectStore = tx.objectStore('s')
const values = [], oneOnly = false;
const predicate = x => true;
const cursor = new IndexedDBAsyncCursor(this,objectStore.openCursor());
/** @type {IDBCursor} **/
var value = null;
while (value = await cursor.next()) {
if (predicate(value)) {
values.push(value.value);
console.log("[IDB] Found value: ",value.value)
if (oneOnly)
break;
}
else {
console.log("[IDB] Value does not match predicate: ",value.value)
}
}
};
在 Chrome 中,此记录:
[IDB CURSOR] Next value: IDBCursorWithValue {value: {…}, source: IDBObjectStore, direction: "next", key: 0, primaryKey: 0}
[IDB] Found value: {name: "s0", num: 0}
[IDB CURSOR] Next value: IDBCursorWithValue {value: {…}, source: IDBObjectStore, direction: "next", key: 1, primaryKey: 1}
[IDB] Found value: {name: "s1", num: 1}
[IDB CURSOR] Next value: IDBCursorWithValue {value: {…}, source: IDBObjectStore, direction: "next", key: 2, primaryKey: 2}
[IDB] Found value: {name: "s2", num: 2}
[IDB CURSOR] Next value: IDBCursorWithValue {value: {…}, source: IDBObjectStore, direction: "next", key: 3, primaryKey: 3}
[IDB] Found value: {name: "s3", num: 3}
[IDB CURSOR] End.
问题是 Chrome 和 Firefox 在执行微任务(即 Promises 的 "then" 回调)时不一致。 Chrome 是per-spec,微任务在整个任务中执行。 Firefox 尚未修复,微任务稍后执行。更多详情请见 https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
正如 other-Josh 所暗示的,这是一个问题,因为在 Firefox 中,continue()
调用最终发生在“success
”事件处理程序之外,这是不允许的。您可以通过对 async next()
实现进行以下代码更改来查看:
try {
this.lastCursor.continue();
} catch (ex) { console.error(ex.name, ex.message); }
在 Firefox 中,此记录:"TransactionInactiveError A request was placed against a transaction which is currently not active, or which is finished." -- 因为 Firefox 在事件任务之外执行包含下一个调用的微任务,事务不再处于活动状态。
为了在 Firefox 中修复微任务问题之前完成这项工作,有必要重组您的代码以直接在“success
”处理程序中调用 continue()
,无论是否不会使用下一个值,这将需要修改您 track/generate 的承诺。
请注意,即使在 Firefox 中修复了微任务问题,不幸的是,编写的代码仍然很脆弱,因为在 while
循环中执行任何其他异步操作(即引入另一个 await
)可能会推动continue()
调用任务。所以如果你想,例如在迭代时做一个 fetch()
会中断。如上所述直接在“success
”处理程序中执行 continue()
将使它更健壮。