forked from mystiq/hydrogen-web
Merge pull request #475 from vector-im/snowpack-ts-storage-4
Snowpack + Typescript conversion (Part 4)
This commit is contained in:
commit
ed082c9869
9 changed files with 229 additions and 197 deletions
|
@ -14,28 +14,33 @@ 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, StoreNames, StorageError } from "../common";
|
import { STORE_NAMES, StoreNames, StorageError } from "../common";
|
||||||
import { reqAsPromise } from "./utils";
|
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;
|
||||||
|
private _IDBKeyRange: typeof IDBKeyRange
|
||||||
|
storeNames: typeof StoreNames;
|
||||||
|
|
||||||
|
constructor(idbDatabase: IDBDatabase, _IDBKeyRange: typeof IDBKeyRange, hasWebkitEarlyCloseTxnBug: boolean) {
|
||||||
this._db = idbDatabase;
|
this._db = idbDatabase;
|
||||||
this._IDBKeyRange = IDBKeyRange;
|
this._IDBKeyRange = _IDBKeyRange;
|
||||||
this._hasWebkitEarlyCloseTxnBug = hasWebkitEarlyCloseTxnBug;
|
this._hasWebkitEarlyCloseTxnBug = hasWebkitEarlyCloseTxnBug;
|
||||||
this.storeNames = StoreNames;
|
this.storeNames = StoreNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
_validateStoreNames(storeNames) {
|
_validateStoreNames(storeNames: StoreNames[]): 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: StoreNames[]): Promise<Transaction> {
|
||||||
this._validateStoreNames(storeNames);
|
this._validateStoreNames(storeNames);
|
||||||
try {
|
try {
|
||||||
const txn = this._db.transaction(storeNames, "readonly");
|
const txn = this._db.transaction(storeNames, "readonly");
|
||||||
|
@ -50,7 +55,7 @@ export class Storage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async readWriteTxn(storeNames) {
|
async readWriteTxn(storeNames: StoreNames[]): Promise<Transaction> {
|
||||||
this._validateStoreNames(storeNames);
|
this._validateStoreNames(storeNames);
|
||||||
try {
|
try {
|
||||||
const txn = this._db.transaction(storeNames, "readwrite");
|
const txn = this._db.transaction(storeNames, "readwrite");
|
||||||
|
@ -65,7 +70,7 @@ export class Storage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
close(): void {
|
||||||
this._db.close();
|
this._db.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -14,19 +14,24 @@ 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";
|
import { openDatabase, reqAsPromise } from "./utils";
|
||||||
import { exportSession, importSession } from "./export.js";
|
import { exportSession, importSession, Export } from "./export";
|
||||||
import { schema } from "./schema.js";
|
import { schema } from "./schema";
|
||||||
import { detectWebkitEarlyCloseTxnBug } from "./quirks.js";
|
import { detectWebkitEarlyCloseTxnBug } from "./quirks";
|
||||||
|
import { BaseLogger } from "../../../logging/BaseLogger.js";
|
||||||
|
|
||||||
const sessionName = sessionId => `hydrogen_session_${sessionId}`;
|
const sessionName = (sessionId: string) => `hydrogen_session_${sessionId}`;
|
||||||
const openDatabaseWithSessionId = function(sessionId, idbFactory, log) {
|
const openDatabaseWithSessionId = function(sessionId: string, idbFactory: IDBFactory, log?: BaseLogger) {
|
||||||
const create = (db, txn, oldVersion, version) => createStores(db, txn, oldVersion, version, log);
|
const create = (db, txn, oldVersion, version) => createStores(db, txn, oldVersion, version, log);
|
||||||
return openDatabase(sessionName(sessionId), create, schema.length, idbFactory);
|
return openDatabase(sessionName(sessionId), create, 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) {
|
||||||
|
@ -44,13 +49,17 @@ async function requestPersistedStorage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class StorageFactory {
|
export class StorageFactory {
|
||||||
constructor(serviceWorkerHandler, idbFactory = window.indexedDB, IDBKeyRange = window.IDBKeyRange) {
|
private _serviceWorkerHandler: ServiceWorkerHandler;
|
||||||
|
private _idbFactory: IDBFactory;
|
||||||
|
private _IDBKeyRange: typeof IDBKeyRange
|
||||||
|
|
||||||
|
constructor(serviceWorkerHandler: ServiceWorkerHandler, idbFactory: IDBFactory = window.indexedDB, _IDBKeyRange = window.IDBKeyRange) {
|
||||||
this._serviceWorkerHandler = serviceWorkerHandler;
|
this._serviceWorkerHandler = serviceWorkerHandler;
|
||||||
this._idbFactory = idbFactory;
|
this._idbFactory = idbFactory;
|
||||||
this._IDBKeyRange = IDBKeyRange;
|
this._IDBKeyRange = _IDBKeyRange;
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(sessionId, log) {
|
async create(sessionId: string, log?: BaseLogger): 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
|
||||||
|
@ -64,24 +73,24 @@ export class StorageFactory {
|
||||||
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<Export> {
|
||||||
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: Export): 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, log) {
|
async function createStores(db: IDBDatabase, txn: IDBTransaction, oldVersion: number | null, version: number, log?: BaseLogger): Promise<void> {
|
||||||
const startIdx = oldVersion || 0;
|
const startIdx = oldVersion || 0;
|
||||||
return log.wrap({l: "storage migration", oldVersion, version}, async log => {
|
return log.wrap({l: "storage migration", oldVersion, version}, async log => {
|
||||||
for(let i = startIdx; i < version; ++i) {
|
for(let i = startIdx; i < version; ++i) {
|
|
@ -1,142 +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 {txnAsPromise} from "./utils";
|
|
||||||
import {StorageError} from "../common";
|
|
||||||
import {Store} from "./Store";
|
|
||||||
import {SessionStore} from "./stores/SessionStore";
|
|
||||||
import {RoomSummaryStore} from "./stores/RoomSummaryStore";
|
|
||||||
import {InviteStore} from "./stores/InviteStore";
|
|
||||||
import {TimelineEventStore} from "./stores/TimelineEventStore";
|
|
||||||
import {TimelineRelationStore} from "./stores/TimelineRelationStore";
|
|
||||||
import {RoomStateStore} from "./stores/RoomStateStore";
|
|
||||||
import {RoomMemberStore} from "./stores/RoomMemberStore";
|
|
||||||
import {TimelineFragmentStore} from "./stores/TimelineFragmentStore";
|
|
||||||
import {PendingEventStore} from "./stores/PendingEventStore";
|
|
||||||
import {UserIdentityStore} from "./stores/UserIdentityStore";
|
|
||||||
import {DeviceIdentityStore} from "./stores/DeviceIdentityStore";
|
|
||||||
import {OlmSessionStore} from "./stores/OlmSessionStore";
|
|
||||||
import {InboundGroupSessionStore} from "./stores/InboundGroupSessionStore";
|
|
||||||
import {OutboundGroupSessionStore} from "./stores/OutboundGroupSessionStore";
|
|
||||||
import {GroupSessionDecryptionStore} from "./stores/GroupSessionDecryptionStore";
|
|
||||||
import {OperationStore} from "./stores/OperationStore";
|
|
||||||
import {AccountDataStore} from "./stores/AccountDataStore";
|
|
||||||
|
|
||||||
export class Transaction {
|
|
||||||
constructor(txn, allowedStoreNames, IDBKeyRange) {
|
|
||||||
this._txn = txn;
|
|
||||||
this._allowedStoreNames = allowedStoreNames;
|
|
||||||
this._stores = {};
|
|
||||||
this.IDBKeyRange = IDBKeyRange;
|
|
||||||
}
|
|
||||||
|
|
||||||
_idbStore(name) {
|
|
||||||
if (!this._allowedStoreNames.includes(name)) {
|
|
||||||
// more specific error? this is a bug, so maybe not ...
|
|
||||||
throw new StorageError(`Invalid store for transaction: ${name}, only ${this._allowedStoreNames.join(", ")} are allowed.`);
|
|
||||||
}
|
|
||||||
return new Store(this._txn.objectStore(name), this);
|
|
||||||
}
|
|
||||||
|
|
||||||
_store(name, mapStore) {
|
|
||||||
if (!this._stores[name]) {
|
|
||||||
const idbStore = this._idbStore(name);
|
|
||||||
this._stores[name] = mapStore(idbStore);
|
|
||||||
}
|
|
||||||
return this._stores[name];
|
|
||||||
}
|
|
||||||
|
|
||||||
get session() {
|
|
||||||
return this._store("session", idbStore => new SessionStore(idbStore));
|
|
||||||
}
|
|
||||||
|
|
||||||
get roomSummary() {
|
|
||||||
return this._store("roomSummary", idbStore => new RoomSummaryStore(idbStore));
|
|
||||||
}
|
|
||||||
|
|
||||||
get archivedRoomSummary() {
|
|
||||||
return this._store("archivedRoomSummary", idbStore => new RoomSummaryStore(idbStore));
|
|
||||||
}
|
|
||||||
|
|
||||||
get invites() {
|
|
||||||
return this._store("invites", idbStore => new InviteStore(idbStore));
|
|
||||||
}
|
|
||||||
|
|
||||||
get timelineFragments() {
|
|
||||||
return this._store("timelineFragments", idbStore => new TimelineFragmentStore(idbStore));
|
|
||||||
}
|
|
||||||
|
|
||||||
get timelineEvents() {
|
|
||||||
return this._store("timelineEvents", idbStore => new TimelineEventStore(idbStore));
|
|
||||||
}
|
|
||||||
|
|
||||||
get timelineRelations() {
|
|
||||||
return this._store("timelineRelations", idbStore => new TimelineRelationStore(idbStore));
|
|
||||||
}
|
|
||||||
|
|
||||||
get roomState() {
|
|
||||||
return this._store("roomState", idbStore => new RoomStateStore(idbStore));
|
|
||||||
}
|
|
||||||
|
|
||||||
get roomMembers() {
|
|
||||||
return this._store("roomMembers", idbStore => new RoomMemberStore(idbStore));
|
|
||||||
}
|
|
||||||
|
|
||||||
get pendingEvents() {
|
|
||||||
return this._store("pendingEvents", idbStore => new PendingEventStore(idbStore));
|
|
||||||
}
|
|
||||||
|
|
||||||
get userIdentities() {
|
|
||||||
return this._store("userIdentities", idbStore => new UserIdentityStore(idbStore));
|
|
||||||
}
|
|
||||||
|
|
||||||
get deviceIdentities() {
|
|
||||||
return this._store("deviceIdentities", idbStore => new DeviceIdentityStore(idbStore));
|
|
||||||
}
|
|
||||||
|
|
||||||
get olmSessions() {
|
|
||||||
return this._store("olmSessions", idbStore => new OlmSessionStore(idbStore));
|
|
||||||
}
|
|
||||||
|
|
||||||
get inboundGroupSessions() {
|
|
||||||
return this._store("inboundGroupSessions", idbStore => new InboundGroupSessionStore(idbStore));
|
|
||||||
}
|
|
||||||
|
|
||||||
get outboundGroupSessions() {
|
|
||||||
return this._store("outboundGroupSessions", idbStore => new OutboundGroupSessionStore(idbStore));
|
|
||||||
}
|
|
||||||
|
|
||||||
get groupSessionDecryptions() {
|
|
||||||
return this._store("groupSessionDecryptions", idbStore => new GroupSessionDecryptionStore(idbStore));
|
|
||||||
}
|
|
||||||
|
|
||||||
get operations() {
|
|
||||||
return this._store("operations", idbStore => new OperationStore(idbStore));
|
|
||||||
}
|
|
||||||
|
|
||||||
get accountData() {
|
|
||||||
return this._store("accountData", idbStore => new AccountDataStore(idbStore));
|
|
||||||
}
|
|
||||||
|
|
||||||
complete() {
|
|
||||||
return txnAsPromise(this._txn);
|
|
||||||
}
|
|
||||||
|
|
||||||
abort() {
|
|
||||||
// TODO: should we wrap the exception in a StorageError?
|
|
||||||
this._txn.abort();
|
|
||||||
}
|
|
||||||
}
|
|
148
src/matrix/storage/idb/Transaction.ts
Normal file
148
src/matrix/storage/idb/Transaction.ts
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
/*
|
||||||
|
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 {StoreNames} from "../common";
|
||||||
|
import {txnAsPromise} from "./utils";
|
||||||
|
import {StorageError} from "../common";
|
||||||
|
import {Store} from "./Store";
|
||||||
|
import {SessionStore} from "./stores/SessionStore";
|
||||||
|
import {RoomSummaryStore} from "./stores/RoomSummaryStore";
|
||||||
|
import {InviteStore} from "./stores/InviteStore";
|
||||||
|
import {TimelineEventStore} from "./stores/TimelineEventStore";
|
||||||
|
import {TimelineRelationStore} from "./stores/TimelineRelationStore";
|
||||||
|
import {RoomStateStore} from "./stores/RoomStateStore";
|
||||||
|
import {RoomMemberStore} from "./stores/RoomMemberStore";
|
||||||
|
import {TimelineFragmentStore} from "./stores/TimelineFragmentStore";
|
||||||
|
import {PendingEventStore} from "./stores/PendingEventStore";
|
||||||
|
import {UserIdentityStore} from "./stores/UserIdentityStore";
|
||||||
|
import {DeviceIdentityStore} from "./stores/DeviceIdentityStore";
|
||||||
|
import {OlmSessionStore} from "./stores/OlmSessionStore";
|
||||||
|
import {InboundGroupSessionStore} from "./stores/InboundGroupSessionStore";
|
||||||
|
import {OutboundGroupSessionStore} from "./stores/OutboundGroupSessionStore";
|
||||||
|
import {GroupSessionDecryptionStore} from "./stores/GroupSessionDecryptionStore";
|
||||||
|
import {OperationStore} from "./stores/OperationStore";
|
||||||
|
import {AccountDataStore} from "./stores/AccountDataStore";
|
||||||
|
|
||||||
|
export class Transaction {
|
||||||
|
private _txn: IDBTransaction;
|
||||||
|
private _allowedStoreNames: StoreNames[];
|
||||||
|
private _stores: { [storeName in StoreNames]?: any };
|
||||||
|
|
||||||
|
constructor(txn: IDBTransaction, allowedStoreNames: StoreNames[], IDBKeyRange) {
|
||||||
|
this._txn = txn;
|
||||||
|
this._allowedStoreNames = allowedStoreNames;
|
||||||
|
this._stores = {};
|
||||||
|
// @ts-ignore
|
||||||
|
this.IDBKeyRange = IDBKeyRange;
|
||||||
|
}
|
||||||
|
|
||||||
|
_idbStore(name: StoreNames): Store<any> {
|
||||||
|
if (!this._allowedStoreNames.includes(name)) {
|
||||||
|
// more specific error? this is a bug, so maybe not ...
|
||||||
|
throw new StorageError(`Invalid store for transaction: ${name}, only ${this._allowedStoreNames.join(", ")} are allowed.`);
|
||||||
|
}
|
||||||
|
return new Store(this._txn.objectStore(name), this);
|
||||||
|
}
|
||||||
|
|
||||||
|
_store<T>(name: StoreNames, mapStore: (idbStore: Store<any>) => T): T {
|
||||||
|
if (!this._stores[name]) {
|
||||||
|
const idbStore = this._idbStore(name);
|
||||||
|
this._stores[name] = mapStore(idbStore);
|
||||||
|
}
|
||||||
|
return this._stores[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
get session(): SessionStore {
|
||||||
|
return this._store(StoreNames.session, idbStore => new SessionStore(idbStore));
|
||||||
|
}
|
||||||
|
|
||||||
|
get roomSummary(): RoomSummaryStore {
|
||||||
|
return this._store(StoreNames.roomSummary, idbStore => new RoomSummaryStore(idbStore));
|
||||||
|
}
|
||||||
|
|
||||||
|
get archivedRoomSummary(): RoomSummaryStore {
|
||||||
|
return this._store(StoreNames.archivedRoomSummary, idbStore => new RoomSummaryStore(idbStore));
|
||||||
|
}
|
||||||
|
|
||||||
|
get invites(): InviteStore {
|
||||||
|
return this._store(StoreNames.invites, idbStore => new InviteStore(idbStore));
|
||||||
|
}
|
||||||
|
|
||||||
|
get timelineFragments(): TimelineFragmentStore {
|
||||||
|
return this._store(StoreNames.timelineFragments, idbStore => new TimelineFragmentStore(idbStore));
|
||||||
|
}
|
||||||
|
|
||||||
|
get timelineEvents(): TimelineEventStore {
|
||||||
|
return this._store(StoreNames.timelineEvents, idbStore => new TimelineEventStore(idbStore));
|
||||||
|
}
|
||||||
|
|
||||||
|
get timelineRelations(): TimelineRelationStore {
|
||||||
|
return this._store(StoreNames.timelineRelations, idbStore => new TimelineRelationStore(idbStore));
|
||||||
|
}
|
||||||
|
|
||||||
|
get roomState(): RoomStateStore {
|
||||||
|
return this._store(StoreNames.roomState, idbStore => new RoomStateStore(idbStore));
|
||||||
|
}
|
||||||
|
|
||||||
|
get roomMembers(): RoomMemberStore {
|
||||||
|
return this._store(StoreNames.roomMembers, idbStore => new RoomMemberStore(idbStore));
|
||||||
|
}
|
||||||
|
|
||||||
|
get pendingEvents(): PendingEventStore {
|
||||||
|
return this._store(StoreNames.pendingEvents, idbStore => new PendingEventStore(idbStore));
|
||||||
|
}
|
||||||
|
|
||||||
|
get userIdentities(): UserIdentityStore {
|
||||||
|
return this._store(StoreNames.userIdentities, idbStore => new UserIdentityStore(idbStore));
|
||||||
|
}
|
||||||
|
|
||||||
|
get deviceIdentities(): DeviceIdentityStore {
|
||||||
|
return this._store(StoreNames.deviceIdentities, idbStore => new DeviceIdentityStore(idbStore));
|
||||||
|
}
|
||||||
|
|
||||||
|
get olmSessions(): OlmSessionStore {
|
||||||
|
return this._store(StoreNames.olmSessions, idbStore => new OlmSessionStore(idbStore));
|
||||||
|
}
|
||||||
|
|
||||||
|
get inboundGroupSessions(): InboundGroupSessionStore {
|
||||||
|
return this._store(StoreNames.inboundGroupSessions, idbStore => new InboundGroupSessionStore(idbStore));
|
||||||
|
}
|
||||||
|
|
||||||
|
get outboundGroupSessions(): OutboundGroupSessionStore {
|
||||||
|
return this._store(StoreNames.outboundGroupSessions, idbStore => new OutboundGroupSessionStore(idbStore));
|
||||||
|
}
|
||||||
|
|
||||||
|
get groupSessionDecryptions(): GroupSessionDecryptionStore {
|
||||||
|
return this._store(StoreNames.groupSessionDecryptions, idbStore => new GroupSessionDecryptionStore(idbStore));
|
||||||
|
}
|
||||||
|
|
||||||
|
get operations(): OperationStore {
|
||||||
|
return this._store(StoreNames.operations, idbStore => new OperationStore(idbStore));
|
||||||
|
}
|
||||||
|
|
||||||
|
get accountData(): AccountDataStore {
|
||||||
|
return this._store(StoreNames.accountData, idbStore => new AccountDataStore(idbStore));
|
||||||
|
}
|
||||||
|
|
||||||
|
complete(): Promise<void> {
|
||||||
|
return txnAsPromise(this._txn);
|
||||||
|
}
|
||||||
|
|
||||||
|
abort(): void {
|
||||||
|
// TODO: should we wrap the exception in a StorageError?
|
||||||
|
this._txn.abort();
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,25 +14,26 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { iterateCursor, txnAsPromise } from "./utils";
|
import { iterateCursor, NOT_DONE, txnAsPromise } from "./utils";
|
||||||
import { STORE_NAMES } from "../common";
|
import { STORE_NAMES, StoreNames } from "../common";
|
||||||
|
|
||||||
export async function exportSession(db) {
|
export type Export = { [storeName in StoreNames] : any[] }
|
||||||
const NOT_DONE = {done: false};
|
|
||||||
|
export async function exportSession(db: IDBDatabase): Promise<Export> {
|
||||||
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;
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
return data;
|
return data as Export;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function importSession(db, data) {
|
export async function importSession(db: IDBDatabase, data: Export): 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);
|
|
@ -18,7 +18,7 @@ limitations under the License.
|
||||||
import {openDatabase, txnAsPromise, reqAsPromise} from "./utils";
|
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,7 +1,9 @@
|
||||||
import {iterateCursor, reqAsPromise} from "./utils";
|
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 {addRoomToIdentity} from "../../e2ee/DeviceTracker.js";
|
import {addRoomToIdentity} from "../../e2ee/DeviceTracker.js";
|
||||||
import {RoomMemberStore} from "./stores/RoomMemberStore";
|
import {SummaryData} from "../../room/RoomSummary";
|
||||||
|
import {RoomMemberStore, MemberData} from "./stores/RoomMemberStore";
|
||||||
|
import {RoomStateEntry} from "./stores/RoomStateStore";
|
||||||
import {SessionStore} from "./stores/SessionStore";
|
import {SessionStore} from "./stores/SessionStore";
|
||||||
import {encodeScopeTypeKey} from "./stores/OperationStore";
|
import {encodeScopeTypeKey} from "./stores/OperationStore";
|
||||||
import {MAX_UNICODE} from "./stores/common";
|
import {MAX_UNICODE} from "./stores/common";
|
||||||
|
@ -23,10 +25,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"});
|
||||||
|
@ -43,11 +47,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);
|
||||||
|
@ -55,10 +60,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;
|
||||||
|
@ -66,7 +72,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);
|
||||||
}
|
}
|
||||||
|
@ -76,7 +83,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});
|
||||||
|
@ -89,13 +96,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|`));
|
||||||
|
@ -108,31 +116,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) {
|
||||||
|
@ -142,31 +151,33 @@ 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"});
|
||||||
}
|
}
|
||||||
|
|
||||||
//v11 doesn't change the schema, but ensures all userIdentities have all the roomIds they should (see #470)
|
//v11 doesn't change the schema, but ensures all userIdentities have all the roomIds they should (see #470)
|
||||||
async function fixMissingRoomsInUserIdentities(db, txn, log) {
|
async function fixMissingRoomsInUserIdentities(db, txn, log) {
|
||||||
const roomSummaryStore = txn.objectStore("roomSummary");
|
const roomSummaryStore = txn.objectStore("roomSummary");
|
||||||
const trackedRoomIds = [];
|
const trackedRoomIds: string[] = [];
|
||||||
await iterateCursor(roomSummaryStore.openCursor(), roomSummary => {
|
await iterateCursor<SummaryData>(roomSummaryStore.openCursor(), roomSummary => {
|
||||||
if (roomSummary.isTrackingMembers) {
|
if (roomSummary.isTrackingMembers) {
|
||||||
trackedRoomIds.push(roomSummary.roomId);
|
trackedRoomIds.push(roomSummary.roomId);
|
||||||
}
|
}
|
||||||
|
return NOT_DONE;
|
||||||
});
|
});
|
||||||
const outboundGroupSessionsStore = txn.objectStore("outboundGroupSessions");
|
const outboundGroupSessionsStore = txn.objectStore("outboundGroupSessions");
|
||||||
const userIdentitiesStore = txn.objectStore("userIdentities");
|
const userIdentitiesStore: IDBObjectStore = txn.objectStore("userIdentities");
|
||||||
const roomMemberStore = txn.objectStore("roomMembers");
|
const roomMemberStore = txn.objectStore("roomMembers");
|
||||||
for (const roomId of trackedRoomIds) {
|
for (const roomId of trackedRoomIds) {
|
||||||
let foundMissing = false;
|
let foundMissing = false;
|
||||||
const joinedUserIds = [];
|
const joinedUserIds: string[] = [];
|
||||||
const memberRange = IDBKeyRange.bound(roomId, `${roomId}|${MAX_UNICODE}`, true, true);
|
const memberRange = IDBKeyRange.bound(roomId, `${roomId}|${MAX_UNICODE}`, true, true);
|
||||||
await log.wrap({l: "room", id: roomId}, async log => {
|
await log.wrap({l: "room", id: roomId}, async log => {
|
||||||
await iterateCursor(roomMemberStore.openCursor(memberRange), member => {
|
await iterateCursor<MemberData>(roomMemberStore.openCursor(memberRange), member => {
|
||||||
if (member.membership === "join") {
|
if (member.membership === "join") {
|
||||||
joinedUserIds.push(member.userId);
|
joinedUserIds.push(member.userId);
|
||||||
}
|
}
|
||||||
|
return NOT_DONE;
|
||||||
});
|
});
|
||||||
log.set("joinedUserIds", joinedUserIds.length);
|
log.set("joinedUserIds", joinedUserIds.length);
|
||||||
for (const userId of joinedUserIds) {
|
for (const userId of joinedUserIds) {
|
|
@ -15,7 +15,7 @@ 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";
|
||||||
import {NullLogItem} from "../logging/NullLogger.js";
|
import {NullLogItem} from "../logging/NullLogger.js";
|
||||||
|
|
||||||
export function createMockStorage() {
|
export function createMockStorage() {
|
||||||
|
|
|
@ -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";
|
||||||
|
|
Loading…
Reference in a new issue