mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-03-26 02:22:17 -04:00
Tweak browser extension sync queue
This commit is contained in:
@@ -44,6 +44,13 @@ import { t } from '@/i18n/StandaloneI18n';
|
||||
let cachedSqliteClient: SqliteClient | null = null;
|
||||
let cachedVaultBlob: string | null = null;
|
||||
|
||||
/**
|
||||
* Global sync queue state.
|
||||
* Prevents multiple simultaneous sync operations and ensures pending changes are synced.
|
||||
*/
|
||||
let isSyncInProgress = false;
|
||||
let hasPendingSync = false;
|
||||
|
||||
/**
|
||||
* Check if the user is logged in and if the vault is locked, and also check for pending migrations.
|
||||
*/
|
||||
@@ -671,7 +678,15 @@ export async function handleUploadVault(
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to upload vault:', error);
|
||||
// E-801: Upload failed
|
||||
|
||||
// Check if error is UPLOAD_OUTDATED (E-802) - server has newer vault
|
||||
const errorMessage = error instanceof Error ? error.message : '';
|
||||
if (errorMessage.includes('E-802')) {
|
||||
// Return status 2 (Outdated) so caller can handle merge
|
||||
return { success: false, status: 2, error: errorMessage };
|
||||
}
|
||||
|
||||
// E-801: Upload failed for other reasons
|
||||
return { success: false, error: formatErrorWithCode(await t('common.errors.unknownError'), AppErrorCode.UPLOAD_FAILED) };
|
||||
}
|
||||
}
|
||||
@@ -956,6 +971,7 @@ export async function handleGetSyncState(): Promise<{
|
||||
isDirty: boolean;
|
||||
mutationSequence: number;
|
||||
serverRevision: number;
|
||||
isSyncInProgress: boolean;
|
||||
}> {
|
||||
const [isDirty, mutationSequence, serverRevision] = await Promise.all([
|
||||
storage.getItem('local:isDirty') as Promise<boolean | null>,
|
||||
@@ -966,7 +982,8 @@ export async function handleGetSyncState(): Promise<{
|
||||
return {
|
||||
isDirty: isDirty ?? false,
|
||||
mutationSequence: mutationSequence ?? 0,
|
||||
serverRevision: serverRevision ?? 0
|
||||
serverRevision: serverRevision ?? 0,
|
||||
isSyncInProgress
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1094,19 +1111,20 @@ export async function handleCheckSyncStatus(): Promise<SyncStatusCheckResult> {
|
||||
/**
|
||||
* Full vault sync orchestration that runs entirely in background context.
|
||||
* This ensures sync completes even if popup closes mid-operation.
|
||||
*
|
||||
* Sync logic:
|
||||
* - If server has newer vault AND we have local changes (isDirty) → merge then upload
|
||||
* - If server has newer vault AND no local changes → just download
|
||||
* - If server has same revision AND we have local changes → upload
|
||||
* - If offline → keep local changes, sync later
|
||||
*
|
||||
* Race detection:
|
||||
* - Upload captures mutationSequence at start
|
||||
* - After upload, only clears isDirty if sequence unchanged
|
||||
* - If sequence changed during upload, stays dirty for next sync
|
||||
*/
|
||||
export async function handleFullVaultSync(): Promise<FullVaultSyncResult> {
|
||||
// Check if sync is already in progress
|
||||
if (isSyncInProgress) {
|
||||
// Mark that we need to sync again after current sync completes
|
||||
hasPendingSync = true;
|
||||
console.info('[VaultSync] Sync already in progress, queued for retry after completion');
|
||||
return { success: true, hasNewVault: false, wasOffline: false, upgradeRequired: false, requiresLogout: false };
|
||||
}
|
||||
|
||||
// Mark sync as in progress
|
||||
isSyncInProgress = true;
|
||||
hasPendingSync = false;
|
||||
|
||||
const webApi = new WebApiService();
|
||||
|
||||
try {
|
||||
@@ -1380,6 +1398,23 @@ export async function handleFullVaultSync(): Promise<FullVaultSyncResult> {
|
||||
? baseMessage
|
||||
: formatErrorWithCode(baseMessage, AppErrorCode.UNKNOWN_ERROR);
|
||||
return { success: false, hasNewVault: false, wasOffline: false, upgradeRequired: false, requiresLogout: false, error: errorMessage };
|
||||
} finally {
|
||||
// Reset sync in progress flag
|
||||
isSyncInProgress = false;
|
||||
|
||||
/*
|
||||
* Check if another sync is needed (mutations happened during this sync).
|
||||
* Trigger follow-up sync asynchronously (don't await to avoid blocking).
|
||||
* Popup will poll isDirty flag to detect when background sync completes.
|
||||
*/
|
||||
if (hasPendingSync) {
|
||||
console.info('[VaultSync] Pending mutations detected, triggering follow-up sync');
|
||||
hasPendingSync = false;
|
||||
|
||||
handleFullVaultSync().catch(err => {
|
||||
console.error('[VaultSync] Follow-up sync failed:', err);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
|
||||
import type { FullVaultSyncResult } from '@/entrypoints/background/VaultMessageHandler';
|
||||
@@ -26,6 +26,7 @@ export function useVaultMutate(): {
|
||||
executeVaultMutationAsync: (operation: () => Promise<void>) => Promise<void>;
|
||||
} {
|
||||
const dbContext = useDb();
|
||||
const pollIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
/**
|
||||
* Execute the provided operation and save locally.
|
||||
@@ -53,13 +54,66 @@ export function useVaultMutate(): {
|
||||
await dbContext.refreshSyncState();
|
||||
}, [dbContext]);
|
||||
|
||||
/**
|
||||
* Start polling to detect when background sync completes.
|
||||
* Polls isDirty flag AND background sync state every 500ms.
|
||||
* Only clears indicator when vault is clean AND background has no sync in progress.
|
||||
*/
|
||||
const startPollingForCompletion = useCallback((): void => {
|
||||
/* Clear any existing poll interval */
|
||||
if (pollIntervalRef.current) {
|
||||
clearInterval(pollIntervalRef.current);
|
||||
}
|
||||
|
||||
console.info('[VaultMutate] Starting to poll for sync completion');
|
||||
|
||||
pollIntervalRef.current = setInterval(async () => {
|
||||
try {
|
||||
/*
|
||||
* Get sync state from background - includes both isDirty flag
|
||||
* and isSyncInProgress status from the background script
|
||||
*/
|
||||
const syncState = await sendMessage('GET_SYNC_STATE', {}, 'background') as {
|
||||
isDirty: boolean;
|
||||
isSyncInProgress: boolean;
|
||||
mutationSequence: number;
|
||||
serverRevision: number;
|
||||
};
|
||||
|
||||
/*
|
||||
* Only clear uploading indicator when:
|
||||
* 1. Vault is not dirty (no pending changes)
|
||||
* 2. Background has no sync in progress (no queued syncs running)
|
||||
*
|
||||
* This prevents clearing the indicator between queued syncs.
|
||||
*/
|
||||
if (!syncState.isDirty && !syncState.isSyncInProgress) {
|
||||
console.info('[VaultMutate] Sync completed (isDirty=false, isSyncInProgress=false), clearing uploading indicator');
|
||||
dbContext.setIsUploading(false);
|
||||
dbContext.setIsSyncing(false);
|
||||
|
||||
/* Refresh React state to match storage */
|
||||
await dbContext.refreshSyncState();
|
||||
|
||||
/* Stop polling */
|
||||
if (pollIntervalRef.current) {
|
||||
clearInterval(pollIntervalRef.current);
|
||||
pollIntervalRef.current = null;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[VaultMutate] Error polling sync state:', error);
|
||||
}
|
||||
}, 500); /* Poll every 500ms */
|
||||
}, [dbContext]);
|
||||
|
||||
/**
|
||||
* Trigger a sync in the background script.
|
||||
* This is fire-and-forget - the sync runs entirely in the background context
|
||||
* and continues even if the popup closes.
|
||||
*
|
||||
* If a merge happened during sync (hasNewVault=true), reload the database
|
||||
* so the popup shows the merged data.
|
||||
* Always polls to detect completion since background sync may queue additional
|
||||
* syncs that we cannot directly observe from the popup context.
|
||||
*/
|
||||
const triggerBackgroundSync = useCallback((): void => {
|
||||
dbContext.setIsUploading(true);
|
||||
@@ -68,20 +122,21 @@ export function useVaultMutate(): {
|
||||
* Fire-and-forget: send message to background without awaiting.
|
||||
* The background script will handle the full sync orchestration
|
||||
* and will re-sync if mutations happened during the sync.
|
||||
*
|
||||
* After sending message, we start polling to detect completion.
|
||||
*/
|
||||
sendMessage('FULL_VAULT_SYNC', {}, 'background').then(async (result: FullVaultSyncResult) => {
|
||||
// If a merge happened, reload the database to show merged data
|
||||
if (result.hasNewVault) {
|
||||
void sendMessage('FULL_VAULT_SYNC', {}, 'background').then(async (result) => {
|
||||
const syncResult = result as FullVaultSyncResult;
|
||||
if (syncResult.hasNewVault) {
|
||||
await dbContext.loadStoredDatabase();
|
||||
}
|
||||
// Refresh sync state if popup is still open
|
||||
await dbContext.refreshSyncState();
|
||||
}).catch((error) => {
|
||||
console.error('Background sync error:', error);
|
||||
}).finally(() => {
|
||||
dbContext.setIsUploading(false);
|
||||
});
|
||||
}, [dbContext]);
|
||||
|
||||
// Start polling for completion
|
||||
startPollingForCompletion();
|
||||
}, [dbContext, startPollingForCompletion]);
|
||||
|
||||
/**
|
||||
* Execute a vault mutation asynchronously: save locally immediately, then
|
||||
|
||||
@@ -124,7 +124,6 @@ export const useVaultSync = (): { syncVault: (options?: VaultSyncOptions) => Pro
|
||||
} finally {
|
||||
// Always clear syncing/uploading states when done
|
||||
dbContext.setIsSyncing(false);
|
||||
dbContext.setIsUploading(false);
|
||||
}
|
||||
}, [app, dbContext, t]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user