Merge pull request #456 from vector-im/snowpack-ts-storage-1

Snowpack  + Typescript conversion (Part 1)
This commit is contained in:
Bruno Windels 2021-08-31 08:48:14 +02:00 committed by GitHub
commit cd900ab842
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 374 additions and 293 deletions

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {KeyLimits} from "../../storage/common.js";
import {KeyLimits} from "../../storage/common";
// key for events in the timelineEvents store
export class EventKey {

View file

@ -17,7 +17,7 @@ limitations under the License.
import {BaseEntry} from "./BaseEntry";
import {Direction} from "../Direction.js";
import {isValidFragmentId} from "../common.js";
import {KeyLimits} from "../../../storage/common.js";
import {KeyLimits} from "../../../storage/common";
export class FragmentBoundaryEntry extends BaseEntry {
constructor(fragment, isFragmentStart, fragmentIdComparer) {

View file

@ -14,34 +14,34 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
export const STORE_NAMES = Object.freeze([
"session",
"roomState",
"roomSummary",
"archivedRoomSummary",
"invites",
"roomMembers",
"timelineEvents",
"timelineRelations",
"timelineFragments",
"pendingEvents",
"userIdentities",
"deviceIdentities",
"olmSessions",
"inboundGroupSessions",
"outboundGroupSessions",
"groupSessionDecryptions",
"operations",
"accountData",
]);
export enum StoreNames {
session = "session",
roomState = "roomState",
roomSummary = "roomSummary",
archivedRoomSummary = "archivedRoomSummary",
invites = "invites",
roomMembers = "roomMembers",
timelineEvents = "timelineEvents",
timelineRelations = "timelineRelations",
timelineFragments = "timelineFragments",
pendingEvents = "pendingEvents",
userIdentities = "userIdentities",
deviceIdentities = "deviceIdentities",
olmSessions = "olmSessions",
inboundGroupSessions = "inboundGroupSessions",
outboundGroupSessions = "outboundGroupSessions",
groupSessionDecryptions = "groupSessionDecryptions",
operations = "operations",
accountData = "accountData",
}
export const STORE_MAP = Object.freeze(STORE_NAMES.reduce((nameMap, name) => {
nameMap[name] = name;
return nameMap;
}, {}));
export const STORE_NAMES: Readonly<StoreNames[]> = Object.values(StoreNames);
export class StorageError extends Error {
constructor(message, cause) {
errcode?: string;
cause: Error | null;
constructor(message: string, cause: Error | null = null) {
super(message);
if (cause) {
this.errcode = cause.name;
@ -49,23 +49,23 @@ export class StorageError extends Error {
this.cause = cause;
}
get name() {
get name(): string {
return "StorageError";
}
}
export const KeyLimits = {
get minStorageKey() {
get minStorageKey(): number {
// for indexeddb, we use unsigned 32 bit integers as keys
return 0;
},
get middleStorageKey() {
get middleStorageKey(): number {
// for indexeddb, we use unsigned 32 bit integers as keys
return 0x7FFFFFFF;
},
get maxStorageKey() {
get maxStorageKey(): number {
// for indexeddb, we use unsigned 32 bit integers as keys
return 0xFFFFFFFF;
}

View file

@ -14,14 +14,29 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {iterateCursor, reqAsPromise} from "./utils.js";
import {iterateCursor, DONE, NOT_DONE, reqAsPromise} from "./utils";
export class QueryTarget {
constructor(target) {
type Reducer<A,B> = (acc: B, val: A) => B
export type IDBQuery = IDBValidKey | IDBKeyRange | undefined | null
interface QueryTargetInterface<T> {
openCursor(range?: IDBQuery, direction?: IDBCursorDirection | undefined): IDBRequest<IDBCursorWithValue | null>;
openKeyCursor(range?: IDBQuery, direction?: IDBCursorDirection | undefined): IDBRequest<IDBCursor | null>;
supports(method: string): boolean;
keyPath: string | string[];
get(key: IDBValidKey | IDBKeyRange): IDBRequest<T | null>;
getKey(key: IDBValidKey | IDBKeyRange): IDBRequest<IDBValidKey | undefined>;
}
export class QueryTarget<T> {
protected _target: QueryTargetInterface<T>;
constructor(target: QueryTargetInterface<T>) {
this._target = target;
}
_openCursor(range, direction) {
_openCursor(range?: IDBQuery, direction?: IDBCursorDirection): IDBRequest<IDBCursorWithValue | null> {
if (range && direction) {
return this._target.openCursor(range, direction);
} else if (range) {
@ -33,95 +48,99 @@ export class QueryTarget {
}
}
supports(methodName) {
supports(methodName: string): boolean {
return this._target.supports(methodName);
}
get(key) {
get(key: IDBValidKey | IDBKeyRange): Promise<T | null> {
return reqAsPromise(this._target.get(key));
}
getKey(key) {
getKey(key: IDBValidKey | IDBKeyRange): Promise<IDBValidKey | undefined> {
if (this._target.supports("getKey")) {
return reqAsPromise(this._target.getKey(key));
} else {
return reqAsPromise(this._target.get(key)).then(value => {
if (value) {
return value[this._target.keyPath];
let keyPath = this._target.keyPath;
if (typeof keyPath === "string") {
keyPath = [keyPath];
}
return keyPath.reduce((obj, key) => obj[key], value);
}
});
}
}
reduce(range, reducer, initialValue) {
reduce<B>(range: IDBQuery, reducer: Reducer<T,B>, initialValue: B): Promise<boolean> {
return this._reduce(range, reducer, initialValue, "next");
}
reduceReverse(range, reducer, initialValue) {
reduceReverse<B>(range: IDBQuery, reducer: Reducer<T,B>, initialValue: B): Promise<boolean> {
return this._reduce(range, reducer, initialValue, "prev");
}
selectLimit(range, amount) {
selectLimit(range: IDBQuery, amount: number): Promise<T[]> {
return this._selectLimit(range, amount, "next");
}
selectLimitReverse(range, amount) {
selectLimitReverse(range: IDBQuery, amount: number): Promise<T[]> {
return this._selectLimit(range, amount, "prev");
}
selectWhile(range, predicate) {
selectWhile(range: IDBQuery, predicate: (v: T) => boolean): Promise<T[]> {
return this._selectWhile(range, predicate, "next");
}
selectWhileReverse(range, predicate) {
selectWhileReverse(range: IDBQuery, predicate: (v: T) => boolean): Promise<T[]> {
return this._selectWhile(range, predicate, "prev");
}
async selectAll(range, direction) {
async selectAll(range?: IDBQuery, direction?: IDBCursorDirection): Promise<T[]> {
const cursor = this._openCursor(range, direction);
const results = [];
await iterateCursor(cursor, (value) => {
const results: T[] = [];
await iterateCursor<T>(cursor, (value) => {
results.push(value);
return {done: false};
return NOT_DONE;
});
return results;
}
selectFirst(range) {
selectFirst(range: IDBQuery): Promise<T | undefined> {
return this._find(range, () => true, "next");
}
selectLast(range) {
selectLast(range: IDBQuery): Promise<T | undefined> {
return this._find(range, () => true, "prev");
}
find(range, predicate) {
find(range: IDBQuery, predicate: (v: T) => boolean): Promise<T | undefined> {
return this._find(range, predicate, "next");
}
findReverse(range, predicate) {
findReverse(range: IDBQuery, predicate: (v : T) => boolean): Promise<T | undefined> {
return this._find(range, predicate, "prev");
}
async findMaxKey(range) {
async findMaxKey(range: IDBQuery): Promise<IDBValidKey | undefined> {
const cursor = this._target.openKeyCursor(range, "prev");
let maxKey;
await iterateCursor(cursor, (_, key) => {
maxKey = key;
return {done: true};
return DONE;
});
return maxKey;
}
async iterateValues(range, callback) {
async iterateValues(range: IDBQuery, callback: (val: T, key: IDBValidKey, cur: IDBCursorWithValue) => boolean): Promise<void> {
const cursor = this._target.openCursor(range, "next");
await iterateCursor(cursor, (value, key, cur) => {
await iterateCursor<T>(cursor, (value, key, cur) => {
return {done: callback(value, key, cur)};
});
}
async iterateKeys(range, callback) {
async iterateKeys(range: IDBQuery, callback: (key: IDBValidKey, cur: IDBCursor) => boolean): Promise<void> {
const cursor = this._target.openKeyCursor(range, "next");
await iterateCursor(cursor, (_, key, cur) => {
return {done: callback(key, cur)};
@ -134,7 +153,7 @@ export class QueryTarget {
* If the callback returns true, the search is halted and callback won't be called again.
* `callback` is called with the same instances of the key as given in `keys`, so direct comparison can be used.
*/
async findExistingKeys(keys, backwards, callback) {
async findExistingKeys(keys: IDBValidKey[], backwards: boolean, callback: (key: IDBValidKey, found: boolean) => boolean): Promise<void> {
const direction = backwards ? "prev" : "next";
const compareKeys = (a, b) => backwards ? -indexedDB.cmp(a, b) : indexedDB.cmp(a, b);
const sortedKeys = keys.slice().sort(compareKeys);
@ -154,7 +173,10 @@ export class QueryTarget {
++i;
}
const done = consumerDone || i >= sortedKeys.length;
const jumpTo = !done && sortedKeys[i];
let jumpTo;
if (!done) {
jumpTo = sortedKeys[i];
}
return {done, jumpTo};
});
// report null for keys we didn't to at the end
@ -164,25 +186,25 @@ export class QueryTarget {
}
}
_reduce(range, reducer, initialValue, direction) {
_reduce<B>(range: IDBQuery, reducer: (reduced: B, value: T) => B, initialValue: B, direction: IDBCursorDirection): Promise<boolean> {
let reducedValue = initialValue;
const cursor = this._openCursor(range, direction);
return iterateCursor(cursor, (value) => {
return iterateCursor<T>(cursor, (value) => {
reducedValue = reducer(reducedValue, value);
return {done: false};
return NOT_DONE;
});
}
_selectLimit(range, amount, direction) {
_selectLimit(range: IDBQuery, amount: number, direction: IDBCursorDirection): Promise<T[]> {
return this._selectUntil(range, (results) => {
return results.length === amount;
}, direction);
}
async _selectUntil(range, predicate, direction) {
async _selectUntil(range: IDBQuery, predicate: (vs: T[], v: T) => boolean, direction: IDBCursorDirection): Promise<T[]> {
const cursor = this._openCursor(range, direction);
const results = [];
await iterateCursor(cursor, (value) => {
const results: T[] = [];
await iterateCursor<T>(cursor, (value) => {
results.push(value);
return {done: predicate(results, value)};
});
@ -190,10 +212,10 @@ export class QueryTarget {
}
// allows you to fetch one too much that won't get added when the predicate fails
async _selectWhile(range, predicate, direction) {
async _selectWhile(range: IDBQuery, predicate: (v: T) => boolean, direction: IDBCursorDirection): Promise<T[]> {
const cursor = this._openCursor(range, direction);
const results = [];
await iterateCursor(cursor, (value) => {
const results: T[] = [];
await iterateCursor<T>(cursor, (value) => {
const passesPredicate = predicate(value);
if (passesPredicate) {
results.push(value);
@ -203,18 +225,18 @@ export class QueryTarget {
return results;
}
async iterateWhile(range, predicate) {
async iterateWhile(range: IDBQuery, predicate: (v: T) => boolean): Promise<void> {
const cursor = this._openCursor(range, "next");
await iterateCursor(cursor, (value) => {
await iterateCursor<T>(cursor, (value) => {
const passesPredicate = predicate(value);
return {done: !passesPredicate};
});
}
async _find(range, predicate, direction) {
async _find(range: IDBQuery, predicate: (v: T) => boolean, direction: IDBCursorDirection): Promise<T | undefined> {
const cursor = this._openCursor(range, direction);
let result;
const found = await iterateCursor(cursor, (value) => {
const found = await iterateCursor<T>(cursor, (value) => {
const found = predicate(value);
if (found) {
result = value;

View file

@ -15,8 +15,8 @@ limitations under the License.
*/
import {Transaction} from "./Transaction.js";
import { STORE_NAMES, StorageError } from "../common.js";
import { reqAsPromise } from "./utils.js";
import { STORE_NAMES, StoreNames, StorageError } from "../common";
import { reqAsPromise } from "./utils";
const WEBKITEARLYCLOSETXNBUG_BOGUS_KEY = "782rh281re38-boguskey";
@ -25,11 +25,7 @@ export class Storage {
this._db = idbDatabase;
this._IDBKeyRange = IDBKeyRange;
this._hasWebkitEarlyCloseTxnBug = hasWebkitEarlyCloseTxnBug;
const nameMap = STORE_NAMES.reduce((nameMap, name) => {
nameMap[name] = name;
return nameMap;
}, {});
this.storeNames = Object.freeze(nameMap);
this.storeNames = StoreNames;
}
_validateStoreNames(storeNames) {

View file

@ -15,7 +15,7 @@ limitations under the License.
*/
import {Storage} from "./Storage.js";
import { openDatabase, reqAsPromise } from "./utils.js";
import { openDatabase, reqAsPromise } from "./utils";
import { exportSession, importSession } from "./export.js";
import { schema } from "./schema.js";
import { detectWebkitEarlyCloseTxnBug } from "./quirks.js";

View file

@ -1,164 +0,0 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {QueryTarget} from "./QueryTarget.js";
import {IDBRequestAttemptError} from "./error.js";
const LOG_REQUESTS = false;
function logRequest(method, params, source) {
const storeName = source?.name;
const databaseName = source?.transaction?.db?.name;
console.info(`${databaseName}.${storeName}.${method}(${params.map(p => JSON.stringify(p)).join(", ")})`);
}
class QueryTargetWrapper {
constructor(qt) {
this._qt = qt;
}
get keyPath() {
if (this._qt.objectStore) {
return this._qt.objectStore.keyPath;
} else {
return this._qt.keyPath;
}
}
supports(methodName) {
return !!this._qt[methodName];
}
openKeyCursor(...params) {
try {
// not supported on Edge 15
if (!this._qt.openKeyCursor) {
LOG_REQUESTS && logRequest("openCursor", params, this._qt);
return this.openCursor(...params);
}
LOG_REQUESTS && logRequest("openKeyCursor", params, this._qt);
return this._qt.openKeyCursor(...params);
} catch(err) {
throw new IDBRequestAttemptError("openKeyCursor", this._qt, err, params);
}
}
openCursor(...params) {
try {
LOG_REQUESTS && logRequest("openCursor", params, this._qt);
return this._qt.openCursor(...params);
} catch(err) {
throw new IDBRequestAttemptError("openCursor", this._qt, err, params);
}
}
put(...params) {
try {
LOG_REQUESTS && logRequest("put", params, this._qt);
return this._qt.put(...params);
} catch(err) {
throw new IDBRequestAttemptError("put", this._qt, err, params);
}
}
add(...params) {
try {
LOG_REQUESTS && logRequest("add", params, this._qt);
return this._qt.add(...params);
} catch(err) {
throw new IDBRequestAttemptError("add", this._qt, err, params);
}
}
get(...params) {
try {
LOG_REQUESTS && logRequest("get", params, this._qt);
return this._qt.get(...params);
} catch(err) {
throw new IDBRequestAttemptError("get", this._qt, err, params);
}
}
getKey(...params) {
try {
LOG_REQUESTS && logRequest("getKey", params, this._qt);
return this._qt.getKey(...params);
} catch(err) {
throw new IDBRequestAttemptError("getKey", this._qt, err, params);
}
}
delete(...params) {
try {
LOG_REQUESTS && logRequest("delete", params, this._qt);
return this._qt.delete(...params);
} catch(err) {
throw new IDBRequestAttemptError("delete", this._qt, err, params);
}
}
index(...params) {
try {
return this._qt.index(...params);
} catch(err) {
// TODO: map to different error? this is not a request
throw new IDBRequestAttemptError("index", this._qt, err, params);
}
}
}
export class Store extends QueryTarget {
constructor(idbStore, transaction) {
super(new QueryTargetWrapper(idbStore));
this._transaction = transaction;
}
get IDBKeyRange() {
return this._transaction.IDBKeyRange;
}
get _idbStore() {
return this._target;
}
index(indexName) {
return new QueryTarget(new QueryTargetWrapper(this._idbStore.index(indexName)));
}
put(value) {
// If this request fails, the error will bubble up to the transaction and abort it,
// which is the behaviour we want. Therefore, it is ok to not create a promise for this
// request and await it.
//
// Perhaps at some later point, we will want to handle an error (like ConstraintError) for
// individual write requests. In that case, we should add a method that returns a promise (e.g. putAndObserve)
// and call preventDefault on the event to prevent it from aborting the transaction
//
// Note that this can still throw synchronously, like it does for TransactionInactiveError,
// see https://www.w3.org/TR/IndexedDB-2/#transaction-lifetime-concept
this._idbStore.put(value);
}
add(value) {
// ok to not monitor result of request, see comment in `put`.
this._idbStore.add(value);
}
delete(keyOrKeyRange) {
// ok to not monitor result of request, see comment in `put`.
this._idbStore.delete(keyOrKeyRange);
}
}

View file

@ -0,0 +1,174 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {QueryTarget, IDBQuery} from "./QueryTarget";
import {IDBRequestAttemptError} from "./error";
import {reqAsPromise} from "./utils";
import {Transaction} from "./Transaction";
const LOG_REQUESTS = false;
function logRequest(method: string, params: any[], source: any): void {
const storeName = source?.name;
const databaseName = source?.transaction?.db?.name;
console.info(`${databaseName}.${storeName}.${method}(${params.map(p => JSON.stringify(p)).join(", ")})`);
}
class QueryTargetWrapper<T> {
private _qt: IDBIndex | IDBObjectStore;
constructor(qt: IDBIndex | IDBObjectStore) {
this._qt = qt;
}
get keyPath(): string | string[] {
return this._qtStore.keyPath;
}
get _qtStore(): IDBObjectStore {
if ("objectStore" in this._qt) {
return this._qt.objectStore;
}
return this._qt;
}
supports(methodName: string): boolean {
return !!this._qt[methodName];
}
openKeyCursor(range?: IDBQuery, direction?: IDBCursorDirection | undefined): IDBRequest<IDBCursor | null> {
try {
// not supported on Edge 15
if (!this._qt.openKeyCursor) {
LOG_REQUESTS && logRequest("openCursor", [range, direction], this._qt);
return this.openCursor(range, direction);
}
LOG_REQUESTS && logRequest("openKeyCursor", [range, direction], this._qt);
return this._qt.openKeyCursor(range, direction)
} catch(err) {
throw new IDBRequestAttemptError("openKeyCursor", this._qt, err, [range, direction]);
}
}
openCursor(range?: IDBQuery, direction?: IDBCursorDirection | undefined): IDBRequest<IDBCursorWithValue | null> {
try {
LOG_REQUESTS && logRequest("openCursor", [], this._qt);
return this._qt.openCursor(range, direction)
} catch(err) {
throw new IDBRequestAttemptError("openCursor", this._qt, err, [range, direction]);
}
}
put(item: T, key?: IDBValidKey | undefined): IDBRequest<IDBValidKey> {
try {
LOG_REQUESTS && logRequest("put", [item, key], this._qt);
return this._qtStore.put(item, key);
} catch(err) {
throw new IDBRequestAttemptError("put", this._qt, err, [item, key]);
}
}
add(item: T, key?: IDBValidKey | undefined): IDBRequest<IDBValidKey> {
try {
LOG_REQUESTS && logRequest("add", [item, key], this._qt);
return this._qtStore.add(item, key);
} catch(err) {
throw new IDBRequestAttemptError("add", this._qt, err, [item, key]);
}
}
get(key: IDBValidKey | IDBKeyRange): IDBRequest<T | null> {
try {
LOG_REQUESTS && logRequest("get", [key], this._qt);
return this._qt.get(key);
} catch(err) {
throw new IDBRequestAttemptError("get", this._qt, err, [key]);
}
}
getKey(key: IDBValidKey | IDBKeyRange): IDBRequest<IDBValidKey | undefined> {
try {
LOG_REQUESTS && logRequest("getKey", [key], this._qt);
return this._qt.getKey(key)
} catch(err) {
throw new IDBRequestAttemptError("getKey", this._qt, err, [key]);
}
}
delete(key: IDBValidKey | IDBKeyRange): IDBRequest<undefined> {
try {
LOG_REQUESTS && logRequest("delete", [key], this._qt);
return this._qtStore.delete(key);
} catch(err) {
throw new IDBRequestAttemptError("delete", this._qt, err, [key]);
}
}
index(name: string): IDBIndex {
try {
return this._qtStore.index(name);
} catch(err) {
// TODO: map to different error? this is not a request
throw new IDBRequestAttemptError("index", this._qt, err, [name]);
}
}
}
export class Store<T> extends QueryTarget<T> {
private _transaction: Transaction;
constructor(idbStore: IDBObjectStore, transaction: Transaction) {
super(new QueryTargetWrapper<T>(idbStore));
this._transaction = transaction;
}
get IDBKeyRange() {
// @ts-ignore
return this._transaction.IDBKeyRange;
}
get _idbStore(): QueryTargetWrapper<T> {
return (this._target as QueryTargetWrapper<T>);
}
index(indexName: string): QueryTarget<T> {
return new QueryTarget<T>(new QueryTargetWrapper<T>(this._idbStore.index(indexName)));
}
put(value: T): Promise<IDBValidKey> {
// If this request fails, the error will bubble up to the transaction and abort it,
// which is the behaviour we want. Therefore, it is ok to not create a promise for this
// request and await it.
//
// Perhaps at some later point, we will want to handle an error (like ConstraintError) for
// individual write requests. In that case, we should add a method that returns a promise (e.g. putAndObserve)
// and call preventDefault on the event to prevent it from aborting the transaction
//
// Note that this can still throw synchronously, like it does for TransactionInactiveError,
// see https://www.w3.org/TR/IndexedDB-2/#transaction-lifetime-concept
return reqAsPromise(this._idbStore.put(value));
}
add(value: T): Promise<IDBValidKey> {
// ok to not monitor result of request, see comment in `put`.
return reqAsPromise(this._idbStore.add(value));
}
delete(keyOrKeyRange: IDBValidKey | IDBKeyRange): Promise<undefined> {
// ok to not monitor result of request, see comment in `put`.
return reqAsPromise(this._idbStore.delete(keyOrKeyRange));
}
}

View file

@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {txnAsPromise} from "./utils.js";
import {StorageError} from "../common.js";
import {Store} from "./Store.js";
import {txnAsPromise} from "./utils";
import {StorageError} from "../common";
import {Store} from "./Store";
import {SessionStore} from "./stores/SessionStore.js";
import {RoomSummaryStore} from "./stores/RoomSummaryStore.js";
import {InviteStore} from "./stores/InviteStore.js";

View file

@ -15,12 +15,28 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { StorageError } from "../common.js";
import { StorageError } from "../common";
function _sourceName(source: IDBIndex | IDBObjectStore): string {
return "objectStore" in source ?
`${source.objectStore.name}.${source.name}` :
source.name;
}
function _sourceDatabase(source: IDBIndex | IDBObjectStore): string {
return "objectStore" in source ?
source.objectStore?.transaction?.db?.name :
source.transaction?.db?.name;
}
export class IDBError extends StorageError {
constructor(message, source, cause) {
const storeName = source?.name || "<unknown store>";
const databaseName = source?.transaction?.db?.name || "<unknown db>";
storeName: string;
databaseName: string;
constructor(message: string, sourceOrCursor: IDBIndex | IDBCursor | IDBObjectStore, cause: DOMException | null = null) {
const source = "source" in sourceOrCursor ? sourceOrCursor.source : sourceOrCursor;
const storeName = _sourceName(source);
const databaseName = _sourceDatabase(source);
let fullMessage = `${message} on ${databaseName}.${storeName}`;
if (cause) {
fullMessage += ": ";
@ -41,7 +57,7 @@ export class IDBError extends StorageError {
}
export class IDBRequestError extends IDBError {
constructor(request, message = "IDBRequest failed") {
constructor(request: IDBRequest, message: string = "IDBRequest failed") {
const source = request.source;
const cause = request.error;
super(message, source, cause);
@ -49,7 +65,7 @@ export class IDBRequestError extends IDBError {
}
export class IDBRequestAttemptError extends IDBError {
constructor(method, source, cause, params) {
constructor(method: string, source: IDBIndex | IDBObjectStore, cause: DOMException, params: any[]) {
super(`${method}(${params.map(p => JSON.stringify(p)).join(", ")}) failed`, source, cause);
}
}

View file

@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { iterateCursor, txnAsPromise } from "./utils.js";
import { STORE_NAMES } from "../common.js";
import { iterateCursor, txnAsPromise } from "./utils";
import { STORE_NAMES } from "../common";
export async function exportSession(db) {
const NOT_DONE = {done: false};

View file

@ -15,7 +15,7 @@ limitations under the License.
*/
import {openDatabase, txnAsPromise, reqAsPromise} from "./utils.js";
import {openDatabase, txnAsPromise, reqAsPromise} from "./utils";
// filed as https://bugs.webkit.org/show_bug.cgi?id=222746
export async function detectWebkitEarlyCloseTxnBug(idbFactory) {

View file

@ -1,4 +1,4 @@
import {iterateCursor, reqAsPromise} from "./utils.js";
import {iterateCursor, reqAsPromise} from "./utils";
import {RoomMember, EVENT_TYPE as MEMBER_EVENT_TYPE} from "../../room/members/RoomMember.js";
import {addRoomToIdentity} from "../../e2ee/DeviceTracker.js";
import {RoomMemberStore} from "./stores/RoomMemberStore.js";

View file

@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { encodeUint32, decodeUint32 } from "../utils.js";
import {KeyLimits} from "../../common.js";
import { encodeUint32, decodeUint32 } from "../utils";
import {KeyLimits} from "../../common";
function encodeKey(roomId, queueIndex) {
return `${roomId}|${encodeUint32(queueIndex)}`;

View file

@ -15,9 +15,9 @@ limitations under the License.
*/
import {EventKey} from "../../../room/timeline/EventKey.js";
import { StorageError } from "../../common.js";
import { encodeUint32 } from "../utils.js";
import {KeyLimits} from "../../common.js";
import { StorageError } from "../../common";
import { encodeUint32 } from "../utils";
import {KeyLimits} from "../../common";
function encodeKey(roomId, fragmentId, eventIndex) {
return `${roomId}|${encodeUint32(fragmentId)}|${encodeUint32(eventIndex)}`;

View file

@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { StorageError } from "../../common.js";
import {KeyLimits} from "../../common.js";
import { encodeUint32 } from "../utils.js";
import { StorageError } from "../../common";
import {KeyLimits} from "../../common";
import { encodeUint32 } from "../utils";
function encodeKey(roomId, fragmentId) {
return `${roomId}|${encodeUint32(fragmentId)}`;

View file

@ -15,17 +15,20 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { IDBRequestError } from "./error.js";
import { StorageError } from "../common.js";
import { IDBRequestError } from "./error";
import { StorageError } from "../common";
let needsSyncPromise = false;
export const DONE = { done: true }
export const NOT_DONE = { done: false }
/* should be called on legacy platforms to see
if transactions close before draining the microtask queue (IE11 on Windows 7).
If this is the case, promises need to be resolved
synchronously from the idb request handler to prevent the transaction from closing prematurely.
*/
export async function checkNeedsSyncPromise() {
export async function checkNeedsSyncPromise(): Promise<boolean> {
// important to have it turned off while doing the test,
// otherwise reqAsPromise would not fail
needsSyncPromise = false;
@ -49,26 +52,29 @@ export async function checkNeedsSyncPromise() {
}
// storage keys are defined to be unsigned 32bit numbers in KeyLimits, which is assumed by idb
export function encodeUint32(n) {
export function encodeUint32(n: number): string {
const hex = n.toString(16);
return "0".repeat(8 - hex.length) + hex;
}
// used for logs where timestamp is part of key, which is larger than 32 bit
export function encodeUint64(n) {
export function encodeUint64(n: number): string {
const hex = n.toString(16);
return "0".repeat(16 - hex.length) + hex;
}
export function decodeUint32(str) {
export function decodeUint32(str: string): number {
return parseInt(str, 16);
}
export function openDatabase(name, createObjectStore, version, idbFactory = window.indexedDB) {
type CreateObjectStore = (db : IDBDatabase, txn: IDBTransaction | null, oldVersion: number, version: number) => any
export function openDatabase(name: string, createObjectStore: CreateObjectStore, version: number, idbFactory: IDBFactory = window.indexedDB): Promise<IDBDatabase> {
const req = idbFactory.open(name, version);
req.onupgradeneeded = async (ev) => {
const db = ev.target.result;
const txn = ev.target.transaction;
req.onupgradeneeded = async (ev : IDBVersionChangeEvent) => {
const req = ev.target as IDBRequest<IDBDatabase>;
const db = req.result;
const txn = req.transaction!;
const oldVersion = ev.oldVersion;
try {
await createObjectStore(db, txn, oldVersion, version);
@ -82,25 +88,28 @@ export function openDatabase(name, createObjectStore, version, idbFactory = wind
return reqAsPromise(req);
}
export function reqAsPromise(req) {
export function reqAsPromise<T>(req: IDBRequest<T>): Promise<T> {
return new Promise((resolve, reject) => {
req.addEventListener("success", event => {
resolve(event.target.result);
resolve((event.target as IDBRequest<T>).result);
// @ts-ignore
needsSyncPromise && Promise._flush && Promise._flush();
});
req.addEventListener("error", event => {
const error = new IDBRequestError(event.target);
const error = new IDBRequestError(event.target as IDBRequest<T>);
reject(error);
// @ts-ignore
needsSyncPromise && Promise._flush && Promise._flush();
});
});
}
export function txnAsPromise(txn) {
export function txnAsPromise(txn): Promise<void> {
let error;
return new Promise((resolve, reject) => {
txn.addEventListener("complete", () => {
resolve();
// @ts-ignore
needsSyncPromise && Promise._flush && Promise._flush();
});
txn.addEventListener("error", event => {
@ -119,33 +128,56 @@ export function txnAsPromise(txn) {
error = new StorageError(`Transaction on ${dbName} with stores ${storeNames} was aborted.`);
}
reject(error);
// @ts-ignore
needsSyncPromise && Promise._flush && Promise._flush();
});
});
}
export function iterateCursor(cursorRequest, processValue) {
/**
* This type is rather complicated, but I hope that this is for a good reason. There
* are currently two uses for `iterateCursor`: iterating a regular cursor, and iterating
* a key-only cursor, which does not have values. These two uses are distinct, and iteration
* never stops or starts having a value halfway through.
*
* Each of the argument functions currently either assumes the value will be there, or that it won't. We thus can't
* just accept a function argument `(T | undefined) => { done: boolean }`, since this messes with
* the type safety in both cases: the former case will have to check for `undefined`, and
* the latter would have an argument that can be `T`, even though it never will.
*
* So the approach here is to let TypeScript infer and accept (via generics) the type of
* the cursor, which is either `IDBCursorWithValue` or `IDBCursor`. Since the type is accepted
* via generics, we can actually vary the types of the actual function arguments depending on it.
* Thus, when a value is available (an `IDBCursorWithValue` is given), we require a function `(T) => ...`, and when it is not, we require
* a function `(undefined) => ...`.
*/
type CursorIterator<T, I extends IDBCursor> = (value: I extends IDBCursorWithValue ? T : undefined, key: IDBValidKey, cursor: I) => { done: boolean, jumpTo?: IDBValidKey }
export function iterateCursor<T, I extends IDBCursor = IDBCursorWithValue>(cursorRequest: IDBRequest<I | null>, processValue: CursorIterator<T, I>): Promise<boolean> {
// TODO: does cursor already have a value here??
return new Promise((resolve, reject) => {
return new Promise<boolean>((resolve, reject) => {
cursorRequest.onerror = () => {
reject(new IDBRequestError(cursorRequest));
// @ts-ignore
needsSyncPromise && Promise._flush && Promise._flush();
};
// collect results
cursorRequest.onsuccess = (event) => {
const cursor = event.target.result;
const cursor = (event.target as IDBRequest<I>).result;
if (!cursor) {
resolve(false);
// @ts-ignore
needsSyncPromise && Promise._flush && Promise._flush();
return; // end of results
}
const result = processValue(cursor.value, cursor.key, cursor);
const result = processValue(cursor["value"], cursor.key, cursor);
// TODO: don't use object for result and assume it's jumpTo when not === true/false or undefined
const done = result?.done;
const jumpTo = result?.jumpTo;
if (done) {
resolve(true);
// @ts-ignore
needsSyncPromise && Promise._flush && Promise._flush();
} else if(jumpTo) {
cursor.continue(jumpTo);
@ -158,16 +190,20 @@ export function iterateCursor(cursorRequest, processValue) {
});
}
export async function fetchResults(cursor, isDone) {
const results = [];
await iterateCursor(cursor, (value) => {
type Pred<T> = (value: T) => boolean
export async function fetchResults<T>(cursor: IDBRequest, isDone: Pred<T[]>): Promise<T[]> {
const results: T[] = [];
await iterateCursor<T>(cursor, (value) => {
results.push(value);
return {done: isDone(results)};
});
return results;
}
export async function select(db, storeName, toCursor, isDone) {
type ToCursor = (store: IDBObjectStore) => IDBRequest
export async function select<T>(db: IDBDatabase, storeName: string, toCursor: ToCursor, isDone: Pred<T[]>): Promise<T[]> {
if (!isDone) {
isDone = () => false;
}
@ -180,7 +216,7 @@ export async function select(db, storeName, toCursor, isDone) {
return await fetchResults(cursor, isDone);
}
export async function findStoreValue(db, storeName, toCursor, matchesValue) {
export async function findStoreValue<T>(db: IDBDatabase, storeName: string, toCursor: ToCursor, matchesValue: Pred<T>): Promise<T> {
if (!matchesValue) {
matchesValue = () => true;
}
@ -192,11 +228,12 @@ export async function findStoreValue(db, storeName, toCursor, matchesValue) {
const store = tx.objectStore(storeName);
const cursor = await reqAsPromise(toCursor(store));
let match;
const matched = await iterateCursor(cursor, (value) => {
const matched = await iterateCursor<T>(cursor, (value) => {
if (matchesValue(value)) {
match = value;
return true;
return DONE;
}
return NOT_DONE;
});
if (!matched) {
throw new StorageError("Value not found");

View file

@ -16,7 +16,7 @@ limitations under the License.
// polyfills needed for IE11
import Promise from "../../../lib/es6-promise/index.js";
import {checkNeedsSyncPromise} from "../../matrix/storage/idb/utils.js";
import {checkNeedsSyncPromise} from "../../matrix/storage/idb/utils";
if (typeof window.Promise === "undefined") {
window.Promise = Promise;