Sync behaviour improvments

This commit is contained in:
tapframe 2025-09-18 15:32:05 +05:30
parent 62defd4773
commit abfccd0e36
3 changed files with 57 additions and 14 deletions

View file

@ -10,6 +10,7 @@ type AccountContextValue = {
signIn: (email: string, password: string) => Promise<string | null>; signIn: (email: string, password: string) => Promise<string | null>;
signUp: (email: string, password: string) => Promise<string | null>; signUp: (email: string, password: string) => Promise<string | null>;
signOut: () => Promise<void>; signOut: () => Promise<void>;
refreshCurrentUser: () => Promise<void>;
updateProfile: (partial: { avatarUrl?: string; displayName?: string }) => Promise<string | null>; updateProfile: (partial: { avatarUrl?: string; displayName?: string }) => Promise<string | null>;
}; };
@ -47,16 +48,21 @@ export const AccountProvider: React.FC<{ children: React.ReactNode }> = ({ child
// Auth state listener // Auth state listener
const { data: subscription } = supabase.auth.onAuthStateChange(async (_event, session) => { const { data: subscription } = supabase.auth.onAuthStateChange(async (_event, session) => {
const fullUser = session?.user ? await accountService.getCurrentUser() : null; setLoading(true);
setUser(fullUser); try {
if (fullUser) { const fullUser = session?.user ? await accountService.getCurrentUser() : null;
await syncService.migrateLocalScopeToUser(); setUser(fullUser);
await syncService.subscribeRealtime(); if (fullUser) {
// Pull first to hydrate local state, then push to avoid wiping server with empty local await syncService.migrateLocalScopeToUser();
await syncService.fullPull(); await syncService.subscribeRealtime();
await syncService.fullPush(); // Pull first to hydrate local state, then push to avoid wiping server with empty local
} else { await syncService.fullPull();
syncService.unsubscribeRealtime(); await syncService.fullPush();
} else {
syncService.unsubscribeRealtime();
}
} finally {
setLoading(false);
} }
}); });
@ -81,6 +87,15 @@ export const AccountProvider: React.FC<{ children: React.ReactNode }> = ({ child
await accountService.signOut(); await accountService.signOut();
setUser(null); setUser(null);
}, },
refreshCurrentUser: async () => {
setLoading(true);
try {
const u = await accountService.getCurrentUser();
setUser(u);
} finally {
setLoading(false);
}
},
updateProfile: async (partial) => { updateProfile: async (partial) => {
const err = await accountService.updateProfile(partial); const err = await accountService.updateProfile(partial);
if (!err) { if (!err) {

View file

@ -231,7 +231,7 @@ const SettingsScreen: React.FC = () => {
const { isAuthenticated, userProfile, refreshAuthStatus } = useTraktContext(); const { isAuthenticated, userProfile, refreshAuthStatus } = useTraktContext();
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const { user, signOut, loading: accountLoading } = useAccount(); const { user, signOut, loading: accountLoading, refreshCurrentUser } = useAccount();
// Tablet-specific state // Tablet-specific state
const [selectedCategory, setSelectedCategory] = useState('account'); const [selectedCategory, setSelectedCategory] = useState('account');
@ -248,10 +248,12 @@ const SettingsScreen: React.FC = () => {
if (__DEV__) console.log('SettingsScreen focused, refreshing auth status. Current state:', { isAuthenticated, userProfile: userProfile?.username }); if (__DEV__) console.log('SettingsScreen focused, refreshing auth status. Current state:', { isAuthenticated, userProfile: userProfile?.username });
} }
refreshAuthStatus(); refreshAuthStatus();
// Also refresh account user in case we returned from auth flow
refreshCurrentUser();
}); });
return unsubscribe; return unsubscribe;
}, [navigation, isAuthenticated, userProfile, refreshAuthStatus]); }, [navigation, isAuthenticated, userProfile, refreshAuthStatus, refreshCurrentUser]);
// States for dynamic content // States for dynamic content
const [addonCount, setAddonCount] = useState<number>(0); const [addonCount, setAddonCount] = useState<number>(0);

View file

@ -693,6 +693,8 @@ class SyncService {
(stremioService as any).addonOrder = order; (stremioService as any).addonOrder = order;
await (stremioService as any).saveInstalledAddons(); await (stremioService as any).saveInstalledAddons();
await (stremioService as any).saveAddonOrder(); await (stremioService as any).saveAddonOrder();
// Mark addons initialized for this user to prevent destructive merges on first push
try { await AsyncStorage.setItem(`@user:${userId}:addons_initialized`, 'true'); } catch {}
// Push merged order to server to preserve across devices // Push merged order to server to preserve across devices
try { try {
const rows = order.map((addonId: string, idx: number) => ({ const rows = order.map((addonId: string, idx: number) => ({
@ -848,9 +850,27 @@ class SyncService {
const user = await accountService.getCurrentUser(); const user = await accountService.getCurrentUser();
if (!user) return; if (!user) return;
const userId = user.id; const userId = user.id;
const addons = await stremioService.getInstalledAddonsAsync(); let addons = await stremioService.getInstalledAddonsAsync();
logger.log(`[Sync] push installed_addons count=${addons.length}`); logger.log(`[Sync] push installed_addons count=${addons.length}`);
const order = (stremioService as any).addonOrder as string[]; let order = (stremioService as any).addonOrder as string[];
// Safety: if this is a first-time push and local addons are fewer than remote, pull before pushing
try {
const initialized = (await AsyncStorage.getItem(`@user:${userId}:addons_initialized`)) === 'true';
const { data: remoteBefore } = await supabase
.from('installed_addons')
.select('addon_id')
.eq('user_id', userId);
const remoteCount = (remoteBefore || []).length;
if (!initialized && remoteCount > addons.length) {
logger.log('[Sync] addons not initialized and local smaller than remote → pulling before push');
await this.pullAddonsSnapshot(userId);
// refresh local state after pull
addons = await stremioService.getInstalledAddonsAsync();
order = (stremioService as any).addonOrder as string[];
}
} catch {}
const rows = addons.map((a: any) => ({ const rows = addons.map((a: any) => ({
user_id: userId, user_id: userId,
addon_id: a.id, addon_id: a.id,
@ -863,12 +883,17 @@ class SyncService {
manifest_data: a, manifest_data: a,
})); }));
// Delete remote addons that no longer exist locally (excluding pre-installed to be safe) // Delete remote addons that no longer exist locally (excluding pre-installed to be safe)
// Guard: do not perform deletions on first-time merge when remote has more addons
try { try {
const { data: remote, error: rErr } = await supabase const { data: remote, error: rErr } = await supabase
.from('installed_addons') .from('installed_addons')
.select('addon_id') .select('addon_id')
.eq('user_id', userId); .eq('user_id', userId);
if (!rErr && remote) { if (!rErr && remote) {
const initialized = (await AsyncStorage.getItem(`@user:${userId}:addons_initialized`)) === 'true';
if (!initialized && (remote as any[]).length > addons.length) {
logger.log('[Sync] skipping deletions during first-time addon merge');
} else {
const localIds = new Set(addons.map((a: any) => a.id)); const localIds = new Set(addons.map((a: any) => a.id));
const toDeletePromises = (remote as any[]) const toDeletePromises = (remote as any[])
.map(r => r.addon_id as string) .map(r => r.addon_id as string)
@ -894,6 +919,7 @@ class SyncService {
.in('addon_id', toDelete); .in('addon_id', toDelete);
if (del.error && __DEV__) console.warn('[SyncService] delete addons error', del.error); if (del.error && __DEV__) console.warn('[SyncService] delete addons error', del.error);
} }
}
} }
} catch (e) { } catch (e) {
if (__DEV__) console.warn('[SyncService] deletion sync for addons failed', e); if (__DEV__) console.warn('[SyncService] deletion sync for addons failed', e);