forked from mystiq/hydrogen-web
Merge pull request #456 from vector-im/snowpack-ts-storage-1
Snowpack + Typescript conversion (Part 1)
This commit is contained in:
commit
cd900ab842
18 changed files with 374 additions and 293 deletions
|
@ -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 {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
|
@ -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;
|
|
@ -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) {
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
174
src/matrix/storage/idb/Store.ts
Normal file
174
src/matrix/storage/idb/Store.ts
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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";
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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};
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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)}`;
|
||||||
|
|
|
@ -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)}`;
|
||||||
|
|
|
@ -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)}`;
|
||||||
|
|
|
@ -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");
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue