/*
 Copyright 2017 Jeremy Scheff
 Copyright 2019-2021 Taler Systems S.A.

 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at

 http://www.apache.org/licenses/LICENSE-2.0

 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 or implied. See the License for the specific language governing
 permissions and limitations under the License.
 */

import {
  Backend,
  ConnectResult,
  DatabaseConnection,
  DatabaseTransaction,
  IndexGetQuery,
  IndexMeta,
  ObjectStoreGetQuery,
  ObjectStoreMeta,
  RecordGetResponse,
  RecordStoreRequest,
  ResultLevel,
  StoreLevel,
} from "./backend-interface.js";
import {
  DOMException,
  DOMStringList,
  EventListener,
  IDBCursor,
  IDBCursorDirection,
  IDBDatabase,
  IDBIndex,
  IDBKeyPath,
  IDBKeyRange,
  IDBObjectStore,
  IDBOpenDBRequest,
  IDBRequest,
  IDBTransaction,
  IDBTransactionMode,
  IDBValidKey,
} from "./idbtypes.js";
import { canInjectKey } from "./util/canInjectKey.js";
import { compareKeys } from "./util/cmp.js";
import { enforceRange } from "./util/enforceRange.js";
import {
  AbortError,
  ConstraintError,
  DataCloneError,
  DataError,
  InvalidAccessError,
  InvalidStateError,
  NotFoundError,
  ReadOnlyError,
  TransactionInactiveError,
  VersionError,
} from "./util/errors.js";
import { fakeDOMStringList } from "./util/fakeDOMStringList.js";
import FakeEvent from "./util/FakeEvent.js";
import FakeEventTarget from "./util/FakeEventTarget.js";
import { makeStoreKeyValue } from "./util/makeStoreKeyValue.js";
import { normalizeKeyPath } from "./util/normalizeKeyPath.js";
import { openPromise } from "./util/openPromise.js";
import queueTask from "./util/queueTask.js";
import { checkStructuredCloneOrThrow } from "./util/structuredClone.js";
import { validateKeyPath } from "./util/validateKeyPath.js";
import { valueToKey } from "./util/valueToKey.js";

export type CursorSource = BridgeIDBIndex | BridgeIDBObjectStore;

export interface RequestObj {
  operation: () => Promise<any>;
  request?: BridgeIDBRequest | undefined;
  source?: any;
}

export interface BridgeIDBDatabaseInfo {
  name: string;
  version: number;
}

function simplifyRange(
  r: IDBValidKey | IDBKeyRange | undefined | null,
): IDBKeyRange | null {
  if (r && typeof r === "object" && "lower" in r) {
    return r;
  }
  if (r === undefined || r === null) {
    return null;
  }
  return BridgeIDBKeyRange.bound(r, r, false, false);
}

/**
 * http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#cursor
 */
export class BridgeIDBCursor implements IDBCursor {
  _request: BridgeIDBRequest | undefined;

  private _gotValue: boolean = false;
  private _range: IDBValidKey | IDBKeyRange | undefined | null;
  // Key of previously returned record
  private _indexPosition: IDBValidKey | undefined = undefined;
  private _objectStorePosition: IDBValidKey | undefined = undefined;
  private _keyOnly: boolean;

  private _source: CursorSource;
  private _direction: IDBCursorDirection;
  private _key: IDBValidKey | undefined = undefined;
  private _primaryKey: IDBValidKey | undefined = undefined;
  private _indexName: string | undefined;
  private _objectStoreName: string;

  protected _value: any = undefined;

  constructor(
    source: CursorSource,
    objectStoreName: string,
    indexName: string | undefined,
    range: IDBValidKey | IDBKeyRange | null | undefined,
    direction: IDBCursorDirection,
    request: BridgeIDBRequest,
    keyOnly: boolean,
  ) {
    this._indexName = indexName;
    this._objectStoreName = objectStoreName;
    this._range = range;
    this._source = source;
    this._direction = direction;
    this._request = request;
    this._keyOnly = keyOnly;
  }

  get _effectiveObjectStore(): BridgeIDBObjectStore {
    if (this.source instanceof BridgeIDBObjectStore) {
      return this.source;
    }
    return this.source._objectStore;
  }

  get _backend(): Backend {
    return this._source._backend;
  }

  // Read only properties
  get source() {
    return this._source;
  }
  set source(val) {
    /* For babel */
  }

  get direction() {
    return this._direction;
  }
  set direction(val) {
    /* For babel */
  }

  get key(): IDBValidKey {
    const k = this._key;
    if (k === null || k === undefined) {
      throw Error("no key");
    }
    return k;
  }
  set key(val) {
    /* For babel */
  }

  get primaryKey(): IDBValidKey {
    const k = this._primaryKey;
    if (k === 0 || k === undefined) {
      throw Error("no key");
    }
    return k;
  }

  set primaryKey(val) {
    /* For babel */
  }

  protected get _isValueCursor(): boolean {
    return false;
  }

  /**
   * https://w3c.github.io/IndexedDB/#iterate-a-cursor
   */
  async _iterate(
    key?: IDBValidKey,
    primaryKey?: IDBValidKey,
  ): Promise<BridgeIDBCursor | null> {
    BridgeIDBFactory.enableTracing &&
      console.log(
        `iterating cursor os=${this._objectStoreName},idx=${this._indexName}`,
      );
    BridgeIDBFactory.enableTracing &&
      console.log("cursor type ", this.toString());
    const indexName = this._indexName;

    const { btx } = this.source._confirmStartedBackendTransaction();

    let response: RecordGetResponse;

    if (indexName != null) {
      const indexRecordGetRequest: IndexGetQuery = {
        direction: this.direction,
        indexName: indexName,
        lastIndexPosition: this._indexPosition,
        lastObjectStorePosition: this._objectStorePosition,
        limit: 1,
        range: simplifyRange(this._range),
        objectStoreName: this._objectStoreName,
        advanceIndexKey: key,
        advancePrimaryKey: primaryKey,
        resultLevel: this._keyOnly ? ResultLevel.OnlyKeys : ResultLevel.Full,
      };
      response = await this._backend.getIndexRecords(
        btx,
        indexRecordGetRequest,
      );
    } else {
      if (primaryKey != null) {
        // Only allowed for index cursors
        throw new InvalidAccessError();
      }
      const objStoreGetRequest: ObjectStoreGetQuery = {
        direction: this.direction,
        lastObjectStorePosition: this._objectStorePosition,
        limit: 1,
        range: simplifyRange(this._range),
        objectStoreName: this._objectStoreName,
        advancePrimaryKey: key,
        resultLevel: this._keyOnly ? ResultLevel.OnlyKeys : ResultLevel.Full,
      };
      response = await this._backend.getObjectStoreRecords(
        btx,
        objStoreGetRequest,
      );
    }

    if (response.count === 0) {
      if (BridgeIDBFactory.enableTracing) {
        console.log("cursor is returning empty result");
      }
      this._gotValue = false;
      this._key = undefined;
      this._value = undefined;
      return null;
    }

    if (response.count !== 1) {
      throw Error("invariant failed");
    }

    if (this._indexName !== undefined) {
      this._key = response.indexKeys![0];
    } else {
      this._key = response.primaryKeys![0];
    }

    this._primaryKey = response.primaryKeys![0];

    if (!this._keyOnly) {
      this._value = response.values![0];
    }

    this._gotValue = true;
    this._objectStorePosition = response.primaryKeys![0];
    if (response.indexKeys !== undefined && response.indexKeys.length > 0) {
      this._indexPosition = response.indexKeys![0];
    }

    return this;
  }

  // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBCursor-update-IDBRequest-any-value
  public update(value: any) {
    if (value === undefined) {
      throw new TypeError();
    }

    const transaction = this._effectiveObjectStore._transaction;

    if (!transaction._active) {
      throw new TransactionInactiveError();
    }

    if (transaction.mode === "readonly") {
      throw new ReadOnlyError();
    }

    if (this._effectiveObjectStore._deleted) {
      throw new InvalidStateError();
    }

    if (
      !(this.source instanceof BridgeIDBObjectStore) &&
      this.source._deleted
    ) {
      throw new InvalidStateError();
    }

    if (!this._gotValue || !this._isValueCursor) {
      throw new InvalidStateError();
    }

    const key = this._primaryKey;

    // Effective object store
    let os: BridgeIDBObjectStore;
    if (this.source instanceof BridgeIDBObjectStore) {
      os = this.source;
    } else {
      os = this.source._objectStore;
    }

    try {
      // Only called for the side effect of throwing an exception
      checkStructuredCloneOrThrow(value);
    } catch (e) {
      throw new DataCloneError();
    }

    if (os.keyPath !== null && os.keyPath !== undefined) {
      if (!canInjectKey(os.keyPath, value)) {
        throw new DataError();
      }
    }

    const storeReq: RecordStoreRequest = {
      key,
      value,
      objectStoreName: this._objectStoreName,
      storeLevel: StoreLevel.UpdateExisting,
    };

    const operation = async () => {
      if (BridgeIDBFactory.enableTracing) {
        console.log("updating at cursor");
      }
      const { btx } = this.source._confirmStartedBackendTransaction();
      await this._backend.storeRecord(btx, storeReq);
      // FIXME: update the index position here!
    };
    return transaction._execRequestAsync({
      operation,
      source: this,
    });
  }

  /**
   * 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) {
      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 () => {
      let res: IDBCursor | null = null;
      for (let i = 0; i < count; i++) {
        res = await this._iterate();
      }
      return res;
    };

    transaction._execRequestAsync({
      operation,
      request: this._request,
      source: this.source,
    });

    this._gotValue = false;
  }

  /**
   * http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBCursor-continue-void-any-key
   */
  public continue(key?: IDBValidKey) {
    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 (key !== undefined) {
      key = valueToKey(key);
      let lastKey =
        this._indexName === undefined
          ? this._objectStorePosition
          : this._indexPosition;

      const cmpResult = compareKeys(key, lastKey);

      if (
        (cmpResult <= 0 &&
          (this.direction === "next" || this.direction === "nextunique")) ||
        (cmpResult >= 0 &&
          (this.direction === "prev" || this.direction === "prevunique"))
      ) {
        throw new DataError();
      }
    }

    if (this._request) {
      this._request.readyState = "pending";
    }

    const operation = async () => {
      return this._iterate(key);
    };

    transaction._execRequestAsync({
      operation,
      request: this._request,
      source: this.source,
    });

    this._gotValue = false;
  }

  // https://w3c.github.io/IndexedDB/#dom-idbcursor-continueprimarykey
  public continuePrimaryKey(key: IDBValidKey, primaryKey: IDBValidKey) {
    throw Error("not implemented");
  }

  public delete() {
    const transaction = this._effectiveObjectStore._transaction;

    if (!transaction._active) {
      throw new TransactionInactiveError();
    }

    if (transaction.mode === "readonly") {
      throw new ReadOnlyError();
    }

    if (this._effectiveObjectStore._deleted) {
      throw new InvalidStateError();
    }
    if (
      !(this.source instanceof BridgeIDBObjectStore) &&
      this.source._deleted
    ) {
      throw new InvalidStateError();
    }

    if (!this._gotValue || !this._isValueCursor) {
      throw new InvalidStateError();
    }

    const operation = async () => {
      const { btx } = this.source._confirmStartedBackendTransaction();
      this._backend.deleteRecord(
        btx,
        this._objectStoreName,
        BridgeIDBKeyRange._valueToKeyRange(this._primaryKey),
      );
    };

    return transaction._execRequestAsync({
      operation,
      source: this,
    });
  }

  public toString() {
    return "[object IDBCursor]";
  }
}

export class BridgeIDBCursorWithValue extends BridgeIDBCursor {
  get value(): any {
    return this._value;
  }

  protected get _isValueCursor(): boolean {
    return true;
  }

  constructor(
    source: CursorSource,
    objectStoreName: string,
    indexName: string | undefined,
    range: IDBValidKey | IDBKeyRange | undefined | null,
    direction: IDBCursorDirection,
    request?: any,
  ) {
    super(source, objectStoreName, indexName, range, direction, request, false);
  }

  public toString() {
    return "[object IDBCursorWithValue]";
  }
}

/**
 * Ensure that an active version change transaction is currently running.
 */
const confirmActiveVersionchangeTransaction = (database: BridgeIDBDatabase) => {
  if (!database._upgradeTransaction) {
    throw new InvalidStateError();
  }

  // Find the latest versionchange transaction
  const transactions = database._transactions.filter(
    (tx: BridgeIDBTransaction) => {
      return tx.mode === "versionchange";
    },
  );
  const transaction = transactions[transactions.length - 1];

  if (!transaction || transaction._finished) {
    throw new InvalidStateError();
  }

  if (!transaction._active) {
    throw new TransactionInactiveError();
  }

  return transaction;
};

// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#database-interface
export class BridgeIDBDatabase extends FakeEventTarget implements IDBDatabase {
  _closePending = false;
  _closed = false;
  _transactions: Array<BridgeIDBTransaction> = [];

  _upgradeTransaction: BridgeIDBTransaction | null = null;

  _backendConnection: DatabaseConnection;
  _backend: Backend;

  _name: string;

  _initialVersion: number;

  _version: number;

  // "object store set" from the spec
  _objectStoreSet: string[];

  // _schema: Schema;

  /**
   * Name that can be set to identify the object store in logs.
   */
  _debugName: string | undefined = undefined;

  get name(): string {
    return this._name;
  }

  get version(): number {
    return this._version;
  }

  get objectStoreNames(): DOMStringList {
    return fakeDOMStringList([...this._objectStoreSet]).sort() as DOMStringList;
  }

  /**
   * http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#database-closing-steps
   */
  _closeConnection() {
    this._closePending = true;

    // Spec is unclear what "complete" means, we assume it's
    // the same as "finished".
    const transactionsComplete = this._transactions.every(
      (transaction: BridgeIDBTransaction) => {
        return transaction._finished;
      },
    );

    if (transactionsComplete) {
      this._closed = true;
      this._backend.close(this._backendConnection).catch((e) => {
        console.error("Error while closing connection.");
        console.error(e);
      });
    } else {
      queueTask(() => {
        this._closeConnection();
      });
    }
  }

  constructor(name: string, backend: Backend, connResult: ConnectResult) {
    super();
    this._name = name;
    this._version = this._initialVersion = connResult.version;
    this._backend = backend;
    this._backendConnection = connResult.conn;
    this._objectStoreSet = connResult.objectStores;
  }

  // http://w3c.github.io/IndexedDB/#dom-idbdatabase-createobjectstore
  public createObjectStore(
    name: string,
    options: {
      autoIncrement?: boolean;
      keyPath?: null | IDBKeyPath | IDBKeyPath[];
    } | null = {},
  ): BridgeIDBObjectStore {
    if (name === undefined) {
      throw new TypeError();
    }
    const transaction = confirmActiveVersionchangeTransaction(this);
    const backendTx = transaction._backendTransaction;
    if (!backendTx) {
      throw Error("invariant violated");
    }

    const keyPath =
      options !== null && options.keyPath !== undefined
        ? options.keyPath
        : null;
    const autoIncrement =
      options !== null && options.autoIncrement !== undefined
        ? options.autoIncrement
        : false;

    if (keyPath !== null) {
      validateKeyPath(keyPath);
    }

    if (this._objectStoreSet.includes(name)) {
      // Already exists
      throw new ConstraintError();
    }

    if (autoIncrement && (keyPath === "" || Array.isArray(keyPath))) {
      throw new InvalidAccessError();
    }

    transaction._backend.createObjectStore(
      backendTx,
      name,
      keyPath !== null ? normalizeKeyPath(keyPath) : null,
      autoIncrement,
    );

    transaction._scope.add(name);
    this._objectStoreSet.push(name);
    this._objectStoreSet.sort();

    const newObjectStore = transaction.objectStore(name);
    newObjectStore._justCreated = true;
    return newObjectStore;
  }

  public deleteObjectStore(name: string): void {
    if (name === undefined) {
      throw new TypeError();
    }
    const transaction = confirmActiveVersionchangeTransaction(this);
    const backendTx = transaction._backendTransaction;
    if (!backendTx) {
      throw Error("invariant violated");
    }
    this._backend.deleteObjectStore(backendTx, name);
    const os = transaction._objectStoresCache.get(name);
    if (os) {
      os._deleted = true;
      transaction._objectStoresCache.delete(name);
    }
    transaction._cachedObjectStoreNames = undefined;
    transaction._scope.delete(name);
    const nameIdx = this._objectStoreSet.indexOf(name);
    this._objectStoreSet.splice(nameIdx, 1);
  }

  public _internalTransaction(
    storeNames: string | string[],
    mode?: IDBTransactionMode,
    backendTransaction?: DatabaseTransaction,
    openRequest?: BridgeIDBOpenDBRequest,
  ): BridgeIDBTransaction {
    mode = mode !== undefined ? mode : "readonly";
    if (
      mode !== "readonly" &&
      mode !== "readwrite" &&
      mode !== "versionchange"
    ) {
      throw new TypeError("Invalid mode: " + mode);
    }

    if (this._upgradeTransaction) {
      throw new InvalidStateError();
    }

    if (this._closePending) {
      throw new InvalidStateError(
        `tried to start transaction on ${this._name}, but a close is pending`,
      );
    }

    if (!Array.isArray(storeNames)) {
      storeNames = [storeNames];
    }
    if (storeNames.length === 0 && mode !== "versionchange") {
      throw new InvalidAccessError();
    }
    for (const storeName of storeNames) {
      if (!this.objectStoreNames.contains(storeName)) {
        throw new NotFoundError(
          "No objectStore named " + storeName + " in this database",
        );
      }
    }

    const tx = new BridgeIDBTransaction(
      storeNames,
      mode,
      this,
      backendTransaction,
      openRequest,
    );
    this._transactions.push(tx);

    queueTask(() => {
      BridgeIDBFactory.enableTracing &&
        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,
  ): BridgeIDBTransaction {
    if (mode === "versionchange") {
      throw new TypeError("Invalid mode: " + mode);
    }
    return this._internalTransaction(storeNames, mode);
  }

  public close() {
    this._closeConnection();
  }

  public toString() {
    return "[object IDBDatabase]";
  }
}

export type DatabaseList = Array<{ name: string; version: number }>;

export class BridgeIDBFactory {
  public cmp = compareKeys;
  private backend: Backend;
  private connections: BridgeIDBDatabase[] = [];
  static enableTracing: boolean = false;

  public constructor(backend: Backend) {
    this.backend = backend;
  }

  // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBFactory-deleteDatabase-IDBOpenDBRequest-DOMString-name
  public deleteDatabase(name: string): BridgeIDBOpenDBRequest {
    const request = new BridgeIDBOpenDBRequest();
    request._source = null;

    queueTask(async () => {
      const databases = await this.backend.getDatabases();
      const dbInfo = databases.find((x) => x.name == name);
      if (!dbInfo) {
        // Database already doesn't exist, success!
        const event = new BridgeIDBVersionChangeEvent("success", {
          newVersion: null,
          oldVersion: 0,
        });
        request.dispatchEvent(event);
        return;
      }
      const oldVersion = dbInfo.version;

      try {
        await this.backend.deleteDatabase(name);
        request.result = undefined;
        request.readyState = "done";

        const event2 = new BridgeIDBVersionChangeEvent("success", {
          newVersion: null,
          oldVersion,
        });
        request.dispatchEvent(event2);
      } catch (err: any) {
        const myErr = new Error();
        myErr.name = err.name;
        myErr.message = err.message;
        request.error = myErr;
        request.readyState = "done";

        const event = new FakeEvent("error", {
          bubbles: true,
          cancelable: true,
        });
        event.eventPath = [];
        request.dispatchEvent(event);
      }
    });

    return request;
  }

  // tslint:disable-next-line max-line-length
  // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBFactory-open-IDBOpenDBRequest-DOMString-name-unsigned-long-long-version
  public open(name: string, version?: number): BridgeIDBOpenDBRequest {
    if (arguments.length > 1 && version !== undefined) {
      // Based on spec, not sure why "MAX_SAFE_INTEGER" instead of "unsigned long long", but it's needed to pass
      // tests
      version = enforceRange(version, "MAX_SAFE_INTEGER");
    }
    if (version === 0) {
      throw new TypeError();
    }

    if (BridgeIDBFactory.enableTracing) {
      console.log(`opening DB ${name} at version ${version}`);
    }

    const request = new BridgeIDBOpenDBRequest();

    queueTask(async () => {
      if (BridgeIDBFactory.enableTracing) {
        console.log(`running task to open DB ${name} at version ${version}`);
      }
      let dbConnRes: ConnectResult;
      try {
        if (BridgeIDBFactory.enableTracing) {
          console.log("TRACE: connecting to database");
        }
        dbConnRes = await this.backend.connectDatabase(name);
        if (BridgeIDBFactory.enableTracing) {
          console.log("TRACE: connected!");
        }
      } catch (err: any) {
        if (BridgeIDBFactory.enableTracing) {
          console.log(
            "TRACE: caught exception while trying to connect with backend:",
            err,
          );
        }
        request._finishWithError(err);
        return;
      }
      const existingVersion = dbConnRes.version;

      if (version === undefined) {
        version = existingVersion !== 0 ? existingVersion : 1;
      }

      const requestedVersion = version;

      if (BridgeIDBFactory.enableTracing) {
        console.log(
          `existing version of DB ${name} is ${existingVersion}, requesting ${requestedVersion}`,
        );
      }

      BridgeIDBFactory.enableTracing &&
        console.log(
          `TRACE: existing version ${existingVersion}, requested version ${requestedVersion}`,
        );

      if (existingVersion > requestedVersion) {
        this.backend.close(dbConnRes.conn).catch((e) => {
          console.error("failed to close database");
          console.error(e);
        });
        request._finishWithError(
          new VersionError(
            `requested version ${requestedVersion}, existing version is ${existingVersion}`,
          ),
        );
        return;
      }

      const db = new BridgeIDBDatabase(name, this.backend, dbConnRes);

      if (existingVersion == requestedVersion) {
        request.result = db;
        request.readyState = "done";

        const event2 = new FakeEvent("success", {
          bubbles: false,
          cancelable: false,
        });
        event2.eventPath = [];
        if (BridgeIDBFactory.enableTracing) {
          console.log(
            "open() requested same version, dispatching 'success' event on transaction",
          );
        }
        request.dispatchEvent(event2);
      } else if (existingVersion < requestedVersion) {
        // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-steps-for-running-a-versionchange-transaction

        for (const otherConn of this.connections) {
          if (otherConn._name != db._name) {
            continue;
          }
          if (otherConn._closePending || otherConn._closed) {
            continue;
          }
          if (BridgeIDBFactory.enableTracing) {
            console.log(
              "dispatching 'versionchange' event to other connection",
            );
          }
          const event = new BridgeIDBVersionChangeEvent("versionchange", {
            newVersion: version,
            oldVersion: existingVersion,
          });
          otherConn.dispatchEvent(event);
        }

        if (this._anyOpen(name)) {
          if (BridgeIDBFactory.enableTracing) {
            console.log(
              "other connections are still open, dispatching 'blocked' event to other connection",
            );
          }
          const event = new BridgeIDBVersionChangeEvent("blocked", {
            newVersion: version,
            oldVersion: existingVersion,
          });
          request.dispatchEvent(event);
        }

        const backendTransaction = await this.backend.enterVersionChange(
          dbConnRes.conn,
          requestedVersion,
        );

        // We need to expose the new version number to the upgrade transaction.
        db._version = version;
        const transaction = db._internalTransaction(
          dbConnRes.objectStores,
          "versionchange",
          backendTransaction,
          request,
        );

        db._upgradeTransaction = transaction;

        if (BridgeIDBFactory.enableTracing) {
          console.log("dispatching upgradeneeded event");
        }

        const event = new BridgeIDBVersionChangeEvent("upgradeneeded", {
          newVersion: version,
          oldVersion: existingVersion,
        });

        transaction._active = true;

        request.readyState = "done";
        request.result = db;
        request.transaction = transaction;
        request.dispatchEvent(event);

        await transaction._waitDone();

        // We reuse the same transaction (as per spec) here.
        transaction._active = true;

        if (db._closed || db._closePending) {
          request.result = undefined;
          request.error = new AbortError();
          request.readyState = "done";
          const event2 = new FakeEvent("error", {
            bubbles: false,
            cancelable: false,
          });
          event2.eventPath = [];
          request.dispatchEvent(event2);
        } else if (transaction._aborted) {
          try {
            await db._backend.close(db._backendConnection);
          } catch (e) {
            console.error("failed to close database");
          }

          request.result = undefined;
          request.error = new AbortError();
          request.readyState = "done";
          const event2 = new FakeEvent("error", {
            bubbles: false,
            cancelable: false,
          });
          event2.eventPath = [];
          request.dispatchEvent(event2);
        } else {
          if (BridgeIDBFactory.enableTracing) {
            console.log("dispatching 'success' event for opening db");
          }

          const event2 = new FakeEvent("success", {
            bubbles: false,
            cancelable: false,
          });
          event2.eventPath = [];

          request.dispatchEvent(event2);
        }
      }

      this.connections.push(db);
      return db;
    });

    return request;
  }

  // https://w3c.github.io/IndexedDB/#dom-idbfactory-databases
  public databases(): Promise<DatabaseList> {
    return this.backend.getDatabases();
  }

  public toString(): string {
    return "[object IDBFactory]";
  }

  private _anyOpen(dbName: string): boolean {
    let numOpen = 0;
    for (const conn of this.connections) {
      if (conn._name == dbName && !conn._closed && !conn._closePending) {
        numOpen++;
      }
    }
    if (numOpen > 0) {
      if (BridgeIDBFactory.enableTracing) {
        console.log(`there are ${numOpen} connections still open to ${dbName}`);
      }
    }
    return numOpen > 0;
  }
}

// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#idl-def-IDBIndex
export class BridgeIDBIndex implements IDBIndex {
  _objectStore: BridgeIDBObjectStore;
  _indexMeta: IndexMeta;
  _originalName: string | undefined = undefined;
  _deleted: boolean = false;
  _name: string;

  /**
   * Was this index newly created in the current transaction?
   */
  _justCreated: boolean = false;

  get objectStore(): IDBObjectStore {
    return this._objectStore;
  }

  get keyPath(): IDBKeyPath | IDBKeyPath[] {
    return this._indexMeta.keyPath;
  }

  get multiEntry(): boolean {
    return this._indexMeta.multiEntry;
  }

  get unique(): boolean {
    return this._indexMeta.multiEntry;
  }

  get _backend(): Backend {
    return this._objectStore._backend;
  }

  constructor(
    objectStore: BridgeIDBObjectStore,
    name: string,
    indexMeta: IndexMeta,
  ) {
    this._name = name;
    this._objectStore = objectStore;
    this._indexMeta = indexMeta;
  }

  _confirmStartedBackendTransaction(): { btx: DatabaseTransaction } {
    return this._objectStore._confirmStartedBackendTransaction();
  }

  _confirmActiveTransaction(): void {
    this._objectStore._confirmActiveTransaction();
  }

  get name() {
    return this._name;
  }

  // https://w3c.github.io/IndexedDB/#dom-idbindex-name
  set name(name: any) {
    const transaction = this._objectStore._transaction;

    if (!transaction._db._upgradeTransaction) {
      throw new InvalidStateError();
    }

    if (!transaction._active) {
      throw new TransactionInactiveError();
    }

    const { btx } = this._confirmStartedBackendTransaction();

    const oldName = this._name;
    const newName = String(name);

    if (newName === oldName) {
      return;
    }
    if (this._originalName != null) {
      this._originalName = oldName;
    }
    this._backend.renameIndex(btx, this._objectStore.name, oldName, newName);
    this._applyNameChange(oldName, newName);
    if (this._objectStore._objectStoreMeta.indexSet.indexOf(name) >= 0) {
      throw new Error("internal invariant violated");
    }
  }

  _applyNameChange(oldName: string, newName: string) {
    this._objectStore._indexHandlesCache.delete(oldName);
    this._objectStore._indexHandlesCache.set(newName, this);
    const indexSet = this._objectStore._objectStoreMeta.indexSet;
    const indexIdx = indexSet.indexOf(oldName);
    indexSet[indexIdx] = newName;
    indexSet.sort();
    this._name = newName;
  }

  _applyDelete() {
    this._objectStore._indexHandlesCache.delete(this._name);
    const indexSet = this._objectStore._objectStoreMeta.indexSet;
    const indexIdx = indexSet.indexOf(this._name);
    indexSet.splice(indexIdx, 1);
  }

  _abort(): void {
    if (this._originalName != null) {
      this._applyNameChange(this._name, this._originalName);
    }
    if (this._justCreated) {
      this._deleted = true;
    }
  }

  // tslint:disable-next-line max-line-length
  // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBIndex-openCursor-IDBRequest-any-range-IDBCursorDirection-direction
  public openCursor(
    range?: BridgeIDBKeyRange | IDBValidKey | null | undefined,
    direction: IDBCursorDirection = "next",
  ) {
    if (this._deleted) {
      throw new InvalidStateError(
        "tried to call 'openCursor' on a deleted index",
      );
    }

    this._confirmActiveTransaction();

    range = simplifyRange(range);

    const request = new BridgeIDBRequest();
    request._source = this;
    request.transaction = this._objectStore._transaction;

    const cursor = new BridgeIDBCursorWithValue(
      this,
      this._objectStore.name,
      this._name,
      range,
      direction,
      request,
    );

    const operation = async () => {
      return cursor._iterate();
    };

    return this._objectStore._transaction._execRequestAsync({
      operation,
      request,
      source: this,
    });
  }

  // tslint:disable-next-line max-line-length
  // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBIndex-openKeyCursor-IDBRequest-any-range-IDBCursorDirection-direction
  public openKeyCursor(
    range?: BridgeIDBKeyRange | IDBValidKey | null | undefined,
    direction: IDBCursorDirection = "next",
  ) {
    this._confirmActiveTransaction();

    if (range === null) {
      range = undefined;
    }
    if (range !== undefined && !(range instanceof BridgeIDBKeyRange)) {
      range = BridgeIDBKeyRange.only(valueToKey(range));
    }

    const request = new BridgeIDBRequest();
    request._source = this;
    request.transaction = this._objectStore._transaction;

    const cursor = new BridgeIDBCursor(
      this,
      this._objectStore.name,
      this._name,
      range,
      direction,
      request,
      true,
    );

    return this._objectStore._transaction._execRequestAsync({
      operation: cursor._iterate.bind(cursor),
      request,
      source: this,
    });
  }

  private _confirmIndexExists() {
    if (this._deleted) {
      throw new InvalidStateError();
    }
    if (this._objectStore._deleted) {
      throw new InvalidStateError();
    }
  }

  get(key: BridgeIDBKeyRange | IDBValidKey) {
    this._confirmIndexExists();
    this._confirmActiveTransaction();

    if (!(key instanceof BridgeIDBKeyRange)) {
      key = BridgeIDBKeyRange._valueToKeyRange(key);
    }

    const getReq: IndexGetQuery = {
      direction: "next",
      indexName: this._name,
      limit: 1,
      range: key,
      objectStoreName: this._objectStore._name,
      resultLevel: ResultLevel.Full,
    };

    const operation = async () => {
      const { btx } = this._confirmStartedBackendTransaction();
      const result = await this._backend.getIndexRecords(btx, getReq);
      if (result.count == 0) {
        return undefined;
      }
      const values = result.values;
      if (!values) {
        throw Error("invariant violated");
      }
      return values[0];
    };

    return this._objectStore._transaction._execRequestAsync({
      operation,
      source: this,
    });
  }

  // http://w3c.github.io/IndexedDB/#dom-idbindex-getall
  public getAll(
    query?: BridgeIDBKeyRange | IDBValidKey,
    count?: number,
  ): IDBRequest<any[]> {
    this._confirmIndexExists();
    this._confirmActiveTransaction();
    if (this._deleted) {
      throw new InvalidStateError();
    }

    if (!(query instanceof BridgeIDBKeyRange)) {
      query = BridgeIDBKeyRange._valueToKeyRange(query);
    }

    if (count === undefined) {
      count = -1;
    }

    const getReq: IndexGetQuery = {
      direction: "next",
      indexName: this._name,
      limit: count,
      range: query,
      objectStoreName: this._objectStore._name,
      resultLevel: ResultLevel.Full,
    };

    const operation = async () => {
      const { btx } = this._confirmStartedBackendTransaction();
      const result = await this._backend.getIndexRecords(btx, getReq);
      const values = result.values;
      if (!values) {
        throw Error("invariant violated");
      }
      return values;
    };

    return this._objectStore._transaction._execRequestAsync({
      operation,
      source: this,
    });
  }

  // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBIndex-getKey-IDBRequest-any-key
  public getKey(key: BridgeIDBKeyRange | IDBValidKey) {
    this._confirmIndexExists();
    this._confirmActiveTransaction();

    if (!(key instanceof BridgeIDBKeyRange)) {
      key = BridgeIDBKeyRange._valueToKeyRange(key);
    }

    const getReq: IndexGetQuery = {
      direction: "next",
      indexName: this._name,
      limit: 1,
      range: key,
      objectStoreName: this._objectStore._name,
      resultLevel: ResultLevel.OnlyKeys,
    };

    const operation = async () => {
      const { btx } = this._confirmStartedBackendTransaction();
      const result = await this._backend.getIndexRecords(btx, getReq);
      if (result.count == 0) {
        return undefined;
      }
      const primaryKeys = result.primaryKeys;
      if (!primaryKeys) {
        throw Error("invariant violated");
      }
      return primaryKeys[0];
    };

    return this._objectStore._transaction._execRequestAsync({
      operation,
      source: this,
    });
  }

  // http://w3c.github.io/IndexedDB/#dom-idbindex-getallkeys
  public getAllKeys(
    query?: BridgeIDBKeyRange | IDBValidKey,
    count?: number,
  ): IDBRequest<IDBValidKey[]> {
    this._confirmIndexExists();
    this._confirmActiveTransaction();

    if (!(query instanceof BridgeIDBKeyRange)) {
      query = BridgeIDBKeyRange._valueToKeyRange(query);
    }

    if (count === undefined) {
      count = -1;
    }

    const getReq: IndexGetQuery = {
      direction: "next",
      indexName: this._name,
      limit: count,
      range: query,
      objectStoreName: this._objectStore._name,
      resultLevel: ResultLevel.OnlyKeys,
    };

    const operation = async () => {
      const { btx } = this._confirmStartedBackendTransaction();
      const result = await this._backend.getIndexRecords(btx, getReq);
      const primaryKeys = result.primaryKeys;
      if (!primaryKeys) {
        throw Error("invariant violated");
      }
      return primaryKeys;
    };

    return this._objectStore._transaction._execRequestAsync({
      operation,
      source: this,
    });
  }

  // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBIndex-count-IDBRequest-any-key
  public count(key: BridgeIDBKeyRange | IDBValidKey | null | undefined) {
    this._confirmIndexExists();
    this._confirmActiveTransaction();

    if (key === null) {
      key = undefined;
    }
    if (key !== undefined && !(key instanceof BridgeIDBKeyRange)) {
      key = BridgeIDBKeyRange.only(valueToKey(key));
    }

    const getReq: IndexGetQuery = {
      direction: "next",
      indexName: this._name,
      limit: 1,
      range: key,
      objectStoreName: this._objectStore._name,
      resultLevel: ResultLevel.OnlyCount,
    };

    const operation = async () => {
      const { btx } = this._confirmStartedBackendTransaction();
      const result = await this._backend.getIndexRecords(btx, getReq);
      return result.count;
    };

    return this._objectStore._transaction._execRequestAsync({
      operation,
      source: this,
    });
  }

  public toString() {
    return "[object IDBIndex]";
  }
}

// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#range-concept
export class BridgeIDBKeyRange {
  public static only(value: IDBValidKey) {
    if (arguments.length === 0) {
      throw new TypeError();
    }
    value = valueToKey(value);
    return new BridgeIDBKeyRange(value, value, false, false);
  }

  static lowerBound(lower: IDBValidKey, open: boolean = false) {
    if (arguments.length === 0) {
      throw new TypeError();
    }
    lower = valueToKey(lower);
    return new BridgeIDBKeyRange(lower, undefined, open, true);
  }

  static upperBound(upper: IDBValidKey, open: boolean = false) {
    if (arguments.length === 0) {
      throw new TypeError();
    }
    upper = valueToKey(upper);
    return new BridgeIDBKeyRange(undefined, upper, true, open);
  }

  static bound(
    lower: IDBValidKey,
    upper: IDBValidKey,
    lowerOpen: boolean = false,
    upperOpen: boolean = false,
  ) {
    if (arguments.length < 2) {
      throw new TypeError();
    }

    const cmpResult = compareKeys(lower, upper);
    if (cmpResult === 1 || (cmpResult === 0 && (lowerOpen || upperOpen))) {
      throw new DataError();
    }

    lower = valueToKey(lower);
    upper = valueToKey(upper);
    return new BridgeIDBKeyRange(lower, upper, lowerOpen, upperOpen);
  }

  readonly lower: IDBValidKey | undefined;
  readonly upper: IDBValidKey | undefined;
  readonly lowerOpen: boolean;
  readonly upperOpen: boolean;

  constructor(
    lower: IDBValidKey | undefined,
    upper: IDBValidKey | undefined,
    lowerOpen: boolean,
    upperOpen: boolean,
  ) {
    this.lower = lower;
    this.upper = upper;
    this.lowerOpen = lowerOpen;
    this.upperOpen = upperOpen;
  }

  // https://w3c.github.io/IndexedDB/#dom-idbkeyrange-includes
  includes(key: IDBValidKey) {
    if (arguments.length === 0) {
      throw new TypeError();
    }
    key = valueToKey(key);

    if (this.lower !== undefined) {
      const cmpResult = compareKeys(this.lower, key);

      if (cmpResult === 1 || (cmpResult === 0 && this.lowerOpen)) {
        return false;
      }
    }
    if (this.upper !== undefined) {
      const cmpResult = compareKeys(this.upper, key);

      if (cmpResult === -1 || (cmpResult === 0 && this.upperOpen)) {
        return false;
      }
    }
    return true;
  }

  toString() {
    return "[object IDBKeyRange]";
  }

  static _valueToKeyRange(value: any, nullDisallowedFlag: boolean = false) {
    if (value instanceof BridgeIDBKeyRange) {
      return value;
    }

    if (value === null || value === undefined) {
      if (nullDisallowedFlag) {
        throw new DataError();
      }
      return new BridgeIDBKeyRange(undefined, undefined, false, false);
    }

    const key = valueToKey(value);

    return BridgeIDBKeyRange.only(key);
  }
}

// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#object-store
export class BridgeIDBObjectStore implements IDBObjectStore {
  _indexHandlesCache: Map<string, BridgeIDBIndex> = new Map();
  _transaction: BridgeIDBTransaction;

  /**
   * Name that can be set to identify the object store in logs.
   */
  _debugName: string | undefined = undefined;

  // Was the object store (not the handle, but the underlying store)
  // created in this upgrade transaction?
  _justCreated: boolean = false;

  _originalName: string | undefined = undefined;
  _objectStoreMeta: ObjectStoreMeta;

  get transaction(): IDBTransaction {
    return this._transaction;
  }

  get autoIncrement(): boolean {
    return this._objectStoreMeta.autoIncrement;
  }

  get indexNames(): DOMStringList {
    return fakeDOMStringList([...this._objectStoreMeta.indexSet]);
  }

  get keyPath(): IDBKeyPath | IDBKeyPath[] {
    // Bug in th official type declarations.  The spec
    // allows returning null here.
    return this._objectStoreMeta.keyPath!;
  }

  _name: string;

  _deleted: boolean = false;

  constructor(
    transaction: BridgeIDBTransaction,
    name: string,
    objectStoreMeta: ObjectStoreMeta,
  ) {
    this._name = name;
    this._transaction = transaction;
    this._objectStoreMeta = objectStoreMeta;
  }

  get name() {
    return this._name;
  }

  get _backend(): Backend {
    return this._transaction._db._backend;
  }

  get _backendConnection(): DatabaseConnection {
    return this._transaction._db._backendConnection;
  }

  _confirmStartedBackendTransaction(): { btx: DatabaseTransaction } {
    const btx = this._transaction._backendTransaction;
    if (!btx) {
      throw new InvalidStateError();
    }
    return { btx };
  }

  /**
   * Confirm that requests can currently placed against the
   * transaction of this object.
   *
   * Note that this is independent from the state of the backend
   * connection.
   */
  _confirmActiveTransaction(): void {
    if (!this._transaction._active) {
      throw new TransactionInactiveError("transaction is not active");
    }
    if (this._transaction._aborted) {
      throw new TransactionInactiveError("transaction has been aborted");
    }
  }

  // http://w3c.github.io/IndexedDB/#dom-idbobjectstore-name
  set name(newName: any) {
    const transaction = this._transaction;

    if (!transaction._db._upgradeTransaction) {
      throw new InvalidStateError();
    }

    let { btx } = this._confirmStartedBackendTransaction();

    newName = String(newName);
    const oldName = this._name;
    if (newName === oldName) {
      return;
    }
    if (this._originalName == null) {
      this._originalName = this._name;
    }
    this._backend.renameObjectStore(btx, oldName, newName);
    this._applyNameChange(oldName, newName);
  }

  _applyNameChange(oldName: string, newName: string) {
    this._transaction._scope.delete(oldName);
    this._transaction._scope.add(newName);
    // We don't modify scope, as the scope of the transaction
    // doesn't matter if we're in an upgrade transaction.
    this._transaction._objectStoresCache.delete(oldName);
    this._transaction._objectStoresCache.set(newName, this);
    this._transaction._cachedObjectStoreNames = undefined;
    const objectStoreSet = this._transaction._db._objectStoreSet;
    const oldIdx = objectStoreSet.indexOf(oldName);
    objectStoreSet[oldIdx] = newName;
    objectStoreSet.sort();
    this._name = newName;
  }

  _applyDelete() {
    this._deleted = true;
    this._transaction._objectStoresCache.delete(this._name);
    this._transaction._cachedObjectStoreNames = undefined;
    const objectStoreSet = this._transaction._db._objectStoreSet;
    const oldIdx = objectStoreSet.indexOf(this._name);
    objectStoreSet.splice(oldIdx, 1);
  }

  /**
   * Roll back changes to the handle after an abort.
   */
  _abort() {
    if (this._originalName != null) {
      this._applyNameChange(this._name, this._originalName);
    }
    if (this._justCreated) {
      this._applyDelete();
    }
  }

  /**
   * "To add or put with handle, value, key, and no-overwrite flag, run these steps:"
   */
  public _store(value: any, key: IDBValidKey | undefined, overwrite: boolean) {
    if (BridgeIDBFactory.enableTracing) {
      console.log(
        `TRACE: IDBObjectStore._store, db=${this._transaction._db._getReadableName()}`,
      );
    }

    if (this._deleted) {
      throw new InvalidStateError(
        "tried to call 'put' on a deleted object store",
      );
    }

    if (!this._transaction._active) {
      throw new TransactionInactiveError();
    }

    if (this._transaction.mode === "readonly") {
      throw new ReadOnlyError();
    }

    const { keyPath, autoIncrement } = this._objectStoreMeta;

    if (key !== null && key !== undefined) {
      valueToKey(key);
    }

    // We only call this to synchronously verify the request.
    // FIXME: The backend should do that!
    makeStoreKeyValue({
      value: value,
      key: key,
      currentKeyGenerator: 1,
      autoIncrement,
      keyPath,
    });

    const operation = async () => {
      const { btx } = this._confirmStartedBackendTransaction();
      const result = await this._backend.storeRecord(btx, {
        objectStoreName: this._name,
        key: key,
        value,
        storeLevel: overwrite
          ? StoreLevel.AllowOverwrite
          : StoreLevel.NoOverwrite,
      });
      return result.key;
    };

    return this._transaction._execRequestAsync({ operation, source: this });
  }

  public put(value: any, key?: IDBValidKey) {
    if (arguments.length === 0) {
      throw new TypeError();
    }
    return this._store(value, key, true);
  }

  public add(value: any, key?: IDBValidKey) {
    if (arguments.length === 0) {
      throw new TypeError();
    }
    return this._store(value, key, false);
  }

  public delete(key: IDBValidKey | BridgeIDBKeyRange) {
    if (arguments.length === 0) {
      throw new TypeError();
    }
    if (this._deleted) {
      throw new InvalidStateError(
        "tried to call 'delete' on a deleted object store",
      );
    }
    if (this._transaction.mode === "readonly") {
      throw new ReadOnlyError();
    }

    let keyRange: BridgeIDBKeyRange;

    if (key instanceof BridgeIDBKeyRange) {
      keyRange = key;
    } else {
      keyRange = BridgeIDBKeyRange.only(valueToKey(key));
    }

    const operation = async () => {
      const { btx } = this._confirmStartedBackendTransaction();
      return this._backend.deleteRecord(btx, this._name, keyRange);
    };

    return this._transaction._execRequestAsync({
      operation,
      source: this,
    });
  }

  public get(key?: BridgeIDBKeyRange | IDBValidKey) {
    if (BridgeIDBFactory.enableTracing) {
      console.log(`getting from object store ${this._name} key ${key}`);
    }

    if (arguments.length === 0) {
      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",
      );
    }

    let keyRange: BridgeIDBKeyRange;

    if (key instanceof BridgeIDBKeyRange) {
      keyRange = key;
    } else {
      try {
        keyRange = BridgeIDBKeyRange.only(valueToKey(key));
      } catch (e) {
        throw new DataError(
          `invalid key (type ${typeof key}) for object store '${this._name}'`,
        );
      }
    }

    const recordRequest: ObjectStoreGetQuery = {
      objectStoreName: this._name,
      lastObjectStorePosition: undefined,
      direction: "next",
      limit: 1,
      resultLevel: ResultLevel.Full,
      range: keyRange,
    };

    const operation = async () => {
      if (BridgeIDBFactory.enableTracing) {
        console.log("running get operation:", recordRequest);
      }
      const { btx } = this._confirmStartedBackendTransaction();
      const result = await this._backend.getObjectStoreRecords(
        btx,
        recordRequest,
      );

      if (BridgeIDBFactory.enableTracing) {
        console.log("get operation result count:", result.count);
      }

      if (result.count === 0) {
        return undefined;
      }
      const values = result.values;
      if (!values) {
        throw Error("invariant violated");
      }
      if (values.length !== 1) {
        throw Error("invariant violated");
      }
      return values[0];
    };

    return this._transaction._execRequestAsync({
      operation,
      source: this,
    });
  }

  // http://w3c.github.io/IndexedDB/#dom-idbobjectstore-getall
  public getAll(
    query?: BridgeIDBKeyRange | IDBValidKey,
    count?: number,
  ): IDBRequest<any[]> {
    if (BridgeIDBFactory.enableTracing) {
      console.log(`getting from object store ${this._name} key ${query}`);
    }

    if (!this._transaction._active) {
      throw new TransactionInactiveError();
    }

    if (this._deleted) {
      throw new InvalidStateError(
        "tried to call 'delete' on a deleted object store",
      );
    }

    if (count === undefined) {
      count = -1;
    }

    let keyRange: BridgeIDBKeyRange | null = simplifyRange(query);

    const recordRequest: ObjectStoreGetQuery = {
      objectStoreName: this._name,
      lastObjectStorePosition: undefined,
      direction: "next",
      limit: count,
      resultLevel: ResultLevel.Full,
      range: keyRange,
    };

    const operation = async () => {
      if (BridgeIDBFactory.enableTracing) {
        console.log("running getAll operation:", recordRequest);
      }
      const { btx } = this._confirmStartedBackendTransaction();
      const result = await this._backend.getObjectStoreRecords(
        btx,
        recordRequest,
      );

      if (BridgeIDBFactory.enableTracing) {
        console.log("get operation result count:", result.count);
      }
      const values = result.values;
      if (!values) {
        throw Error("invariant violated");
      }
      return values;
    };

    return this._transaction._execRequestAsync({
      operation,
      source: this,
    });
  }

  // http://w3c.github.io/IndexedDB/#dom-idbobjectstore-getkey
  public getKey(
    query?: BridgeIDBKeyRange | IDBValidKey,
  ): IDBRequest<IDBValidKey | undefined> {
    if (arguments.length === 0) {
      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",
      );
    }

    let keyRange: BridgeIDBKeyRange | null = simplifyRange(query);

    const recordRequest: ObjectStoreGetQuery = {
      objectStoreName: this._name,
      lastObjectStorePosition: undefined,
      direction: "next",
      limit: 1,
      resultLevel: ResultLevel.OnlyKeys,
      range: keyRange,
    };

    const operation = async () => {
      if (BridgeIDBFactory.enableTracing) {
        console.log("running getKey operation:", recordRequest);
      }
      const { btx } = this._confirmStartedBackendTransaction();
      const result = await this._backend.getObjectStoreRecords(
        btx,
        recordRequest,
      );

      if (BridgeIDBFactory.enableTracing) {
        console.log("getKey operation result count:", result.count);
      }

      if (result.count === 0) {
        return undefined;
      }
      const primaryKeys = result.primaryKeys;
      if (!primaryKeys) {
        throw Error("invariant violated");
      }
      if (primaryKeys.length !== 1) {
        throw Error("invariant violated");
      }
      return primaryKeys[0];
    };

    return this._transaction._execRequestAsync({
      operation,
      source: this,
    });
  }

  // http://w3c.github.io/IndexedDB/#dom-idbobjectstore-getallkeys
  public getAllKeys(
    query?: BridgeIDBKeyRange | IDBValidKey,
    count?: number,
  ): IDBRequest<any[]> {
    if (arguments.length === 0) {
      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",
      );
    }

    if (count === undefined) {
      count = -1;
    }

    let keyRange: BridgeIDBKeyRange;

    if (query instanceof BridgeIDBKeyRange) {
      keyRange = query;
    } else {
      try {
        keyRange = BridgeIDBKeyRange.only(valueToKey(query));
      } catch (e) {
        throw new DataError(
          `invalid key (type ${typeof query}) for object store '${this._name}'`,
        );
      }
    }

    const recordRequest: ObjectStoreGetQuery = {
      objectStoreName: this._name,
      lastObjectStorePosition: undefined,
      direction: "next",
      limit: count,
      resultLevel: ResultLevel.OnlyKeys,
      range: keyRange,
    };

    const operation = async () => {
      const { btx } = this._confirmStartedBackendTransaction();
      const result = await this._backend.getObjectStoreRecords(
        btx,
        recordRequest,
      );

      const primaryKeys = result.primaryKeys;
      if (!primaryKeys) {
        throw Error("invariant violated");
      }
      return primaryKeys;
    };

    return this._transaction._execRequestAsync({
      operation,
      source: this,
    });
  }

  public clear(): IDBRequest {
    if (!this._transaction._active) {
      throw new TransactionInactiveError();
    }

    if (this._deleted) {
      throw new InvalidStateError(
        "tried to call 'delete' on a deleted object store",
      );
    }

    const operation = async () => {
      const { btx } = this._confirmStartedBackendTransaction();
      await this._backend.clearObjectStore(btx, this._name);
    };

    return this._transaction._execRequestAsync({
      operation,
      source: this,
    });
  }

  public openCursor(
    range?: IDBKeyRange | IDBValidKey,
    direction: IDBCursorDirection = "next",
  ) {
    if (this._deleted) {
      throw new InvalidStateError(
        "tried to call 'openCursor' on a deleted object store",
      );
    }
    if (range === null) {
      range = undefined;
    }
    if (range !== undefined && !(range instanceof BridgeIDBKeyRange)) {
      range = BridgeIDBKeyRange.only(valueToKey(range));
    }

    const request = new BridgeIDBRequest();
    request._source = this;
    request.transaction = this._transaction;

    const cursor = new BridgeIDBCursorWithValue(
      this,
      this._name,
      undefined,
      range,
      direction,
      request,
    );

    return this._transaction._execRequestAsync({
      operation: () => cursor._iterate(),
      request,
      source: this,
    });
  }

  public openKeyCursor(
    range?: BridgeIDBKeyRange | IDBValidKey,
    direction?: IDBCursorDirection,
  ) {
    if (this._deleted) {
      throw new InvalidStateError(
        "tried to call 'openKeyCursor' on a deleted object store",
      );
    }
    if (range === null) {
      range = undefined;
    }
    if (range !== undefined && !(range instanceof BridgeIDBKeyRange)) {
      range = BridgeIDBKeyRange.only(valueToKey(range));
    }

    if (!direction) {
      direction = "next";
    }

    const request = new BridgeIDBRequest();
    request._source = this;
    request.transaction = this._transaction;

    const cursor = new BridgeIDBCursor(
      this,
      this._name,
      undefined,
      range,
      direction,
      request,
      true,
    );

    return this._transaction._execRequestAsync({
      operation: cursor._iterate.bind(cursor),
      request,
      source: this,
    });
  }

  // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBObjectStore-createIndex-IDBIndex-DOMString-name-DOMString-sequence-DOMString--keyPath-IDBIndexParameters-optionalParameters
  public createIndex(
    indexName: string,
    keyPath: IDBKeyPath,
    optionalParameters: { multiEntry?: boolean; unique?: boolean } = {},
  ) {
    if (arguments.length < 2) {
      throw new TypeError();
    }

    if (!this._transaction._db._upgradeTransaction) {
      throw new InvalidStateError();
    }

    const { btx } = this._confirmStartedBackendTransaction();

    const multiEntry =
      optionalParameters.multiEntry !== undefined
        ? optionalParameters.multiEntry
        : false;
    const unique =
      optionalParameters.unique !== undefined
        ? optionalParameters.unique
        : false;

    if (this._transaction.mode !== "versionchange") {
      throw new InvalidStateError();
    }

    if (this._objectStoreMeta.indexSet.indexOf(indexName) >= 0) {
      throw new ConstraintError();
    }

    validateKeyPath(keyPath);

    if (Array.isArray(keyPath) && multiEntry) {
      throw new InvalidAccessError();
    }

    this._backend.createIndex(
      btx,
      indexName,
      this._name,
      normalizeKeyPath(keyPath),
      multiEntry,
      unique,
    );

    this._objectStoreMeta.indexSet.push(indexName);
    this._objectStoreMeta.indexSet.sort();

    const idx = this.index(indexName);
    idx._justCreated = true;
    return idx;
  }

  // https://w3c.github.io/IndexedDB/#dom-idbobjectstore-index
  public index(name: string) {
    if (arguments.length === 0) {
      throw new TypeError();
    }

    if (this._transaction._finished) {
      throw new InvalidStateError();
    }
    const index = this._indexHandlesCache.get(name);
    if (index !== undefined) {
      return index;
    }
    const indexMeta = this._backend.getIndexMeta(
      this._backendConnection,
      this._name,
      name,
    );
    if (!indexMeta) {
      throw new NotFoundError(
        `IndexedDB index '${name}' not found (no metadata available)`,
      );
    }
    const newIndex = new BridgeIDBIndex(this, name, indexMeta);
    this._indexHandlesCache.set(name, newIndex);
    this._transaction._usedIndexes.push(newIndex);
    return newIndex;
  }

  public deleteIndex(indexName: string) {
    if (arguments.length === 0) {
      throw new TypeError();
    }

    if (this._transaction.mode !== "versionchange") {
      throw new InvalidStateError();
    }

    if (!this._transaction._db._upgradeTransaction) {
      throw new InvalidStateError();
    }

    const { btx } = this._confirmStartedBackendTransaction();

    const index = this._indexHandlesCache.get(indexName);
    if (index !== undefined) {
      index._deleted = true;
      this._indexHandlesCache.delete(indexName);
    }

    const indexIdx = this._objectStoreMeta.indexSet.indexOf(indexName);
    this._objectStoreMeta.indexSet.splice(indexIdx, 1);

    this._backend.deleteIndex(btx, this._name, indexName);
  }

  // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBObjectStore-count-IDBRequest-any-key
  public count(key?: IDBValidKey | BridgeIDBKeyRange) {
    if (key === null) {
      key = undefined;
    }
    if (key !== undefined && !(key instanceof BridgeIDBKeyRange)) {
      key = BridgeIDBKeyRange.only(valueToKey(key));
    }

    const recordGetRequest: ObjectStoreGetQuery = {
      direction: "next",
      limit: 0,
      objectStoreName: this._name,
      lastObjectStorePosition: undefined,
      range: key,
      resultLevel: ResultLevel.OnlyCount,
    };

    const operation = async () => {
      const { btx } = this._confirmStartedBackendTransaction();
      const result = await this._backend.getObjectStoreRecords(
        btx,
        recordGetRequest,
      );
      return result.count;
    };

    return this._transaction._execRequestAsync({ operation, source: this });
  }

  public toString() {
    return "[object IDBObjectStore]";
  }
}

export class BridgeIDBRequest extends FakeEventTarget implements IDBRequest {
  _result: any = null;
  _error: Error | null | undefined = null;
  get source(): IDBObjectStore | IDBIndex | IDBCursor {
    if (!this._source) {
      throw Error("internal invariant failed: source is null");
    }
    return this._source;
  }
  _source: BridgeIDBCursor | BridgeIDBIndex | BridgeIDBObjectStore | null =
    null;
  transaction: BridgeIDBTransaction | null = null;
  readyState: "done" | "pending" = "pending";
  onsuccess: EventListener | null = null;
  onerror: EventListener | null = null;

  _debugName: string | undefined;

  get error() {
    if (this.readyState === "pending") {
      throw new InvalidStateError();
    }
    return this._error;
  }

  set error(value: any) {
    this._error = value;
  }

  get result() {
    if (this.readyState === "pending") {
      throw new InvalidStateError();
    }
    return this._result;
  }

  set result(value: any) {
    this._result = value;
  }

  toString() {
    return "[object IDBRequest]";
  }

  _finishWithError(err: Error) {
    this.result = undefined;
    this.readyState = "done";

    this.error = new Error(err.message);
    this.error.name = err.name;

    const event = new FakeEvent("error", {
      bubbles: true,
      cancelable: true,
    });
    event.eventPath = [];

    this.dispatchEvent(event);
  }
}

export class BridgeIDBOpenDBRequest
  extends BridgeIDBRequest
  implements IDBOpenDBRequest
{
  public onupgradeneeded: 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() {
    super();
    // https://www.w3.org/TR/IndexedDB/#open-requests
    this._source = null;
  }

  public toString() {
    return "[object IDBOpenDBRequest]";
  }
}

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
export class BridgeIDBTransaction
  extends FakeEventTarget
  implements IDBTransaction
{
  _committed: boolean = false;
  /**
   * A transaction is active as long as new operations can be
   * placed against it.
   */
  _active: boolean = false;
  _started: boolean = false;
  _aborted: boolean = false;
  _objectStoresCache: Map<string, BridgeIDBObjectStore> = new Map();

  /**
   * Object stores used during the transaction.
   */
  _usedObjectStores: BridgeIDBObjectStore[] = [];

  /**
   * Object stores used during the transaction.
   */
  _usedIndexes: BridgeIDBIndex[] = [];

  /**
   * 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
   *
   * When a transaction is committed or aborted, it is said to be finished.
   */
  get _finished(): boolean {
    return this._committed || this._aborted;
  }

  _counter = 0;

  _openRequest: BridgeIDBOpenDBRequest | null = null;

  _backendTransaction?: DatabaseTransaction;

  _cachedObjectStoreNames: DOMStringList | undefined;

  get objectStoreNames(): DOMStringList {
    if (!this._cachedObjectStoreNames) {
      this._cachedObjectStoreNames = fakeDOMStringList(
        Array.from(this._scope).sort(),
      );
    }
    return this._cachedObjectStoreNames;
  }
  mode: IDBTransactionMode;
  _db: BridgeIDBDatabase;

  get db(): IDBDatabase {
    return this._db;
  }

  _error: Error | null = null;

  get error(): DOMException {
    return this._error as DOMException;
  }

  public onabort: EventListener | null = null;
  public oncomplete: EventListener | null = null;
  public onerror: EventListener | null = null;

  private _waitPromise: Promise<void>;
  private _resolveWait: () => void;

  public _scope: Set<string>;
  private _requests: Array<{
    operation: () => Promise<void>;
    request: BridgeIDBRequest;
  }> = [];

  get _backend(): Backend {
    return this._db._backend;
  }

  constructor(
    storeNames: string[],
    mode: IDBTransactionMode,
    db: BridgeIDBDatabase,
    backendTransaction?: DatabaseTransaction,
    openRequest?: BridgeIDBOpenDBRequest,
  ) {
    super();

    const myOpenPromise = openPromise<void>();
    this._waitPromise = myOpenPromise.promise;
    this._resolveWait = myOpenPromise.resolve;

    this._scope = new Set(storeNames);
    this._backendTransaction = backendTransaction;
    this.mode = mode;
    this._db = db;

    this._db._transactions.push(this);

    this._openRequest = openRequest ?? null;
  }

  // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-steps-for-aborting-a-transaction
  _abort(errName: string | null): void {
    if (BridgeIDBFactory.enableTracing) {
      console.log("TRACE: aborting transaction");
    }

    if (this._aborted) {
      return;
    }

    this._aborted = true;
    this._active = false;

    if (errName !== null) {
      const e = new Error();
      e.name = errName;
      this._error = e;
    }

    if (BridgeIDBFactory.enableTracing) {
      console.log(`TRACE: aborting ${this._requests.length} requests`);
    }

    // Should this directly remove from _requests?
    for (const { request } of this._requests) {
      if (request.readyState !== "done") {
        // This will cancel execution of this request's operation
        request.readyState = "done";
        if (BridgeIDBFactory.enableTracing) {
          console.log("dispatching error event");
        }
        request.result = undefined;
        request.error = new AbortError();

        const event = new FakeEvent("error", {
          bubbles: true,
          cancelable: true,
        });
        event.eventPath = [this._db, this];
        request.dispatchEvent(event);
      }
    }

    // All steps before happened synchronously.  Now
    // we asynchronously roll back the backend transaction,
    // if necessary/possible.

    {
      const maybeBtx = this._backendTransaction;
      if (maybeBtx) {
        this._backendTransaction = undefined;
        this._backend.rollback(maybeBtx).catch((e) => {
          console.warn(`error during rollback: ${e}`);
        });
      }
    }

    // "Any object stores and indexes which were created during the
    // transaction are now considered deleted for the purposes of other
    // algorithms."
    if (this._db._upgradeTransaction) {
      for (const os of this._usedObjectStores) {
        os._abort();
      }
      for (const ind of this._usedIndexes) {
        ind._abort();
      }
    }

    this._db._version = this._db._initialVersion;

    // ("abort a transaction", step 5.1)
    if (this._openRequest) {
      this._db._upgradeTransaction = null;
    }

    queueTask(() => {
      const event = new FakeEvent("abort", {
        bubbles: true,
        cancelable: false,
      });
      event.eventPath = [this._db];
      this.dispatchEvent(event);
    });

    if (this._openRequest) {
      this._openRequest.transaction = null;
      this._openRequest.result = undefined;
      this._openRequest.readyState = "pending";
    }
  }

  public abort() {
    if (this._finished) {
      throw new InvalidStateError();
    }
    this._abort(null);
  }

  // http://w3c.github.io/IndexedDB/#dom-idbtransaction-objectstore
  public objectStore(name: string): BridgeIDBObjectStore {
    if (!this._active) {
      throw new TransactionInactiveError();
    }

    if (!this._scope.has(name)) {
      throw new NotFoundError(`object store ${name} not in transaction scope`);
    }

    const objectStore = this._objectStoresCache.get(name);
    if (objectStore !== undefined) {
      return objectStore;
    }

    const objectStoreMeta = this._backend.getObjectStoreMeta(
      this._db._backendConnection,
      name,
    );

    if (!objectStoreMeta) {
      throw new NotFoundError(
        `object store metadata for ${name} found in current transaction (bug!)`,
      );
    }

    const newObjectStore = new BridgeIDBObjectStore(
      this,
      name,
      objectStoreMeta,
    );
    this._objectStoresCache.set(name, newObjectStore);
    this._usedObjectStores.push(newObjectStore);
    return newObjectStore;
  }

  // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-steps-for-asynchronously-executing-a-request
  public _execRequestAsync(obj: RequestObj) {
    const source = obj.source;
    const operation = obj.operation;
    let request = obj.hasOwnProperty("request") ? obj.request : null;

    if (!this._active) {
      throw new TransactionInactiveError();
    }

    // Request should only be passed for cursors
    if (!request) {
      if (!source) {
        // Special requests like indexes that just need to run some code
        request = new BridgeIDBRequest();
      } else {
        request = new BridgeIDBRequest();
        request._source = source;
        request.transaction = (source as any).transaction;
      }
    }

    this._requests.push({
      operation,
      request,
    });

    return request;
  }

  /**
   * Actually execute the scheduled work for this transaction.
   */
  public async _start() {
    if (BridgeIDBFactory.enableTracing) {
      console.log(
        `TRACE: IDBTransaction._start, ${this._requests.length} queued`,
      );
    }

    this._started = true;

    // 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) {
      const r = this._requests.shift();

      // This should only be false if transaction was aborted
      if (r && r.request.readyState !== "done") {
        request = r.request;
        operation = r.operation;
        break;
      }
    }

    if (request && operation) {
      if (!this._backendTransaction && !this._aborted) {
        if (BridgeIDBFactory.enableTracing) {
          console.log("beginning backend transaction to process operation");
        }
        const newBackendTx = await this._backend.beginTransaction(
          this._db._backendConnection,
          Array.from(this._scope),
          this.mode,
        );
        if (BridgeIDBFactory.enableTracing) {
          console.log(
            `started backend transaction (${newBackendTx.transactionCookie})`,
          );
        }
        if (this._aborted) {
          // Probably there is a more elegant way to do this by aborting the
          // beginTransaction call when the transaction was aborted.
          // That would require changing the DB backend API.

          this._backend.rollback(newBackendTx).catch((e) => {
            console.error("error during rollback");
            console.error(e);
          });
        } else {
          this._backendTransaction = newBackendTx;
        }
      }

      if (!request._source) {
        // Special requests like indexes that just need to run some code,
        // with error handling already built into operation
        await operation();
      } else {
        this._counter++;
        if (this._counter > 100) {
          this._counter = 0;
          // Give a chance for macro tasks to do something
          // If we don't do this at all, we break WPT tests.
          // If we always wait, performance is bad.
          await waitMacroQueue();
        }

        let event;
        try {
          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: request (${
                request._debugName ?? "??"
              }) in transaction finished with success`,
            );
          request.readyState = "done";
          request.result = result;
          request.error = undefined;

          // https://www.w3.org/TR/IndexedDB-2/#fire-error-event
          this._active = true;
          event = new FakeEvent("success", {
            bubbles: false,
            cancelable: false,
          });

          queueTask(() => {
            this._active = false;
          });

          try {
            event.eventPath = [this._db, this];
            request.dispatchEvent(event);
          } catch (err) {
            if (BridgeIDBFactory.enableTracing) {
              console.log(
                "TRACING: caught error in transaction success event handler",
              );
            }
            this._abort("AbortError");
            this._active = false;
            throw err;
          }
        } catch (err: any) {
          if (BridgeIDBFactory.enableTracing) {
            console.log("TRACING: error during operation: ", err);
          }
          request.readyState = "done";
          request.result = undefined;
          request.error = err;

          // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-fire-an-error-event
          this._active = true;
          queueTask(() => {
            this._active = false;
          });
          event = new FakeEvent("error", {
            bubbles: true,
            cancelable: true,
          });

          try {
            event.eventPath = [this._db, this];
            request.dispatchEvent(event);
          } catch (err) {
            this._abort("AbortError");
            throw err;
          }
          if (!event.canceled) {
            this._abort(err.name);
          }
        }
      }

      // On to the next one
      if (this._requests.length > 0) {
        this._start();
      } else {
        // Give it another chance for new handlers to be set before finishing
        queueTask(() => this._start());
      }
      return;
    }

    if (!this._finished && !this._committed) {
      if (BridgeIDBFactory.enableTracing) {
        console.log(
          `setting transaction to inactive, db=${this._db._getReadableName()}`,
        );
      }

      this._active = false;

      // We only have a backend transaction if any requests were placed
      // against the transactions.
      if (this._backendTransaction) {
        const backendTx = this._backendTransaction;
        this._backendTransaction = undefined;
        await this._backend.commit(backendTx);
      }

      // We must exit the upgrade transaction here, so that the "complete"
      // event handler can already do other transactions.
      this._db._upgradeTransaction = null;

      this._committed = true;
      if (!this._error) {
        if (BridgeIDBFactory.enableTracing) {
          console.log(
            `dispatching 'complete' event on transaction (${
              this._debugName ?? "??"
            })`,
          );
        }
        const event = new FakeEvent("complete");
        event.eventPath = [this._db, this];
        this.dispatchEvent(event);
      }

      const idx = this._db._transactions.indexOf(this);
      if (idx < 0) {
        throw Error("invariant failed");
      }
      this._db._transactions.splice(idx, 1);

      this._resolveWait();
    }
    if (this._aborted) {
      this._resolveWait();
    }
  }

  public commit() {
    // The current spec doesn't even have an explicit commit method.
    // We still support it, effectively as a "no-operation" that
    // prevents new operations from being scheduled.
    if (!this._active) {
      throw new InvalidStateError();
    }
    this._active = false;
  }

  public toString() {
    return "[object IDBRequest]";
  }

  _waitDone(): Promise<void> {
    return this._waitPromise;
  }
}

export class BridgeIDBVersionChangeEvent extends FakeEvent {
  public newVersion: number | null;
  public oldVersion: number;

  constructor(
    type: "blocked" | "success" | "upgradeneeded" | "versionchange",
    parameters: { newVersion?: number | null; oldVersion?: number } = {},
  ) {
    super(type);

    this.newVersion =
      parameters.newVersion !== undefined ? parameters.newVersion : null;
    this.oldVersion =
      parameters.oldVersion !== undefined ? parameters.oldVersion : 0;
  }

  public toString() {
    return "[object IDBVersionChangeEvent]";
  }
}
