gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] 01/02: idb: more tests, fix DB deletion, exception o


From: gnunet
Subject: [taler-wallet-core] 01/02: idb: more tests, fix DB deletion, exception ordering and transaction active checks
Date: Mon, 22 Feb 2021 14:28:02 +0100

This is an automated email from the git hooks/post-receive script.

dold pushed a commit to branch master
in repository wallet-core.

commit e6946694f2e7ae6ff25f490fa76f3da583c44c74
Author: Florian Dold <florian@dold.me>
AuthorDate: Fri Feb 19 21:27:49 2021 +0100

    idb: more tests, fix DB deletion, exception ordering and transaction active 
checks
---
 packages/idb-bridge/src/MemoryBackend.ts           | 122 ++++------
 packages/idb-bridge/src/backend-interface.ts       |   8 +-
 packages/idb-bridge/src/bridge-idb.ts              | 118 +++++++---
 .../idb-wpt-ported/idbcursor-advance-index.test.ts | 255 +++++++++++++++++++++
 .../idbobjectstore-add-put-exception-order.test.ts | 104 +++++++++
 .../idb-bridge/src/idb-wpt-ported/wptsupport.ts    |  58 +++++
 6 files changed, 547 insertions(+), 118 deletions(-)

diff --git a/packages/idb-bridge/src/MemoryBackend.ts 
b/packages/idb-bridge/src/MemoryBackend.ts
index 0051005e..53355bf7 100644
--- a/packages/idb-bridge/src/MemoryBackend.ts
+++ b/packages/idb-bridge/src/MemoryBackend.ts
@@ -131,11 +131,6 @@ interface Connection {
 
   modifiedSchema: Schema;
 
-  /**
-   * Has the underlying database been deleted?
-   */
-  deleted: boolean;
-
   /**
    * Map from the effective name of an object store during
    * the transaction to the real name.
@@ -412,13 +407,9 @@ export class MemoryBackend implements Backend {
     return dbList;
   }
 
-  async deleteDatabase(tx: DatabaseTransaction, name: string): Promise<void> {
+  async deleteDatabase(name: string): Promise<void> {
     if (this.enableTracing) {
-      console.log("TRACING: deleteDatabase");
-    }
-    const myConn = this.connectionsByTransaction[tx.transactionCookie];
-    if (!myConn) {
-      throw Error("no connection associated with transaction");
+      console.log(`TRACING: deleteDatabase(${name})`);
     }
     const myDb = this.databases[name];
     if (!myDb) {
@@ -427,13 +418,13 @@ export class MemoryBackend implements Backend {
     if (myDb.committedSchema.databaseName !== name) {
       throw Error("name does not match");
     }
-    if (myDb.txLevel < TransactionLevel.VersionChange) {
-      throw new InvalidStateError();
+
+    while (myDb.txLevel !== TransactionLevel.None) {
+      await this.transactionDoneCond.wait();
     }
-    // if (myDb.connectionCookie !== tx.transactionCookie) {
-    //   throw new InvalidAccessError();
-    // }
+
     myDb.deleted = true;
+    delete this.databases[name];
   }
 
   async connectDatabase(name: string): Promise<DatabaseConnection> {
@@ -469,7 +460,6 @@ export class MemoryBackend implements Backend {
 
     const myConn: Connection = {
       dbName: name,
-      deleted: false,
       objectStoreMap: this.makeObjectStoreMap(database),
       modifiedSchema: structuredClone(database.committedSchema),
     };
@@ -560,28 +550,38 @@ export class MemoryBackend implements Backend {
     if (!myConn) {
       throw Error("connection not found - already closed?");
     }
-    if (!myConn.deleted) {
-      const myDb = this.databases[myConn.dbName];
-      // if (myDb.connectionCookies.includes(conn.connectionCookie)) {
-      //   throw Error("invalid state");
-      // }
-      // FIXME: what if we're still in a transaction?
-      myDb.connectionCookies = myDb.connectionCookies.filter(
-        (x) => x != conn.connectionCookie,
-      );
-    }
+    const myDb = this.databases[myConn.dbName];
+    // FIXME: what if we're still in a transaction?
+    myDb.connectionCookies = myDb.connectionCookies.filter(
+      (x) => x != conn.connectionCookie,
+    );
     delete this.connections[conn.connectionCookie];
     this.disconnectCond.trigger();
   }
 
+  private requireConnection(dbConn: DatabaseConnection): Connection {
+    const myConn = this.connections[dbConn.connectionCookie];
+    if (!myConn) {
+      throw Error(`unknown connection (${dbConn.connectionCookie})`);
+    }
+    return myConn;
+  }
+
+  private requireConnectionFromTransaction(
+    btx: DatabaseTransaction,
+  ): Connection {
+    const myConn = this.connectionsByTransaction[btx.transactionCookie];
+    if (!myConn) {
+      throw Error(`unknown transaction (${btx.transactionCookie})`);
+    }
+    return myConn;
+  }
+
   getSchema(dbConn: DatabaseConnection): Schema {
     if (this.enableTracing) {
       console.log(`TRACING: getSchema`);
     }
-    const myConn = this.connections[dbConn.connectionCookie];
-    if (!myConn) {
-      throw Error("unknown connection");
-    }
+    const myConn = this.requireConnection(dbConn);
     const db = this.databases[myConn.dbName];
     if (!db) {
       throw Error("db not found");
@@ -590,10 +590,7 @@ export class MemoryBackend implements Backend {
   }
 
   getCurrentTransactionSchema(btx: DatabaseTransaction): Schema {
-    const myConn = this.connectionsByTransaction[btx.transactionCookie];
-    if (!myConn) {
-      throw Error("unknown connection");
-    }
+    const myConn = this.requireConnectionFromTransaction(btx);
     const db = this.databases[myConn.dbName];
     if (!db) {
       throw Error("db not found");
@@ -602,10 +599,7 @@ export class MemoryBackend implements Backend {
   }
 
   getInitialTransactionSchema(btx: DatabaseTransaction): Schema {
-    const myConn = this.connectionsByTransaction[btx.transactionCookie];
-    if (!myConn) {
-      throw Error("unknown connection");
-    }
+    const myConn = this.requireConnectionFromTransaction(btx);
     const db = this.databases[myConn.dbName];
     if (!db) {
       throw Error("db not found");
@@ -622,10 +616,7 @@ export class MemoryBackend implements Backend {
     if (this.enableTracing) {
       console.log(`TRACING: renameIndex(?, ${oldName}, ${newName})`);
     }
-    const myConn = this.connectionsByTransaction[btx.transactionCookie];
-    if (!myConn) {
-      throw Error("unknown connection");
-    }
+    const myConn = this.requireConnectionFromTransaction(btx);
     const db = this.databases[myConn.dbName];
     if (!db) {
       throw Error("db not found");
@@ -664,10 +655,7 @@ export class MemoryBackend implements Backend {
     if (this.enableTracing) {
       console.log(`TRACING: deleteIndex(${indexName})`);
     }
-    const myConn = this.connectionsByTransaction[btx.transactionCookie];
-    if (!myConn) {
-      throw Error("unknown connection");
-    }
+    const myConn = this.requireConnectionFromTransaction(btx);
     const db = this.databases[myConn.dbName];
     if (!db) {
       throw Error("db not found");
@@ -698,10 +686,7 @@ export class MemoryBackend implements Backend {
         `TRACING: deleteObjectStore(${name}) in ${btx.transactionCookie}`,
       );
     }
-    const myConn = this.connectionsByTransaction[btx.transactionCookie];
-    if (!myConn) {
-      throw Error("unknown connection");
-    }
+    const myConn = this.requireConnectionFromTransaction(btx);
     const db = this.databases[myConn.dbName];
     if (!db) {
       throw Error("db not found");
@@ -740,10 +725,7 @@ export class MemoryBackend implements Backend {
       console.log(`TRACING: renameObjectStore(?, ${oldName}, ${newName})`);
     }
 
-    const myConn = this.connectionsByTransaction[btx.transactionCookie];
-    if (!myConn) {
-      throw Error("unknown connection");
-    }
+    const myConn = this.requireConnectionFromTransaction(btx);
     const db = this.databases[myConn.dbName];
     if (!db) {
       throw Error("db not found");
@@ -783,10 +765,7 @@ export class MemoryBackend implements Backend {
         `TRACING: createObjectStore(${btx.transactionCookie}, ${name})`,
       );
     }
-    const myConn = this.connectionsByTransaction[btx.transactionCookie];
-    if (!myConn) {
-      throw Error("unknown connection");
-    }
+    const myConn = this.requireConnectionFromTransaction(btx);
     const db = this.databases[myConn.dbName];
     if (!db) {
       throw Error("db not found");
@@ -828,10 +807,7 @@ export class MemoryBackend implements Backend {
     if (this.enableTracing) {
       console.log(`TRACING: createIndex(${indexName})`);
     }
-    const myConn = this.connectionsByTransaction[btx.transactionCookie];
-    if (!myConn) {
-      throw Error("unknown connection");
-    }
+    const myConn = this.requireConnectionFromTransaction(btx);
     const db = this.databases[myConn.dbName];
     if (!db) {
       throw Error("db not found");
@@ -892,10 +868,7 @@ export class MemoryBackend implements Backend {
     if (this.enableTracing) {
       console.log(`TRACING: deleteRecord from store ${objectStoreName}`);
     }
-    const myConn = this.connectionsByTransaction[btx.transactionCookie];
-    if (!myConn) {
-      throw Error("unknown connection");
-    }
+    const myConn = this.requireConnectionFromTransaction(btx);
     const db = this.databases[myConn.dbName];
     if (!db) {
       throw Error("db not found");
@@ -1057,10 +1030,7 @@ export class MemoryBackend implements Backend {
       console.log(`TRACING: getRecords`);
       console.log("query", req);
     }
-    const myConn = this.connectionsByTransaction[btx.transactionCookie];
-    if (!myConn) {
-      throw Error("unknown connection");
-    }
+    const myConn = this.requireConnectionFromTransaction(btx);
     const db = this.databases[myConn.dbName];
     if (!db) {
       throw Error("db not found");
@@ -1388,10 +1358,7 @@ export class MemoryBackend implements Backend {
     if (this.enableTracing) {
       console.log(`TRACING: storeRecord`);
     }
-    const myConn = this.connectionsByTransaction[btx.transactionCookie];
-    if (!myConn) {
-      throw Error("unknown connection");
-    }
+    const myConn = this.requireConnectionFromTransaction(btx);
     const db = this.databases[myConn.dbName];
     if (!db) {
       throw Error("db not found");
@@ -1626,10 +1593,7 @@ export class MemoryBackend implements Backend {
     if (this.enableTracing) {
       console.log(`TRACING: commit`);
     }
-    const myConn = this.connectionsByTransaction[btx.transactionCookie];
-    if (!myConn) {
-      throw Error("unknown connection");
-    }
+    const myConn = this.requireConnectionFromTransaction(btx);
     const db = this.databases[myConn.dbName];
     if (!db) {
       throw Error("db not found");
diff --git a/packages/idb-bridge/src/backend-interface.ts 
b/packages/idb-bridge/src/backend-interface.ts
index 7b74c35e..164996e7 100644
--- a/packages/idb-bridge/src/backend-interface.ts
+++ b/packages/idb-bridge/src/backend-interface.ts
@@ -21,7 +21,6 @@ import {
   IDBValidKey,
 } from "./idbtypes";
 
-
 /** @public */
 export interface ObjectStoreProperties {
   keyPath: string[] | null;
@@ -151,12 +150,7 @@ export interface Backend {
     newVersion: number,
   ): Promise<DatabaseTransaction>;
 
-  /**
-   * Even though the standard interface for indexedDB doesn't require
-   * the client to run deleteDatabase in a version transaction, there is
-   * implicitly one running.
-   */
-  deleteDatabase(btx: DatabaseTransaction, name: string): Promise<void>;
+  deleteDatabase(name: string): Promise<void>;
 
   close(db: DatabaseConnection): Promise<void>;
 
diff --git a/packages/idb-bridge/src/bridge-idb.ts 
b/packages/idb-bridge/src/bridge-idb.ts
index 6ca6633a..643a98de 100644
--- a/packages/idb-bridge/src/bridge-idb.ts
+++ b/packages/idb-bridge/src/bridge-idb.ts
@@ -195,7 +195,10 @@ export class BridgeIDBCursor implements IDBCursor {
   /**
    * https://w3c.github.io/IndexedDB/#iterate-a-cursor
    */
-  async _iterate(key?: IDBValidKey, primaryKey?: IDBValidKey): Promise<any> {
+  async _iterate(
+    key?: IDBValidKey,
+    primaryKey?: IDBValidKey,
+  ): Promise<BridgeIDBCursor | null> {
     BridgeIDBFactory.enableTracing &&
       console.log(
         `iterating cursor os=${this._objectStoreName},idx=${this._indexName}`,
@@ -312,6 +315,10 @@ export class BridgeIDBCursor implements IDBCursor {
    * 
http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBCursor-advance-void-unsigned-long-count
    */
   public advance(count: number) {
+    if (typeof count !== "number" || count <= 0) {
+      throw TypeError("count must be positive number");
+    }
+
     const transaction = this._effectiveObjectStore._transaction;
 
     if (!transaction._active) {
@@ -337,9 +344,11 @@ export class BridgeIDBCursor implements IDBCursor {
     }
 
     const operation = async () => {
+      let res: IDBCursor | null = null;
       for (let i = 0; i < count; i++) {
-        await this._iterate();
+        res = await this._iterate();
       }
+      return res;
     };
 
     transaction._execRequestAsync({
@@ -527,6 +536,11 @@ export class BridgeIDBDatabase extends FakeEventTarget 
implements IDBDatabase {
 
   _schema: Schema;
 
+  /**
+   * Name that can be set to identify the object store in logs.
+   */
+  _debugName: string | undefined = undefined;
+
   get name(): string {
     return this._schema.databaseName;
   }
@@ -686,12 +700,23 @@ export class BridgeIDBDatabase extends FakeEventTarget 
implements IDBDatabase {
       openRequest,
     );
     this._transactions.push(tx);
-    queueTask(() => tx._start());
+
+    queueTask(() => {
+      console.log("TRACE: calling auto-commit", this._getReadableName());
+      tx._start();
+    });
+    if (BridgeIDBFactory.enableTracing) {
+      console.log("TRACE: queued task to auto-commit", 
this._getReadableName());
+    }
     // "When a transaction is created its active flag is initially set."
     tx._active = true;
     return tx;
   }
 
+  _getReadableName(): string {
+    return `${this.name}(${this._debugName ?? "??"})`;
+  }
+
   public transaction(
     storeNames: string | string[],
     mode?: IDBTransactionMode,
@@ -745,15 +770,7 @@ export class BridgeIDBFactory {
       const oldVersion = dbInfo.version;
 
       try {
-        const dbconn = await this.backend.connectDatabase(name);
-        const backendTransaction = await this.backend.enterVersionChange(
-          dbconn,
-          0,
-        );
-        await this.backend.deleteDatabase(backendTransaction, name);
-        await this.backend.commit(backendTransaction);
-        await this.backend.close(dbconn);
-
+        await this.backend.deleteDatabase(name);
         request.result = undefined;
         request.readyState = "done";
 
@@ -797,15 +814,11 @@ export class BridgeIDBFactory {
       let dbconn: DatabaseConnection;
       try {
         if (BridgeIDBFactory.enableTracing) {
-          console.log(
-            "TRACE: connecting to database",
-          );
+          console.log("TRACE: connecting to database");
         }
         dbconn = await this.backend.connectDatabase(name);
         if (BridgeIDBFactory.enableTracing) {
-          console.log(
-            "TRACE: connected!",
-          );
+          console.log("TRACE: connected!");
         }
       } catch (err) {
         if (BridgeIDBFactory.enableTracing) {
@@ -1385,6 +1398,11 @@ export class BridgeIDBObjectStore implements 
IDBObjectStore {
 
   _transaction: BridgeIDBTransaction;
 
+  /**
+   * Name that can be set to identify the object store in logs.
+   */
+  _debugName: string | undefined = undefined;
+
   get transaction(): IDBTransaction {
     return this._transaction;
   }
@@ -1490,8 +1508,15 @@ export class BridgeIDBObjectStore implements 
IDBObjectStore {
 
   public _store(value: any, key: IDBValidKey | undefined, overwrite: boolean) {
     if (BridgeIDBFactory.enableTracing) {
-      console.log(`TRACE: IDBObjectStore._store`);
+      console.log(
+        `TRACE: IDBObjectStore._store, 
db=${this._transaction._db._getReadableName()}`,
+      );
     }
+
+    if (!this._transaction._active) {
+      throw new TransactionInactiveError();
+    }
+
     if (this._transaction.mode === "readonly") {
       throw new ReadOnlyError();
     }
@@ -1988,6 +2013,11 @@ export class BridgeIDBTransaction
   _aborted: boolean = false;
   _objectStoresCache: Map<string, BridgeIDBObjectStore> = new Map();
 
+  /**
+   * Name that can be set to identify the transaction in logs.
+   */
+  _debugName: string | undefined = undefined;
+
   /**
    * https://www.w3.org/TR/IndexedDB-2/#transaction-lifetime-concept
    *
@@ -2074,7 +2104,12 @@ export class BridgeIDBTransaction
       console.log("TRACE: aborting transaction");
     }
 
+    if (this._aborted) {
+      return;
+    }
+
     this._aborted = true;
+    this._active = false;
 
     if (errName !== null) {
       const e = new Error();
@@ -2116,6 +2151,7 @@ export class BridgeIDBTransaction
       this._db._schema = this._backend.getInitialTransactionSchema(maybeBtx);
       // Only roll back if we actually executed the scheduled operations.
       await this._backend.rollback(maybeBtx);
+      this._backendTransaction = undefined;
     } else {
       this._db._schema = this._backend.getSchema(this._db._backendConnection);
     }
@@ -2208,17 +2244,11 @@ export class BridgeIDBTransaction
         `TRACE: IDBTransaction._start, ${this._requests.length} queued`,
       );
     }
-    this._started = true;
 
-    if (!this._backendTransaction) {
-      this._backendTransaction = await this._backend.beginTransaction(
-        this._db._backendConnection,
-        Array.from(this._scope),
-        this.mode,
-      );
-    }
+    this._started = true;
 
-    // Remove from request queue - cursor ones will be added back if necessary 
by cursor.continue and such
+    // Remove from request queue - cursor ones will be added back if necessary
+    // by cursor.continue and such
     let operation;
     let request;
     while (this._requests.length > 0) {
@@ -2233,9 +2263,25 @@ export class BridgeIDBTransaction
     }
 
     if (request && operation) {
+      if (!this._backendTransaction && !this._aborted) {
+        if (BridgeIDBFactory.enableTracing) {
+          console.log("beginning backend transaction to process operation");
+        }
+        this._backendTransaction = await this._backend.beginTransaction(
+          this._db._backendConnection,
+          Array.from(this._scope),
+          this.mode,
+        );
+        if (BridgeIDBFactory.enableTracing) {
+          console.log(
+            `started backend transaction 
(${this._backendTransaction.transactionCookie})`,
+          );
+        }
+      }
+
       if (!request._source) {
-        // Special requests like indexes that just need to run some code, with 
error handling already built into
-        // operation
+        // Special requests like indexes that just need to run some code,
+        // with error handling already built into operation
         await operation();
       } else {
         let event;
@@ -2311,10 +2357,18 @@ export class BridgeIDBTransaction
 
     if (!this._finished && !this._committed) {
       if (BridgeIDBFactory.enableTracing) {
-        console.log("finishing transaction");
+        console.log(
+          `setting transaction to inactive, db=${this._db._getReadableName()}`,
+        );
       }
 
-      await this._backend.commit(this._backendTransaction);
+      this._active = false;
+
+      // We only have a backend transaction if any requests were placed
+      // against the transactions.
+      if (this._backendTransaction) {
+        await this._backend.commit(this._backendTransaction);
+      }
       this._committed = true;
       if (!this._error) {
         if (BridgeIDBFactory.enableTracing) {
diff --git 
a/packages/idb-bridge/src/idb-wpt-ported/idbcursor-advance-index.test.ts 
b/packages/idb-bridge/src/idb-wpt-ported/idbcursor-advance-index.test.ts
index a7be31f2..2d449a9a 100644
--- a/packages/idb-bridge/src/idb-wpt-ported/idbcursor-advance-index.test.ts
+++ b/packages/idb-bridge/src/idb-wpt-ported/idbcursor-advance-index.test.ts
@@ -1,5 +1,7 @@
 import test from "ava";
 import { BridgeIDBCursor } from "..";
+import { BridgeIDBRequest } from "../bridge-idb";
+import { InvalidStateError } from "../util/errors";
 import { createdb } from "./wptsupport";
 
 test("WPT test idbcursor_advance_index.htm", async (t) => {
@@ -34,6 +36,7 @@ test("WPT test idbcursor_advance_index.htm", async (t) => {
       cursor_rq.onsuccess = function (e: any) {
         var cursor = e.target.result;
         t.log(cursor);
+        t.true(e.target instanceof BridgeIDBRequest);
         t.true(cursor instanceof BridgeIDBCursor);
 
         switch (count) {
@@ -51,7 +54,259 @@ test("WPT test idbcursor_advance_index.htm", async (t) => {
             t.fail("unexpected count");
             break;
         }
+      };
+    };
+  });
+});
+
+// IDBCursor.advance() - attempt to pass a count parameter that is not a number
+test("WPT test idbcursor_advance_index2.htm", async (t) => {
+  await new Promise<void>((resolve, reject) => {
+    var db: any;
+
+    const records = [
+      { pKey: "primaryKey_0", iKey: "indexKey_0" },
+      { pKey: "primaryKey_1", iKey: "indexKey_1" },
+    ];
+
+    var open_rq = createdb(t);
+    open_rq.onupgradeneeded = function (e: any) {
+      db = e.target.result;
+      var objStore = db.createObjectStore("test", { keyPath: "pKey" });
+
+      objStore.createIndex("index", "iKey");
+
+      for (var i = 0; i < records.length; i++) objStore.add(records[i]);
+    };
+
+    open_rq.onsuccess = function (e) {
+      var cursor_rq = db
+        .transaction("test")
+        .objectStore("test")
+        .index("index")
+        .openCursor();
+
+      cursor_rq.onsuccess = function (e: any) {
+        var cursor = e.target.result;
+
+        t.true(cursor != null, "cursor exist");
+        t.throws(
+          () => {
+            // Original test uses "document".
+            cursor.advance({ foo: 42 });
+          },
+          { instanceOf: TypeError },
+        );
+        resolve();
+      };
+    };
+  });
+});
+
+// IDBCursor.advance() - index - attempt to advance backwards
+test("WPT test idbcursor_advance_index3.htm", async (t) => {
+  await new Promise<void>((resolve, reject) => {
+    var db: any;
+
+    const records = [
+      { pKey: "primaryKey_0", iKey: "indexKey_0" },
+      { pKey: "primaryKey_1", iKey: "indexKey_1" },
+    ];
+
+    var open_rq = createdb(t);
+    open_rq.onupgradeneeded = function (e: any) {
+      db = e.target.result;
+      var objStore = db.createObjectStore("test", { keyPath: "pKey" });
+
+      objStore.createIndex("index", "iKey");
+
+      for (var i = 0; i < records.length; i++) objStore.add(records[i]);
+    };
+
+    open_rq.onsuccess = function (e) {
+      var cursor_rq = db
+        .transaction("test")
+        .objectStore("test")
+        .index("index")
+        .openCursor();
+
+      cursor_rq.onsuccess = function (e: any) {
+        var cursor = e.target.result;
+
+        t.true(cursor != null, "cursor exist");
+        t.throws(
+          () => {
+            cursor.advance(-1);
+          },
+          { instanceOf: TypeError },
+        );
+        resolve();
+      };
+    };
+  });
+});
+
+// IDBCursor.advance() - index - iterate to the next record
+test("WPT test idbcursor_advance_index5.htm", async (t) => {
+  await new Promise<void>((resolve, reject) => {
+    var db: any;
+    let count = 0;
+    const records = [
+        { pKey: "primaryKey_0", iKey: "indexKey_0" },
+        { pKey: "primaryKey_1", iKey: "indexKey_1" },
+        { pKey: "primaryKey_1-2", iKey: "indexKey_1" },
+      ],
+      expected = [
+        { pKey: "primaryKey_0", iKey: "indexKey_0" },
+        { pKey: "primaryKey_1-2", iKey: "indexKey_1" },
+      ];
+
+    var open_rq = createdb(t);
+    open_rq.onupgradeneeded = function (e: any) {
+      db = e.target.result;
+      var objStore = db.createObjectStore("test", { keyPath: "pKey" });
+
+      objStore.createIndex("index", "iKey");
+
+      for (var i = 0; i < records.length; i++) objStore.add(records[i]);
+    };
+
+    open_rq.onsuccess = function (e: any) {
+      var cursor_rq = db
+        .transaction("test")
+        .objectStore("test")
+        .index("index")
+        .openCursor();
+
+      cursor_rq.onsuccess = function (e: any) {
+        var cursor = e.target.result;
+        if (!cursor) {
+          t.deepEqual(count, expected.length, "cursor run count");
+          resolve();
+        }
+
+        var record = cursor.value;
+        t.deepEqual(record.pKey, expected[count].pKey, "primary key");
+        t.deepEqual(record.iKey, expected[count].iKey, "index key");
+
+        cursor.advance(2);
+        count++;
+      };
+    };
+  });
+});
+
+// IDBCursor.advance() - index - throw TransactionInactiveError
+test("WPT test idbcursor_advance_index7.htm", async (t) => {
+  await new Promise<void>((resolve, reject) => {
+    var db: any;
+    const records = [
+      { pKey: "primaryKey_0", iKey: "indexKey_0" },
+      { pKey: "primaryKey_1", iKey: "indexKey_1" },
+    ];
+
+    var open_rq = createdb(t);
+    open_rq.onupgradeneeded = function (event: any) {
+      db = event.target.result;
+      var objStore = db.createObjectStore("store", { keyPath: "pKey" });
+      objStore.createIndex("index", "iKey");
+      for (var i = 0; i < records.length; i++) {
+        objStore.add(records[i]);
+      }
+      var rq = objStore.index("index").openCursor();
+      rq.onsuccess = function (event: any) {
+        var cursor = event.target.result;
+        t.true(cursor instanceof BridgeIDBCursor);
+
+        event.target.transaction.abort();
+        t.throws(
+          () => {
+            cursor.advance(1);
+          },
+          { name: "TransactionInactiveError" },
+          "Calling advance() should throws an exception 
TransactionInactiveError when the transaction is not active.",
+        );
+        resolve();
+      };
+    };
+  });
+});
+
+// IDBCursor.advance() - index - throw InvalidStateError
+test("WPT test idbcursor_advance_index8.htm", async (t) => {
+  await new Promise<void>((resolve, reject) => {
+    var db: any;
+    const records = [
+      { pKey: "primaryKey_0", iKey: "indexKey_0" },
+      { pKey: "primaryKey_1", iKey: "indexKey_1" },
+    ];
+
+    var open_rq = createdb(t);
+    open_rq.onupgradeneeded = function (event: any) {
+      db = event.target.result;
+      var objStore = db.createObjectStore("store", { keyPath: "pKey" });
+      objStore.createIndex("index", "iKey");
+      for (var i = 0; i < records.length; i++) {
+        objStore.add(records[i]);
       }
+      var rq = objStore.index("index").openCursor();
+      let called = false;
+      rq.onsuccess = function (event: any) {
+        if (called) {
+          return;
+        }
+        called = true;
+        var cursor = event.target.result;
+        t.true(cursor instanceof BridgeIDBCursor);
+
+        cursor.advance(1);
+        t.throws(
+          () => {
+            cursor.advance(1);
+          },
+          { name: "InvalidStateError" },
+          "Calling advance() should throw DOMException when the cursor is 
currently being iterated.",
+        );
+        t.pass();
+        resolve();
+      };
+    };
+  });
+});
+
+// IDBCursor.advance() - index - throw InvalidStateError caused by object 
store been deleted
+test("WPT test idbcursor_advance_index9.htm", async (t) => {
+  await new Promise<void>((resolve, reject) => {
+    var db: any;
+    const records = [
+      { pKey: "primaryKey_0", iKey: "indexKey_0" },
+      { pKey: "primaryKey_1", iKey: "indexKey_1" },
+    ];
+
+    var open_rq = createdb(t);
+    open_rq.onupgradeneeded = function (event: any) {
+      db = event.target.result;
+      var objStore = db.createObjectStore("store", { keyPath: "pKey" });
+      objStore.createIndex("index", "iKey");
+      for (var i = 0; i < records.length; i++) {
+        objStore.add(records[i]);
+      }
+      var rq = objStore.index("index").openCursor();
+      rq.onsuccess = function (event: any) {
+        var cursor = event.target.result;
+        t.true(cursor instanceof BridgeIDBCursor, "cursor exist");
+
+        db.deleteObjectStore("store");
+        t.throws(
+          () => {
+            cursor.advance(1);
+          },
+          { name: "InvalidStateError" },
+          "If the cursor's source or effective object store has been deleted, 
the implementation MUST throw a DOMException of type InvalidStateError",
+        );
+
+        resolve();
+      };
     };
   });
 });
diff --git 
a/packages/idb-bridge/src/idb-wpt-ported/idbobjectstore-add-put-exception-order.test.ts
 
b/packages/idb-bridge/src/idb-wpt-ported/idbobjectstore-add-put-exception-order.test.ts
new file mode 100644
index 00000000..77c4a939
--- /dev/null
+++ 
b/packages/idb-bridge/src/idb-wpt-ported/idbobjectstore-add-put-exception-order.test.ts
@@ -0,0 +1,104 @@
+import test, { ExecutionContext } from "ava";
+import { BridgeIDBCursor } from "..";
+import { BridgeIDBRequest } from "../bridge-idb";
+import { InvalidStateError } from "../util/errors";
+import { createdb, indexeddb_test } from "./wptsupport";
+
+async function t1(t: ExecutionContext, method: string): Promise<void> {
+  await indexeddb_test(
+    t,
+    (done, db) => {
+      const store = db.createObjectStore("s");
+      const store2 = db.createObjectStore("s2");
+
+      db.deleteObjectStore("s2");
+
+      setTimeout(() => {
+        t.throws(
+          () => {
+            (store2 as any)[method]("key", "value");
+          },
+          { name: "InvalidStateError" },
+          '"has been deleted" check (InvalidStateError) should precede ' +
+            '"not active" check (TransactionInactiveError)',
+        );
+        done();
+      }, 0);
+    },
+    (done, db) => {},
+    "t1",
+  );
+}
+
+/**
+ * IDBObjectStore.${method} exception order: 'TransactionInactiveError vs. 
ReadOnlyError'
+ */
+async function t2(t: ExecutionContext, method: string): Promise<void> {
+  await indexeddb_test(
+    t,
+    (done, db) => {
+      const store = db.createObjectStore("s");
+    },
+    (done, db) => {
+      (db as any)._debugName = method;
+      const tx = db.transaction("s", "readonly");
+      const store = tx.objectStore("s");
+
+      setTimeout(() => {
+        t.throws(
+          () => {
+            console.log(`calling ${method}`);
+            (store as any)[method]("key", "value");
+          },
+          {
+            name: "TransactionInactiveError",
+          },
+          '"not active" check (TransactionInactiveError) should precede ' +
+            '"read only" check (ReadOnlyError)',
+        );
+
+        done();
+      }, 0);
+
+      console.log(`queued task for ${method}`);
+    },
+    "t2",
+  );
+}
+
+/**
+ * IDBObjectStore.${method} exception order: 'ReadOnlyError vs. DataError'
+ */
+async function t3(t: ExecutionContext, method: string): Promise<void> {
+  await indexeddb_test(
+    t,
+    (done, db) => {
+      const store = db.createObjectStore("s");
+    },
+    (done, db) => {
+      const tx = db.transaction("s", "readonly");
+      const store = tx.objectStore("s");
+
+      t.throws(
+        () => {
+          (store as any)[method]({}, "value");
+        },
+        { name: "ReadOnlyError" },
+        '"read only" check (ReadOnlyError) should precede ' +
+          "key/data check (DataError)",
+      );
+
+      done();
+    },
+    "t3",
+  );
+}
+
+test("WPT idbobjectstore-add-put-exception-order.html (add, t1)", t1, "add");
+test("WPT idbobjectstore-add-put-exception-order.html (put, t1)", t1, "put");
+
+test("WPT idbobjectstore-add-put-exception-order.html (add, t2)", t2, "add");
+test("WPT idbobjectstore-add-put-exception-order.html (put, t2)", t2, "put");
+
+test("WPT idbobjectstore-add-put-exception-order.html (add, t3)", t3, "add");
+test("WPT idbobjectstore-add-put-exception-order.html (put, t3)", t3, "put");
diff --git a/packages/idb-bridge/src/idb-wpt-ported/wptsupport.ts 
b/packages/idb-bridge/src/idb-wpt-ported/wptsupport.ts
index 4a7205f8..6777dc12 100644
--- a/packages/idb-bridge/src/idb-wpt-ported/wptsupport.ts
+++ b/packages/idb-bridge/src/idb-wpt-ported/wptsupport.ts
@@ -422,3 +422,61 @@ export function format_value(val: any, seen?: any): string 
{
       }
   }
 }
+
+// Usage:
+//   indexeddb_test(
+//     (test_object, db_connection, upgrade_tx, open_request) => {
+//        // Database creation logic.
+//     },
+//     (test_object, db_connection, open_request) => {
+//        // Test logic.
+//        test_object.done();
+//     },
+//     'Test case description');
+export function indexeddb_test(
+  t: ExecutionContext,
+  upgrade_func: (
+    done: () => void,
+    db: IDBDatabase,
+    tx: IDBTransaction,
+    open: IDBOpenDBRequest,
+  ) => void,
+  open_func: (
+    done: () => void,
+    db: IDBDatabase,
+    open: IDBOpenDBRequest,
+  ) => void,
+  dbsuffix?: string,
+  options?: any,
+): Promise<void> {
+  return new Promise((resolve, reject) => {
+    options = Object.assign({ upgrade_will_abort: false }, options);
+    const dbname =
+      "testdb-" + new Date().getTime() + Math.random() + (dbsuffix ?? "");
+    var del = self.indexedDB.deleteDatabase(dbname);
+    del.onerror = () => t.fail("deleteDatabase should succeed");
+    var open = self.indexedDB.open(dbname, 1);
+    open.onupgradeneeded = function () {
+      var db = open.result;
+      t.teardown(function () {
+        // If open didn't succeed already, ignore the error.
+        open.onerror = function (e) {
+          e.preventDefault();
+        };
+        db.close();
+        self.indexedDB.deleteDatabase(db.name);
+      });
+      var tx = open.transaction!;
+      upgrade_func(resolve, db, tx, open);
+    };
+    if (options.upgrade_will_abort) {
+      open.onsuccess = () => t.fail("open should not succeed");
+    } else {
+      open.onerror = () => t.fail("open should succeed");
+      open.onsuccess = function () {
+        var db = open.result;
+        if (open_func) open_func(resolve, db, open);
+      };
+    }
+  });
+}

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]