diff --git a/src/main.js b/src/main.js index 01760ab7..1f8286fd 100644 --- a/src/main.js +++ b/src/main.js @@ -1,5 +1,6 @@ -import Network from "../network.js"; -import Session from "../session.js"; +import Network from "./network.js"; +import Session from "./session.js"; +import createIdbStorage from "./storage/idb/factory.js"; const HOMESERVER = "http://localhost:8008"; async function getLoginData(username, password) { @@ -17,7 +18,7 @@ async function getLoginData(username, password) { async function main() { const loginData = await getLoginData("bruno1", "testtest"); const network = new Network(HOMESERVER, loginData.access_token); - const storage = new IdbStorage("morpheus_session"); + const storage = await createIdbStorage("morpheus_session"); const session = new Session(loginData, storage); await session.load(); const sync = new Sync(network, session, storage); diff --git a/src/storage/idb/factory.js b/src/storage/idb/factory.js new file mode 100644 index 00000000..54cf82fe --- /dev/null +++ b/src/storage/idb/factory.js @@ -0,0 +1,18 @@ +export default async function createIdbStorage(databaseName) { + const db = await openDatabase(databaseName, createStores); + return new Storage(db); +} + +function createStores(db) { + db.createObjectStore("sync"); //sync token + db.createObjectStore("roomSummary", "room_id", {unique: true}); + const timeline = db.createObjectStore("roomTimeline", ["room_id", "sort_key"]); + timeline.createIndex("by_event_id", ["room_id", "event.event_id"], {unique: true}); + // how to get the first/last x events for a room? + // we don't want to specify the sort key, but would need an index for the room_id? + // take sort_key as primary key then and have index on event_id? + // still, you also can't have a PK of [room_id, sort_key] and get the last or first events with just the room_id? the only thing that changes it that the PK will provide an inherent sorting that you inherit in an index that only has room_id as keyPath??? There must be a better way, need to write a prototype test for this. + // SOLUTION: with numeric keys, you can just us a min/max value to get first/last + // db.createObjectStore("members", ["room_id", "state_key"]); + const state = db.createObjectStore("roomState", ["event.room_id", "event.type", "event.state_key"]); +} \ No newline at end of file diff --git a/src/storage/idb/index.js b/src/storage/idb/index.js new file mode 100644 index 00000000..5e5dd7a9 --- /dev/null +++ b/src/storage/idb/index.js @@ -0,0 +1,11 @@ +import QueryTarget from "./query-target.js"; + +export default class Index extends QueryTarget { + constructor(index) { + this._index = index; + } + + _queryTarget() { + return this._index; + } +} \ No newline at end of file diff --git a/src/storage/idb/db.js b/src/storage/idb/query-target.js similarity index 53% rename from src/storage/idb/db.js rename to src/storage/idb/query-target.js index 38d258cf..b0fbf538 100644 --- a/src/storage/idb/db.js +++ b/src/storage/idb/query-target.js @@ -1,80 +1,6 @@ -const SYNC_STORES = [ - "sync", - "summary", - "timeline", - "members", - "state" -]; +import {iterateCursor} from "./utils"; -class Database { - constructor(idbDatabase) { - this._db = idbDatabase; - this._syncTxn = null; - } - - async startSyncTxn() { - const txn = this._db.transaction(SYNC_STORES, "readwrite"); - return new Transaction(txn, SYNC_STORES); - } - - startReadOnlyTxn(storeName) { - if (this._syncTxn && SYNC_STORES.includes(storeName)) { - return this._syncTxn; - } else { - return this._db.transaction([storeName], "readonly"); - } - } - - startReadWriteTxn(storeName) { - if (this._syncTxn && SYNC_STORES.includes(storeName)) { - return this._syncTxn; - } else { - return this._db.transaction([storeName], "readwrite"); - } - } - - store(storeName) { - return new ObjectStore(this, storeName); - } -} - -class Transaction { - constructor(txn, allowedStoreNames) { - this._txn = txn; - this._stores = { - sync: null, - summary: null, - timeline: null, - state: null, - }; - this._allowedStoreNames = allowedStoreNames; - } - - _idbStore(name) { - if (!this._allowedStoreNames.includes(name)) { - throw new Error(`Invalid store for transaction: ${name}, only ${this._allowedStoreNames.join(", ")} are allowed.`); - } - return new ObjectStore(this._txn.getObjectStore(name)); - } - - get timeline() { - if (!this._stores.timeline) { - const idbStore = this._idbStore("timeline"); - this._stores.timeline = new TimelineStore(idbStore); - } - return this._stores.timeline; - } - - complete() { - return txnAsPromise(this._txn); - } - - abort() { - this._txn.abort(); - } -} - -class QueryTarget { +export default class QueryTarget { reduce(range, reducer, initialValue) { return this._reduce(range, reducer, initialValue, "next"); } @@ -165,28 +91,4 @@ class QueryTarget { _queryTarget() { throw new Error("override this"); } -} - -class ObjectStore extends QueryTarget { - constructor(store) { - this._store = store; - } - - _queryTarget() { - return this._store; - } - - index(indexName) { - return new Index(this._store.index(indexName)); - } -} - -class Index extends QueryTarget { - constructor(index) { - this._index = index; - } - - _queryTarget() { - return this._index; - } } \ No newline at end of file diff --git a/src/storage/idb/storage.js b/src/storage/idb/storage.js new file mode 100644 index 00000000..ba16573b --- /dev/null +++ b/src/storage/idb/storage.js @@ -0,0 +1,31 @@ +export const STORE_NAMES = ["sync", "roomState", "roomSummary", "roomTimeline"]; + +export default class Storage { + constructor(idbDatabase) { + this._db = idbDatabase; + const nameMap = STORE_NAMES.reduce((nameMap, name) => { + nameMap[name] = name; + return nameMap; + }, {}); + this.storeNames = Object.freeze(nameMap); + } + + _validateStoreNames(storeNames) { + const unknownStoreName = storeNames.find(name => !STORE_NAMES.includes(name)); + if (unknownStoreName) { + throw new Error(`Tried to open a transaction for unknown store ${unknownStoreName}`); + } + } + + startReadOnlyTxn(storeNames) { + this._validateStoreNames(storeNames); + const txn = this._db.transaction(storeNames, "readonly"); + return new Transaction(txn, storeNames); + } + + startReadWriteTxn(storeNames) { + this._validateStoreNames(storeNames); + const txn = this._db.transaction(storeNames, "readwrite"); + return new Transaction(txn, storeNames); + } +} \ No newline at end of file diff --git a/src/storage/idb/store.js b/src/storage/idb/store.js new file mode 100644 index 00000000..800bca17 --- /dev/null +++ b/src/storage/idb/store.js @@ -0,0 +1,16 @@ +import QueryTarget from "./query-target.js"; +import Index from "./index.js"; + +export default class Store extends QueryTarget { + constructor(store) { + this._store = store; + } + + _queryTarget() { + return this._store; + } + + index(indexName) { + return new Index(this._store.index(indexName)); + } +} \ No newline at end of file diff --git a/src/storage/idb/member.js b/src/storage/idb/stores/member.js similarity index 100% rename from src/storage/idb/member.js rename to src/storage/idb/stores/member.js diff --git a/src/storage/idb/room.js b/src/storage/idb/stores/room.js similarity index 100% rename from src/storage/idb/room.js rename to src/storage/idb/stores/room.js diff --git a/src/storage/idb/session-index.js b/src/storage/idb/stores/session-index.js similarity index 85% rename from src/storage/idb/session-index.js rename to src/storage/idb/stores/session-index.js index 095660f8..a67096a4 100644 --- a/src/storage/idb/session-index.js +++ b/src/storage/idb/stores/session-index.js @@ -6,8 +6,8 @@ function createSessionsStore(db) { function createStores(db) { db.createObjectStore("sync"); //sync token - db.createObjectStore("summary", "room_id", {unique: true}); - const timeline = db.createObjectStore("timeline", ["room_id", "sort_key"]); + db.createObjectStore("roomSummary", "room_id", {unique: true}); + const timeline = db.createObjectStore("roomTimeline", ["room_id", "sort_key"]); timeline.createIndex("by_event_id", ["room_id", "event.event_id"], {unique: true}); // how to get the first/last x events for a room? // we don't want to specify the sort key, but would need an index for the room_id? @@ -15,7 +15,7 @@ function createStores(db) { // still, you also can't have a PK of [room_id, sort_key] and get the last or first events with just the room_id? the only thing that changes it that the PK will provide an inherent sorting that you inherit in an index that only has room_id as keyPath??? There must be a better way, need to write a prototype test for this. // SOLUTION: with numeric keys, you can just us a min/max value to get first/last // db.createObjectStore("members", ["room_id", "state_key"]); - const state = db.createObjectStore("state", ["event.room_id", "event.type", "event.state_key"]); + const state = db.createObjectStore("roomState", ["event.room_id", "event.type", "event.state_key"]); } class Sessions { diff --git a/src/storage/idb/session.js b/src/storage/idb/stores/session.js similarity index 100% rename from src/storage/idb/session.js rename to src/storage/idb/stores/session.js diff --git a/src/storage/idb/state.js b/src/storage/idb/stores/state.js similarity index 82% rename from src/storage/idb/state.js rename to src/storage/idb/stores/state.js index 47895283..f303dd8d 100644 --- a/src/storage/idb/state.js +++ b/src/storage/idb/stores/state.js @@ -1,5 +1,5 @@ class StateStore { - constructor(roomId, db) { + constructor(db) { } diff --git a/src/storage/idb/timeline.js b/src/storage/idb/stores/timeline.js similarity index 100% rename from src/storage/idb/timeline.js rename to src/storage/idb/stores/timeline.js diff --git a/src/storage/idb/sync.js b/src/storage/idb/sync.js deleted file mode 100644 index e23d206e..00000000 --- a/src/storage/idb/sync.js +++ /dev/null @@ -1,13 +0,0 @@ -class SyncTransaction { - setSyncToken(syncToken, lastSynced) { - - } - - getRoomStore(roomId) { - new RoomStore(new Database(null, this._txn), roomId) - } - - result() { - return txnAsPromise(this._txn); - } -} \ No newline at end of file diff --git a/src/storage/idb/transaction.js b/src/storage/idb/transaction.js new file mode 100644 index 00000000..4dfaea39 --- /dev/null +++ b/src/storage/idb/transaction.js @@ -0,0 +1,40 @@ +import {txnAsPromise} from "./utils"; +import Store from "./store.js"; +import TimelineStore from "./stores/timeline.js"; + +export default class Transaction { + constructor(txn, allowedStoreNames) { + this._txn = txn; + this._allowedStoreNames = allowedStoreNames; + this._stores = { + sync: null, + summary: null, + timeline: null, + state: null, + }; + } + + _store(name) { + if (!this._allowedStoreNames.includes(name)) { + // more specific error? this is a bug, so maybe not ... + throw new Error(`Invalid store for transaction: ${name}, only ${this._allowedStoreNames.join(", ")} are allowed.`); + } + return new Store(this._txn.getObjectStore(name)); + } + + get timeline() { + if (!this._stores.timeline) { + const idbStore = this._idbStore("timeline"); + this._stores.timeline = new TimelineStore(idbStore); + } + return this._stores.timeline; + } + + complete() { + return txnAsPromise(this._txn); + } + + abort() { + this._txn.abort(); + } +} \ No newline at end of file diff --git a/src/storage/idb/gapsortkey.js b/src/storage/sortkey.js similarity index 100% rename from src/storage/idb/gapsortkey.js rename to src/storage/sortkey.js diff --git a/src/sync.js b/src/sync.js index 1cf755bd..b6c707d6 100644 --- a/src/sync.js +++ b/src/sync.js @@ -62,7 +62,12 @@ export class Sync { this._currentRequest = this._network.sync(timeout, syncToken); const response = await this._currentRequest.response; syncToken = response.next_batch; - const txn = this._storage.startSyncTxn(); + const storeNames = this._storage.storeNames; + const txn = this._storage.startReadWriteTxn([ + storeNames.timeline, + storeNames.sync, + storeNames.state + ]); try { session.applySync(syncToken, response.account_data, txn); // to_device