Write sync interactions from scratch
Doesn't work because we lack `room.preparation` objects when writing sync results...
This commit is contained in:
parent
81fddc008c
commit
1ef6963018
3 changed files with 136 additions and 89 deletions
|
@ -126,6 +126,8 @@ export class Sync3 {
|
||||||
// Hydrogen only has 1 list currently (no DM section) so we only need 1 range
|
// Hydrogen only has 1 list currently (no DM section) so we only need 1 range
|
||||||
this.ranges = [[0, 99]];
|
this.ranges = [[0, 99]];
|
||||||
this.roomIndexToRoomId = {};
|
this.roomIndexToRoomId = {};
|
||||||
|
console.log("session", session);
|
||||||
|
console.log("storage", storage);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start syncing. Probably call this at startup once you have an access_token.
|
// Start syncing. Probably call this at startup once you have an access_token.
|
||||||
|
@ -139,6 +141,17 @@ export class Sync3 {
|
||||||
this.syncLoop(undefined);
|
this.syncLoop(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
if (this.status.get() === SyncStatus.Stopped) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.status.set(SyncStatus.Stopped);
|
||||||
|
if (this.currentRequest) {
|
||||||
|
this.currentRequest.abort();
|
||||||
|
this.currentRequest = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// The purpose of this function is to do repeated /sync calls and call processResponse. It doesn't
|
// The purpose of this function is to do repeated /sync calls and call processResponse. It doesn't
|
||||||
// know or care how to handle the response, it only cares about the position and retries.
|
// know or care how to handle the response, it only cares about the position and retries.
|
||||||
private async syncLoop(pos?: number) {
|
private async syncLoop(pos?: number) {
|
||||||
|
@ -181,7 +194,7 @@ export class Sync3 {
|
||||||
backoffCounter = 0;
|
backoffCounter = 0;
|
||||||
// we have to wait for some parts of the response to be saved to disk before we can go on
|
// we have to wait for some parts of the response to be saved to disk before we can go on
|
||||||
// hence the await.
|
// hence the await.
|
||||||
await this.processResponse(resp);
|
await this.processResponse(isFirstSync, resp);
|
||||||
// increment our position to tell the server we got everything, similar to using ?since= in v2
|
// increment our position to tell the server we got everything, similar to using ?since= in v2
|
||||||
pos = resp.pos;
|
pos = resp.pos;
|
||||||
if (isFirstSync) {
|
if (isFirstSync) {
|
||||||
|
@ -211,37 +224,49 @@ export class Sync3 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Handle room updates
|
|
||||||
// TODO: atomically swap indexToRoom
|
|
||||||
|
|
||||||
// The purpose of this function is process the /sync response and atomically update sync state.
|
// The purpose of this function is process the /sync response and atomically update sync state.
|
||||||
private async processResponse(resp: Sync3Response) {
|
private async processResponse(isFirstSync: boolean, resp: Sync3Response) {
|
||||||
console.log(resp);
|
console.log(resp);
|
||||||
let { indexToRoom, updates } = this.processOps(resp.ops);
|
let { indexToRoom, updates } = this.processOps(resp.ops);
|
||||||
|
|
||||||
// process the room updates: new rooms, new timeline events, updated room names, that sort of thing.
|
// process the room updates: new rooms, new timeline events, updated room names, that sort of thing.
|
||||||
|
// we're kinda forced to use the logger as most functions expect an ILogItem
|
||||||
this.prepareResponse(updates);
|
await this.logger.run("sync", async log => {
|
||||||
|
const syncTxn = await this.openSyncTxn();
|
||||||
|
|
||||||
/*
|
|
||||||
try {
|
try {
|
||||||
await log.wrap("prepare", log => this._prepareSync(sessionState, roomStates, response, log));
|
// this.session.writeSync() // write account data, device lists, etc.
|
||||||
await log.wrap("afterPrepareSync", log => Promise.all(roomStates.map(rs => {
|
await Promise.all(updates.map(async (roomResponse) => {
|
||||||
return rs.room.afterPrepareSync(rs.preparation, log);
|
// get or create a room
|
||||||
})));
|
let room = this.session.rooms.get(roomResponse.room_id);
|
||||||
await log.wrap("write", async log => this._writeSync(
|
if (!room) {
|
||||||
sessionState, inviteStates, roomStates, archivedRoomStates,
|
room = this.session.createRoom(roomResponse.room_id);
|
||||||
response, syncFilterId, isInitialSync, log));
|
|
||||||
} finally {
|
|
||||||
sessionState.dispose();
|
|
||||||
}
|
}
|
||||||
// sync txn comitted, emit updates and apply changes to in-memory state
|
room.writeSync(
|
||||||
log.wrap("after", log => this._afterSync(
|
roomResponse, isFirstSync, {}, syncTxn, log
|
||||||
sessionState, inviteStates, roomStates, archivedRoomStates, log));
|
)
|
||||||
*/
|
}))
|
||||||
// instantly move all the rooms to their new positions
|
} catch (err) {
|
||||||
|
// avoid corrupting state by only
|
||||||
|
// storing the sync up till the point
|
||||||
|
// the exception occurred
|
||||||
|
syncTxn.abort(log);
|
||||||
|
throw syncTxn.getCause(err);
|
||||||
|
}
|
||||||
|
await syncTxn.complete(log);
|
||||||
|
|
||||||
|
// update in-memory structs
|
||||||
|
this.session.afterSync(); // ???
|
||||||
|
|
||||||
|
updates.forEach((roomResponse) => {
|
||||||
|
// get room then afterSync() ???
|
||||||
|
});
|
||||||
|
|
||||||
|
this.session.applyRoomCollectionChangesAfterSync(null, roomStates, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// instantly move all the rooms to their new positions
|
||||||
|
this.roomIndexToRoomId = indexToRoom;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The purpose of this function is to process the response `ops` array by modifying the current
|
// The purpose of this function is to process the response `ops` array by modifying the current
|
||||||
|
@ -368,60 +393,35 @@ export class Sync3 {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async prepareResponse(updates: RoomResponse[]) {
|
private openSyncTxn() {
|
||||||
/*
|
|
||||||
// IF WE HAVE TO-DEVICE MSGS THEN this.session.obtainSyncLock(response) (which checks response.to_device.events)
|
|
||||||
const prepareTxn = await this.openPrepareSyncTxn();
|
|
||||||
// IF WE HAVE TO-DEVICE MSGS THEN this.session.prepareSync(syncResponse, lock, txn, log) => {newKeysByRoom}
|
|
||||||
// purpose: add any rooms with new keys but no sync response to the list of rooms to be synced
|
|
||||||
|
|
||||||
await Promise.all(updates.map(async (roomResponse) => {
|
|
||||||
let storedRoom = this.session.rooms.get(roomResponse.room_id);
|
|
||||||
if (!storedRoom) {
|
|
||||||
storedRoom = this.session.createRoom(roomResponse.room_id);
|
|
||||||
} else {
|
|
||||||
// if previously joined and we still have the timeline for it,
|
|
||||||
// this loads the syncWriter at the correct position to continue writing the timeline
|
|
||||||
await storedRoom.load(null, prepareTxn, this.logger);
|
|
||||||
}
|
|
||||||
return storedRoom.prepareSync(
|
|
||||||
rs.roomResponse, rs.membership, rs.invite, newKeys, prepareTxn, log)
|
|
||||||
}));
|
|
||||||
// This is needed for safari to not throw TransactionInactiveErrors on the syncTxn. See docs/INDEXEDDB.md
|
|
||||||
await prepareTxn.complete();
|
|
||||||
await Promise.all(updates.map(rs => {
|
|
||||||
return rs.room.afterPrepareSync(rs.preparation, log);
|
|
||||||
})); */
|
|
||||||
}
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
if (this.status.get() === SyncStatus.Stopped) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.status.set(SyncStatus.Stopped);
|
|
||||||
if (this.currentRequest) {
|
|
||||||
this.currentRequest.abort();
|
|
||||||
this.currentRequest = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// storage locking shenanigans here
|
|
||||||
|
|
||||||
openPrepareSyncTxn() {
|
|
||||||
const storeNames = this.storage.storeNames;
|
const storeNames = this.storage.storeNames;
|
||||||
return this.storage.readTxn([
|
return this.storage.readWriteTxn([
|
||||||
|
storeNames.session,
|
||||||
|
storeNames.roomSummary,
|
||||||
|
storeNames.archivedRoomSummary,
|
||||||
|
storeNames.invites,
|
||||||
|
storeNames.roomState,
|
||||||
|
storeNames.roomMembers,
|
||||||
|
storeNames.timelineEvents,
|
||||||
|
storeNames.timelineRelations,
|
||||||
|
storeNames.timelineFragments,
|
||||||
|
storeNames.pendingEvents,
|
||||||
|
storeNames.userIdentities,
|
||||||
|
storeNames.groupSessionDecryptions,
|
||||||
|
storeNames.deviceIdentities,
|
||||||
|
// to discard outbound session when somebody leaves a room
|
||||||
|
// and to create room key messages when somebody joins
|
||||||
|
storeNames.outboundGroupSessions,
|
||||||
|
storeNames.operations,
|
||||||
|
storeNames.accountData,
|
||||||
|
// to decrypt and store new room keys
|
||||||
storeNames.olmSessions,
|
storeNames.olmSessions,
|
||||||
storeNames.inboundGroupSessions,
|
storeNames.inboundGroupSessions,
|
||||||
// to read fragments when loading sync writer when rejoining archived room
|
|
||||||
storeNames.timelineFragments,
|
|
||||||
// to read fragments when loading sync writer when rejoining archived room
|
|
||||||
// to read events that can now be decrypted
|
|
||||||
storeNames.timelineEvents,
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const sleep = (ms) => {
|
const sleep = (ms: number) => {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -430,7 +430,7 @@ const sleep = (ms) => {
|
||||||
// a b c d e f
|
// a b c d e f
|
||||||
// a b c d _ f
|
// a b c d _ f
|
||||||
// e a b c d f <--- c=3 is wrong as we are not tracking it, ergo we need to see if `i` is in range else drop it
|
// e a b c d f <--- c=3 is wrong as we are not tracking it, ergo we need to see if `i` is in range else drop it
|
||||||
const indexInRange = (ranges, i) => {
|
const indexInRange = (ranges: number[][], i: number) => {
|
||||||
let isInRange = false;
|
let isInRange = false;
|
||||||
ranges.forEach((r) => {
|
ranges.forEach((r) => {
|
||||||
if (r[0] <= i && i <= r[1]) {
|
if (r[0] <= i && i <= r[1]) {
|
||||||
|
|
|
@ -54,6 +54,7 @@ export class Room extends BaseRoom {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// {}, string, bool?, [], txn, log
|
||||||
async prepareSync(roomResponse, membership, invite, newKeys, txn, log) {
|
async prepareSync(roomResponse, membership, invite, newKeys, txn, log) {
|
||||||
log.set("id", this.id);
|
log.set("id", this.id);
|
||||||
if (newKeys) {
|
if (newKeys) {
|
||||||
|
|
|
@ -15,17 +15,58 @@
|
||||||
<input id="tokensubmit" type="button" value="Start" />
|
<input id="tokensubmit" type="button" value="Start" />
|
||||||
<div id="session-status" class="hydrogen" style="height: 500px;"></div>
|
<div id="session-status" class="hydrogen" style="height: 500px;"></div>
|
||||||
<script id="main" type="module">
|
<script id="main" type="module">
|
||||||
|
// core hydrogen imports
|
||||||
|
import {Platform} from "./platform/web/Platform";
|
||||||
|
import {createNavigation, createRouter} from "./domain/navigation/index.js";
|
||||||
|
import {StorageFactory} from "./matrix/storage/idb/StorageFactory";
|
||||||
|
import {Session} from "./matrix/Session.js";
|
||||||
|
import {ObservableMap} from "./observable/index.js";
|
||||||
|
|
||||||
|
// left panel specific
|
||||||
import {LeftPanelView} from "./platform/web/ui/session/leftpanel/LeftPanelView.js";
|
import {LeftPanelView} from "./platform/web/ui/session/leftpanel/LeftPanelView.js";
|
||||||
import {LeftPanelViewModel} from "./domain/session/leftpanel/LeftPanelViewModel";
|
import {LeftPanelViewModel} from "./domain/session/leftpanel/LeftPanelViewModel";
|
||||||
import {Navigation} from "./domain/navigation/Navigation.js";
|
|
||||||
import {URLRouter} from "./domain/navigation/URLRouter.js";
|
// matrix specific bits
|
||||||
import {parseUrlPath, stringifyPath} from "./domain/navigation/index.js";
|
|
||||||
import {ObservableMap} from "./observable/index.js";
|
|
||||||
import {History} from "./platform/web/dom/History.js";
|
|
||||||
import {HomeServerApi} from "./matrix/net/HomeServerApi.js";
|
import {HomeServerApi} from "./matrix/net/HomeServerApi.js";
|
||||||
import {Sync3} from "./matrix/Sync3";
|
import {Sync3} from "./matrix/Sync3";
|
||||||
import {xhrRequest} from "./platform/web/dom/request/xhr.js";
|
|
||||||
const navigation = new Navigation(() => false);
|
const sleep = (ms) => {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// dependency inject everything...
|
||||||
|
const platform = new Platform(document.body, {
|
||||||
|
// worker: "src/worker.js",
|
||||||
|
downloadSandbox: "assets/download-sandbox.html",
|
||||||
|
defaultHomeServer: "localhost",
|
||||||
|
// NOTE: uncomment this if you want the service worker for local development
|
||||||
|
// serviceWorker: "sw.js",
|
||||||
|
// NOTE: provide push config if you want push notifs for local development
|
||||||
|
// see assets/config.json for what the config looks like
|
||||||
|
// push: {...},
|
||||||
|
olm: {
|
||||||
|
wasm: "lib/olm/olm.wasm",
|
||||||
|
legacyBundle: "lib/olm/olm_legacy.js",
|
||||||
|
wasmBundle: "lib/olm/olm.js",
|
||||||
|
}
|
||||||
|
}, null, {development: true});
|
||||||
|
const navigation = createNavigation();
|
||||||
|
platform.setNavigation(navigation);
|
||||||
|
const urlRouter = createRouter({navigation, history: platform.history});
|
||||||
|
urlRouter.attach();
|
||||||
|
const hydrogenSessionID = "demo";
|
||||||
|
const factory = new StorageFactory(null); // TODO: this needs to be a fake idb
|
||||||
|
let storage;
|
||||||
|
const loadStorage = async () => {
|
||||||
|
await platform.logger.run("login", async log => {
|
||||||
|
storage = await factory.create(hydrogenSessionID, log);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
await loadStorage();
|
||||||
|
|
||||||
|
|
||||||
|
// make some placeholder rooms
|
||||||
const rooms = new ObservableMap();
|
const rooms = new ObservableMap();
|
||||||
for (let i = 0; i < 1000; i++) {
|
for (let i = 0; i < 1000; i++) {
|
||||||
let r = {
|
let r = {
|
||||||
|
@ -35,21 +76,15 @@
|
||||||
};
|
};
|
||||||
rooms.add(r.id, r);
|
rooms.add(r.id, r);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// make a left panel
|
||||||
const leftPanel = new LeftPanelViewModel({
|
const leftPanel = new LeftPanelViewModel({
|
||||||
invites: new ObservableMap(),
|
invites: new ObservableMap(),
|
||||||
rooms: rooms,
|
rooms: rooms,
|
||||||
navigation: navigation,
|
navigation: navigation,
|
||||||
urlCreator: new URLRouter({
|
urlCreator: urlRouter,
|
||||||
navigation: navigation,
|
|
||||||
history: new History(),
|
|
||||||
stringifyPath: stringifyPath,
|
|
||||||
parseUrlPath: parseUrlPath,
|
|
||||||
}),
|
|
||||||
platform: null,
|
platform: null,
|
||||||
});
|
});
|
||||||
const sleep = (ms) => {
|
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
||||||
};
|
|
||||||
leftPanel.loadRoomRange = async (range) => {
|
leftPanel.loadRoomRange = async (range) => {
|
||||||
// pretend to load something
|
// pretend to load something
|
||||||
await sleep(200);
|
await sleep(200);
|
||||||
|
@ -71,15 +106,26 @@
|
||||||
const view = new LeftPanelView(leftPanel);
|
const view = new LeftPanelView(leftPanel);
|
||||||
document.getElementById("session-status").appendChild(view.mount());
|
document.getElementById("session-status").appendChild(view.mount());
|
||||||
|
|
||||||
|
// kick off a sync v3 loop when an access token is provided
|
||||||
document.getElementById("tokensubmit").addEventListener("click", () => {
|
document.getElementById("tokensubmit").addEventListener("click", () => {
|
||||||
const accessToken = document.getElementById("tokeninput").value;
|
const accessToken = document.getElementById("tokeninput").value;
|
||||||
const sessionId = new Date().getTime() + "";
|
const sessionId = new Date().getTime() + "";
|
||||||
const hs = new HomeServerApi({
|
const hs = new HomeServerApi({
|
||||||
homeserver: "http://localhost:8008",
|
homeserver: "http://localhost:8008",
|
||||||
accessToken: accessToken,
|
accessToken: accessToken,
|
||||||
request: xhrRequest,
|
request: platform.request,
|
||||||
});
|
});
|
||||||
const syncer = new Sync3(hs, null, null, null);
|
const session = new Session({
|
||||||
|
storage: storage,
|
||||||
|
hsApi: hs,
|
||||||
|
sessionInfo: {
|
||||||
|
id: hydrogenSessionID,
|
||||||
|
deviceId: null,
|
||||||
|
userId: null,
|
||||||
|
homeserver: "http://localhost:8008",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const syncer = new Sync3(hs, session, storage, platform.logger);
|
||||||
syncer.start();
|
syncer.start();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Reference in a new issue