Skip to content
This repository has been archived by the owner on May 16, 2019. It is now read-only.

Commit

Permalink
Merge branch 'travis/stored-syncs'
Browse files Browse the repository at this point in the history
  • Loading branch information
turt2live committed Mar 8, 2018
2 parents 2995e70 + 6bdd753 commit b25a616
Show file tree
Hide file tree
Showing 12 changed files with 423 additions and 39 deletions.
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"@ng-bootstrap/ng-bootstrap": "^1.0.0",
"@types/jquery": "^3.3.0",
"@types/node": "^9.4.6",
"angular2-indexeddb": "^1.2.2",
"angular2-template-loader": "^0.6.2",
"angular2-toaster": "^4.0.2",
"angular2-ui-switch": "^1.2.0",
Expand Down
7 changes: 7 additions & 0 deletions src/app/models/matrix/dto/room.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export interface RoomUpdatedEvent {
export class MatrixRoom {
public static readonly UPDATED_STREAM: Observable<RoomUpdatedEvent> = new ReplaySubject<RoomUpdatedEvent>();

public backfillToken: string;

constructor(private _roomId: string,
private _isDirect: boolean,
private _state: RoomStateEvent[],
Expand Down Expand Up @@ -167,6 +169,11 @@ export class MatrixRoom {
}
}

public addAllToTimeline(events: RoomEvent[]): void {
for (const event of events) this.timeline.push(event);
this.publishUpdate("timeline");
}

public addPendingEvent(event: IncompleteRoomEvent): void {
this.pendingEvents.push(event);
this.publishUpdate("pendingEvents");
Expand Down
21 changes: 12 additions & 9 deletions src/app/models/matrix/http/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,22 @@ import { PresenceEvent } from "../events/ephemeral/m.presence";
import { RoomStateEvent } from "../events/room/state/room-state-event";
import { RoomEvent } from "../events/room/room-event";
import { EphemeralEvent } from "../events/ephemeral/ephemeral-event";
import { RoomAccountDataEvent } from "../events/account/room_account/room-account-data-event";

export interface RoomTimeline {
limited: boolean;
prev_batch: string;
events: RoomEvent[];
}

export interface RoomAccountData {
events: RoomAccountDataEvent[];
}

export interface RoomEphemeralTimeline {
events: EphemeralEvent[];
}

export interface SyncJoinedRooms {
[roomId: string]: {
unread_notifications: {
Expand All @@ -20,12 +29,8 @@ export interface SyncJoinedRooms {
state: {
events: RoomStateEvent[];
};
ephemeral: {
events: EphemeralEvent[];
};
account_data: {
events: AccountDataEvent[];
};
ephemeral: RoomEphemeralTimeline;
account_data: RoomAccountData;
timeline: RoomTimeline;
};
}
Expand Down Expand Up @@ -61,9 +66,7 @@ export interface SyncResponse {
state: {
events: RoomStateEvent[];
};
account_data: {
events: AccountDataEvent[];
};
account_data: RoomAccountData;
timeline: {
limited: boolean;
prev_batch: string;
Expand Down
30 changes: 30 additions & 0 deletions src/app/models/storage/event-batch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { RoomEvent } from "../matrix/events/room/room-event";

export interface RawEventBatch {
id: number;
roomId: string;
startToken: string;
endToken: string;
events: RoomEvent[];
}

export class PersistedEventBatch {
constructor(public id: number, public roomId: string, public events: RoomEvent[], public startToken: string, public endToken: string) {
}

public toRaw(withId = true): RawEventBatch {
const result = {
id: this.id,
roomId: this.roomId,
startToken: this.startToken,
endToken: this.endToken,
events: this.events,
};
if (!withId) delete result['id'];
return result;
}

public static parseAll(batches: RawEventBatch[]): PersistedEventBatch[] {
return batches.map(b => new PersistedEventBatch(b.id, b.roomId, b.events, b.startToken, b.endToken));
}
}
32 changes: 32 additions & 0 deletions src/app/models/storage/room-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { RoomStateEvent } from "../matrix/events/room/state/room-state-event";
import { MatrixRoom } from "../matrix/dto/room";

export interface RawRoomState {
id: number;
roomId: string;
events: RoomStateEvent[];
}

export class PersistedRoomState {
constructor(public id: number, public roomId: string, public events: RoomStateEvent[]) {
}

public toRaw(withId = true): RawRoomState {
const result = {
id: this.id,
roomId: this.roomId,
events: this.events,
};
if (!withId) delete result['id'];
return result;
}

public static parse(state: RawRoomState): PersistedRoomState {
return new PersistedRoomState(state.id, state.roomId, state.events);
}

public static fromRoom(room: MatrixRoom): PersistedRoomState {
console.log(room.id + " has " + room.state.length + " state events");
return new PersistedRoomState(null, room.id, room.state);
}
}
113 changes: 106 additions & 7 deletions src/app/services/matrix/account.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,139 @@ import { MatrixAuthService } from "./auth.service";
import { AccountDataEvent } from "../../models/matrix/events/account/account-data-event";
import { ReplaySubject } from "rxjs/ReplaySubject";
import { Observable } from "rxjs/Observable";
import { AngularIndexedDB } from "angular2-indexeddb/angular2-indexeddb";
import { RoomAccountDataEvent } from "../../models/matrix/events/account/room_account/room-account-data-event";
import { MatrixRoom } from "../../models/matrix/dto/room";

export interface RoomAccountDataUpdatedEvent {
event: RoomAccountDataEvent;
room: MatrixRoom;
}

@Injectable()
export class MatrixAccountService extends AuthenticatedApi {

private static ACCOUNT_DATA: { [eventType: string]: AccountDataEvent } = {};
private static ACCOUNT_DATA_STREAM = new ReplaySubject<AccountDataEvent>();
private static ROOM_ACCOUNT_DATA: { [roomId: string]: { [eventType: string]: RoomAccountDataEvent } } = {};
private static ROOM_ACCOUNT_DATA_STREAM = new ReplaySubject<RoomAccountDataUpdatedEvent>();

private db: AngularIndexedDB;

constructor(http: HttpClient, auth: MatrixAuthService,
private hs: MatrixHomeserverService) {
super(http, auth);
}

/**
* Initializes the internal database for account data. No-ops if the database has already been set up
* @returns {Promise<any>} Resolves when the database is ready.
*/
private initDb(): Promise<any> {
if (this.db) return Promise.resolve();

const db = new AngularIndexedDB("evelium.account_data", 1);
return db.openDatabase(1, evt => {
const batches = evt.currentTarget.result.createObjectStore("account_data", {
keyPath: "id",
autoIncrement: true,
});
batches.createIndex("roomId", "roomId", {unique: false}); // nullable
batches.createIndex("eventType", "eventType", {unique: false});
batches.createIndex("content", "content", {unique: false});
}).then(() => this.db = db);
}

public getAccountDataStream(): Observable<AccountDataEvent> {
return MatrixAccountService.ACCOUNT_DATA_STREAM;
}

public getAccountData<T extends AccountDataEvent>(eventType: string): T {
return <T>MatrixAccountService.ACCOUNT_DATA[eventType];
public getRoomAccountDataStream(): Observable<RoomAccountDataUpdatedEvent> {
return MatrixAccountService.ROOM_ACCOUNT_DATA_STREAM;
}

public setAccountData(event: AccountDataEvent, cacheOnly = false): Promise<any> {
const oldEvent = this.getAccountData(event.type);
MatrixAccountService.ACCOUNT_DATA[event.type] = event;
/**
* Gets account data from the underlying store. If the event requested does not exist then null will be returned.
* @param {string} eventType The event type to look up
* @returns {Promise<T extends AccountDataEvent|AccountDataEvent>} Resolves to the requested account data, or null if not found
*/
public getAccountData<T extends AccountDataEvent | AccountDataEvent>(eventType: string): Promise<T | AccountDataEvent> {
return this.lookupAccountData(eventType, null);
}

/**
* Gets account data from the underlying store for a given room. If the event requested does not exist then null will be returned.
* @param {string} eventType The event type to look up
* @param {MatrixRoom} room The room to get account data for
* @returns {Promise<T extends RoomAccountDataEvent|RoomAccountDataEvent>} Resolves to the requested account data, or null if not found
*/
public getRoomAccountData<T extends RoomAccountDataEvent | RoomAccountDataEvent>(eventType: string, room: MatrixRoom): Promise<T | RoomAccountDataEvent> {
return this.lookupAccountData(eventType, room.id).then(e => <RoomAccountDataEvent>e);
}

private lookupAccountData(eventType: string, roomId: string): Promise<AccountDataEvent> {
const store = roomId ? MatrixAccountService.ROOM_ACCOUNT_DATA[roomId] : MatrixAccountService.ACCOUNT_DATA;
if (store && store[eventType]) return Promise.resolve(store[eventType]);

return this.initDb()
.then(() => this.db.getByKey("account_data", {eventType: eventType, roomId: roomId}))
.then(record => <AccountDataEvent>{type: record.eventType, content: record.content})
.catch(() => Promise.resolve(null)); // intentionally ignore errors
}

public async setAccountData(event: AccountDataEvent, cacheOnly = false): Promise<any> {
const oldEvent = await this.getAccountData(event.type);
MatrixAccountService.ACCOUNT_DATA[event.type] = event;
MatrixAccountService.ACCOUNT_DATA_STREAM.next(event);
if (cacheOnly) return Promise.resolve(); // Stop here
await this.persistAccountData(event, null);
if (cacheOnly) return; // stop here

return this.put(this.hs.buildCsUrl(`user/${this.auth.userId}/account_data/${event.type}`, event.content)).toPromise().then(() => {
console.log("Successfully saved account data: " + event.type);
}).catch(e => {
console.error(e);
MatrixAccountService.ACCOUNT_DATA[event.type] = oldEvent;
MatrixAccountService.ACCOUNT_DATA_STREAM.next(oldEvent); // Send the reverted event
return Promise.reject(e); // re-throw
return this.persistAccountData(event, null).then(() => Promise.reject(e)); // persist and rethrow
});
}

public async setRoomAccountData(event: RoomAccountDataEvent, room: MatrixRoom, cacheOnly = false): Promise<any> {
if (!MatrixAccountService.ROOM_ACCOUNT_DATA[room.id]) MatrixAccountService.ROOM_ACCOUNT_DATA[room.id] = {};

const oldEvent = await this.getRoomAccountData(event.type, room);
MatrixAccountService.ROOM_ACCOUNT_DATA[room.id][event.type] = event;
MatrixAccountService.ROOM_ACCOUNT_DATA_STREAM.next({event: event, room: room});
await this.persistAccountData(event, room.id);
if (cacheOnly) return; // stop here

return this.put(this.hs.buildCsUrl(`user/${this.auth.userId}/rooms/${room.id}/account_data/${event.type}`, event.content)).toPromise().then(() => {
console.log("Successfully saved room account data: " + event.type + " in " + room.id);
}).catch(e => {
console.error(e);
MatrixAccountService.ROOM_ACCOUNT_DATA[room.id][event.type] = oldEvent;
MatrixAccountService.ROOM_ACCOUNT_DATA_STREAM.next({event: event, room: room}); // Send the reverted event
return this.persistAccountData(event, room.id).then(() => Promise.reject(e)); // persist and rethrow
});
}

private persistAccountData(event: AccountDataEvent, roomId: string): Promise<any> {
return this.initDb()
.then(() => this.db.getByKey("account_data", {eventType: event.type, roomId: roomId}))
.then(record => {
// Exists: update
return this.db.update("account_data", {
eventType: event.type,
roomId: roomId,
content: event.content
}, record.id);
}, () => {
// Doesn't exist: add
return this.db.add("account_data", {
eventType: event.type,
roomId: roomId,
content: event.content
});
});
}
}
2 changes: 1 addition & 1 deletion src/app/services/matrix/room.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export class MatrixRoomService extends AuthenticatedApi {

private static ROOM_CACHE: { [roomId: string]: MatrixRoom } = {};

private dmRoomMap: DmMap = {users: {}, rooms: {}};
private dmRoomMap: DmMap = {users: {}, rooms: {}} = {users: {}, rooms: {}};

constructor(http: HttpClient, auth: MatrixAuthService,
private account: MatrixAccountService) {
Expand Down
Loading

0 comments on commit b25a616

Please sign in to comment.