gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated (c800e801 -> 3eced74a)


From: gnunet
Subject: [taler-wallet-core] branch master updated (c800e801 -> 3eced74a)
Date: Mon, 22 Feb 2021 14:28:01 +0100

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

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

    from c800e801 idb: fix shimming
     new e6946694 idb: more tests, fix DB deletion, exception ordering and 
transaction active checks
     new 3eced74a more tests, fix event ordering issue

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 packages/idb-bridge/src/MemoryBackend.ts           | 122 ++++------
 packages/idb-bridge/src/backend-interface.ts       |   8 +-
 packages/idb-bridge/src/bridge-idb.ts              | 162 ++++++++++---
 .../event-dispatch-active-flag.test.ts             |  57 +++++
 .../idb-wpt-ported/idbcursor-advance-index.test.ts | 255 +++++++++++++++++++++
 .../idbobjectstore-add-put-exception-order.test.ts | 103 +++++++++
 .../idbtransaction-oncomplete.test.ts              |  49 ++++
 .../idb-bridge/src/idb-wpt-ported/wptsupport.ts    | 122 +++++++++-
 packages/idb-bridge/src/util/queueTask.ts          |  15 +-
 9 files changed, 770 insertions(+), 123 deletions(-)
 create mode 100644 
packages/idb-bridge/src/idb-wpt-ported/event-dispatch-active-flag.test.ts
 create mode 100644 
packages/idb-bridge/src/idb-wpt-ported/idbobjectstore-add-put-exception-order.test.ts
 create mode 100644 
packages/idb-bridge/src/idb-wpt-ported/idbtransaction-oncomplete.test.ts

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..ceba618d 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();
     }
@@ -1584,6 +1609,10 @@ export class BridgeIDBObjectStore implements 
IDBObjectStore {
       throw new TypeError();
     }
 
+    if (!this._transaction._active) {
+      throw new TransactionInactiveError();
+    }
+
     if (this._deleted) {
       throw new InvalidStateError(
         "tried to call 'delete' on a deleted object store",
@@ -1893,6 +1922,8 @@ export class BridgeIDBRequest extends FakeEventTarget 
implements IDBRequest {
   onsuccess: EventListener | null = null;
   onerror: EventListener | null = null;
 
+  _debugName: string | undefined;
+
   get error() {
     if (this.readyState === "pending") {
       throw new InvalidStateError();
@@ -1973,6 +2004,25 @@ export class BridgeIDBOpenDBRequest
   }
 }
 
+function waitMacroQueue(): Promise<void> {
+  return new Promise<void>((resolve, reject) => {
+    let immCalled = false;
+    let timeoutCalled = false;
+    setImmediate(() => {
+      immCalled = true;
+      if (immCalled && timeoutCalled) {
+        resolve();
+      }
+    });
+    setTimeout(() => {
+      timeoutCalled = true;
+      if (immCalled && timeoutCalled) {
+        resolve();
+      }
+    }, 0);
+  });
+}
+
 // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#transaction
 /** @public */
 export class BridgeIDBTransaction
@@ -1988,6 +2038,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 +2129,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 +2176,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);
     }
@@ -2146,7 +2207,7 @@ export class BridgeIDBTransaction
   // http://w3c.github.io/IndexedDB/#dom-idbtransaction-objectstore
   public objectStore(name: string): BridgeIDBObjectStore {
     if (!this._active) {
-      throw new InvalidStateError();
+      throw new TransactionInactiveError();
     }
 
     if (!this._db._schema.objectStores[name]) {
@@ -2208,17 +2269,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 +2288,27 @@ 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})`,
+          );
+        }
+      }
+
+      await waitMacroQueue();
+
       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;
@@ -2243,9 +2316,12 @@ export class BridgeIDBTransaction
           BridgeIDBFactory.enableTracing &&
             console.log("TRACE: running operation in transaction");
           const result = await operation();
+          // Wait until setTimeout/setImmediate tasks are run
           BridgeIDBFactory.enableTracing &&
             console.log(
-              "TRACE: operation in transaction finished with success",
+              `TRACE: request (${
+                request._debugName ?? "??"
+              }) in transaction finished with success`,
             );
           request.readyState = "done";
           request.result = result;
@@ -2258,6 +2334,10 @@ export class BridgeIDBTransaction
             cancelable: false,
           });
 
+          queueTask(() => {
+            this._active = false;
+          });
+
           try {
             event.eventPath = [this._db, this];
             request.dispatchEvent(event);
@@ -2311,14 +2391,26 @@ 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) {
-          console.log("dispatching 'complete' event on transaction");
+          console.log(
+            `dispatching 'complete' event on transaction (${
+              this._debugName ?? "??"
+            })`,
+          );
         }
         const event = new FakeEvent("complete");
         event.eventPath = [this._db, this];
diff --git 
a/packages/idb-bridge/src/idb-wpt-ported/event-dispatch-active-flag.test.ts 
b/packages/idb-bridge/src/idb-wpt-ported/event-dispatch-active-flag.test.ts
new file mode 100644
index 00000000..f5668c90
--- /dev/null
+++ b/packages/idb-bridge/src/idb-wpt-ported/event-dispatch-active-flag.test.ts
@@ -0,0 +1,57 @@
+import test from "ava";
+import { BridgeIDBRequest } from "..";
+import {
+  createdb,
+  indexeddb_test,
+  is_transaction_active,
+  keep_alive,
+} from "./wptsupport";
+
+test("WPT test abort-in-initial-upgradeneeded.htm", async (t) => {
+  // Transactions are active during success handlers
+  await indexeddb_test(
+    t,
+    (done, db, tx) => {
+      db.createObjectStore("store");
+    },
+    (done, db) => {
+      const tx = db.transaction("store");
+      const release_tx = keep_alive(t, tx, "store");
+
+      t.assert(
+        is_transaction_active(t, tx, "store"),
+        "Transaction should be active after creation",
+      );
+
+      const request = tx.objectStore("store").get(4242);
+      (request as BridgeIDBRequest)._debugName = "req-main"; 
+      request.onerror = () => t.fail("request should succeed");
+      request.onsuccess = () => {
+
+        t.true(
+          is_transaction_active(t, tx, "store"),
+          "Transaction should be active during success handler",
+        );
+
+        let saw_handler_promise = false;
+        Promise.resolve().then(() => {
+          saw_handler_promise = true;
+          t.true(
+            is_transaction_active(t, tx, "store"),
+            "Transaction should be active in handler's microtasks",
+          );
+        });
+
+        setTimeout(() => {
+          t.true(saw_handler_promise);
+          t.false(
+            is_transaction_active(t, tx, "store"),
+            "Transaction should be inactive in next task",
+          );
+          release_tx();
+          done();
+        }, 0);
+      };
+    },
+  );
+});
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..a3aead9d
--- /dev/null
+++ 
b/packages/idb-bridge/src/idb-wpt-ported/idbobjectstore-add-put-exception-order.test.ts
@@ -0,0 +1,103 @@
+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) => {
+      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/idbtransaction-oncomplete.test.ts 
b/packages/idb-bridge/src/idb-wpt-ported/idbtransaction-oncomplete.test.ts
new file mode 100644
index 00000000..8e0b4387
--- /dev/null
+++ b/packages/idb-bridge/src/idb-wpt-ported/idbtransaction-oncomplete.test.ts
@@ -0,0 +1,49 @@
+import test from "ava";
+import { createdb } from "./wptsupport";
+
+// IDBTransaction - complete event
+test("WPT idbtransaction-oncomplete.htm", async (t) => {
+  await new Promise<void>((resolve, reject) => {
+    var db: any;
+    var store: any;
+    let open_rq = createdb(t);
+    let stages: any[] = [];
+
+    open_rq.onupgradeneeded = function (e: any) {
+      stages.push("upgradeneeded");
+
+      db = e.target.result;
+      store = db.createObjectStore("store");
+
+      e.target.transaction.oncomplete = function () {
+        stages.push("complete");
+      };
+    };
+
+    open_rq.onsuccess = function (e) {
+      stages.push("success");
+
+      // Making a totally new transaction to check
+      db
+        .transaction("store")
+        .objectStore("store")
+        .count().onsuccess = function (e: any) {
+        t.deepEqual(stages, ["upgradeneeded", "complete", "success"]);
+        resolve();
+      };
+      // XXX: Make one with real transactions, not only open() versionchange 
one
+
+      /*db.transaction.objectStore('store').openCursor().onsuccess = 
function(e) {
+          stages.push("opencursor1");
+      }
+      store.openCursor().onsuccess = function(e) {
+          stages.push("opencursor2");
+      }
+      e.target.transaction.objectStore('store').openCursor().onsuccess = 
function(e) {
+          stages.push("opencursor3");
+      }
+      */
+    };
+  });
+  t.pass();
+});
diff --git a/packages/idb-bridge/src/idb-wpt-ported/wptsupport.ts 
b/packages/idb-bridge/src/idb-wpt-ported/wptsupport.ts
index 4a7205f8..9ec46c76 100644
--- a/packages/idb-bridge/src/idb-wpt-ported/wptsupport.ts
+++ b/packages/idb-bridge/src/idb-wpt-ported/wptsupport.ts
@@ -1,5 +1,5 @@
 import test, { ExecutionContext } from "ava";
-import { BridgeIDBFactory } from "..";
+import { BridgeIDBFactory, BridgeIDBRequest } from "..";
 import {
   IDBDatabase,
   IDBIndex,
@@ -422,3 +422,123 @@ 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);
+      };
+    }
+  });
+}
+
+/**
+ * Keeps the passed transaction alive indefinitely (by making requests
+ * against the named store). Returns a function that asserts that the
+ * transaction has not already completed and then ends the request loop so that
+ * the transaction may autocommit and complete.
+ */
+export function keep_alive(
+  t: ExecutionContext,
+  tx: IDBTransaction,
+  store_name: string,
+) {
+  let completed = false;
+  tx.addEventListener("complete", () => {
+    completed = true;
+  });
+
+  let keepSpinning = true;
+  let spinCount = 0;
+
+  function spin() {
+    console.log("spinning");
+    if (!keepSpinning) return;
+    const request = tx.objectStore(store_name).get(0);
+    (request as BridgeIDBRequest)._debugName = `req-spin-${spinCount}`;
+    spinCount++;
+    request.onsuccess = spin;
+  }
+  spin();
+
+  return () => {
+    t.log("stopping spin");
+    t.false(completed, "Transaction completed while kept alive");
+    keepSpinning = false;
+  };
+}
+
+// Checks to see if the passed transaction is active (by making
+// requests against the named store).
+export function is_transaction_active(
+  t: ExecutionContext,
+  tx: IDBTransaction,
+  store_name: string,
+) {
+  try {
+    const request = tx.objectStore(store_name).get(0);
+    request.onerror = (e) => {
+      e.preventDefault();
+      e.stopPropagation();
+    };
+    return true;
+  } catch (ex) {
+    console.log(ex.stack);
+    t.deepEqual(
+      ex.name,
+      "TransactionInactiveError",
+      "Active check should either not throw anything, or throw " +
+        "TransactionInactiveError",
+    );
+    return false;
+  }
+}
diff --git a/packages/idb-bridge/src/util/queueTask.ts 
b/packages/idb-bridge/src/util/queueTask.ts
index 53563ffd..297602c6 100644
--- a/packages/idb-bridge/src/util/queueTask.ts
+++ b/packages/idb-bridge/src/util/queueTask.ts
@@ -15,7 +15,20 @@
  */
 
 export function queueTask(fn: () => void) {
-  setImmediate(fn);
+  let called = false;
+  const callFirst = () => {
+    if (called) {
+      return;
+    }
+    called = true;
+    fn();
+  };
+  // We must schedule both of these,
+  // since on node, there is no guarantee
+  // that a setImmediate function that is registered
+  // before a setTimeout function is called first.
+  setImmediate(callFirst);
+  setTimeout(callFirst, 0);
 }
 
 export default queueTask;

-- 
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]