| author | Stephen Paul Weber
<singpolyma@singpolyma.net> 2026-05-11 14:52:26 UTC |
| committer | Stephen Paul Weber
<singpolyma@singpolyma.net> 2026-05-11 14:52:26 UTC |
| parent | 227942df71a3453d4bcde621128592f0386f19c0 |
| 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"); + }); });