as we'll need to await a bogus request first thing after opening the txn funny enough, we originally made it sync to accommodate the same bug in safari, but that didn't prevent any microtask being awaited before scheduling a request in the calling code closing the txn. We'll await a bogus request within the transaction class now so it doesn't depend on the calling code
147 lines
5.7 KiB
JavaScript
147 lines
5.7 KiB
JavaScript
/*
|
|
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.
|
|
*/
|
|
|
|
import {directionalConcat, directionalAppend} from "./common.js";
|
|
import {Direction} from "../Direction.js";
|
|
import {EventEntry} from "../entries/EventEntry.js";
|
|
import {FragmentBoundaryEntry} from "../entries/FragmentBoundaryEntry.js";
|
|
|
|
class ReaderRequest {
|
|
constructor(fn) {
|
|
this.decryptRequest = null;
|
|
this._promise = fn(this);
|
|
}
|
|
|
|
complete() {
|
|
return this._promise;
|
|
}
|
|
|
|
dispose() {
|
|
if (this.decryptRequest) {
|
|
this.decryptRequest.dispose();
|
|
this.decryptRequest = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Raw because it doesn't do decryption and in the future it should not read relations either.
|
|
* It is just about reading entries and following fragment links
|
|
*/
|
|
async function readRawTimelineEntriesWithTxn(roomId, eventKey, direction, amount, fragmentIdComparer, txn) {
|
|
let entries = [];
|
|
const timelineStore = txn.timelineEvents;
|
|
const fragmentStore = txn.timelineFragments;
|
|
|
|
while (entries.length < amount && eventKey) {
|
|
let eventsWithinFragment;
|
|
if (direction.isForward) {
|
|
// TODO: should we pass amount - entries.length here?
|
|
eventsWithinFragment = await timelineStore.eventsAfter(roomId, eventKey, amount);
|
|
} else {
|
|
eventsWithinFragment = await timelineStore.eventsBefore(roomId, eventKey, amount);
|
|
}
|
|
let eventEntries = eventsWithinFragment.map(e => new EventEntry(e, fragmentIdComparer));
|
|
entries = directionalConcat(entries, eventEntries, direction);
|
|
// prepend or append eventsWithinFragment to entries, and wrap them in EventEntry
|
|
|
|
if (entries.length < amount) {
|
|
const fragment = await fragmentStore.get(roomId, eventKey.fragmentId);
|
|
// TODO: why does the first fragment not need to be added? (the next *is* added below)
|
|
// it looks like this would be fine when loading in the sync island
|
|
// (as the live fragment should be added already) but not for permalinks when we support them
|
|
//
|
|
// fragmentIdComparer.addFragment(fragment);
|
|
let fragmentEntry = new FragmentBoundaryEntry(fragment, direction.isBackward, fragmentIdComparer);
|
|
// append or prepend fragmentEntry, reuse func from GapWriter?
|
|
directionalAppend(entries, fragmentEntry, direction);
|
|
// only continue loading if the fragment boundary can't be backfilled
|
|
if (!fragmentEntry.token && fragmentEntry.hasLinkedFragment) {
|
|
const nextFragment = await fragmentStore.get(roomId, fragmentEntry.linkedFragmentId);
|
|
fragmentIdComparer.add(nextFragment);
|
|
const nextFragmentEntry = new FragmentBoundaryEntry(nextFragment, direction.isForward, fragmentIdComparer);
|
|
directionalAppend(entries, nextFragmentEntry, direction);
|
|
eventKey = nextFragmentEntry.asEventKey();
|
|
} else {
|
|
eventKey = null;
|
|
}
|
|
}
|
|
}
|
|
return entries;
|
|
}
|
|
|
|
export class TimelineReader {
|
|
constructor({roomId, storage, fragmentIdComparer}) {
|
|
this._roomId = roomId;
|
|
this._storage = storage;
|
|
this._fragmentIdComparer = fragmentIdComparer;
|
|
this._decryptEntries = null;
|
|
}
|
|
|
|
enableEncryption(decryptEntries) {
|
|
this._decryptEntries = decryptEntries;
|
|
}
|
|
|
|
get readTxnStores() {
|
|
const stores = [
|
|
this._storage.storeNames.timelineEvents,
|
|
this._storage.storeNames.timelineFragments,
|
|
];
|
|
if (this._decryptEntries) {
|
|
stores.push(this._storage.storeNames.inboundGroupSessions);
|
|
}
|
|
return stores;
|
|
}
|
|
|
|
readFrom(eventKey, direction, amount) {
|
|
return new ReaderRequest(async r => {
|
|
const txn = await this._storage.readTxn(this.readTxnStores);
|
|
return await this._readFrom(eventKey, direction, amount, r, txn);
|
|
});
|
|
}
|
|
|
|
readFromEnd(amount, existingTxn = null) {
|
|
return new ReaderRequest(async r => {
|
|
const txn = existingTxn || await this._storage.readTxn(this.readTxnStores);
|
|
const liveFragment = await txn.timelineFragments.liveFragment(this._roomId);
|
|
let entries;
|
|
// room hasn't been synced yet
|
|
if (!liveFragment) {
|
|
entries = [];
|
|
} else {
|
|
this._fragmentIdComparer.add(liveFragment);
|
|
const liveFragmentEntry = FragmentBoundaryEntry.end(liveFragment, this._fragmentIdComparer);
|
|
const eventKey = liveFragmentEntry.asEventKey();
|
|
entries = await this._readFrom(eventKey, Direction.Backward, amount, r, txn);
|
|
entries.unshift(liveFragmentEntry);
|
|
}
|
|
return entries;
|
|
});
|
|
}
|
|
|
|
async _readFrom(eventKey, direction, amount, r, txn) {
|
|
const entries = await readRawTimelineEntriesWithTxn(this._roomId, eventKey, direction, amount, this._fragmentIdComparer, txn);
|
|
if (this._decryptEntries) {
|
|
r.decryptRequest = this._decryptEntries(entries, txn);
|
|
try {
|
|
await r.decryptRequest.complete();
|
|
} finally {
|
|
r.decryptRequest = null;
|
|
}
|
|
}
|
|
return entries;
|
|
}
|
|
}
|