objectStores: join()

in src/IndexedDbProvider.ts [384:740]


                  objectStores: join(closedDBConnection.objectStoreNames, ","),
                  type: "unexpectedClosure",
                };
              }
              this._handleOnClose(payload);
            }
          };
        });
      },
      (err) => {
        if (
          err &&
          err.type === "error" &&
          err.target &&
          err.target.error &&
          err.target.error.name === "VersionError"
        ) {
          if (!wipeIfExists) {
            console.log(
              "Database version too new, Wiping: " +
                (err.target.error.message || err.target.error.name)
            );

            return this.open(dbName, schema, true, verbose);
          }
        }
        return Promise.reject<void>(err);
      }
    );
  }

  close(): Promise<void> {
    if (!this._db) {
      return Promise.reject("Database already closed");
    }

    this._db.close();

    if (this._handleOnClose) {
      let payload: IDBCloseConnectionPayload = {
        name: this._db.name,
        objectStores: join(this._db.objectStoreNames, ","),
        type: "expectedClosure",
      };
      this._handleOnClose(payload);
    }

    this._db = undefined;
    return Promise.resolve<void>(void 0);
  }

  protected _deleteDatabaseInternal(): Promise<void> {
    const trans = attempt(() => {
      return this._dbFactory.deleteDatabase(this._dbName!!!);
    });

    if (isError(trans)) {
      return Promise.reject(trans);
    }

    return new Promise((resolve, reject) => {
      trans.onsuccess = () => {
        resolve(void 0);
      };
      trans.onerror = (ev) => {
        reject(ev);
      };
    });
  }

  openTransaction(
    storeNames: string[],
    writeNeeded: boolean
  ): Promise<DbTransaction> {
    if (!this._db) {
      return Promise.reject("Can't openTransaction, database is closed");
    }

    let intStoreNames = storeNames;

    if (this._fakeComplicatedKeys) {
      // Clone the list becuase we're going to add fake store names to it
      intStoreNames = clone(storeNames);

      // Pull the alternate multientry stores into the transaction as well
      let missingStores: string[] = [];
      each(storeNames, (storeName) => {
        let storeSchema = find(
          this._schema!!!.stores,
          (s) => s.name === storeName
        );
        if (!storeSchema) {
          missingStores.push(storeName);
          return;
        }
        if (storeSchema.indexes) {
          each(storeSchema.indexes, (indexSchema) => {
            if (indexSchema.multiEntry || indexSchema.fullText) {
              intStoreNames.push(storeSchema!!!.name + "_" + indexSchema.name);
            }
          });
        }
      });
      if (missingStores.length > 0) {
        return Promise.reject(
          "Can't find store(s): " + missingStores.join(",")
        );
      }
    }

    return this._lockHelper!!!.openTransaction(storeNames, writeNeeded).then(
      (transToken) => {
        const trans = attempt(() => {
          return this._db!!!.transaction(
            intStoreNames,
            writeNeeded ? "readwrite" : "readonly"
          );
        });
        if (isError(trans)) {
          return Promise.reject(trans);
        }

        return Promise.resolve(
          new IndexedDbTransaction(
            trans,
            this._lockHelper,
            transToken,
            this._schema!!!,
            this._fakeComplicatedKeys
          )
        );
      }
    );
  }
}

// DbTransaction implementation for the IndexedDB DbProvider.
class IndexedDbTransaction implements DbTransaction {
  private _stores: IDBObjectStore[];

  constructor(
    private _trans: IDBTransaction,
    lockHelper: TransactionLockHelper | undefined,
    private _transToken: TransactionToken,
    private _schema: DbSchema,
    private _fakeComplicatedKeys: boolean
  ) {
    this._stores = map(this._transToken.storeNames, (storeName) =>
      this._trans.objectStore(storeName)
    );

    if (lockHelper) {
      // Chromium seems to have a bug in their indexeddb implementation that lets it start a timeout
      // while the app is in the middle of a commit (it does a two-phase commit).  It can then finish
      // the commit, and later fire the timeout, despite the transaction having been written out already.
      // In this case, it appears that we should be completely fine to ignore the spurious timeout.
      //
      // Applicable Chromium source code here:
      // https://chromium.googlesource.com/chromium/src/+/master/content/browser/indexed_db/indexed_db_transaction.cc
      let history: string[] = [];

      this._trans.oncomplete = () => {
        history.push("complete");

        lockHelper.transactionComplete(this._transToken);
      };

      this._trans.onerror = () => {
        history.push(
          "error-" + (this._trans.error ? this._trans.error.message : "")
        );

        if (history.length > 1) {
          console.warn(
            "IndexedDbTransaction Errored after Resolution, Swallowing. Error: " +
              (this._trans.error ? this._trans.error.message : undefined) +
              ", History: " +
              history.join(",")
          );
          return;
        }

        lockHelper.transactionFailed(
          this._transToken,
          "IndexedDbTransaction OnError: " +
            (this._trans.error ? this._trans.error.message : undefined) +
            ", History: " +
            history.join(",")
        );
      };

      this._trans.onabort = () => {
        history.push(
          "abort-" + (this._trans.error ? this._trans.error.message : "")
        );

        if (history.length > 1) {
          console.warn(
            "IndexedDbTransaction Aborted after Resolution, Swallowing. Error: " +
              (this._trans.error ? this._trans.error.message : undefined) +
              ", History: " +
              history.join(",")
          );
          return;
        }

        lockHelper.transactionFailed(
          this._transToken,
          "IndexedDbTransaction Aborted, Error: " +
            (this._trans.error ? this._trans.error.message : undefined) +
            ", History: " +
            history.join(",")
        );
      };
    }
  }

  getStore(storeName: string): DbStore {
    const store = find(this._stores, (s) => s.name === storeName);
    const storeSchema = find(this._schema.stores, (s) => s.name === storeName);
    if (!store || !storeSchema) {
      throw new Error("Store not found: " + storeName);
    }

    const indexStores: IDBObjectStore[] = [];
    if (this._fakeComplicatedKeys && storeSchema.indexes) {
      // Pull the alternate multientry stores in as well
      each(storeSchema.indexes, (indexSchema) => {
        if (indexSchema.multiEntry || indexSchema.fullText) {
          indexStores.push(
            this._trans.objectStore(storeSchema.name + "_" + indexSchema.name)
          );
        }
      });
    }

    return new IndexedDbStore(
      store,
      indexStores,
      storeSchema,
      this._fakeComplicatedKeys
    );
  }

  getCompletionPromise(): Promise<void> {
    return this._transToken.completionPromise;
  }

  abort(): void {
    // This will wrap through the onAbort above
    this._trans.abort();
  }

  markCompleted(): void {
    // noop
  }
}

function removeFullTextMetadataAndReturn<T>(schema: StoreSchema, val: T): T {
  if (val) {
    // We have full text index fields as real fields on the result, so nuke them before returning them to the caller.
    each(schema.indexes, (index) => {
      if (index.fullText) {
        delete (val as any)[IndexPrefix + index.name];
      }
    });
  }

  return val;
}

// DbStore implementation for the IndexedDB DbProvider.  Again, fairly closely maps to the standard IndexedDB spec, aside from
// a bunch of hacks to support compound keypaths on IE.
class IndexedDbStore implements DbStore {
  constructor(
    private _store: IDBObjectStore,
    private _indexStores: IDBObjectStore[],
    private _schema: StoreSchema,
    private _fakeComplicatedKeys: boolean
  ) {
    // NOP
  }

  get(key: KeyType): Promise<ItemType | undefined> {
    if (
      this._fakeComplicatedKeys &&
      isCompoundKeyPath(this._schema.primaryKeyPath)
    ) {
      const err = attempt(() => {
        key = serializeKeyToString(key, this._schema.primaryKeyPath);
      });
      if (err) {
        return Promise.reject(err);
      }
    }

    return IndexedDbProvider.WrapRequest(this._store.get(key)).then((val) =>
      removeFullTextMetadataAndReturn(this._schema, val)
    );
  }

  getMultiple(keyOrKeys: KeyType | KeyType[]): Promise<ItemType[]> {
    const keys = attempt(() => {
      const keys = formListOfKeys(keyOrKeys, this._schema.primaryKeyPath);

      if (
        this._fakeComplicatedKeys &&
        isCompoundKeyPath(this._schema.primaryKeyPath)
      ) {
        return map(keys, (key) =>
          serializeKeyToString(key, this._schema.primaryKeyPath)
        );
      }
      return keys;
    });
    if (isError(keys)) {
      return Promise.reject(keys);
    }
    // There isn't a more optimized way to do this with indexeddb, have to get the results one by one
    return Promise.all(
      map(keys, (key) =>
        IndexedDbProvider.WrapRequest(this._store.get(key)).then((val) =>
          removeFullTextMetadataAndReturn(this._schema, val)
        )
      )
    ).then(compact);
  }

  put(itemOrItems: ItemType | ItemType[]): Promise<void> {
    let items = arrayify(itemOrItems);

    let promises: Promise<void>[] = [];

    const err = attempt(() => {
      each(items, (item) => {
        let errToReport: any;
        let fakedPk = false;

        if (this._fakeComplicatedKeys) {
          // Fill out any compound-key indexes
          if (isCompoundKeyPath(this._schema.primaryKeyPath)) {
            fakedPk = true;
            (item as any)["nsp_pk"] = getSerializedKeyForKeypath(
              item,
              this._schema.primaryKeyPath
            );
          }

          each(this._schema.indexes, (index) => {
            if (index.multiEntry || index.fullText) {
              let indexStore = find(
                this._indexStores,
                (store) => store.name === this._schema.name + "_" + index.name
              )!!!;

              let keys: any[];
              if (index.fullText) {