git » sdk » commit e67728c

Support roster versioning

author Stephen Paul Weber
2026-07-22 19:53:31 UTC
committer Stephen Paul Weber
2026-07-22 19:59:15 UTC
parent 04787b4066ad5833b98b6d2140d86735b8b109bb

Support roster versioning

borogove/Client.hx +18 -5
borogove/Persistence.hx +4 -3
borogove/persistence/Dummy.hx +3 -3
borogove/persistence/IDB.js +5 -3
borogove/persistence/Sqlite.hx +15 -7
test/TestSqlite.hx +3 -3

diff --git a/borogove/Client.hx b/borogove/Client.hx
index 8940aab..b4be2d6 100644
--- a/borogove/Client.hx
+++ b/borogove/Client.hx
@@ -117,6 +117,7 @@ class Client extends EventEmitter {
 	private var token: Null<String> = null;
 	private var fastCount: Null<Int> = null;
 	private var sortId: String = "a ";
+	private var rosterVer: Null<String> = null;
 	private final pendingCaps: Map<String, Array<(Null<Caps>)->Chat>> = [];
 	private final brokenAvatars: Map<String, JID> = [];
 
@@ -161,7 +162,7 @@ class Client extends EventEmitter {
 
 		stream.on("fast-token", (data) -> {
 			token = data.token;
-			persistence.storeLogin(this.jid.asBare().asString(), stream.clientId ?? this.jid.resource, displayName(), token);
+			persistence.storeLogin(this.jid.asBare().asString(), stream.clientId ?? this.jid.resource, displayName(), rosterVer, token);
 			return EventHandled;
 		});
 
@@ -295,7 +296,11 @@ class Client extends EventEmitter {
 
 			var roster = new RosterGet();
 			roster.handleResponse(stanza);
-			var items = roster.getResult();
+			final items = roster.getResult();
+			if (roster.ver != null) {
+				rosterVer = roster.ver;
+				persistence.storeLogin(this.accountId(), stream.clientId ?? jid.resource, displayName(), rosterVer, null);
+			}
 			if (items.length == 0) return IqNoResult;
 
 			final chatsToUpdate = [];
@@ -808,6 +813,7 @@ class Client extends EventEmitter {
 			return persistence.getLogin(accountId());
 		}).then(login -> {
 			token = login.token;
+			rosterVer = login.rosterVer;
 			fastCount = login.fastCount;
 			stream.clientId = login.clientId ?? ID.unique();
 			jid = jid.withResource(stream.clientId);
@@ -1000,7 +1006,7 @@ class Client extends EventEmitter {
 	private function updateDisplayName(fn: String) {
 		if (fn == null || fn == "" || fn == displayName()) return false;
 		_displayName = fn;
-		persistence.storeLogin(jid.asBare().asString(), stream.clientId ?? jid.resource, fn, null);
+		persistence.storeLogin(jid.asBare().asString(), stream.clientId ?? jid.resource, fn, rosterVer, null);
 		pingAllChannels(false);
 		return true;
 	}
@@ -1029,7 +1035,7 @@ class Client extends EventEmitter {
 	private function onConnected(data) { // Fired on connect or reconnect
 		if (data != null && data.jid != null) {
 			jid = JID.parse(data.jid);
-			if (stream.clientId == null && !jid.isBare()) persistence.storeLogin(jid.asBare().asString(), stream.clientId ?? jid.resource, displayName(), null);
+			if (stream.clientId == null && !jid.isBare()) persistence.storeLogin(jid.asBare().asString(), stream.clientId ?? jid.resource, displayName(), rosterVer, null);
 		}
 
 		if (data.resumed) {
@@ -2017,14 +2023,21 @@ class Client extends EventEmitter {
 	}
 
 	private function rosterGet() {
-		var rosterGet = new RosterGet();
+		var rosterGet = new RosterGet(rosterVer);
 		rosterGet.onFinished(() -> {
 			final chatsToUpdate = [];
+			// TODO: We should update all chats not in this list as not trusted.
+			// Special case for when it is empty but there was no <query/> child
+			// to do nothing is needed also.
 			for (item in rosterGet.getResult()) {
 				var chat = getDirectChat(item.jid, false);
 				chat.updateFromRoster(item);
 				chatsToUpdate.push(cast (chat, Chat));
 			}
+			if (rosterGet.ver != null) {
+				rosterVer = rosterGet.ver;
+				persistence.storeLogin(accountId(), stream.clientId ?? jid.resource, displayName(), rosterVer, null);
+			}
 			persistence.storeChats(accountId(), chatsToUpdate);
 			this.trigger("chats/update", chatsToUpdate);
 		});
diff --git a/borogove/Persistence.hx b/borogove/Persistence.hx
index 5964b43..4b79c98 100644
--- a/borogove/Persistence.hx
+++ b/borogove/Persistence.hx
@@ -260,10 +260,11 @@ interface Persistence {
 		@param accountId the account to store login state for
 		@param clientId negotiated client ID
 		@param displayName last known display name
-		@param token persisted token or null to clear it
+		@param rosterVer roster version string if known
+		@param token persisted token or null to keep the current value
 		@returns Promise resolving to true when store succeeded
 	**/
-	public function storeLogin(accountId:String, clientId:String, displayName:String, token:Null<String>): Promise<Bool>;
+	public function storeLogin(accountId:String, clientId:String, displayName:String, rosterVer: Null<String>, token:Null<String>): Promise<Bool>;
 
 	/**
 		Load persisted login-related state for an account
@@ -272,7 +273,7 @@ interface Persistence {
 		@returns Promise resolving to stored login data
 	**/
 	@HaxeCBridge.noemit
-	public function getLogin(accountId:String): Promise<{ clientId:Null<String>, token:Null<String>, fastCount: Int, displayName:Null<String> }>;
+	public function getLogin(accountId:String): Promise<{ clientId:Null<String>, token:Null<String>, fastCount: Int, displayName:Null<String>, rosterVer: Null<String> }>;
 
 	/**
 		Remove stored data for an account
diff --git a/borogove/persistence/Dummy.hx b/borogove/persistence/Dummy.hx
index 83950f6..2f94855 100644
--- a/borogove/persistence/Dummy.hx
+++ b/borogove/persistence/Dummy.hx
@@ -145,13 +145,13 @@ class Dummy implements Persistence {
 	}
 
 	@HaxeCBridge.noemit
-	public function storeLogin(login:String, clientId:String, displayName:String, token:Null<String>): Promise<Bool> {
+	public function storeLogin(login:String, clientId:String, displayName:String, rosterVer: Null<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> }> {
-		return Promise.resolve({ clientId: null, token: null, fastCount: 0, displayName: null });
+	public function getLogin(login:String): Promise<{ clientId:Null<String>, token:Null<String>, fastCount: Int, displayName:Null<String>, rosterVer: Null<String> }> {
+		return Promise.resolve({ clientId: null, token: null, fastCount: 0, displayName: null, rosterVer: null });
 	}
 
 	@HaxeCBridge.noemit
diff --git a/borogove/persistence/IDB.js b/borogove/persistence/IDB.js
index cef1d01..664065d 100644
--- a/borogove/persistence/IDB.js
+++ b/borogove/persistence/IDB.js
@@ -1112,11 +1112,12 @@ tx.onerror = console.error;
 			return null;
 		},
 
-		async storeLogin(login, clientId, displayName, token) {
+		async storeLogin(login, clientId, displayName, rosterVer, token) {
 			const tx = db.transaction(["keyvaluepairs"], "readwrite");
 			const store = tx.objectStore("keyvaluepairs");
 			await promisifyRequest(store.put(clientId, "login:clientId:" + login));
 			await promisifyRequest(store.put(displayName, "fn:" + login));
+			await promisifyRequest(store.put(rosterVer, "rosterVer:" + login));
 			if (token != null) {
 				await promisifyRequest(store.put(token, "login:token:" + login));
 				await promisifyRequest(store.put(0, "login:fastCount:" + login));
@@ -1246,7 +1247,7 @@ tx.onerror = console.error;
 			}
 		},
 
-		getLogin: function(login) {
+		getLogin(login) {
 			const tx = db.transaction(["keyvaluepairs"], "readwrite");
 			const store = tx.objectStore("keyvaluepairs");
 			return Promise.all([
@@ -1254,11 +1255,12 @@ tx.onerror = console.error;
 				promisifyRequest(store.get("login:token:" + login)),
 				promisifyRequest(store.get("login:fastCount:" + login)),
 				promisifyRequest(store.get("fn:" + login)),
+				promisifyRequest(store.get("rosterVer:" + login)),
 			]).then((result) => {
 				if (result[1]) {
 					store.put((result[2] || 0) + 1, "login:fastCount:" + login).onerror = console.error;
 				}
-				return { clientId: result[0], token: result[1], fastCount: result[2] || 0, displayName: result[3] };
+				return { clientId: result[0], token: result[1], fastCount: result[2] || 0, displayName: result[3], rosterVer: result[4] };
 			});
 		},
 
diff --git a/borogove/persistence/Sqlite.hx b/borogove/persistence/Sqlite.hx
index 24d11a9..771daca 100644
--- a/borogove/persistence/Sqlite.hx
+++ b/borogove/persistence/Sqlite.hx
@@ -262,6 +262,12 @@ class Sqlite implements Persistence implements KeyValueStore {
 						"PRAGMA user_version = 12"]);
 					}
 					return Promise.resolve(null);
+				}).then(_ -> {
+					if (version < 13) {
+						return exec(["ALTER TABLE accounts ADD COLUMN roster_ver TEXT",
+						"PRAGMA user_version = 13"]);
+					}
+					return Promise.resolve(null);
 				});
 			});
 		});
@@ -1057,14 +1063,14 @@ class Sqlite implements Persistence implements KeyValueStore {
 	}
 
 	@HaxeCBridge.noemit
-	public function storeLogin(accountId:String, clientId:String, displayName:String, token:Null<String>): Promise<Bool> {
-		final params = [accountId, clientId, displayName];
+	public function storeLogin(accountId:String, clientId:String, displayName:String, rosterVer:Null<String>, token:Null<String>): Promise<Bool> {
+		final params = [accountId, clientId, displayName, rosterVer];
 		final q = new StringBuf();
-		q.add("INSERT INTO accounts (account_id, client_id, display_name");
+		q.add("INSERT INTO accounts (account_id, client_id, display_name, roster_ver");
 		if (token != null) {
 			q.add(", token, fast_count");
 		}
-		q.add(") VALUES (?,?,?");
+		q.add(") VALUES (?,?,?,?");
 		if (token != null) {
 			q.add(",?");
 			params.push(token);
@@ -1074,6 +1080,8 @@ class Sqlite implements Persistence implements KeyValueStore {
 		params.push(clientId);
 		q.add(", display_name=?");
 		params.push(displayName);
+		q.add(", roster_ver=?");
+		params.push(rosterVer);
 		if (token != null) {
 			q.add(", token=?");
 			params.push(token);
@@ -1083,9 +1091,9 @@ class Sqlite implements Persistence implements KeyValueStore {
 	}
 
 	@HaxeCBridge.noemit
-	public function getLogin(accountId: String): Promise<{ clientId:Null<String>, token:Null<String>, fastCount: Int, displayName:Null<String> }> {
+	public function getLogin(accountId: String): Promise<{ clientId:Null<String>, token:Null<String>, fastCount: Int, displayName:Null<String>, rosterVer: Null<String> }> {
 		return db.exec(
-			"SELECT client_id AS clientId, display_name AS displayName, token, COALESCE(fast_count, 0) AS fastCount FROM accounts WHERE account_id=? LIMIT 1",
+			"SELECT client_id AS clientId, display_name AS displayName, roster_ver AS rosterVer, token, COALESCE(fast_count, 0) AS fastCount FROM accounts WHERE account_id=? LIMIT 1",
 			[accountId]
 		).then(result -> {
 			for (row in result) {
@@ -1096,7 +1104,7 @@ class Sqlite implements Persistence implements KeyValueStore {
 				return r;
 			}
 
-			return { clientId: null, token: null, fastCount: 0, displayName: null };
+			return { clientId: null, token: null, fastCount: 0, displayName: null, rosterVer: null };
 		});
 	}
 
diff --git a/test/TestSqlite.hx b/test/TestSqlite.hx
index 646818e..35cda77 100644
--- a/test/TestSqlite.hx
+++ b/test/TestSqlite.hx
@@ -695,8 +695,8 @@ class TestSqlite extends utest.Test {
 		final account1 = "alice@example.com";
 		final account2 = "bob@example.com";
 
-		persistence.storeLogin(account1, "client1", "Alice", null);
-		persistence.storeLogin(account2, "client2", "Bob", null);
+		persistence.storeLogin(account1, "client1", "Alice", null, null);
+		persistence.storeLogin(account2, "client2", "Bob", null, null);
 
 		persistence.listAccounts().then(accountsBefore -> {
 			Assert.contains(account1, accountsBefore);
@@ -969,7 +969,7 @@ class TestSqlite extends utest.Test {
 
 	@:timeout(3000)
 	public function testStoreStreamManamagementAndGetStreamManagement(async: Async) {
-		persistence.storeLogin("alice@example.com", "", "", null).then(_ ->
+		persistence.storeLogin("alice@example.com", "", "", null, null).then(_ ->
 			persistence.storeStreamManagement("alice@example.com", Bytes.ofHex("01020004").getData(), "ZZ")
 		).then(_ ->
 			persistence.getStreamManagement("alice@example.com")