git » sdk » commit df8340f

Find parent if we get it later

author Stephen Paul Weber
2026-04-19 20:35:12 UTC
committer Stephen Paul Weber
2026-04-19 20:54:15 UTC
parent 152b8abf0c21bdc1f15e4004d0e21c296abb3af4

Find parent if we get it later

If the parent was not in DB at storeMessage time, then we only have the
account and serverId (and maybe serverIdBy) but not the localId. But
this is still enough to find the parent with, so let's do that.

borogove/persistence/IDB.js +5 -1
test/idb.spec.ts +76 -0

diff --git a/borogove/persistence/IDB.js b/borogove/persistence/IDB.js
index d2f0361..f176f22 100644
--- a/borogove/persistence/IDB.js
+++ b/borogove/persistence/IDB.js
@@ -321,7 +321,11 @@ export default async (dbname, media, tokenize, stemmer) => {
 		const message = hydrateMessageSync(value);
 		const tx = db.transaction(["messages"], "readonly");
 		const store = tx.objectStore("messages");
-		const replyToMessage = value.replyToMessage && value.replyToMessage[1] !== message.serverId && value.replyToMessage[3] !== message.localId && await hydrateMessage((await promisifyRequest(store.openCursor(IDBKeyRange.only(value.replyToMessage))))?.value);
+		if (value.replyToMessage && !value.replyToMessage[2]) value.replyToMessage[2] = value.serverIdBy ?? value.chatId;
+		const range = value.replyToMessage && value.replyToMessage[1] !== message.serverId && value.replyToMessage[3] !== message.localId && (!value.replyToMessage[3] ?
+			IDBKeyRange.bound(value.replyToMessage.slice(0, 3), [...value.replyToMessage.slice(0, 3), []])
+			: IDBKeyRange.only(value.replyToMessage));
+		const replyToMessage = range && await hydrateMessage((await promisifyRequest(store.openCursor(range)))?.value);
 
 		message.replyToMessage = replyToMessage;
 		message.versions = await Promise.all((value.versions || []).map(hydrateMessage));
diff --git a/test/idb.spec.ts b/test/idb.spec.ts
index 7bafca0..6edd3de 100644
--- a/test/idb.spec.ts
+++ b/test/idb.spec.ts
@@ -774,3 +774,79 @@ test("media storage functions", async ({ page }) => {
 	expect(result.hasBefore).toBe(true);
 	expect(result.hasAfter).toBe(false);
 });
+
+test("hydrate message with incomplete replyToMessage", async ({ page }) => {
+	page.route("https://localhost/", route => route.fulfill({ body: "<html></html>" }));
+	const code = fs.readFileSync("playwright/.cache/borogove.js", "utf8");
+	await page.goto("https://localhost/");
+	const result = await page.evaluate(async (code) => {
+		const blob = new Blob([code], { type: 'text/javascript' });
+		const borogove = await import(URL.createObjectURL(blob));
+
+		const mediaStore = await borogove.persistence.MediaStoreCache("snikket");
+		const persistence = await borogove.persistence.IDB("snikket", mediaStore);
+
+		const builder = new borogove.ChatMessageBuilder({
+			serverId: "parent",
+			serverIdBy: "hatter@example.com",
+			localId: "loc1",
+			senderId: "hatter@example.com",
+			direction: 0,
+		});
+		builder.sortId = "a0";
+		builder.to = borogove.JID.parse("alice@example.com");
+		builder.from = borogove.JID.parse("hatter@example.com");
+		builder.recipients = [builder.to];
+		builder.replyTo = [builder.from];
+		const parentMsg = builder.build();
+
+		const builder2 = new borogove.ChatMessageBuilder({
+			serverId: "child",
+			serverIdBy: "hatter@example.com",
+			localId: "loc2",
+			senderId: "hatter@example.com",
+			direction: 0,
+		});
+		builder2.sortId = "a1";
+		builder2.to = borogove.JID.parse("alice@example.com");
+		builder2.from = borogove.JID.parse("hatter@example.com");
+		builder2.recipients = [builder2.to];
+		builder2.replyTo = [builder2.from];
+		builder2.replyToMessage = parentMsg;
+		const childMsg = builder2.build();
+
+		await persistence.storeMessages("alice@example.com", [parentMsg, childMsg]);
+
+		const db = await new Promise((resolve, reject) => {
+			const req = indexedDB.open("snikket");
+			req.onsuccess = () => resolve(req.result);
+			req.onerror = () => reject(req.error);
+		});
+		const tx = db.transaction(["messages"], "readwrite");
+		const store = tx.objectStore("messages");
+		const key = ["alice@example.com", "child", "hatter@example.com", "loc2"];
+		const rawChild = await new Promise((resolve) => {
+			const req = store.get(key);
+			req.onsuccess = () => resolve(req.result);
+		});
+
+		rawChild.replyToMessage = ["alice@example.com", "parent", "", ""];
+
+		await new Promise((resolve) => {
+			const req = store.put(rawChild);
+			req.onsuccess = () => resolve();
+		});
+		await new Promise((resolve) => {
+			tx.oncomplete = () => resolve();
+		});
+
+		const retrievedChild = await persistence.getMessage("alice@example.com", "hatter@example.com", "child", "loc2");
+		return {
+			hasReply: !!retrievedChild.replyToMessage,
+			replyServerId: retrievedChild.replyToMessage ? retrievedChild.replyToMessage.serverId : null
+		};
+	}, code);
+
+	expect(result.hasReply).toBe(true);
+	expect(result.replyServerId).toBe("parent");
+});