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. limitations under the License.
*/ */
import {KeyLimits} from "../../storage/common.js"; import {KeyLimits} from "../../storage/common";
// key for events in the timelineEvents store // key for events in the timelineEvents store
export class EventKey { export class EventKey {

View file

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

View file

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

View file

@ -14,14 +14,29 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {iterateCursor, reqAsPromise} from "./utils.js"; import {iterateCursor, DONE, NOT_DONE, reqAsPromise} from "./utils";
export class QueryTarget { type Reducer<A,B> = (acc: B, val: A) => B
constructor(target) {
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; this._target = target;
} }
_openCursor(range, direction) { _openCursor(range?: IDBQuery, direction?: IDBCursorDirection): IDBRequest<IDBCursorWithValue | null> {
if (range && direction) { if (range && direction) {
return this._target.openCursor(range, direction); return this._target.openCursor(range, direction);
} else if (range) { } else if (range) {
@ -33,95 +48,99 @@ export class QueryTarget {
} }
} }
supports(methodName) { supports(methodName: string): boolean {
return this._target.supports(methodName); return this._target.supports(methodName);
} }
get(key) { get(key: IDBValidKey | IDBKeyRange): Promise<T | null> {
return reqAsPromise(this._target.get(key)); return reqAsPromise(this._target.get(key));
} }
getKey(key) { getKey(key: IDBValidKey | IDBKeyRange): Promise<IDBValidKey | undefined> {
if (this._target.supports("getKey")) { if (this._target.supports("getKey")) {
return reqAsPromise(this._target.getKey(key)); return reqAsPromise(this._target.getKey(key));
} else { } else {
return reqAsPromise(this._target.get(key)).then(value => { return reqAsPromise(this._target.get(key)).then(value => {
if (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"); 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"); return this._reduce(range, reducer, initialValue, "prev");
} }
selectLimit(range, amount) { selectLimit(range: IDBQuery, amount: number): Promise<T[]> {
return this._selectLimit(range, amount, "next"); return this._selectLimit(range, amount, "next");
} }
selectLimitReverse(range, amount) { selectLimitReverse(range: IDBQuery, amount: number): Promise<T[]> {
return this._selectLimit(range, amount, "prev"); return this._selectLimit(range, amount, "prev");
} }
selectWhile(range, predicate) { selectWhile(range: IDBQuery, predicate: (v: T) => boolean): Promise<T[]> {
return this._selectWhile(range, predicate, "next"); return this._selectWhile(range, predicate, "next");
} }
selectWhileReverse(range, predicate) { selectWhileReverse(range: IDBQuery, predicate: (v: T) => boolean): Promise<T[]> {
return this._selectWhile(range, predicate, "prev"); 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 cursor = this._openCursor(range, direction);
const results = []; const results: T[] = [];
await iterateCursor(cursor, (value) => { await iterateCursor<T>(cursor, (value) => {
results.push(value); results.push(value);
return {done: false}; return NOT_DONE;
}); });
return results; return results;
} }
selectFirst(range) { selectFirst(range: IDBQuery): Promise<T | undefined> {
return this._find(range, () => true, "next"); return this._find(range, () => true, "next");
} }
selectLast(range) { selectLast(range: IDBQuery): Promise<T | undefined> {
return this._find(range, () => true, "prev"); 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"); 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"); return this._find(range, predicate, "prev");
} }
async findMaxKey(range) { async findMaxKey(range: IDBQuery): Promise<IDBValidKey | undefined> {
const cursor = this._target.openKeyCursor(range, "prev"); const cursor = this._target.openKeyCursor(range, "prev");
let maxKey; let maxKey;
await iterateCursor(cursor, (_, key) => { await iterateCursor(cursor, (_, key) => {
maxKey = key; maxKey = key;
return {done: true}; return DONE;
}); });
return maxKey; 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"); 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)}; 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"); const cursor = this._target.openKeyCursor(range, "next");
await iterateCursor(cursor, (_, key, cur) => { await iterateCursor(cursor, (_, key, cur) => {
return {done: callback(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. * 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. * `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 direction = backwards ? "prev" : "next";
const compareKeys = (a, b) => backwards ? -indexedDB.cmp(a, b) : indexedDB.cmp(a, b); const compareKeys = (a, b) => backwards ? -indexedDB.cmp(a, b) : indexedDB.cmp(a, b);
const sortedKeys = keys.slice().sort(compareKeys); const sortedKeys = keys.slice().sort(compareKeys);
@ -154,7 +173,10 @@ export class QueryTarget {
++i; ++i;
} }
const done = consumerDone || i >= sortedKeys.length; const done = consumerDone || i >= sortedKeys.length;
const jumpTo = !done && sortedKeys[i]; let jumpTo;
if (!done) {
jumpTo = sortedKeys[i];
}
return {done, jumpTo}; return {done, jumpTo};
}); });
// report null for keys we didn't to at the end // 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; let reducedValue = initialValue;
const cursor = this._openCursor(range, direction); const cursor = this._openCursor(range, direction);
return iterateCursor(cursor, (value) => { return iterateCursor<T>(cursor, (value) => {
reducedValue = reducer(reducedValue, 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 this._selectUntil(range, (results) => {
return results.length === amount; return results.length === amount;
}, direction); }, 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 cursor = this._openCursor(range, direction);
const results = []; const results: T[] = [];
await iterateCursor(cursor, (value) => { await iterateCursor<T>(cursor, (value) => {
results.push(value); results.push(value);
return {done: predicate(results, 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 // 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 cursor = this._openCursor(range, direction);
const results = []; const results: T[] = [];
await iterateCursor(cursor, (value) => { await iterateCursor<T>(cursor, (value) => {
const passesPredicate = predicate(value); const passesPredicate = predicate(value);
if (passesPredicate) { if (passesPredicate) {
results.push(value); results.push(value);
@ -203,18 +225,18 @@ export class QueryTarget {
return results; return results;
} }
async iterateWhile(range, predicate) { async iterateWhile(range: IDBQuery, predicate: (v: T) => boolean): Promise<void> {
const cursor = this._openCursor(range, "next"); const cursor = this._openCursor(range, "next");
await iterateCursor(cursor, (value) => { await iterateCursor<T>(cursor, (value) => {
const passesPredicate = predicate(value); const passesPredicate = predicate(value);
return {done: !passesPredicate}; 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); const cursor = this._openCursor(range, direction);
let result; let result;
const found = await iterateCursor(cursor, (value) => { const found = await iterateCursor<T>(cursor, (value) => {
const found = predicate(value); const found = predicate(value);
if (found) { if (found) {
result = value; result = value;

View file

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

View file

@ -15,7 +15,7 @@ limitations under the License.
*/ */
import {Storage} from "./Storage.js"; import {Storage} from "./Storage.js";
import { openDatabase, reqAsPromise } from "./utils.js"; import { openDatabase, reqAsPromise } from "./utils";
import { exportSession, importSession } from "./export.js"; import { exportSession, importSession } from "./export.js";
import { schema } from "./schema.js"; import { schema } from "./schema.js";
import { detectWebkitEarlyCloseTxnBug } from "./quirks.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. limitations under the License.
*/ */
import {txnAsPromise} from "./utils.js"; import {txnAsPromise} from "./utils";
import {StorageError} from "../common.js"; import {StorageError} from "../common";
import {Store} from "./Store.js"; import {Store} from "./Store";
import {SessionStore} from "./stores/SessionStore.js"; import {SessionStore} from "./stores/SessionStore.js";
import {RoomSummaryStore} from "./stores/RoomSummaryStore.js"; import {RoomSummaryStore} from "./stores/RoomSummaryStore.js";
import {InviteStore} from "./stores/InviteStore.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. 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 { export class IDBError extends StorageError {
constructor(message, source, cause) { storeName: string;
const storeName = source?.name || "<unknown store>"; databaseName: string;
const databaseName = source?.transaction?.db?.name || "<unknown db>";
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}`; let fullMessage = `${message} on ${databaseName}.${storeName}`;
if (cause) { if (cause) {
fullMessage += ": "; fullMessage += ": ";
@ -41,7 +57,7 @@ export class IDBError extends StorageError {
} }
export class IDBRequestError extends IDBError { export class IDBRequestError extends IDBError {
constructor(request, message = "IDBRequest failed") { constructor(request: IDBRequest, message: string = "IDBRequest failed") {
const source = request.source; const source = request.source;
const cause = request.error; const cause = request.error;
super(message, source, cause); super(message, source, cause);
@ -49,7 +65,7 @@ export class IDBRequestError extends IDBError {
} }
export class IDBRequestAttemptError 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); 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. limitations under the License.
*/ */
import { iterateCursor, txnAsPromise } from "./utils.js"; import { iterateCursor, txnAsPromise } from "./utils";
import { STORE_NAMES } from "../common.js"; import { STORE_NAMES } from "../common";
export async function exportSession(db) { export async function exportSession(db) {
const NOT_DONE = {done: false}; 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 // filed as https://bugs.webkit.org/show_bug.cgi?id=222746
export async function detectWebkitEarlyCloseTxnBug(idbFactory) { 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 {RoomMember, EVENT_TYPE as MEMBER_EVENT_TYPE} from "../../room/members/RoomMember.js";
import {addRoomToIdentity} from "../../e2ee/DeviceTracker.js"; import {addRoomToIdentity} from "../../e2ee/DeviceTracker.js";
import {RoomMemberStore} from "./stores/RoomMemberStore.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. limitations under the License.
*/ */
import { encodeUint32, decodeUint32 } from "../utils.js"; import { encodeUint32, decodeUint32 } from "../utils";
import {KeyLimits} from "../../common.js"; import {KeyLimits} from "../../common";
function encodeKey(roomId, queueIndex) { function encodeKey(roomId, queueIndex) {
return `${roomId}|${encodeUint32(queueIndex)}`; return `${roomId}|${encodeUint32(queueIndex)}`;

View file

@ -15,9 +15,9 @@ limitations under the License.
*/ */
import {EventKey} from "../../../room/timeline/EventKey.js"; import {EventKey} from "../../../room/timeline/EventKey.js";
import { StorageError } from "../../common.js"; import { StorageError } from "../../common";
import { encodeUint32 } from "../utils.js"; import { encodeUint32 } from "../utils";
import {KeyLimits} from "../../common.js"; import {KeyLimits} from "../../common";
function encodeKey(roomId, fragmentId, eventIndex) { function encodeKey(roomId, fragmentId, eventIndex) {
return `${roomId}|${encodeUint32(fragmentId)}|${encodeUint32(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. limitations under the License.
*/ */
import { StorageError } from "../../common.js"; import { StorageError } from "../../common";
import {KeyLimits} from "../../common.js"; import {KeyLimits} from "../../common";
import { encodeUint32 } from "../utils.js"; import { encodeUint32 } from "../utils";
function encodeKey(roomId, fragmentId) { function encodeKey(roomId, fragmentId) {
return `${roomId}|${encodeUint32(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. limitations under the License.
*/ */
import { IDBRequestError } from "./error.js"; import { IDBRequestError } from "./error";
import { StorageError } from "../common.js"; import { StorageError } from "../common";
let needsSyncPromise = false; let needsSyncPromise = false;
export const DONE = { done: true }
export const NOT_DONE = { done: false }
/* should be called on legacy platforms to see /* should be called on legacy platforms to see
if transactions close before draining the microtask queue (IE11 on Windows 7). if transactions close before draining the microtask queue (IE11 on Windows 7).
If this is the case, promises need to be resolved If this is the case, promises need to be resolved
synchronously from the idb request handler to prevent the transaction from closing prematurely. 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, // important to have it turned off while doing the test,
// otherwise reqAsPromise would not fail // otherwise reqAsPromise would not fail
needsSyncPromise = false; 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 // 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); const hex = n.toString(16);
return "0".repeat(8 - hex.length) + hex; return "0".repeat(8 - hex.length) + hex;
} }
// used for logs where timestamp is part of key, which is larger than 32 bit // 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); const hex = n.toString(16);
return "0".repeat(16 - hex.length) + hex; return "0".repeat(16 - hex.length) + hex;
} }
export function decodeUint32(str) { export function decodeUint32(str: string): number {
return parseInt(str, 16); 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); const req = idbFactory.open(name, version);
req.onupgradeneeded = async (ev) => { req.onupgradeneeded = async (ev : IDBVersionChangeEvent) => {
const db = ev.target.result; const req = ev.target as IDBRequest<IDBDatabase>;
const txn = ev.target.transaction; const db = req.result;
const txn = req.transaction!;
const oldVersion = ev.oldVersion; const oldVersion = ev.oldVersion;
try { try {
await createObjectStore(db, txn, oldVersion, version); await createObjectStore(db, txn, oldVersion, version);
@ -82,25 +88,28 @@ export function openDatabase(name, createObjectStore, version, idbFactory = wind
return reqAsPromise(req); return reqAsPromise(req);
} }
export function reqAsPromise(req) { export function reqAsPromise<T>(req: IDBRequest<T>): Promise<T> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
req.addEventListener("success", event => { req.addEventListener("success", event => {
resolve(event.target.result); resolve((event.target as IDBRequest<T>).result);
// @ts-ignore
needsSyncPromise && Promise._flush && Promise._flush(); needsSyncPromise && Promise._flush && Promise._flush();
}); });
req.addEventListener("error", event => { req.addEventListener("error", event => {
const error = new IDBRequestError(event.target); const error = new IDBRequestError(event.target as IDBRequest<T>);
reject(error); reject(error);
// @ts-ignore
needsSyncPromise && Promise._flush && Promise._flush(); needsSyncPromise && Promise._flush && Promise._flush();
}); });
}); });
} }
export function txnAsPromise(txn) { export function txnAsPromise(txn): Promise<void> {
let error; let error;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
txn.addEventListener("complete", () => { txn.addEventListener("complete", () => {
resolve(); resolve();
// @ts-ignore
needsSyncPromise && Promise._flush && Promise._flush(); needsSyncPromise && Promise._flush && Promise._flush();
}); });
txn.addEventListener("error", event => { txn.addEventListener("error", event => {
@ -119,33 +128,56 @@ export function txnAsPromise(txn) {
error = new StorageError(`Transaction on ${dbName} with stores ${storeNames} was aborted.`); error = new StorageError(`Transaction on ${dbName} with stores ${storeNames} was aborted.`);
} }
reject(error); reject(error);
// @ts-ignore
needsSyncPromise && Promise._flush && Promise._flush(); 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?? // TODO: does cursor already have a value here??
return new Promise((resolve, reject) => { return new Promise<boolean>((resolve, reject) => {
cursorRequest.onerror = () => { cursorRequest.onerror = () => {
reject(new IDBRequestError(cursorRequest)); reject(new IDBRequestError(cursorRequest));
// @ts-ignore
needsSyncPromise && Promise._flush && Promise._flush(); needsSyncPromise && Promise._flush && Promise._flush();
}; };
// collect results // collect results
cursorRequest.onsuccess = (event) => { cursorRequest.onsuccess = (event) => {
const cursor = event.target.result; const cursor = (event.target as IDBRequest<I>).result;
if (!cursor) { if (!cursor) {
resolve(false); resolve(false);
// @ts-ignore
needsSyncPromise && Promise._flush && Promise._flush(); needsSyncPromise && Promise._flush && Promise._flush();
return; // end of results 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 // TODO: don't use object for result and assume it's jumpTo when not === true/false or undefined
const done = result?.done; const done = result?.done;
const jumpTo = result?.jumpTo; const jumpTo = result?.jumpTo;
if (done) { if (done) {
resolve(true); resolve(true);
// @ts-ignore
needsSyncPromise && Promise._flush && Promise._flush(); needsSyncPromise && Promise._flush && Promise._flush();
} else if(jumpTo) { } else if(jumpTo) {
cursor.continue(jumpTo); cursor.continue(jumpTo);
@ -158,16 +190,20 @@ export function iterateCursor(cursorRequest, processValue) {
}); });
} }
export async function fetchResults(cursor, isDone) { type Pred<T> = (value: T) => boolean
const results = [];
await iterateCursor(cursor, (value) => { export async function fetchResults<T>(cursor: IDBRequest, isDone: Pred<T[]>): Promise<T[]> {
const results: T[] = [];
await iterateCursor<T>(cursor, (value) => {
results.push(value); results.push(value);
return {done: isDone(results)}; return {done: isDone(results)};
}); });
return 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) { if (!isDone) {
isDone = () => false; isDone = () => false;
} }
@ -180,7 +216,7 @@ export async function select(db, storeName, toCursor, isDone) {
return await fetchResults(cursor, 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) { if (!matchesValue) {
matchesValue = () => true; matchesValue = () => true;
} }
@ -192,11 +228,12 @@ export async function findStoreValue(db, storeName, toCursor, matchesValue) {
const store = tx.objectStore(storeName); const store = tx.objectStore(storeName);
const cursor = await reqAsPromise(toCursor(store)); const cursor = await reqAsPromise(toCursor(store));
let match; let match;
const matched = await iterateCursor(cursor, (value) => { const matched = await iterateCursor<T>(cursor, (value) => {
if (matchesValue(value)) { if (matchesValue(value)) {
match = value; match = value;
return true; return DONE;
} }
return NOT_DONE;
}); });
if (!matched) { if (!matched) {
throw new StorageError("Value not found"); throw new StorageError("Value not found");

View file

@ -16,7 +16,7 @@ limitations under the License.
// polyfills needed for IE11 // polyfills needed for IE11
import Promise from "../../../lib/es6-promise/index.js"; 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") { if (typeof window.Promise === "undefined") {
window.Promise = Promise; window.Promise = Promise;