use auto sync

This commit is contained in:
m
2026-08-29 09:31:53 +02:00
parent e43aa2d793
commit 479c8ab61c
5 changed files with 327 additions and 93 deletions
+35 -4
View File
@@ -9,7 +9,15 @@ import { activeUploadUris } from '../services/uploadQueue';
import { apiClient } from '../api/client';
import { API_BASE_URL, ENDPOINTS } from '../constants/api';
import { ApiError } from '../types';
import { setIsSyncing } from './useSyncQueue';
import {
setIsSyncing,
setSyncProgress,
isSyncLoopRunning,
setSyncLoopRunning,
requestSyncCancel,
isSyncCancelRequested,
consumeSyncCancel,
} from './useSyncQueue';
const MAX_RETRIES = 5;
const RETRY_MMKV_ID = 'vaultdrop-sync-retries';
@@ -73,6 +81,8 @@ function canSyncBasedOnNetwork(netInfo: NetInfoState, cellularAllowed: boolean):
return true;
}
let autoSyncLoopStarted = false;
export function useAutoSync() {
const queryClient = useQueryClient();
const isRunning = useRef(false);
@@ -83,8 +93,13 @@ export function useAutoSync() {
);
}, []);
const cancelSync = useCallback(() => {
requestSyncCancel();
}, []);
const runPendingSync = useCallback(async (pendingFiles: Array<ReturnType<typeof fileStore.getAllLocal>[number]>) => {
if (isRunning.current) return;
if (isSyncLoopRunning()) return;
const globalCellular = safDirectory.getGlobalSyncCellular();
const netInfo = await NetInfo.fetch();
@@ -92,11 +107,18 @@ export function useAutoSync() {
if (!canSyncBasedOnNetwork(netInfo, globalCellular)) return;
isRunning.current = true;
setSyncLoopRunning(true);
try {
setIsSyncing(true);
consumeSyncCancel();
for (const entry of pendingFiles) {
const progressFiles = pendingFiles.map((f) => ({ id: f.id, name: f.name }));
for (let index = 0; index < pendingFiles.length; index++) {
if (isSyncCancelRequested()) break;
const entry = pendingFiles[index];
setSyncProgress({ files: progressFiles, currentIndex: index });
const uri = entry.localUri;
if (!uri || activeUploadUris.has(uri)) continue;
activeUploadUris.add(uri);
@@ -126,10 +148,16 @@ export function useAutoSync() {
}
}
if (consumeSyncCancel()) {
return;
}
queryClient.invalidateQueries({ queryKey: ['resources'] });
} finally {
setSyncProgress(null);
setIsSyncing(false);
isRunning.current = false;
setSyncLoopRunning(false);
}
}, [queryClient]);
@@ -161,12 +189,15 @@ export function useAutoSync() {
}, [getPendingFiles, runPendingSync]);
useEffect(() => {
if (autoSyncLoopStarted) return;
autoSyncLoopStarted = true;
const timeout = setTimeout(() => {
checkAndSync();
}, 5_000);
const interval = setInterval(checkAndSync, 30_000);
return () => { clearTimeout(timeout); clearInterval(interval); };
return () => { clearTimeout(timeout); clearInterval(interval); autoSyncLoopStarted = false; };
}, [checkAndSync]);
return { triggerSync: checkAndSync, syncManually };
return { triggerSync: checkAndSync, syncManually, cancelSync };
}
+106 -13
View File
@@ -1,8 +1,22 @@
import { useState, useEffect, useCallback } from 'react';
import { useState, useEffect, useCallback, useSyncExternalStore } from 'react';
import { createMMKV } from 'react-native-mmkv';
import { fileStore } from '../services/fileStore';
export type SyncProgress = {
files: Array<{ id: string; name: string }>;
currentIndex: number;
} | null;
const syncStateStorage = createMMKV({ id: 'vaultdrop-sync-state' });
const PROGRESS_KEY = 'sync_progress';
let syncRunning = false;
let cancelRequested = false;
const listeners = new Set<() => void>();
function notify() {
listeners.forEach((l) => l());
}
export function getIsSyncing(): boolean {
return syncStateStorage.getString('is_syncing') === 'true';
@@ -10,24 +24,98 @@ export function getIsSyncing(): boolean {
export function setIsSyncing(value: boolean) {
syncStateStorage.set('is_syncing', value ? 'true' : 'false');
notify();
}
export function getSyncProgress(): SyncProgress {
return parseProgress(syncStateStorage.getString(PROGRESS_KEY));
}
function parseProgress(raw: string | undefined): SyncProgress {
if (!raw) return null;
try {
const parsed = JSON.parse(raw) as { files: Array<{ id: string; name: string }>; currentIndex: number };
if (!parsed || !Array.isArray(parsed.files)) return null;
return parsed;
} catch {
return null;
}
}
export function setSyncProgress(value: SyncProgress) {
if (value === null) {
syncStateStorage.remove(PROGRESS_KEY);
} else {
syncStateStorage.set(PROGRESS_KEY, JSON.stringify(value));
}
notify();
}
export function isSyncLoopRunning(): boolean {
return syncRunning;
}
export function setSyncLoopRunning(value: boolean) {
syncRunning = value;
}
export function requestSyncCancel() {
cancelRequested = true;
}
export function isSyncCancelRequested(): boolean {
return cancelRequested;
}
export function consumeSyncCancel(): boolean {
const value = cancelRequested;
cancelRequested = false;
return value;
}
export function resetSyncState() {
syncStateStorage.remove('is_syncing');
syncStateStorage.remove(PROGRESS_KEY);
syncRunning = false;
cancelRequested = false;
notify();
}
function subscribe(listener: () => void): () => void {
listeners.add(listener);
return () => listeners.delete(listener);
}
let cachedIsSyncing = getIsSyncing();
let lastIsSyncingRaw: string | undefined;
function getIsSyncingSnapshot(): boolean {
const raw = syncStateStorage.getString('is_syncing');
if (raw !== lastIsSyncingRaw) {
lastIsSyncingRaw = raw;
cachedIsSyncing = raw === 'true';
}
return cachedIsSyncing;
}
let cachedProgress = getSyncProgress();
let lastProgressRaw: string | undefined;
function getSyncProgressSnapshot(): SyncProgress {
const raw = syncStateStorage.getString(PROGRESS_KEY);
if (raw !== lastProgressRaw) {
lastProgressRaw = raw;
cachedProgress = parseProgress(raw);
}
return cachedProgress;
}
export function useSyncQueue() {
const isSyncing = useSyncExternalStore(subscribe, getIsSyncingSnapshot, getIsSyncingSnapshot);
const [pendingCount, setPendingCount] = useState(0);
const [isSyncing, setIsSyncingState] = useState(() => getIsSyncing());
const refresh = useCallback(() => {
const registry = fileStore.getAllLocal();
let count = 0;
for (const entry of registry) {
if (entry.backendId || !entry.localUri) continue;
if (entry.syncStatus !== 'local' && entry.syncStatus !== 'error') continue;
count++;
}
setPendingCount(count);
setIsSyncingState(getIsSyncing());
setPendingCount(fileStore.countPendingSync());
}, []);
useEffect(() => {
@@ -38,3 +126,8 @@ export function useSyncQueue() {
return { pendingCount, isSyncing, refresh };
}
export function useSyncProgress() {
const syncProgress = useSyncExternalStore(subscribe, getSyncProgressSnapshot, getSyncProgressSnapshot);
return { syncProgress };
}