forked from mystiq/hydrogen-web
Merge branch 'snowpack-ts-storage-3' into snowpack-ts-storage-4
This commit is contained in:
commit
7f8089eaff
32 changed files with 485 additions and 255 deletions
|
@ -37,3 +37,5 @@ There are no npm modules yet published for Hydrogen. The easiest is probably to
|
||||||
For example, for a single room chat, you could create an instance of `Platform`, you create a new `SessionContainer` with it, call `startWithLogin` on it, observe `sessionContainer.loadStatus` to know when initial sync is done, then do `sessionContainer.session.rooms.get('roomid')` and you create a `RoomViewModel` with it and pass that to a `RoomView`. Then you call `document.appendChild(roomView.mount())` and you should see a syncing room.
|
For example, for a single room chat, you could create an instance of `Platform`, you create a new `SessionContainer` with it, call `startWithLogin` on it, observe `sessionContainer.loadStatus` to know when initial sync is done, then do `sessionContainer.session.rooms.get('roomid')` and you create a `RoomViewModel` with it and pass that to a `RoomView`. Then you call `document.appendChild(roomView.mount())` and you should see a syncing room.
|
||||||
|
|
||||||
Feel free to ask for pointers in #hydrogen:matrix.org as the documentation is still lacking considerably. Note that at this early, pre 1.0 stage of the project, there is no promise of API stability yet.
|
Feel free to ask for pointers in #hydrogen:matrix.org as the documentation is still lacking considerably. Note that at this early, pre 1.0 stage of the project, there is no promise of API stability yet.
|
||||||
|
|
||||||
|
Also, to make end-to-end encryption work, you'll likely need some tweaks to your build system, see [this issue](https://github.com/vector-im/hydrogen-web/issues/467).
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "hydrogen-web",
|
"name": "hydrogen-web",
|
||||||
"version": "0.2.7",
|
"version": "0.2.8",
|
||||||
"description": "A javascript matrix client prototype, trying to minize RAM usage by offloading as much as possible to IndexedDB",
|
"description": "A javascript matrix client prototype, trying to minize RAM usage by offloading as much as possible to IndexedDB",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"directories": {
|
"directories": {
|
||||||
|
|
|
@ -204,7 +204,7 @@ export class SessionContainer {
|
||||||
reconnector: this._reconnector,
|
reconnector: this._reconnector,
|
||||||
});
|
});
|
||||||
this._sessionId = sessionInfo.id;
|
this._sessionId = sessionInfo.id;
|
||||||
this._storage = await this._platform.storageFactory.create(sessionInfo.id);
|
this._storage = await this._platform.storageFactory.create(sessionInfo.id, log);
|
||||||
// no need to pass access token to session
|
// no need to pass access token to session
|
||||||
const filteredSessionInfo = {
|
const filteredSessionInfo = {
|
||||||
id: sessionInfo.id,
|
id: sessionInfo.id,
|
||||||
|
|
|
@ -19,6 +19,22 @@ import {verifyEd25519Signature, SIGNATURE_ALGORITHM} from "./common.js";
|
||||||
const TRACKING_STATUS_OUTDATED = 0;
|
const TRACKING_STATUS_OUTDATED = 0;
|
||||||
const TRACKING_STATUS_UPTODATE = 1;
|
const TRACKING_STATUS_UPTODATE = 1;
|
||||||
|
|
||||||
|
export function addRoomToIdentity(identity, userId, roomId) {
|
||||||
|
if (!identity) {
|
||||||
|
identity = {
|
||||||
|
userId: userId,
|
||||||
|
roomIds: [roomId],
|
||||||
|
deviceTrackingStatus: TRACKING_STATUS_OUTDATED,
|
||||||
|
};
|
||||||
|
return identity;
|
||||||
|
} else {
|
||||||
|
if (!identity.roomIds.includes(roomId)) {
|
||||||
|
identity.roomIds.push(roomId);
|
||||||
|
return identity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// map 1 device from /keys/query response to DeviceIdentity
|
// map 1 device from /keys/query response to DeviceIdentity
|
||||||
function deviceKeysAsDeviceIdentity(deviceSection) {
|
function deviceKeysAsDeviceIdentity(deviceSection) {
|
||||||
const deviceId = deviceSection["device_id"];
|
const deviceId = deviceSection["device_id"];
|
||||||
|
@ -107,17 +123,9 @@ export class DeviceTracker {
|
||||||
async _writeMember(member, txn) {
|
async _writeMember(member, txn) {
|
||||||
const {userIdentities} = txn;
|
const {userIdentities} = txn;
|
||||||
const identity = await userIdentities.get(member.userId);
|
const identity = await userIdentities.get(member.userId);
|
||||||
if (!identity) {
|
const updatedIdentity = addRoomToIdentity(identity, member.userId, member.roomId);
|
||||||
userIdentities.set({
|
if (updatedIdentity) {
|
||||||
userId: member.userId,
|
userIdentities.set(updatedIdentity);
|
||||||
roomIds: [member.roomId],
|
|
||||||
deviceTrackingStatus: TRACKING_STATUS_OUTDATED,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
if (!identity.roomIds.includes(member.roomId)) {
|
|
||||||
identity.roomIds.push(member.roomId);
|
|
||||||
userIdentities.set(identity);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,7 @@ const EVENT_ENCRYPTED_TYPE = "m.room.encrypted";
|
||||||
export class Room extends BaseRoom {
|
export class Room extends BaseRoom {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
|
// TODO: pass pendingEvents to start like pendingOperations?
|
||||||
const {pendingEvents} = options;
|
const {pendingEvents} = options;
|
||||||
const relationWriter = new RelationWriter({
|
const relationWriter = new RelationWriter({
|
||||||
roomId: this.id,
|
roomId: this.id,
|
||||||
|
@ -120,7 +121,8 @@ export class Room extends BaseRoom {
|
||||||
txn.roomMembers.removeAllForRoom(this.id);
|
txn.roomMembers.removeAllForRoom(this.id);
|
||||||
}
|
}
|
||||||
const {entries: newEntries, updatedEntries, newLiveKey, memberChanges} =
|
const {entries: newEntries, updatedEntries, newLiveKey, memberChanges} =
|
||||||
await log.wrap("syncWriter", log => this._syncWriter.writeSync(roomResponse, isRejoin, txn, log), log.level.Detail);
|
await log.wrap("syncWriter", log => this._syncWriter.writeSync(
|
||||||
|
roomResponse, isRejoin, summaryChanges.hasFetchedMembers, txn, log), log.level.Detail);
|
||||||
if (decryptChanges) {
|
if (decryptChanges) {
|
||||||
const decryption = await log.wrap("decryptChanges", log => decryptChanges.write(txn, log));
|
const decryption = await log.wrap("decryptChanges", log => decryptChanges.write(txn, log));
|
||||||
log.set("decryptionResults", decryption.results.size);
|
log.set("decryptionResults", decryption.results.size);
|
||||||
|
|
|
@ -141,6 +141,16 @@ export class MemberChange {
|
||||||
return this.previousMembership === "join" && this.membership !== "join";
|
return this.previousMembership === "join" && this.membership !== "join";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The result can be a false negative when all of these apply:
|
||||||
|
* - the complete set of room members hasn't been fetched yet.
|
||||||
|
* - the member event for this change was received in the
|
||||||
|
* state section and wasn't present in the timeline section.
|
||||||
|
* - the room response was limited, e.g. there was a gap.
|
||||||
|
*
|
||||||
|
* This is because during sync, in this case it is not possible
|
||||||
|
* to distinguish between a new member that joined the room
|
||||||
|
* during a gap and a lazy-loading member.
|
||||||
|
* */
|
||||||
get hasJoined() {
|
get hasJoined() {
|
||||||
return this.previousMembership !== "join" && this.membership === "join";
|
return this.previousMembership !== "join" && this.membership === "join";
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,57 +23,27 @@ export class MemberWriter {
|
||||||
this._cache = new LRUCache(5, member => member.userId);
|
this._cache = new LRUCache(5, member => member.userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
writeTimelineMemberEvent(event, txn) {
|
prepareMemberSync(stateEvents, timelineEvents, hasFetchedMembers) {
|
||||||
return this._writeMemberEvent(event, false, txn);
|
return new MemberSync(this, stateEvents, timelineEvents, hasFetchedMembers);
|
||||||
}
|
}
|
||||||
|
|
||||||
writeStateMemberEvent(event, isLimited, txn) {
|
async _writeMember(member, txn) {
|
||||||
// member events in the state section when the room response
|
let existingMember = this._cache.get(member.userId);
|
||||||
// is not limited must always be lazy loaded members.
|
|
||||||
// If they are not, they will be repeated in the timeline anyway.
|
|
||||||
return this._writeMemberEvent(event, !isLimited, txn);
|
|
||||||
}
|
|
||||||
|
|
||||||
async _writeMemberEvent(event, isLazyLoadingMember, txn) {
|
|
||||||
const userId = event.state_key;
|
|
||||||
if (!userId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const member = RoomMember.fromMemberEvent(this._roomId, event);
|
|
||||||
if (!member) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let existingMember = this._cache.get(userId);
|
|
||||||
if (!existingMember) {
|
if (!existingMember) {
|
||||||
const memberData = await txn.roomMembers.get(this._roomId, userId);
|
const memberData = await txn.roomMembers.get(this._roomId, member.userId);
|
||||||
if (memberData) {
|
if (memberData) {
|
||||||
existingMember = new RoomMember(memberData);
|
existingMember = new RoomMember(memberData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// either never heard of the member, or something changed
|
// either never heard of the member, or something changed
|
||||||
if (!existingMember || !existingMember.equals(member)) {
|
if (!existingMember || !existingMember.equals(member)) {
|
||||||
txn.roomMembers.set(member.serialize());
|
txn.roomMembers.set(member.serialize());
|
||||||
this._cache.set(member);
|
this._cache.set(member);
|
||||||
// we also return a member change for lazy loading members if something changed,
|
|
||||||
// so when the dupe timeline event comes and it doesn't see a diff
|
|
||||||
// with the cache, we already returned the event here.
|
|
||||||
//
|
|
||||||
// it's just important that we don't consider the first LL event
|
|
||||||
// for a user we see as a membership change, or we'll share keys with
|
|
||||||
// them, etc...
|
|
||||||
if (isLazyLoadingMember && !existingMember) {
|
|
||||||
// we don't have a previous member, but we know this is not a
|
|
||||||
// membership change as it's a lazy loaded
|
|
||||||
// member so take the membership from the member
|
|
||||||
return new MemberChange(member, member.membership);
|
|
||||||
}
|
|
||||||
return new MemberChange(member, existingMember?.membership);
|
return new MemberChange(member, existingMember?.membership);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async lookupMember(userId, event, timelineEvents, txn) {
|
async lookupMember(userId, txn) {
|
||||||
let member = this._cache.get(userId);
|
let member = this._cache.get(userId);
|
||||||
if (!member) {
|
if (!member) {
|
||||||
const memberData = await txn.roomMembers.get(this._roomId, userId);
|
const memberData = await txn.roomMembers.get(this._roomId, userId);
|
||||||
|
@ -82,61 +52,154 @@ export class MemberWriter {
|
||||||
this._cache.set(member);
|
this._cache.set(member);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!member) {
|
return member;
|
||||||
// sometimes the member event isn't included in state, but rather in the timeline,
|
}
|
||||||
// even if it is not the first event in the timeline. In this case, go look for
|
}
|
||||||
// the last one before the event, or if none is found,
|
|
||||||
// the least recent matching member event in the timeline.
|
class MemberSync {
|
||||||
// The latter is needed because of new joins picking up their own display name
|
constructor(memberWriter, stateEvents, timelineEvents, hasFetchedMembers) {
|
||||||
let foundEvent = false;
|
this._memberWriter = memberWriter;
|
||||||
let memberEventBefore;
|
this._timelineEvents = timelineEvents;
|
||||||
let firstMemberEvent;
|
this._hasFetchedMembers = hasFetchedMembers;
|
||||||
for (let i = timelineEvents.length - 1; i >= 0; i -= 1) {
|
this._newStateMembers = null;
|
||||||
|
if (stateEvents) {
|
||||||
|
this._newStateMembers = this._stateEventsToMembers(stateEvents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get _roomId() {
|
||||||
|
return this._memberWriter._roomId;
|
||||||
|
}
|
||||||
|
|
||||||
|
_stateEventsToMembers(stateEvents) {
|
||||||
|
let members;
|
||||||
|
for (const event of stateEvents) {
|
||||||
|
if (event.type === MEMBER_EVENT_TYPE) {
|
||||||
|
const member = RoomMember.fromMemberEvent(this._roomId, event);
|
||||||
|
if (member) {
|
||||||
|
if (!members) {
|
||||||
|
members = new Map();
|
||||||
|
}
|
||||||
|
members.set(member.userId, member);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return members;
|
||||||
|
}
|
||||||
|
|
||||||
|
_timelineEventsToMembers(timelineEvents) {
|
||||||
|
let members;
|
||||||
|
// iterate backwards to only add the last member in the timeline
|
||||||
|
for (let i = timelineEvents.length - 1; i >= 0; i--) {
|
||||||
const e = timelineEvents[i];
|
const e = timelineEvents[i];
|
||||||
let matchingEvent;
|
const userId = e.state_key;
|
||||||
if (e.type === MEMBER_EVENT_TYPE && e.state_key === userId) {
|
if (e.type === MEMBER_EVENT_TYPE && !members?.has(userId)) {
|
||||||
matchingEvent = e;
|
const member = RoomMember.fromMemberEvent(this._roomId, e);
|
||||||
firstMemberEvent = matchingEvent;
|
if (member) {
|
||||||
|
if (!members) {
|
||||||
|
members = new Map();
|
||||||
}
|
}
|
||||||
if (!foundEvent) {
|
members.set(member.userId, member);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return members;
|
||||||
|
}
|
||||||
|
|
||||||
|
async lookupMemberAtEvent(userId, event, txn) {
|
||||||
|
let member;
|
||||||
|
if (this._timelineEvents) {
|
||||||
|
member = this._findPrecedingMemberEventInTimeline(userId, event);
|
||||||
|
if (member) {
|
||||||
|
return member;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
member = this._newStateMembers?.get(userId);
|
||||||
|
if (member) {
|
||||||
|
return member;
|
||||||
|
}
|
||||||
|
return await this._memberWriter.lookupMember(userId, txn);
|
||||||
|
}
|
||||||
|
|
||||||
|
async write(txn) {
|
||||||
|
const memberChanges = new Map();
|
||||||
|
let newTimelineMembers;
|
||||||
|
if (this._timelineEvents) {
|
||||||
|
newTimelineMembers = this._timelineEventsToMembers(this._timelineEvents);
|
||||||
|
}
|
||||||
|
if (this._newStateMembers) {
|
||||||
|
for (const member of this._newStateMembers.values()) {
|
||||||
|
if (!newTimelineMembers?.has(member.userId)) {
|
||||||
|
const memberChange = await this._memberWriter._writeMember(member, txn);
|
||||||
|
if (memberChange) {
|
||||||
|
// if the member event appeared only in the state section,
|
||||||
|
// AND we haven't heard about it AND we haven't fetched all members yet (to avoid #470),
|
||||||
|
// this may be a lazy loading member (if it's not in a gap, we are certain
|
||||||
|
// it is a ll member, in a gap, we can't tell), so we pass in our own membership as
|
||||||
|
// as the previous one so we won't consider it a join to not have false positives (to avoid #192).
|
||||||
|
// see also MemberChange.hasJoined
|
||||||
|
const maybeLazyLoadingMember = !this._hasFetchedMembers && !memberChange.previousMembership;
|
||||||
|
if (maybeLazyLoadingMember) {
|
||||||
|
memberChange.previousMembership = member.membership;
|
||||||
|
}
|
||||||
|
memberChanges.set(memberChange.userId, memberChange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (newTimelineMembers) {
|
||||||
|
for (const member of newTimelineMembers.values()) {
|
||||||
|
const memberChange = await this._memberWriter._writeMember(member, txn);
|
||||||
|
if (memberChange) {
|
||||||
|
memberChanges.set(memberChange.userId, memberChange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return memberChanges;
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to find the first member event before the given event,
|
||||||
|
// so we respect historical display names within the chunk of timeline
|
||||||
|
_findPrecedingMemberEventInTimeline(userId, event) {
|
||||||
|
let eventIndex = -1;
|
||||||
|
for (let i = this._timelineEvents.length - 1; i >= 0; i--) {
|
||||||
|
const e = this._timelineEvents[i];
|
||||||
if (e.event_id === event.event_id) {
|
if (e.event_id === event.event_id) {
|
||||||
foundEvent = true;
|
eventIndex = i;
|
||||||
}
|
|
||||||
} else if (matchingEvent) {
|
|
||||||
memberEventBefore = matchingEvent;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// first see if we found a member event before the event we're looking up the sender for
|
for (let i = eventIndex - 1; i >= 0; i--) {
|
||||||
if (memberEventBefore) {
|
const e = this._timelineEvents[i];
|
||||||
member = RoomMember.fromMemberEvent(this._roomId, memberEventBefore);
|
if (e.type === MEMBER_EVENT_TYPE && e.state_key === userId) {
|
||||||
}
|
const member = RoomMember.fromMemberEvent(this._roomId, e);
|
||||||
// and only if we didn't, fall back to the first member event,
|
if (member) {
|
||||||
// regardless of where it is positioned relative to the lookup event
|
|
||||||
else if (firstMemberEvent) {
|
|
||||||
member = RoomMember.fromMemberEvent(this._roomId, firstMemberEvent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return member;
|
return member;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function tests() {
|
export function tests() {
|
||||||
|
|
||||||
|
let idCounter = 0;
|
||||||
|
|
||||||
function createMemberEvent(membership, userId, displayName, avatarUrl) {
|
function createMemberEvent(membership, userId, displayName, avatarUrl) {
|
||||||
|
idCounter += 1;
|
||||||
return {
|
return {
|
||||||
content: {
|
content: {
|
||||||
membership,
|
membership,
|
||||||
"displayname": displayName,
|
"displayname": displayName,
|
||||||
"avatar_url": avatarUrl
|
"avatar_url": avatarUrl
|
||||||
},
|
},
|
||||||
|
event_id: `$${idCounter}`,
|
||||||
sender: userId,
|
sender: userId,
|
||||||
"state_key": userId,
|
"state_key": userId,
|
||||||
type: "m.room.member"
|
type: "m.room.member"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function createStorage(initialMembers = []) {
|
function createStorage(initialMembers = []) {
|
||||||
const members = new Map();
|
const members = new Map();
|
||||||
for (const m of initialMembers) {
|
for (const m of initialMembers) {
|
||||||
|
@ -164,102 +227,195 @@ export function tests() {
|
||||||
const avatar = "mxc://hs.tld/def";
|
const avatar = "mxc://hs.tld/def";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"new join through state": async assert => {
|
"new join": async assert => {
|
||||||
const writer = new MemberWriter(roomId);
|
const writer = new MemberWriter(roomId);
|
||||||
const txn = createStorage();
|
const txn = createStorage();
|
||||||
const change = await writer.writeStateMemberEvent(createMemberEvent("join", alice), true, txn);
|
const memberSync = writer.prepareMemberSync([], [createMemberEvent("join", alice)], false);
|
||||||
|
const changes = await memberSync.write(txn);
|
||||||
|
assert.equal(changes.size, 1);
|
||||||
|
const change = changes.get(alice);
|
||||||
assert(change.hasJoined);
|
assert(change.hasJoined);
|
||||||
assert.equal(txn.members.get(alice).membership, "join");
|
assert.equal(txn.members.get(alice).membership, "join");
|
||||||
},
|
},
|
||||||
"accept invite through state": async assert => {
|
"accept invite": async assert => {
|
||||||
const writer = new MemberWriter(roomId);
|
const writer = new MemberWriter(roomId);
|
||||||
const txn = createStorage([member("invite", alice)]);
|
const txn = createStorage([member("invite", alice)]);
|
||||||
const change = await writer.writeStateMemberEvent(createMemberEvent("join", alice), true, txn);
|
const memberSync = writer.prepareMemberSync([], [createMemberEvent("join", alice)], false);
|
||||||
|
const changes = await memberSync.write(txn);
|
||||||
|
assert.equal(changes.size, 1);
|
||||||
|
const change = changes.get(alice);
|
||||||
assert.equal(change.previousMembership, "invite");
|
assert.equal(change.previousMembership, "invite");
|
||||||
assert(change.hasJoined);
|
assert(change.hasJoined);
|
||||||
assert.equal(txn.members.get(alice).membership, "join");
|
assert.equal(txn.members.get(alice).membership, "join");
|
||||||
},
|
},
|
||||||
"change display name through timeline": async assert => {
|
"change display name": async assert => {
|
||||||
const writer = new MemberWriter(roomId);
|
const writer = new MemberWriter(roomId);
|
||||||
const txn = createStorage([member("join", alice, "Alice")]);
|
const txn = createStorage([member("join", alice, "Alice")]);
|
||||||
const change = await writer.writeTimelineMemberEvent(createMemberEvent("join", alice, "Alies"), txn);
|
const memberSync = writer.prepareMemberSync([], [createMemberEvent("join", alice, "Alies")], false);
|
||||||
|
const changes = await memberSync.write(txn);
|
||||||
|
assert.equal(changes.size, 1);
|
||||||
|
const change = changes.get(alice);
|
||||||
assert(!change.hasJoined);
|
assert(!change.hasJoined);
|
||||||
assert.equal(change.member.displayName, "Alies");
|
assert.equal(change.member.displayName, "Alies");
|
||||||
assert.equal(txn.members.get(alice).displayName, "Alies");
|
assert.equal(txn.members.get(alice).displayName, "Alies");
|
||||||
},
|
},
|
||||||
"set avatar through timeline": async assert => {
|
"set avatar": async assert => {
|
||||||
const writer = new MemberWriter(roomId);
|
const writer = new MemberWriter(roomId);
|
||||||
const txn = createStorage([member("join", alice, "Alice")]);
|
const txn = createStorage([member("join", alice, "Alice")]);
|
||||||
const change = await writer.writeTimelineMemberEvent(createMemberEvent("join", alice, "Alice", avatar), txn);
|
const memberSync = writer.prepareMemberSync([], [createMemberEvent("join", alice, "Alice", avatar)], false);
|
||||||
|
const changes = await memberSync.write(txn);
|
||||||
|
assert.equal(changes.size, 1);
|
||||||
|
const change = changes.get(alice);
|
||||||
assert(!change.hasJoined);
|
assert(!change.hasJoined);
|
||||||
assert.equal(change.member.avatarUrl, avatar);
|
assert.equal(change.member.avatarUrl, avatar);
|
||||||
assert.equal(txn.members.get(alice).avatarUrl, avatar);
|
assert.equal(txn.members.get(alice).avatarUrl, avatar);
|
||||||
},
|
},
|
||||||
"ignore redundant member event": async assert => {
|
"ignore redundant member event in timeline": async assert => {
|
||||||
const writer = new MemberWriter(roomId);
|
const writer = new MemberWriter(roomId);
|
||||||
const txn = createStorage([member("join", alice, "Alice", avatar)]);
|
const txn = createStorage([member("join", alice, "Alice", avatar)]);
|
||||||
const change = await writer.writeTimelineMemberEvent(createMemberEvent("join", alice, "Alice", avatar), txn);
|
const memberSync = writer.prepareMemberSync([], [createMemberEvent("join", alice, "Alice", avatar)], false);
|
||||||
assert(!change);
|
const changes = await memberSync.write(txn);
|
||||||
|
assert.equal(changes.size, 0);
|
||||||
|
},
|
||||||
|
"ignore redundant member event in state": async assert => {
|
||||||
|
const writer = new MemberWriter(roomId);
|
||||||
|
const txn = createStorage([member("join", alice, "Alice", avatar)]);
|
||||||
|
const memberSync = writer.prepareMemberSync([createMemberEvent("join", alice, "Alice", avatar)], [], false);
|
||||||
|
const changes = await memberSync.write(txn);
|
||||||
|
assert.equal(changes.size, 0);
|
||||||
},
|
},
|
||||||
"leave": async assert => {
|
"leave": async assert => {
|
||||||
const writer = new MemberWriter(roomId);
|
const writer = new MemberWriter(roomId);
|
||||||
const txn = createStorage([member("join", alice, "Alice")]);
|
const txn = createStorage([member("join", alice, "Alice")]);
|
||||||
const change = await writer.writeTimelineMemberEvent(createMemberEvent("leave", alice, "Alice"), txn);
|
const memberSync = writer.prepareMemberSync([], [createMemberEvent("leave", alice, "Alice")], false);
|
||||||
|
const changes = await memberSync.write(txn);
|
||||||
|
assert.equal(changes.size, 1);
|
||||||
|
const change = changes.get(alice);
|
||||||
assert(change.hasLeft);
|
assert(change.hasLeft);
|
||||||
assert(!change.hasJoined);
|
assert(!change.hasJoined);
|
||||||
},
|
},
|
||||||
"ban": async assert => {
|
"ban": async assert => {
|
||||||
const writer = new MemberWriter(roomId);
|
const writer = new MemberWriter(roomId);
|
||||||
const txn = createStorage([member("join", alice, "Alice")]);
|
const txn = createStorage([member("join", alice, "Alice")]);
|
||||||
const change = await writer.writeTimelineMemberEvent(createMemberEvent("ban", alice, "Alice"), txn);
|
const memberSync = writer.prepareMemberSync([], [createMemberEvent("ban", alice, "Alice")], false);
|
||||||
|
const changes = await memberSync.write(txn);
|
||||||
|
assert.equal(changes.size, 1);
|
||||||
|
const change = changes.get(alice);
|
||||||
assert(change.hasLeft);
|
assert(change.hasLeft);
|
||||||
assert(!change.hasJoined);
|
assert(!change.hasJoined);
|
||||||
},
|
},
|
||||||
"reject invite": async assert => {
|
"reject invite": async assert => {
|
||||||
const writer = new MemberWriter(roomId);
|
const writer = new MemberWriter(roomId);
|
||||||
const txn = createStorage([member("invite", alice, "Alice")]);
|
const txn = createStorage([member("invite", alice, "Alice")]);
|
||||||
const change = await writer.writeTimelineMemberEvent(createMemberEvent("leave", alice, "Alice"), txn);
|
const memberSync = writer.prepareMemberSync([], [createMemberEvent("leave", alice, "Alice")], false);
|
||||||
|
const changes = await memberSync.write(txn);
|
||||||
|
assert.equal(changes.size, 1);
|
||||||
|
const change = changes.get(alice);
|
||||||
assert(!change.hasLeft);
|
assert(!change.hasLeft);
|
||||||
assert(!change.hasJoined);
|
assert(!change.hasJoined);
|
||||||
},
|
},
|
||||||
"lazy loaded member we already know about doens't return change": async assert => {
|
"lazy loaded member we already know about doens't return change": async assert => {
|
||||||
const writer = new MemberWriter(roomId);
|
const writer = new MemberWriter(roomId);
|
||||||
const txn = createStorage([member("join", alice, "Alice")]);
|
const txn = createStorage([member("join", alice, "Alice")]);
|
||||||
const change = await writer.writeStateMemberEvent(createMemberEvent("join", alice, "Alice"), false, txn);
|
const memberSync = writer.prepareMemberSync([createMemberEvent("join", alice, "Alice")], [], false);
|
||||||
assert(!change);
|
const changes = await memberSync.write(txn);
|
||||||
|
assert.equal(changes.size, 0);
|
||||||
},
|
},
|
||||||
"lazy loaded member we already know about changes display name": async assert => {
|
"lazy loaded member we already know about changes display name": async assert => {
|
||||||
const writer = new MemberWriter(roomId);
|
const writer = new MemberWriter(roomId);
|
||||||
const txn = createStorage([member("join", alice, "Alice")]);
|
const txn = createStorage([member("join", alice, "Alice")]);
|
||||||
const change = await writer.writeStateMemberEvent(createMemberEvent("join", alice, "Alies"), false, txn);
|
const memberSync = writer.prepareMemberSync([createMemberEvent("join", alice, "Alies")], [], false);
|
||||||
|
const changes = await memberSync.write(txn);
|
||||||
|
assert.equal(changes.size, 1);
|
||||||
|
const change = changes.get(alice);
|
||||||
|
assert(!change.hasJoined);
|
||||||
assert.equal(change.member.displayName, "Alies");
|
assert.equal(change.member.displayName, "Alies");
|
||||||
},
|
},
|
||||||
"unknown lazy loaded member returns change, but not considered a membership change": async assert => {
|
"unknown lazy loaded member returns change, but not considered a join": async assert => {
|
||||||
const writer = new MemberWriter(roomId);
|
const writer = new MemberWriter(roomId);
|
||||||
const txn = createStorage();
|
const txn = createStorage();
|
||||||
const change = await writer.writeStateMemberEvent(createMemberEvent("join", alice, "Alice"), false, txn);
|
const memberSync = writer.prepareMemberSync([createMemberEvent("join", alice, "Alice")], [], false);
|
||||||
|
const changes = await memberSync.write(txn);
|
||||||
|
assert.equal(changes.size, 1);
|
||||||
|
const change = changes.get(alice);
|
||||||
assert(!change.hasJoined);
|
assert(!change.hasJoined);
|
||||||
assert(!change.hasLeft);
|
assert(!change.hasLeft);
|
||||||
assert.equal(change.member.membership, "join");
|
assert.equal(change.member.membership, "join");
|
||||||
assert.equal(txn.members.get(alice).displayName, "Alice");
|
assert.equal(txn.members.get(alice).displayName, "Alice");
|
||||||
},
|
},
|
||||||
"newly joined member causes a change with lookup done first": async assert => {
|
"new join through both timeline and state": async assert => {
|
||||||
const event = createMemberEvent("join", alice, "Alice");
|
|
||||||
const writer = new MemberWriter(roomId);
|
const writer = new MemberWriter(roomId);
|
||||||
const txn = createStorage();
|
const txn = createStorage();
|
||||||
const member = await writer.lookupMember(event.sender, event, [event], txn);
|
const aliceJoin = createMemberEvent("join", alice, "Alice");
|
||||||
assert(member);
|
const memberSync = writer.prepareMemberSync([aliceJoin], [aliceJoin], false);
|
||||||
const change = await writer.writeTimelineMemberEvent(event, txn);
|
const changes = await memberSync.write(txn);
|
||||||
assert(change);
|
assert.equal(changes.size, 1);
|
||||||
|
const change = changes.get(alice);
|
||||||
|
assert(change.hasJoined);
|
||||||
|
assert(!change.hasLeft);
|
||||||
},
|
},
|
||||||
"lookupMember returns closest member in the past": async assert => {
|
"change display name in timeline with lazy loaded member in state": async assert => {
|
||||||
|
const writer = new MemberWriter(roomId);
|
||||||
|
const txn = createStorage();
|
||||||
|
const memberSync = writer.prepareMemberSync(
|
||||||
|
[createMemberEvent("join", alice, "Alice")],
|
||||||
|
[createMemberEvent("join", alice, "Alies")],
|
||||||
|
false);
|
||||||
|
const changes = await memberSync.write(txn);
|
||||||
|
assert.equal(changes.size, 1);
|
||||||
|
const change = changes.get(alice);
|
||||||
|
assert(change.hasJoined);
|
||||||
|
assert(!change.hasLeft);
|
||||||
|
assert.equal(change.member.displayName, "Alies");
|
||||||
|
},
|
||||||
|
"lookupMemberAtEvent returns closest member in the past": async assert => {
|
||||||
const event1 = createMemberEvent("join", alice, "Alice");
|
const event1 = createMemberEvent("join", alice, "Alice");
|
||||||
const event2 = createMemberEvent("join", alice, "Alies");
|
const event2 = createMemberEvent("join", alice, "Alies");
|
||||||
const event3 = createMemberEvent("join", alice, "Alys");
|
const event3 = createMemberEvent("join", alice, "Alys");
|
||||||
|
const events = [event1, event2, event3];
|
||||||
|
// we write first because the MemberWriter assumes it is called before
|
||||||
|
// the SyncWriter does any lookups
|
||||||
const writer = new MemberWriter(roomId);
|
const writer = new MemberWriter(roomId);
|
||||||
const txn = createStorage();
|
const txn = createStorage();
|
||||||
const member = await writer.lookupMember(event3.sender, event3, [event1, event2, event3], txn);
|
const memberSync = await writer.prepareMemberSync([], events, false);
|
||||||
|
let member = await memberSync.lookupMemberAtEvent(event1.sender, event1, txn);
|
||||||
|
assert.equal(member, undefined);
|
||||||
|
member = await memberSync.lookupMemberAtEvent(event2.sender, event2, txn);
|
||||||
|
assert.equal(member.displayName, "Alice");
|
||||||
|
member = await memberSync.lookupMemberAtEvent(event3.sender, event3, txn);
|
||||||
assert.equal(member.displayName, "Alies");
|
assert.equal(member.displayName, "Alies");
|
||||||
|
|
||||||
|
assert.equal(txn.members.size, 0);
|
||||||
|
const changes = await memberSync.write(txn);
|
||||||
|
assert.equal(txn.members.size, 1);
|
||||||
|
assert.equal(changes.size, 1);
|
||||||
|
const change = changes.get(alice);
|
||||||
|
assert(change.hasJoined);
|
||||||
|
},
|
||||||
|
"lookupMemberAtEvent falls back on state event": async assert => {
|
||||||
|
const event1 = createMemberEvent("join", alice, "Alice");
|
||||||
|
const event2 = createMemberEvent("join", alice, "Alies");
|
||||||
|
// we write first because the MemberWriter assumes it is called before
|
||||||
|
// the SyncWriter does any lookups
|
||||||
|
const writer = new MemberWriter(roomId);
|
||||||
|
const txn = createStorage();
|
||||||
|
const memberSync = await writer.prepareMemberSync([event1], [event2], false);
|
||||||
|
const member = await memberSync.lookupMemberAtEvent(event2.sender, event2, txn);
|
||||||
|
assert.equal(member.displayName, "Alice");
|
||||||
|
|
||||||
|
assert.equal(txn.members.size, 0);
|
||||||
|
const changes = await memberSync.write(txn);
|
||||||
|
assert.equal(txn.members.size, 1);
|
||||||
|
assert.equal(changes.size, 1);
|
||||||
|
const change = changes.get(alice);
|
||||||
|
assert(change.hasJoined);
|
||||||
|
},
|
||||||
|
"write works without event arrays": async assert => {
|
||||||
|
const writer = new MemberWriter(roomId);
|
||||||
|
const txn = createStorage();
|
||||||
|
const memberSync = await writer.prepareMemberSync(undefined, undefined, false);
|
||||||
|
const changes = await memberSync.write(txn);
|
||||||
|
assert.equal(changes.size, 0);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -133,38 +133,31 @@ export class SyncWriter {
|
||||||
return currentKey;
|
return currentKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _writeStateEvents(roomResponse, memberChanges, isLimited, txn, log) {
|
async _writeStateEvents(stateEvents, txn, log) {
|
||||||
// persist state
|
let nonMemberStateEvents = 0;
|
||||||
const {state} = roomResponse;
|
for (const event of stateEvents) {
|
||||||
if (Array.isArray(state?.events)) {
|
// member events are written prior by MemberWriter
|
||||||
log.set("stateEvents", state.events.length);
|
if (event.type !== MEMBER_EVENT_TYPE) {
|
||||||
for (const event of state.events) {
|
|
||||||
if (event.type === MEMBER_EVENT_TYPE) {
|
|
||||||
const memberChange = await this._memberWriter.writeStateMemberEvent(event, isLimited, txn);
|
|
||||||
if (memberChange) {
|
|
||||||
memberChanges.set(memberChange.userId, memberChange);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
txn.roomState.set(this._roomId, event);
|
txn.roomState.set(this._roomId, event);
|
||||||
|
nonMemberStateEvents += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
log.set("stateEvents", nonMemberStateEvents);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _writeTimeline(timeline, currentKey, memberChanges, txn, log) {
|
async _writeTimeline(timelineEvents, timeline, memberSync, currentKey, txn, log) {
|
||||||
const entries = [];
|
const entries = [];
|
||||||
const updatedEntries = [];
|
const updatedEntries = [];
|
||||||
if (Array.isArray(timeline?.events) && timeline.events.length) {
|
if (timelineEvents?.length) {
|
||||||
// only create a fragment when we will really write an event
|
// only create a fragment when we will really write an event
|
||||||
currentKey = await this._ensureLiveFragment(currentKey, entries, timeline, txn, log);
|
currentKey = await this._ensureLiveFragment(currentKey, entries, timeline, txn, log);
|
||||||
const events = deduplicateEvents(timeline.events);
|
log.set("timelineEvents", timelineEvents.length);
|
||||||
log.set("timelineEvents", events.length);
|
|
||||||
let timelineStateEventCount = 0;
|
let timelineStateEventCount = 0;
|
||||||
for(const event of events) {
|
for(const event of timelineEvents) {
|
||||||
// store event in timeline
|
// store event in timeline
|
||||||
currentKey = currentKey.nextKey();
|
currentKey = currentKey.nextKey();
|
||||||
const storageEntry = createEventEntry(currentKey, this._roomId, event);
|
const storageEntry = createEventEntry(currentKey, this._roomId, event);
|
||||||
let member = await this._memberWriter.lookupMember(event.sender, event, events, txn);
|
let member = await memberSync.lookupMemberAtEvent(event.sender, event, txn);
|
||||||
if (member) {
|
if (member) {
|
||||||
storageEntry.displayName = member.displayName;
|
storageEntry.displayName = member.displayName;
|
||||||
storageEntry.avatarUrl = member.avatarUrl;
|
storageEntry.avatarUrl = member.avatarUrl;
|
||||||
|
@ -178,19 +171,13 @@ export class SyncWriter {
|
||||||
}
|
}
|
||||||
// update state events after writing event, so for a member event,
|
// update state events after writing event, so for a member event,
|
||||||
// we only update the member info after having written the member event
|
// we only update the member info after having written the member event
|
||||||
// to the timeline, as we want that event to have the old profile info
|
// to the timeline, as we want that event to have the old profile info.
|
||||||
if (typeof event.state_key === "string") {
|
// member events are written prior by MemberWriter.
|
||||||
|
if (typeof event.state_key === "string" && event.type !== MEMBER_EVENT_TYPE) {
|
||||||
timelineStateEventCount += 1;
|
timelineStateEventCount += 1;
|
||||||
if (event.type === MEMBER_EVENT_TYPE) {
|
|
||||||
const memberChange = await this._memberWriter.writeTimelineMemberEvent(event, txn);
|
|
||||||
if (memberChange) {
|
|
||||||
memberChanges.set(memberChange.userId, memberChange);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
txn.roomState.set(this._roomId, event);
|
txn.roomState.set(this._roomId, event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
log.set("timelineStateEventCount", timelineStateEventCount);
|
log.set("timelineStateEventCount", timelineStateEventCount);
|
||||||
}
|
}
|
||||||
return {currentKey, entries, updatedEntries};
|
return {currentKey, entries, updatedEntries};
|
||||||
|
@ -224,14 +211,13 @@ export class SyncWriter {
|
||||||
* @type {SyncWriterResult}
|
* @type {SyncWriterResult}
|
||||||
* @property {Array<BaseEntry>} entries new timeline entries written
|
* @property {Array<BaseEntry>} entries new timeline entries written
|
||||||
* @property {EventKey} newLiveKey the advanced key to write events at
|
* @property {EventKey} newLiveKey the advanced key to write events at
|
||||||
* @property {Map<string, MemberChange>} memberChanges member changes in the processed sync ny user id
|
|
||||||
*
|
*
|
||||||
* @param {Object} roomResponse [description]
|
* @param {Object} roomResponse [description]
|
||||||
* @param {boolean} isRejoin whether the room was rejoined in the sync being processed
|
* @param {boolean} isRejoin whether the room was rejoined in the sync being processed
|
||||||
* @param {Transaction} txn
|
* @param {Transaction} txn
|
||||||
* @return {SyncWriterResult}
|
* @return {SyncWriterResult}
|
||||||
*/
|
*/
|
||||||
async writeSync(roomResponse, isRejoin, txn, log) {
|
async writeSync(roomResponse, isRejoin, hasFetchedMembers, txn, log) {
|
||||||
let {timeline} = roomResponse;
|
let {timeline} = roomResponse;
|
||||||
// we have rejoined the room after having synced it before,
|
// we have rejoined the room after having synced it before,
|
||||||
// check for overlap with the last synced event
|
// check for overlap with the last synced event
|
||||||
|
@ -239,13 +225,22 @@ export class SyncWriter {
|
||||||
if (isRejoin) {
|
if (isRejoin) {
|
||||||
timeline = await this._handleRejoinOverlap(timeline, txn, log);
|
timeline = await this._handleRejoinOverlap(timeline, txn, log);
|
||||||
}
|
}
|
||||||
const memberChanges = new Map();
|
let timelineEvents;
|
||||||
// important this happens before _writeTimeline so
|
if (Array.isArray(timeline?.events)) {
|
||||||
// members are available in the transaction
|
timelineEvents = deduplicateEvents(timeline.events);
|
||||||
await this._writeStateEvents(roomResponse, memberChanges, timeline?.limited, txn, log);
|
}
|
||||||
|
const {state} = roomResponse;
|
||||||
|
let stateEvents;
|
||||||
|
if (Array.isArray(state?.events)) {
|
||||||
|
stateEvents = state.events;
|
||||||
|
}
|
||||||
|
const memberSync = this._memberWriter.prepareMemberSync(stateEvents, timelineEvents, hasFetchedMembers);
|
||||||
|
if (stateEvents) {
|
||||||
|
await this._writeStateEvents(stateEvents, txn, log);
|
||||||
|
}
|
||||||
const {currentKey, entries, updatedEntries} =
|
const {currentKey, entries, updatedEntries} =
|
||||||
await this._writeTimeline(timeline, this._lastLiveKey, memberChanges, txn, log);
|
await this._writeTimeline(timelineEvents, timeline, memberSync, this._lastLiveKey, txn, log);
|
||||||
log.set("memberChanges", memberChanges.size);
|
const memberChanges = await memberSync.write(txn);
|
||||||
return {entries, updatedEntries, newLiveKey: currentKey, memberChanges};
|
return {entries, updatedEntries, newLiveKey: currentKey, memberChanges};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,9 +20,12 @@ import { exportSession, importSession } from "./export";
|
||||||
import { schema } from "./schema";
|
import { schema } from "./schema";
|
||||||
import { detectWebkitEarlyCloseTxnBug } from "./quirks";
|
import { detectWebkitEarlyCloseTxnBug } from "./quirks";
|
||||||
|
|
||||||
|
type LogType = any
|
||||||
|
|
||||||
const sessionName = (sessionId: string) => `hydrogen_session_${sessionId}`;
|
const sessionName = (sessionId: string) => `hydrogen_session_${sessionId}`;
|
||||||
const openDatabaseWithSessionId = function(sessionId: string, idbFactory: IDBFactory): Promise<IDBDatabase> {
|
const openDatabaseWithSessionId = function(sessionId: string, idbFactory: IDBFactory, log?: LogType) {
|
||||||
return openDatabase(sessionName(sessionId), createStores, schema.length, idbFactory);
|
const create = (db, txn, oldVersion, version) => createStores(db, txn, oldVersion, version, log);
|
||||||
|
return openDatabase(sessionName(sessionId), create, schema.length, idbFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ServiceWorkerHandler {
|
interface ServiceWorkerHandler {
|
||||||
|
@ -49,15 +52,15 @@ async function requestPersistedStorage(): Promise<boolean> {
|
||||||
export class StorageFactory {
|
export class StorageFactory {
|
||||||
private _serviceWorkerHandler: ServiceWorkerHandler;
|
private _serviceWorkerHandler: ServiceWorkerHandler;
|
||||||
private _idbFactory: IDBFactory;
|
private _idbFactory: IDBFactory;
|
||||||
|
private _IDBKeyRange: typeof IDBKeyRange
|
||||||
|
|
||||||
constructor(serviceWorkerHandler: ServiceWorkerHandler, idbFactory: IDBFactory = window.indexedDB, IDBKeyRange = window.IDBKeyRange) {
|
constructor(serviceWorkerHandler: ServiceWorkerHandler, idbFactory: IDBFactory = window.indexedDB, _IDBKeyRange = window.IDBKeyRange) {
|
||||||
this._serviceWorkerHandler = serviceWorkerHandler;
|
this._serviceWorkerHandler = serviceWorkerHandler;
|
||||||
this._idbFactory = idbFactory;
|
this._idbFactory = idbFactory;
|
||||||
// @ts-ignore
|
this._IDBKeyRange = _IDBKeyRange;
|
||||||
this._IDBKeyRange = IDBKeyRange;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(sessionId: string): Promise<Storage> {
|
async create(sessionId: string, log?: LogType): Promise<Storage> {
|
||||||
await this._serviceWorkerHandler?.preventConcurrentSessionAccess(sessionId);
|
await this._serviceWorkerHandler?.preventConcurrentSessionAccess(sessionId);
|
||||||
requestPersistedStorage().then(persisted => {
|
requestPersistedStorage().then(persisted => {
|
||||||
// Firefox lies here though, and returns true even if the user denied the request
|
// Firefox lies here though, and returns true even if the user denied the request
|
||||||
|
@ -67,8 +70,7 @@ export class StorageFactory {
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasWebkitEarlyCloseTxnBug = await detectWebkitEarlyCloseTxnBug(this._idbFactory);
|
const hasWebkitEarlyCloseTxnBug = await detectWebkitEarlyCloseTxnBug(this._idbFactory);
|
||||||
const db = await openDatabaseWithSessionId(sessionId, this._idbFactory);
|
const db = await openDatabaseWithSessionId(sessionId, this._idbFactory, log);
|
||||||
// @ts-ignore
|
|
||||||
return new Storage(db, this._IDBKeyRange, hasWebkitEarlyCloseTxnBug);
|
return new Storage(db, this._IDBKeyRange, hasWebkitEarlyCloseTxnBug);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,10 +91,11 @@ export class StorageFactory {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createStores(db: IDBDatabase, txn: IDBTransaction, oldVersion: number | null, version: number): Promise<void> {
|
async function createStores(db: IDBDatabase, txn: IDBTransaction, oldVersion: number | null, version: number, log?: LogType): Promise<void> {
|
||||||
const startIdx = oldVersion || 0;
|
const startIdx = oldVersion || 0;
|
||||||
|
return log.wrap({l: "storage migration", oldVersion, version}, async log => {
|
||||||
for(let i = startIdx; i < version; ++i) {
|
for(let i = startIdx; i < version; ++i) {
|
||||||
await schema[i](db, txn);
|
await log.wrap(`v${i + 1}`, log => schema[i](db, txn, log));
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -148,7 +148,7 @@ export class Store<T> extends QueryTarget<T> {
|
||||||
return new QueryTarget<T>(new QueryTargetWrapper<T>(this._idbStore.index(indexName)));
|
return new QueryTarget<T>(new QueryTargetWrapper<T>(this._idbStore.index(indexName)));
|
||||||
}
|
}
|
||||||
|
|
||||||
put(value: T): Promise<IDBValidKey> {
|
put(value: T): void {
|
||||||
// If this request fails, the error will bubble up to the transaction and abort it,
|
// If this request fails, the error will bubble up to the transaction and abort it,
|
||||||
// which is the behaviour we want. Therefore, it is ok to not create a promise for this
|
// which is the behaviour we want. Therefore, it is ok to not create a promise for this
|
||||||
// request and await it.
|
// request and await it.
|
||||||
|
@ -159,12 +159,12 @@ export class Store<T> extends QueryTarget<T> {
|
||||||
//
|
//
|
||||||
// Note that this can still throw synchronously, like it does for TransactionInactiveError,
|
// Note that this can still throw synchronously, like it does for TransactionInactiveError,
|
||||||
// see https://www.w3.org/TR/IndexedDB-2/#transaction-lifetime-concept
|
// see https://www.w3.org/TR/IndexedDB-2/#transaction-lifetime-concept
|
||||||
return reqAsPromise(this._idbStore.put(value));
|
this._idbStore.put(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
add(value: T): Promise<IDBValidKey> {
|
add(value: T): void {
|
||||||
// ok to not monitor result of request, see comment in `put`.
|
// ok to not monitor result of request, see comment in `put`.
|
||||||
return reqAsPromise(this._idbStore.add(value));
|
this._idbStore.add(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(keyOrKeyRange: IDBValidKey | IDBKeyRange): Promise<undefined> {
|
delete(keyOrKeyRange: IDBValidKey | IDBKeyRange): Promise<undefined> {
|
||||||
|
|
|
@ -25,23 +25,18 @@ function _sourceName(source: IDBIndex | IDBObjectStore): string {
|
||||||
|
|
||||||
function _sourceDatabase(source: IDBIndex | IDBObjectStore): string {
|
function _sourceDatabase(source: IDBIndex | IDBObjectStore): string {
|
||||||
return "objectStore" in source ?
|
return "objectStore" in source ?
|
||||||
source.objectStore.transaction.db.name :
|
source.objectStore?.transaction?.db?.name :
|
||||||
source.transaction.db.name;
|
source.transaction?.db?.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class IDBError extends StorageError {
|
export class IDBError extends StorageError {
|
||||||
storeName: string;
|
storeName: string;
|
||||||
databaseName: string;
|
databaseName: string;
|
||||||
|
|
||||||
constructor(message: string, source: IDBIndex | IDBCursor | IDBObjectStore, cause: DOMException | null = null) {
|
constructor(message: string, sourceOrCursor: IDBIndex | IDBCursor | IDBObjectStore, cause: DOMException | null = null) {
|
||||||
let storeName: string, databaseName: string;
|
const source = "source" in sourceOrCursor ? sourceOrCursor.source : sourceOrCursor;
|
||||||
if (source instanceof IDBCursor) {
|
const storeName = _sourceName(source);
|
||||||
storeName = _sourceName(source.source);
|
const databaseName = _sourceDatabase(source);
|
||||||
databaseName = _sourceDatabase(source.source);
|
|
||||||
} else {
|
|
||||||
storeName = _sourceName(source);
|
|
||||||
databaseName = _sourceDatabase(source);
|
|
||||||
}
|
|
||||||
let fullMessage = `${message} on ${databaseName}.${storeName}`;
|
let fullMessage = `${message} on ${databaseName}.${storeName}`;
|
||||||
if (cause) {
|
if (cause) {
|
||||||
fullMessage += ": ";
|
fullMessage += ": ";
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import {iterateCursor, NOT_DONE, reqAsPromise} from "./utils";
|
import {iterateCursor, NOT_DONE, reqAsPromise} from "./utils";
|
||||||
import {RoomMember, EVENT_TYPE as MEMBER_EVENT_TYPE} from "../../room/members/RoomMember.js";
|
import {RoomMember, EVENT_TYPE as MEMBER_EVENT_TYPE} from "../../room/members/RoomMember.js";
|
||||||
|
import {addRoomToIdentity} from "../../e2ee/DeviceTracker.js";
|
||||||
import {RoomMemberStore} from "./stores/RoomMemberStore";
|
import {RoomMemberStore} from "./stores/RoomMemberStore";
|
||||||
import {RoomStateEntry} from "./stores/RoomStateStore";
|
import {RoomStateEntry} from "./stores/RoomStateStore";
|
||||||
import {SessionStore} from "./stores/SessionStore";
|
import {SessionStore} from "./stores/SessionStore";
|
||||||
import {encodeScopeTypeKey} from "./stores/OperationStore";
|
import {encodeScopeTypeKey} from "./stores/OperationStore";
|
||||||
|
import {MAX_UNICODE} from "./stores/common";
|
||||||
|
|
||||||
// FUNCTIONS SHOULD ONLY BE APPENDED!!
|
// FUNCTIONS SHOULD ONLY BE APPENDED!!
|
||||||
// the index in the array is the database version
|
// the index in the array is the database version
|
||||||
|
@ -18,6 +20,7 @@ export const schema = [
|
||||||
createArchivedRoomSummaryStore,
|
createArchivedRoomSummaryStore,
|
||||||
migrateOperationScopeIndex,
|
migrateOperationScopeIndex,
|
||||||
createTimelineRelationsStore,
|
createTimelineRelationsStore,
|
||||||
|
fixMissingRoomsInUserIdentities
|
||||||
];
|
];
|
||||||
// TODO: how to deal with git merge conflicts of this array?
|
// TODO: how to deal with git merge conflicts of this array?
|
||||||
|
|
||||||
|
@ -150,3 +153,47 @@ async function migrateOperationScopeIndex(db: IDBDatabase, txn: IDBTransaction):
|
||||||
function createTimelineRelationsStore(db: IDBDatabase) : void {
|
function createTimelineRelationsStore(db: IDBDatabase) : void {
|
||||||
db.createObjectStore("timelineRelations", {keyPath: "key"});
|
db.createObjectStore("timelineRelations", {keyPath: "key"});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//v11 doesn't change the schema, but ensures all userIdentities have all the roomIds they should (see #470)
|
||||||
|
async function fixMissingRoomsInUserIdentities(db, txn, log) {
|
||||||
|
const roomSummaryStore = txn.objectStore("roomSummary");
|
||||||
|
const trackedRoomIds = [];
|
||||||
|
await iterateCursor(roomSummaryStore.openCursor(), roomSummary => {
|
||||||
|
if (roomSummary.isTrackingMembers) {
|
||||||
|
trackedRoomIds.push(roomSummary.roomId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const outboundGroupSessionsStore = txn.objectStore("outboundGroupSessions");
|
||||||
|
const userIdentitiesStore = txn.objectStore("userIdentities");
|
||||||
|
const roomMemberStore = txn.objectStore("roomMembers");
|
||||||
|
for (const roomId of trackedRoomIds) {
|
||||||
|
let foundMissing = false;
|
||||||
|
const joinedUserIds = [];
|
||||||
|
const memberRange = IDBKeyRange.bound(roomId, `${roomId}|${MAX_UNICODE}`, true, true);
|
||||||
|
await log.wrap({l: "room", id: roomId}, async log => {
|
||||||
|
await iterateCursor(roomMemberStore.openCursor(memberRange), member => {
|
||||||
|
if (member.membership === "join") {
|
||||||
|
joinedUserIds.push(member.userId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
log.set("joinedUserIds", joinedUserIds.length);
|
||||||
|
for (const userId of joinedUserIds) {
|
||||||
|
const identity = await reqAsPromise(userIdentitiesStore.get(userId));
|
||||||
|
const originalRoomCount = identity?.roomIds?.length;
|
||||||
|
const updatedIdentity = addRoomToIdentity(identity, userId, roomId);
|
||||||
|
if (updatedIdentity) {
|
||||||
|
log.log({l: `fixing up`, id: userId,
|
||||||
|
roomsBefore: originalRoomCount, roomsAfter: updatedIdentity.roomIds.length});
|
||||||
|
userIdentitiesStore.put(updatedIdentity);
|
||||||
|
foundMissing = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.set("foundMissing", foundMissing);
|
||||||
|
if (foundMissing) {
|
||||||
|
// clear outbound megolm session,
|
||||||
|
// so we'll create a new one on the next message that will be properly shared
|
||||||
|
outboundGroupSessionsStore.delete(roomId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ export class AccountDataStore {
|
||||||
return await this._store.get(type);
|
return await this._store.get(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
set(event: AccountDataEntry): Promise<IDBValidKey> {
|
set(event: AccountDataEntry): void {
|
||||||
return this._store.put(event);
|
this._store.put(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,9 +69,9 @@ export class DeviceIdentityStore {
|
||||||
return this._store.get(encodeKey(userId, deviceId));
|
return this._store.get(encodeKey(userId, deviceId));
|
||||||
}
|
}
|
||||||
|
|
||||||
set(deviceIdentity: DeviceIdentity): Promise<IDBValidKey> {
|
set(deviceIdentity: DeviceIdentity): void {
|
||||||
deviceIdentity.key = encodeKey(deviceIdentity.userId, deviceIdentity.deviceId);
|
deviceIdentity.key = encodeKey(deviceIdentity.userId, deviceIdentity.deviceId);
|
||||||
return this._store.put(deviceIdentity);
|
this._store.put(deviceIdentity);
|
||||||
}
|
}
|
||||||
|
|
||||||
getByCurve25519Key(curve25519Key: string): Promise<DeviceIdentity | null> {
|
getByCurve25519Key(curve25519Key: string): Promise<DeviceIdentity | null> {
|
||||||
|
|
|
@ -24,13 +24,14 @@ function encodeKey(roomId: string, sessionId: string, messageIndex: number | str
|
||||||
interface GroupSessionDecryption {
|
interface GroupSessionDecryption {
|
||||||
eventId: string;
|
eventId: string;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
key: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GroupSessionDecryptionStore {
|
type GroupSessionEntry = GroupSessionDecryption & { key: string }
|
||||||
private _store: Store<GroupSessionDecryption>;
|
|
||||||
|
|
||||||
constructor(store: Store<GroupSessionDecryption>) {
|
export class GroupSessionDecryptionStore {
|
||||||
|
private _store: Store<GroupSessionEntry>;
|
||||||
|
|
||||||
|
constructor(store: Store<GroupSessionEntry>) {
|
||||||
this._store = store;
|
this._store = store;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,9 +39,9 @@ export class GroupSessionDecryptionStore {
|
||||||
return this._store.get(encodeKey(roomId, sessionId, messageIndex));
|
return this._store.get(encodeKey(roomId, sessionId, messageIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
set(roomId: string, sessionId: string, messageIndex: number, decryption: GroupSessionDecryption): Promise<IDBValidKey> {
|
set(roomId: string, sessionId: string, messageIndex: number, decryption: GroupSessionDecryption): void {
|
||||||
decryption.key = encodeKey(roomId, sessionId, messageIndex);
|
(decryption as GroupSessionEntry).key = encodeKey(roomId, sessionId, messageIndex);
|
||||||
return this._store.put(decryption);
|
this._store.put(decryption as GroupSessionEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeAllForRoom(roomId: string): Promise<undefined> {
|
removeAllForRoom(roomId: string): Promise<undefined> {
|
||||||
|
|
|
@ -48,9 +48,9 @@ export class InboundGroupSessionStore {
|
||||||
return this._store.get(encodeKey(roomId, senderKey, sessionId));
|
return this._store.get(encodeKey(roomId, senderKey, sessionId));
|
||||||
}
|
}
|
||||||
|
|
||||||
set(session: InboundGroupSession): Promise<IDBValidKey> {
|
set(session: InboundGroupSession): void {
|
||||||
session.key = encodeKey(session.roomId, session.senderKey, session.sessionId);
|
session.key = encodeKey(session.roomId, session.senderKey, session.sessionId);
|
||||||
return this._store.put(session);
|
this._store.put(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeAllForRoom(roomId: string): Promise<undefined> {
|
removeAllForRoom(roomId: string): Promise<undefined> {
|
||||||
|
|
|
@ -41,8 +41,8 @@ export class InviteStore {
|
||||||
return this._inviteStore.selectAll();
|
return this._inviteStore.selectAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
set(invite: InviteData): Promise<IDBValidKey> {
|
set(invite: InviteData): void {
|
||||||
return this._inviteStore.put(invite);
|
this._inviteStore.put(invite);
|
||||||
}
|
}
|
||||||
|
|
||||||
remove(roomId: string): void {
|
remove(roomId: string): void {
|
||||||
|
|
|
@ -65,9 +65,9 @@ export class OlmSessionStore {
|
||||||
return this._store.get(encodeKey(senderKey, sessionId));
|
return this._store.get(encodeKey(senderKey, sessionId));
|
||||||
}
|
}
|
||||||
|
|
||||||
set(session: OlmSession): Promise<IDBValidKey> {
|
set(session: OlmSession): void {
|
||||||
session.key = encodeKey(session.senderKey, session.sessionId);
|
session.key = encodeKey(session.senderKey, session.sessionId);
|
||||||
return this._store.put(session);
|
this._store.put(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
remove(senderKey: string, sessionId: string): Promise<undefined> {
|
remove(senderKey: string, sessionId: string): Promise<undefined> {
|
||||||
|
|
|
@ -20,15 +20,18 @@ export function encodeScopeTypeKey(scope: string, type: string): string {
|
||||||
return `${scope}|${type}`;
|
return `${scope}|${type}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Operation {
|
interface BaseOperation {
|
||||||
id: string;
|
id: string;
|
||||||
type: string;
|
|
||||||
scope: string;
|
scope: string;
|
||||||
userIds: string[];
|
userIds: string[];
|
||||||
scopeTypeKey: string;
|
|
||||||
roomKeyMessage: RoomKeyMessage;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OperationType = { type: "share_room_key"; roomKeyMessage: RoomKeyMessage; }
|
||||||
|
|
||||||
|
type Operation = BaseOperation & OperationType
|
||||||
|
|
||||||
|
type OperationEntry = Operation & { scopeTypeKey: string; }
|
||||||
|
|
||||||
interface RoomKeyMessage {
|
interface RoomKeyMessage {
|
||||||
room_id: string;
|
room_id: string;
|
||||||
session_id: string;
|
session_id: string;
|
||||||
|
@ -38,9 +41,9 @@ interface RoomKeyMessage {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class OperationStore {
|
export class OperationStore {
|
||||||
private _store: Store<Operation>;
|
private _store: Store<OperationEntry>;
|
||||||
|
|
||||||
constructor(store: Store<Operation>) {
|
constructor(store: Store<OperationEntry>) {
|
||||||
this._store = store;
|
this._store = store;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,13 +64,13 @@ export class OperationStore {
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
add(operation: Operation): Promise<IDBValidKey> {
|
add(operation: Operation): void {
|
||||||
operation.scopeTypeKey = encodeScopeTypeKey(operation.scope, operation.type);
|
(operation as OperationEntry).scopeTypeKey = encodeScopeTypeKey(operation.scope, operation.type);
|
||||||
return this._store.add(operation);
|
this._store.add(operation as OperationEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
update(operation: Operation): Promise<IDBValidKey> {
|
update(operation: Operation): void {
|
||||||
return this._store.put(operation);
|
this._store.put(operation as OperationEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
remove(id: string): Promise<undefined> {
|
remove(id: string): Promise<undefined> {
|
||||||
|
|
|
@ -36,7 +36,7 @@ export class OutboundGroupSessionStore {
|
||||||
return this._store.get(roomId);
|
return this._store.get(roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
set(session: OutboundSession): Promise<IDBValidKey> {
|
set(session: OutboundSession): void {
|
||||||
return this._store.put(session);
|
this._store.put(session);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,13 +73,13 @@ export class PendingEventStore {
|
||||||
return !!key;
|
return !!key;
|
||||||
}
|
}
|
||||||
|
|
||||||
add(pendingEvent: PendingEntry): Promise<IDBValidKey> {
|
add(pendingEvent: PendingEntry): void {
|
||||||
pendingEvent.key = encodeKey(pendingEvent.roomId, pendingEvent.queueIndex);
|
pendingEvent.key = encodeKey(pendingEvent.roomId, pendingEvent.queueIndex);
|
||||||
return this._eventStore.add(pendingEvent);
|
this._eventStore.add(pendingEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
update(pendingEvent: PendingEntry): Promise<IDBValidKey> {
|
update(pendingEvent: PendingEntry): void {
|
||||||
return this._eventStore.put(pendingEvent);
|
this._eventStore.put(pendingEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
getAll(): Promise<PendingEntry[]> {
|
getAll(): Promise<PendingEntry[]> {
|
||||||
|
|
|
@ -50,10 +50,10 @@ export class RoomMemberStore {
|
||||||
return this._roomMembersStore.get(encodeKey(roomId, userId));
|
return this._roomMembersStore.get(encodeKey(roomId, userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
async set(member: MemberData): Promise<IDBValidKey> {
|
set(member: MemberData): void {
|
||||||
// Object.assign would be more typesafe, but small objects
|
// Object.assign would be more typesafe, but small objects
|
||||||
(member as any).key = encodeKey(member.roomId, member.userId);
|
(member as MemberStorageEntry).key = encodeKey(member.roomId, member.userId);
|
||||||
return this._roomMembersStore.put(member as MemberStorageEntry);
|
this._roomMembersStore.put(member as MemberStorageEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
getAll(roomId: string): Promise<MemberData[]> {
|
getAll(roomId: string): Promise<MemberData[]> {
|
||||||
|
@ -78,10 +78,10 @@ export class RoomMemberStore {
|
||||||
return userIds;
|
return userIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
removeAllForRoom(roomId: string): Promise<undefined> {
|
removeAllForRoom(roomId: string): void {
|
||||||
// exclude both keys as they are theoretical min and max,
|
// exclude both keys as they are theoretical min and max,
|
||||||
// but we should't have a match for just the room id, or room id with max
|
// but we should't have a match for just the room id, or room id with max
|
||||||
const range = this._roomMembersStore.IDBKeyRange.bound(roomId, `${roomId}|${MAX_UNICODE}`, true, true);
|
const range = this._roomMembersStore.IDBKeyRange.bound(roomId, `${roomId}|${MAX_UNICODE}`, true, true);
|
||||||
return this._roomMembersStore.delete(range);
|
this._roomMembersStore.delete(range);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,10 +41,10 @@ export class RoomStateStore {
|
||||||
return this._roomStateStore.get(key);
|
return this._roomStateStore.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
set(roomId: string, event: StateEvent): Promise<IDBValidKey> {
|
set(roomId: string, event: StateEvent): void {
|
||||||
const key = encodeKey(roomId, event.type, event.state_key);
|
const key = encodeKey(roomId, event.type, event.state_key);
|
||||||
const entry = {roomId, event, key};
|
const entry = {roomId, event, key};
|
||||||
return this._roomStateStore.put(entry);
|
this._roomStateStore.put(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeAllForRoom(roomId: string): Promise<undefined> {
|
removeAllForRoom(roomId: string): Promise<undefined> {
|
||||||
|
|
|
@ -42,8 +42,8 @@ export class RoomSummaryStore {
|
||||||
return this._summaryStore.selectAll();
|
return this._summaryStore.selectAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
set(summary: SummaryData): Promise<IDBValidKey> {
|
set(summary: SummaryData): void {
|
||||||
return this._summaryStore.put(summary);
|
this._summaryStore.put(summary);
|
||||||
}
|
}
|
||||||
|
|
||||||
get(roomId: string): Promise<SummaryData | null> {
|
get(roomId: string): Promise<SummaryData | null> {
|
||||||
|
|
|
@ -27,22 +27,22 @@ export class SessionStore {
|
||||||
this._sessionStore = sessionStore;
|
this._sessionStore = sessionStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(key: IDBValidKey): Promise<any> {
|
async get(key: string): Promise<any> {
|
||||||
const entry = await this._sessionStore.get(key);
|
const entry = await this._sessionStore.get(key);
|
||||||
if (entry) {
|
if (entry) {
|
||||||
return entry.value;
|
return entry.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
set(key: string, value: any): Promise<IDBValidKey> {
|
set(key: string, value: any): void {
|
||||||
return this._sessionStore.put({key, value});
|
this._sessionStore.put({key, value});
|
||||||
}
|
}
|
||||||
|
|
||||||
add(key: string, value: any): Promise<IDBValidKey> {
|
add(key: string, value: any): void {
|
||||||
return this._sessionStore.add({key, value});
|
this._sessionStore.add({key, value});
|
||||||
}
|
}
|
||||||
|
|
||||||
remove(key: IDBValidKey): Promise<undefined> {
|
remove(key: string): void {
|
||||||
return this._sessionStore.delete(key);
|
this._sessionStore.delete(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { StorageError } from "../../common";
|
||||||
import { encodeUint32 } from "../utils";
|
import { encodeUint32 } from "../utils";
|
||||||
import {KeyLimits} from "../../common";
|
import {KeyLimits} from "../../common";
|
||||||
import {Store} from "../Store";
|
import {Store} from "../Store";
|
||||||
import {RoomEvent, StateEvent} from "../../types";
|
import {TimelineEvent, StateEvent} from "../../types";
|
||||||
|
|
||||||
interface Annotation {
|
interface Annotation {
|
||||||
count: number;
|
count: number;
|
||||||
|
@ -31,7 +31,7 @@ interface StorageEntry {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
fragmentId: number;
|
fragmentId: number;
|
||||||
eventIndex: number;
|
eventIndex: number;
|
||||||
event: RoomEvent | StateEvent;
|
event: TimelineEvent | StateEvent;
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
avatarUrl?: string;
|
avatarUrl?: string;
|
||||||
annotations?: { [key : string]: Annotation };
|
annotations?: { [key : string]: Annotation };
|
||||||
|
@ -53,15 +53,15 @@ function decodeEventIdKey(eventIdKey: string): { roomId: string, eventId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
class Range {
|
class Range {
|
||||||
private _IDBKeyRange: any; // TODO what's the appropriate representation here?
|
private _IDBKeyRange: typeof IDBKeyRange;
|
||||||
private _only?: EventKey;
|
private _only?: EventKey;
|
||||||
private _lower?: EventKey;
|
private _lower?: EventKey;
|
||||||
private _upper?: EventKey;
|
private _upper?: EventKey;
|
||||||
private _lowerOpen: boolean;
|
private _lowerOpen: boolean;
|
||||||
private _upperOpen: boolean;
|
private _upperOpen: boolean;
|
||||||
|
|
||||||
constructor(IDBKeyRange: any, only?: EventKey, lower?: EventKey, upper?: EventKey, lowerOpen: boolean = false, upperOpen: boolean = false) {
|
constructor(_IDBKeyRange: any, only?: EventKey, lower?: EventKey, upper?: EventKey, lowerOpen: boolean = false, upperOpen: boolean = false) {
|
||||||
this._IDBKeyRange = IDBKeyRange;
|
this._IDBKeyRange = _IDBKeyRange;
|
||||||
this._only = only;
|
this._only = only;
|
||||||
this._lower = lower;
|
this._lower = lower;
|
||||||
this._upper = upper;
|
this._upper = upper;
|
||||||
|
@ -262,23 +262,23 @@ export class TimelineEventStore {
|
||||||
|
|
||||||
/** Inserts a new entry into the store. The combination of roomId and eventKey should not exist yet, or an error is thrown.
|
/** Inserts a new entry into the store. The combination of roomId and eventKey should not exist yet, or an error is thrown.
|
||||||
* @param entry the entry to insert
|
* @param entry the entry to insert
|
||||||
* @return a promise resolving to undefined if the operation was successful, or a StorageError if not.
|
* @return nothing. To wait for the operation to finish, await the transaction it's part of.
|
||||||
* @throws {StorageError} ...
|
* @throws {StorageError} ...
|
||||||
*/
|
*/
|
||||||
insert(entry: StorageEntry): Promise<IDBValidKey> {
|
insert(entry: StorageEntry): void {
|
||||||
entry.key = encodeKey(entry.roomId, entry.fragmentId, entry.eventIndex);
|
entry.key = encodeKey(entry.roomId, entry.fragmentId, entry.eventIndex);
|
||||||
entry.eventIdKey = encodeEventIdKey(entry.roomId, entry.event.event_id);
|
entry.eventIdKey = encodeEventIdKey(entry.roomId, entry.event.event_id);
|
||||||
// TODO: map error? or in idb/store?
|
// TODO: map error? or in idb/store?
|
||||||
return this._timelineStore.add(entry);
|
this._timelineStore.add(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Updates the entry into the store with the given [roomId, eventKey] combination.
|
/** Updates the entry into the store with the given [roomId, eventKey] combination.
|
||||||
* If not yet present, will insert. Might be slower than add.
|
* If not yet present, will insert. Might be slower than add.
|
||||||
* @param entry the entry to update.
|
* @param entry the entry to update.
|
||||||
* @return a promise resolving to undefined if the operation was successful, or a StorageError if not.
|
* @return nothing. To wait for the operation to finish, await the transaction it's part of.
|
||||||
*/
|
*/
|
||||||
update(entry: StorageEntry): Promise<IDBValidKey> {
|
update(entry: StorageEntry): void {
|
||||||
return this._timelineStore.put(entry);
|
this._timelineStore.put(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
get(roomId: string, eventKey: EventKey): Promise<StorageEntry | null> {
|
get(roomId: string, eventKey: EventKey): Promise<StorageEntry | null> {
|
||||||
|
@ -289,10 +289,10 @@ export class TimelineEventStore {
|
||||||
return this._timelineStore.index("byEventId").get(encodeEventIdKey(roomId, eventId));
|
return this._timelineStore.index("byEventId").get(encodeEventIdKey(roomId, eventId));
|
||||||
}
|
}
|
||||||
|
|
||||||
removeAllForRoom(roomId: string): Promise<undefined> {
|
removeAllForRoom(roomId: string): void {
|
||||||
const minKey = encodeKey(roomId, KeyLimits.minStorageKey, KeyLimits.minStorageKey);
|
const minKey = encodeKey(roomId, KeyLimits.minStorageKey, KeyLimits.minStorageKey);
|
||||||
const maxKey = encodeKey(roomId, KeyLimits.maxStorageKey, KeyLimits.maxStorageKey);
|
const maxKey = encodeKey(roomId, KeyLimits.maxStorageKey, KeyLimits.maxStorageKey);
|
||||||
const range = this._timelineStore.IDBKeyRange.bound(minKey, maxKey);
|
const range = this._timelineStore.IDBKeyRange.bound(minKey, maxKey);
|
||||||
return this._timelineStore.delete(range);
|
this._timelineStore.delete(range);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,13 +74,13 @@ export class TimelineFragmentStore {
|
||||||
// should generate an id an return it?
|
// should generate an id an return it?
|
||||||
// depends if we want to do anything smart with fragment ids,
|
// depends if we want to do anything smart with fragment ids,
|
||||||
// like give them meaning depending on range. not for now probably ...
|
// like give them meaning depending on range. not for now probably ...
|
||||||
add(fragment: Fragment): Promise<IDBValidKey> {
|
add(fragment: Fragment): void {
|
||||||
(fragment as any).key = encodeKey(fragment.roomId, fragment.id);
|
(fragment as FragmentEntry).key = encodeKey(fragment.roomId, fragment.id);
|
||||||
return this._store.add(fragment as FragmentEntry);
|
this._store.add(fragment as FragmentEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
update(fragment: FragmentEntry): Promise<IDBValidKey> {
|
update(fragment: FragmentEntry): void {
|
||||||
return this._store.put(fragment);
|
this._store.put(fragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
get(roomId: string, fragmentId: number): Promise<FragmentEntry | null> {
|
get(roomId: string, fragmentId: number): Promise<FragmentEntry | null> {
|
||||||
|
|
|
@ -39,8 +39,8 @@ export class TimelineRelationStore {
|
||||||
this._store = store;
|
this._store = store;
|
||||||
}
|
}
|
||||||
|
|
||||||
add(roomId: string, targetEventId: string, relType: string, sourceEventId: string): Promise<IDBValidKey> {
|
add(roomId: string, targetEventId: string, relType: string, sourceEventId: string): void {
|
||||||
return this._store.add({key: encodeKey(roomId, targetEventId, relType, sourceEventId)});
|
this._store.add({key: encodeKey(roomId, targetEventId, relType, sourceEventId)});
|
||||||
}
|
}
|
||||||
|
|
||||||
remove(roomId: string, targetEventId: string, relType: string, sourceEventId: string): Promise<undefined> {
|
remove(roomId: string, targetEventId: string, relType: string, sourceEventId: string): Promise<undefined> {
|
||||||
|
|
|
@ -32,8 +32,8 @@ export class UserIdentityStore {
|
||||||
return this._store.get(userId);
|
return this._store.get(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
set(userIdentity: UserIdentity): Promise<IDBValidKey> {
|
set(userIdentity: UserIdentity): void {
|
||||||
return this._store.put(userIdentity);
|
this._store.put(userIdentity);
|
||||||
}
|
}
|
||||||
|
|
||||||
remove(userId: string): Promise<undefined> {
|
remove(userId: string): Promise<undefined> {
|
||||||
|
|
|
@ -71,12 +71,19 @@ type CreateObjectStore = (db : IDBDatabase, txn: IDBTransaction | null, oldVersi
|
||||||
|
|
||||||
export function openDatabase(name: string, createObjectStore: CreateObjectStore, version: number, idbFactory: IDBFactory = window.indexedDB): Promise<IDBDatabase> {
|
export function openDatabase(name: string, createObjectStore: CreateObjectStore, version: number, idbFactory: IDBFactory = window.indexedDB): Promise<IDBDatabase> {
|
||||||
const req = idbFactory.open(name, version);
|
const req = idbFactory.open(name, version);
|
||||||
req.onupgradeneeded = (ev : IDBVersionChangeEvent) => {
|
req.onupgradeneeded = async (ev : IDBVersionChangeEvent) => {
|
||||||
const req = ev.target as IDBRequest<IDBDatabase>;
|
const req = ev.target as IDBRequest<IDBDatabase>;
|
||||||
const db = req.result;
|
const db = req.result;
|
||||||
const txn = req.transaction;
|
const txn = req.transaction!;
|
||||||
const oldVersion = ev.oldVersion;
|
const oldVersion = ev.oldVersion;
|
||||||
createObjectStore(db, txn, oldVersion, version);
|
try {
|
||||||
|
await createObjectStore(db, txn, oldVersion, version);
|
||||||
|
} catch (err) {
|
||||||
|
// try aborting on error, if that hasn't been done already
|
||||||
|
try {
|
||||||
|
txn.abort();
|
||||||
|
} catch (err) {}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
return reqAsPromise(req);
|
return reqAsPromise(req);
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
export type Content = { [key: string]: any }
|
export type Content = { [key: string]: any }
|
||||||
|
|
||||||
export interface RoomEvent {
|
export interface TimelineEvent {
|
||||||
content: Content;
|
content: Content;
|
||||||
type: string;
|
type: string;
|
||||||
event_id: string;
|
event_id: string;
|
||||||
|
@ -25,4 +25,4 @@ export interface RoomEvent {
|
||||||
unsigned?: Content;
|
unsigned?: Content;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type StateEvent = RoomEvent & { prev_content?: Content, state_key: string }
|
export type StateEvent = TimelineEvent & { prev_content?: Content, state_key: string }
|
||||||
|
|
|
@ -16,7 +16,8 @@ limitations under the License.
|
||||||
|
|
||||||
import {FDBFactory, FDBKeyRange} from "../../lib/fake-indexeddb/index.js";
|
import {FDBFactory, FDBKeyRange} from "../../lib/fake-indexeddb/index.js";
|
||||||
import {StorageFactory} from "../matrix/storage/idb/StorageFactory";
|
import {StorageFactory} from "../matrix/storage/idb/StorageFactory";
|
||||||
|
import {NullLogItem} from "../logging/NullLogger.js";
|
||||||
|
|
||||||
export function createMockStorage() {
|
export function createMockStorage() {
|
||||||
return new StorageFactory(null, new FDBFactory(), FDBKeyRange).create(1);
|
return new StorageFactory(null, new FDBFactory(), FDBKeyRange).create(1, new NullLogItem());
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue