in src/IndexedDbProvider.ts [142:413]
async open(
dbName: string,
schema: DbSchema,
wipeIfExists: boolean,
verbose: boolean
): Promise<void> {
// Note: DbProvider returns null instead of a promise that needs waiting for.
super.open(dbName, schema, wipeIfExists, verbose);
if (!this._dbFactory) {
// Couldn't even find a supported indexeddb object on the browser...
return Promise.reject<void>("No support for IndexedDB in this browser");
}
if (wipeIfExists) {
try {
let req = this._dbFactory.deleteDatabase(dbName);
await IndexedDbProvider.WrapRequest(req);
} catch (e) {
// Don't care
}
}
this._lockHelper = new TransactionLockHelper(schema, true);
const dbOpen = this._dbFactory.open(dbName, schema.version);
let migrationPutters: Promise<void>[] = [];
dbOpen.onupgradeneeded = (event) => {
const db: IDBDatabase = dbOpen.result;
const target = <IDBOpenDBRequest>(event.currentTarget || event.target);
const trans = target.transaction;
if (!trans) {
throw new Error("onupgradeneeded: target is null!");
}
if (
schema.lastUsableVersion &&
event.oldVersion < schema.lastUsableVersion
) {
// Clear all stores if it's past the usable version
console.log(
"Old version detected (" + event.oldVersion + "), clearing all data"
);
each(db.objectStoreNames, (name) => {
db.deleteObjectStore(name);
});
}
// Delete dead stores
each(db.objectStoreNames, (storeName) => {
if (!some(schema.stores, (store) => store.name === storeName)) {
db.deleteObjectStore(storeName);
}
});
// Create all stores
each(schema.stores, (storeSchema) => {
let store: IDBObjectStore;
const storeExistedBefore = includes(
db.objectStoreNames,
storeSchema.name
);
if (!storeExistedBefore) {
// store doesn't exist yet
let primaryKeyPath = storeSchema.primaryKeyPath;
if (this._fakeComplicatedKeys && isCompoundKeyPath(primaryKeyPath)) {
// Going to have to hack the compound primary key index into a column, so here it is.
primaryKeyPath = "nsp_pk";
}
// Any is to fix a lib.d.ts issue in TS 2.0.3 - it doesn't realize that keypaths can be compound for some reason...
store = db.createObjectStore(storeSchema.name, {
keyPath: primaryKeyPath,
} as any);
} else {
// store exists, might need to update indexes and migrate the data
store = trans.objectStore(storeSchema.name);
// Check for any indexes no longer in the schema or have been changed
each(store.indexNames, (indexName) => {
const index = store.index(indexName);
let nuke = false;
const indexSchema = find(
storeSchema.indexes,
(idx) => idx.name === indexName
);
if (!indexSchema || !isObject(indexSchema)) {
nuke = true;
} else if (typeof index.keyPath !== typeof indexSchema.keyPath) {
nuke = true;
} else if (typeof index.keyPath === "string") {
if (index.keyPath !== indexSchema.keyPath) {
nuke = true;
}
} /* Keypath is array */ else if (
index.keyPath.length !== indexSchema.keyPath.length
) {
// Keypath length doesn't match, don't bother doing a comparison of each element
nuke = true;
} else {
for (let i = 0; i < index.keyPath.length; i++) {
if (index.keyPath[i] !== indexSchema.keyPath[i]) {
nuke = true;
break;
}
}
}
if (nuke) {
store.deleteIndex(indexName);
}
});
}
// IndexedDB deals well with adding new indexes on the fly, so we don't need to force migrate,
// unless adding multiEntry or fullText index
let needsMigrate = false;
// Check any indexes in the schema that need to be created
each(storeSchema.indexes, (indexSchema) => {
if (!includes(store.indexNames, indexSchema.name)) {
const keyPath = indexSchema.keyPath;
if (this._fakeComplicatedKeys) {
if (indexSchema.multiEntry || indexSchema.fullText) {
if (isCompoundKeyPath(keyPath)) {
throw new Error("Can't use multiEntry and compound keys");
} else {
// Create an object store for the index
let indexStore = db.createObjectStore(
storeSchema.name + "_" + indexSchema.name,
{ autoIncrement: true }
);
indexStore.createIndex("key", "key");
indexStore.createIndex("refkey", "refkey");
if (storeExistedBefore && !indexSchema.doNotBackfill) {
needsMigrate = true;
}
}
} else if (isCompoundKeyPath(keyPath)) {
// Going to have to hack the compound index into a column, so here it is.
store.createIndex(
indexSchema.name,
IndexPrefix + indexSchema.name,
{
unique: indexSchema.unique,
}
);
} else {
store.createIndex(indexSchema.name, keyPath, {
unique: indexSchema.unique,
});
}
} else if (indexSchema.fullText) {
store.createIndex(
indexSchema.name,
IndexPrefix + indexSchema.name,
{
unique: false,
multiEntry: true,
}
);
if (storeExistedBefore && !indexSchema.doNotBackfill) {
needsMigrate = true;
}
} else {
store.createIndex(indexSchema.name, keyPath, {
unique: indexSchema.unique,
multiEntry: indexSchema.multiEntry,
});
}
}
});
if (needsMigrate) {
// Walk every element in the store and re-put it to fill out the new index.
const fakeToken: TransactionToken = {
storeNames: [storeSchema.name],
exclusive: false,
completionPromise: new Promise((resolve) => resolve()),
};
const iTrans = new IndexedDbTransaction(
trans,
undefined,
fakeToken,
schema,
this._fakeComplicatedKeys
);
const tStore = iTrans.getStore(storeSchema.name);
const cursorReq = store.openCursor();
let thisIndexPutters: Promise<void>[] = [];
migrationPutters.push(
IndexedDbIndex.iterateOverCursorRequest(cursorReq, (cursor) => {
const err = attempt(() => {
const item = removeFullTextMetadataAndReturn(
storeSchema,
(cursor as any).value
);
thisIndexPutters.push(tStore.put(item));
});
if (err) {
thisIndexPutters.push(Promise.reject<void>(err));
}
}).then(() => Promise.all(thisIndexPutters).then(noop))
);
}
});
};
const promise = IndexedDbProvider.WrapRequest<IDBDatabase>(dbOpen);
return promise.then(
(db) => {
return Promise.all(migrationPutters).then(() => {
this._db = db;
this._db.onclose = (event: Event) => {
if (this._handleOnClose) {
// instantiate payload
let payload: IDBCloseConnectionPayload = {
name: this._db ? this._db.name : "",
objectStores: this._db
? join(this._db.objectStoreNames, ",")
: "",
type: "unexpectedClosure",
};
// modify payload based on event
const closedDBConnection: IDBCloseConnectionEventDetails = <any>(
event.target
);
if (
closedDBConnection &&
closedDBConnection.name &&
closedDBConnection.objectStoreNames
) {
payload = {
name: closedDBConnection.name,
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);
}
);
}