git » sdk » commit 62d6118

Replace message details if we get the original again

author Eric Roberts
2026-09-28 14:06:00 UTC
committer Stephen Paul Weber
2026-09-28 14:08:30 UTC
parent 875abe97d19c0ec5400dadbb8a01eb856af71b2f

Replace message details if we get the original again

This initially surfaced as a sort issue, when a mam would run and give
us messages we already received, and that message had been edited, we
would fall through to the correct message path, which would preserve the
sortId of the original message in the database. But if we're actually
getting exactly the same message again we should use the details of that
message.

This only happened in the correction path because if we receive a
duplicate message that isn't an edited message (has no versions) then we
delete the original and persist the new one.

borogove/persistence/IDB.js +28 -14
test/persistence-tests.ts +54 -0

diff --git a/borogove/persistence/IDB.js b/borogove/persistence/IDB.js
index 3e68e95..48b399d 100644
--- a/borogove/persistence/IDB.js
+++ b/borogove/persistence/IDB.js
@@ -610,24 +610,25 @@ export default async (dbname, media, tokenize, stemmer) => {
 			)
 			.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
 		const head = { ...versions[0] };
+		const outer = getOuter(result.value, message);
 		// Can't change primary key
-		head.serverIdBy = result.value.serverIdBy;
-		head.serverId = result.value.serverId;
-		head.localId = result.value.localId;
-		head.replyId = result.value.replyId;
+		head.serverIdBy = outer.serverIdBy;
+		head.serverId = outer.serverId;
+		head.localId = outer.localId;
+		head.replyId = outer.replyId;
 		// Edited version is not newer
-		head.timestamp = result.value.timestamp;
-		head.sortId = result.value.sortId;
+		head.timestamp = new Date(outer.timestamp);
+		head.sortId = outer.sortId;
 		head.versions = versions;
-		head.reactions = result.value.reactions; // Preserve these, edit doesn't touch them
+		head.reactions = outer.reactions; // Preserve these, edit doesn't touch them
 		// Calls can "edit" from multiple senders, but the original direction and sender holds
-		if (result.value.type === enums.borogove_MessageType.MessageCall) {
-			head.direction = result.value.direction;
-			head.senderId = result.value.senderId;
-			head.from = result.value.from;
-			head.to = result.value.to;
-			head.replyTo = result.value.replyTo;
-			head.recipients = result.value.recipients;
+		if (outer.type === enums.borogove_MessageType.MessageCall) {
+			head.direction = outer.direction;
+			head.senderId = outer.senderId;
+			head.from = outer.from;
+			head.to = outer.to;
+			head.replyTo = outer.replyTo;
+			head.recipients = outer.recipients;
 		}
 		result.update(head);
 		if (!message.isIncoming()) {
@@ -638,6 +639,19 @@ export default async (dbname, media, tokenize, stemmer) => {
 		return head;
 	}
 
+	function getOuter(original, possibleReplacement) {
+		if (!original.serverId) return original;
+
+		if (
+			original.serverId === possibleReplacement.serverId &&
+			original.serverIdBy === possibleReplacement.serverIdBy
+		) {
+			return possibleReplacement;
+		}
+
+		return original;
+	}
+
 	function setReactions(reactionsMap, sender, reactions) {
 		for (const [reaction, reacts] of reactionsMap) {
 			const newReacts = reacts.filter((react) => react.senderId !== sender);
diff --git a/test/persistence-tests.ts b/test/persistence-tests.ts
index 7577243..a3d15ff 100644
--- a/test/persistence-tests.ts
+++ b/test/persistence-tests.ts
@@ -515,6 +515,60 @@ export function sharedPersistenceTests(test) {
 		}
 	});
 
+	test("updates sortId when replaying an original after its correction", async ({
+		borogove,
+		persistence,
+	}) => {
+		const account = "correction-sort-alice@example.com";
+		const chatId = "correction-sort-hatter@example.com";
+		const originalBuilder = new borogove.ChatMessageBuilder({
+			serverId: "archive-original",
+			serverIdBy: chatId,
+			localId: "correction-sort-original",
+			senderId: chatId,
+			direction: 0,
+			timestamp: "2026-08-26T12:00:00Z",
+		});
+		originalBuilder.sortId = "b0";
+		originalBuilder.text = "Original text";
+		originalBuilder.to = borogove.JID.parse(account);
+		originalBuilder.from = borogove.JID.parse(chatId);
+		originalBuilder.recipients = [originalBuilder.to];
+		originalBuilder.replyTo = [originalBuilder.from];
+		const original = originalBuilder.build();
+
+		const correctionBuilder = new borogove.ChatMessageBuilder({
+			serverId: "archive-correction",
+			serverIdBy: chatId,
+			localId: "correction-sort-correction",
+			senderId: chatId,
+			direction: 0,
+			timestamp: "2026-08-26T12:01:00Z",
+		});
+		correctionBuilder.sortId = "b0";
+		correctionBuilder.text = "Corrected text";
+		correctionBuilder.to = borogove.JID.parse(account);
+		correctionBuilder.from = borogove.JID.parse(chatId);
+		correctionBuilder.recipients = [correctionBuilder.to];
+		correctionBuilder.replyTo = [correctionBuilder.from];
+		const correctionVersion = correctionBuilder.build();
+		correctionBuilder.versions = [correctionVersion];
+		correctionBuilder.localId = original.localId;
+
+		await persistence.storeMessages(account, [original]);
+		await persistence.storeMessages(account, [correctionBuilder.build()]);
+
+		originalBuilder.sortId = "a0";
+		const [stored] = await persistence.storeMessages(account, [
+			originalBuilder.build(),
+		]);
+
+		await persistence.getMessage(account, chatId, original.serverId, null);
+
+		expect(stored.text).toBe("Corrected text");
+		expect(stored.sortId).toBe("a0");
+	});
+
 	test("storeReaction", async ({ borogove, persistence }) => {
 		const builder = new borogove.ChatMessageBuilder({
 			serverId: "srv1",