1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-07-31 15:24:23 +03:00

Add functions to assist in immutability of Event objects

This commit is contained in:
Travis Ralston
2021-06-07 20:08:00 -06:00
parent 9f6ed4fb33
commit 13c9c4bea5
4 changed files with 156 additions and 1 deletions

View File

@ -0,0 +1,60 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
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 { MatrixEvent } from "../../../src/models/event";
describe('MatrixEvent', () => {
it('should create copies of itself', () => {
const a = new MatrixEvent({
type: "com.example.test",
content: {
isTest: true,
num: 42,
},
});
const clone = a.getSnapshotCopy();
expect(clone).toBeDefined();
expect(clone).not.toBe(a);
expect(clone.event).not.toBe(a.event);
expect(clone.event).toMatchObject(a.event);
// The other properties we're not super interested in, honestly.
});
it('should compare itself to other events using json', () => {
const a = new MatrixEvent({
type: "com.example.test",
content: {
isTest: true,
num: 42,
},
});
const b = new MatrixEvent({
type: "com.example.test______B",
content: {
isTest: true,
num: 42,
},
});
expect(a.isEquivalentTo(b)).toBe(false);
expect(a.isEquivalentTo(a)).toBe(true);
expect(b.isEquivalentTo(a)).toBe(false);
expect(b.isEquivalentTo(b)).toBe(true);
expect(a.getSnapshotCopy().isEquivalentTo(a)).toBe(true);
expect(a.getSnapshotCopy().isEquivalentTo(b)).toBe(false);
});
});

View File

@ -2,7 +2,7 @@ import * as utils from "../../src/utils";
import {
alphabetPad,
averageBetweenStrings,
baseToString,
baseToString, deepSortedObjectEntries,
DEFAULT_ALPHABET,
lexicographicCompare,
nextString,
@ -429,4 +429,38 @@ describe("utils", function() {
expect(lexicographicCompare('a', 'A') > 0).toBe(true);
});
});
describe('deepSortedObjectEntries', () => {
it('should auto-return non-objects', () => {
expect(deepSortedObjectEntries(42)).toEqual(42);
expect(deepSortedObjectEntries("not object")).toEqual("not object");
expect(deepSortedObjectEntries(true)).toEqual(true);
expect(deepSortedObjectEntries([42])).toEqual([42]);
expect(deepSortedObjectEntries(null)).toEqual(null);
expect(deepSortedObjectEntries(undefined)).toEqual(undefined);
});
it('should sort objects appropriately', () => {
const input = {
a: 42,
b: {
d: {},
a: "test",
b: "alpha",
},
[72]: "test",
};
const output = [
["72", "test"],
["a", 42],
["b", [
["a", "test"],
["b", "alpha"],
["d", []],
]],
];
expect(deepSortedObjectEntries(input)).toMatchObject(output);
});
});
});

View File

@ -24,6 +24,7 @@ limitations under the License.
import { EventEmitter } from 'events';
import * as utils from '../utils';
import { logger } from '../logger';
import { deepSortedObjectEntries } from "../utils";
/**
* Enum for event statuses.
@ -1143,6 +1144,41 @@ utils.extend(MatrixEvent.prototype, {
getTxnId() {
return this._txnId;
},
/**
* Get a copy/snapshot of this event. The returned copy will be loosely linked
* back to this instance, though will have "frozen" event information. Other
* properties may mutate depending on the state of this instance at the time
* of snapshotting.
*
* This is meant to be used to snapshot the event details themselves, not the
* features (such as sender) surrounding the event.
* @returns {MatrixEvent} A snapshot of this event.
*/
getSnapshotCopy() {
const ev = new MatrixEvent(JSON.parse(JSON.stringify(this.event)));
for (const [p, v] of Object.entries(this)) {
if (p !== "event") { // exclude the thing we just cloned
ev[p] = v;
}
}
return ev;
},
/**
* Determines if this event is equivalent to the given event. This only checks
* the event object itself, not the other properties of the event. Intended for
* use with getSnapshotCopy() to identify events changing.
* @param {MatrixEvent} otherEvent The other event to check against.
* @returns {boolean} True if the events are the same, false otherwise.
*/
isEquivalentTo(otherEvent) {
if (!otherEvent) return false;
if (otherEvent === this) return true;
const myProps = deepSortedObjectEntries(this.event);
const theirProps = deepSortedObjectEntries(otherEvent.event);
return JSON.stringify(myProps) === JSON.stringify(theirProps);
},
});
/* _REDACT_KEEP_KEY_MAP gives the keys we keep when an event is redacted

View File

@ -235,6 +235,31 @@ export function deepCompare(x: any, y: any): boolean {
return true;
}
/**
* Creates an array of object properties/values (entries) then
* sorts the result by key, recursively. The input object must
* ensure it does not have loops. If the input is not an object
* then it will be returned as-is.
* @param {*} obj The object to get entries of
* @returns {[string, *][]} The entries, sorted by key.
*/
export function deepSortedObjectEntries(obj: any): [string, any][] {
if (typeof(obj) !== "object") return obj;
// Apparently these are object types...
if (obj === null || obj === undefined || Array.isArray(obj)) return obj;
const pairs: [string, any][] = [];
for (const [k, v] of Object.entries(obj)) {
pairs.push([k, deepSortedObjectEntries(v)]);
}
// lexicographicCompare is faster than localeCompare, so let's use that.
pairs.sort((a, b) => lexicographicCompare(a[0], b[0]));
return pairs;
}
/**
* Copy properties from one object to another.
*