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

View file

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

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
export const STORE_NAMES = Object.freeze([ export const STORE_NAMES: Readonly<string[]> = Object.freeze([
"session", "session",
"roomState", "roomState",
"roomSummary", "roomSummary",
@ -35,13 +35,16 @@ export const STORE_NAMES = Object.freeze([
"accountData", "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; nameMap[name] = name;
return nameMap; return nameMap;
}, {})); }, {}));
export class StorageError extends Error { export class StorageError extends Error {
constructor(message, cause) { errcode?: string;
cause?: Error;
constructor(message: string, cause?: Error) {
super(message); super(message);
if (cause) { if (cause) {
this.errcode = cause.name; this.errcode = cause.name;
@ -49,23 +52,23 @@ export class StorageError extends Error {
this.cause = cause; this.cause = cause;
} }
get name() { get name(): string {
return "StorageError"; return "StorageError";
} }
} }
export const KeyLimits = { export const KeyLimits = {
get minStorageKey() { get minStorageKey(): number {
// for indexeddb, we use unsigned 32 bit integers as keys // for indexeddb, we use unsigned 32 bit integers as keys
return 0; return 0;
}, },
get middleStorageKey() { get middleStorageKey(): number {
// for indexeddb, we use unsigned 32 bit integers as keys // for indexeddb, we use unsigned 32 bit integers as keys
return 0x7FFFFFFF; return 0x7FFFFFFF;
}, },
get maxStorageKey() { get maxStorageKey(): number {
// for indexeddb, we use unsigned 32 bit integers as keys // for indexeddb, we use unsigned 32 bit integers as keys
return 0xFFFFFFFF; return 0xFFFFFFFF;
} }

View file

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

View file

@ -14,15 +14,20 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {Transaction} from "./Transaction.js"; import {Transaction} from "./Transaction";
import { STORE_NAMES, StorageError } from "../common.js"; import { STORE_NAMES, StorageError } from "../common";
import { reqAsPromise } from "./utils.js"; import { reqAsPromise } from "./utils";
const WEBKITEARLYCLOSETXNBUG_BOGUS_KEY = "782rh281re38-boguskey"; const WEBKITEARLYCLOSETXNBUG_BOGUS_KEY = "782rh281re38-boguskey";
export class Storage { 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; this._db = idbDatabase;
// @ts-ignore
this._IDBKeyRange = IDBKeyRange; this._IDBKeyRange = IDBKeyRange;
this._hasWebkitEarlyCloseTxnBug = hasWebkitEarlyCloseTxnBug; this._hasWebkitEarlyCloseTxnBug = hasWebkitEarlyCloseTxnBug;
const nameMap = STORE_NAMES.reduce((nameMap, name) => { const nameMap = STORE_NAMES.reduce((nameMap, name) => {
@ -32,14 +37,14 @@ export class Storage {
this.storeNames = Object.freeze(nameMap); this.storeNames = Object.freeze(nameMap);
} }
_validateStoreNames(storeNames) { _validateStoreNames(storeNames: string[]): void {
const idx = storeNames.findIndex(name => !STORE_NAMES.includes(name)); const idx = storeNames.findIndex(name => !STORE_NAMES.includes(name));
if (idx !== -1) { if (idx !== -1) {
throw new StorageError(`Tried top, a transaction unknown store ${storeNames[idx]}`); throw new StorageError(`Tried top, a transaction unknown store ${storeNames[idx]}`);
} }
} }
async readTxn(storeNames) { async readTxn(storeNames: string[]): Promise<Transaction> {
this._validateStoreNames(storeNames); this._validateStoreNames(storeNames);
try { try {
const txn = this._db.transaction(storeNames, "readonly"); const txn = this._db.transaction(storeNames, "readonly");
@ -48,13 +53,14 @@ export class Storage {
if (this._hasWebkitEarlyCloseTxnBug) { if (this._hasWebkitEarlyCloseTxnBug) {
await reqAsPromise(txn.objectStore(storeNames[0]).get(WEBKITEARLYCLOSETXNBUG_BOGUS_KEY)); await reqAsPromise(txn.objectStore(storeNames[0]).get(WEBKITEARLYCLOSETXNBUG_BOGUS_KEY));
} }
// @ts-ignore
return new Transaction(txn, storeNames, this._IDBKeyRange); return new Transaction(txn, storeNames, this._IDBKeyRange);
} catch(err) { } catch(err) {
throw new StorageError("readTxn failed", err); throw new StorageError("readTxn failed", err);
} }
} }
async readWriteTxn(storeNames) { async readWriteTxn(storeNames: string[]): Promise<Transaction> {
this._validateStoreNames(storeNames); this._validateStoreNames(storeNames);
try { try {
const txn = this._db.transaction(storeNames, "readwrite"); const txn = this._db.transaction(storeNames, "readwrite");
@ -63,13 +69,14 @@ export class Storage {
if (this._hasWebkitEarlyCloseTxnBug) { if (this._hasWebkitEarlyCloseTxnBug) {
await reqAsPromise(txn.objectStore(storeNames[0]).get(WEBKITEARLYCLOSETXNBUG_BOGUS_KEY)); await reqAsPromise(txn.objectStore(storeNames[0]).get(WEBKITEARLYCLOSETXNBUG_BOGUS_KEY));
} }
// @ts-ignore
return new Transaction(txn, storeNames, this._IDBKeyRange); return new Transaction(txn, storeNames, this._IDBKeyRange);
} catch(err) { } catch(err) {
throw new StorageError("readWriteTxn failed", err); throw new StorageError("readWriteTxn failed", err);
} }
} }
close() { close(): void {
this._db.close(); this._db.close();
} }
} }

View file

@ -14,18 +14,22 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {Storage} from "./Storage.js"; import {Storage} from "./Storage";
import { openDatabase, reqAsPromise } from "./utils.js"; import { openDatabase, reqAsPromise } from "./utils";
import { exportSession, importSession } from "./export.js"; import { exportSession, importSession } from "./export";
import { schema } from "./schema.js"; import { schema } from "./schema";
import { detectWebkitEarlyCloseTxnBug } from "./quirks.js"; import { detectWebkitEarlyCloseTxnBug } from "./quirks";
const sessionName = sessionId => `hydrogen_session_${sessionId}`; const sessionName = (sessionId: string) => `hydrogen_session_${sessionId}`;
const openDatabaseWithSessionId = function(sessionId, idbFactory) { const openDatabaseWithSessionId = function(sessionId: string, idbFactory: IDBFactory): Promise<IDBDatabase> {
return openDatabase(sessionName(sessionId), createStores, schema.length, idbFactory); 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 // don't assume browser so we can run in node with fake-idb
const glob = this; const glob = this;
if (glob?.navigator?.storage?.persist) { if (glob?.navigator?.storage?.persist) {
@ -43,13 +47,17 @@ async function requestPersistedStorage() {
} }
export class StorageFactory { 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._serviceWorkerHandler = serviceWorkerHandler;
this._idbFactory = idbFactory; this._idbFactory = idbFactory;
// @ts-ignore
this._IDBKeyRange = IDBKeyRange; this._IDBKeyRange = IDBKeyRange;
} }
async create(sessionId) { async create(sessionId: string): Promise<Storage> {
await this._serviceWorkerHandler?.preventConcurrentSessionAccess(sessionId); await this._serviceWorkerHandler?.preventConcurrentSessionAccess(sessionId);
requestPersistedStorage().then(persisted => { requestPersistedStorage().then(persisted => {
// Firefox lies here though, and returns true even if the user denied the request // 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 hasWebkitEarlyCloseTxnBug = await detectWebkitEarlyCloseTxnBug(this._idbFactory);
const db = await openDatabaseWithSessionId(sessionId, this._idbFactory); const db = await openDatabaseWithSessionId(sessionId, this._idbFactory);
// @ts-ignore
return new Storage(db, this._IDBKeyRange, hasWebkitEarlyCloseTxnBug); return new Storage(db, this._IDBKeyRange, hasWebkitEarlyCloseTxnBug);
} }
delete(sessionId) { delete(sessionId: string): Promise<IDBDatabase> {
const databaseName = sessionName(sessionId); const databaseName = sessionName(sessionId);
const req = this._idbFactory.deleteDatabase(databaseName); const req = this._idbFactory.deleteDatabase(databaseName);
return reqAsPromise(req); return reqAsPromise(req);
} }
async export(sessionId) { async export(sessionId: string): Promise<{ [storeName: string]: any }> {
const db = await openDatabaseWithSessionId(sessionId, this._idbFactory); const db = await openDatabaseWithSessionId(sessionId, this._idbFactory);
return await exportSession(db); 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); const db = await openDatabaseWithSessionId(sessionId, this._idbFactory);
return await importSession(db, data); 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; const startIdx = oldVersion || 0;
for(let i = startIdx; i < version; ++i) { 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. limitations under the License.
*/ */
import {txnAsPromise} from "./utils.js"; import {txnAsPromise} from "./utils";
import {StorageError} from "../common.js"; import {StorageError} from "../common";
import {Store} from "./Store.js"; import {Store} from "./Store";
import {SessionStore} from "./stores/SessionStore.js"; import {SessionStore} from "./stores/SessionStore";
import {RoomSummaryStore} from "./stores/RoomSummaryStore.js"; import {RoomSummaryStore} from "./stores/RoomSummaryStore";
import {InviteStore} from "./stores/InviteStore.js"; import {InviteStore} from "./stores/InviteStore";
import {TimelineEventStore} from "./stores/TimelineEventStore.js"; import {TimelineEventStore} from "./stores/TimelineEventStore";
import {TimelineRelationStore} from "./stores/TimelineRelationStore.js"; import {TimelineRelationStore} from "./stores/TimelineRelationStore";
import {RoomStateStore} from "./stores/RoomStateStore.js"; import {RoomStateStore} from "./stores/RoomStateStore";
import {RoomMemberStore} from "./stores/RoomMemberStore.js"; import {RoomMemberStore} from "./stores/RoomMemberStore";
import {TimelineFragmentStore} from "./stores/TimelineFragmentStore.js"; import {TimelineFragmentStore} from "./stores/TimelineFragmentStore";
import {PendingEventStore} from "./stores/PendingEventStore.js"; import {PendingEventStore} from "./stores/PendingEventStore";
import {UserIdentityStore} from "./stores/UserIdentityStore.js"; import {UserIdentityStore} from "./stores/UserIdentityStore";
import {DeviceIdentityStore} from "./stores/DeviceIdentityStore.js"; import {DeviceIdentityStore} from "./stores/DeviceIdentityStore";
import {OlmSessionStore} from "./stores/OlmSessionStore.js"; import {OlmSessionStore} from "./stores/OlmSessionStore";
import {InboundGroupSessionStore} from "./stores/InboundGroupSessionStore.js"; import {InboundGroupSessionStore} from "./stores/InboundGroupSessionStore";
import {OutboundGroupSessionStore} from "./stores/OutboundGroupSessionStore.js"; import {OutboundGroupSessionStore} from "./stores/OutboundGroupSessionStore";
import {GroupSessionDecryptionStore} from "./stores/GroupSessionDecryptionStore.js"; import {GroupSessionDecryptionStore} from "./stores/GroupSessionDecryptionStore";
import {OperationStore} from "./stores/OperationStore.js"; import {OperationStore} from "./stores/OperationStore";
import {AccountDataStore} from "./stores/AccountDataStore.js"; import {AccountDataStore} from "./stores/AccountDataStore";
export class Transaction { 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._txn = txn;
this._allowedStoreNames = allowedStoreNames; this._allowedStoreNames = allowedStoreNames;
this._stores = {}; this._stores = {};
// @ts-ignore
this.IDBKeyRange = IDBKeyRange; this.IDBKeyRange = IDBKeyRange;
} }
_idbStore(name) { _idbStore(name: string): Store<any> {
if (!this._allowedStoreNames.includes(name)) { if (!this._allowedStoreNames.includes(name)) {
// more specific error? this is a bug, so maybe not ... // more specific error? this is a bug, so maybe not ...
throw new StorageError(`Invalid store for transaction: ${name}, only ${this._allowedStoreNames.join(", ")} are allowed.`); 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); return new Store(this._txn.objectStore(name), this);
} }
_store(name, mapStore) { _store(name: string, mapStore: (idbStore: Store<any>) => any): any {
if (!this._stores[name]) { if (!this._stores[name]) {
const idbStore = this._idbStore(name); const idbStore = this._idbStore(name);
this._stores[name] = mapStore(idbStore); this._stores[name] = mapStore(idbStore);
@ -59,83 +64,83 @@ export class Transaction {
return this._stores[name]; return this._stores[name];
} }
get session() { get session(): SessionStore {
return this._store("session", idbStore => new SessionStore(idbStore)); return this._store("session", idbStore => new SessionStore(idbStore));
} }
get roomSummary() { get roomSummary(): RoomSummaryStore {
return this._store("roomSummary", idbStore => new RoomSummaryStore(idbStore)); return this._store("roomSummary", idbStore => new RoomSummaryStore(idbStore));
} }
get archivedRoomSummary() { get archivedRoomSummary(): RoomSummaryStore {
return this._store("archivedRoomSummary", idbStore => new RoomSummaryStore(idbStore)); return this._store("archivedRoomSummary", idbStore => new RoomSummaryStore(idbStore));
} }
get invites() { get invites(): InviteStore {
return this._store("invites", idbStore => new InviteStore(idbStore)); return this._store("invites", idbStore => new InviteStore(idbStore));
} }
get timelineFragments() { get timelineFragments(): TimelineFragmentStore {
return this._store("timelineFragments", idbStore => new TimelineFragmentStore(idbStore)); return this._store("timelineFragments", idbStore => new TimelineFragmentStore(idbStore));
} }
get timelineEvents() { get timelineEvents(): TimelineEventStore {
return this._store("timelineEvents", idbStore => new TimelineEventStore(idbStore)); return this._store("timelineEvents", idbStore => new TimelineEventStore(idbStore));
} }
get timelineRelations() { get timelineRelations(): TimelineRelationStore {
return this._store("timelineRelations", idbStore => new TimelineRelationStore(idbStore)); return this._store("timelineRelations", idbStore => new TimelineRelationStore(idbStore));
} }
get roomState() { get roomState(): RoomStateStore {
return this._store("roomState", idbStore => new RoomStateStore(idbStore)); return this._store("roomState", idbStore => new RoomStateStore(idbStore));
} }
get roomMembers() { get roomMembers(): RoomMemberStore {
return this._store("roomMembers", idbStore => new RoomMemberStore(idbStore)); return this._store("roomMembers", idbStore => new RoomMemberStore(idbStore));
} }
get pendingEvents() { get pendingEvents(): PendingEventStore {
return this._store("pendingEvents", idbStore => new PendingEventStore(idbStore)); return this._store("pendingEvents", idbStore => new PendingEventStore(idbStore));
} }
get userIdentities() { get userIdentities(): UserIdentityStore {
return this._store("userIdentities", idbStore => new UserIdentityStore(idbStore)); return this._store("userIdentities", idbStore => new UserIdentityStore(idbStore));
} }
get deviceIdentities() { get deviceIdentities(): DeviceIdentityStore {
return this._store("deviceIdentities", idbStore => new DeviceIdentityStore(idbStore)); return this._store("deviceIdentities", idbStore => new DeviceIdentityStore(idbStore));
} }
get olmSessions() { get olmSessions(): OlmSessionStore {
return this._store("olmSessions", idbStore => new OlmSessionStore(idbStore)); return this._store("olmSessions", idbStore => new OlmSessionStore(idbStore));
} }
get inboundGroupSessions() { get inboundGroupSessions(): InboundGroupSessionStore {
return this._store("inboundGroupSessions", idbStore => new InboundGroupSessionStore(idbStore)); return this._store("inboundGroupSessions", idbStore => new InboundGroupSessionStore(idbStore));
} }
get outboundGroupSessions() { get outboundGroupSessions(): OutboundGroupSessionStore {
return this._store("outboundGroupSessions", idbStore => new OutboundGroupSessionStore(idbStore)); return this._store("outboundGroupSessions", idbStore => new OutboundGroupSessionStore(idbStore));
} }
get groupSessionDecryptions() { get groupSessionDecryptions(): GroupSessionDecryptionStore {
return this._store("groupSessionDecryptions", idbStore => new GroupSessionDecryptionStore(idbStore)); return this._store("groupSessionDecryptions", idbStore => new GroupSessionDecryptionStore(idbStore));
} }
get operations() { get operations(): OperationStore {
return this._store("operations", idbStore => new OperationStore(idbStore)); return this._store("operations", idbStore => new OperationStore(idbStore));
} }
get accountData() { get accountData(): AccountDataStore {
return this._store("accountData", idbStore => new AccountDataStore(idbStore)); return this._store("accountData", idbStore => new AccountDataStore(idbStore));
} }
complete() { complete(): Promise<void> {
return txnAsPromise(this._txn); return txnAsPromise(this._txn);
} }
abort() { abort(): void {
// TODO: should we wrap the exception in a StorageError? // TODO: should we wrap the exception in a StorageError?
this._txn.abort(); this._txn.abort();
} }

View file

@ -15,10 +15,13 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { StorageError } from "../common.js"; import { StorageError } from "../common";
export class IDBError extends StorageError { 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 storeName = source?.name || "<unknown store>";
const databaseName = source?.transaction?.db?.name || "<unknown db>"; const databaseName = source?.transaction?.db?.name || "<unknown db>";
let fullMessage = `${message} on ${databaseName}.${storeName}`; let fullMessage = `${message} on ${databaseName}.${storeName}`;
@ -34,14 +37,14 @@ export class IDBError extends StorageError {
if (cause) { if (cause) {
fullMessage += cause.message; fullMessage += cause.message;
} }
super(fullMessage, cause); super(fullMessage, cause || undefined);
this.storeName = storeName; this.storeName = storeName;
this.databaseName = databaseName; this.databaseName = databaseName;
} }
} }
export class IDBRequestError extends IDBError { export class IDBRequestError extends IDBError {
constructor(request, message = "IDBRequest failed") { constructor(request: IDBRequest, message: string = "IDBRequest failed") {
const source = request.source; const source = request.source;
const cause = request.error; const cause = request.error;
super(message, source, cause); super(message, source, cause);
@ -49,7 +52,7 @@ export class IDBRequestError extends IDBError {
} }
export class IDBRequestAttemptError extends IDBError { export class IDBRequestAttemptError extends IDBError {
constructor(method, source, cause, params) { constructor(method: string, source, cause: DOMException, params: any[]) {
super(`${method}(${params.map(p => JSON.stringify(p)).join(", ")}) failed`, source, cause); super(`${method}(${params.map(p => JSON.stringify(p)).join(", ")}) failed`, source, cause);
} }
} }

View file

@ -14,17 +14,16 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { iterateCursor, txnAsPromise } from "./utils.js"; import { iterateCursor, NOT_DONE, txnAsPromise } from "./utils";
import { STORE_NAMES } from "../common.js"; import { STORE_NAMES } from "../common";
export async function exportSession(db) { export async function exportSession(db: IDBDatabase): Promise<{ [storeName : string] : any }> {
const NOT_DONE = {done: false};
const txn = db.transaction(STORE_NAMES, "readonly"); const txn = db.transaction(STORE_NAMES, "readonly");
const data = {}; const data = {};
await Promise.all(STORE_NAMES.map(async name => { 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); const store = txn.objectStore(name);
await iterateCursor(store.openCursor(), (value) => { await iterateCursor<any>(store.openCursor(), (value) => {
results.push(value); results.push(value);
return NOT_DONE; return NOT_DONE;
}); });
@ -32,7 +31,7 @@ export async function exportSession(db) {
return data; 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"); const txn = db.transaction(STORE_NAMES, "readwrite");
for (const name of STORE_NAMES) { for (const name of STORE_NAMES) {
const store = txn.objectStore(name); 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 // 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"; const dbName = "hydrogen_webkit_test_inactive_txn_bug";
try { try {
const db = await openDatabase(dbName, db => { 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 {RoomMember, EVENT_TYPE as MEMBER_EVENT_TYPE} from "../../room/members/RoomMember.js";
import {RoomMemberStore} from "./stores/RoomMemberStore.js"; import {RoomMemberStore} from "./stores/RoomMemberStore";
import {SessionStore} from "./stores/SessionStore.js"; import {RoomStateEntry} from "./stores/RoomStateStore";
import {encodeScopeTypeKey} from "./stores/OperationStore.js"; import {SessionStore} from "./stores/SessionStore";
import {encodeScopeTypeKey} from "./stores/OperationStore";
// FUNCTIONS SHOULD ONLY BE APPENDED!! // FUNCTIONS SHOULD ONLY BE APPENDED!!
// the index in the array is the database version // 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? // 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 // how do we deal with schema updates vs existing data migration in a way that
//v1 //v1
function createInitialStores(db) { function createInitialStores(db: IDBDatabase): void {
db.createObjectStore("session", {keyPath: "key"}); db.createObjectStore("session", {keyPath: "key"});
// any way to make keys unique here? (just use put?) // any way to make keys unique here? (just use put?)
db.createObjectStore("roomSummary", {keyPath: "roomId"}); db.createObjectStore("roomSummary", {keyPath: "roomId"});
@ -40,11 +43,12 @@ function createInitialStores(db) {
db.createObjectStore("pendingEvents", {keyPath: "key"}); db.createObjectStore("pendingEvents", {keyPath: "key"});
} }
//v2 //v2
async function createMemberStore(db, txn) { async function createMemberStore(db: IDBDatabase, txn: IDBTransaction): Promise<void> {
const roomMembers = new RoomMemberStore(db.createObjectStore("roomMembers", {keyPath: "key"})); // 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 // migrate existing member state events over
const roomState = txn.objectStore("roomState"); const roomState = txn.objectStore("roomState");
await iterateCursor(roomState.openCursor(), entry => { await iterateCursor<RoomStateEntry>(roomState.openCursor(), entry => {
if (entry.event.type === MEMBER_EVENT_TYPE) { if (entry.event.type === MEMBER_EVENT_TYPE) {
roomState.delete(entry.key); roomState.delete(entry.key);
const member = RoomMember.fromMemberEvent(entry.roomId, entry.event); const member = RoomMember.fromMemberEvent(entry.roomId, entry.event);
@ -52,10 +56,11 @@ async function createMemberStore(db, txn) {
roomMembers.set(member.serialize()); roomMembers.set(member.serialize());
} }
} }
return NOT_DONE;
}); });
} }
//v3 //v3
async function migrateSession(db, txn) { async function migrateSession(db: IDBDatabase, txn: IDBTransaction): Promise<void> {
const session = txn.objectStore("session"); const session = txn.objectStore("session");
try { try {
const PRE_MIGRATION_KEY = 1; const PRE_MIGRATION_KEY = 1;
@ -63,7 +68,8 @@ async function migrateSession(db, txn) {
if (entry) { if (entry) {
session.delete(PRE_MIGRATION_KEY); session.delete(PRE_MIGRATION_KEY);
const {syncToken, syncFilterId, serverVersions} = entry.value; 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("sync", {token: syncToken, filterId: syncFilterId});
store.set("serverVersions", serverVersions); store.set("serverVersions", serverVersions);
} }
@ -73,7 +79,7 @@ async function migrateSession(db, txn) {
} }
} }
//v4 //v4
function createE2EEStores(db) { function createE2EEStores(db: IDBDatabase): void {
db.createObjectStore("userIdentities", {keyPath: "userId"}); db.createObjectStore("userIdentities", {keyPath: "userId"});
const deviceIdentities = db.createObjectStore("deviceIdentities", {keyPath: "key"}); const deviceIdentities = db.createObjectStore("deviceIdentities", {keyPath: "key"});
deviceIdentities.createIndex("byCurve25519Key", "curve25519Key", {unique: true}); deviceIdentities.createIndex("byCurve25519Key", "curve25519Key", {unique: true});
@ -86,13 +92,14 @@ function createE2EEStores(db) {
} }
// v5 // v5
async function migrateEncryptionFlag(db, txn) { async function migrateEncryptionFlag(db: IDBDatabase, txn: IDBTransaction): Promise<void> {
// migrate room summary isEncrypted -> encryption prop // migrate room summary isEncrypted -> encryption prop
const roomSummary = txn.objectStore("roomSummary"); const roomSummary = txn.objectStore("roomSummary");
const roomState = txn.objectStore("roomState"); const roomState = txn.objectStore("roomState");
const summaries = []; const summaries: any[] = [];
await iterateCursor(roomSummary.openCursor(), summary => { await iterateCursor<any>(roomSummary.openCursor(), summary => {
summaries.push(summary); summaries.push(summary);
return NOT_DONE;
}); });
for (const summary of summaries) { for (const summary of summaries) {
const encryptionEntry = await reqAsPromise(roomState.get(`${summary.roomId}|m.room.encryption|`)); const encryptionEntry = await reqAsPromise(roomState.get(`${summary.roomId}|m.room.encryption|`));
@ -105,31 +112,32 @@ async function migrateEncryptionFlag(db, txn) {
} }
// v6 // v6
function createAccountDataStore(db) { function createAccountDataStore(db: IDBDatabase): void {
db.createObjectStore("accountData", {keyPath: "type"}); db.createObjectStore("accountData", {keyPath: "type"});
} }
// v7 // v7
function createInviteStore(db) { function createInviteStore(db: IDBDatabase): void {
db.createObjectStore("invites", {keyPath: "roomId"}); db.createObjectStore("invites", {keyPath: "roomId"});
} }
// v8 // v8
function createArchivedRoomSummaryStore(db) { function createArchivedRoomSummaryStore(db: IDBDatabase): void {
db.createObjectStore("archivedRoomSummary", {keyPath: "summary.roomId"}); db.createObjectStore("archivedRoomSummary", {keyPath: "summary.roomId"});
} }
// v9 // v9
async function migrateOperationScopeIndex(db, txn) { async function migrateOperationScopeIndex(db: IDBDatabase, txn: IDBTransaction): Promise<void> {
try { try {
const operations = txn.objectStore("operations"); const operations = txn.objectStore("operations");
operations.deleteIndex("byTypeAndScope"); operations.deleteIndex("byTypeAndScope");
await iterateCursor(operations.openCursor(), (op, key, cur) => { await iterateCursor<any>(operations.openCursor(), (op, key, cur) => {
const {typeScopeKey} = op; const {typeScopeKey} = op;
delete op.typeScopeKey; delete op.typeScopeKey;
const [type, scope] = typeScopeKey.split("|"); const [type, scope] = typeScopeKey.split("|");
op.scopeTypeKey = encodeScopeTypeKey(scope, type); op.scopeTypeKey = encodeScopeTypeKey(scope, type);
cur.update(op); cur.update(op);
return NOT_DONE;
}); });
operations.createIndex("byScopeAndType", "scopeTypeKey", {unique: false}); operations.createIndex("byScopeAndType", "scopeTypeKey", {unique: false});
} catch (err) { } catch (err) {
@ -139,6 +147,6 @@ async function migrateOperationScopeIndex(db, txn) {
} }
//v10 //v10
function createTimelineRelationsStore(db) { function createTimelineRelationsStore(db: IDBDatabase) : void {
db.createObjectStore("timelineRelations", {keyPath: "key"}); 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 See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {Store} from "../Store";
import {Content} from "../../types";
interface AccountDataEntry {
type: string;
content: Content;
}
export class AccountDataStore { export class AccountDataStore {
constructor(store) { private _store: Store<AccountDataEntry>;
this._store = store;
}
async get(type) { constructor(store: Store<AccountDataEntry>) {
return await this._store.get(type); this._store = store;
} }
set(event) { async get(type: string): Promise<AccountDataEntry | null> {
return this._store.put(event); 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. 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}`; return `${userId}|${deviceId}`;
} }
function decodeKey(key) { function decodeKey(key: string): { userId: string, deviceId: string } {
const [userId, deviceId] = key.split("|"); const [userId, deviceId] = key.split("|");
return {userId, deviceId}; return {userId, deviceId};
} }
export class DeviceIdentityStore { export class DeviceIdentityStore {
constructor(store) { private _store: Store<DeviceIdentity>;
constructor(store: Store<DeviceIdentity>) {
this._store = store; this._store = store;
} }
getAllForUserId(userId) { getAllForUserId(userId: string): Promise<DeviceIdentity[]> {
const range = this._store.IDBKeyRange.lowerBound(encodeKey(userId, "")); const range = this._store.IDBKeyRange.lowerBound(encodeKey(userId, ""));
return this._store.selectWhile(range, device => { return this._store.selectWhile(range, device => {
return device.userId === userId; return device.userId === userId;
}); });
} }
async getAllDeviceIds(userId) { async getAllDeviceIds(userId: string): Promise<string[]> {
const deviceIds = []; const deviceIds: string[] = [];
const range = this._store.IDBKeyRange.lowerBound(encodeKey(userId, "")); const range = this._store.IDBKeyRange.lowerBound(encodeKey(userId, ""));
await this._store.iterateKeys(range, key => { await this._store.iterateKeys(range, key => {
const decodedKey = decodeKey(key); const decodedKey = decodeKey(key as string);
// prevent running into the next room // prevent running into the next room
if (decodedKey.userId === userId) { if (decodedKey.userId === userId) {
deviceIds.push(decodedKey.deviceId); deviceIds.push(decodedKey.deviceId);
@ -52,27 +65,27 @@ export class DeviceIdentityStore {
return deviceIds; return deviceIds;
} }
get(userId, deviceId) { get(userId: string, deviceId: string): Promise<DeviceIdentity | null> {
return this._store.get(encodeKey(userId, deviceId)); return this._store.get(encodeKey(userId, deviceId));
} }
set(deviceIdentity) { set(deviceIdentity: DeviceIdentity): Promise<IDBValidKey> {
deviceIdentity.key = encodeKey(deviceIdentity.userId, deviceIdentity.deviceId); 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); return this._store.index("byCurve25519Key").get(curve25519Key);
} }
remove(userId, deviceId) { remove(userId: string, deviceId: string): Promise<undefined> {
this._store.delete(encodeKey(userId, deviceId)); return this._store.delete(encodeKey(userId, deviceId));
} }
removeAllForUser(userId) { removeAllForUser(userId: string): Promise<undefined> {
// exclude both keys as they are theoretical min and max, // 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 // 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); 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. 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}`; return `${roomId}|${sessionId}|${messageIndex}`;
} }
interface GroupSessionDecryption {
eventId: string;
timestamp: number;
key: string;
}
export class GroupSessionDecryptionStore { export class GroupSessionDecryptionStore {
constructor(store) { private _store: Store<GroupSessionDecryption>;
constructor(store: Store<GroupSessionDecryption>) {
this._store = store; 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)); 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); 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( const range = this._store.IDBKeyRange.bound(
encodeKey(roomId, MIN_UNICODE, MIN_UNICODE), encodeKey(roomId, MIN_UNICODE, MIN_UNICODE),
encodeKey(roomId, MAX_UNICODE, MAX_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. 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}`; return `${roomId}|${senderKey}|${sessionId}`;
} }
export class InboundGroupSessionStore { export class InboundGroupSessionStore {
constructor(store) { private _store: Store<InboundGroupSession>;
constructor(store: Store<InboundGroupSession>) {
this._store = store; 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 key = encodeKey(roomId, senderKey, sessionId);
const fetchedKey = await this._store.getKey(key); const fetchedKey = await this._store.getKey(key);
return key === fetchedKey; 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)); return this._store.get(encodeKey(roomId, senderKey, sessionId));
} }
set(session) { set(session: InboundGroupSession): Promise<IDBValidKey> {
session.key = encodeKey(session.roomId, session.senderKey, session.sessionId); 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( const range = this._store.IDBKeyRange.bound(
encodeKey(roomId, MIN_UNICODE, MIN_UNICODE), encodeKey(roomId, MIN_UNICODE, MIN_UNICODE),
encodeKey(roomId, MAX_UNICODE, MAX_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 See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {Store} from "../Store";
function encodeKey(senderKey, sessionId) { function encodeKey(senderKey: string, sessionId: string): string {
return `${senderKey}|${sessionId}`; return `${senderKey}|${sessionId}`;
} }
function decodeKey(key) { function decodeKey(key: string): { senderKey: string, sessionId: string } {
const [senderKey, sessionId] = key.split("|"); const [senderKey, sessionId] = key.split("|");
return {senderKey, sessionId}; return {senderKey, sessionId};
} }
interface OlmSession {
session: string;
sessionId: string;
senderKey: string;
lastUsed: number;
key: string;
}
export class OlmSessionStore { export class OlmSessionStore {
constructor(store) { private _store: Store<OlmSession>;
constructor(store: Store<OlmSession>) {
this._store = store; this._store = store;
} }
async getSessionIds(senderKey) { async getSessionIds(senderKey: string): Promise<string[]> {
const sessionIds = []; const sessionIds: string[] = [];
const range = this._store.IDBKeyRange.lowerBound(encodeKey(senderKey, "")); const range = this._store.IDBKeyRange.lowerBound(encodeKey(senderKey, ""));
await this._store.iterateKeys(range, key => { await this._store.iterateKeys(range, key => {
const decodedKey = decodeKey(key); const decodedKey = decodeKey(key as string);
// prevent running into the next room // prevent running into the next room
if (decodedKey.senderKey === senderKey) { if (decodedKey.senderKey === senderKey) {
sessionIds.push(decodedKey.sessionId); sessionIds.push(decodedKey.sessionId);
@ -43,23 +54,23 @@ export class OlmSessionStore {
return sessionIds; return sessionIds;
} }
getAll(senderKey) { getAll(senderKey: string): Promise<OlmSession[]> {
const range = this._store.IDBKeyRange.lowerBound(encodeKey(senderKey, "")); const range = this._store.IDBKeyRange.lowerBound(encodeKey(senderKey, ""));
return this._store.selectWhile(range, session => { return this._store.selectWhile(range, session => {
return session.senderKey === senderKey; return session.senderKey === senderKey;
}); });
} }
get(senderKey, sessionId) { get(senderKey: string, sessionId: string): Promise<OlmSession | null> {
return this._store.get(encodeKey(senderKey, sessionId)); return this._store.get(encodeKey(senderKey, sessionId));
} }
set(session) { set(session: OlmSession): Promise<IDBValidKey> {
session.key = encodeKey(session.senderKey, session.sessionId); session.key = encodeKey(session.senderKey, session.sessionId);
return this._store.put(session); return this._store.put(session);
} }
remove(senderKey, sessionId) { remove(senderKey: string, sessionId: string): Promise<undefined> {
return this._store.delete(encodeKey(senderKey, sessionId)); 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 See the License for the specific language governing permissions and
limitations under the License. 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}`; 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 { export class OperationStore {
constructor(store) { private _store: Store<Operation>;
constructor(store: Store<Operation>) {
this._store = store; this._store = store;
} }
getAll() { getAll(): Promise<Operation[]> {
return this._store.selectAll(); return this._store.selectAll();
} }
async getAllByTypeAndScope(type, scope) { async getAllByTypeAndScope(type: string, scope: string): Promise<Operation[]> {
const key = encodeScopeTypeKey(scope, type); const key = encodeScopeTypeKey(scope, type);
const results = []; const results: Operation[] = [];
await this._store.index("byScopeAndType").iterateWhile(key, value => { await this._store.index("byScopeAndType").iterateWhile(key, value => {
if (value.scopeTypeKey !== key) { if (value.scopeTypeKey !== key) {
return false; return false;
@ -41,20 +61,20 @@ export class OperationStore {
return results; return results;
} }
add(operation) { add(operation: Operation): Promise<IDBValidKey> {
operation.scopeTypeKey = encodeScopeTypeKey(operation.scope, operation.type); operation.scopeTypeKey = encodeScopeTypeKey(operation.scope, operation.type);
this._store.add(operation); return this._store.add(operation);
} }
update(operation) { update(operation: Operation): Promise<IDBValidKey> {
this._store.put(operation); return this._store.put(operation);
} }
remove(id) { remove(id: string): Promise<undefined> {
this._store.delete(id); return this._store.delete(id);
} }
async removeAllForScope(scope) { async removeAllForScope(scope: string): Promise<undefined> {
const range = this._store.IDBKeyRange.bound( const range = this._store.IDBKeyRange.bound(
encodeScopeTypeKey(scope, MIN_UNICODE), encodeScopeTypeKey(scope, MIN_UNICODE),
encodeScopeTypeKey(scope, MAX_UNICODE) encodeScopeTypeKey(scope, MAX_UNICODE)
@ -64,5 +84,6 @@ export class OperationStore {
cur.delete(); cur.delete();
return true; 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 See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {Store} from "../Store";
interface OutboundSession {
roomId: string;
session: string;
createdAt: number;
}
export class OutboundGroupSessionStore { export class OutboundGroupSessionStore {
constructor(store) { private _store: Store<OutboundSession>;
constructor(store: Store<OutboundSession>) {
this._store = store; this._store = store;
} }
remove(roomId) { remove(roomId: string): Promise<undefined> {
this._store.delete(roomId); return this._store.delete(roomId);
} }
get(roomId) { get(roomId: string): Promise<OutboundSession | null> {
return this._store.get(roomId); return this._store.get(roomId);
} }
set(session) { set(session: OutboundSession): Promise<IDBValidKey> {
this._store.put(session); 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. limitations under the License.
*/ */
import { encodeUint32, decodeUint32 } from "../utils.js"; import { encodeUint32, decodeUint32 } from "../utils";
import {KeyLimits} from "../../common.js"; 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)}`; return `${roomId}|${encodeUint32(queueIndex)}`;
} }
function decodeKey(key) { function decodeKey(key: string): { roomId: string, queueIndex: number } {
const [roomId, encodedQueueIndex] = key.split("|"); const [roomId, encodedQueueIndex] = key.split("|");
const queueIndex = decodeUint32(encodedQueueIndex); const queueIndex = decodeUint32(encodedQueueIndex);
return {roomId, queueIndex}; return {roomId, queueIndex};
} }
export class PendingEventStore { export class PendingEventStore {
constructor(eventStore) { private _eventStore: Store<PendingEntry>;
constructor(eventStore: Store<PendingEntry>) {
this._eventStore = eventStore; this._eventStore = eventStore;
} }
async getMaxQueueIndex(roomId) { async getMaxQueueIndex(roomId: string): Promise<number | undefined> {
const range = this._eventStore.IDBKeyRange.bound( const range = this._eventStore.IDBKeyRange.bound(
encodeKey(roomId, KeyLimits.minStorageKey), encodeKey(roomId, KeyLimits.minStorageKey),
encodeKey(roomId, KeyLimits.maxStorageKey), encodeKey(roomId, KeyLimits.maxStorageKey),
@ -41,38 +58,38 @@ export class PendingEventStore {
); );
const maxKey = await this._eventStore.findMaxKey(range); const maxKey = await this._eventStore.findMaxKey(range);
if (maxKey) { 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)); 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 keyRange = this._eventStore.IDBKeyRange.only(encodeKey(roomId, queueIndex));
const key = await this._eventStore.getKey(keyRange); const key = await this._eventStore.getKey(keyRange);
return !!key; return !!key;
} }
add(pendingEvent) { add(pendingEvent: PendingEntry): Promise<IDBValidKey> {
pendingEvent.key = encodeKey(pendingEvent.roomId, pendingEvent.queueIndex); pendingEvent.key = encodeKey(pendingEvent.roomId, pendingEvent.queueIndex);
this._eventStore.add(pendingEvent); return this._eventStore.add(pendingEvent);
} }
update(pendingEvent) { update(pendingEvent: PendingEntry): Promise<IDBValidKey> {
this._eventStore.put(pendingEvent); return this._eventStore.put(pendingEvent);
} }
getAll() { getAll(): Promise<PendingEntry[]> {
return this._eventStore.selectAll(); return this._eventStore.selectAll();
} }
removeAllForRoom(roomId) { removeAllForRoom(roomId: string): Promise<undefined> {
const minKey = encodeKey(roomId, KeyLimits.minStorageKey); const minKey = encodeKey(roomId, KeyLimits.minStorageKey);
const maxKey = encodeKey(roomId, KeyLimits.maxStorageKey); const maxKey = encodeKey(roomId, KeyLimits.maxStorageKey);
const range = this._eventStore.IDBKeyRange.bound(minKey, maxKey); 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. 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}`; return `${roomId}|${userId}`;
} }
function decodeKey(key) { function decodeKey(key: string): { roomId: string, userId: string } {
const [roomId, userId] = key.split("|"); const [roomId, userId] = key.split("|");
return {roomId, userId}; 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 // no historical members
export class RoomMemberStore { export class RoomMemberStore {
constructor(roomMembersStore) { private _roomMembersStore: Store<MemberStorageEntry>;
constructor(roomMembersStore: Store<MemberStorageEntry>) {
this._roomMembersStore = roomMembersStore; this._roomMembersStore = roomMembersStore;
} }
get(roomId, userId) { get(roomId: string, userId: string): Promise<MemberStorageEntry | null> {
return this._roomMembersStore.get(encodeKey(roomId, userId)); return this._roomMembersStore.get(encodeKey(roomId, userId));
} }
async set(member) { async set(member: MemberData): Promise<IDBValidKey> {
member.key = encodeKey(member.roomId, member.userId); // Object.assign would be more typesafe, but small objects
return this._roomMembersStore.put(member); (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, "")); const range = this._roomMembersStore.IDBKeyRange.lowerBound(encodeKey(roomId, ""));
return this._roomMembersStore.selectWhile(range, member => { return this._roomMembersStore.selectWhile(range, member => {
return member.roomId === roomId; return member.roomId === roomId;
}); });
} }
async getAllUserIds(roomId) { async getAllUserIds(roomId: string): Promise<string[]> {
const userIds = []; const userIds: string[] = [];
const range = this._roomMembersStore.IDBKeyRange.lowerBound(encodeKey(roomId, "")); const range = this._roomMembersStore.IDBKeyRange.lowerBound(encodeKey(roomId, ""));
await this._roomMembersStore.iterateKeys(range, key => { await this._roomMembersStore.iterateKeys(range, key => {
const decodedKey = decodeKey(key); const decodedKey = decodeKey(key as string);
// prevent running into the next room // prevent running into the next room
if (decodedKey.roomId === roomId) { if (decodedKey.roomId === roomId) {
userIds.push(decodedKey.userId); userIds.push(decodedKey.userId);
@ -63,10 +78,10 @@ export class RoomMemberStore {
return userIds; return userIds;
} }
removeAllForRoom(roomId) { removeAllForRoom(roomId: string): Promise<undefined> {
// exclude both keys as they are theoretical min and max, // 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 // 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); 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. 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}`; return `${roomId}|${eventType}|${stateKey}`;
} }
export interface RoomStateEntry {
roomId: string;
event: StateEvent;
key: string;
}
export class RoomStateStore { export class RoomStateStore {
constructor(idbStore) { private _roomStateStore: Store<RoomStateEntry>;
constructor(idbStore: Store<RoomStateEntry>) {
this._roomStateStore = idbStore; this._roomStateStore = idbStore;
} }
get(roomId, type, stateKey) { get(roomId: string, type: string, stateKey: string): Promise<RoomStateEntry | null> {
const key = encodeKey(roomId, type, stateKey); const key = encodeKey(roomId, type, stateKey);
return this._roomStateStore.get(key); 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 key = encodeKey(roomId, event.type, event.state_key);
const entry = {roomId, event, key}; const entry = {roomId, event, key};
return this._roomStateStore.put(entry); return this._roomStateStore.put(entry);
} }
removeAllForRoom(roomId) { removeAllForRoom(roomId: string): Promise<undefined> {
// exclude both keys as they are theoretical min and max, // 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 // 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); 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 inviteCount
joinCount joinCount
*/ */
import {Store} from "../Store";
import {SummaryData} from "../../../room/RoomSummary";
/** Used for both roomSummary and archivedRoomSummary stores */ /** Used for both roomSummary and archivedRoomSummary stores */
export class RoomSummaryStore { export class RoomSummaryStore {
constructor(summaryStore) { private _summaryStore: Store<SummaryData>;
this._summaryStore = summaryStore;
}
getAll() { constructor(summaryStore: Store<SummaryData>) {
return this._summaryStore.selectAll(); this._summaryStore = summaryStore;
} }
set(summary) { getAll(): Promise<SummaryData[]> {
return this._summaryStore.put(summary); return this._summaryStore.selectAll();
} }
get(roomId) { set(summary: SummaryData): Promise<IDBValidKey> {
return this._summaryStore.get(roomId); 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); const fetchedKey = await this._summaryStore.getKey(roomId);
return roomId === fetchedKey; return roomId === fetchedKey;
} }
remove(roomId) { remove(roomId: string): Promise<undefined> {
return this._summaryStore.delete(roomId); 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 {EventKey} from "../../../room/timeline/EventKey.js";
import { StorageError } from "../../common.js"; import { StorageError } from "../../common";
import { encodeUint32 } from "../utils.js"; import { encodeUint32 } from "../utils";
import {KeyLimits} from "../../common.js"; import {KeyLimits} from "../../common";
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)}`; return `${roomId}|${encodeUint32(fragmentId)}|${encodeUint32(eventIndex)}`;
} }
function encodeEventIdKey(roomId, eventId) { function encodeEventIdKey(roomId: string, eventId: string): string {
return `${roomId}|${eventId}`; return `${roomId}|${eventId}`;
} }
function decodeEventIdKey(eventIdKey) { function decodeEventIdKey(eventIdKey: string): { roomId: string, eventId: string } {
const [roomId, eventId] = eventIdKey.split("|"); const [roomId, eventId] = eventIdKey.split("|");
return {roomId, eventId}; return {roomId, eventId};
} }
class Range { 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._IDBKeyRange = IDBKeyRange;
this._only = only; this._only = only;
this._lower = lower; this._lower = lower;
@ -42,7 +69,7 @@ class Range {
this._upperOpen = upperOpen; this._upperOpen = upperOpen;
} }
asIDBKeyRange(roomId) { asIDBKeyRange(roomId: string): IDBKeyRange | undefined {
try { try {
// only // only
if (this._only) { if (this._only) {
@ -99,66 +126,68 @@ class Range {
* @property {?Gap} gap if a gap entry, the gap * @property {?Gap} gap if a gap entry, the gap
*/ */
export class TimelineEventStore { export class TimelineEventStore {
constructor(timelineStore) { private _timelineStore: Store<StorageEntry>;
constructor(timelineStore: Store<StorageEntry>) {
this._timelineStore = timelineStore; this._timelineStore = timelineStore;
} }
/** Creates a range that only includes the given key /** Creates a range that only includes the given key
* @param {EventKey} eventKey the key * @param eventKey the key
* @return {Range} the created range * @return the created range
*/ */
onlyRange(eventKey) { onlyRange(eventKey: EventKey): Range {
return new Range(this._timelineStore.IDBKeyRange, eventKey); return new Range(this._timelineStore.IDBKeyRange, eventKey);
} }
/** Creates a range that includes all keys before eventKey, and optionally also the key itself. /** Creates a range that includes all keys before eventKey, and optionally also the key itself.
* @param {EventKey} eventKey the key * @param eventKey the key
* @param {boolean} [open=false] whether the key is included (false) or excluded (true) from the range at the upper end. * @param [open=false] whether the key is included (false) or excluded (true) from the range at the upper end.
* @return {Range} the created range * @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); 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. /** Creates a range that includes all keys after eventKey, and optionally also the key itself.
* @param {EventKey} eventKey the key * @param eventKey the key
* @param {boolean} [open=false] whether the key is included (false) or excluded (true) from the range at the lower end. * @param [open=false] whether the key is included (false) or excluded (true) from the range at the lower end.
* @return {Range} the created range * @return the created range
*/ */
lowerBoundRange(eventKey, open=false) { lowerBoundRange(eventKey: EventKey, open=false): Range {
return new Range(this._timelineStore.IDBKeyRange, undefined, eventKey, undefined, open); 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. /** 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 lower the lower key
* @param {EventKey} upper the upper key * @param upper the upper key
* @param {boolean} [lowerOpen=false] whether the lower key is included (false) or excluded (true) from the range. * @param [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. * @param [upperOpen=false] whether the upper key is included (false) or excluded (true) from the range.
* @return {Range} the created 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); return new Range(this._timelineStore.IDBKeyRange, undefined, lower, upper, lowerOpen, upperOpen);
} }
/** Looks up the last `amount` entries in the timeline for `roomId`. /** Looks up the last `amount` entries in the timeline for `roomId`.
* @param {string} roomId * @param roomId
* @param {number} fragmentId * @param fragmentId
* @param {number} amount * @param amount
* @return {Promise<Entry[]>} a promise resolving to an array with 0 or more entries, in ascending order. * @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; const eventKey = EventKey.maxKey;
eventKey.fragmentId = fragmentId; eventKey.fragmentId = fragmentId;
return this.eventsBefore(roomId, eventKey, amount); return this.eventsBefore(roomId, eventKey, amount);
} }
/** Looks up the first `amount` entries in the timeline for `roomId`. /** Looks up the first `amount` entries in the timeline for `roomId`.
* @param {string} roomId * @param roomId
* @param {number} fragmentId * @param fragmentId
* @param {number} amount * @param amount
* @return {Promise<Entry[]>} a promise resolving to an array with 0 or more entries, in ascending order. * @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; const eventKey = EventKey.minKey;
eventKey.fragmentId = fragmentId; eventKey.fragmentId = fragmentId;
return this.eventsAfter(roomId, eventKey, amount); 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. /** Looks up `amount` entries after `eventKey` in the timeline for `roomId` within the same fragment.
* The entry for `eventKey` is not included. * The entry for `eventKey` is not included.
* @param {string} roomId * @param roomId
* @param {EventKey} eventKey * @param eventKey
* @param {number} amount * @param amount
* @return {Promise<Entry[]>} a promise resolving to an array with 0 or more entries, in ascending order. * @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); const idbRange = this.lowerBoundRange(eventKey, true).asIDBKeyRange(roomId);
return this._timelineStore.selectLimit(idbRange, amount); return this._timelineStore.selectLimit(idbRange, amount);
} }
/** Looks up `amount` entries before `eventKey` in the timeline for `roomId` within the same fragment. /** Looks up `amount` entries before `eventKey` in the timeline for `roomId` within the same fragment.
* The entry for `eventKey` is not included. * The entry for `eventKey` is not included.
* @param {string} roomId * @param roomId
* @param {EventKey} eventKey * @param eventKey
* @param {number} amount * @param amount
* @return {Promise<Entry[]>} a promise resolving to an array with 0 or more entries, in ascending order. * @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 range = this.upperBoundRange(eventKey, true).asIDBKeyRange(roomId);
const events = await this._timelineStore.selectLimitReverse(range, amount); const events = await this._timelineStore.selectLimitReverse(range, amount);
events.reverse(); // because we fetched them backwards 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`. * The order in which results are returned might be different than `eventIds`.
* Call the return value to obtain the next {id, event} pair. * Call the return value to obtain the next {id, event} pair.
* @param {string} roomId * @param roomId
* @param {string[]} eventIds * @param eventIds
* @return {Function<Promise>} * @return
*/ */
// performance comment from above refers to the fact that there *might* // performance comment from above refers to the fact that there *might*
// be a correlation between event_id sorting order and chronology. // 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 // 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. // 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. // 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 byEventId = this._timelineStore.index("byEventId");
const keys = eventIds.map(eventId => encodeEventIdKey(roomId, eventId)); const keys = eventIds.map(eventId => encodeEventIdKey(roomId, eventId));
const results = new Array(keys.length); 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 // 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) { for(let i = 0; i < results.length; ++i) {
if (results[i] === undefined) { if (results[i] === undefined) {
return; return;
@ -222,7 +251,8 @@ export class TimelineEventStore {
} }
await byEventId.findExistingKeys(keys, false, (key, found) => { 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; results[index] = found;
firstFoundKey = firstFoundAndPrecedingResolved(); firstFoundKey = firstFoundAndPrecedingResolved();
return !!firstFoundKey; 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. /** 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 * @param entry the entry to insert
* @return {Promise<>} a promise resolving to undefined if the operation was successful, or a StorageError if not. * @return a promise resolving to undefined if the operation was successful, or a StorageError if not.
* @throws {StorageError} ... * @throws {StorageError} ...
*/ */
insert(entry) { insert(entry: StorageEntry): Promise<IDBValidKey> {
entry.key = encodeKey(entry.roomId, entry.fragmentId, entry.eventIndex); entry.key = encodeKey(entry.roomId, entry.fragmentId, entry.eventIndex);
entry.eventIdKey = encodeEventIdKey(entry.roomId, entry.event.event_id); entry.eventIdKey = encodeEventIdKey(entry.roomId, entry.event.event_id);
// TODO: map error? or in idb/store? // 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. /** Updates the entry into the store with the given [roomId, eventKey] combination.
* If not yet present, will insert. Might be slower than add. * If not yet present, will insert. Might be slower than add.
* @param {Entry} entry the entry to update. * @param entry the entry to update.
* @return {Promise<>} a promise resolving to undefined if the operation was successful, or a StorageError if not. * @return a promise resolving to undefined if the operation was successful, or a StorageError if not.
*/ */
update(entry) { update(entry: StorageEntry): Promise<IDBValidKey> {
this._timelineStore.put(entry); 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)); 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)); 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 minKey = encodeKey(roomId, KeyLimits.minStorageKey, KeyLimits.minStorageKey);
const maxKey = encodeKey(roomId, KeyLimits.maxStorageKey, KeyLimits.maxStorageKey); const maxKey = encodeKey(roomId, KeyLimits.maxStorageKey, KeyLimits.maxStorageKey);
const range = this._timelineStore.IDBKeyRange.bound(minKey, maxKey); 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. limitations under the License.
*/ */
import { StorageError } from "../../common.js"; import { StorageError } from "../../common";
import {KeyLimits} from "../../common.js"; import {KeyLimits} from "../../common";
import { encodeUint32 } from "../utils.js"; import { encodeUint32 } from "../utils";
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)}`; return `${roomId}|${encodeUint32(fragmentId)}`;
} }
export class TimelineFragmentStore { export class TimelineFragmentStore {
constructor(store) { private _store: Store<FragmentEntry>;
constructor(store: Store<FragmentEntry>) {
this._store = store; this._store = store;
} }
_allRange(roomId) { _allRange(roomId: string): IDBKeyRange {
try { try {
return this._store.IDBKeyRange.bound( return this._store.IDBKeyRange.bound(
encodeKey(roomId, KeyLimits.minStorageKey), 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)); return this._store.selectAll(this._allRange(roomId));
} }
/** Returns the fragment without a nextToken and without nextId, /** Returns the fragment without a nextToken and without nextId,
if any, with the largest id if there are multiple (which should not happen) */ 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? // why do we need this?
// Ok, take the case where you've got a /context fragment and a /sync fragment // 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 ... // 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? // should generate an id an return it?
// depends if we want to do anything smart with fragment ids, // depends if we want to do anything smart with fragment ids,
// like give them meaning depending on range. not for now probably ... // like give them meaning depending on range. not for now probably ...
add(fragment) { add(fragment: Fragment): Promise<IDBValidKey> {
fragment.key = encodeKey(fragment.roomId, fragment.id); (fragment as any).key = encodeKey(fragment.roomId, fragment.id);
this._store.add(fragment); return this._store.add(fragment as FragmentEntry);
} }
update(fragment) { update(fragment: FragmentEntry): Promise<IDBValidKey> {
this._store.put(fragment); return this._store.put(fragment);
} }
get(roomId, fragmentId) { get(roomId: string, fragmentId: number): Promise<FragmentEntry | null> {
return this._store.get(encodeKey(roomId, fragmentId)); return this._store.get(encodeKey(roomId, fragmentId));
} }
removeAllForRoom(roomId) { removeAllForRoom(roomId: string): Promise<undefined> {
this._store.delete(this._allRange(roomId)); 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 See the License for the specific language governing permissions and
limitations under the License. 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}`; 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("|"); const [roomId, targetEventId, relType, sourceEventId] = key.split("|");
return {roomId, targetEventId, relType, sourceEventId}; return {roomId, targetEventId, relType, sourceEventId};
} }
export class TimelineRelationStore { export class TimelineRelationStore {
constructor(store) { private _store: Store<{ key: string }>;
constructor(store: Store<{ key: string }>) {
this._store = store; 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)}); 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)); 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( const range = this._store.IDBKeyRange.bound(
encodeKey(roomId, targetId, MIN_UNICODE, MIN_UNICODE), encodeKey(roomId, targetId, MIN_UNICODE, MIN_UNICODE),
encodeKey(roomId, targetId, MAX_UNICODE, MAX_UNICODE), encodeKey(roomId, targetId, MAX_UNICODE, MAX_UNICODE),
@ -47,7 +57,7 @@ export class TimelineRelationStore {
return this._store.delete(range); 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, // 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 // but we should't have a match for just the room id, or room id with max
const range = this._store.IDBKeyRange.bound( const range = this._store.IDBKeyRange.bound(
@ -60,7 +70,7 @@ export class TimelineRelationStore {
return items.map(i => decodeKey(i.key)); 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, // 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 // but we should't have a match for just the room id, or room id with max
const range = this._store.IDBKeyRange.bound( 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 See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {Store} from "../Store";
interface UserIdentity {
userId: string;
roomIds: string[];
deviceTrackingStatus: number;
}
export class UserIdentityStore { export class UserIdentityStore {
constructor(store) { private _store: Store<UserIdentity>;
constructor(store: Store<UserIdentity>) {
this._store = store; this._store = store;
} }
get(userId) { get(userId: string): Promise<UserIdentity | null> {
return this._store.get(userId); return this._store.get(userId);
} }
set(userIdentity) { set(userIdentity: UserIdentity): Promise<IDBValidKey> {
this._store.put(userIdentity); return this._store.put(userIdentity);
} }
remove(userId) { remove(userId: string): Promise<undefined> {
return this._store.delete(userId); 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. limitations under the License.
*/ */
import { IDBRequestError } from "./error.js"; import { IDBRequestError } from "./error";
import { StorageError } from "../common.js"; import { StorageError } from "../common";
export const NOT_DONE = { done: false };
export const DONE = { done: true };
let needsSyncPromise = false; let needsSyncPromise = false;
@ -25,7 +28,7 @@ let needsSyncPromise = false;
If this is the case, promises need to be resolved If this is the case, promises need to be resolved
synchronously from the idb request handler to prevent the transaction from closing prematurely. synchronously from the idb request handler to prevent the transaction from closing prematurely.
*/ */
export async function checkNeedsSyncPromise() { export async function checkNeedsSyncPromise(): Promise<boolean> {
// important to have it turned off while doing the test, // important to have it turned off while doing the test,
// otherwise reqAsPromise would not fail // otherwise reqAsPromise would not fail
needsSyncPromise = false; needsSyncPromise = false;
@ -49,51 +52,57 @@ export async function checkNeedsSyncPromise() {
} }
// storage keys are defined to be unsigned 32bit numbers in KeyLimits, which is assumed by idb // storage keys are defined to be unsigned 32bit numbers in KeyLimits, which is assumed by idb
export function encodeUint32(n) { export function encodeUint32(n: number): string {
const hex = n.toString(16); const hex = n.toString(16);
return "0".repeat(8 - hex.length) + hex; return "0".repeat(8 - hex.length) + hex;
} }
// used for logs where timestamp is part of key, which is larger than 32 bit // used for logs where timestamp is part of key, which is larger than 32 bit
export function encodeUint64(n) { export function encodeUint64(n: number): string {
const hex = n.toString(16); const hex = n.toString(16);
return "0".repeat(16 - hex.length) + hex; return "0".repeat(16 - hex.length) + hex;
} }
export function decodeUint32(str) { export function decodeUint32(str: string): number {
return parseInt(str, 16); return parseInt(str, 16);
} }
export function openDatabase(name, createObjectStore, version, idbFactory = window.indexedDB) { type CreateObjectStore = (db : IDBDatabase, txn: IDBTransaction | null, oldVersion: number, version: number) => any
export function openDatabase(name: string, createObjectStore: CreateObjectStore, version: number, idbFactory: IDBFactory = window.indexedDB): Promise<IDBDatabase> {
const req = idbFactory.open(name, version); const req = idbFactory.open(name, version);
req.onupgradeneeded = (ev) => { req.onupgradeneeded = (ev : IDBVersionChangeEvent) => {
const db = ev.target.result; const req = ev.target as IDBRequest<IDBDatabase>;
const txn = ev.target.transaction; const db = req.result;
const txn = req.transaction;
const oldVersion = ev.oldVersion; const oldVersion = ev.oldVersion;
createObjectStore(db, txn, oldVersion, version); createObjectStore(db, txn, oldVersion, version);
}; };
return reqAsPromise(req); return reqAsPromise(req);
} }
export function reqAsPromise(req) { export function reqAsPromise<T>(req: IDBRequest<T>): Promise<T> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
req.addEventListener("success", event => { req.addEventListener("success", event => {
resolve(event.target.result); resolve((event.target as IDBRequest<T>).result);
// @ts-ignore
needsSyncPromise && Promise._flush && Promise._flush(); needsSyncPromise && Promise._flush && Promise._flush();
}); });
req.addEventListener("error", event => { req.addEventListener("error", event => {
const error = new IDBRequestError(event.target); const error = new IDBRequestError(event.target as IDBRequest<T>);
reject(error); reject(error);
// @ts-ignore
needsSyncPromise && Promise._flush && Promise._flush(); needsSyncPromise && Promise._flush && Promise._flush();
}); });
}); });
} }
export function txnAsPromise(txn) { export function txnAsPromise(txn): Promise<void> {
let error; let error;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
txn.addEventListener("complete", () => { txn.addEventListener("complete", () => {
resolve(); resolve();
// @ts-ignore
needsSyncPromise && Promise._flush && Promise._flush(); needsSyncPromise && Promise._flush && Promise._flush();
}); });
txn.addEventListener("error", event => { txn.addEventListener("error", event => {
@ -112,33 +121,41 @@ export function txnAsPromise(txn) {
error = new StorageError(`Transaction on ${dbName} with stores ${storeNames} was aborted.`); error = new StorageError(`Transaction on ${dbName} with stores ${storeNames} was aborted.`);
} }
reject(error); reject(error);
// @ts-ignore
needsSyncPromise && Promise._flush && Promise._flush(); needsSyncPromise && Promise._flush && Promise._flush();
}); });
}); });
} }
export function iterateCursor(cursorRequest, processValue) { 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?? // TODO: does cursor already have a value here??
return new Promise((resolve, reject) => { return new Promise<boolean>((resolve, reject) => {
cursorRequest.onerror = () => { cursorRequest.onerror = () => {
reject(new IDBRequestError(cursorRequest)); reject(new IDBRequestError(cursorRequest));
// @ts-ignore
needsSyncPromise && Promise._flush && Promise._flush(); needsSyncPromise && Promise._flush && Promise._flush();
}; };
// collect results // collect results
cursorRequest.onsuccess = (event) => { cursorRequest.onsuccess = (event) => {
const cursor = event.target.result; const cursor = (event.target as IDBRequest<I>).result;
if (!cursor) { if (!cursor) {
resolve(false); resolve(false);
// @ts-ignore
needsSyncPromise && Promise._flush && Promise._flush(); needsSyncPromise && Promise._flush && Promise._flush();
return; // end of results return; // end of results
} }
const result = processValue(cursor.value, cursor.key, cursor); const result = processValue(cursor["value"], cursor.key, cursor);
// TODO: don't use object for result and assume it's jumpTo when not === true/false or undefined // TODO: don't use object for result and assume it's jumpTo when not === true/false or undefined
const done = result?.done; const done = result?.done;
const jumpTo = result?.jumpTo; const jumpTo = result?.jumpTo;
if (done) { if (done) {
resolve(true); resolve(true);
// @ts-ignore
needsSyncPromise && Promise._flush && Promise._flush(); needsSyncPromise && Promise._flush && Promise._flush();
} else if(jumpTo) { } else if(jumpTo) {
cursor.continue(jumpTo); cursor.continue(jumpTo);
@ -151,16 +168,20 @@ export function iterateCursor(cursorRequest, processValue) {
}); });
} }
export async function fetchResults(cursor, isDone) { type Pred<T> = (value: T) => boolean
const results = [];
await iterateCursor(cursor, (value) => { export async function fetchResults<T>(cursor: IDBRequest, isDone: Pred<T[]>): Promise<T[]> {
const results: T[] = [];
await iterateCursor<T>(cursor, (value) => {
results.push(value); results.push(value);
return {done: isDone(results)}; return {done: isDone(results)};
}); });
return results; return results;
} }
export async function select(db, storeName, toCursor, isDone) { type ToCursor = (store: IDBObjectStore) => IDBRequest
export async function select<T>(db: IDBDatabase, storeName: string, toCursor: ToCursor, isDone: Pred<T[]>): Promise<T[]> {
if (!isDone) { if (!isDone) {
isDone = () => false; isDone = () => false;
} }
@ -173,7 +194,7 @@ export async function select(db, storeName, toCursor, isDone) {
return await fetchResults(cursor, isDone); return await fetchResults(cursor, isDone);
} }
export async function findStoreValue(db, storeName, toCursor, matchesValue) { export async function findStoreValue<T>(db: IDBDatabase, storeName: string, toCursor: ToCursor, matchesValue: Pred<T>): Promise<T> {
if (!matchesValue) { if (!matchesValue) {
matchesValue = () => true; matchesValue = () => true;
} }
@ -185,11 +206,12 @@ export async function findStoreValue(db, storeName, toCursor, matchesValue) {
const store = tx.objectStore(storeName); const store = tx.objectStore(storeName);
const cursor = await reqAsPromise(toCursor(store)); const cursor = await reqAsPromise(toCursor(store));
let match; let match;
const matched = await iterateCursor(cursor, (value) => { const matched = await iterateCursor<T>(cursor, (value) => {
if (matchesValue(value)) { if (matchesValue(value)) {
match = value; match = value;
return true; return { done: true };
} }
return { done: false };
}); });
if (!matched) { if (!matched) {
throw new StorageError("Value not found"); throw new StorageError("Value not found");

View file

@ -15,7 +15,7 @@ limitations under the License.
*/ */
import {Transaction} from "./Transaction.js"; import {Transaction} from "./Transaction.js";
import { STORE_MAP, STORE_NAMES } from "../common.js"; import { STORE_MAP, STORE_NAMES } from "../common";
export class Storage { export class Storage {
constructor(initialStoreValues = {}) { constructor(initialStoreValues = {}) {

View file

@ -14,20 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
export class InviteStore { export type Content = { [key: string]: any }
constructor(inviteStore) {
this._inviteStore = inviteStore;
}
getAll() { export interface RoomEvent {
return this._inviteStore.selectAll(); content: Content;
} type: string;
event_id: string;
set(invite) { sender: string;
return this._inviteStore.put(invite); origin_server_ts: number;
} unsigned?: Content;
remove(roomId) {
this._inviteStore.delete(roomId);
}
} }
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 {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() { export function createMockStorage() {
return new StorageFactory(null, new FDBFactory(), FDBKeyRange).create(1); 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 {createFetchRequest} from "./dom/request/fetch.js";
import {xhrRequest} from "./dom/request/xhr.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 {SessionInfoStorage} from "../../matrix/sessioninfo/localstorage/SessionInfoStorage.js";
import {SettingsStorage} from "./dom/SettingsStorage.js"; import {SettingsStorage} from "./dom/SettingsStorage.js";
import {Encoding} from "./utils/Encoding.js"; import {Encoding} from "./utils/Encoding.js";

View file

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