hydrogen-web/src/mocks/TimelineMock.ts

264 lines
11 KiB
TypeScript

import {createEvent, withTextBody, withSender} from "./event.js";
import {TimelineEvent} from "../matrix/storage/types";
export const TIMELINE_START_TOKEN = "timeline_start";
export function eventId(i: number): string {
return `$event${i}`;
}
/** `from` is included, `to` is excluded */
export function eventIds(from: number, to: number): string[] {
return [...Array(to-from).keys()].map(i => eventId(i + from));
}
export class TimelineMock {
private _counter: number;
private _dagOrder: TimelineEvent[];
private _syncOrder: TimelineEvent[];
private _defaultSender: string;
constructor(defaultSender: string) {
this._counter = 0;
this._dagOrder = [];
this._syncOrder = [];
this._defaultSender = defaultSender;
}
_defaultEvent(id: string): TimelineEvent {
return withTextBody(`This is event ${id}`, withSender(this._defaultSender, createEvent("m.room.message", id)));
}
_createEvent(func?: (eventId: string) => TimelineEvent): TimelineEvent {
const id = eventId(this._counter++);
return func ? func(id) : this._defaultEvent(id);
}
_createEvents(n: number, func?: (eventId: string) => TimelineEvent) {
const events: TimelineEvent[] = [];
for (let i = 0; i < n; i++) {
events.push(this._createEvent(func));
}
return events;
}
insertAfter(token: string, n: number, func?: (eventId: string) => TimelineEvent) {
const events = this._createEvents(n, func);
const index = this._findIndex(token, "f", this._dagOrder);
this._dagOrder.splice(index, 0, ...events);
this._syncOrder.push(...events);
return events[events.length - 1]?.event_id;
}
append(n: number, func?: (eventId: string) => TimelineEvent) {
const events = this._createEvents(n, func);
this._dagOrder.push(...events);
this._syncOrder.push(...events);
return events[events.length - 1]?.event_id;
}
_getStep(direction: "f" | "b") : 1 | -1 {
return direction === "f" ? 1 : -1;
}
_findIndex(token: string, direction: "f" | "b", eventOrdering: TimelineEvent[]): number {
const step = this._getStep(direction);
if (token === TIMELINE_START_TOKEN) {
const firstSyncEvent = this._syncOrder[0];
if (!firstSyncEvent) {
// We have no events at all. Wherever you start looking,
// you'll stop looking right away. Zero works as well as anything else.
return 0;
}
const orderIndex = eventOrdering.findIndex(e => e.event_id === firstSyncEvent.event_id);
return orderIndex;
}
// All other tokens are (non-inclusive) event indices
const index = eventOrdering.findIndex(e => e.event_id === token);
if (index === -1) {
// We didn't find this event token at all. What are we
// even looking at?
throw new Error("Invalid token passed to TimelineMock");
}
return index + step;
}
messages(begin: string, end: string | undefined, direction: "f" | "b", limit: number = 10) {
const step = this._getStep(direction);
let index = this._findIndex(begin, direction, this._dagOrder);
const chunk: TimelineEvent[] = [];
for (; limit > 0 && index >= 0 && index < this._dagOrder.length; index += step, limit--) {
if (this._dagOrder[index].event_id === end) {
break;
}
chunk.push(this._dagOrder[index]);
}
return {
start: begin,
end: chunk[chunk.length - 1]?.event_id || begin,
chunk,
state: []
};
}
context(eventId: string, limit: number = 10) {
if (limit <= 0) {
throw new Error("Cannot fetch zero or less events!");
}
let eventIndex = this._dagOrder.findIndex(e => e.event_id === eventId);
if (eventIndex === -1) {
throw new Error("Fetching context for unknown event");
}
const event = this._dagOrder[eventIndex];
let offset = 1;
const eventsBefore: TimelineEvent[] = [];
const eventsAfter: TimelineEvent[] = [];
while (limit !== 0 && (eventIndex - offset >= 0 || eventIndex + offset < this._dagOrder.length)) {
if (eventIndex - offset >= 0) {
eventsBefore.push(this._dagOrder[eventIndex - offset]);
limit--;
}
if (limit !== 0 && eventIndex + offset < this._dagOrder.length) {
eventsAfter.push(this._dagOrder[eventIndex + offset]);
limit--;
}
offset++;
}
return {
start: eventsBefore[eventsBefore.length - 1]?.event_id || eventId,
end: eventsAfter[eventsAfter.length - 1]?.event_id || eventId,
event,
events_before: eventsBefore,
events_after: eventsAfter,
state: []
};
}
sync(since?: string, limit: number = 10) {
const startAt = since ? this._findIndex(since, "f", this._syncOrder) : 0;
const index = Math.max(this._syncOrder.length - limit, startAt);
const limited = this._syncOrder.length - startAt > limit;
const events: TimelineEvent[] = [];
for(let i = index; i < this._syncOrder.length; i++) {
events.push(this._syncOrder[i]);
}
return {
next_batch: events[events.length - 1]?.event_id || since || TIMELINE_START_TOKEN,
timeline: {
prev_batch: events[0]?.event_id || since || TIMELINE_START_TOKEN,
events,
limited
}
}
}
}
export function tests() {
const SENDER = "@alice:hs.tdl";
return {
"Append events are returned via sync": assert => {
const timeline = new TimelineMock(SENDER);
timeline.append(10);
const syncResponse = timeline.sync();
const events = syncResponse.timeline.events.map(e => e.event_id);
assert.deepEqual(events, eventIds(0, 10));
assert.equal(syncResponse.timeline.limited, false);
},
"Limiting a sync properly limits the returned events": assert => {
const timeline = new TimelineMock(SENDER);
timeline.append(20);
const syncResponse = timeline.sync(undefined, 10);
const events = syncResponse.timeline.events.map(e => e.event_id);
assert.deepEqual(events, eventIds(10, 20));
assert.equal(syncResponse.timeline.limited, true);
},
"The context endpoint returns messages in DAG order around an event": assert => {
const timeline = new TimelineMock(SENDER);
timeline.append(30);
const context = timeline.context(eventId(15));
assert.equal(context.event.event_id, eventId(15));
assert.deepEqual(context.events_before.map(e => e.event_id).reverse(), eventIds(10, 15));
assert.deepEqual(context.events_after.map(e => e.event_id), eventIds(16, 21));
},
"The context endpoint returns the proper number of messages": assert => {
const timeline = new TimelineMock(SENDER);
timeline.append(30);
for (const i of new Array(29).keys()) {
const middleFetch = timeline.context(eventId(15), i + 1);
assert.equal(middleFetch.events_before.length + middleFetch.events_after.length, i + 1);
const startFetch = timeline.context(eventId(1), i + 1);
assert.equal(startFetch.events_before.length + startFetch.events_after.length, i + 1);
const endFetch = timeline.context(eventId(28), i + 1);
assert.equal(endFetch.events_before.length + endFetch.events_after.length, i + 1);
}
},
"The previous batch from a sync returns the previous events": assert => {
const timeline = new TimelineMock(SENDER);
timeline.append(20);
const sync = timeline.sync(undefined, 10);
const messages = timeline.messages(sync.timeline.prev_batch, undefined, "b");
const events = messages.chunk.map(e => e.event_id).reverse();
assert.deepEqual(events, eventIds(0, 10));
},
"Two consecutive message fetches are continuous if no new events are inserted": assert => {
const timeline = new TimelineMock(SENDER);
timeline.append(30);
const sync = timeline.sync(undefined, 10);
const messages1 = timeline.messages(sync.timeline.prev_batch, undefined, "b");
const events1 = messages1.chunk.map(e => e.event_id).reverse();
assert.deepEqual(events1, eventIds(10, 20));
const messages2 = timeline.messages(messages1.end, undefined, "b");
const events2 = messages2.chunk.map(e => e.event_id).reverse();
assert.deepEqual(events2, eventIds(0, 10));
},
"Two consecutive message fetches detect newly inserted event": assert => {
const timeline = new TimelineMock(SENDER);
timeline.append(30);
const messages1 = timeline.messages(eventId(20), undefined, "b", 10);
const events1 = messages1.chunk.map(e => e.event_id).reverse();
assert.deepEqual(events1, eventIds(10, 20));
timeline.insertAfter(eventId(9), 1);
const messages2 = timeline.messages(eventId(10), undefined, "b", 10);
const events2 = messages2.chunk.map(e => e.event_id).reverse();
const expectedEvents2 = eventIds(1, 10);
expectedEvents2.push(eventId(30));
assert.deepEqual(events2, expectedEvents2);
},
"A sync that receives no events has the same next batch as it started with": assert => {
const timeline = new TimelineMock(SENDER);
timeline.append(10);
const sync1 = timeline.sync();
const sync2 = timeline.sync(sync1.next_batch);
assert.equal(sync1.next_batch, sync2.next_batch);
},
"An event inserted at the staart still shows up in a sync": assert => {
const timeline = new TimelineMock(SENDER);
timeline.append(30);
const sync1 = timeline.sync(undefined, 10);
const sync2 = timeline.sync(sync1.next_batch, 10)
assert.deepEqual(sync2.timeline.events, []);
assert.equal(sync2.timeline.limited, false);
timeline.insertAfter(TIMELINE_START_TOKEN, 1);
const sync3 = timeline.sync(sync2.next_batch, 10)
const events = sync3.timeline.events.map(e => e.event_id);
assert.deepEqual(events, [eventId(30)]);
},
"An event inserted at the start does not show up in a non-overlapping message fetch": assert => {
const timeline = new TimelineMock(SENDER);
timeline.append(30);
const sync1 = timeline.sync(undefined, 10);
const messages1 = timeline.messages(sync1.timeline.prev_batch, undefined, "f", 10);
timeline.insertAfter(TIMELINE_START_TOKEN, 1);
const messages2 = timeline.messages(sync1.timeline.prev_batch, undefined, "f", 10);
assert.deepEqual(messages1.chunk, messages2.chunk);
},
}
}