wallet-core/packages/idb-bridge/src/BridgeIDBFactory.ts
2019-08-17 01:03:55 +02:00

227 lines
6.9 KiB
TypeScript

/*
* Copyright 2017 Jeremy Scheff
* Copyright 2019 Florian Dold
*
* 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 BridgeIDBDatabase from "./BridgeIDBDatabase";
import BridgeIDBOpenDBRequest from "./BridgeIDBOpenDBRequest";
import BridgeIDBVersionChangeEvent from "./BridgeIDBVersionChangeEvent";
import compareKeys from "./util/cmp";
import enforceRange from "./util/enforceRange";
import { AbortError, VersionError } from "./util/errors";
import FakeEvent from "./util/FakeEvent";
import { Backend, DatabaseConnection } from "./backend-interface";
import queueTask from "./util/queueTask";
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 {
const dbconn = await this.backend.connectDatabase(name);
const backendTransaction = await this.backend.enterVersionChange(
dbconn,
0,
);
await this.backend.deleteDatabase(backendTransaction, name);
await this.backend.commit(backendTransaction);
await this.backend.close(dbconn);
request.result = undefined;
request.readyState = "done";
const event2 = new BridgeIDBVersionChangeEvent("success", {
newVersion: null,
oldVersion,
});
request.dispatchEvent(event2);
} catch (err) {
request.error = new Error();
request.error.name = err.name;
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) {
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();
}
const request = new BridgeIDBOpenDBRequest();
queueTask(async () => {
let dbconn: DatabaseConnection;
try {
dbconn = await this.backend.connectDatabase(name);
} catch (err) {
request._finishWithError(err);
return;
}
const schema = this.backend.getSchema(dbconn);
const existingVersion = schema.databaseVersion;
if (version === undefined) {
version = existingVersion !== 0 ? existingVersion : 1;
}
const requestedVersion = version;
BridgeIDBFactory.enableTracing &&
console.log(
`TRACE: existing version ${existingVersion}, requested version ${requestedVersion}`,
);
if (existingVersion > requestedVersion) {
request._finishWithError(new VersionError());
return;
}
const db = new BridgeIDBDatabase(this.backend, dbconn);
if (existingVersion == requestedVersion) {
request.result = db;
request.readyState = "done";
const event2 = new FakeEvent("success", {
bubbles: false,
cancelable: false,
});
event2.eventPath = [request];
request.dispatchEvent(event2);
}
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) {
const event = new BridgeIDBVersionChangeEvent("versionchange", {
newVersion: version,
oldVersion: existingVersion,
});
otherConn.dispatchEvent(event);
}
if (this._anyOpen()) {
const event = new BridgeIDBVersionChangeEvent("blocked", {
newVersion: version,
oldVersion: existingVersion,
});
request.dispatchEvent(event);
}
const backendTransaction = await this.backend.enterVersionChange(
dbconn,
requestedVersion,
);
db._runningVersionchangeTransaction = true;
const transaction = db._internalTransaction(
[],
"versionchange",
backendTransaction,
);
const event = new BridgeIDBVersionChangeEvent("upgradeneeded", {
newVersion: version,
oldVersion: existingVersion,
});
request.result = db;
request.readyState = "done";
request.transaction = transaction;
request.dispatchEvent(event);
await transaction._waitDone();
// We don't explicitly exit the versionchange transaction,
// since this is already done by the BridgeIDBTransaction.
db._runningVersionchangeTransaction = false;
const event2 = new FakeEvent("success", {
bubbles: false,
cancelable: false,
});
event2.eventPath = [request];
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(): boolean {
return this.connections.some(c => !c._closed && !c._closePending);
}
}
export default BridgeIDBFactory;