Compare commits
70 commits
master
...
snowpack-t
Author | SHA1 | Date | |
---|---|---|---|
|
08e7616897 | ||
|
410bd4ab8b | ||
|
a4375c0e15 | ||
|
3ed639d1c5 | ||
|
b31cf4fdce | ||
|
31b02f1eff | ||
|
c8f4cb5046 | ||
|
1f368dcec1 | ||
|
6c6991e622 | ||
|
fd4ddf5667 | ||
|
f9fa7cdb49 | ||
|
2cb8944a78 | ||
|
9fcc776a29 | ||
|
39e9b828e6 | ||
|
64b767e7eb | ||
|
2eea5d5ab8 | ||
|
287242deda | ||
|
31c951c68c | ||
|
ea4de29975 | ||
|
f390d21881 | ||
|
8c7e13f40f | ||
|
e0aa804971 | ||
|
9e459aa003 | ||
|
61d0108b3b | ||
|
51d8e3cb66 | ||
|
c21b187683 | ||
|
736b122fc7 | ||
|
016b51ba37 | ||
|
3128e072fd | ||
|
2883149086 | ||
|
f4ba26cb1e | ||
|
3213a0baa0 | ||
|
15d7d57b09 | ||
|
279f149408 | ||
|
218bac7883 | ||
|
c5a209258e | ||
|
5fb7871ca6 | ||
|
ee00aa3339 | ||
|
e433a234fe | ||
|
526ff53728 | ||
|
3ec222eae3 | ||
|
7f907427ee | ||
|
243d0e76ff | ||
|
ec38337223 | ||
|
9b4b303b01 | ||
|
50897cfbe3 | ||
|
8e93487ebe | ||
|
ddf09af05b | ||
|
d07c38effd | ||
|
3b5b91cf1b | ||
|
88ecc58b14 | ||
|
53228e88a2 | ||
|
de6fdba526 | ||
|
29c87b7c01 | ||
|
8ad2857c6a | ||
|
f8613e9e96 | ||
|
5177c35d0d | ||
|
97a50c835d | ||
|
2b44878332 | ||
|
46c306b487 | ||
|
e837a91a80 | ||
|
1d5b105c34 | ||
|
fefa15cd85 | ||
|
a31f6a2c52 | ||
|
69ab345a89 | ||
|
a0017cb720 | ||
|
0c80f78e9b | ||
|
4fb93ad104 | ||
|
caed99df69 | ||
|
96a4ef47a7 |
38 changed files with 961 additions and 624 deletions
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {KeyLimits} from "../../storage/common.js";
|
||||
import {KeyLimits} from "../../storage/common";
|
||||
|
||||
// key for events in the timelineEvents store
|
||||
export class EventKey {
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
import {BaseEntry} from "./BaseEntry";
|
||||
import {Direction} from "../Direction.js";
|
||||
import {isValidFragmentId} from "../common.js";
|
||||
import {KeyLimits} from "../../../storage/common.js";
|
||||
import {KeyLimits} from "../../../storage/common";
|
||||
|
||||
export class FragmentBoundaryEntry extends BaseEntry {
|
||||
constructor(fragment, isFragmentStart, fragmentIdComparer) {
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
export const STORE_NAMES = Object.freeze([
|
||||
export const STORE_NAMES: Readonly<string[]> = Object.freeze([
|
||||
"session",
|
||||
"roomState",
|
||||
"roomSummary",
|
||||
|
@ -35,13 +35,16 @@ export const STORE_NAMES = Object.freeze([
|
|||
"accountData",
|
||||
]);
|
||||
|
||||
export const STORE_MAP = Object.freeze(STORE_NAMES.reduce((nameMap, name) => {
|
||||
export const STORE_MAP: Readonly<{ [name : string]: string }> = Object.freeze(STORE_NAMES.reduce((nameMap, name) => {
|
||||
nameMap[name] = name;
|
||||
return nameMap;
|
||||
}, {}));
|
||||
|
||||
export class StorageError extends Error {
|
||||
constructor(message, cause) {
|
||||
errcode?: string;
|
||||
cause?: Error;
|
||||
|
||||
constructor(message: string, cause?: Error) {
|
||||
super(message);
|
||||
if (cause) {
|
||||
this.errcode = cause.name;
|
||||
|
@ -49,23 +52,23 @@ export class StorageError extends Error {
|
|||
this.cause = cause;
|
||||
}
|
||||
|
||||
get name() {
|
||||
get name(): string {
|
||||
return "StorageError";
|
||||
}
|
||||
}
|
||||
|
||||
export const KeyLimits = {
|
||||
get minStorageKey() {
|
||||
get minStorageKey(): number {
|
||||
// for indexeddb, we use unsigned 32 bit integers as keys
|
||||
return 0;
|
||||
},
|
||||
|
||||
get middleStorageKey() {
|
||||
get middleStorageKey(): number {
|
||||
// for indexeddb, we use unsigned 32 bit integers as keys
|
||||
return 0x7FFFFFFF;
|
||||
},
|
||||
|
||||
get maxStorageKey() {
|
||||
get maxStorageKey(): number {
|
||||
// for indexeddb, we use unsigned 32 bit integers as keys
|
||||
return 0xFFFFFFFF;
|
||||
}
|
|
@ -14,14 +14,29 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {iterateCursor, reqAsPromise} from "./utils.js";
|
||||
import {iterateCursor, DONE, NOT_DONE, reqAsPromise} from "./utils";
|
||||
|
||||
export class QueryTarget {
|
||||
constructor(target) {
|
||||
type Reducer<A,B> = (acc: B, val: A) => B
|
||||
|
||||
export type IDBQuery = IDBValidKey | IDBKeyRange | undefined | null
|
||||
|
||||
interface QueryTargetInterface<T> {
|
||||
openCursor: (range?: IDBQuery, direction?: IDBCursorDirection | undefined) => IDBRequest<IDBCursorWithValue | null>;
|
||||
openKeyCursor: (range?: IDBQuery, direction?: IDBCursorDirection | undefined) => IDBRequest<IDBCursor | null>;
|
||||
supports: (method: string) => boolean;
|
||||
keyPath: string | string[];
|
||||
get: (key: IDBValidKey | IDBKeyRange) => IDBRequest<T | null>;
|
||||
getKey: (key: IDBValidKey | IDBKeyRange) => IDBRequest<IDBValidKey | undefined>;
|
||||
}
|
||||
|
||||
export class QueryTarget<T> {
|
||||
protected _target: QueryTargetInterface<T>;
|
||||
|
||||
constructor(target: QueryTargetInterface<T>) {
|
||||
this._target = target;
|
||||
}
|
||||
|
||||
_openCursor(range, direction) {
|
||||
_openCursor(range?: IDBQuery, direction?: IDBCursorDirection): IDBRequest<IDBCursorWithValue | null> {
|
||||
if (range && direction) {
|
||||
return this._target.openCursor(range, direction);
|
||||
} else if (range) {
|
||||
|
@ -33,95 +48,99 @@ export class QueryTarget {
|
|||
}
|
||||
}
|
||||
|
||||
supports(methodName) {
|
||||
supports(methodName: string): boolean {
|
||||
return this._target.supports(methodName);
|
||||
}
|
||||
|
||||
get(key) {
|
||||
get(key: IDBValidKey | IDBKeyRange): Promise<T | null> {
|
||||
return reqAsPromise(this._target.get(key));
|
||||
}
|
||||
|
||||
getKey(key) {
|
||||
getKey(key: IDBValidKey | IDBKeyRange): Promise<IDBValidKey | undefined> {
|
||||
if (this._target.supports("getKey")) {
|
||||
return reqAsPromise(this._target.getKey(key));
|
||||
} else {
|
||||
return reqAsPromise(this._target.get(key)).then(value => {
|
||||
if (value) {
|
||||
return value[this._target.keyPath];
|
||||
let keyPath = this._target.keyPath;
|
||||
if (typeof keyPath === "string") {
|
||||
keyPath = [keyPath];
|
||||
}
|
||||
return keyPath.reduce((obj, key) => obj[key], value);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
reduce(range, reducer, initialValue) {
|
||||
reduce<B>(range: IDBQuery, reducer: Reducer<T,B>, initialValue: B): Promise<boolean> {
|
||||
return this._reduce(range, reducer, initialValue, "next");
|
||||
}
|
||||
|
||||
reduceReverse(range, reducer, initialValue) {
|
||||
reduceReverse<B>(range: IDBQuery, reducer: Reducer<T,B>, initialValue: B): Promise<boolean> {
|
||||
return this._reduce(range, reducer, initialValue, "prev");
|
||||
}
|
||||
|
||||
selectLimit(range, amount) {
|
||||
selectLimit(range: IDBQuery, amount: number): Promise<T[]> {
|
||||
return this._selectLimit(range, amount, "next");
|
||||
}
|
||||
|
||||
selectLimitReverse(range, amount) {
|
||||
selectLimitReverse(range: IDBQuery, amount: number): Promise<T[]> {
|
||||
return this._selectLimit(range, amount, "prev");
|
||||
}
|
||||
|
||||
selectWhile(range, predicate) {
|
||||
selectWhile(range: IDBQuery, predicate: (v: T) => boolean): Promise<T[]> {
|
||||
return this._selectWhile(range, predicate, "next");
|
||||
}
|
||||
|
||||
selectWhileReverse(range, predicate) {
|
||||
selectWhileReverse(range: IDBQuery, predicate: (v: T) => boolean): Promise<T[]> {
|
||||
return this._selectWhile(range, predicate, "prev");
|
||||
}
|
||||
|
||||
async selectAll(range, direction) {
|
||||
async selectAll(range?: IDBQuery, direction?: IDBCursorDirection): Promise<T[]> {
|
||||
const cursor = this._openCursor(range, direction);
|
||||
const results = [];
|
||||
await iterateCursor(cursor, (value) => {
|
||||
const results: T[] = [];
|
||||
await iterateCursor<T>(cursor, (value) => {
|
||||
results.push(value);
|
||||
return {done: false};
|
||||
return NOT_DONE;
|
||||
});
|
||||
return results;
|
||||
}
|
||||
|
||||
selectFirst(range) {
|
||||
selectFirst(range: IDBQuery): Promise<T | undefined> {
|
||||
return this._find(range, () => true, "next");
|
||||
}
|
||||
|
||||
selectLast(range) {
|
||||
selectLast(range: IDBQuery): Promise<T | undefined> {
|
||||
return this._find(range, () => true, "prev");
|
||||
}
|
||||
|
||||
find(range, predicate) {
|
||||
find(range: IDBQuery, predicate: (v: T) => boolean): Promise<T | undefined> {
|
||||
return this._find(range, predicate, "next");
|
||||
}
|
||||
|
||||
findReverse(range, predicate) {
|
||||
findReverse(range: IDBQuery, predicate: (v : T) => boolean): Promise<T | undefined> {
|
||||
return this._find(range, predicate, "prev");
|
||||
}
|
||||
|
||||
async findMaxKey(range) {
|
||||
async findMaxKey(range: IDBQuery): Promise<IDBValidKey | undefined> {
|
||||
const cursor = this._target.openKeyCursor(range, "prev");
|
||||
let maxKey;
|
||||
let maxKey: IDBValidKey | undefined;
|
||||
await iterateCursor(cursor, (_, key) => {
|
||||
maxKey = key;
|
||||
return {done: true};
|
||||
return DONE;
|
||||
});
|
||||
return maxKey;
|
||||
}
|
||||
|
||||
|
||||
async iterateValues(range, callback) {
|
||||
async iterateValues(range: IDBQuery, callback: (val: T, key: IDBValidKey, cur: IDBCursorWithValue) => boolean): Promise<void> {
|
||||
const cursor = this._target.openCursor(range, "next");
|
||||
await iterateCursor(cursor, (value, key, cur) => {
|
||||
await iterateCursor<T>(cursor, (value, key, cur) => {
|
||||
return {done: callback(value, key, cur)};
|
||||
});
|
||||
}
|
||||
|
||||
async iterateKeys(range, callback) {
|
||||
async iterateKeys(range: IDBQuery, callback: (key: IDBValidKey, cur: IDBCursor) => boolean): Promise<void> {
|
||||
const cursor = this._target.openKeyCursor(range, "next");
|
||||
await iterateCursor(cursor, (_, key, cur) => {
|
||||
return {done: callback(key, cur)};
|
||||
|
@ -134,7 +153,7 @@ export class QueryTarget {
|
|||
* If the callback returns true, the search is halted and callback won't be called again.
|
||||
* `callback` is called with the same instances of the key as given in `keys`, so direct comparison can be used.
|
||||
*/
|
||||
async findExistingKeys(keys, backwards, callback) {
|
||||
async findExistingKeys(keys: IDBValidKey[], backwards: boolean, callback: (key: IDBValidKey, found: boolean) => boolean): Promise<void> {
|
||||
const direction = backwards ? "prev" : "next";
|
||||
const compareKeys = (a, b) => backwards ? -indexedDB.cmp(a, b) : indexedDB.cmp(a, b);
|
||||
const sortedKeys = keys.slice().sort(compareKeys);
|
||||
|
@ -154,7 +173,10 @@ export class QueryTarget {
|
|||
++i;
|
||||
}
|
||||
const done = consumerDone || i >= sortedKeys.length;
|
||||
const jumpTo = !done && sortedKeys[i];
|
||||
let jumpTo;
|
||||
if (!done) {
|
||||
jumpTo = sortedKeys[i];
|
||||
}
|
||||
return {done, jumpTo};
|
||||
});
|
||||
// report null for keys we didn't to at the end
|
||||
|
@ -164,25 +186,25 @@ export class QueryTarget {
|
|||
}
|
||||
}
|
||||
|
||||
_reduce(range, reducer, initialValue, direction) {
|
||||
_reduce<B>(range: IDBQuery, reducer: (reduced: B, value: T) => B, initialValue: B, direction: IDBCursorDirection): Promise<boolean> {
|
||||
let reducedValue = initialValue;
|
||||
const cursor = this._openCursor(range, direction);
|
||||
return iterateCursor(cursor, (value) => {
|
||||
return iterateCursor<T>(cursor, (value) => {
|
||||
reducedValue = reducer(reducedValue, value);
|
||||
return {done: false};
|
||||
return NOT_DONE;
|
||||
});
|
||||
}
|
||||
|
||||
_selectLimit(range, amount, direction) {
|
||||
_selectLimit(range: IDBQuery, amount: number, direction: IDBCursorDirection): Promise<T[]> {
|
||||
return this._selectUntil(range, (results) => {
|
||||
return results.length === amount;
|
||||
}, direction);
|
||||
}
|
||||
|
||||
async _selectUntil(range, predicate, direction) {
|
||||
async _selectUntil(range: IDBQuery, predicate: (vs: T[], v: T) => boolean, direction: IDBCursorDirection): Promise<T[]> {
|
||||
const cursor = this._openCursor(range, direction);
|
||||
const results = [];
|
||||
await iterateCursor(cursor, (value) => {
|
||||
const results: T[] = [];
|
||||
await iterateCursor<T>(cursor, (value) => {
|
||||
results.push(value);
|
||||
return {done: predicate(results, value)};
|
||||
});
|
||||
|
@ -190,10 +212,10 @@ export class QueryTarget {
|
|||
}
|
||||
|
||||
// allows you to fetch one too much that won't get added when the predicate fails
|
||||
async _selectWhile(range, predicate, direction) {
|
||||
async _selectWhile(range: IDBQuery, predicate: (v: T) => boolean, direction: IDBCursorDirection): Promise<T[]> {
|
||||
const cursor = this._openCursor(range, direction);
|
||||
const results = [];
|
||||
await iterateCursor(cursor, (value) => {
|
||||
const results: T[] = [];
|
||||
await iterateCursor<T>(cursor, (value) => {
|
||||
const passesPredicate = predicate(value);
|
||||
if (passesPredicate) {
|
||||
results.push(value);
|
||||
|
@ -203,18 +225,18 @@ export class QueryTarget {
|
|||
return results;
|
||||
}
|
||||
|
||||
async iterateWhile(range, predicate) {
|
||||
async iterateWhile(range: IDBQuery, predicate: (v: T) => boolean): Promise<void> {
|
||||
const cursor = this._openCursor(range, "next");
|
||||
await iterateCursor(cursor, (value) => {
|
||||
await iterateCursor<T>(cursor, (value) => {
|
||||
const passesPredicate = predicate(value);
|
||||
return {done: !passesPredicate};
|
||||
});
|
||||
}
|
||||
|
||||
async _find(range, predicate, direction) {
|
||||
async _find(range: IDBQuery, predicate: (v: T) => boolean, direction: IDBCursorDirection): Promise<T | undefined> {
|
||||
const cursor = this._openCursor(range, direction);
|
||||
let result;
|
||||
const found = await iterateCursor(cursor, (value) => {
|
||||
const found = await iterateCursor<T>(cursor, (value) => {
|
||||
const found = predicate(value);
|
||||
if (found) {
|
||||
result = value;
|
|
@ -14,15 +14,20 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {Transaction} from "./Transaction.js";
|
||||
import { STORE_NAMES, StorageError } from "../common.js";
|
||||
import { reqAsPromise } from "./utils.js";
|
||||
import {Transaction} from "./Transaction";
|
||||
import { STORE_NAMES, StorageError } from "../common";
|
||||
import { reqAsPromise } from "./utils";
|
||||
|
||||
const WEBKITEARLYCLOSETXNBUG_BOGUS_KEY = "782rh281re38-boguskey";
|
||||
|
||||
export class Storage {
|
||||
constructor(idbDatabase, IDBKeyRange, hasWebkitEarlyCloseTxnBug) {
|
||||
private _db: IDBDatabase;
|
||||
private _hasWebkitEarlyCloseTxnBug: boolean;
|
||||
storeNames: { [ name : string] : string };
|
||||
|
||||
constructor(idbDatabase: IDBDatabase, IDBKeyRange, hasWebkitEarlyCloseTxnBug: boolean) {
|
||||
this._db = idbDatabase;
|
||||
// @ts-ignore
|
||||
this._IDBKeyRange = IDBKeyRange;
|
||||
this._hasWebkitEarlyCloseTxnBug = hasWebkitEarlyCloseTxnBug;
|
||||
const nameMap = STORE_NAMES.reduce((nameMap, name) => {
|
||||
|
@ -32,14 +37,14 @@ export class Storage {
|
|||
this.storeNames = Object.freeze(nameMap);
|
||||
}
|
||||
|
||||
_validateStoreNames(storeNames) {
|
||||
_validateStoreNames(storeNames: string[]): void {
|
||||
const idx = storeNames.findIndex(name => !STORE_NAMES.includes(name));
|
||||
if (idx !== -1) {
|
||||
throw new StorageError(`Tried top, a transaction unknown store ${storeNames[idx]}`);
|
||||
}
|
||||
}
|
||||
|
||||
async readTxn(storeNames) {
|
||||
async readTxn(storeNames: string[]): Promise<Transaction> {
|
||||
this._validateStoreNames(storeNames);
|
||||
try {
|
||||
const txn = this._db.transaction(storeNames, "readonly");
|
||||
|
@ -48,13 +53,14 @@ export class Storage {
|
|||
if (this._hasWebkitEarlyCloseTxnBug) {
|
||||
await reqAsPromise(txn.objectStore(storeNames[0]).get(WEBKITEARLYCLOSETXNBUG_BOGUS_KEY));
|
||||
}
|
||||
// @ts-ignore
|
||||
return new Transaction(txn, storeNames, this._IDBKeyRange);
|
||||
} catch(err) {
|
||||
throw new StorageError("readTxn failed", err);
|
||||
}
|
||||
}
|
||||
|
||||
async readWriteTxn(storeNames) {
|
||||
async readWriteTxn(storeNames: string[]): Promise<Transaction> {
|
||||
this._validateStoreNames(storeNames);
|
||||
try {
|
||||
const txn = this._db.transaction(storeNames, "readwrite");
|
||||
|
@ -63,13 +69,14 @@ export class Storage {
|
|||
if (this._hasWebkitEarlyCloseTxnBug) {
|
||||
await reqAsPromise(txn.objectStore(storeNames[0]).get(WEBKITEARLYCLOSETXNBUG_BOGUS_KEY));
|
||||
}
|
||||
// @ts-ignore
|
||||
return new Transaction(txn, storeNames, this._IDBKeyRange);
|
||||
} catch(err) {
|
||||
throw new StorageError("readWriteTxn failed", err);
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
close(): void {
|
||||
this._db.close();
|
||||
}
|
||||
}
|
|
@ -14,18 +14,22 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {Storage} from "./Storage.js";
|
||||
import { openDatabase, reqAsPromise } from "./utils.js";
|
||||
import { exportSession, importSession } from "./export.js";
|
||||
import { schema } from "./schema.js";
|
||||
import { detectWebkitEarlyCloseTxnBug } from "./quirks.js";
|
||||
import {Storage} from "./Storage";
|
||||
import { openDatabase, reqAsPromise } from "./utils";
|
||||
import { exportSession, importSession } from "./export";
|
||||
import { schema } from "./schema";
|
||||
import { detectWebkitEarlyCloseTxnBug } from "./quirks";
|
||||
|
||||
const sessionName = sessionId => `hydrogen_session_${sessionId}`;
|
||||
const openDatabaseWithSessionId = function(sessionId, idbFactory) {
|
||||
const sessionName = (sessionId: string) => `hydrogen_session_${sessionId}`;
|
||||
const openDatabaseWithSessionId = function(sessionId: string, idbFactory: IDBFactory): Promise<IDBDatabase> {
|
||||
return openDatabase(sessionName(sessionId), createStores, schema.length, idbFactory);
|
||||
}
|
||||
|
||||
async function requestPersistedStorage() {
|
||||
interface ServiceWorkerHandler {
|
||||
preventConcurrentSessionAccess: (sessionId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
async function requestPersistedStorage(): Promise<boolean> {
|
||||
// don't assume browser so we can run in node with fake-idb
|
||||
const glob = this;
|
||||
if (glob?.navigator?.storage?.persist) {
|
||||
|
@ -43,13 +47,17 @@ async function requestPersistedStorage() {
|
|||
}
|
||||
|
||||
export class StorageFactory {
|
||||
constructor(serviceWorkerHandler, idbFactory = window.indexedDB, IDBKeyRange = window.IDBKeyRange) {
|
||||
private _serviceWorkerHandler: ServiceWorkerHandler;
|
||||
private _idbFactory: IDBFactory;
|
||||
|
||||
constructor(serviceWorkerHandler: ServiceWorkerHandler, idbFactory: IDBFactory = window.indexedDB, IDBKeyRange = window.IDBKeyRange) {
|
||||
this._serviceWorkerHandler = serviceWorkerHandler;
|
||||
this._idbFactory = idbFactory;
|
||||
// @ts-ignore
|
||||
this._IDBKeyRange = IDBKeyRange;
|
||||
}
|
||||
|
||||
async create(sessionId) {
|
||||
async create(sessionId: string): Promise<Storage> {
|
||||
await this._serviceWorkerHandler?.preventConcurrentSessionAccess(sessionId);
|
||||
requestPersistedStorage().then(persisted => {
|
||||
// Firefox lies here though, and returns true even if the user denied the request
|
||||
|
@ -60,27 +68,28 @@ export class StorageFactory {
|
|||
|
||||
const hasWebkitEarlyCloseTxnBug = await detectWebkitEarlyCloseTxnBug(this._idbFactory);
|
||||
const db = await openDatabaseWithSessionId(sessionId, this._idbFactory);
|
||||
// @ts-ignore
|
||||
return new Storage(db, this._IDBKeyRange, hasWebkitEarlyCloseTxnBug);
|
||||
}
|
||||
|
||||
delete(sessionId) {
|
||||
delete(sessionId: string): Promise<IDBDatabase> {
|
||||
const databaseName = sessionName(sessionId);
|
||||
const req = this._idbFactory.deleteDatabase(databaseName);
|
||||
return reqAsPromise(req);
|
||||
}
|
||||
|
||||
async export(sessionId) {
|
||||
async export(sessionId: string): Promise<{ [storeName: string]: any }> {
|
||||
const db = await openDatabaseWithSessionId(sessionId, this._idbFactory);
|
||||
return await exportSession(db);
|
||||
}
|
||||
|
||||
async import(sessionId, data) {
|
||||
async import(sessionId: string, data: { [storeName: string]: any }): Promise<void> {
|
||||
const db = await openDatabaseWithSessionId(sessionId, this._idbFactory);
|
||||
return await importSession(db, data);
|
||||
}
|
||||
}
|
||||
|
||||
async function createStores(db, txn, oldVersion, version) {
|
||||
async function createStores(db: IDBDatabase, txn: IDBTransaction, oldVersion: number | null, version: number): Promise<void> {
|
||||
const startIdx = oldVersion || 0;
|
||||
|
||||
for(let i = startIdx; i < version; ++i) {
|
|
@ -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);
|
||||
}
|
||||
}
|
175
src/matrix/storage/idb/Store.ts
Normal file
175
src/matrix/storage/idb/Store.ts
Normal file
|
@ -0,0 +1,175 @@
|
|||
/*
|
||||
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[] {
|
||||
if (this._qt["objectStore"]) {
|
||||
return (this._qt as IDBIndex).objectStore.keyPath;
|
||||
} else {
|
||||
return this._qt.keyPath;
|
||||
}
|
||||
}
|
||||
|
||||
get _qtStore(): IDBObjectStore {
|
||||
return this._qt as IDBObjectStore;
|
||||
}
|
||||
|
||||
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,36 +14,41 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {txnAsPromise} from "./utils.js";
|
||||
import {StorageError} from "../common.js";
|
||||
import {Store} from "./Store.js";
|
||||
import {SessionStore} from "./stores/SessionStore.js";
|
||||
import {RoomSummaryStore} from "./stores/RoomSummaryStore.js";
|
||||
import {InviteStore} from "./stores/InviteStore.js";
|
||||
import {TimelineEventStore} from "./stores/TimelineEventStore.js";
|
||||
import {TimelineRelationStore} from "./stores/TimelineRelationStore.js";
|
||||
import {RoomStateStore} from "./stores/RoomStateStore.js";
|
||||
import {RoomMemberStore} from "./stores/RoomMemberStore.js";
|
||||
import {TimelineFragmentStore} from "./stores/TimelineFragmentStore.js";
|
||||
import {PendingEventStore} from "./stores/PendingEventStore.js";
|
||||
import {UserIdentityStore} from "./stores/UserIdentityStore.js";
|
||||
import {DeviceIdentityStore} from "./stores/DeviceIdentityStore.js";
|
||||
import {OlmSessionStore} from "./stores/OlmSessionStore.js";
|
||||
import {InboundGroupSessionStore} from "./stores/InboundGroupSessionStore.js";
|
||||
import {OutboundGroupSessionStore} from "./stores/OutboundGroupSessionStore.js";
|
||||
import {GroupSessionDecryptionStore} from "./stores/GroupSessionDecryptionStore.js";
|
||||
import {OperationStore} from "./stores/OperationStore.js";
|
||||
import {AccountDataStore} from "./stores/AccountDataStore.js";
|
||||
import {txnAsPromise} from "./utils";
|
||||
import {StorageError} from "../common";
|
||||
import {Store} from "./Store";
|
||||
import {SessionStore} from "./stores/SessionStore";
|
||||
import {RoomSummaryStore} from "./stores/RoomSummaryStore";
|
||||
import {InviteStore} from "./stores/InviteStore";
|
||||
import {TimelineEventStore} from "./stores/TimelineEventStore";
|
||||
import {TimelineRelationStore} from "./stores/TimelineRelationStore";
|
||||
import {RoomStateStore} from "./stores/RoomStateStore";
|
||||
import {RoomMemberStore} from "./stores/RoomMemberStore";
|
||||
import {TimelineFragmentStore} from "./stores/TimelineFragmentStore";
|
||||
import {PendingEventStore} from "./stores/PendingEventStore";
|
||||
import {UserIdentityStore} from "./stores/UserIdentityStore";
|
||||
import {DeviceIdentityStore} from "./stores/DeviceIdentityStore";
|
||||
import {OlmSessionStore} from "./stores/OlmSessionStore";
|
||||
import {InboundGroupSessionStore} from "./stores/InboundGroupSessionStore";
|
||||
import {OutboundGroupSessionStore} from "./stores/OutboundGroupSessionStore";
|
||||
import {GroupSessionDecryptionStore} from "./stores/GroupSessionDecryptionStore";
|
||||
import {OperationStore} from "./stores/OperationStore";
|
||||
import {AccountDataStore} from "./stores/AccountDataStore";
|
||||
|
||||
export class Transaction {
|
||||
constructor(txn, allowedStoreNames, IDBKeyRange) {
|
||||
private _txn: IDBTransaction;
|
||||
private _allowedStoreNames: string[];
|
||||
private _stores: { [storeName : string] : any };
|
||||
|
||||
constructor(txn: IDBTransaction, allowedStoreNames: string[], IDBKeyRange) {
|
||||
this._txn = txn;
|
||||
this._allowedStoreNames = allowedStoreNames;
|
||||
this._stores = {};
|
||||
// @ts-ignore
|
||||
this.IDBKeyRange = IDBKeyRange;
|
||||
}
|
||||
|
||||
_idbStore(name) {
|
||||
_idbStore(name: string): Store<any> {
|
||||
if (!this._allowedStoreNames.includes(name)) {
|
||||
// more specific error? this is a bug, so maybe not ...
|
||||
throw new StorageError(`Invalid store for transaction: ${name}, only ${this._allowedStoreNames.join(", ")} are allowed.`);
|
||||
|
@ -51,7 +56,7 @@ export class Transaction {
|
|||
return new Store(this._txn.objectStore(name), this);
|
||||
}
|
||||
|
||||
_store(name, mapStore) {
|
||||
_store(name: string, mapStore: (idbStore: Store<any>) => any): any {
|
||||
if (!this._stores[name]) {
|
||||
const idbStore = this._idbStore(name);
|
||||
this._stores[name] = mapStore(idbStore);
|
||||
|
@ -59,83 +64,83 @@ export class Transaction {
|
|||
return this._stores[name];
|
||||
}
|
||||
|
||||
get session() {
|
||||
get session(): SessionStore {
|
||||
return this._store("session", idbStore => new SessionStore(idbStore));
|
||||
}
|
||||
|
||||
get roomSummary() {
|
||||
get roomSummary(): RoomSummaryStore {
|
||||
return this._store("roomSummary", idbStore => new RoomSummaryStore(idbStore));
|
||||
}
|
||||
|
||||
get archivedRoomSummary() {
|
||||
get archivedRoomSummary(): RoomSummaryStore {
|
||||
return this._store("archivedRoomSummary", idbStore => new RoomSummaryStore(idbStore));
|
||||
}
|
||||
|
||||
get invites() {
|
||||
get invites(): InviteStore {
|
||||
return this._store("invites", idbStore => new InviteStore(idbStore));
|
||||
}
|
||||
|
||||
get timelineFragments() {
|
||||
get timelineFragments(): TimelineFragmentStore {
|
||||
return this._store("timelineFragments", idbStore => new TimelineFragmentStore(idbStore));
|
||||
}
|
||||
|
||||
get timelineEvents() {
|
||||
get timelineEvents(): TimelineEventStore {
|
||||
return this._store("timelineEvents", idbStore => new TimelineEventStore(idbStore));
|
||||
}
|
||||
|
||||
get timelineRelations() {
|
||||
get timelineRelations(): TimelineRelationStore {
|
||||
return this._store("timelineRelations", idbStore => new TimelineRelationStore(idbStore));
|
||||
}
|
||||
|
||||
get roomState() {
|
||||
get roomState(): RoomStateStore {
|
||||
return this._store("roomState", idbStore => new RoomStateStore(idbStore));
|
||||
}
|
||||
|
||||
get roomMembers() {
|
||||
get roomMembers(): RoomMemberStore {
|
||||
return this._store("roomMembers", idbStore => new RoomMemberStore(idbStore));
|
||||
}
|
||||
|
||||
get pendingEvents() {
|
||||
get pendingEvents(): PendingEventStore {
|
||||
return this._store("pendingEvents", idbStore => new PendingEventStore(idbStore));
|
||||
}
|
||||
|
||||
get userIdentities() {
|
||||
get userIdentities(): UserIdentityStore {
|
||||
return this._store("userIdentities", idbStore => new UserIdentityStore(idbStore));
|
||||
}
|
||||
|
||||
get deviceIdentities() {
|
||||
get deviceIdentities(): DeviceIdentityStore {
|
||||
return this._store("deviceIdentities", idbStore => new DeviceIdentityStore(idbStore));
|
||||
}
|
||||
|
||||
get olmSessions() {
|
||||
get olmSessions(): OlmSessionStore {
|
||||
return this._store("olmSessions", idbStore => new OlmSessionStore(idbStore));
|
||||
}
|
||||
|
||||
get inboundGroupSessions() {
|
||||
get inboundGroupSessions(): InboundGroupSessionStore {
|
||||
return this._store("inboundGroupSessions", idbStore => new InboundGroupSessionStore(idbStore));
|
||||
}
|
||||
|
||||
get outboundGroupSessions() {
|
||||
get outboundGroupSessions(): OutboundGroupSessionStore {
|
||||
return this._store("outboundGroupSessions", idbStore => new OutboundGroupSessionStore(idbStore));
|
||||
}
|
||||
|
||||
get groupSessionDecryptions() {
|
||||
get groupSessionDecryptions(): GroupSessionDecryptionStore {
|
||||
return this._store("groupSessionDecryptions", idbStore => new GroupSessionDecryptionStore(idbStore));
|
||||
}
|
||||
|
||||
get operations() {
|
||||
get operations(): OperationStore {
|
||||
return this._store("operations", idbStore => new OperationStore(idbStore));
|
||||
}
|
||||
|
||||
get accountData() {
|
||||
get accountData(): AccountDataStore {
|
||||
return this._store("accountData", idbStore => new AccountDataStore(idbStore));
|
||||
}
|
||||
|
||||
complete() {
|
||||
complete(): Promise<void> {
|
||||
return txnAsPromise(this._txn);
|
||||
}
|
||||
|
||||
abort() {
|
||||
abort(): void {
|
||||
// TODO: should we wrap the exception in a StorageError?
|
||||
this._txn.abort();
|
||||
}
|
|
@ -15,10 +15,13 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { StorageError } from "../common.js";
|
||||
import { StorageError } from "../common";
|
||||
|
||||
export class IDBError extends StorageError {
|
||||
constructor(message, source, cause) {
|
||||
storeName: string;
|
||||
databaseName: string;
|
||||
|
||||
constructor(message: string, source, cause: DOMException | null) {
|
||||
const storeName = source?.name || "<unknown store>";
|
||||
const databaseName = source?.transaction?.db?.name || "<unknown db>";
|
||||
let fullMessage = `${message} on ${databaseName}.${storeName}`;
|
||||
|
@ -34,14 +37,14 @@ export class IDBError extends StorageError {
|
|||
if (cause) {
|
||||
fullMessage += cause.message;
|
||||
}
|
||||
super(fullMessage, cause);
|
||||
super(fullMessage, cause || undefined);
|
||||
this.storeName = storeName;
|
||||
this.databaseName = databaseName;
|
||||
}
|
||||
}
|
||||
|
||||
export class IDBRequestError extends IDBError {
|
||||
constructor(request, message = "IDBRequest failed") {
|
||||
constructor(request: IDBRequest, message: string = "IDBRequest failed") {
|
||||
const source = request.source;
|
||||
const cause = request.error;
|
||||
super(message, source, cause);
|
||||
|
@ -49,7 +52,7 @@ export class IDBRequestError extends IDBError {
|
|||
}
|
||||
|
||||
export class IDBRequestAttemptError extends IDBError {
|
||||
constructor(method, source, cause, params) {
|
||||
constructor(method: string, source, cause: DOMException, params: any[]) {
|
||||
super(`${method}(${params.map(p => JSON.stringify(p)).join(", ")}) failed`, source, cause);
|
||||
}
|
||||
}
|
|
@ -14,17 +14,16 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { iterateCursor, txnAsPromise } from "./utils.js";
|
||||
import { STORE_NAMES } from "../common.js";
|
||||
import { iterateCursor, NOT_DONE, txnAsPromise } from "./utils";
|
||||
import { STORE_NAMES } from "../common";
|
||||
|
||||
export async function exportSession(db) {
|
||||
const NOT_DONE = {done: false};
|
||||
export async function exportSession(db: IDBDatabase): Promise<{ [storeName : string] : any }> {
|
||||
const txn = db.transaction(STORE_NAMES, "readonly");
|
||||
const data = {};
|
||||
await Promise.all(STORE_NAMES.map(async name => {
|
||||
const results = data[name] = []; // initialize in deterministic order
|
||||
const results: any[] = data[name] = []; // initialize in deterministic order
|
||||
const store = txn.objectStore(name);
|
||||
await iterateCursor(store.openCursor(), (value) => {
|
||||
await iterateCursor<any>(store.openCursor(), (value) => {
|
||||
results.push(value);
|
||||
return NOT_DONE;
|
||||
});
|
||||
|
@ -32,7 +31,7 @@ export async function exportSession(db) {
|
|||
return data;
|
||||
}
|
||||
|
||||
export async function importSession(db, data) {
|
||||
export async function importSession(db: IDBDatabase, data: { [storeName: string]: any }): Promise<void> {
|
||||
const txn = db.transaction(STORE_NAMES, "readwrite");
|
||||
for (const name of STORE_NAMES) {
|
||||
const store = txn.objectStore(name);
|
|
@ -15,10 +15,10 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
|
||||
import {openDatabase, txnAsPromise, reqAsPromise} from "./utils.js";
|
||||
import {openDatabase, txnAsPromise, reqAsPromise} from "./utils";
|
||||
|
||||
// filed as https://bugs.webkit.org/show_bug.cgi?id=222746
|
||||
export async function detectWebkitEarlyCloseTxnBug(idbFactory) {
|
||||
export async function detectWebkitEarlyCloseTxnBug(idbFactory: IDBFactory): Promise<boolean> {
|
||||
const dbName = "hydrogen_webkit_test_inactive_txn_bug";
|
||||
try {
|
||||
const db = await openDatabase(dbName, db => {
|
|
@ -1,8 +1,9 @@
|
|||
import {iterateCursor, reqAsPromise} from "./utils.js";
|
||||
import {iterateCursor, NOT_DONE, reqAsPromise} from "./utils";
|
||||
import {RoomMember, EVENT_TYPE as MEMBER_EVENT_TYPE} from "../../room/members/RoomMember.js";
|
||||
import {RoomMemberStore} from "./stores/RoomMemberStore.js";
|
||||
import {SessionStore} from "./stores/SessionStore.js";
|
||||
import {encodeScopeTypeKey} from "./stores/OperationStore.js";
|
||||
import {RoomMemberStore} from "./stores/RoomMemberStore";
|
||||
import {RoomStateEntry} from "./stores/RoomStateStore";
|
||||
import {SessionStore} from "./stores/SessionStore";
|
||||
import {encodeScopeTypeKey} from "./stores/OperationStore";
|
||||
|
||||
// FUNCTIONS SHOULD ONLY BE APPENDED!!
|
||||
// the index in the array is the database version
|
||||
|
@ -20,10 +21,12 @@ export const schema = [
|
|||
];
|
||||
// TODO: how to deal with git merge conflicts of this array?
|
||||
|
||||
// TypeScript note: for now, do not bother introducing interfaces / alias
|
||||
// for old schemas. Just take them as `any`.
|
||||
|
||||
// how do we deal with schema updates vs existing data migration in a way that
|
||||
//v1
|
||||
function createInitialStores(db) {
|
||||
function createInitialStores(db: IDBDatabase): void {
|
||||
db.createObjectStore("session", {keyPath: "key"});
|
||||
// any way to make keys unique here? (just use put?)
|
||||
db.createObjectStore("roomSummary", {keyPath: "roomId"});
|
||||
|
@ -40,11 +43,12 @@ function createInitialStores(db) {
|
|||
db.createObjectStore("pendingEvents", {keyPath: "key"});
|
||||
}
|
||||
//v2
|
||||
async function createMemberStore(db, txn) {
|
||||
const roomMembers = new RoomMemberStore(db.createObjectStore("roomMembers", {keyPath: "key"}));
|
||||
async function createMemberStore(db: IDBDatabase, txn: IDBTransaction): Promise<void> {
|
||||
// Cast ok here because only "set" is used
|
||||
const roomMembers = new RoomMemberStore(db.createObjectStore("roomMembers", {keyPath: "key"}) as any);
|
||||
// migrate existing member state events over
|
||||
const roomState = txn.objectStore("roomState");
|
||||
await iterateCursor(roomState.openCursor(), entry => {
|
||||
await iterateCursor<RoomStateEntry>(roomState.openCursor(), entry => {
|
||||
if (entry.event.type === MEMBER_EVENT_TYPE) {
|
||||
roomState.delete(entry.key);
|
||||
const member = RoomMember.fromMemberEvent(entry.roomId, entry.event);
|
||||
|
@ -52,10 +56,11 @@ async function createMemberStore(db, txn) {
|
|||
roomMembers.set(member.serialize());
|
||||
}
|
||||
}
|
||||
return NOT_DONE;
|
||||
});
|
||||
}
|
||||
//v3
|
||||
async function migrateSession(db, txn) {
|
||||
async function migrateSession(db: IDBDatabase, txn: IDBTransaction): Promise<void> {
|
||||
const session = txn.objectStore("session");
|
||||
try {
|
||||
const PRE_MIGRATION_KEY = 1;
|
||||
|
@ -63,7 +68,8 @@ async function migrateSession(db, txn) {
|
|||
if (entry) {
|
||||
session.delete(PRE_MIGRATION_KEY);
|
||||
const {syncToken, syncFilterId, serverVersions} = entry.value;
|
||||
const store = new SessionStore(session);
|
||||
// Cast ok here because only "set" is used and we don't look into return
|
||||
const store = new SessionStore(session as any);
|
||||
store.set("sync", {token: syncToken, filterId: syncFilterId});
|
||||
store.set("serverVersions", serverVersions);
|
||||
}
|
||||
|
@ -73,7 +79,7 @@ async function migrateSession(db, txn) {
|
|||
}
|
||||
}
|
||||
//v4
|
||||
function createE2EEStores(db) {
|
||||
function createE2EEStores(db: IDBDatabase): void {
|
||||
db.createObjectStore("userIdentities", {keyPath: "userId"});
|
||||
const deviceIdentities = db.createObjectStore("deviceIdentities", {keyPath: "key"});
|
||||
deviceIdentities.createIndex("byCurve25519Key", "curve25519Key", {unique: true});
|
||||
|
@ -86,13 +92,14 @@ function createE2EEStores(db) {
|
|||
}
|
||||
|
||||
// v5
|
||||
async function migrateEncryptionFlag(db, txn) {
|
||||
async function migrateEncryptionFlag(db: IDBDatabase, txn: IDBTransaction): Promise<void> {
|
||||
// migrate room summary isEncrypted -> encryption prop
|
||||
const roomSummary = txn.objectStore("roomSummary");
|
||||
const roomState = txn.objectStore("roomState");
|
||||
const summaries = [];
|
||||
await iterateCursor(roomSummary.openCursor(), summary => {
|
||||
const summaries: any[] = [];
|
||||
await iterateCursor<any>(roomSummary.openCursor(), summary => {
|
||||
summaries.push(summary);
|
||||
return NOT_DONE;
|
||||
});
|
||||
for (const summary of summaries) {
|
||||
const encryptionEntry = await reqAsPromise(roomState.get(`${summary.roomId}|m.room.encryption|`));
|
||||
|
@ -105,31 +112,32 @@ async function migrateEncryptionFlag(db, txn) {
|
|||
}
|
||||
|
||||
// v6
|
||||
function createAccountDataStore(db) {
|
||||
function createAccountDataStore(db: IDBDatabase): void {
|
||||
db.createObjectStore("accountData", {keyPath: "type"});
|
||||
}
|
||||
|
||||
// v7
|
||||
function createInviteStore(db) {
|
||||
function createInviteStore(db: IDBDatabase): void {
|
||||
db.createObjectStore("invites", {keyPath: "roomId"});
|
||||
}
|
||||
|
||||
// v8
|
||||
function createArchivedRoomSummaryStore(db) {
|
||||
function createArchivedRoomSummaryStore(db: IDBDatabase): void {
|
||||
db.createObjectStore("archivedRoomSummary", {keyPath: "summary.roomId"});
|
||||
}
|
||||
|
||||
// v9
|
||||
async function migrateOperationScopeIndex(db, txn) {
|
||||
async function migrateOperationScopeIndex(db: IDBDatabase, txn: IDBTransaction): Promise<void> {
|
||||
try {
|
||||
const operations = txn.objectStore("operations");
|
||||
operations.deleteIndex("byTypeAndScope");
|
||||
await iterateCursor(operations.openCursor(), (op, key, cur) => {
|
||||
await iterateCursor<any>(operations.openCursor(), (op, key, cur) => {
|
||||
const {typeScopeKey} = op;
|
||||
delete op.typeScopeKey;
|
||||
const [type, scope] = typeScopeKey.split("|");
|
||||
op.scopeTypeKey = encodeScopeTypeKey(scope, type);
|
||||
cur.update(op);
|
||||
return NOT_DONE;
|
||||
});
|
||||
operations.createIndex("byScopeAndType", "scopeTypeKey", {unique: false});
|
||||
} catch (err) {
|
||||
|
@ -139,6 +147,6 @@ async function migrateOperationScopeIndex(db, txn) {
|
|||
}
|
||||
|
||||
//v10
|
||||
function createTimelineRelationsStore(db) {
|
||||
function createTimelineRelationsStore(db: IDBDatabase) : void {
|
||||
db.createObjectStore("timelineRelations", {keyPath: "key"});
|
||||
}
|
|
@ -13,17 +13,26 @@ 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 {Store} from "../Store";
|
||||
import {Content} from "../../types";
|
||||
|
||||
interface AccountDataEntry {
|
||||
type: string;
|
||||
content: Content;
|
||||
}
|
||||
|
||||
export class AccountDataStore {
|
||||
constructor(store) {
|
||||
this._store = store;
|
||||
}
|
||||
private _store: Store<AccountDataEntry>;
|
||||
|
||||
async get(type) {
|
||||
return await this._store.get(type);
|
||||
}
|
||||
constructor(store: Store<AccountDataEntry>) {
|
||||
this._store = store;
|
||||
}
|
||||
|
||||
set(event) {
|
||||
return this._store.put(event);
|
||||
}
|
||||
async get(type: string): Promise<AccountDataEntry | null> {
|
||||
return await this._store.get(type);
|
||||
}
|
||||
|
||||
set(event: AccountDataEntry): Promise<IDBValidKey> {
|
||||
return this._store.put(event);
|
||||
}
|
||||
}
|
|
@ -14,34 +14,47 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {MAX_UNICODE, MIN_UNICODE} from "./common.js";
|
||||
import {MAX_UNICODE, MIN_UNICODE} from "./common";
|
||||
import {Store} from "../Store";
|
||||
|
||||
function encodeKey(userId, deviceId) {
|
||||
interface DeviceIdentity {
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
ed25519Key: string;
|
||||
curve25519Key: string;
|
||||
algorithms: string[];
|
||||
displayName: string;
|
||||
key: string;
|
||||
}
|
||||
|
||||
function encodeKey(userId: string, deviceId: string): string {
|
||||
return `${userId}|${deviceId}`;
|
||||
}
|
||||
|
||||
function decodeKey(key) {
|
||||
function decodeKey(key: string): { userId: string, deviceId: string } {
|
||||
const [userId, deviceId] = key.split("|");
|
||||
return {userId, deviceId};
|
||||
}
|
||||
|
||||
export class DeviceIdentityStore {
|
||||
constructor(store) {
|
||||
private _store: Store<DeviceIdentity>;
|
||||
|
||||
constructor(store: Store<DeviceIdentity>) {
|
||||
this._store = store;
|
||||
}
|
||||
|
||||
getAllForUserId(userId) {
|
||||
getAllForUserId(userId: string): Promise<DeviceIdentity[]> {
|
||||
const range = this._store.IDBKeyRange.lowerBound(encodeKey(userId, ""));
|
||||
return this._store.selectWhile(range, device => {
|
||||
return device.userId === userId;
|
||||
});
|
||||
}
|
||||
|
||||
async getAllDeviceIds(userId) {
|
||||
const deviceIds = [];
|
||||
async getAllDeviceIds(userId: string): Promise<string[]> {
|
||||
const deviceIds: string[] = [];
|
||||
const range = this._store.IDBKeyRange.lowerBound(encodeKey(userId, ""));
|
||||
await this._store.iterateKeys(range, key => {
|
||||
const decodedKey = decodeKey(key);
|
||||
const decodedKey = decodeKey(key as string);
|
||||
// prevent running into the next room
|
||||
if (decodedKey.userId === userId) {
|
||||
deviceIds.push(decodedKey.deviceId);
|
||||
|
@ -52,27 +65,27 @@ export class DeviceIdentityStore {
|
|||
return deviceIds;
|
||||
}
|
||||
|
||||
get(userId, deviceId) {
|
||||
get(userId: string, deviceId: string): Promise<DeviceIdentity | null> {
|
||||
return this._store.get(encodeKey(userId, deviceId));
|
||||
}
|
||||
|
||||
set(deviceIdentity) {
|
||||
set(deviceIdentity: DeviceIdentity): Promise<IDBValidKey> {
|
||||
deviceIdentity.key = encodeKey(deviceIdentity.userId, deviceIdentity.deviceId);
|
||||
this._store.put(deviceIdentity);
|
||||
return this._store.put(deviceIdentity);
|
||||
}
|
||||
|
||||
getByCurve25519Key(curve25519Key) {
|
||||
getByCurve25519Key(curve25519Key: string): Promise<DeviceIdentity | null> {
|
||||
return this._store.index("byCurve25519Key").get(curve25519Key);
|
||||
}
|
||||
|
||||
remove(userId, deviceId) {
|
||||
this._store.delete(encodeKey(userId, deviceId));
|
||||
remove(userId: string, deviceId: string): Promise<undefined> {
|
||||
return this._store.delete(encodeKey(userId, deviceId));
|
||||
}
|
||||
|
||||
removeAllForUser(userId) {
|
||||
removeAllForUser(userId: string): Promise<undefined> {
|
||||
// exclude both keys as they are theoretical min and max,
|
||||
// but we should't have a match for just the room id, or room id with max
|
||||
const range = this._store.IDBKeyRange.bound(encodeKey(userId, MIN_UNICODE), encodeKey(userId, MAX_UNICODE), true, true);
|
||||
this._store.delete(range);
|
||||
return this._store.delete(range);
|
||||
}
|
||||
}
|
|
@ -14,31 +14,40 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {MIN_UNICODE, MAX_UNICODE} from "./common.js";
|
||||
import {MIN_UNICODE, MAX_UNICODE} from "./common";
|
||||
import {Store} from "../Store";
|
||||
|
||||
function encodeKey(roomId, sessionId, messageIndex) {
|
||||
function encodeKey(roomId: string, sessionId: string, messageIndex: number | string): string {
|
||||
return `${roomId}|${sessionId}|${messageIndex}`;
|
||||
}
|
||||
|
||||
interface GroupSessionDecryption {
|
||||
eventId: string;
|
||||
timestamp: number;
|
||||
key: string;
|
||||
}
|
||||
|
||||
export class GroupSessionDecryptionStore {
|
||||
constructor(store) {
|
||||
private _store: Store<GroupSessionDecryption>;
|
||||
|
||||
constructor(store: Store<GroupSessionDecryption>) {
|
||||
this._store = store;
|
||||
}
|
||||
|
||||
get(roomId, sessionId, messageIndex) {
|
||||
get(roomId: string, sessionId: string, messageIndex: number): Promise<GroupSessionDecryption | null> {
|
||||
return this._store.get(encodeKey(roomId, sessionId, messageIndex));
|
||||
}
|
||||
|
||||
set(roomId, sessionId, messageIndex, decryption) {
|
||||
set(roomId: string, sessionId: string, messageIndex: number, decryption: GroupSessionDecryption): Promise<IDBValidKey> {
|
||||
decryption.key = encodeKey(roomId, sessionId, messageIndex);
|
||||
this._store.put(decryption);
|
||||
return this._store.put(decryption);
|
||||
}
|
||||
|
||||
removeAllForRoom(roomId) {
|
||||
removeAllForRoom(roomId: string): Promise<undefined> {
|
||||
const range = this._store.IDBKeyRange.bound(
|
||||
encodeKey(roomId, MIN_UNICODE, MIN_UNICODE),
|
||||
encodeKey(roomId, MAX_UNICODE, MAX_UNICODE)
|
||||
);
|
||||
this._store.delete(range);
|
||||
return this._store.delete(range);
|
||||
}
|
||||
}
|
|
@ -14,37 +14,50 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {MIN_UNICODE, MAX_UNICODE} from "./common.js";
|
||||
import {MIN_UNICODE, MAX_UNICODE} from "./common";
|
||||
import {Store} from "../Store";
|
||||
|
||||
function encodeKey(roomId, senderKey, sessionId) {
|
||||
interface InboundGroupSession {
|
||||
roomId: string;
|
||||
senderKey: string;
|
||||
sessionId: string;
|
||||
session?: string;
|
||||
claimedKeys?: { [algorithm : string] : string };
|
||||
eventIds?: string[];
|
||||
key: string;
|
||||
}
|
||||
|
||||
function encodeKey(roomId: string, senderKey: string, sessionId: string): string {
|
||||
return `${roomId}|${senderKey}|${sessionId}`;
|
||||
}
|
||||
|
||||
export class InboundGroupSessionStore {
|
||||
constructor(store) {
|
||||
private _store: Store<InboundGroupSession>;
|
||||
|
||||
constructor(store: Store<InboundGroupSession>) {
|
||||
this._store = store;
|
||||
}
|
||||
|
||||
async has(roomId, senderKey, sessionId) {
|
||||
async has(roomId: string, senderKey: string, sessionId: string): Promise<boolean> {
|
||||
const key = encodeKey(roomId, senderKey, sessionId);
|
||||
const fetchedKey = await this._store.getKey(key);
|
||||
return key === fetchedKey;
|
||||
}
|
||||
|
||||
get(roomId, senderKey, sessionId) {
|
||||
get(roomId: string, senderKey: string, sessionId: string): Promise<InboundGroupSession | null> {
|
||||
return this._store.get(encodeKey(roomId, senderKey, sessionId));
|
||||
}
|
||||
|
||||
set(session) {
|
||||
set(session: InboundGroupSession): Promise<IDBValidKey> {
|
||||
session.key = encodeKey(session.roomId, session.senderKey, session.sessionId);
|
||||
this._store.put(session);
|
||||
return this._store.put(session);
|
||||
}
|
||||
|
||||
removeAllForRoom(roomId) {
|
||||
removeAllForRoom(roomId: string): Promise<undefined> {
|
||||
const range = this._store.IDBKeyRange.bound(
|
||||
encodeKey(roomId, MIN_UNICODE, MIN_UNICODE),
|
||||
encodeKey(roomId, MAX_UNICODE, MAX_UNICODE)
|
||||
);
|
||||
this._store.delete(range);
|
||||
return this._store.delete(range);
|
||||
}
|
||||
}
|
51
src/matrix/storage/idb/stores/InviteStore.ts
Normal file
51
src/matrix/storage/idb/stores/InviteStore.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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 {Store} from "../Store";
|
||||
import {MemberData} from "./RoomMemberStore";
|
||||
|
||||
// TODO: Move to Invite when that's TypeScript.
|
||||
export interface InviteData {
|
||||
roomId: string;
|
||||
isEncrypted: boolean;
|
||||
isDirectMessage: boolean;
|
||||
name?: string;
|
||||
avatarUrl?: string;
|
||||
avatarColorId: number;
|
||||
canonicalAlias?: string;
|
||||
timestamp: number;
|
||||
joinRule: string;
|
||||
inviter?: MemberData;
|
||||
}
|
||||
|
||||
export class InviteStore {
|
||||
private _inviteStore: Store<InviteData>;
|
||||
|
||||
constructor(inviteStore: Store<InviteData>) {
|
||||
this._inviteStore = inviteStore;
|
||||
}
|
||||
|
||||
getAll(): Promise<InviteData[]> {
|
||||
return this._inviteStore.selectAll();
|
||||
}
|
||||
|
||||
set(invite: InviteData): Promise<IDBValidKey> {
|
||||
return this._inviteStore.put(invite);
|
||||
}
|
||||
|
||||
remove(roomId: string): void {
|
||||
this._inviteStore.delete(roomId);
|
||||
}
|
||||
}
|
|
@ -13,26 +13,37 @@ 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 {Store} from "../Store";
|
||||
|
||||
function encodeKey(senderKey, sessionId) {
|
||||
function encodeKey(senderKey: string, sessionId: string): string {
|
||||
return `${senderKey}|${sessionId}`;
|
||||
}
|
||||
|
||||
function decodeKey(key) {
|
||||
function decodeKey(key: string): { senderKey: string, sessionId: string } {
|
||||
const [senderKey, sessionId] = key.split("|");
|
||||
return {senderKey, sessionId};
|
||||
}
|
||||
|
||||
interface OlmSession {
|
||||
session: string;
|
||||
sessionId: string;
|
||||
senderKey: string;
|
||||
lastUsed: number;
|
||||
key: string;
|
||||
}
|
||||
|
||||
export class OlmSessionStore {
|
||||
constructor(store) {
|
||||
private _store: Store<OlmSession>;
|
||||
|
||||
constructor(store: Store<OlmSession>) {
|
||||
this._store = store;
|
||||
}
|
||||
|
||||
async getSessionIds(senderKey) {
|
||||
const sessionIds = [];
|
||||
async getSessionIds(senderKey: string): Promise<string[]> {
|
||||
const sessionIds: string[] = [];
|
||||
const range = this._store.IDBKeyRange.lowerBound(encodeKey(senderKey, ""));
|
||||
await this._store.iterateKeys(range, key => {
|
||||
const decodedKey = decodeKey(key);
|
||||
const decodedKey = decodeKey(key as string);
|
||||
// prevent running into the next room
|
||||
if (decodedKey.senderKey === senderKey) {
|
||||
sessionIds.push(decodedKey.sessionId);
|
||||
|
@ -43,23 +54,23 @@ export class OlmSessionStore {
|
|||
return sessionIds;
|
||||
}
|
||||
|
||||
getAll(senderKey) {
|
||||
getAll(senderKey: string): Promise<OlmSession[]> {
|
||||
const range = this._store.IDBKeyRange.lowerBound(encodeKey(senderKey, ""));
|
||||
return this._store.selectWhile(range, session => {
|
||||
return session.senderKey === senderKey;
|
||||
});
|
||||
}
|
||||
|
||||
get(senderKey, sessionId) {
|
||||
get(senderKey: string, sessionId: string): Promise<OlmSession | null> {
|
||||
return this._store.get(encodeKey(senderKey, sessionId));
|
||||
}
|
||||
|
||||
set(session) {
|
||||
set(session: OlmSession): Promise<IDBValidKey> {
|
||||
session.key = encodeKey(session.senderKey, session.sessionId);
|
||||
return this._store.put(session);
|
||||
}
|
||||
|
||||
remove(senderKey, sessionId) {
|
||||
remove(senderKey: string, sessionId: string): Promise<undefined> {
|
||||
return this._store.delete(encodeKey(senderKey, sessionId));
|
||||
}
|
||||
}
|
|
@ -13,24 +13,44 @@ 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 {MIN_UNICODE, MAX_UNICODE} from "./common.js";
|
||||
import {MIN_UNICODE, MAX_UNICODE} from "./common";
|
||||
import {Store} from "../Store";
|
||||
|
||||
export function encodeScopeTypeKey(scope, type) {
|
||||
export function encodeScopeTypeKey(scope: string, type: string): string {
|
||||
return `${scope}|${type}`;
|
||||
}
|
||||
|
||||
interface Operation {
|
||||
id: string;
|
||||
type: string;
|
||||
scope: string;
|
||||
userIds: string[];
|
||||
scopeTypeKey: string;
|
||||
roomKeyMessage: RoomKeyMessage;
|
||||
}
|
||||
|
||||
interface RoomKeyMessage {
|
||||
room_id: string;
|
||||
session_id: string;
|
||||
session_key: string;
|
||||
algorithm: string;
|
||||
chain_index: number;
|
||||
}
|
||||
|
||||
export class OperationStore {
|
||||
constructor(store) {
|
||||
private _store: Store<Operation>;
|
||||
|
||||
constructor(store: Store<Operation>) {
|
||||
this._store = store;
|
||||
}
|
||||
|
||||
getAll() {
|
||||
getAll(): Promise<Operation[]> {
|
||||
return this._store.selectAll();
|
||||
}
|
||||
|
||||
async getAllByTypeAndScope(type, scope) {
|
||||
async getAllByTypeAndScope(type: string, scope: string): Promise<Operation[]> {
|
||||
const key = encodeScopeTypeKey(scope, type);
|
||||
const results = [];
|
||||
const results: Operation[] = [];
|
||||
await this._store.index("byScopeAndType").iterateWhile(key, value => {
|
||||
if (value.scopeTypeKey !== key) {
|
||||
return false;
|
||||
|
@ -41,20 +61,20 @@ export class OperationStore {
|
|||
return results;
|
||||
}
|
||||
|
||||
add(operation) {
|
||||
add(operation: Operation): Promise<IDBValidKey> {
|
||||
operation.scopeTypeKey = encodeScopeTypeKey(operation.scope, operation.type);
|
||||
this._store.add(operation);
|
||||
return this._store.add(operation);
|
||||
}
|
||||
|
||||
update(operation) {
|
||||
this._store.put(operation);
|
||||
update(operation: Operation): Promise<IDBValidKey> {
|
||||
return this._store.put(operation);
|
||||
}
|
||||
|
||||
remove(id) {
|
||||
this._store.delete(id);
|
||||
remove(id: string): Promise<undefined> {
|
||||
return this._store.delete(id);
|
||||
}
|
||||
|
||||
async removeAllForScope(scope) {
|
||||
async removeAllForScope(scope: string): Promise<undefined> {
|
||||
const range = this._store.IDBKeyRange.bound(
|
||||
encodeScopeTypeKey(scope, MIN_UNICODE),
|
||||
encodeScopeTypeKey(scope, MAX_UNICODE)
|
||||
|
@ -64,5 +84,6 @@ export class OperationStore {
|
|||
cur.delete();
|
||||
return true;
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
|
@ -13,21 +13,30 @@ 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 {Store} from "../Store";
|
||||
|
||||
interface OutboundSession {
|
||||
roomId: string;
|
||||
session: string;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
export class OutboundGroupSessionStore {
|
||||
constructor(store) {
|
||||
private _store: Store<OutboundSession>;
|
||||
|
||||
constructor(store: Store<OutboundSession>) {
|
||||
this._store = store;
|
||||
}
|
||||
|
||||
remove(roomId) {
|
||||
this._store.delete(roomId);
|
||||
remove(roomId: string): Promise<undefined> {
|
||||
return this._store.delete(roomId);
|
||||
}
|
||||
|
||||
get(roomId) {
|
||||
get(roomId: string): Promise<OutboundSession | null> {
|
||||
return this._store.get(roomId);
|
||||
}
|
||||
|
||||
set(session) {
|
||||
this._store.put(session);
|
||||
set(session: OutboundSession): Promise<IDBValidKey> {
|
||||
return this._store.put(session);
|
||||
}
|
||||
}
|
|
@ -14,25 +14,42 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { encodeUint32, decodeUint32 } from "../utils.js";
|
||||
import {KeyLimits} from "../../common.js";
|
||||
import { encodeUint32, decodeUint32 } from "../utils";
|
||||
import {KeyLimits} from "../../common";
|
||||
import {Store} from "../Store";
|
||||
import {Content} from "../../types";
|
||||
|
||||
function encodeKey(roomId, queueIndex) {
|
||||
interface PendingEntry {
|
||||
roomId: string;
|
||||
queueIndex: number;
|
||||
eventType: string;
|
||||
content: Content;
|
||||
relatexTxnId: string | null;
|
||||
relatedEventId: string | null;
|
||||
txnId?: string;
|
||||
needsEncryption: boolean;
|
||||
needsUpload: boolean;
|
||||
key: string;
|
||||
}
|
||||
|
||||
function encodeKey(roomId: string, queueIndex: number): string {
|
||||
return `${roomId}|${encodeUint32(queueIndex)}`;
|
||||
}
|
||||
|
||||
function decodeKey(key) {
|
||||
function decodeKey(key: string): { roomId: string, queueIndex: number } {
|
||||
const [roomId, encodedQueueIndex] = key.split("|");
|
||||
const queueIndex = decodeUint32(encodedQueueIndex);
|
||||
return {roomId, queueIndex};
|
||||
}
|
||||
|
||||
export class PendingEventStore {
|
||||
constructor(eventStore) {
|
||||
private _eventStore: Store<PendingEntry>;
|
||||
|
||||
constructor(eventStore: Store<PendingEntry>) {
|
||||
this._eventStore = eventStore;
|
||||
}
|
||||
|
||||
async getMaxQueueIndex(roomId) {
|
||||
async getMaxQueueIndex(roomId: string): Promise<number | undefined> {
|
||||
const range = this._eventStore.IDBKeyRange.bound(
|
||||
encodeKey(roomId, KeyLimits.minStorageKey),
|
||||
encodeKey(roomId, KeyLimits.maxStorageKey),
|
||||
|
@ -41,38 +58,38 @@ export class PendingEventStore {
|
|||
);
|
||||
const maxKey = await this._eventStore.findMaxKey(range);
|
||||
if (maxKey) {
|
||||
return decodeKey(maxKey).queueIndex;
|
||||
return decodeKey(maxKey as string).queueIndex;
|
||||
}
|
||||
}
|
||||
|
||||
remove(roomId, queueIndex) {
|
||||
remove(roomId: string, queueIndex: number): Promise<undefined> {
|
||||
const keyRange = this._eventStore.IDBKeyRange.only(encodeKey(roomId, queueIndex));
|
||||
this._eventStore.delete(keyRange);
|
||||
return this._eventStore.delete(keyRange);
|
||||
}
|
||||
|
||||
async exists(roomId, queueIndex) {
|
||||
async exists(roomId: string, queueIndex: number): Promise<boolean> {
|
||||
const keyRange = this._eventStore.IDBKeyRange.only(encodeKey(roomId, queueIndex));
|
||||
const key = await this._eventStore.getKey(keyRange);
|
||||
return !!key;
|
||||
}
|
||||
|
||||
add(pendingEvent) {
|
||||
add(pendingEvent: PendingEntry): Promise<IDBValidKey> {
|
||||
pendingEvent.key = encodeKey(pendingEvent.roomId, pendingEvent.queueIndex);
|
||||
this._eventStore.add(pendingEvent);
|
||||
return this._eventStore.add(pendingEvent);
|
||||
}
|
||||
|
||||
update(pendingEvent) {
|
||||
this._eventStore.put(pendingEvent);
|
||||
update(pendingEvent: PendingEntry): Promise<IDBValidKey> {
|
||||
return this._eventStore.put(pendingEvent);
|
||||
}
|
||||
|
||||
getAll() {
|
||||
getAll(): Promise<PendingEntry[]> {
|
||||
return this._eventStore.selectAll();
|
||||
}
|
||||
|
||||
removeAllForRoom(roomId) {
|
||||
removeAllForRoom(roomId: string): Promise<undefined> {
|
||||
const minKey = encodeKey(roomId, KeyLimits.minStorageKey);
|
||||
const maxKey = encodeKey(roomId, KeyLimits.maxStorageKey);
|
||||
const range = this._eventStore.IDBKeyRange.bound(minKey, maxKey);
|
||||
this._eventStore.delete(range);
|
||||
return this._eventStore.delete(range);
|
||||
}
|
||||
}
|
|
@ -15,44 +15,59 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {MAX_UNICODE} from "./common.js";
|
||||
import {MAX_UNICODE} from "./common";
|
||||
import {Store} from "../Store";
|
||||
|
||||
function encodeKey(roomId, userId) {
|
||||
function encodeKey(roomId: string, userId: string) {
|
||||
return `${roomId}|${userId}`;
|
||||
}
|
||||
|
||||
function decodeKey(key) {
|
||||
function decodeKey(key: string): { roomId: string, userId: string } {
|
||||
const [roomId, userId] = key.split("|");
|
||||
return {roomId, userId};
|
||||
}
|
||||
|
||||
// TODO: Move to RoomMember when that's TypeScript.
|
||||
export interface MemberData {
|
||||
roomId: string;
|
||||
userId: string;
|
||||
avatarUrl: string;
|
||||
displayName: string;
|
||||
membership: "join" | "leave" | "invite" | "ban";
|
||||
}
|
||||
|
||||
type MemberStorageEntry = MemberData & { key: string }
|
||||
|
||||
// no historical members
|
||||
export class RoomMemberStore {
|
||||
constructor(roomMembersStore) {
|
||||
private _roomMembersStore: Store<MemberStorageEntry>;
|
||||
|
||||
constructor(roomMembersStore: Store<MemberStorageEntry>) {
|
||||
this._roomMembersStore = roomMembersStore;
|
||||
}
|
||||
|
||||
get(roomId, userId) {
|
||||
get(roomId: string, userId: string): Promise<MemberStorageEntry | null> {
|
||||
return this._roomMembersStore.get(encodeKey(roomId, userId));
|
||||
}
|
||||
}
|
||||
|
||||
async set(member) {
|
||||
member.key = encodeKey(member.roomId, member.userId);
|
||||
return this._roomMembersStore.put(member);
|
||||
}
|
||||
async set(member: MemberData): Promise<IDBValidKey> {
|
||||
// Object.assign would be more typesafe, but small objects
|
||||
(member as any).key = encodeKey(member.roomId, member.userId);
|
||||
return this._roomMembersStore.put(member as MemberStorageEntry);
|
||||
}
|
||||
|
||||
getAll(roomId) {
|
||||
getAll(roomId: string): Promise<MemberData[]> {
|
||||
const range = this._roomMembersStore.IDBKeyRange.lowerBound(encodeKey(roomId, ""));
|
||||
return this._roomMembersStore.selectWhile(range, member => {
|
||||
return member.roomId === roomId;
|
||||
});
|
||||
}
|
||||
|
||||
async getAllUserIds(roomId) {
|
||||
const userIds = [];
|
||||
async getAllUserIds(roomId: string): Promise<string[]> {
|
||||
const userIds: string[] = [];
|
||||
const range = this._roomMembersStore.IDBKeyRange.lowerBound(encodeKey(roomId, ""));
|
||||
await this._roomMembersStore.iterateKeys(range, key => {
|
||||
const decodedKey = decodeKey(key);
|
||||
const decodedKey = decodeKey(key as string);
|
||||
// prevent running into the next room
|
||||
if (decodedKey.roomId === roomId) {
|
||||
userIds.push(decodedKey.userId);
|
||||
|
@ -63,10 +78,10 @@ export class RoomMemberStore {
|
|||
return userIds;
|
||||
}
|
||||
|
||||
removeAllForRoom(roomId) {
|
||||
removeAllForRoom(roomId: string): Promise<undefined> {
|
||||
// exclude both keys as they are theoretical min and max,
|
||||
// but we should't have a match for just the room id, or room id with max
|
||||
const range = this._roomMembersStore.IDBKeyRange.bound(roomId, `${roomId}|${MAX_UNICODE}`, true, true);
|
||||
this._roomMembersStore.delete(range);
|
||||
return this._roomMembersStore.delete(range);
|
||||
}
|
||||
}
|
|
@ -15,32 +15,42 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {MAX_UNICODE} from "./common.js";
|
||||
import {MAX_UNICODE} from "./common";
|
||||
import {Store} from "../Store";
|
||||
import {StateEvent} from "../../types";
|
||||
|
||||
function encodeKey(roomId, eventType, stateKey) {
|
||||
function encodeKey(roomId: string, eventType: string, stateKey: string) {
|
||||
return `${roomId}|${eventType}|${stateKey}`;
|
||||
}
|
||||
|
||||
export interface RoomStateEntry {
|
||||
roomId: string;
|
||||
event: StateEvent;
|
||||
key: string;
|
||||
}
|
||||
|
||||
export class RoomStateStore {
|
||||
constructor(idbStore) {
|
||||
private _roomStateStore: Store<RoomStateEntry>;
|
||||
|
||||
constructor(idbStore: Store<RoomStateEntry>) {
|
||||
this._roomStateStore = idbStore;
|
||||
}
|
||||
|
||||
get(roomId, type, stateKey) {
|
||||
get(roomId: string, type: string, stateKey: string): Promise<RoomStateEntry | null> {
|
||||
const key = encodeKey(roomId, type, stateKey);
|
||||
return this._roomStateStore.get(key);
|
||||
}
|
||||
|
||||
set(roomId, event) {
|
||||
set(roomId: string, event: StateEvent): Promise<IDBValidKey> {
|
||||
const key = encodeKey(roomId, event.type, event.state_key);
|
||||
const entry = {roomId, event, key};
|
||||
return this._roomStateStore.put(entry);
|
||||
}
|
||||
|
||||
removeAllForRoom(roomId) {
|
||||
removeAllForRoom(roomId: string): Promise<undefined> {
|
||||
// exclude both keys as they are theoretical min and max,
|
||||
// but we should't have a match for just the room id, or room id with max
|
||||
const range = this._roomStateStore.IDBKeyRange.bound(roomId, `${roomId}|${MAX_UNICODE}`, true, true);
|
||||
this._roomStateStore.delete(range);
|
||||
return this._roomStateStore.delete(range);
|
||||
}
|
||||
}
|
|
@ -27,31 +27,35 @@ store contains:
|
|||
inviteCount
|
||||
joinCount
|
||||
*/
|
||||
import {Store} from "../Store";
|
||||
import {SummaryData} from "../../../room/RoomSummary";
|
||||
|
||||
/** Used for both roomSummary and archivedRoomSummary stores */
|
||||
export class RoomSummaryStore {
|
||||
constructor(summaryStore) {
|
||||
this._summaryStore = summaryStore;
|
||||
}
|
||||
private _summaryStore: Store<SummaryData>;
|
||||
|
||||
getAll() {
|
||||
return this._summaryStore.selectAll();
|
||||
}
|
||||
constructor(summaryStore: Store<SummaryData>) {
|
||||
this._summaryStore = summaryStore;
|
||||
}
|
||||
|
||||
set(summary) {
|
||||
return this._summaryStore.put(summary);
|
||||
}
|
||||
getAll(): Promise<SummaryData[]> {
|
||||
return this._summaryStore.selectAll();
|
||||
}
|
||||
|
||||
get(roomId) {
|
||||
return this._summaryStore.get(roomId);
|
||||
}
|
||||
set(summary: SummaryData): Promise<IDBValidKey> {
|
||||
return this._summaryStore.put(summary);
|
||||
}
|
||||
|
||||
async has(roomId) {
|
||||
get(roomId: string): Promise<SummaryData | null> {
|
||||
return this._summaryStore.get(roomId);
|
||||
}
|
||||
|
||||
async has(roomId: string): Promise<boolean> {
|
||||
const fetchedKey = await this._summaryStore.getKey(roomId);
|
||||
return roomId === fetchedKey;
|
||||
}
|
||||
}
|
||||
|
||||
remove(roomId) {
|
||||
return this._summaryStore.delete(roomId);
|
||||
}
|
||||
remove(roomId: string): Promise<undefined> {
|
||||
return this._summaryStore.delete(roomId);
|
||||
}
|
||||
}
|
|
@ -1,40 +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.
|
||||
*/
|
||||
|
||||
export class SessionStore {
|
||||
constructor(sessionStore) {
|
||||
this._sessionStore = sessionStore;
|
||||
}
|
||||
|
||||
async get(key) {
|
||||
const entry = await this._sessionStore.get(key);
|
||||
if (entry) {
|
||||
return entry.value;
|
||||
}
|
||||
}
|
||||
|
||||
set(key, value) {
|
||||
this._sessionStore.put({key, value});
|
||||
}
|
||||
|
||||
add(key, value) {
|
||||
this._sessionStore.add({key, value});
|
||||
}
|
||||
|
||||
remove(key) {
|
||||
this._sessionStore.delete(key);
|
||||
}
|
||||
}
|
48
src/matrix/storage/idb/stores/SessionStore.ts
Normal file
48
src/matrix/storage/idb/stores/SessionStore.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
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 {Store} from "../Store";
|
||||
|
||||
export interface SessionEntry {
|
||||
key: string;
|
||||
value: any;
|
||||
}
|
||||
|
||||
export class SessionStore {
|
||||
private _sessionStore: Store<SessionEntry>
|
||||
|
||||
constructor(sessionStore: Store<SessionEntry>) {
|
||||
this._sessionStore = sessionStore;
|
||||
}
|
||||
|
||||
async get(key: IDBValidKey): Promise<any> {
|
||||
const entry = await this._sessionStore.get(key);
|
||||
if (entry) {
|
||||
return entry.value;
|
||||
}
|
||||
}
|
||||
|
||||
set(key: string, value: any): Promise<IDBValidKey> {
|
||||
return this._sessionStore.put({key, value});
|
||||
}
|
||||
|
||||
add(key: string, value: any): Promise<IDBValidKey> {
|
||||
return this._sessionStore.add({key, value});
|
||||
}
|
||||
|
||||
remove(key: IDBValidKey): Promise<undefined> {
|
||||
return this._sessionStore.delete(key);
|
||||
}
|
||||
}
|
|
@ -15,25 +15,52 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import {EventKey} from "../../../room/timeline/EventKey.js";
|
||||
import { StorageError } from "../../common.js";
|
||||
import { encodeUint32 } from "../utils.js";
|
||||
import {KeyLimits} from "../../common.js";
|
||||
import { StorageError } from "../../common";
|
||||
import { encodeUint32 } from "../utils";
|
||||
import {KeyLimits} from "../../common";
|
||||
import {Store} from "../Store";
|
||||
import {RoomEvent, StateEvent} from "../../types";
|
||||
|
||||
function encodeKey(roomId, fragmentId, eventIndex) {
|
||||
interface Annotation {
|
||||
count: number;
|
||||
me: boolean;
|
||||
firstTimestamp: number;
|
||||
}
|
||||
|
||||
interface StorageEntry {
|
||||
roomId: string;
|
||||
fragmentId: number;
|
||||
eventIndex: number;
|
||||
event: RoomEvent | StateEvent;
|
||||
displayName?: string;
|
||||
avatarUrl?: string;
|
||||
annotations?: { [key : string]: Annotation };
|
||||
key: string;
|
||||
eventIdKey: string;
|
||||
}
|
||||
|
||||
function encodeKey(roomId: string, fragmentId: number, eventIndex: number): string {
|
||||
return `${roomId}|${encodeUint32(fragmentId)}|${encodeUint32(eventIndex)}`;
|
||||
}
|
||||
|
||||
function encodeEventIdKey(roomId, eventId) {
|
||||
function encodeEventIdKey(roomId: string, eventId: string): string {
|
||||
return `${roomId}|${eventId}`;
|
||||
}
|
||||
|
||||
function decodeEventIdKey(eventIdKey) {
|
||||
function decodeEventIdKey(eventIdKey: string): { roomId: string, eventId: string } {
|
||||
const [roomId, eventId] = eventIdKey.split("|");
|
||||
return {roomId, eventId};
|
||||
}
|
||||
|
||||
class Range {
|
||||
constructor(IDBKeyRange, only, lower, upper, lowerOpen, upperOpen) {
|
||||
private _IDBKeyRange: any; // TODO what's the appropriate representation here?
|
||||
private _only?: EventKey;
|
||||
private _lower?: EventKey;
|
||||
private _upper?: EventKey;
|
||||
private _lowerOpen: boolean;
|
||||
private _upperOpen: boolean;
|
||||
|
||||
constructor(IDBKeyRange: any, only?: EventKey, lower?: EventKey, upper?: EventKey, lowerOpen: boolean = false, upperOpen: boolean = false) {
|
||||
this._IDBKeyRange = IDBKeyRange;
|
||||
this._only = only;
|
||||
this._lower = lower;
|
||||
|
@ -42,7 +69,7 @@ class Range {
|
|||
this._upperOpen = upperOpen;
|
||||
}
|
||||
|
||||
asIDBKeyRange(roomId) {
|
||||
asIDBKeyRange(roomId: string): IDBKeyRange | undefined {
|
||||
try {
|
||||
// only
|
||||
if (this._only) {
|
||||
|
@ -99,66 +126,68 @@ class Range {
|
|||
* @property {?Gap} gap if a gap entry, the gap
|
||||
*/
|
||||
export class TimelineEventStore {
|
||||
constructor(timelineStore) {
|
||||
private _timelineStore: Store<StorageEntry>;
|
||||
|
||||
constructor(timelineStore: Store<StorageEntry>) {
|
||||
this._timelineStore = timelineStore;
|
||||
}
|
||||
|
||||
/** Creates a range that only includes the given key
|
||||
* @param {EventKey} eventKey the key
|
||||
* @return {Range} the created range
|
||||
* @param eventKey the key
|
||||
* @return the created range
|
||||
*/
|
||||
onlyRange(eventKey) {
|
||||
onlyRange(eventKey: EventKey): Range {
|
||||
return new Range(this._timelineStore.IDBKeyRange, eventKey);
|
||||
}
|
||||
|
||||
/** Creates a range that includes all keys before eventKey, and optionally also the key itself.
|
||||
* @param {EventKey} eventKey the key
|
||||
* @param {boolean} [open=false] whether the key is included (false) or excluded (true) from the range at the upper end.
|
||||
* @return {Range} the created range
|
||||
* @param eventKey the key
|
||||
* @param [open=false] whether the key is included (false) or excluded (true) from the range at the upper end.
|
||||
* @return the created range
|
||||
*/
|
||||
upperBoundRange(eventKey, open=false) {
|
||||
upperBoundRange(eventKey: EventKey, open=false): Range {
|
||||
return new Range(this._timelineStore.IDBKeyRange, undefined, undefined, eventKey, undefined, open);
|
||||
}
|
||||
|
||||
/** Creates a range that includes all keys after eventKey, and optionally also the key itself.
|
||||
* @param {EventKey} eventKey the key
|
||||
* @param {boolean} [open=false] whether the key is included (false) or excluded (true) from the range at the lower end.
|
||||
* @return {Range} the created range
|
||||
* @param eventKey the key
|
||||
* @param [open=false] whether the key is included (false) or excluded (true) from the range at the lower end.
|
||||
* @return the created range
|
||||
*/
|
||||
lowerBoundRange(eventKey, open=false) {
|
||||
lowerBoundRange(eventKey: EventKey, open=false): Range {
|
||||
return new Range(this._timelineStore.IDBKeyRange, undefined, eventKey, undefined, open);
|
||||
}
|
||||
|
||||
/** Creates a range that includes all keys between `lower` and `upper`, and optionally the given keys as well.
|
||||
* @param {EventKey} lower the lower key
|
||||
* @param {EventKey} upper the upper key
|
||||
* @param {boolean} [lowerOpen=false] whether the lower key is included (false) or excluded (true) from the range.
|
||||
* @param {boolean} [upperOpen=false] whether the upper key is included (false) or excluded (true) from the range.
|
||||
* @return {Range} the created range
|
||||
* @param lower the lower key
|
||||
* @param upper the upper key
|
||||
* @param [lowerOpen=false] whether the lower key is included (false) or excluded (true) from the range.
|
||||
* @param [upperOpen=false] whether the upper key is included (false) or excluded (true) from the range.
|
||||
* @return the created range
|
||||
*/
|
||||
boundRange(lower, upper, lowerOpen=false, upperOpen=false) {
|
||||
boundRange(lower: EventKey, upper: EventKey, lowerOpen=false, upperOpen=false): Range {
|
||||
return new Range(this._timelineStore.IDBKeyRange, undefined, lower, upper, lowerOpen, upperOpen);
|
||||
}
|
||||
|
||||
/** Looks up the last `amount` entries in the timeline for `roomId`.
|
||||
* @param {string} roomId
|
||||
* @param {number} fragmentId
|
||||
* @param {number} amount
|
||||
* @return {Promise<Entry[]>} a promise resolving to an array with 0 or more entries, in ascending order.
|
||||
* @param roomId
|
||||
* @param fragmentId
|
||||
* @param amount
|
||||
* @return a promise resolving to an array with 0 or more entries, in ascending order.
|
||||
*/
|
||||
async lastEvents(roomId, fragmentId, amount) {
|
||||
async lastEvents(roomId: string, fragmentId: number, amount: number): Promise<StorageEntry[]> {
|
||||
const eventKey = EventKey.maxKey;
|
||||
eventKey.fragmentId = fragmentId;
|
||||
return this.eventsBefore(roomId, eventKey, amount);
|
||||
}
|
||||
|
||||
/** Looks up the first `amount` entries in the timeline for `roomId`.
|
||||
* @param {string} roomId
|
||||
* @param {number} fragmentId
|
||||
* @param {number} amount
|
||||
* @return {Promise<Entry[]>} a promise resolving to an array with 0 or more entries, in ascending order.
|
||||
* @param roomId
|
||||
* @param fragmentId
|
||||
* @param amount
|
||||
* @return a promise resolving to an array with 0 or more entries, in ascending order.
|
||||
*/
|
||||
async firstEvents(roomId, fragmentId, amount) {
|
||||
async firstEvents(roomId: string, fragmentId: number, amount: number): Promise<StorageEntry[]> {
|
||||
const eventKey = EventKey.minKey;
|
||||
eventKey.fragmentId = fragmentId;
|
||||
return this.eventsAfter(roomId, eventKey, amount);
|
||||
|
@ -166,24 +195,24 @@ export class TimelineEventStore {
|
|||
|
||||
/** Looks up `amount` entries after `eventKey` in the timeline for `roomId` within the same fragment.
|
||||
* The entry for `eventKey` is not included.
|
||||
* @param {string} roomId
|
||||
* @param {EventKey} eventKey
|
||||
* @param {number} amount
|
||||
* @return {Promise<Entry[]>} a promise resolving to an array with 0 or more entries, in ascending order.
|
||||
* @param roomId
|
||||
* @param eventKey
|
||||
* @param amount
|
||||
* @return a promise resolving to an array with 0 or more entries, in ascending order.
|
||||
*/
|
||||
eventsAfter(roomId, eventKey, amount) {
|
||||
eventsAfter(roomId: string, eventKey: EventKey, amount: number): Promise<StorageEntry[]> {
|
||||
const idbRange = this.lowerBoundRange(eventKey, true).asIDBKeyRange(roomId);
|
||||
return this._timelineStore.selectLimit(idbRange, amount);
|
||||
}
|
||||
|
||||
/** Looks up `amount` entries before `eventKey` in the timeline for `roomId` within the same fragment.
|
||||
* The entry for `eventKey` is not included.
|
||||
* @param {string} roomId
|
||||
* @param {EventKey} eventKey
|
||||
* @param {number} amount
|
||||
* @return {Promise<Entry[]>} a promise resolving to an array with 0 or more entries, in ascending order.
|
||||
* @param roomId
|
||||
* @param eventKey
|
||||
* @param amount
|
||||
* @return a promise resolving to an array with 0 or more entries, in ascending order.
|
||||
*/
|
||||
async eventsBefore(roomId, eventKey, amount) {
|
||||
async eventsBefore(roomId: string, eventKey: EventKey, amount: number): Promise<StorageEntry[]> {
|
||||
const range = this.upperBoundRange(eventKey, true).asIDBKeyRange(roomId);
|
||||
const events = await this._timelineStore.selectLimitReverse(range, amount);
|
||||
events.reverse(); // because we fetched them backwards
|
||||
|
@ -195,23 +224,23 @@ export class TimelineEventStore {
|
|||
*
|
||||
* The order in which results are returned might be different than `eventIds`.
|
||||
* Call the return value to obtain the next {id, event} pair.
|
||||
* @param {string} roomId
|
||||
* @param {string[]} eventIds
|
||||
* @return {Function<Promise>}
|
||||
* @param roomId
|
||||
* @param eventIds
|
||||
* @return
|
||||
*/
|
||||
// performance comment from above refers to the fact that there *might*
|
||||
// be a correlation between event_id sorting order and chronology.
|
||||
// In that case we could avoid running over all eventIds, as the reported order by findExistingKeys
|
||||
// would match the order of eventIds. That's why findLast is also passed as backwards to keysExist.
|
||||
// also passing them in chronological order makes sense as that's how we'll receive them almost always.
|
||||
async findFirstOccurringEventId(roomId, eventIds) {
|
||||
async findFirstOccurringEventId(roomId: string, eventIds: string[]): Promise<string | undefined> {
|
||||
const byEventId = this._timelineStore.index("byEventId");
|
||||
const keys = eventIds.map(eventId => encodeEventIdKey(roomId, eventId));
|
||||
const results = new Array(keys.length);
|
||||
let firstFoundKey;
|
||||
let firstFoundKey: string | undefined;
|
||||
|
||||
// find first result that is found and has no undefined results before it
|
||||
function firstFoundAndPrecedingResolved() {
|
||||
function firstFoundAndPrecedingResolved(): string | undefined {
|
||||
for(let i = 0; i < results.length; ++i) {
|
||||
if (results[i] === undefined) {
|
||||
return;
|
||||
|
@ -222,7 +251,8 @@ export class TimelineEventStore {
|
|||
}
|
||||
|
||||
await byEventId.findExistingKeys(keys, false, (key, found) => {
|
||||
const index = keys.indexOf(key);
|
||||
// T[].search(T, number), but we want T[].search(R, number), so cast
|
||||
const index = (keys as IDBValidKey[]).indexOf(key);
|
||||
results[index] = found;
|
||||
firstFoundKey = firstFoundAndPrecedingResolved();
|
||||
return !!firstFoundKey;
|
||||
|
@ -231,38 +261,38 @@ export class TimelineEventStore {
|
|||
}
|
||||
|
||||
/** Inserts a new entry into the store. The combination of roomId and eventKey should not exist yet, or an error is thrown.
|
||||
* @param {Entry} entry the entry to insert
|
||||
* @return {Promise<>} a promise resolving to undefined if the operation was successful, or a StorageError if not.
|
||||
* @param entry the entry to insert
|
||||
* @return a promise resolving to undefined if the operation was successful, or a StorageError if not.
|
||||
* @throws {StorageError} ...
|
||||
*/
|
||||
insert(entry) {
|
||||
insert(entry: StorageEntry): Promise<IDBValidKey> {
|
||||
entry.key = encodeKey(entry.roomId, entry.fragmentId, entry.eventIndex);
|
||||
entry.eventIdKey = encodeEventIdKey(entry.roomId, entry.event.event_id);
|
||||
// TODO: map error? or in idb/store?
|
||||
this._timelineStore.add(entry);
|
||||
return this._timelineStore.add(entry);
|
||||
}
|
||||
|
||||
/** Updates the entry into the store with the given [roomId, eventKey] combination.
|
||||
* If not yet present, will insert. Might be slower than add.
|
||||
* @param {Entry} entry the entry to update.
|
||||
* @return {Promise<>} a promise resolving to undefined if the operation was successful, or a StorageError if not.
|
||||
* @param entry the entry to update.
|
||||
* @return a promise resolving to undefined if the operation was successful, or a StorageError if not.
|
||||
*/
|
||||
update(entry) {
|
||||
this._timelineStore.put(entry);
|
||||
update(entry: StorageEntry): Promise<IDBValidKey> {
|
||||
return this._timelineStore.put(entry);
|
||||
}
|
||||
|
||||
get(roomId, eventKey) {
|
||||
get(roomId: string, eventKey: EventKey): Promise<StorageEntry | null> {
|
||||
return this._timelineStore.get(encodeKey(roomId, eventKey.fragmentId, eventKey.eventIndex));
|
||||
}
|
||||
|
||||
getByEventId(roomId, eventId) {
|
||||
getByEventId(roomId: string, eventId: string): Promise<StorageEntry | null> {
|
||||
return this._timelineStore.index("byEventId").get(encodeEventIdKey(roomId, eventId));
|
||||
}
|
||||
|
||||
removeAllForRoom(roomId) {
|
||||
removeAllForRoom(roomId: string): Promise<undefined> {
|
||||
const minKey = encodeKey(roomId, KeyLimits.minStorageKey, KeyLimits.minStorageKey);
|
||||
const maxKey = encodeKey(roomId, KeyLimits.maxStorageKey, KeyLimits.maxStorageKey);
|
||||
const range = this._timelineStore.IDBKeyRange.bound(minKey, maxKey);
|
||||
this._timelineStore.delete(range);
|
||||
return this._timelineStore.delete(range);
|
||||
}
|
||||
}
|
|
@ -14,20 +14,34 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { StorageError } from "../../common.js";
|
||||
import {KeyLimits} from "../../common.js";
|
||||
import { encodeUint32 } from "../utils.js";
|
||||
import { StorageError } from "../../common";
|
||||
import {KeyLimits} from "../../common";
|
||||
import { encodeUint32 } from "../utils";
|
||||
import {Store} from "../Store";
|
||||
|
||||
function encodeKey(roomId, fragmentId) {
|
||||
interface Fragment {
|
||||
roomId: string;
|
||||
id: number;
|
||||
previousId: number | null;
|
||||
nextId: number | null;
|
||||
previousToken: string | null;
|
||||
nextToken: string | null;
|
||||
}
|
||||
|
||||
type FragmentEntry = Fragment & { key: string }
|
||||
|
||||
function encodeKey(roomId: string, fragmentId: number): string {
|
||||
return `${roomId}|${encodeUint32(fragmentId)}`;
|
||||
}
|
||||
|
||||
export class TimelineFragmentStore {
|
||||
constructor(store) {
|
||||
private _store: Store<FragmentEntry>;
|
||||
|
||||
constructor(store: Store<FragmentEntry>) {
|
||||
this._store = store;
|
||||
}
|
||||
|
||||
_allRange(roomId) {
|
||||
_allRange(roomId: string): IDBKeyRange {
|
||||
try {
|
||||
return this._store.IDBKeyRange.bound(
|
||||
encodeKey(roomId, KeyLimits.minStorageKey),
|
||||
|
@ -38,13 +52,13 @@ export class TimelineFragmentStore {
|
|||
}
|
||||
}
|
||||
|
||||
all(roomId) {
|
||||
all(roomId: string): Promise<FragmentEntry[]> {
|
||||
return this._store.selectAll(this._allRange(roomId));
|
||||
}
|
||||
|
||||
/** Returns the fragment without a nextToken and without nextId,
|
||||
if any, with the largest id if there are multiple (which should not happen) */
|
||||
liveFragment(roomId) {
|
||||
liveFragment(roomId: string): Promise<FragmentEntry | undefined> {
|
||||
// why do we need this?
|
||||
// Ok, take the case where you've got a /context fragment and a /sync fragment
|
||||
// They are not connected. So, upon loading the persister, which one do we take? We can't sort them ...
|
||||
|
@ -60,20 +74,20 @@ export class TimelineFragmentStore {
|
|||
// should generate an id an return it?
|
||||
// depends if we want to do anything smart with fragment ids,
|
||||
// like give them meaning depending on range. not for now probably ...
|
||||
add(fragment) {
|
||||
fragment.key = encodeKey(fragment.roomId, fragment.id);
|
||||
this._store.add(fragment);
|
||||
add(fragment: Fragment): Promise<IDBValidKey> {
|
||||
(fragment as any).key = encodeKey(fragment.roomId, fragment.id);
|
||||
return this._store.add(fragment as FragmentEntry);
|
||||
}
|
||||
|
||||
update(fragment) {
|
||||
this._store.put(fragment);
|
||||
update(fragment: FragmentEntry): Promise<IDBValidKey> {
|
||||
return this._store.put(fragment);
|
||||
}
|
||||
|
||||
get(roomId, fragmentId) {
|
||||
get(roomId: string, fragmentId: number): Promise<FragmentEntry | null> {
|
||||
return this._store.get(encodeKey(roomId, fragmentId));
|
||||
}
|
||||
|
||||
removeAllForRoom(roomId) {
|
||||
this._store.delete(this._allRange(roomId));
|
||||
removeAllForRoom(roomId: string): Promise<undefined> {
|
||||
return this._store.delete(this._allRange(roomId));
|
||||
}
|
||||
}
|
|
@ -13,31 +13,41 @@ 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 {MIN_UNICODE, MAX_UNICODE} from "./common.js";
|
||||
import {MIN_UNICODE, MAX_UNICODE} from "./common";
|
||||
import {Store} from "../Store";
|
||||
|
||||
function encodeKey(roomId, targetEventId, relType, sourceEventId) {
|
||||
function encodeKey(roomId: string, targetEventId: string, relType: string, sourceEventId: string): string {
|
||||
return `${roomId}|${targetEventId}|${relType}|${sourceEventId}`;
|
||||
}
|
||||
|
||||
function decodeKey(key) {
|
||||
interface RelationEntry {
|
||||
roomId: string;
|
||||
targetEventId: string;
|
||||
sourceEventId: string;
|
||||
relType: string;
|
||||
}
|
||||
|
||||
function decodeKey(key: string): RelationEntry {
|
||||
const [roomId, targetEventId, relType, sourceEventId] = key.split("|");
|
||||
return {roomId, targetEventId, relType, sourceEventId};
|
||||
}
|
||||
|
||||
export class TimelineRelationStore {
|
||||
constructor(store) {
|
||||
private _store: Store<{ key: string }>;
|
||||
|
||||
constructor(store: Store<{ key: string }>) {
|
||||
this._store = store;
|
||||
}
|
||||
|
||||
add(roomId, targetEventId, relType, sourceEventId) {
|
||||
add(roomId: string, targetEventId: string, relType: string, sourceEventId: string): Promise<IDBValidKey> {
|
||||
return this._store.add({key: encodeKey(roomId, targetEventId, relType, sourceEventId)});
|
||||
}
|
||||
|
||||
remove(roomId, targetEventId, relType, sourceEventId) {
|
||||
remove(roomId: string, targetEventId: string, relType: string, sourceEventId: string): Promise<undefined> {
|
||||
return this._store.delete(encodeKey(roomId, targetEventId, relType, sourceEventId));
|
||||
}
|
||||
|
||||
removeAllForTarget(roomId, targetId) {
|
||||
removeAllForTarget(roomId: string, targetId: string): Promise<undefined> {
|
||||
const range = this._store.IDBKeyRange.bound(
|
||||
encodeKey(roomId, targetId, MIN_UNICODE, MIN_UNICODE),
|
||||
encodeKey(roomId, targetId, MAX_UNICODE, MAX_UNICODE),
|
||||
|
@ -47,7 +57,7 @@ export class TimelineRelationStore {
|
|||
return this._store.delete(range);
|
||||
}
|
||||
|
||||
async getForTargetAndType(roomId, targetId, relType) {
|
||||
async getForTargetAndType(roomId: string, targetId: string, relType: string): Promise<RelationEntry[]> {
|
||||
// exclude both keys as they are theoretical min and max,
|
||||
// but we should't have a match for just the room id, or room id with max
|
||||
const range = this._store.IDBKeyRange.bound(
|
||||
|
@ -60,7 +70,7 @@ export class TimelineRelationStore {
|
|||
return items.map(i => decodeKey(i.key));
|
||||
}
|
||||
|
||||
async getAllForTarget(roomId, targetId) {
|
||||
async getAllForTarget(roomId: string, targetId: string): Promise<RelationEntry[]> {
|
||||
// exclude both keys as they are theoretical min and max,
|
||||
// but we should't have a match for just the room id, or room id with max
|
||||
const range = this._store.IDBKeyRange.bound(
|
|
@ -13,21 +13,30 @@ 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 {Store} from "../Store";
|
||||
|
||||
interface UserIdentity {
|
||||
userId: string;
|
||||
roomIds: string[];
|
||||
deviceTrackingStatus: number;
|
||||
}
|
||||
|
||||
export class UserIdentityStore {
|
||||
constructor(store) {
|
||||
private _store: Store<UserIdentity>;
|
||||
|
||||
constructor(store: Store<UserIdentity>) {
|
||||
this._store = store;
|
||||
}
|
||||
|
||||
get(userId) {
|
||||
get(userId: string): Promise<UserIdentity | null> {
|
||||
return this._store.get(userId);
|
||||
}
|
||||
|
||||
set(userIdentity) {
|
||||
this._store.put(userIdentity);
|
||||
set(userIdentity: UserIdentity): Promise<IDBValidKey> {
|
||||
return this._store.put(userIdentity);
|
||||
}
|
||||
|
||||
remove(userId) {
|
||||
remove(userId: string): Promise<undefined> {
|
||||
return this._store.delete(userId);
|
||||
}
|
||||
}
|
|
@ -15,8 +15,11 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { IDBRequestError } from "./error.js";
|
||||
import { StorageError } from "../common.js";
|
||||
import { IDBRequestError } from "./error";
|
||||
import { StorageError } from "../common";
|
||||
|
||||
export const NOT_DONE = { done: false };
|
||||
export const DONE = { done: true };
|
||||
|
||||
let needsSyncPromise = false;
|
||||
|
||||
|
@ -25,7 +28,7 @@ let needsSyncPromise = false;
|
|||
If this is the case, promises need to be resolved
|
||||
synchronously from the idb request handler to prevent the transaction from closing prematurely.
|
||||
*/
|
||||
export async function checkNeedsSyncPromise() {
|
||||
export async function checkNeedsSyncPromise(): Promise<boolean> {
|
||||
// important to have it turned off while doing the test,
|
||||
// otherwise reqAsPromise would not fail
|
||||
needsSyncPromise = false;
|
||||
|
@ -49,51 +52,57 @@ export async function checkNeedsSyncPromise() {
|
|||
}
|
||||
|
||||
// storage keys are defined to be unsigned 32bit numbers in KeyLimits, which is assumed by idb
|
||||
export function encodeUint32(n) {
|
||||
export function encodeUint32(n: number): string {
|
||||
const hex = n.toString(16);
|
||||
return "0".repeat(8 - hex.length) + hex;
|
||||
}
|
||||
|
||||
// used for logs where timestamp is part of key, which is larger than 32 bit
|
||||
export function encodeUint64(n) {
|
||||
export function encodeUint64(n: number): string {
|
||||
const hex = n.toString(16);
|
||||
return "0".repeat(16 - hex.length) + hex;
|
||||
}
|
||||
|
||||
export function decodeUint32(str) {
|
||||
export function decodeUint32(str: string): number {
|
||||
return parseInt(str, 16);
|
||||
}
|
||||
|
||||
export function openDatabase(name, createObjectStore, version, idbFactory = window.indexedDB) {
|
||||
type CreateObjectStore = (db : IDBDatabase, txn: IDBTransaction | null, oldVersion: number, version: number) => any
|
||||
|
||||
export function openDatabase(name: string, createObjectStore: CreateObjectStore, version: number, idbFactory: IDBFactory = window.indexedDB): Promise<IDBDatabase> {
|
||||
const req = idbFactory.open(name, version);
|
||||
req.onupgradeneeded = (ev) => {
|
||||
const db = ev.target.result;
|
||||
const txn = ev.target.transaction;
|
||||
req.onupgradeneeded = (ev : IDBVersionChangeEvent) => {
|
||||
const req = ev.target as IDBRequest<IDBDatabase>;
|
||||
const db = req.result;
|
||||
const txn = req.transaction;
|
||||
const oldVersion = ev.oldVersion;
|
||||
createObjectStore(db, txn, oldVersion, version);
|
||||
};
|
||||
return reqAsPromise(req);
|
||||
}
|
||||
|
||||
export function reqAsPromise(req) {
|
||||
export function reqAsPromise<T>(req: IDBRequest<T>): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
req.addEventListener("success", event => {
|
||||
resolve(event.target.result);
|
||||
resolve((event.target as IDBRequest<T>).result);
|
||||
// @ts-ignore
|
||||
needsSyncPromise && Promise._flush && Promise._flush();
|
||||
});
|
||||
req.addEventListener("error", event => {
|
||||
const error = new IDBRequestError(event.target);
|
||||
const error = new IDBRequestError(event.target as IDBRequest<T>);
|
||||
reject(error);
|
||||
// @ts-ignore
|
||||
needsSyncPromise && Promise._flush && Promise._flush();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function txnAsPromise(txn) {
|
||||
export function txnAsPromise(txn): Promise<void> {
|
||||
let error;
|
||||
return new Promise((resolve, reject) => {
|
||||
txn.addEventListener("complete", () => {
|
||||
resolve();
|
||||
// @ts-ignore
|
||||
needsSyncPromise && Promise._flush && Promise._flush();
|
||||
});
|
||||
txn.addEventListener("error", event => {
|
||||
|
@ -112,33 +121,41 @@ export function txnAsPromise(txn) {
|
|||
error = new StorageError(`Transaction on ${dbName} with stores ${storeNames} was aborted.`);
|
||||
}
|
||||
reject(error);
|
||||
// @ts-ignore
|
||||
needsSyncPromise && Promise._flush && Promise._flush();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function iterateCursor(cursorRequest, processValue) {
|
||||
type CursorIterator<T, I extends IDBCursor> = I extends IDBCursorWithValue ?
|
||||
(value: T, key: IDBValidKey, cursor: IDBCursorWithValue) => { done: boolean, jumpTo?: IDBValidKey } :
|
||||
(value: undefined, key: IDBValidKey, cursor: IDBCursor) => { done: boolean, jumpTo?: IDBValidKey }
|
||||
|
||||
export function iterateCursor<T, I extends IDBCursor = IDBCursorWithValue>(cursorRequest: IDBRequest<I | null>, processValue: CursorIterator<T, I>): Promise<boolean> {
|
||||
// TODO: does cursor already have a value here??
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
cursorRequest.onerror = () => {
|
||||
reject(new IDBRequestError(cursorRequest));
|
||||
// @ts-ignore
|
||||
needsSyncPromise && Promise._flush && Promise._flush();
|
||||
};
|
||||
// collect results
|
||||
cursorRequest.onsuccess = (event) => {
|
||||
const cursor = event.target.result;
|
||||
const cursor = (event.target as IDBRequest<I>).result;
|
||||
if (!cursor) {
|
||||
resolve(false);
|
||||
// @ts-ignore
|
||||
needsSyncPromise && Promise._flush && Promise._flush();
|
||||
return; // end of results
|
||||
}
|
||||
const result = processValue(cursor.value, cursor.key, cursor);
|
||||
const result = processValue(cursor["value"], cursor.key, cursor);
|
||||
// TODO: don't use object for result and assume it's jumpTo when not === true/false or undefined
|
||||
const done = result?.done;
|
||||
const jumpTo = result?.jumpTo;
|
||||
|
||||
if (done) {
|
||||
resolve(true);
|
||||
// @ts-ignore
|
||||
needsSyncPromise && Promise._flush && Promise._flush();
|
||||
} else if(jumpTo) {
|
||||
cursor.continue(jumpTo);
|
||||
|
@ -151,16 +168,20 @@ export function iterateCursor(cursorRequest, processValue) {
|
|||
});
|
||||
}
|
||||
|
||||
export async function fetchResults(cursor, isDone) {
|
||||
const results = [];
|
||||
await iterateCursor(cursor, (value) => {
|
||||
type Pred<T> = (value: T) => boolean
|
||||
|
||||
export async function fetchResults<T>(cursor: IDBRequest, isDone: Pred<T[]>): Promise<T[]> {
|
||||
const results: T[] = [];
|
||||
await iterateCursor<T>(cursor, (value) => {
|
||||
results.push(value);
|
||||
return {done: isDone(results)};
|
||||
});
|
||||
return results;
|
||||
}
|
||||
|
||||
export async function select(db, storeName, toCursor, isDone) {
|
||||
type ToCursor = (store: IDBObjectStore) => IDBRequest
|
||||
|
||||
export async function select<T>(db: IDBDatabase, storeName: string, toCursor: ToCursor, isDone: Pred<T[]>): Promise<T[]> {
|
||||
if (!isDone) {
|
||||
isDone = () => false;
|
||||
}
|
||||
|
@ -173,7 +194,7 @@ export async function select(db, storeName, toCursor, isDone) {
|
|||
return await fetchResults(cursor, isDone);
|
||||
}
|
||||
|
||||
export async function findStoreValue(db, storeName, toCursor, matchesValue) {
|
||||
export async function findStoreValue<T>(db: IDBDatabase, storeName: string, toCursor: ToCursor, matchesValue: Pred<T>): Promise<T> {
|
||||
if (!matchesValue) {
|
||||
matchesValue = () => true;
|
||||
}
|
||||
|
@ -185,11 +206,12 @@ export async function findStoreValue(db, storeName, toCursor, matchesValue) {
|
|||
const store = tx.objectStore(storeName);
|
||||
const cursor = await reqAsPromise(toCursor(store));
|
||||
let match;
|
||||
const matched = await iterateCursor(cursor, (value) => {
|
||||
const matched = await iterateCursor<T>(cursor, (value) => {
|
||||
if (matchesValue(value)) {
|
||||
match = value;
|
||||
return true;
|
||||
return { done: true };
|
||||
}
|
||||
return { done: false };
|
||||
});
|
||||
if (!matched) {
|
||||
throw new StorageError("Value not found");
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import {Transaction} from "./Transaction.js";
|
||||
import { STORE_MAP, STORE_NAMES } from "../common.js";
|
||||
import { STORE_MAP, STORE_NAMES } from "../common";
|
||||
|
||||
export class Storage {
|
||||
constructor(initialStoreValues = {}) {
|
||||
|
|
|
@ -14,20 +14,15 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
export class InviteStore {
|
||||
constructor(inviteStore) {
|
||||
this._inviteStore = inviteStore;
|
||||
}
|
||||
export type Content = { [key: string]: any }
|
||||
|
||||
getAll() {
|
||||
return this._inviteStore.selectAll();
|
||||
}
|
||||
|
||||
set(invite) {
|
||||
return this._inviteStore.put(invite);
|
||||
}
|
||||
|
||||
remove(roomId) {
|
||||
this._inviteStore.delete(roomId);
|
||||
}
|
||||
export interface RoomEvent {
|
||||
content: Content;
|
||||
type: string;
|
||||
event_id: string;
|
||||
sender: string;
|
||||
origin_server_ts: number;
|
||||
unsigned?: Content;
|
||||
}
|
||||
|
||||
export type StateEvent = RoomEvent & { prev_content?: Content, state_key: string }
|
|
@ -15,8 +15,8 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import {FDBFactory, FDBKeyRange} from "../../lib/fake-indexeddb/index.js";
|
||||
import {StorageFactory} from "../matrix/storage/idb/StorageFactory.js";
|
||||
import {StorageFactory} from "../matrix/storage/idb/StorageFactory";
|
||||
|
||||
export function createMockStorage() {
|
||||
return new StorageFactory(null, new FDBFactory(), FDBKeyRange).create(1);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
|
||||
import {createFetchRequest} from "./dom/request/fetch.js";
|
||||
import {xhrRequest} from "./dom/request/xhr.js";
|
||||
import {StorageFactory} from "../../matrix/storage/idb/StorageFactory.js";
|
||||
import {StorageFactory} from "../../matrix/storage/idb/StorageFactory";
|
||||
import {SessionInfoStorage} from "../../matrix/sessioninfo/localstorage/SessionInfoStorage.js";
|
||||
import {SettingsStorage} from "./dom/SettingsStorage.js";
|
||||
import {Encoding} from "./utils/Encoding.js";
|
||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
|
||||
// polyfills needed for IE11
|
||||
import Promise from "../../../lib/es6-promise/index.js";
|
||||
import {checkNeedsSyncPromise} from "../../matrix/storage/idb/utils.js";
|
||||
import {checkNeedsSyncPromise} from "../../matrix/storage/idb/utils";
|
||||
|
||||
if (typeof window.Promise === "undefined") {
|
||||
window.Promise = Promise;
|
||||
|
|
Reference in a new issue