idb: fix renaming, make renaming tests pass

This commit is contained in:
Florian Dold 2021-02-17 11:45:28 +01:00
parent 47bddb2a1b
commit 69b62c62a0
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
4 changed files with 264 additions and 181 deletions

View File

@ -55,7 +55,7 @@ import {
TransactionInactiveError, TransactionInactiveError,
VersionError, VersionError,
} from "./util/errors"; } from "./util/errors";
import { fakeDOMStringList } from "./util/fakeDOMStringList"; import { FakeDOMStringList, fakeDOMStringList } from "./util/fakeDOMStringList";
import FakeEvent from "./util/FakeEvent"; import FakeEvent from "./util/FakeEvent";
import FakeEventTarget from "./util/FakeEventTarget"; import FakeEventTarget from "./util/FakeEventTarget";
import { makeStoreKeyValue } from "./util/makeStoreKeyValue"; import { makeStoreKeyValue } from "./util/makeStoreKeyValue";
@ -73,12 +73,6 @@ import { valueToKey } from "./util/valueToKey";
/** @public */ /** @public */
export type CursorSource = BridgeIDBIndex | BridgeIDBObjectStore; export type CursorSource = BridgeIDBIndex | BridgeIDBObjectStore;
/** @public */
export interface FakeDOMStringList extends Array<string> {
contains: (value: string) => boolean;
item: (i: number) => string | undefined;
}
/** @public */ /** @public */
export interface RequestObj { export interface RequestObj {
operation: () => Promise<any>; operation: () => Promise<any>;
@ -828,7 +822,9 @@ export class BridgeIDBFactory {
); );
// We need to expose the new version number to the upgrade transaction. // We need to expose the new version number to the upgrade transaction.
db._schema = this.backend.getCurrentTransactionSchema(backendTransaction); db._schema = this.backend.getCurrentTransactionSchema(
backendTransaction,
);
const transaction = db._internalTransaction( const transaction = db._internalTransaction(
[], [],
@ -1405,6 +1401,14 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
this._transaction._db._schema = this._backend.getCurrentTransactionSchema( this._transaction._db._schema = this._backend.getCurrentTransactionSchema(
btx, btx,
); );
// We don't modify scope, as the scope of the transaction
// doesn't matter if we're in an upgrade transaction.
this._transaction._objectStoresCache.delete(oldName);
this._transaction._objectStoresCache.set(newName, this);
this._transaction._cachedObjectStoreNames = undefined;
this._name = newName;
} }
public _store(value: any, key: IDBValidKey | undefined, overwrite: boolean) { public _store(value: any, key: IDBValidKey | undefined, overwrite: boolean) {
@ -1910,9 +1914,19 @@ export class BridgeIDBTransaction
_backendTransaction?: DatabaseTransaction; _backendTransaction?: DatabaseTransaction;
_objectStoreNames: FakeDOMStringList; _cachedObjectStoreNames: DOMStringList | undefined;
get objectStoreNames(): DOMStringList { get objectStoreNames(): DOMStringList {
return this._objectStoreNames as DOMStringList; if (!this._cachedObjectStoreNames) {
if (this._openRequest) {
this._cachedObjectStoreNames = this._db.objectStoreNames;
} else {
this._cachedObjectStoreNames = fakeDOMStringList(
Array.from(this._scope).sort(),
);
}
}
return this._cachedObjectStoreNames;
} }
mode: IDBTransactionMode; mode: IDBTransactionMode;
_db: BridgeIDBDatabase; _db: BridgeIDBDatabase;
@ -1961,7 +1975,6 @@ export class BridgeIDBTransaction
this._backendTransaction = backendTransaction; this._backendTransaction = backendTransaction;
this.mode = mode; this.mode = mode;
this._db = db; this._db = db;
this._objectStoreNames = fakeDOMStringList(Array.from(this._scope).sort());
this._db._transactions.push(this); this._db._transactions.push(this);
@ -2049,12 +2062,24 @@ export class BridgeIDBTransaction
throw new InvalidStateError(); throw new InvalidStateError();
} }
if (!this._db._schema.objectStores[name]) {
throw new NotFoundError();
}
if (!this._db._upgradeTransaction) {
if (!this._scope.has(name)) {
throw new NotFoundError();
}
}
const objectStore = this._objectStoresCache.get(name); const objectStore = this._objectStoresCache.get(name);
if (objectStore !== undefined) { if (objectStore !== undefined) {
return objectStore; return objectStore;
} }
return new BridgeIDBObjectStore(this, name); const newObjectStore = new BridgeIDBObjectStore(this, name);
this._objectStoresCache.set(name, newObjectStore);
return newObjectStore;
} }
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-steps-for-asynchronously-executing-a-request // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-steps-for-asynchronously-executing-a-request

View File

@ -1,4 +1,6 @@
import test from "ava"; import test from "ava";
import { BridgeIDBVersionChangeEvent } from "../bridge-idb";
import FakeEvent from "../util/FakeEvent";
import { createdb, format_value, idbFactory } from "./wptsupport"; import { createdb, format_value, idbFactory } from "./wptsupport";
// IDBFactory.open() - request has no source // IDBFactory.open() - request has no source
@ -466,3 +468,62 @@ test("WPT idbfactory-open11.htm", async (t) => {
}); });
t.pass(); t.pass();
}); });
// IDBFactory.open() - upgradeneeded gets VersionChangeEvent
test("WPT idbfactory-open12.htm", async (t) => {
const indexedDB = idbFactory;
var db: any;
var open_rq = createdb(t, undefined, 9);
await new Promise<void>((resolve, reject) => {
open_rq.onupgradeneeded = function (e: any) {
db = e.target.result;
t.true(
e instanceof BridgeIDBVersionChangeEvent,
"e instanceof IDBVersionChangeEvent",
);
t.deepEqual(e.oldVersion, 0, "oldVersion");
t.deepEqual(e.newVersion, 9, "newVersion");
t.deepEqual(e.type, "upgradeneeded", "event type");
t.deepEqual(db.version, 9, "db.version");
};
open_rq.onsuccess = function (e) {
t.true(e instanceof FakeEvent, "e instanceof Event");
t.false(
e instanceof BridgeIDBVersionChangeEvent,
"e not instanceof IDBVersionChangeEvent",
);
t.deepEqual(e.type, "success", "event type");
resolve();
};
});
await new Promise<void>((resolve, reject) => {
/**
* Second test
*/
db.onversionchange = function () {
db.close();
};
var open_rq2 = createdb(t, db.name, 10);
open_rq2.onupgradeneeded = function (e: any) {
var db2 = e.target.result;
t.true(
e instanceof BridgeIDBVersionChangeEvent,
"e instanceof IDBVersionChangeEvent",
);
t.deepEqual(e.oldVersion, 9, "oldVersion");
t.deepEqual(e.newVersion, 10, "newVersion");
t.deepEqual(e.type, "upgradeneeded", "event type");
t.deepEqual(db2.version, 10, "new db.version");
resolve();
};
});
t.pass();
});

View File

@ -1,13 +1,10 @@
import test, { ExecutionContext } from "ava"; import test, { ExecutionContext } from "ava";
import { BridgeIDBRequest } from "..";
import { EventTarget, IDBDatabase } from "../idbtypes";
import { import {
checkStoreContents, checkStoreContents,
checkStoreGenerator, checkStoreGenerator,
checkStoreIndexes, checkStoreIndexes,
createBooksStore, createBooksStore,
createDatabase, createDatabase,
createdb,
createNotBooksStore, createNotBooksStore,
migrateDatabase, migrateDatabase,
} from "./wptsupport"; } from "./wptsupport";
@ -15,183 +12,175 @@ import {
// IndexedDB: object store renaming support // IndexedDB: object store renaming support
// IndexedDB object store rename in new transaction // IndexedDB object store rename in new transaction
test("WPT idbobjectstore-rename-store.html (subtest 1)", async (t) => { test("WPT idbobjectstore-rename-store.html (subtest 1)", async (t) => {
await new Promise<void>((resolve, reject) => { let bookStore: any = null;
let bookStore: any = null; let bookStore2: any = null;
let bookStore2: any = null; let renamedBookStore: any = null;
let renamedBookStore: any = null; let renamedBookStore2: any = null;
let renamedBookStore2: any = null; await createDatabase(t, (database, transaction) => {
bookStore = createBooksStore(t, database);
return createDatabase(t, (database, transaction) => { })
bookStore = createBooksStore(t, database); .then((database) => {
t.deepEqual(
database.objectStoreNames as any,
["books"],
'Test setup should have created a "books" object store',
);
const transaction = database.transaction("books", "readonly");
bookStore2 = transaction.objectStore("books");
return checkStoreContents(
t,
bookStore2,
"The store should have the expected contents before any renaming",
).then(() => database.close());
}) })
.then((database) => { .then(() =>
t.deepEqual( migrateDatabase(t, 2, (database, transaction) => {
database.objectStoreNames as any, renamedBookStore = transaction.objectStore("books");
["books"], renamedBookStore.name = "renamed_books";
'Test setup should have created a "books" object store',
);
const transaction = database.transaction("books", "readonly");
bookStore2 = transaction.objectStore("books");
return checkStoreContents(
t,
bookStore2,
"The store should have the expected contents before any renaming",
).then(() => database.close());
})
.then(() =>
migrateDatabase(t, 2, (database, transaction) => {
renamedBookStore = transaction.objectStore("books");
renamedBookStore.name = "renamed_books";
t.deepEqual(
renamedBookStore.name,
"renamed_books",
"IDBObjectStore name should change immediately after a rename",
);
t.deepEqual(
database.objectStoreNames as any,
["renamed_books"],
"IDBDatabase.objectStoreNames should immediately reflect the " +
"rename",
);
t.deepEqual(
transaction.objectStoreNames as any,
["renamed_books"],
"IDBTransaction.objectStoreNames should immediately reflect the " +
"rename",
);
t.deepEqual(
transaction.objectStore("renamed_books"),
renamedBookStore,
"IDBTransaction.objectStore should return the renamed object " +
"store when queried using the new name immediately after the " +
"rename",
);
t.throws(
() => transaction.objectStore("books"),
{ name: "NotFoundError" },
"IDBTransaction.objectStore should throw when queried using the " +
"renamed object store's old name immediately after the rename",
);
}),
)
.then((database) => {
t.deepEqual(
database.objectStoreNames as any,
["renamed_books"],
"IDBDatabase.objectStoreNames should still reflect the rename " +
"after the versionchange transaction commits",
);
const transaction = database.transaction("renamed_books", "readonly");
renamedBookStore2 = transaction.objectStore("renamed_books");
return checkStoreContents(
t,
renamedBookStore2,
"Renaming an object store should not change its records",
).then(() => database.close());
})
.then(() => {
t.deepEqual(
bookStore.name,
"books",
"IDBObjectStore obtained before the rename transaction should " +
"not reflect the rename",
);
t.deepEqual(
bookStore2.name,
"books",
"IDBObjectStore obtained before the rename transaction should " +
"not reflect the rename",
);
t.deepEqual( t.deepEqual(
renamedBookStore.name, renamedBookStore.name,
"renamed_books", "renamed_books",
"IDBObjectStore used in the rename transaction should keep " + "IDBObjectStore name should change immediately after a rename",
"reflecting the new name after the transaction is committed",
); );
t.deepEqual( t.deepEqual(
renamedBookStore2.name, database.objectStoreNames as any,
"renamed_books", ["renamed_books"],
"IDBObjectStore obtained after the rename transaction should " + "IDBDatabase.objectStoreNames should immediately reflect the " +
"reflect the new name", "rename",
); );
}); t.deepEqual(
}); transaction.objectStoreNames as any,
t.pass(); ["renamed_books"],
"IDBTransaction.objectStoreNames should immediately reflect the " +
"rename",
);
t.deepEqual(
transaction.objectStore("renamed_books"),
renamedBookStore,
"IDBTransaction.objectStore should return the renamed object " +
"store when queried using the new name immediately after the " +
"rename",
);
t.throws(
() => transaction.objectStore("books"),
{ name: "NotFoundError" },
"IDBTransaction.objectStore should throw when queried using the " +
"renamed object store's old name immediately after the rename",
);
}),
)
.then((database) => {
t.deepEqual(
database.objectStoreNames as any,
["renamed_books"],
"IDBDatabase.objectStoreNames should still reflect the rename " +
"after the versionchange transaction commits",
);
const transaction = database.transaction("renamed_books", "readonly");
renamedBookStore2 = transaction.objectStore("renamed_books");
return checkStoreContents(
t,
renamedBookStore2,
"Renaming an object store should not change its records",
).then(() => database.close());
})
.then(() => {
t.deepEqual(
bookStore.name,
"books",
"IDBObjectStore obtained before the rename transaction should " +
"not reflect the rename",
);
t.deepEqual(
bookStore2.name,
"books",
"IDBObjectStore obtained before the rename transaction should " +
"not reflect the rename",
);
t.deepEqual(
renamedBookStore.name,
"renamed_books",
"IDBObjectStore used in the rename transaction should keep " +
"reflecting the new name after the transaction is committed",
);
t.deepEqual(
renamedBookStore2.name,
"renamed_books",
"IDBObjectStore obtained after the rename transaction should " +
"reflect the new name",
);
});
}); });
// IndexedDB: object store renaming support // IndexedDB: object store renaming support
// IndexedDB object store rename in the transaction where it is created // IndexedDB object store rename in the transaction where it is created
test("WPT idbobjectstore-rename-store.html (subtest 2)", async (t) => { test("WPT idbobjectstore-rename-store.html (subtest 2)", async (t) => {
await new Promise<void>((resolve, reject) => { let renamedBookStore: any = null,
let renamedBookStore: any = null, renamedBookStore2: any = null;
renamedBookStore2: any = null; await createDatabase(t, (database, transaction) => {
return createDatabase(t, (database, transaction) => { renamedBookStore = createBooksStore(t, database);
renamedBookStore = createBooksStore(t, database); renamedBookStore.name = "renamed_books";
renamedBookStore.name = "renamed_books";
t.deepEqual( t.deepEqual(
renamedBookStore.name, renamedBookStore.name,
"renamed_books", "renamed_books",
"IDBObjectStore name should change immediately after a rename", "IDBObjectStore name should change immediately after a rename",
); );
t.deepEqual(
database.objectStoreNames as any,
["renamed_books"],
"IDBDatabase.objectStoreNames should immediately reflect the " + "rename",
);
t.deepEqual(
transaction.objectStoreNames as any,
["renamed_books"],
"IDBTransaction.objectStoreNames should immediately reflect the " +
"rename",
);
t.deepEqual(
transaction.objectStore("renamed_books"),
renamedBookStore,
"IDBTransaction.objectStore should return the renamed object " +
"store when queried using the new name immediately after the " +
"rename",
);
t.throws(
() => transaction.objectStore("books"),
{ name: "NotFoundError" },
"IDBTransaction.objectStore should throw when queried using the " +
"renamed object store's old name immediately after the rename",
);
})
.then((database) => {
t.deepEqual( t.deepEqual(
database.objectStoreNames as any, database.objectStoreNames as any,
["renamed_books"], ["renamed_books"],
"IDBDatabase.objectStoreNames should immediately reflect the " + "IDBDatabase.objectStoreNames should still reflect the rename " +
"rename", "after the versionchange transaction commits",
);
t.deepEqual(
transaction.objectStoreNames as any,
["renamed_books"],
"IDBTransaction.objectStoreNames should immediately reflect the " +
"rename",
);
t.deepEqual(
transaction.objectStore("renamed_books"),
renamedBookStore,
"IDBTransaction.objectStore should return the renamed object " +
"store when queried using the new name immediately after the " +
"rename",
);
t.throws(
() => transaction.objectStore("books"),
{ name: "NotFoundError" },
"IDBTransaction.objectStore should throw when queried using the " +
"renamed object store's old name immediately after the rename",
); );
const transaction = database.transaction("renamed_books", "readonly");
renamedBookStore2 = transaction.objectStore("renamed_books");
return checkStoreContents(
t,
renamedBookStore2,
"Renaming an object store should not change its records",
).then(() => database.close());
}) })
.then((database) => { .then(() => {
t.deepEqual( t.deepEqual(
database.objectStoreNames as any, renamedBookStore.name,
["renamed_books"], "renamed_books",
"IDBDatabase.objectStoreNames should still reflect the rename " + "IDBObjectStore used in the rename transaction should keep " +
"after the versionchange transaction commits", "reflecting the new name after the transaction is committed",
); );
const transaction = database.transaction("renamed_books", "readonly"); t.deepEqual(
renamedBookStore2 = transaction.objectStore("renamed_books"); renamedBookStore2.name,
return checkStoreContents( "renamed_books",
t, "IDBObjectStore obtained after the rename transaction should " +
renamedBookStore2, "reflect the new name",
"Renaming an object store should not change its records", );
).then(() => database.close()); });
})
.then(() => {
t.deepEqual(
renamedBookStore.name,
"renamed_books",
"IDBObjectStore used in the rename transaction should keep " +
"reflecting the new name after the transaction is committed",
);
t.deepEqual(
renamedBookStore2.name,
"renamed_books",
"IDBObjectStore obtained after the rename transaction should " +
"reflect the new name",
);
});
});
t.pass();
}); });
// Renames the 'books' store to 'renamed_books'. // Renames the 'books' store to 'renamed_books'.
@ -333,13 +322,13 @@ test("WPT idbobjectstore-rename-store.html (IndexedDB object store swapping via
"IDBDatabase.objectStoreNames should immediately reflect the swap", "IDBDatabase.objectStoreNames should immediately reflect the swap",
); );
t.deepEqual( t.is(
transaction.objectStore("books"), transaction.objectStore("books"),
notBookStore, notBookStore,
'IDBTransaction.objectStore should return the original "books" ' + 'IDBTransaction.objectStore should return the original "books" ' +
'store when queried with "not_books" after the swap', 'store when queried with "not_books" after the swap',
); );
t.deepEqual( t.is(
transaction.objectStore("not_books"), transaction.objectStore("not_books"),
bookStore, bookStore,
"IDBTransaction.objectStore should return the original " + "IDBTransaction.objectStore should return the original " +
@ -452,9 +441,12 @@ test("WPT idbobjectstore-rename-store.html (IndexedDB object store rename string
t.pass(); t.pass();
}); });
function rename_test_macro(t: ExecutionContext, escapedName: string) { function rename_test_macro(
t: ExecutionContext,
escapedName: string,
): Promise<void> {
const name = JSON.parse('"' + escapedName + '"'); const name = JSON.parse('"' + escapedName + '"');
createDatabase(t, (database, transaction) => { return createDatabase(t, (database, transaction) => {
createBooksStore(t, database); createBooksStore(t, database);
}) })
.then((database) => { .then((database) => {

View File

@ -14,10 +14,12 @@
* permissions and limitations under the License. * permissions and limitations under the License.
*/ */
import { DOMStringList } from "../idbtypes";
/** @public */ /** @public */
export interface FakeDOMStringList extends Array<string> { export interface FakeDOMStringList extends Array<string> {
contains: (value: string) => boolean; contains: (value: string) => boolean;
item: (i: number) => string | undefined; item: (i: number) => string | null;
} }
// Would be nicer to sublcass Array, but I'd have to sacrifice Node 4 support to do that. // Would be nicer to sublcass Array, but I'd have to sacrifice Node 4 support to do that.
@ -26,13 +28,16 @@ export const fakeDOMStringList = (arr: string[]): FakeDOMStringList => {
const arr2 = arr.slice(); const arr2 = arr.slice();
Object.defineProperty(arr2, "contains", { Object.defineProperty(arr2, "contains", {
// tslint:disable-next-line object-literal-shorthand
value: (value: string) => arr2.indexOf(value) >= 0, value: (value: string) => arr2.indexOf(value) >= 0,
}); });
Object.defineProperty(arr2, "item", { Object.defineProperty(arr2, "item", {
// tslint:disable-next-line object-literal-shorthand value: (i: number) => {
value: (i: number) => arr2[i], if (i < 0 || i >= arr2.length) {
return null;
}
return arr2[i];
},
}); });
return arr2 as FakeDOMStringList; return arr2 as FakeDOMStringList;