2020-08-05 22:08:55 +05:30
|
|
|
/*
|
|
|
|
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.
|
|
|
|
*/
|
|
|
|
|
2020-04-21 01:05:53 +05:30
|
|
|
import {EventEmitter} from "../../utils/EventEmitter.js";
|
2020-04-21 01:11:10 +05:30
|
|
|
import {RoomSummary} from "./RoomSummary.js";
|
2020-04-21 00:56:39 +05:30
|
|
|
import {SyncWriter} from "./timeline/persistence/SyncWriter.js";
|
|
|
|
import {GapWriter} from "./timeline/persistence/GapWriter.js";
|
|
|
|
import {Timeline} from "./timeline/Timeline.js";
|
|
|
|
import {FragmentIdComparer} from "./timeline/FragmentIdComparer.js";
|
|
|
|
import {SendQueue} from "./sending/SendQueue.js";
|
2020-08-17 14:18:00 +05:30
|
|
|
import {WrappedError} from "../error.js"
|
2018-12-21 19:05:24 +05:30
|
|
|
|
2020-04-21 00:56:39 +05:30
|
|
|
export class Room extends EventEmitter {
|
2019-07-29 13:53:15 +05:30
|
|
|
constructor({roomId, storage, hsApi, emitCollectionChange, sendScheduler, pendingEvents, user}) {
|
2019-02-21 04:18:16 +05:30
|
|
|
super();
|
2019-03-09 00:33:18 +05:30
|
|
|
this._roomId = roomId;
|
|
|
|
this._storage = storage;
|
|
|
|
this._hsApi = hsApi;
|
2019-02-11 01:55:29 +05:30
|
|
|
this._summary = new RoomSummary(roomId);
|
2019-05-12 23:54:06 +05:30
|
|
|
this._fragmentIdComparer = new FragmentIdComparer([]);
|
2020-01-05 00:34:57 +05:30
|
|
|
this._syncWriter = new SyncWriter({roomId, fragmentIdComparer: this._fragmentIdComparer});
|
2019-02-21 04:18:16 +05:30
|
|
|
this._emitCollectionChange = emitCollectionChange;
|
2019-07-27 02:03:33 +05:30
|
|
|
this._sendQueue = new SendQueue({roomId, storage, sendScheduler, pendingEvents});
|
2019-02-28 03:20:08 +05:30
|
|
|
this._timeline = null;
|
2019-07-29 13:53:15 +05:30
|
|
|
this._user = user;
|
2020-08-19 19:42:49 +05:30
|
|
|
this._changedMembersDuringSync = null;
|
2018-12-21 19:05:24 +05:30
|
|
|
}
|
|
|
|
|
2020-03-15 01:16:49 +05:30
|
|
|
async writeSync(roomResponse, membership, txn) {
|
|
|
|
const summaryChanges = this._summary.writeSync(roomResponse, membership, txn);
|
2020-08-19 19:42:49 +05:30
|
|
|
const {entries, newLiveKey, changedMembers} = await this._syncWriter.writeSync(roomResponse, txn);
|
2019-07-27 02:03:33 +05:30
|
|
|
let removedPendingEvents;
|
|
|
|
if (roomResponse.timeline && roomResponse.timeline.events) {
|
|
|
|
removedPendingEvents = this._sendQueue.removeRemoteEchos(roomResponse.timeline.events, txn);
|
|
|
|
}
|
2020-08-19 19:42:49 +05:30
|
|
|
return {summaryChanges, newTimelineEntries: entries, newLiveKey, removedPendingEvents, changedMembers};
|
2019-02-27 23:57:45 +05:30
|
|
|
}
|
|
|
|
|
2020-08-19 19:42:49 +05:30
|
|
|
afterSync({summaryChanges, newTimelineEntries, newLiveKey, removedPendingEvents, changedMembers}) {
|
2020-03-15 01:19:15 +05:30
|
|
|
this._syncWriter.afterSync(newLiveKey);
|
2020-08-19 19:42:49 +05:30
|
|
|
if (changedMembers.length) {
|
|
|
|
if (this._changedMembersDuringSync) {
|
|
|
|
for (const member of changedMembers) {
|
|
|
|
this._changedMembersDuringSync.set(member.userId, member);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (this._memberList) {
|
|
|
|
this._memberList.afterSync(changedMembers);
|
|
|
|
}
|
|
|
|
}
|
2020-03-15 01:16:49 +05:30
|
|
|
if (summaryChanges) {
|
2020-06-27 02:56:24 +05:30
|
|
|
this._summary.applyChanges(summaryChanges);
|
2019-02-21 04:18:16 +05:30
|
|
|
this.emit("change");
|
2019-02-28 03:52:47 +05:30
|
|
|
this._emitCollectionChange(this);
|
2019-02-21 04:18:16 +05:30
|
|
|
}
|
2019-02-28 03:20:08 +05:30
|
|
|
if (this._timeline) {
|
|
|
|
this._timeline.appendLiveEntries(newTimelineEntries);
|
|
|
|
}
|
2019-07-27 02:03:33 +05:30
|
|
|
if (removedPendingEvents) {
|
|
|
|
this._sendQueue.emitRemovals(removedPendingEvents);
|
|
|
|
}
|
2018-12-21 19:05:24 +05:30
|
|
|
}
|
|
|
|
|
2019-07-27 02:10:39 +05:30
|
|
|
resumeSending() {
|
|
|
|
this._sendQueue.resumeSending();
|
|
|
|
}
|
|
|
|
|
2019-02-11 01:55:29 +05:30
|
|
|
load(summary, txn) {
|
2020-08-17 14:18:00 +05:30
|
|
|
try {
|
|
|
|
this._summary.load(summary);
|
|
|
|
return this._syncWriter.load(txn);
|
|
|
|
} catch (err) {
|
|
|
|
throw new WrappedError(`Could not load room ${this._roomId}`, err);
|
|
|
|
}
|
2018-12-21 19:05:24 +05:30
|
|
|
}
|
2019-02-27 03:15:58 +05:30
|
|
|
|
2019-07-27 02:03:33 +05:30
|
|
|
sendEvent(eventType, content) {
|
2020-03-31 01:03:04 +05:30
|
|
|
return this._sendQueue.enqueueEvent(eventType, content);
|
2019-07-27 02:03:33 +05:30
|
|
|
}
|
|
|
|
|
2020-06-27 02:56:24 +05:30
|
|
|
async loadMemberList() {
|
|
|
|
let members;
|
|
|
|
if (!this._summary.hasFetchedMembers) {
|
|
|
|
// we need to get the syncToken here!
|
|
|
|
const memberResponse = await this._hsApi.members(this._roomId, syncToken).response;
|
|
|
|
|
|
|
|
const txn = await this._storage.readWriteTxn([
|
|
|
|
this._storage.storeNames.roomSummary,
|
|
|
|
this._storage.storeNames.roomMembers,
|
|
|
|
]);
|
|
|
|
const summaryChanges = this._summary.writeHasFetchedMembers(true, txn);
|
|
|
|
const {roomMembers} = txn;
|
|
|
|
const memberEvents = memberResponse.chunk;
|
|
|
|
if (!Array.isArray(memberEvents)) {
|
|
|
|
throw new Error("malformed");
|
|
|
|
}
|
|
|
|
members = await Promise.all(memberEvents.map(async memberEvent => {
|
|
|
|
const userId = memberEvent && memberEvent.state_key;
|
|
|
|
if (!userId) {
|
|
|
|
throw new Error("malformed");
|
|
|
|
}
|
|
|
|
const memberData = await roomMembers.get(this._roomId, userId);
|
|
|
|
const member = updateOrCreateMember(this._roomId, memberData, event);
|
|
|
|
if (member) {
|
|
|
|
roomMembers.set(member.serialize());
|
|
|
|
}
|
|
|
|
return member;
|
|
|
|
}));
|
|
|
|
await txn.complete();
|
|
|
|
this._summary.applyChanges(summaryChanges);
|
|
|
|
}
|
|
|
|
return new MemberList(this._roomId, members, this._storage);
|
|
|
|
}
|
|
|
|
|
2020-03-22 04:10:40 +05:30
|
|
|
|
|
|
|
/** @public */
|
|
|
|
async fillGap(fragmentEntry, amount) {
|
2020-08-17 21:11:10 +05:30
|
|
|
if (fragmentEntry.edgeReached) {
|
|
|
|
return;
|
|
|
|
}
|
2020-03-22 04:10:40 +05:30
|
|
|
const response = await this._hsApi.messages(this._roomId, {
|
|
|
|
from: fragmentEntry.token,
|
|
|
|
dir: fragmentEntry.direction.asApiString(),
|
|
|
|
limit: amount,
|
|
|
|
filter: {lazy_load_members: true}
|
|
|
|
}).response();
|
2020-03-22 04:37:37 +05:30
|
|
|
|
|
|
|
const txn = await this._storage.readWriteTxn([
|
|
|
|
this._storage.storeNames.pendingEvents,
|
|
|
|
this._storage.storeNames.timelineEvents,
|
|
|
|
this._storage.storeNames.timelineFragments,
|
|
|
|
]);
|
|
|
|
let removedPendingEvents;
|
2020-03-31 00:16:52 +05:30
|
|
|
let gapResult;
|
2020-03-22 04:37:37 +05:30
|
|
|
try {
|
|
|
|
// detect remote echos of pending messages in the gap
|
|
|
|
removedPendingEvents = this._sendQueue.removeRemoteEchos(response.chunk, txn);
|
|
|
|
// write new events into gap
|
|
|
|
const gapWriter = new GapWriter({
|
|
|
|
roomId: this._roomId,
|
|
|
|
storage: this._storage,
|
|
|
|
fragmentIdComparer: this._fragmentIdComparer
|
|
|
|
});
|
2020-03-31 00:16:52 +05:30
|
|
|
gapResult = await gapWriter.writeFragmentFill(fragmentEntry, response, txn);
|
2020-03-22 04:37:37 +05:30
|
|
|
} catch (err) {
|
|
|
|
txn.abort();
|
|
|
|
throw err;
|
|
|
|
}
|
|
|
|
await txn.complete();
|
2020-03-31 00:16:52 +05:30
|
|
|
// once txn is committed, update in-memory state & emit events
|
|
|
|
for (const fragment of gapResult.fragments) {
|
|
|
|
this._fragmentIdComparer.add(fragment);
|
|
|
|
}
|
2020-03-22 04:37:37 +05:30
|
|
|
if (removedPendingEvents) {
|
|
|
|
this._sendQueue.emitRemovals(removedPendingEvents);
|
|
|
|
}
|
2020-03-22 04:10:40 +05:30
|
|
|
if (this._timeline) {
|
2020-03-31 00:16:52 +05:30
|
|
|
this._timeline.addGapEntries(gapResult.entries);
|
2020-03-22 04:10:40 +05:30
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-02-27 03:15:58 +05:30
|
|
|
get name() {
|
|
|
|
return this._summary.name;
|
|
|
|
}
|
2019-02-27 03:57:06 +05:30
|
|
|
|
|
|
|
get id() {
|
|
|
|
return this._roomId;
|
|
|
|
}
|
2019-02-28 03:20:08 +05:30
|
|
|
|
|
|
|
async openTimeline() {
|
|
|
|
if (this._timeline) {
|
|
|
|
throw new Error("not dealing with load race here for now");
|
|
|
|
}
|
2020-05-07 22:44:30 +05:30
|
|
|
console.log(`opening the timeline for ${this._roomId}`);
|
2019-02-28 03:20:08 +05:30
|
|
|
this._timeline = new Timeline({
|
|
|
|
roomId: this.id,
|
|
|
|
storage: this._storage,
|
2019-05-12 23:56:03 +05:30
|
|
|
fragmentIdComparer: this._fragmentIdComparer,
|
2019-07-27 02:03:33 +05:30
|
|
|
pendingEvents: this._sendQueue.pendingEvents,
|
2020-05-07 22:44:30 +05:30
|
|
|
closeCallback: () => {
|
|
|
|
console.log(`closing the timeline for ${this._roomId}`);
|
|
|
|
this._timeline = null;
|
|
|
|
},
|
2019-07-29 13:53:15 +05:30
|
|
|
user: this._user,
|
2019-02-28 03:20:08 +05:30
|
|
|
});
|
|
|
|
await this._timeline.load();
|
|
|
|
return this._timeline;
|
|
|
|
}
|
2020-05-09 23:32:08 +05:30
|
|
|
|
|
|
|
mxcUrlThumbnail(url, width, height, method) {
|
|
|
|
return this._hsApi.mxcUrlThumbnail(url, width, height, method);
|
|
|
|
}
|
|
|
|
|
|
|
|
mxcUrl(url) {
|
|
|
|
return this._hsApi.mxcUrl(url);
|
|
|
|
}
|
2019-02-21 04:18:16 +05:30
|
|
|
}
|
2019-02-28 03:20:08 +05:30
|
|
|
|