Compare commits
70 commits
master
...
snowpack-t
Author | SHA1 | Date | |
---|---|---|---|
|
08e7616897 | ||
|
410bd4ab8b | ||
|
a4375c0e15 | ||
|
3ed639d1c5 | ||
|
b31cf4fdce | ||
|
31b02f1eff | ||
|
c8f4cb5046 | ||
|
1f368dcec1 | ||
|
6c6991e622 | ||
|
fd4ddf5667 | ||
|
f9fa7cdb49 | ||
|
2cb8944a78 | ||
|
9fcc776a29 | ||
|
39e9b828e6 | ||
|
64b767e7eb | ||
|
2eea5d5ab8 | ||
|
287242deda | ||
|
31c951c68c | ||
|
ea4de29975 | ||
|
f390d21881 | ||
|
8c7e13f40f | ||
|
e0aa804971 | ||
|
9e459aa003 | ||
|
61d0108b3b | ||
|
51d8e3cb66 | ||
|
c21b187683 | ||
|
736b122fc7 | ||
|
016b51ba37 | ||
|
3128e072fd | ||
|
2883149086 | ||
|
f4ba26cb1e | ||
|
3213a0baa0 | ||
|
15d7d57b09 | ||
|
279f149408 | ||
|
218bac7883 | ||
|
c5a209258e | ||
|
5fb7871ca6 | ||
|
ee00aa3339 | ||
|
e433a234fe | ||
|
526ff53728 | ||
|
3ec222eae3 | ||
|
7f907427ee | ||
|
243d0e76ff | ||
|
ec38337223 | ||
|
9b4b303b01 | ||
|
50897cfbe3 | ||
|
8e93487ebe | ||
|
ddf09af05b | ||
|
d07c38effd | ||
|
3b5b91cf1b | ||
|
88ecc58b14 | ||
|
53228e88a2 | ||
|
de6fdba526 | ||
|
29c87b7c01 | ||
|
8ad2857c6a | ||
|
f8613e9e96 | ||
|
5177c35d0d | ||
|
97a50c835d | ||
|
2b44878332 | ||
|
46c306b487 | ||
|
e837a91a80 | ||
|
1d5b105c34 | ||
|
fefa15cd85 | ||
|
a31f6a2c52 | ||
|
69ab345a89 | ||
|
a0017cb720 | ||
|
0c80f78e9b | ||
|
4fb93ad104 | ||
|
caed99df69 | ||
|
96a4ef47a7 |
38 changed files with 961 additions and 624 deletions
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {KeyLimits} from "../../storage/common.js";
|
import {KeyLimits} from "../../storage/common";
|
||||||
|
|
||||||
// key for events in the timelineEvents store
|
// key for events in the timelineEvents store
|
||||||
export class EventKey {
|
export class EventKey {
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
import {BaseEntry} from "./BaseEntry";
|
import {BaseEntry} from "./BaseEntry";
|
||||||
import {Direction} from "../Direction.js";
|
import {Direction} from "../Direction.js";
|
||||||
import {isValidFragmentId} from "../common.js";
|
import {isValidFragmentId} from "../common.js";
|
||||||
import {KeyLimits} from "../../../storage/common.js";
|
import {KeyLimits} from "../../../storage/common";
|
||||||
|
|
||||||
export class FragmentBoundaryEntry extends BaseEntry {
|
export class FragmentBoundaryEntry extends BaseEntry {
|
||||||
constructor(fragment, isFragmentStart, fragmentIdComparer) {
|
constructor(fragment, isFragmentStart, fragmentIdComparer) {
|
||||||
|
|
|
@ -14,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;
|
||||||
}
|
}
|
|
@ -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;
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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) {
|
|
@ -1,164 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {QueryTarget} from "./QueryTarget.js";
|
|
||||||
import {IDBRequestAttemptError} from "./error.js";
|
|
||||||
|
|
||||||
const LOG_REQUESTS = false;
|
|
||||||
|
|
||||||
function logRequest(method, params, source) {
|
|
||||||
const storeName = source?.name;
|
|
||||||
const databaseName = source?.transaction?.db?.name;
|
|
||||||
console.info(`${databaseName}.${storeName}.${method}(${params.map(p => JSON.stringify(p)).join(", ")})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
class QueryTargetWrapper {
|
|
||||||
constructor(qt) {
|
|
||||||
this._qt = qt;
|
|
||||||
}
|
|
||||||
|
|
||||||
get keyPath() {
|
|
||||||
if (this._qt.objectStore) {
|
|
||||||
return this._qt.objectStore.keyPath;
|
|
||||||
} else {
|
|
||||||
return this._qt.keyPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
supports(methodName) {
|
|
||||||
return !!this._qt[methodName];
|
|
||||||
}
|
|
||||||
|
|
||||||
openKeyCursor(...params) {
|
|
||||||
try {
|
|
||||||
// not supported on Edge 15
|
|
||||||
if (!this._qt.openKeyCursor) {
|
|
||||||
LOG_REQUESTS && logRequest("openCursor", params, this._qt);
|
|
||||||
return this.openCursor(...params);
|
|
||||||
}
|
|
||||||
LOG_REQUESTS && logRequest("openKeyCursor", params, this._qt);
|
|
||||||
return this._qt.openKeyCursor(...params);
|
|
||||||
} catch(err) {
|
|
||||||
throw new IDBRequestAttemptError("openKeyCursor", this._qt, err, params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
openCursor(...params) {
|
|
||||||
try {
|
|
||||||
LOG_REQUESTS && logRequest("openCursor", params, this._qt);
|
|
||||||
return this._qt.openCursor(...params);
|
|
||||||
} catch(err) {
|
|
||||||
throw new IDBRequestAttemptError("openCursor", this._qt, err, params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
put(...params) {
|
|
||||||
try {
|
|
||||||
LOG_REQUESTS && logRequest("put", params, this._qt);
|
|
||||||
return this._qt.put(...params);
|
|
||||||
} catch(err) {
|
|
||||||
throw new IDBRequestAttemptError("put", this._qt, err, params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
add(...params) {
|
|
||||||
try {
|
|
||||||
LOG_REQUESTS && logRequest("add", params, this._qt);
|
|
||||||
return this._qt.add(...params);
|
|
||||||
} catch(err) {
|
|
||||||
throw new IDBRequestAttemptError("add", this._qt, err, params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get(...params) {
|
|
||||||
try {
|
|
||||||
LOG_REQUESTS && logRequest("get", params, this._qt);
|
|
||||||
return this._qt.get(...params);
|
|
||||||
} catch(err) {
|
|
||||||
throw new IDBRequestAttemptError("get", this._qt, err, params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getKey(...params) {
|
|
||||||
try {
|
|
||||||
LOG_REQUESTS && logRequest("getKey", params, this._qt);
|
|
||||||
return this._qt.getKey(...params);
|
|
||||||
} catch(err) {
|
|
||||||
throw new IDBRequestAttemptError("getKey", this._qt, err, params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(...params) {
|
|
||||||
try {
|
|
||||||
LOG_REQUESTS && logRequest("delete", params, this._qt);
|
|
||||||
return this._qt.delete(...params);
|
|
||||||
} catch(err) {
|
|
||||||
throw new IDBRequestAttemptError("delete", this._qt, err, params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
index(...params) {
|
|
||||||
try {
|
|
||||||
return this._qt.index(...params);
|
|
||||||
} catch(err) {
|
|
||||||
// TODO: map to different error? this is not a request
|
|
||||||
throw new IDBRequestAttemptError("index", this._qt, err, params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Store extends QueryTarget {
|
|
||||||
constructor(idbStore, transaction) {
|
|
||||||
super(new QueryTargetWrapper(idbStore));
|
|
||||||
this._transaction = transaction;
|
|
||||||
}
|
|
||||||
|
|
||||||
get IDBKeyRange() {
|
|
||||||
return this._transaction.IDBKeyRange;
|
|
||||||
}
|
|
||||||
|
|
||||||
get _idbStore() {
|
|
||||||
return this._target;
|
|
||||||
}
|
|
||||||
|
|
||||||
index(indexName) {
|
|
||||||
return new QueryTarget(new QueryTargetWrapper(this._idbStore.index(indexName)));
|
|
||||||
}
|
|
||||||
|
|
||||||
put(value) {
|
|
||||||
// If this request fails, the error will bubble up to the transaction and abort it,
|
|
||||||
// which is the behaviour we want. Therefore, it is ok to not create a promise for this
|
|
||||||
// request and await it.
|
|
||||||
//
|
|
||||||
// Perhaps at some later point, we will want to handle an error (like ConstraintError) for
|
|
||||||
// individual write requests. In that case, we should add a method that returns a promise (e.g. putAndObserve)
|
|
||||||
// and call preventDefault on the event to prevent it from aborting the transaction
|
|
||||||
//
|
|
||||||
// Note that this can still throw synchronously, like it does for TransactionInactiveError,
|
|
||||||
// see https://www.w3.org/TR/IndexedDB-2/#transaction-lifetime-concept
|
|
||||||
this._idbStore.put(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
add(value) {
|
|
||||||
// ok to not monitor result of request, see comment in `put`.
|
|
||||||
this._idbStore.add(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(keyOrKeyRange) {
|
|
||||||
// ok to not monitor result of request, see comment in `put`.
|
|
||||||
this._idbStore.delete(keyOrKeyRange);
|
|
||||||
}
|
|
||||||
}
|
|
175
src/matrix/storage/idb/Store.ts
Normal file
175
src/matrix/storage/idb/Store.ts
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {QueryTarget, IDBQuery} from "./QueryTarget";
|
||||||
|
import {IDBRequestAttemptError} from "./error";
|
||||||
|
import {reqAsPromise} from "./utils";
|
||||||
|
import {Transaction} from "./Transaction";
|
||||||
|
|
||||||
|
const LOG_REQUESTS = false;
|
||||||
|
|
||||||
|
function logRequest(method: string, params: any[], source: any): void {
|
||||||
|
const storeName = source?.name;
|
||||||
|
const databaseName = source?.transaction?.db?.name;
|
||||||
|
console.info(`${databaseName}.${storeName}.${method}(${params.map(p => JSON.stringify(p)).join(", ")})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
class QueryTargetWrapper<T> {
|
||||||
|
private _qt: IDBIndex | IDBObjectStore;
|
||||||
|
|
||||||
|
constructor(qt: IDBIndex | IDBObjectStore) {
|
||||||
|
this._qt = qt;
|
||||||
|
}
|
||||||
|
|
||||||
|
get keyPath(): string | string[] {
|
||||||
|
if (this._qt["objectStore"]) {
|
||||||
|
return (this._qt as IDBIndex).objectStore.keyPath;
|
||||||
|
} else {
|
||||||
|
return this._qt.keyPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get _qtStore(): IDBObjectStore {
|
||||||
|
return this._qt as IDBObjectStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
supports(methodName: string): boolean {
|
||||||
|
return !!this._qt[methodName];
|
||||||
|
}
|
||||||
|
|
||||||
|
openKeyCursor(range?: IDBQuery, direction?: IDBCursorDirection | undefined): IDBRequest<IDBCursor | null> {
|
||||||
|
try {
|
||||||
|
// not supported on Edge 15
|
||||||
|
if (!this._qt.openKeyCursor) {
|
||||||
|
LOG_REQUESTS && logRequest("openCursor", [range, direction], this._qt);
|
||||||
|
return this.openCursor(range, direction);
|
||||||
|
}
|
||||||
|
LOG_REQUESTS && logRequest("openKeyCursor", [range, direction], this._qt);
|
||||||
|
return this._qt.openKeyCursor(range, direction)
|
||||||
|
} catch(err) {
|
||||||
|
throw new IDBRequestAttemptError("openKeyCursor", this._qt, err, [range, direction]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
openCursor(range?: IDBQuery, direction?: IDBCursorDirection | undefined): IDBRequest<IDBCursorWithValue | null> {
|
||||||
|
try {
|
||||||
|
LOG_REQUESTS && logRequest("openCursor", [], this._qt);
|
||||||
|
return this._qt.openCursor(range, direction)
|
||||||
|
} catch(err) {
|
||||||
|
throw new IDBRequestAttemptError("openCursor", this._qt, err, [range, direction]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
put(item: T, key?: IDBValidKey | undefined): IDBRequest<IDBValidKey> {
|
||||||
|
try {
|
||||||
|
LOG_REQUESTS && logRequest("put", [item, key], this._qt);
|
||||||
|
return this._qtStore.put(item, key);
|
||||||
|
} catch(err) {
|
||||||
|
throw new IDBRequestAttemptError("put", this._qt, err, [item, key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
add(item: T, key?: IDBValidKey | undefined): IDBRequest<IDBValidKey> {
|
||||||
|
try {
|
||||||
|
LOG_REQUESTS && logRequest("add", [item, key], this._qt);
|
||||||
|
return this._qtStore.add(item, key);
|
||||||
|
} catch(err) {
|
||||||
|
throw new IDBRequestAttemptError("add", this._qt, err, [item, key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get(key: IDBValidKey | IDBKeyRange): IDBRequest<T | null> {
|
||||||
|
try {
|
||||||
|
LOG_REQUESTS && logRequest("get", [key], this._qt);
|
||||||
|
return this._qt.get(key);
|
||||||
|
} catch(err) {
|
||||||
|
throw new IDBRequestAttemptError("get", this._qt, err, [key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getKey(key: IDBValidKey | IDBKeyRange): IDBRequest<IDBValidKey | undefined> {
|
||||||
|
try {
|
||||||
|
LOG_REQUESTS && logRequest("getKey", [key], this._qt);
|
||||||
|
return this._qt.getKey(key)
|
||||||
|
} catch(err) {
|
||||||
|
throw new IDBRequestAttemptError("getKey", this._qt, err, [key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(key: IDBValidKey | IDBKeyRange): IDBRequest<undefined> {
|
||||||
|
try {
|
||||||
|
LOG_REQUESTS && logRequest("delete", [key], this._qt);
|
||||||
|
return this._qtStore.delete(key);
|
||||||
|
} catch(err) {
|
||||||
|
throw new IDBRequestAttemptError("delete", this._qt, err, [key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
index(name: string): IDBIndex {
|
||||||
|
try {
|
||||||
|
return this._qtStore.index(name);
|
||||||
|
} catch(err) {
|
||||||
|
// TODO: map to different error? this is not a request
|
||||||
|
throw new IDBRequestAttemptError("index", this._qt, err, [name]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Store<T> extends QueryTarget<T> {
|
||||||
|
private _transaction: Transaction;
|
||||||
|
|
||||||
|
constructor(idbStore: IDBObjectStore, transaction: Transaction) {
|
||||||
|
super(new QueryTargetWrapper<T>(idbStore));
|
||||||
|
this._transaction = transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
get IDBKeyRange() {
|
||||||
|
// @ts-ignore
|
||||||
|
return this._transaction.IDBKeyRange;
|
||||||
|
}
|
||||||
|
|
||||||
|
get _idbStore(): QueryTargetWrapper<T> {
|
||||||
|
return (this._target as QueryTargetWrapper<T>);
|
||||||
|
}
|
||||||
|
|
||||||
|
index(indexName: string): QueryTarget<T> {
|
||||||
|
return new QueryTarget<T>(new QueryTargetWrapper<T>(this._idbStore.index(indexName)));
|
||||||
|
}
|
||||||
|
|
||||||
|
put(value: T): Promise<IDBValidKey> {
|
||||||
|
// If this request fails, the error will bubble up to the transaction and abort it,
|
||||||
|
// which is the behaviour we want. Therefore, it is ok to not create a promise for this
|
||||||
|
// request and await it.
|
||||||
|
//
|
||||||
|
// Perhaps at some later point, we will want to handle an error (like ConstraintError) for
|
||||||
|
// individual write requests. In that case, we should add a method that returns a promise (e.g. putAndObserve)
|
||||||
|
// and call preventDefault on the event to prevent it from aborting the transaction
|
||||||
|
//
|
||||||
|
// Note that this can still throw synchronously, like it does for TransactionInactiveError,
|
||||||
|
// see https://www.w3.org/TR/IndexedDB-2/#transaction-lifetime-concept
|
||||||
|
return reqAsPromise(this._idbStore.put(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
add(value: T): Promise<IDBValidKey> {
|
||||||
|
// ok to not monitor result of request, see comment in `put`.
|
||||||
|
return reqAsPromise(this._idbStore.add(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(keyOrKeyRange: IDBValidKey | IDBKeyRange): Promise<undefined> {
|
||||||
|
// ok to not monitor result of request, see comment in `put`.
|
||||||
|
return reqAsPromise(this._idbStore.delete(keyOrKeyRange));
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,36 +14,41 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
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();
|
||||||
}
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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);
|
|
@ -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 => {
|
|
@ -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"});
|
||||||
}
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
51
src/matrix/storage/idb/stores/InviteStore.ts
Normal file
51
src/matrix/storage/idb/stores/InviteStore.ts
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
import {Store} from "../Store";
|
||||||
|
import {MemberData} from "./RoomMemberStore";
|
||||||
|
|
||||||
|
// TODO: Move to Invite when that's TypeScript.
|
||||||
|
export interface InviteData {
|
||||||
|
roomId: string;
|
||||||
|
isEncrypted: boolean;
|
||||||
|
isDirectMessage: boolean;
|
||||||
|
name?: string;
|
||||||
|
avatarUrl?: string;
|
||||||
|
avatarColorId: number;
|
||||||
|
canonicalAlias?: string;
|
||||||
|
timestamp: number;
|
||||||
|
joinRule: string;
|
||||||
|
inviter?: MemberData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class InviteStore {
|
||||||
|
private _inviteStore: Store<InviteData>;
|
||||||
|
|
||||||
|
constructor(inviteStore: Store<InviteData>) {
|
||||||
|
this._inviteStore = inviteStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAll(): Promise<InviteData[]> {
|
||||||
|
return this._inviteStore.selectAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
set(invite: InviteData): Promise<IDBValidKey> {
|
||||||
|
return this._inviteStore.put(invite);
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(roomId: string): void {
|
||||||
|
this._inviteStore.delete(roomId);
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,26 +13,37 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,40 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export class SessionStore {
|
|
||||||
constructor(sessionStore) {
|
|
||||||
this._sessionStore = sessionStore;
|
|
||||||
}
|
|
||||||
|
|
||||||
async get(key) {
|
|
||||||
const entry = await this._sessionStore.get(key);
|
|
||||||
if (entry) {
|
|
||||||
return entry.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
set(key, value) {
|
|
||||||
this._sessionStore.put({key, value});
|
|
||||||
}
|
|
||||||
|
|
||||||
add(key, value) {
|
|
||||||
this._sessionStore.add({key, value});
|
|
||||||
}
|
|
||||||
|
|
||||||
remove(key) {
|
|
||||||
this._sessionStore.delete(key);
|
|
||||||
}
|
|
||||||
}
|
|
48
src/matrix/storage/idb/stores/SessionStore.ts
Normal file
48
src/matrix/storage/idb/stores/SessionStore.ts
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
import {Store} from "../Store";
|
||||||
|
|
||||||
|
export interface SessionEntry {
|
||||||
|
key: string;
|
||||||
|
value: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SessionStore {
|
||||||
|
private _sessionStore: Store<SessionEntry>
|
||||||
|
|
||||||
|
constructor(sessionStore: Store<SessionEntry>) {
|
||||||
|
this._sessionStore = sessionStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(key: IDBValidKey): Promise<any> {
|
||||||
|
const entry = await this._sessionStore.get(key);
|
||||||
|
if (entry) {
|
||||||
|
return entry.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set(key: string, value: any): Promise<IDBValidKey> {
|
||||||
|
return this._sessionStore.put({key, value});
|
||||||
|
}
|
||||||
|
|
||||||
|
add(key: string, value: any): Promise<IDBValidKey> {
|
||||||
|
return this._sessionStore.add({key, value});
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(key: IDBValidKey): Promise<undefined> {
|
||||||
|
return this._sessionStore.delete(key);
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,25 +15,52 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {EventKey} from "../../../room/timeline/EventKey.js";
|
import {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);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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(
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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");
|
|
@ -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 = {}) {
|
||||||
|
|
|
@ -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 }
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Reference in a new issue