git » sdk » commit ad623fb

Test and fix sqlite blobs on js

author Stephen Paul Weber
2026-05-11 14:52:26 UTC
committer Stephen Paul Weber
2026-05-11 14:52:26 UTC
parent 227942df71a3453d4bcde621128592f0386f19c0

Test and fix sqlite blobs on js

borogove/Persistence.hx +4 -2
borogove/persistence/Dummy.hx +6 -2
borogove/persistence/IDB.js +7 -5
borogove/persistence/Sqlite.hx +12 -9
borogove/persistence/SqliteDriver.js.hx +14 -2
test/TestSqlite.hx +13 -0
test/idb.spec.ts +26 -5
test/sqlite.spec.ts +56 -0

diff --git a/borogove/Persistence.hx b/borogove/Persistence.hx
index febb2b3..e81d410 100644
--- a/borogove/Persistence.hx
+++ b/borogove/Persistence.hx
@@ -186,8 +186,9 @@ interface Persistence {
 		@param clientId negotiated client ID
 		@param displayName last known display name
 		@param token persisted token or null to clear it
+		@returns Promise resolving to true when store succeeded
 	**/
-	public function storeLogin(accountId:String, clientId:String, displayName:String, token:Null<String>):Void;
+	public function storeLogin(accountId:String, clientId:String, displayName:String, token:Null<String>): Promise<Bool>;
 
 	/**
 		Load persisted login-related state for an account
@@ -220,9 +221,10 @@ interface Persistence {
 		@param accountId the account to store resumption data for
 		@param data stream management payload, or null to clear it
 		@param sortId highest sortId ever seen by this stream
+		@returns Promise resolving to true when store succeeded
 	**/
 	@HaxeCBridge.noemit
-	public function storeStreamManagement(accountId:String, data:Null<BytesData>, sortId: String):Void;
+	public function storeStreamManagement(accountId:String, data:Null<BytesData>, sortId: String): Promise<Bool>;
 
 	/**
 		Load stream management resumption data for an account
diff --git a/borogove/persistence/Dummy.hx b/borogove/persistence/Dummy.hx
index 425df28..4fc55aa 100644
--- a/borogove/persistence/Dummy.hx
+++ b/borogove/persistence/Dummy.hx
@@ -110,7 +110,9 @@ class Dummy implements Persistence {
 	}
 
 	@HaxeCBridge.noemit
-	public function storeLogin(login:String, clientId:String, displayName:String, token:Null<String>) { }
+	public function storeLogin(login:String, clientId:String, displayName:String, token:Null<String>): Promise<Bool> {
+		return Promise.resolve(false);
+	}
 
 	@HaxeCBridge.noemit
 	public function getLogin(login:String): Promise<{ clientId:Null<String>, token:Null<String>, fastCount: Int, displayName:Null<String> }> {
@@ -128,7 +130,9 @@ class Dummy implements Persistence {
 	}
 
 	@HaxeCBridge.noemit
-	public function storeStreamManagement(accountId:String, sm:Null<BytesData>, sortId: String) { }
+	public function storeStreamManagement(accountId:String, sm:Null<BytesData>, sortId: String): Promise<Bool> {
+		return Promise.resolve(false);
+	}
 
 	@HaxeCBridge.noemit
 	public function getStreamManagement(accountId:String): Promise<{ sm: Null<BytesData>, sortId: String }> {
diff --git a/borogove/persistence/IDB.js b/borogove/persistence/IDB.js
index fa69d80..0b6984c 100644
--- a/borogove/persistence/IDB.js
+++ b/borogove/persistence/IDB.js
@@ -875,15 +875,16 @@ export default async (dbname, media, tokenize, stemmer) => {
 			return null;
 		},
 
-		storeLogin: function(login, clientId, displayName, token) {
+		async storeLogin(login, clientId, displayName, token) {
 			const tx = db.transaction(["keyvaluepairs"], "readwrite");
 			const store = tx.objectStore("keyvaluepairs");
-			store.put(clientId, "login:clientId:" + login).onerror = console.error;
-			store.put(displayName, "fn:" + login).onerror = console.error;
+			await promisifyRequest(store.put(clientId, "login:clientId:" + login));
+			await promisifyRequest(store.put(displayName, "fn:" + login));
 			if (token != null) {
-				store.put(token, "login:token:" + login).onerror = console.error;
-				store.put(0, "login:fastCount:" + login).onerror = console.error;
+				await promisifyRequest(store.put(token, "login:token:" + login));
+				await promisifyRequest(store.put(0, "login:fastCount:" + login));
 			}
+			return true;
 		},
 
 		storeOmemoId: function(account, omemoId) {
@@ -1003,6 +1004,7 @@ export default async (dbname, media, tokenize, stemmer) => {
 				await promisifyRequest(store.put(sm, "sm:" + account)),
 				await promisifyRequest(store.put(sortId, "sortId:" + account))
 			]);
+			return true;
 		},
 
 		async getStreamManagement(account) {
diff --git a/borogove/persistence/Sqlite.hx b/borogove/persistence/Sqlite.hx
index 848d712..31bc71c 100644
--- a/borogove/persistence/Sqlite.hx
+++ b/borogove/persistence/Sqlite.hx
@@ -56,12 +56,11 @@ class Sqlite implements Persistence implements KeyValueStore {
 					Std.string(p);
 				case TNull:
 					"NULL";
-				case TClass(Array):
-					var bytes:Bytes = Bytes.ofData(p);
+				case TClass(BytesData):
+					var bytes = Bytes.ofData(p);
 					"X'" + bytes.toHex() + "'";
 				case TClass(haxe.io.Bytes):
-					var bytes:Bytes = cast p;
-					"X'" + bytes.toHex() + "'";
+					"X'" + p.toHex() + "'";
 				case _:
 					throw("UKNONWN: " + Type.typeof(p));
 			}
@@ -702,7 +701,7 @@ class Sqlite implements Persistence implements KeyValueStore {
 	}
 
 	@HaxeCBridge.noemit
-	public function storeLogin(accountId:String, clientId:String, displayName:String, token:Null<String>) {
+	public function storeLogin(accountId:String, clientId:String, displayName:String, token:Null<String>): Promise<Bool> {
 		final params = [accountId, clientId, displayName];
 		final q = new StringBuf();
 		q.add("INSERT INTO accounts (account_id, client_id, display_name");
@@ -724,7 +723,7 @@ class Sqlite implements Persistence implements KeyValueStore {
 			params.push(token);
 			q.add(", fast_count=0"); // reset count to zero on new token
 		}
-		db.exec(q.toString(), params);
+		return db.exec(q.toString(), params).then(_ -> true);
 	}
 
 	@HaxeCBridge.noemit
@@ -784,14 +783,18 @@ class Sqlite implements Persistence implements KeyValueStore {
 		smStoreIdNext = sortId;
 		if (!smStoreInProgress) {
 			smStoreInProgress = true;
-			db.exec(
+			return db.exec(
 				"UPDATE accounts SET sm_state=?, sort_id=? WHERE account_id=?",
 				[sm, sortId, accountId]
 			).then(_ -> {
 				smStoreInProgress = false;
-				if (smStoreNext != sm || smStoreIdNext != sortId) storeStreamManagement(accountId, sm, sortId);
-			});
+				if (smStoreNext != sm || smStoreIdNext != sortId) storeStreamManagement(accountId, smStoreNext, smStoreIdNext);
+				return null;
+			}).then(_ -> Promise.resolve(true));
 		}
+
+		// Hmm, we're not really done yet?
+		return Promise.resolve(true);
 	}
 
 	@HaxeCBridge.noemit
diff --git a/borogove/persistence/SqliteDriver.js.hx b/borogove/persistence/SqliteDriver.js.hx
index 4dd192f..22845fb 100644
--- a/borogove/persistence/SqliteDriver.js.hx
+++ b/borogove/persistence/SqliteDriver.js.hx
@@ -33,7 +33,15 @@ class SqliteDriver {
 				for (const q of qs) {
 					db.exec(q);
 				}
-				parentPort.postMessage({ id, result: db.prepare(lastQ).all() });
+				const result = db.prepare(lastQ).all().map(row => {
+					for (const k of Object.keys(row)) {
+						// NodeJS sqlite produces Uin8Array for blob
+						// but Haxe expects ArrayBuffer for BytesData
+						if (row[k] instanceof Uint8Array) row[k] = row[k].buffer;
+					}
+					return row;
+				});
+				parentPort.postMessage({ id, result });
 				if (qs.length > 0) db.exec("COMMIT");
 			} catch (error) {
 				if (qs.length > 0) db.exec("ROLLBACK");
@@ -151,7 +159,11 @@ class SqliteDriver {
 				if (r.rowNumber == null) {
 					signalAllDone(null);
 				} else {
-					items.push(r.row);
+					final row: haxe.DynamicAccess<Dynamic> = r.row;
+					for (k => v in row) {
+						if (Std.isOfType(v, js.lib.Uint8Array)) row[k] = row[k].buffer;
+					}
+					items.push(row);
 				}
 				null;
 			}
diff --git a/test/TestSqlite.hx b/test/TestSqlite.hx
index 6f4c405..d7be968 100644
--- a/test/TestSqlite.hx
+++ b/test/TestSqlite.hx
@@ -813,4 +813,17 @@ class TestSqlite extends utest.Test {
 			});
 		}, 200);
 	}
+
+	public function testStoreStreamManamagementAndGetStreamManagement(async: Async) {
+		persistence.storeLogin("alice@example.com", "", "", null).then(_ ->
+			persistence.storeStreamManagement("alice@example.com", Bytes.ofHex("01020004").getData(), "ZZ")
+		).then(_ ->
+			persistence.getStreamManagement("alice@example.com")
+		).then(result -> {
+			Assert.equals(Bytes.ofData(result.sm).toHex(), "01020004");
+			Assert.isTrue(Std.isOfType(result.sm, BytesData), "Should be BytesData");
+			Assert.equals(result.sortId, "ZZ");
+			async.done();
+		});
+	}
 }
diff --git a/test/idb.spec.ts b/test/idb.spec.ts
index 0194cf9..dee0f72 100644
--- a/test/idb.spec.ts
+++ b/test/idb.spec.ts
@@ -1055,11 +1055,8 @@ test("storeChats and getChats with status", async ({ page }) => {
 		const borogove = await import(URL.createObjectURL(blob));
 
 		const mediaStore =
-			await borogove.persistence.MediaStoreCache("snikket_status");
-		const persistence = await borogove.persistence.IDB(
-			"snikket_status",
-			mediaStore,
-		);
+			await borogove.persistence.MediaStoreCache("snikket");
+		const persistence = await borogove.persistence.IDB("snikket", mediaStore);
 
 		const chat = Object.create(borogove.DirectChat.prototype);
 		chat.chatId = "hatter@example.com";
@@ -1084,3 +1081,27 @@ test("storeChats and getChats with status", async ({ page }) => {
 	expect(result.statusEmoji).toBe("🎩");
 	expect(result.statusText).toBe("Time for tea!");
 });
+
+test("storeStreamManamagement and getStreamManagement", 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);
+
+		await persistence.storeLogin("alice@example.com", "", "", null); // or updating with SM may not work
+		await persistence.storeStreamManagement("alice@example.com", new Uint8Array([1,2,0,4]).buffer, "ZZ");
+		const result = await persistence.getStreamManagement("alice@example.com");
+		return { smIsArrayBuffer: result.sm instanceof ArrayBuffer, smIsEq: result.sm ? indexedDB.cmp(result.sm, new Uint8Array([1,2,0,4]).buffer) : "null", sortId: result.sortId };
+	}, code);
+
+	expect(result.smIsEq).toBe(0);
+	expect(result.smIsArrayBuffer).toBe(true);
+	expect(result.sortId).toBe("ZZ");
+});
diff --git a/test/sqlite.spec.ts b/test/sqlite.spec.ts
index 1243ad6..4213d63 100644
--- a/test/sqlite.spec.ts
+++ b/test/sqlite.spec.ts
@@ -1559,4 +1559,60 @@ test.describe("not webkit", () => {
 		expect(result.statusEmoji).toBe("🎩");
 		expect(result.statusText).toBe("Time for tea!");
 	});
+
+	test("storeStreamManamagement and getStreamManagement", async ({ page }) => {
+		page.route("https://localhost/", (route) =>
+			route.fulfill({
+				body: "<html></html>",
+				headers: {
+					"Cross-Origin-Opener-Policy": "same-origin",
+					"Cross-Origin-Embedder-Policy": "same-origin",
+					"Cross-Origin-Resource-Policy": "same-origin",
+				},
+			}),
+		);
+		const code = fs.readFileSync("playwright/.cache/borogove.js", "utf8");
+		const sqlite = fs.readFileSync("playwright/.cache/sqlite-wasm.js", "utf8");
+		const worker1 = fs.readFileSync(
+			"playwright/.cache/sqlite-worker1.js",
+			"utf8",
+		);
+		await page.goto("https://localhost/");
+		const result = await page.evaluate(
+			async ([code, sqliteCode, worker1Code]) => {
+				const borogove = await import(
+					URL.createObjectURL(new Blob([code], { type: "text/javascript" }))
+				);
+				const sqlite = await import(
+					URL.createObjectURL(
+						new Blob([sqliteCode], { type: "text/javascript" }),
+					)
+				);
+				window.sqliteWorker1Url = new URL(
+					URL.createObjectURL(
+						new Blob([worker1Code], { type: "text/javascript" }),
+					),
+				);
+				const persistence = new sqlite.borogove_persistence_Sqlite(
+					"snikket",
+					await borogove.persistence.MediaStoreCache("snikket"),
+				);
+
+				try {
+					await persistence.storeLogin("alice@example.com", "", "", null); // or updating with SM may not work
+					await persistence.storeStreamManagement("alice@example.com", new Uint8Array([1,2,0,4]).buffer, "ZZ");
+					const result = await persistence.getStreamManagement("alice@example.com");
+					return { smIsArrayBuffer: result.sm instanceof ArrayBuffer, smIsEq: result.sm ? indexedDB.cmp(result.sm, new Uint8Array([1,2,0,4]).buffer) : "null", sortId: result.sortId };
+				} catch (e) {
+					console.error(e, e.result);
+					throw e.result ? JSON.stringify(e.result) : e.message;
+				}
+			},
+			[code, sqlite, worker1],
+		);
+
+		expect(result.smIsEq).toBe(0);
+		expect(result.smIsArrayBuffer).toBe(true);
+		expect(result.sortId).toBe("ZZ");
+	});
 });