| author | Stephen Paul Weber
<singpolyma@singpolyma.net> 2026-09-28 14:54:49 UTC |
| committer | Stephen Paul Weber
<singpolyma@singpolyma.net> 2026-09-28 14:54:49 UTC |
| parent | 62d6118aefb01b1cbfbfa6a3fc97ec440a58347d |
| 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",