feat: improve offline PWA resilience

This commit is contained in:
shuaiplus
2026-06-09 14:09:46 +08:00
parent 1a10df4a18
commit 615caf5946
23 changed files with 432 additions and 21 deletions
+120
View File
@@ -302,6 +302,11 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
await Promise.all([refetchCiphers(), refetchFolders(), refetchSends()]);
};
const requireOnlineWrite = () => {
if (session?.accessToken) return;
throw new Error(t('txt_offline_vault_readonly'));
};
const syncVaultCoreInBackground = (options?: { includeFolders?: boolean }) => {
const tasks: Promise<unknown>[] = [Promise.resolve(refetchCiphers())];
if (options?.includeFolders) {
@@ -447,6 +452,12 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
async createVaultItem(draft: VaultDraft, attachments: File[] = []) {
if (!session) return;
try {
requireOnlineWrite();
} catch (error) {
onNotify('error', error instanceof Error ? error.message : t('txt_offline_vault_readonly'));
throw error;
}
const optimistic = optimisticCipherFromDraft(draft, null);
patchDecryptedCiphers((prev) => [optimistic, ...prev.filter((cipher) => cipher.id !== optimistic.id)]);
try {
@@ -471,6 +482,12 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
async updateVaultItem(cipher: Cipher, draft: VaultDraft, options?: { addFiles?: File[]; removeAttachmentIds?: string[] }) {
if (!session) return;
try {
requireOnlineWrite();
} catch (error) {
onNotify('error', error instanceof Error ? error.message : t('txt_offline_vault_readonly'));
throw error;
}
if (hasUnresolvedCipherData(cipher)) {
throw new Error(t('txt_decrypt_failed_2'));
}
@@ -543,6 +560,12 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
},
async deleteVaultItem(cipher: Cipher) {
try {
requireOnlineWrite();
} catch (error) {
onNotify('error', error instanceof Error ? error.message : t('txt_offline_vault_readonly'));
throw error;
}
const previousCipher = { ...cipher };
if (cipher.deletedDate || (cipher as { deletedAt?: string | null }).deletedAt) {
try {
@@ -571,6 +594,12 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
},
async archiveVaultItem(cipher: Cipher) {
try {
requireOnlineWrite();
} catch (error) {
onNotify('error', error instanceof Error ? error.message : t('txt_offline_vault_readonly'));
throw error;
}
const previousCipher = { ...cipher };
const archivedDate = new Date().toISOString();
patchCipherBatch([cipher.id], (current) => ({ ...current, archivedDate, deletedDate: null, revisionDate: archivedDate }));
@@ -587,6 +616,12 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
},
async unarchiveVaultItem(cipher: Cipher) {
try {
requireOnlineWrite();
} catch (error) {
onNotify('error', error instanceof Error ? error.message : t('txt_offline_vault_readonly'));
throw error;
}
const previousCipher = { ...cipher };
const revisionDate = new Date().toISOString();
patchCipherBatch([cipher.id], (current) => ({ ...current, archivedDate: null, revisionDate }));
@@ -603,6 +638,12 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
},
async bulkDeleteVaultItems(ids: string[]) {
try {
requireOnlineWrite();
} catch (error) {
onNotify('error', error instanceof Error ? error.message : t('txt_offline_vault_readonly'));
throw error;
}
try {
await bulkDeleteCiphers(authedFetch, ids);
const deletedDate = new Date().toISOString();
@@ -616,6 +657,12 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
},
async bulkArchiveVaultItems(ids: string[]) {
try {
requireOnlineWrite();
} catch (error) {
onNotify('error', error instanceof Error ? error.message : t('txt_offline_vault_readonly'));
throw error;
}
try {
await bulkArchiveCiphers(authedFetch, ids);
const archivedDate = new Date().toISOString();
@@ -629,6 +676,12 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
},
async bulkUnarchiveVaultItems(ids: string[]) {
try {
requireOnlineWrite();
} catch (error) {
onNotify('error', error instanceof Error ? error.message : t('txt_offline_vault_readonly'));
throw error;
}
try {
await bulkUnarchiveCiphers(authedFetch, ids);
patchCipherBatch(ids, (cipher) => ({ ...cipher, archivedDate: null }));
@@ -641,6 +694,12 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
},
async bulkMoveVaultItems(ids: string[], folderId: string | null) {
try {
requireOnlineWrite();
} catch (error) {
onNotify('error', error instanceof Error ? error.message : t('txt_offline_vault_readonly'));
throw error;
}
try {
await bulkMoveCiphers(authedFetch, ids, folderId);
patchCipherBatch(ids, (cipher) => ({ ...cipher, folderId }));
@@ -658,6 +717,12 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
onNotify('error', t('txt_folder_name_is_required'));
return;
}
try {
requireOnlineWrite();
} catch (error) {
onNotify('error', error instanceof Error ? error.message : t('txt_offline_vault_readonly'));
throw error;
}
try {
if (!session) throw new Error(t('txt_vault_key_unavailable'));
const created = await createFolder(authedFetch, session, folderName);
@@ -683,6 +748,12 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
onNotify('error', t('txt_folder_not_found'));
return;
}
try {
requireOnlineWrite();
} catch (error) {
onNotify('error', error instanceof Error ? error.message : t('txt_offline_vault_readonly'));
throw error;
}
try {
await deleteFolder(authedFetch, id);
patchFolderBatch([id], () => null);
@@ -706,6 +777,12 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
onNotify('error', t('txt_folder_name_is_required'));
return;
}
try {
requireOnlineWrite();
} catch (error) {
onNotify('error', error instanceof Error ? error.message : t('txt_offline_vault_readonly'));
throw error;
}
try {
if (!session) throw new Error(t('txt_vault_key_unavailable'));
await updateFolder(authedFetch, session, id, nextName);
@@ -719,6 +796,12 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
},
async bulkRestoreVaultItems(ids: string[]) {
try {
requireOnlineWrite();
} catch (error) {
onNotify('error', error instanceof Error ? error.message : t('txt_offline_vault_readonly'));
throw error;
}
try {
await bulkRestoreCiphers(authedFetch, ids);
patchCipherBatch(ids, (cipher) => ({ ...cipher, deletedDate: null }));
@@ -731,6 +814,12 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
},
async bulkPermanentDeleteVaultItems(ids: string[]) {
try {
requireOnlineWrite();
} catch (error) {
onNotify('error', error instanceof Error ? error.message : t('txt_offline_vault_readonly'));
throw error;
}
try {
await bulkPermanentDeleteCiphers(authedFetch, ids);
patchCipherBatch(ids, () => null);
@@ -745,6 +834,12 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
async bulkDeleteFolders(folderIds: string[]) {
const ids = Array.from(new Set(folderIds.map((id) => String(id || '').trim()).filter(Boolean)));
if (!ids.length) return;
try {
requireOnlineWrite();
} catch (error) {
onNotify('error', error instanceof Error ? error.message : t('txt_offline_vault_readonly'));
throw error;
}
try {
await bulkDeleteFolders(authedFetch, ids);
const removedIds = new Set(ids);
@@ -765,6 +860,12 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
async createSend(draft: SendDraft, autoCopyLink: boolean) {
if (!session) return;
try {
requireOnlineWrite();
} catch (error) {
onNotify('error', error instanceof Error ? error.message : t('txt_offline_vault_readonly'));
throw error;
}
try {
const fileName = draft.type === 'file' ? String(draft.file?.name || '').trim() : '';
if (fileName) {
@@ -790,6 +891,12 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
async updateSend(send: Send, draft: SendDraft, autoCopyLink: boolean) {
if (!session) return;
try {
requireOnlineWrite();
} catch (error) {
onNotify('error', error instanceof Error ? error.message : t('txt_offline_vault_readonly'));
throw error;
}
try {
const updated = await updateSend(authedFetch, session, send, draft);
await refetchSends();
@@ -806,6 +913,12 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
},
async deleteSend(send: Send) {
try {
requireOnlineWrite();
} catch (error) {
onNotify('error', error instanceof Error ? error.message : t('txt_offline_vault_readonly'));
throw error;
}
try {
await deleteSend(authedFetch, send.id);
await refetchSends();
@@ -817,6 +930,12 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
},
async bulkDeleteSends(ids: string[]) {
try {
requireOnlineWrite();
} catch (error) {
onNotify('error', error instanceof Error ? error.message : t('txt_offline_vault_readonly'));
throw error;
}
try {
await bulkDeleteSends(authedFetch, ids);
await refetchSends();
@@ -833,6 +952,7 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
attachments: ImportAttachmentFile[] = []
): Promise<ImportResultSummary> {
if (!session?.symEncKey || !session?.symMacKey) throw new Error(t('txt_vault_key_unavailable'));
requireOnlineWrite();
const mode = options.folderMode || 'original';
const targetFolderId = (options.targetFolderId || '').trim() || null;