feat: LWW timestamps for bidirectional federation profile & layout sync

Profile and space layout changes on remote instances were being
overwritten by stale data on reconnect. Adds Last-Writer-Wins
timestamps so the client-relay mesh rejects stale writes:

- profile_updated_at column on users table with migration + backfill
- Server LWW guards on PATCH /users/@me and PUT /space-layout
- Bidirectional profileSync: pulls newer remote profiles to home
- LWW layout sync replaces home-authoritative _layoutFromTrueHome flag
- Layout pushes to ALL connected instances in parallel
This commit is contained in:
Jannis Braun
2026-03-12 18:28:37 +01:00
parent acbcf4d4e8
commit 83699d7e91
10 changed files with 294 additions and 115 deletions
+19
View File
@@ -117,6 +117,12 @@ export function runMigrations(db: Database.Database): void {
columns: [
{ name: 'category_id', type: 'TEXT' },
]
},
{
name: 'users',
columns: [
{ name: 'profile_updated_at', type: 'INTEGER' },
]
}
];
@@ -233,6 +239,9 @@ export function runMigrations(db: Database.Database): void {
// ─── Convert video channels to voice (video type removed) ─────────────────
migrateVideoChannels(db);
// ─── Backfill profile_updated_at from created_at ──────────────────────────
migrateProfileUpdatedAt(db);
// ─── Ensure user_space_layout table exists ────────────────────────────────
db.exec(`
CREATE TABLE IF NOT EXISTS user_space_layout (
@@ -651,6 +660,16 @@ function migrateVideoChannels(db: Database.Database): void {
}
}
/** Backfill profile_updated_at from created_at for existing users */
function migrateProfileUpdatedAt(db: Database.Database): void {
const result = db.prepare(
'UPDATE users SET profile_updated_at = created_at WHERE profile_updated_at IS NULL'
).run();
if (result.changes > 0) {
console.log(`Migrating: Backfilled profile_updated_at for ${result.changes} user(s)`);
}
}
function migrateReplicatedUsernames(db: Database.Database): void {
const rows = db.prepare(
"SELECT id, username, home_instance FROM users WHERE home_instance IS NOT NULL AND username NOT LIKE '%@%'"