git » sdk » commit 2e4f56f

Dedupe message versions

author Stephen Paul Weber
2026-09-28 14:54:49 UTC
committer Stephen Paul Weber
2026-09-28 14:54:49 UTC
parent 62d6118aefb01b1cbfbfa6a3fc97ec440a58347d

Dedupe message versions

And make sure we keep the "new" one if we're replacing one

borogove/persistence/IDB.js +18 -15
borogove/persistence/Sqlite.hx +3 -0
test/persistence-tests.ts +55 -0

diff --git a/borogove/persistence/IDB.js b/borogove/persistence/IDB.js
index 48b399d..97eb0d6 100644
--- a/borogove/persistence/IDB.js
+++ b/borogove/persistence/IDB.js
@@ -594,21 +594,24 @@ export default async (dbname, media, tokenize, stemmer) => {
 
 	function correctMessage(account, message, result) {
 		// Newest (by timestamp) version wins for head
-		const newVersions =
-			message.versions.length < 1 ? [message] : message.versions;
-		const storedVersions = result.value.versions || [];
-		// TODO: dedupe? There shouldn't be dupes...
-		const versions = (
-			storedVersions.length < 1 ? [result.value] : storedVersions
-		)
-			.concat(
-				newVersions
-					.filter(
-						(nv) => !storedVersions.find((sv) => nv.serverId === sv.serverId),
-					)
-					.map((nv) => serializeMessage(account, nv)),
-			)
-			.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
+		const newVersions = (
+			message.versions.length < 1 ? [message] : message.versions
+		).map((nv) => serializeMessage(account, nv));
+		const storedVersions =
+			(result.value.versions || []).length < 1
+				? [result.value]
+				: result.value.versions;
+		const dedup = {};
+		const versions = [];
+		for (const version of newVersions.concat(storedVersions)) {
+			if (version.serverId) {
+				const key = version.serverId + "\n" + version.serverIdBy;
+				if (dedup[key]) continue;
+				dedup[key] = true;
+			}
+			versions.push(version);
+		}
+		versions.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
 		const head = { ...versions[0] };
 		const outer = getOuter(result.value, message);
 		// Can't change primary key
diff --git a/borogove/persistence/Sqlite.hx b/borogove/persistence/Sqlite.hx
index 1aea032..83a4b71 100644
--- a/borogove/persistence/Sqlite.hx
+++ b/borogove/persistence/Sqlite.hx
@@ -1542,6 +1542,7 @@ class Sqlite implements Persistence implements KeyValueStore {
 			if (row.versions != null) {
 				final versions: DynamicAccess<{
 					timestamp: String,
+					sort_id: String,
 					stanza: String,
 					encryption: Dynamic,
 					debug: Dynamic,
@@ -1552,6 +1553,7 @@ class Sqlite implements Persistence implements KeyValueStore {
 						final versionM = ChatMessage.fromStanza(Stanza.parse(version.stanza), accountJid, (toPushB, _) -> {
 							if (toPushB.serverId == null && versionId != toPushB.localId) toPushB.serverId = versionId;
 							toPushB.timestamp = version.timestamp;
+							toPushB.sortId = version.sort_id;
 							toPushB.debug = version.debug;
 							return toPushB;
 						}, hydrateEncryption(version.encryption));
@@ -1838,6 +1840,7 @@ class Sqlite implements Persistence implements KeyValueStore {
 			END,
 			json_object(
 				'timestamp', strftime('%FT%H:%M:%fZ', versions.created_at / 1000.0, 'unixepoch'),
+				'sort_id', versions.sort_id,
 				'stanza', versions.stanza,
 				'encryption', json(versions.encryption),
 				'debug', json(versions.debug)
diff --git a/test/persistence-tests.ts b/test/persistence-tests.ts
index a3d15ff..a35a3e7 100644
--- a/test/persistence-tests.ts
+++ b/test/persistence-tests.ts
@@ -569,6 +569,61 @@ export function sharedPersistenceTests(test) {
 		expect(stored.sortId).toBe("a0");
 	});
 
+	test("dedupes versions when storing a 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 = "b1";
+		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.versions.length).toBe(2);
+		expect(stored.versions[1].serverId).toBe("archive-original");
+		expect(stored.versions[1].sortId).toBe("a0");
+	});
+
 	test("storeReaction", async ({ borogove, persistence }) => {
 		const builder = new borogove.ChatMessageBuilder({
 			serverId: "srv1",