idb: make more tests pass, implement Cursor.advance()

This commit is contained in:
Florian Dold 2021-02-17 17:38:47 +01:00
parent 69b62c62a0
commit 4b4640dbcb
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
3 changed files with 135 additions and 39 deletions

View File

@ -46,11 +46,10 @@ type Key = IDBValidKey;
type Value = unknown; type Value = unknown;
enum TransactionLevel { enum TransactionLevel {
Disconnected = 0, None = 0,
Connected = 1, Read = 1,
Read = 2, Write = 2,
Write = 3, VersionChange = 3,
VersionChange = 4,
} }
interface ObjectStore { interface ObjectStore {
@ -83,12 +82,18 @@ interface Database {
txLevel: TransactionLevel; txLevel: TransactionLevel;
txOwnerConnectionCookie?: string;
txOwnerTransactionCookie?: string;
/** /**
* Object stores that the transaction is allowed to access. * Object stores that the transaction is allowed to access.
*/ */
txRestrictObjectStores: string[] | undefined; txRestrictObjectStores: string[] | undefined;
connectionCookie: string | undefined; /**
* Connection cookies of current connections.
*/
connectionCookies: string[];
} }
/** @public */ /** @public */
@ -245,7 +250,7 @@ export class MemoryBackend implements Backend {
private disconnectCond: AsyncCondition = new AsyncCondition(); private disconnectCond: AsyncCondition = new AsyncCondition();
/** /**
* Conditation that is triggered whenever a transaction finishes. * Condition that is triggered whenever a transaction finishes.
*/ */
private transactionDoneCond: AsyncCondition = new AsyncCondition(); private transactionDoneCond: AsyncCondition = new AsyncCondition();
@ -327,8 +332,8 @@ export class MemoryBackend implements Backend {
deleted: false, deleted: false,
committedObjectStores: objectStores, committedObjectStores: objectStores,
committedSchema: structuredClone(schema), committedSchema: structuredClone(schema),
connectionCookie: undefined, connectionCookies: [],
txLevel: TransactionLevel.Disconnected, txLevel: TransactionLevel.None,
txRestrictObjectStores: undefined, txRestrictObjectStores: undefined,
}; };
this.databases[dbName] = db; this.databases[dbName] = db;
@ -425,9 +430,9 @@ export class MemoryBackend implements Backend {
if (myDb.txLevel < TransactionLevel.VersionChange) { if (myDb.txLevel < TransactionLevel.VersionChange) {
throw new InvalidStateError(); throw new InvalidStateError();
} }
if (myDb.connectionCookie !== tx.transactionCookie) { // if (myDb.connectionCookie !== tx.transactionCookie) {
throw new InvalidAccessError(); // throw new InvalidAccessError();
} // }
myDb.deleted = true; myDb.deleted = true;
} }
@ -449,20 +454,18 @@ export class MemoryBackend implements Backend {
committedSchema: schema, committedSchema: schema,
deleted: false, deleted: false,
committedObjectStores: {}, committedObjectStores: {},
txLevel: TransactionLevel.Disconnected, txLevel: TransactionLevel.None,
connectionCookie: undefined, connectionCookies: [],
txRestrictObjectStores: undefined, txRestrictObjectStores: undefined,
}; };
this.databases[name] = database; this.databases[name] = database;
} }
while (database.txLevel !== TransactionLevel.Disconnected) { if (database.connectionCookies.includes(connectionCookie)) {
await this.disconnectCond.wait(); throw Error("already connected");
} }
database.txLevel = TransactionLevel.Connected; database.connectionCookies.push(connectionCookie);
database.txRestrictObjectStores = undefined;
database.connectionCookie = connectionCookie;
const myConn: Connection = { const myConn: Connection = {
dbName: name, dbName: name,
@ -494,7 +497,7 @@ export class MemoryBackend implements Backend {
throw Error("db not found"); throw Error("db not found");
} }
while (myDb.txLevel !== TransactionLevel.Connected) { while (myDb.txLevel !== TransactionLevel.None) {
if (this.enableTracing) { if (this.enableTracing) {
console.log(`TRACING: beginTransaction -- waiting for others to close`); console.log(`TRACING: beginTransaction -- waiting for others to close`);
} }
@ -533,11 +536,13 @@ export class MemoryBackend implements Backend {
throw Error("db not found"); throw Error("db not found");
} }
while (myDb.txLevel !== TransactionLevel.Connected) { while (myDb.txLevel !== TransactionLevel.None) {
await this.transactionDoneCond.wait(); await this.transactionDoneCond.wait();
} }
myDb.txLevel = TransactionLevel.VersionChange; myDb.txLevel = TransactionLevel.VersionChange;
myDb.txOwnerConnectionCookie = conn.connectionCookie;
myDb.txOwnerTransactionCookie = transactionCookie;
myDb.txRestrictObjectStores = undefined; myDb.txRestrictObjectStores = undefined;
this.connectionsByTransaction[transactionCookie] = myConn; this.connectionsByTransaction[transactionCookie] = myConn;
@ -557,11 +562,13 @@ export class MemoryBackend implements Backend {
} }
if (!myConn.deleted) { if (!myConn.deleted) {
const myDb = this.databases[myConn.dbName]; const myDb = this.databases[myConn.dbName];
if (myDb.txLevel != TransactionLevel.Connected) { // if (myDb.connectionCookies.includes(conn.connectionCookie)) {
throw Error("invalid state"); // throw Error("invalid state");
} // }
myDb.txLevel = TransactionLevel.Disconnected; // FIXME: what if we're still in a transaction?
myDb.txRestrictObjectStores = undefined; myDb.connectionCookies = myDb.connectionCookies.filter(
(x) => x != conn.connectionCookie,
);
} }
delete this.connections[conn.connectionCookie]; delete this.connections[conn.connectionCookie];
this.disconnectCond.trigger(); this.disconnectCond.trigger();
@ -1390,7 +1397,7 @@ export class MemoryBackend implements Backend {
throw Error("db not found"); throw Error("db not found");
} }
if (db.txLevel < TransactionLevel.Write) { if (db.txLevel < TransactionLevel.Write) {
throw Error("only allowed while running a transaction"); throw Error("store operation only allowed while running a transaction");
} }
if ( if (
db.txRestrictObjectStores && db.txRestrictObjectStores &&
@ -1588,9 +1595,9 @@ export class MemoryBackend implements Backend {
throw Error("db not found"); throw Error("db not found");
} }
if (db.txLevel < TransactionLevel.Read) { if (db.txLevel < TransactionLevel.Read) {
throw Error("only allowed while running a transaction"); throw Error("rollback is only allowed while running a transaction");
} }
db.txLevel = TransactionLevel.Connected; db.txLevel = TransactionLevel.None;
db.txRestrictObjectStores = undefined; db.txRestrictObjectStores = undefined;
myConn.modifiedSchema = structuredClone(db.committedSchema); myConn.modifiedSchema = structuredClone(db.committedSchema);
myConn.objectStoreMap = this.makeObjectStoreMap(db); myConn.objectStoreMap = this.makeObjectStoreMap(db);
@ -1633,7 +1640,7 @@ export class MemoryBackend implements Backend {
} }
db.committedSchema = structuredClone(myConn.modifiedSchema); db.committedSchema = structuredClone(myConn.modifiedSchema);
db.txLevel = TransactionLevel.Connected; db.txLevel = TransactionLevel.None;
db.txRestrictObjectStores = undefined; db.txRestrictObjectStores = undefined;
db.committedObjectStores = {}; db.committedObjectStores = {};

View File

@ -312,7 +312,43 @@ export class BridgeIDBCursor implements IDBCursor {
* http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBCursor-advance-void-unsigned-long-count * http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBCursor-advance-void-unsigned-long-count
*/ */
public advance(count: number) { public advance(count: number) {
throw Error("not implemented"); const transaction = this._effectiveObjectStore._transaction;
if (!transaction._active) {
throw new TransactionInactiveError();
}
if (this._effectiveObjectStore._deleted) {
throw new InvalidStateError();
}
if (
!(this.source instanceof BridgeIDBObjectStore) &&
this.source._deleted
) {
throw new InvalidStateError();
}
if (!this._gotValue) {
throw new InvalidStateError();
}
if (this._request) {
this._request.readyState = "pending";
}
const operation = async () => {
for (let i = 0; i < count; i++) {
await this._iterate();
}
};
transaction._execRequestAsync({
operation,
request: this._request,
source: this.source,
});
this._gotValue = false;
} }
/** /**
@ -760,8 +796,23 @@ export class BridgeIDBFactory {
queueTask(async () => { queueTask(async () => {
let dbconn: DatabaseConnection; let dbconn: DatabaseConnection;
try { try {
if (BridgeIDBFactory.enableTracing) {
console.log(
"TRACE: connecting to database",
);
}
dbconn = await this.backend.connectDatabase(name); dbconn = await this.backend.connectDatabase(name);
if (BridgeIDBFactory.enableTracing) {
console.log(
"TRACE: connected!",
);
}
} catch (err) { } catch (err) {
if (BridgeIDBFactory.enableTracing) {
console.log(
"TRACE: caught exception while trying to connect with backend",
);
}
request._finishWithError(err); request._finishWithError(err);
return; return;
} }
@ -796,11 +847,24 @@ export class BridgeIDBFactory {
cancelable: false, cancelable: false,
}); });
event2.eventPath = []; event2.eventPath = [];
if (BridgeIDBFactory.enableTracing) {
console.log(
"open() requested same version, dispatching 'success' event on transaction",
);
}
request.dispatchEvent(event2); request.dispatchEvent(event2);
} else if (existingVersion < requestedVersion) { } else if (existingVersion < requestedVersion) {
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-steps-for-running-a-versionchange-transaction // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-steps-for-running-a-versionchange-transaction
for (const otherConn of this.connections) { for (const otherConn of this.connections) {
if (otherConn._closePending) {
continue;
}
if (BridgeIDBFactory.enableTracing) {
console.log(
"dispatching 'versionchange' event to other connection",
);
}
const event = new BridgeIDBVersionChangeEvent("versionchange", { const event = new BridgeIDBVersionChangeEvent("versionchange", {
newVersion: version, newVersion: version,
oldVersion: existingVersion, oldVersion: existingVersion,
@ -809,6 +873,11 @@ export class BridgeIDBFactory {
} }
if (this._anyOpen()) { if (this._anyOpen()) {
if (BridgeIDBFactory.enableTracing) {
console.log(
"other connections are still open, dispatching 'blocked' event to other connection",
);
}
const event = new BridgeIDBVersionChangeEvent("blocked", { const event = new BridgeIDBVersionChangeEvent("blocked", {
newVersion: version, newVersion: version,
oldVersion: existingVersion, oldVersion: existingVersion,
@ -835,6 +904,10 @@ export class BridgeIDBFactory {
db._upgradeTransaction = transaction; db._upgradeTransaction = transaction;
if (BridgeIDBFactory.enableTracing) {
console.log("dispatching upgradeneeded event");
}
const event = new BridgeIDBVersionChangeEvent("upgradeneeded", { const event = new BridgeIDBVersionChangeEvent("upgradeneeded", {
newVersion: version, newVersion: version,
oldVersion: existingVersion, oldVersion: existingVersion,
@ -866,6 +939,10 @@ export class BridgeIDBFactory {
event2.eventPath = []; event2.eventPath = [];
request.dispatchEvent(event2); request.dispatchEvent(event2);
} else { } else {
if (BridgeIDBFactory.enableTracing) {
console.log("dispatching 'success' event for opening db");
}
const event2 = new FakeEvent("success", { const event2 = new FakeEvent("success", {
bubbles: false, bubbles: false,
cancelable: false, cancelable: false,
@ -1801,10 +1878,10 @@ export class BridgeIDBRequest extends FakeEventTarget implements IDBRequest {
_result: any = null; _result: any = null;
_error: Error | null | undefined = null; _error: Error | null | undefined = null;
get source(): IDBObjectStore | IDBIndex | IDBCursor { get source(): IDBObjectStore | IDBIndex | IDBCursor {
if (this._source) { if (!this._source) {
return this._source; throw Error("internal invariant failed: source is null");
} }
throw Error("source is null"); return this._source;
} }
_source: _source:
| BridgeIDBCursor | BridgeIDBCursor
@ -1875,6 +1952,16 @@ export class BridgeIDBOpenDBRequest
public onupgradeneeded: EventListener | null = null; public onupgradeneeded: EventListener | null = null;
public onblocked: EventListener | null = null; public onblocked: EventListener | null = null;
get source(): IDBObjectStore | IDBIndex | IDBCursor {
// This is a type safety violation, but it is required by the
// IndexedDB standard.
// On the one hand, IDBOpenDBRequest implements IDBRequest.
// But that's technically impossible, as the "source" of the
// IDBOpenDB request may be null, while the one from IDBRequest
// may not be null.
return this._source as any;
}
constructor() { constructor() {
super(); super();
// https://www.w3.org/TR/IndexedDB/#open-requests // https://www.w3.org/TR/IndexedDB/#open-requests

View File

@ -20,7 +20,7 @@ test("WPT idbfactory-open.htm", async (t) => {
// IDBFactory.open() - database 'name' and 'version' are correctly set // IDBFactory.open() - database 'name' and 'version' are correctly set
test("WPT idbfactory-open2.htm", async (t) => { test("WPT idbfactory-open2.htm", async (t) => {
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
var database_name = __filename + "-database_name"; var database_name = t.title + "-database_name";
var open_rq = createdb(t, database_name, 13); var open_rq = createdb(t, database_name, 13);
open_rq.onupgradeneeded = function (e) {}; open_rq.onupgradeneeded = function (e) {};
@ -28,7 +28,7 @@ test("WPT idbfactory-open2.htm", async (t) => {
var db = e.target.result; var db = e.target.result;
t.deepEqual(db.name, database_name, "db.name"); t.deepEqual(db.name, database_name, "db.name");
t.deepEqual(db.version, 13, "db.version"); t.deepEqual(db.version, 13, "db.version");
resolve; resolve();
}; };
}); });
t.pass(); t.pass();
@ -63,7 +63,7 @@ test("WPT idbfactory-open3.htm", async (t) => {
test("WPT idbfactory-open4.htm", async (t) => { test("WPT idbfactory-open4.htm", async (t) => {
const indexedDB = idbFactory; const indexedDB = idbFactory;
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
var open_rq = createdb(t, __filename + "-database_name"); var open_rq = createdb(t, t.title + "-database_name");
open_rq.onupgradeneeded = function (e: any) { open_rq.onupgradeneeded = function (e: any) {
t.deepEqual(e.target.result.version, 1, "db.version"); t.deepEqual(e.target.result.version, 1, "db.version");
@ -80,7 +80,7 @@ test("WPT idbfactory-open4.htm", async (t) => {
test("WPT idbfactory-open5.htm", async (t) => { test("WPT idbfactory-open5.htm", async (t) => {
const indexedDB = idbFactory; const indexedDB = idbFactory;
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
var open_rq = createdb(t, __filename + "-database_name"); var open_rq = createdb(t, t.title + "-database_name");
open_rq.onupgradeneeded = function () {}; open_rq.onupgradeneeded = function () {};
open_rq.onsuccess = function (e: any) { open_rq.onsuccess = function (e: any) {
@ -100,7 +100,6 @@ test("WPT idbfactory-open6.htm", async (t) => {
const indexedDB = idbFactory; const indexedDB = idbFactory;
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
var open_rq = createdb(t, undefined, 13); var open_rq = createdb(t, undefined, 13);
var did_upgrade = false;
var open_rq2: any; var open_rq2: any;
open_rq.onupgradeneeded = function () {}; open_rq.onupgradeneeded = function () {};
@ -115,8 +114,10 @@ test("WPT idbfactory-open6.htm", async (t) => {
}; };
function open_previous_db(e: any) { function open_previous_db(e: any) {
t.log("opening previous DB");
var open_rq3 = indexedDB.open(e.target.result.name, 13); var open_rq3 = indexedDB.open(e.target.result.name, 13);
open_rq3.onerror = function (e: any) { open_rq3.onerror = function (e: any) {
t.log("got open error");
t.deepEqual(e.target.error.name, "VersionError", "e.target.error.name"); t.deepEqual(e.target.error.name, "VersionError", "e.target.error.name");
open_rq2.result.close(); open_rq2.result.close();
resolve(); resolve();
@ -506,6 +507,7 @@ test("WPT idbfactory-open12.htm", async (t) => {
* Second test * Second test
*/ */
db.onversionchange = function () { db.onversionchange = function () {
t.log("onversionchange called");
db.close(); db.close();
}; };