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) {