Compare commits

...
This repository has been archived on 2022-08-19. You can view files and clone it, but cannot push or open issues or pull requests.

70 commits

Author SHA1 Message Date
Danila Fedorin
08e7616897 Remove unnecessary generic 2021-08-17 09:57:36 -07:00
Danila Fedorin
410bd4ab8b Avoid using small objects where possible 2021-08-17 09:57:36 -07:00
Danila Fedorin
a4375c0e15 Update type signatures to match IDB's types 2021-08-17 09:57:36 -07:00
Danila Fedorin
3ed639d1c5 Allow query to be undefined (I think that's allowed) 2021-08-17 09:57:36 -07:00
Danila Fedorin
b31cf4fdce Add missing return types and semicolons 2021-08-17 09:57:36 -07:00
Danila Fedorin
31b02f1eff Add missing semicolons 2021-08-17 09:57:36 -07:00
Danila Fedorin
c8f4cb5046 Extract events to their own file 2021-08-17 09:57:36 -07:00
Danila Fedorin
1f368dcec1 Add an interface for events 2021-08-17 09:57:36 -07:00
Danila Fedorin
6c6991e622 Add type annotations to StorageFactory 2021-08-17 09:57:35 -07:00
Danila Fedorin
fd4ddf5667 Start migrating StorageFactory to TypeScript 2021-08-17 09:57:35 -07:00
Danila Fedorin
f9fa7cdb49 Translate quirks 2021-08-17 09:57:35 -07:00
Danila Fedorin
2cb8944a78 Start migrating quirks to TypeScript 2021-08-17 09:57:35 -07:00
Danila Fedorin
9fcc776a29 Add initial translation of schema 2021-08-17 09:57:35 -07:00
Danila Fedorin
39e9b828e6 Start migrating schema to TypeScript 2021-08-17 09:57:35 -07:00
Danila Fedorin
64b767e7eb Add type annotations to export 2021-08-17 09:57:35 -07:00
Danila Fedorin
2eea5d5ab8 Start migrating export to TypeScript 2021-08-17 09:57:35 -07:00
Danila Fedorin
287242deda Add type annotations to Storage 2021-08-17 09:57:35 -07:00
Danila Fedorin
31c951c68c Start migrating Storage to TypeScript 2021-08-17 09:57:35 -07:00
Danila Fedorin
ea4de29975 Properly encode session store 2021-08-17 09:57:35 -07:00
Danila Fedorin
f390d21881 Add type annotations to Transaction 2021-08-17 09:57:35 -07:00
Danila Fedorin
8c7e13f40f Start migrating Transaction to TypeScript 2021-08-17 09:57:35 -07:00
Danila Fedorin
e0aa804971 Translate AccountDataStore 2021-08-17 09:57:35 -07:00
Danila Fedorin
9e459aa003 Allow queries on keys (docs say this works) 2021-08-17 09:57:35 -07:00
Danila Fedorin
61d0108b3b Add type annotations to OperationStore 2021-08-17 09:57:35 -07:00
Danila Fedorin
51d8e3cb66 Start migrating OperationStore to TypeScript 2021-08-17 09:57:35 -07:00
Danila Fedorin
c21b187683 Add type annotations to GroupSessionDecryptionStore 2021-08-17 09:57:35 -07:00
Danila Fedorin
736b122fc7 Start migrating GroupSessionDecryptionStore to TypeScript 2021-08-17 09:57:35 -07:00
Danila Fedorin
016b51ba37 Add type annotations to OutboundGroupSessionStore 2021-08-17 09:57:35 -07:00
Danila Fedorin
3128e072fd Start migrating OutboundGroupSessionStore to TypeScript 2021-08-17 09:57:35 -07:00
Danila Fedorin
2883149086 Add type annotations to InboundGroupSessionStore 2021-08-17 09:57:35 -07:00
Danila Fedorin
f4ba26cb1e Start migrating InboundGroupSessionStore to TypeScript 2021-08-17 09:57:35 -07:00
Danila Fedorin
3213a0baa0 Add type annotations to OlmSessionStore 2021-08-17 09:57:35 -07:00
Danila Fedorin
15d7d57b09 Start migrating OlmSessionStore to TypeScript 2021-08-17 09:57:35 -07:00
Danila Fedorin
279f149408 Add type annotations to DeviceIdentityStore 2021-08-17 09:57:35 -07:00
Danila Fedorin
218bac7883 Start migrating DeviceIdentityStore to TypeScript 2021-08-17 09:57:35 -07:00
Danila Fedorin
c5a209258e Add type annotations to UserIdentityStore 2021-08-17 09:57:35 -07:00
Danila Fedorin
5fb7871ca6 Start migrating UserIdentityStore to TypeScript 2021-08-17 09:57:35 -07:00
Danila Fedorin
ee00aa3339 Add type annotations to PendingEventStore 2021-08-17 09:57:35 -07:00
Danila Fedorin
e433a234fe Start migrating PendingEventStore.js to TypeScript 2021-08-17 09:57:35 -07:00
Danila Fedorin
526ff53728 Add type annotations to TimelineFragmentStore 2021-08-17 09:57:35 -07:00
Danila Fedorin
3ec222eae3 Start migrating TimelineFragmentStore.js to TypeScript 2021-08-17 09:57:35 -07:00
Danila Fedorin
7f907427ee Add type annotations to RoomMemberStore 2021-08-17 09:57:35 -07:00
Danila Fedorin
243d0e76ff Add more data to member interface 2021-08-17 09:57:35 -07:00
Danila Fedorin
ec38337223 Start migrating RoomMemberStore.js to TypeScript 2021-08-17 09:57:35 -07:00
Danila Fedorin
9b4b303b01 Add type annotations to RoomStateStore.
Not sure how to type events, since they're so malleable.
2021-08-17 09:57:35 -07:00
Danila Fedorin
50897cfbe3 Start migrating RoomStateStore.js to TypeScript 2021-08-17 09:57:35 -07:00
Danila Fedorin
8e93487ebe Add type annotations to TimelineRelationStore 2021-08-17 09:57:35 -07:00
Danila Fedorin
ddf09af05b Start migrating TimelineRelationStore.js to TypeScript 2021-08-17 09:57:35 -07:00
Danila Fedorin
d07c38effd Migrate common.ts 2021-08-17 09:57:35 -07:00
Danila Fedorin
3b5b91cf1b Update signatures in other stores to return promises 2021-08-17 09:57:35 -07:00
Danila Fedorin
88ecc58b14 Add initial translation of TimelineEventStore.js 2021-08-17 09:57:35 -07:00
Danila Fedorin
53228e88a2 Start migrating TimelineEventStore.js to TypeScript 2021-08-17 09:57:35 -07:00
Danila Fedorin
de6fdba526 Make operations return promises 2021-08-17 09:57:35 -07:00
Danila Fedorin
29c87b7c01 Add initial translation of InviteStore.js 2021-08-17 09:57:35 -07:00
Danila Fedorin
8ad2857c6a Annotate with return types 2021-08-17 09:57:35 -07:00
Danila Fedorin
f8613e9e96 Add initial translation of RoomSummaryStore.js 2021-08-17 09:57:35 -07:00
Danila Fedorin
5177c35d0d Add initial translation of SessionStore.js 2021-08-17 09:57:35 -07:00
Danila Fedorin
97a50c835d Add initial stab at annotating Store 2021-08-17 09:57:35 -07:00
Danila Fedorin
2b44878332 Restrict type for iterateCursor 2021-08-17 09:57:35 -07:00
Danila Fedorin
46c306b487 Start migrating Store.js to TypeScript 2021-08-17 09:57:35 -07:00
Danila Fedorin
e837a91a80 Stop relying on QueryTargetWrapper in QueryTarget. 2021-08-17 09:57:35 -07:00
Danila Fedorin
1d5b105c34 Add initial stab at annotating QueryTarget 2021-08-17 09:57:35 -07:00
Danila Fedorin
fefa15cd85 Start migrating QueryTarget.js to TypeScript 2021-08-17 09:57:35 -07:00
Danila Fedorin
a31f6a2c52 Add initial stab at annotating error 2021-08-17 09:57:35 -07:00
Danila Fedorin
69ab345a89 Start migrating error.js to TypeScript 2021-08-17 09:57:35 -07:00
Danila Fedorin
a0017cb720 Seemingly fix a bug in utils.ts 2021-08-17 09:57:35 -07:00
Danila Fedorin
0c80f78e9b Add initial stab at annotating utils 2021-08-17 09:57:35 -07:00
Danila Fedorin
4fb93ad104 Start migrating utils.js to TypeScript 2021-08-17 09:57:35 -07:00
Danila Fedorin
caed99df69 Add initial stab at annotating common 2021-08-17 09:57:35 -07:00
Danila Fedorin
96a4ef47a7 Start migrating common.js to TypeScript 2021-08-17 09:57:35 -07:00
38 changed files with 961 additions and 624 deletions

View file

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

View file

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

View file

@ -14,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;
}

View file

@ -14,14 +14,29 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {iterateCursor, reqAsPromise} from "./utils.js";
import {iterateCursor, DONE, NOT_DONE, reqAsPromise} from "./utils";
export class QueryTarget {
constructor(target) {
type Reducer<A,B> = (acc: B, val: A) => B
export type IDBQuery = IDBValidKey | IDBKeyRange | undefined | null
interface QueryTargetInterface<T> {
openCursor: (range?: IDBQuery, direction?: IDBCursorDirection | undefined) => IDBRequest<IDBCursorWithValue | null>;
openKeyCursor: (range?: IDBQuery, direction?: IDBCursorDirection | undefined) => IDBRequest<IDBCursor | null>;
supports: (method: string) => boolean;
keyPath: string | string[];
get: (key: IDBValidKey | IDBKeyRange) => IDBRequest<T | null>;
getKey: (key: IDBValidKey | IDBKeyRange) => IDBRequest<IDBValidKey | undefined>;
}
export class QueryTarget<T> {
protected _target: QueryTargetInterface<T>;
constructor(target: QueryTargetInterface<T>) {
this._target = target;
}
_openCursor(range, direction) {
_openCursor(range?: IDBQuery, direction?: IDBCursorDirection): IDBRequest<IDBCursorWithValue | null> {
if (range && direction) {
return this._target.openCursor(range, direction);
} else if (range) {
@ -33,95 +48,99 @@ export class QueryTarget {
}
}
supports(methodName) {
supports(methodName: string): boolean {
return this._target.supports(methodName);
}
get(key) {
get(key: IDBValidKey | IDBKeyRange): Promise<T | null> {
return reqAsPromise(this._target.get(key));
}
getKey(key) {
getKey(key: IDBValidKey | IDBKeyRange): Promise<IDBValidKey | undefined> {
if (this._target.supports("getKey")) {
return reqAsPromise(this._target.getKey(key));
} else {
return reqAsPromise(this._target.get(key)).then(value => {
if (value) {
return value[this._target.keyPath];
let keyPath = this._target.keyPath;
if (typeof keyPath === "string") {
keyPath = [keyPath];
}
return keyPath.reduce((obj, key) => obj[key], value);
}
});
}
}
reduce(range, reducer, initialValue) {
reduce<B>(range: IDBQuery, reducer: Reducer<T,B>, initialValue: B): Promise<boolean> {
return this._reduce(range, reducer, initialValue, "next");
}
reduceReverse(range, reducer, initialValue) {
reduceReverse<B>(range: IDBQuery, reducer: Reducer<T,B>, initialValue: B): Promise<boolean> {
return this._reduce(range, reducer, initialValue, "prev");
}
selectLimit(range, amount) {
selectLimit(range: IDBQuery, amount: number): Promise<T[]> {
return this._selectLimit(range, amount, "next");
}
selectLimitReverse(range, amount) {
selectLimitReverse(range: IDBQuery, amount: number): Promise<T[]> {
return this._selectLimit(range, amount, "prev");
}
selectWhile(range, predicate) {
selectWhile(range: IDBQuery, predicate: (v: T) => boolean): Promise<T[]> {
return this._selectWhile(range, predicate, "next");
}
selectWhileReverse(range, predicate) {
selectWhileReverse(range: IDBQuery, predicate: (v: T) => boolean): Promise<T[]> {
return this._selectWhile(range, predicate, "prev");
}
async selectAll(range, direction) {
async selectAll(range?: IDBQuery, direction?: IDBCursorDirection): Promise<T[]> {
const cursor = this._openCursor(range, direction);
const results = [];
await iterateCursor(cursor, (value) => {
const results: T[] = [];
await iterateCursor<T>(cursor, (value) => {
results.push(value);
return {done: false};
return NOT_DONE;
});
return results;
}
selectFirst(range) {
selectFirst(range: IDBQuery): Promise<T | undefined> {
return this._find(range, () => true, "next");
}
selectLast(range) {
selectLast(range: IDBQuery): Promise<T | undefined> {
return this._find(range, () => true, "prev");
}
find(range, predicate) {
find(range: IDBQuery, predicate: (v: T) => boolean): Promise<T | undefined> {
return this._find(range, predicate, "next");
}
findReverse(range, predicate) {
findReverse(range: IDBQuery, predicate: (v : T) => boolean): Promise<T | undefined> {
return this._find(range, predicate, "prev");
}
async findMaxKey(range) {
async findMaxKey(range: IDBQuery): Promise<IDBValidKey | undefined> {
const cursor = this._target.openKeyCursor(range, "prev");
let maxKey;
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;

View file

@ -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();
}
}

View file

@ -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) {

View file

@ -1,164 +0,0 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import {QueryTarget} from "./QueryTarget.js";
import {IDBRequestAttemptError} from "./error.js";
const LOG_REQUESTS = false;
function logRequest(method, params, source) {
const storeName = source?.name;
const databaseName = source?.transaction?.db?.name;
console.info(`${databaseName}.${storeName}.${method}(${params.map(p => JSON.stringify(p)).join(", ")})`);
}
class QueryTargetWrapper {
constructor(qt) {
this._qt = qt;
}
get keyPath() {
if (this._qt.objectStore) {
return this._qt.objectStore.keyPath;
} else {
return this._qt.keyPath;
}
}
supports(methodName) {
return !!this._qt[methodName];
}
openKeyCursor(...params) {
try {
// not supported on Edge 15
if (!this._qt.openKeyCursor) {
LOG_REQUESTS && logRequest("openCursor", params, this._qt);
return this.openCursor(...params);
}
LOG_REQUESTS && logRequest("openKeyCursor", params, this._qt);
return this._qt.openKeyCursor(...params);
} catch(err) {
throw new IDBRequestAttemptError("openKeyCursor", this._qt, err, params);
}
}
openCursor(...params) {
try {
LOG_REQUESTS && logRequest("openCursor", params, this._qt);
return this._qt.openCursor(...params);
} catch(err) {
throw new IDBRequestAttemptError("openCursor", this._qt, err, params);
}
}
put(...params) {
try {
LOG_REQUESTS && logRequest("put", params, this._qt);
return this._qt.put(...params);
} catch(err) {
throw new IDBRequestAttemptError("put", this._qt, err, params);
}
}
add(...params) {
try {
LOG_REQUESTS && logRequest("add", params, this._qt);
return this._qt.add(...params);
} catch(err) {
throw new IDBRequestAttemptError("add", this._qt, err, params);
}
}
get(...params) {
try {
LOG_REQUESTS && logRequest("get", params, this._qt);
return this._qt.get(...params);
} catch(err) {
throw new IDBRequestAttemptError("get", this._qt, err, params);
}
}
getKey(...params) {
try {
LOG_REQUESTS && logRequest("getKey", params, this._qt);
return this._qt.getKey(...params);
} catch(err) {
throw new IDBRequestAttemptError("getKey", this._qt, err, params);
}
}
delete(...params) {
try {
LOG_REQUESTS && logRequest("delete", params, this._qt);
return this._qt.delete(...params);
} catch(err) {
throw new IDBRequestAttemptError("delete", this._qt, err, params);
}
}
index(...params) {
try {
return this._qt.index(...params);
} catch(err) {
// TODO: map to different error? this is not a request
throw new IDBRequestAttemptError("index", this._qt, err, params);
}
}
}
export class Store extends QueryTarget {
constructor(idbStore, transaction) {
super(new QueryTargetWrapper(idbStore));
this._transaction = transaction;
}
get IDBKeyRange() {
return this._transaction.IDBKeyRange;
}
get _idbStore() {
return this._target;
}
index(indexName) {
return new QueryTarget(new QueryTargetWrapper(this._idbStore.index(indexName)));
}
put(value) {
// If this request fails, the error will bubble up to the transaction and abort it,
// which is the behaviour we want. Therefore, it is ok to not create a promise for this
// request and await it.
//
// Perhaps at some later point, we will want to handle an error (like ConstraintError) for
// individual write requests. In that case, we should add a method that returns a promise (e.g. putAndObserve)
// and call preventDefault on the event to prevent it from aborting the transaction
//
// Note that this can still throw synchronously, like it does for TransactionInactiveError,
// see https://www.w3.org/TR/IndexedDB-2/#transaction-lifetime-concept
this._idbStore.put(value);
}
add(value) {
// ok to not monitor result of request, see comment in `put`.
this._idbStore.add(value);
}
delete(keyOrKeyRange) {
// ok to not monitor result of request, see comment in `put`.
this._idbStore.delete(keyOrKeyRange);
}
}

View file

@ -0,0 +1,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));
}
}

View file

@ -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();
}

View file

@ -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);
}
}

View file

@ -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);

View file

@ -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 => {

View file

@ -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"});
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View 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);
}
}

View file

@ -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));
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View 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);
}
}

View file

@ -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);
}
}

View file

@ -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));
}
}

View file

@ -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(

View file

@ -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);
}
}

View file

@ -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");

View file

@ -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 = {}) {

View file

@ -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 }

View file

@ -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);
}
}

View file

@ -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";

View file

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