Ignore self-originated realtime sync events

This commit is contained in:
tapframe 2026-07-02 18:42:44 +05:30
parent dccbfc43c2
commit 173584b191
13 changed files with 120 additions and 2 deletions

View file

@ -12,6 +12,7 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import com.nuvio.app.core.auth.AuthStorage
import com.nuvio.app.core.deeplink.handleAppUrl
import com.nuvio.app.core.storage.PlatformLocalAccountDataCleaner
import com.nuvio.app.core.sync.SyncClientIdentityStorage
import com.nuvio.app.features.addons.AddonStorage
import com.nuvio.app.features.collection.CollectionMobileSettingsStorage
import com.nuvio.app.features.collection.CollectionStorage
@ -66,6 +67,7 @@ class MainActivity : AppCompatActivity() {
ThemeSettingsStorage.initialize(applicationContext)
super.onCreate(savedInstanceState)
window.setBackgroundDrawableResource(R.color.nuvio_background)
SyncClientIdentityStorage.initialize(applicationContext)
AddonStorage.initialize(applicationContext)
AuthStorage.initialize(applicationContext)
LibraryStorage.initialize(applicationContext)

View file

@ -0,0 +1,25 @@
package com.nuvio.app.core.sync
import android.content.Context
import android.content.SharedPreferences
actual object SyncClientIdentityStorage {
private const val preferencesName = "nuvio_sync_client_identity"
private const val clientIdKey = "client_instance_id"
private var preferences: SharedPreferences? = null
fun initialize(context: Context) {
preferences = context.getSharedPreferences(preferencesName, Context.MODE_PRIVATE)
}
actual fun loadClientId(): String? =
preferences?.getString(clientIdKey, null)
actual fun saveClientId(clientId: String) {
preferences
?.edit()
?.putString(clientIdKey, clientId)
?.apply()
}
}

View file

@ -206,6 +206,7 @@ object ProfileSettingsSync {
put("p_profile_id", profileId)
put("p_platform", MOBILE_SYNC_PLATFORM)
put("p_settings_json", json.encodeToJsonElement(MobileProfileSettingsBlob.serializer(), blob))
putSyncOriginClientId()
}
SupabaseProvider.client.postgrest.rpc("sync_push_profile_settings_blob", params)
log.d { "pushToRemoteLocked(profileId=$profileId) — success" }

View file

@ -135,13 +135,19 @@ object RealtimeSyncInvalidationService {
private fun handleInsert(profileId: Int, record: JsonObject) {
val eventId = record["id"]?.jsonPrimitive?.contentOrNull
val createdAt = record["created_at"]?.jsonPrimitive?.contentOrNull
val originClientId = record["origin_client_id"]?.jsonPrimitive?.contentOrNull
if (originClientId != null && originClientId == SyncClientIdentity.currentClientId()) {
log.d { "Ignoring self-originated sync invalidation id=$eventId originClientId=$originClientId" }
return
}
val surface = record["surface"]?.jsonPrimitive?.contentOrNull ?: run {
log.w { "Received sync invalidation without surface id=$eventId createdAt=$createdAt keys=${record.keys}" }
return
}
val eventProfileId = record["profile_id"]?.jsonPrimitive?.intOrNull
log.i {
"Received sync invalidation id=$eventId surface=$surface eventProfile=$eventProfileId activeProfile=$profileId createdAt=$createdAt"
"Received sync invalidation id=$eventId surface=$surface eventProfile=$eventProfileId " +
"activeProfile=$profileId originClientId=$originClientId createdAt=$createdAt"
}
if (surface != "profiles" && eventProfileId != null && eventProfileId != profileId) {
log.d { "Ignoring sync invalidation id=$eventId for inactive profile $eventProfileId" }

View file

@ -0,0 +1,50 @@
package com.nuvio.app.core.sync
import kotlinx.serialization.json.JsonObjectBuilder
import kotlinx.serialization.json.put
import kotlin.random.Random
private const val CLIENT_ID_LENGTH = 32
private const val CLIENT_ID_PREFIX = "nuvio-mobile-"
private const val ORIGIN_CLIENT_ID_PARAM = "p_origin_client_id"
private const val CLIENT_ID_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789"
object SyncClientIdentity {
private var cachedClientId: String? = null
fun currentClientId(): String {
cachedClientId?.let { return it }
val stored = SyncClientIdentityStorage.loadClientId()
?.trim()
?.takeIf { it.isValidSyncClientId() }
if (stored != null) {
cachedClientId = stored
return stored
}
val generated = generateClientId()
SyncClientIdentityStorage.saveClientId(generated)
cachedClientId = generated
return generated
}
private fun generateClientId(): String =
CLIENT_ID_PREFIX + buildString(CLIENT_ID_LENGTH) {
repeat(CLIENT_ID_LENGTH) {
append(CLIENT_ID_ALPHABET[Random.nextInt(CLIENT_ID_ALPHABET.length)])
}
}
private fun String.isValidSyncClientId(): Boolean =
length in 16..96 && all { it.isLetterOrDigit() || it == '-' || it == '_' }
}
internal fun JsonObjectBuilder.putSyncOriginClientId() {
put(ORIGIN_CLIENT_ID_PARAM, SyncClientIdentity.currentClientId())
}
internal expect object SyncClientIdentityStorage {
fun loadClientId(): String?
fun saveClientId(clientId: String)
}

View file

@ -2,6 +2,7 @@ package com.nuvio.app.features.addons
import co.touchlab.kermit.Logger
import com.nuvio.app.core.network.SupabaseProvider
import com.nuvio.app.core.sync.putSyncOriginClientId
import com.nuvio.app.features.profiles.ProfileRepository
import io.github.jan.supabase.postgrest.postgrest
import io.github.jan.supabase.postgrest.query.Order
@ -151,6 +152,7 @@ object AddonRepository {
val params = buildJsonObject {
put("p_profile_id", currentProfileId)
put("p_addons", json.encodeToJsonElement(addons))
putSyncOriginClientId()
}
SupabaseProvider.client.postgrest.rpc("sync_push_addons", params)
log.i { "pullFromServer() — migration push done (${addons.size} addons)" }
@ -396,6 +398,7 @@ object AddonRepository {
val params = buildJsonObject {
put("p_profile_id", profileId)
put("p_addons", json.encodeToJsonElement(addons))
putSyncOriginClientId()
}
SupabaseProvider.client.postgrest.rpc("sync_push_addons", params)
log.d { "pushToServer() — success" }

View file

@ -4,6 +4,7 @@ import co.touchlab.kermit.Logger
import com.nuvio.app.core.auth.AuthRepository
import com.nuvio.app.core.auth.AuthState
import com.nuvio.app.core.network.SupabaseProvider
import com.nuvio.app.core.sync.putSyncOriginClientId
import com.nuvio.app.features.profiles.ProfileRepository
import io.github.jan.supabase.postgrest.postgrest
import io.github.jan.supabase.postgrest.rpc
@ -117,6 +118,7 @@ object CollectionSyncService {
val params = buildJsonObject {
put("p_profile_id", profileId)
put("p_collections_json", jsonElement)
putSyncOriginClientId()
}
SupabaseProvider.client.postgrest.rpc("sync_push_collections", params)
log.d { "pushToRemote — success" }

View file

@ -6,6 +6,7 @@ import com.nuvio.app.core.auth.AuthState
import com.nuvio.app.core.network.SupabaseProvider
import com.nuvio.app.core.sync.HOME_CATALOG_LEGACY_SYNC_PLATFORMS
import com.nuvio.app.core.sync.HOME_CATALOG_SHARED_SYNC_PLATFORM
import com.nuvio.app.core.sync.putSyncOriginClientId
import com.nuvio.app.features.profiles.ProfileRepository
import io.github.jan.supabase.postgrest.postgrest
import io.github.jan.supabase.postgrest.rpc
@ -136,6 +137,7 @@ object HomeCatalogSettingsSyncService {
put("p_profile_id", profileId)
put("p_platform", HOME_CATALOG_SHARED_SYNC_PLATFORM)
put("p_settings_json", jsonElement)
putSyncOriginClientId()
}
SupabaseProvider.client.postgrest.rpc("sync_push_home_catalog_settings", params)
log.d { "pushToRemote — success" }

View file

@ -5,6 +5,7 @@ import com.nuvio.app.core.auth.AuthRepository
import com.nuvio.app.core.ui.NuvioToastController
import com.nuvio.app.core.auth.AuthState
import com.nuvio.app.core.network.SupabaseProvider
import com.nuvio.app.core.sync.putSyncOriginClientId
import com.nuvio.app.features.home.PosterShape
import com.nuvio.app.features.profiles.ProfileRepository
import com.nuvio.app.features.trakt.TraktAuthRepository
@ -421,6 +422,7 @@ object LibraryRepository {
val params = buildJsonObject {
put("p_profile_id", profileId)
put("p_items", json.encodeToJsonElement(syncItems))
putSyncOriginClientId()
}
log.i { "Pushing library to server profile=$profileId itemCount=${syncItems.size}" }
SupabaseProvider.client.postgrest.rpc("sync_push_library", params)

View file

@ -5,6 +5,7 @@ import com.nuvio.app.core.auth.AuthRepository
import com.nuvio.app.core.auth.AuthState
import com.nuvio.app.core.auth.isAnonymous
import com.nuvio.app.core.network.SupabaseProvider
import com.nuvio.app.core.sync.putSyncOriginClientId
import com.nuvio.app.features.addons.AddonRepository
import com.nuvio.app.features.collection.CollectionMobileSettingsRepository
import com.nuvio.app.features.collection.CollectionRepository
@ -187,6 +188,7 @@ object ProfileRepository {
val params = buildJsonObject {
put("p_client_max_profiles", MAX_PROFILES)
put("p_profiles", json.encodeToJsonElement(profiles))
putSyncOriginClientId()
}
SupabaseProvider.client.postgrest.rpc("sync_push_profiles", params)
pullProfiles()
@ -277,7 +279,10 @@ object ProfileRepository {
return
}
try {
val params = buildJsonObject { put("p_profile_id", profileIndex) }
val params = buildJsonObject {
put("p_profile_id", profileIndex)
putSyncOriginClientId()
}
SupabaseProvider.client.postgrest.rpc("sync_delete_profile_data", params)
pullProfiles()
} catch (e: Throwable) {

View file

@ -1,6 +1,7 @@
package com.nuvio.app.features.watching.sync
import com.nuvio.app.core.network.SupabaseProvider
import com.nuvio.app.core.sync.putSyncOriginClientId
import com.nuvio.app.features.watchprogress.WatchProgressEntry
import io.github.jan.supabase.postgrest.postgrest
import io.github.jan.supabase.postgrest.rpc
@ -104,6 +105,7 @@ object SupabaseProgressSyncAdapter : ProgressSyncAdapter {
val params = buildJsonObject {
put("p_profile_id", profileId)
put("p_entries", json.encodeToJsonElement(syncEntries))
putSyncOriginClientId()
}
SupabaseProvider.client.postgrest.rpc("sync_push_watch_progress", params)
}
@ -122,6 +124,7 @@ object SupabaseProgressSyncAdapter : ProgressSyncAdapter {
val params = buildJsonObject {
put("p_profile_id", profileId)
put("p_keys", json.encodeToJsonElement(progressKeys))
putSyncOriginClientId()
}
SupabaseProvider.client.postgrest.rpc("sync_delete_watch_progress", params)
}

View file

@ -1,6 +1,7 @@
package com.nuvio.app.features.watching.sync
import com.nuvio.app.core.network.SupabaseProvider
import com.nuvio.app.core.sync.putSyncOriginClientId
import com.nuvio.app.features.watched.WatchedItem
import com.nuvio.app.features.watched.normalizeWatchedMarkedAtEpochMs
import io.github.jan.supabase.postgrest.postgrest
@ -102,6 +103,7 @@ object SupabaseWatchedSyncAdapter : WatchedSyncAdapter {
val params = buildJsonObject {
put("p_profile_id", profileId)
put("p_items", json.encodeToJsonElement(syncItems))
putSyncOriginClientId()
}
SupabaseProvider.client.postgrest.rpc("sync_push_watched_items", params)
}
@ -120,6 +122,7 @@ object SupabaseWatchedSyncAdapter : WatchedSyncAdapter {
val params = buildJsonObject {
put("p_profile_id", profileId)
put("p_keys", json.encodeToJsonElement(keys))
putSyncOriginClientId()
}
SupabaseProvider.client.postgrest.rpc("sync_delete_watched_items", params)
}

View file

@ -0,0 +1,14 @@
package com.nuvio.app.core.sync
import platform.Foundation.NSUserDefaults
actual object SyncClientIdentityStorage {
private const val clientIdKey = "client_instance_id"
actual fun loadClientId(): String? =
NSUserDefaults.standardUserDefaults.stringForKey(clientIdKey)
actual fun saveClientId(clientId: String) {
NSUserDefaults.standardUserDefaults.setObject(clientId, forKey = clientIdKey)
}
}