determine if user cancelled an operation

This commit is contained in:
FTCHD
2026-07-11 18:10:13 +03:00
parent 02bfdb8ddf
commit 9e06d2b4fa
4 changed files with 38 additions and 36 deletions
+9
View File
@@ -1,6 +1,15 @@
import * as Sentry from '@sentry/browser'
import { message } from '@tauri-apps/plugin-dialog'
// Signals that the user explicitly stopped a call (e.g. dismissed the reconnect prompt), so retry
// layers should abort instead of re-running and re-prompting.
export class UserCancelledError extends Error {
constructor(message: string) {
super(message)
this.name = 'UserCancelledError'
}
}
// Coerce an unknown thrown value into a user-facing string. Mirrors the
// `error instanceof Error ? error.message : <fallback>` idiom hand-written across the app.
// Pass `String(error)` as the fallback to preserve sites that surfaced the raw value.
+3
View File
@@ -1,12 +1,15 @@
import { persistQueryClient } from '@tanstack/query-persist-client-core'
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister'
import { QueryClient } from '@tanstack/react-query'
import { UserCancelledError } from './errors'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// staleTime: 60_000,
// gcTime: 3_600_000,
retry: (failureCount, error) =>
!(error instanceof UserCancelledError) && failureCount < 3,
},
},
})
+15 -30
View File
@@ -6,6 +6,7 @@ import { selectActiveConfigFile, useHostStore } from '../../store/host'
import { useStore } from '../../store/memory'
import type { JobItem } from '../../types/jobs'
import type { FlagValue } from '../../types/rclone'
import { UserCancelledError, formatErrorMessage } from '../errors'
import { getFsInfo } from '../format'
import { restartActiveRclone, runRcloneCli } from './cli'
import rclone, { rcloneAsync } from './client'
@@ -17,6 +18,10 @@ const RE_WINDOWS_EXTENDED_PATH = /(\/\/\?\/|\\\\\?\\)/
const RE_WINDOWS_DRIVE_ROOT = /^:local:[a-zA-Z]:\/$/
const RE_WINDOWS_DRIVE_LETTER = /^[a-zA-Z]:$/
const RETRY_OPTIONS = {
retries: 3,
shouldRetry: ({ error }: { error: unknown }) => !(error instanceof UserCancelledError),
}
export async function startDryRun<T>(operation: () => Promise<T>): Promise<T> {
await rclone('/options/set', {
body: {
@@ -652,9 +657,7 @@ export async function startMount({
},
},
}),
{
retries: 3,
}
RETRY_OPTIONS
)
return response?.mountPoint
}
@@ -681,9 +684,7 @@ export async function startMount({
},
},
}),
{
retries: 3,
}
RETRY_OPTIONS
)
if (!r || !r.item) {
directoryExists = false
@@ -713,9 +714,7 @@ export async function startMount({
},
},
}),
{
retries: 3,
}
RETRY_OPTIONS
)
isEmpty = !list || list.length === 0
} catch (err) {
@@ -738,9 +737,7 @@ export async function startMount({
},
},
}),
{
retries: 3,
}
RETRY_OPTIONS
)
} catch (err) {
console.error('[Mount] Error removing directory:', err)
@@ -758,9 +755,7 @@ export async function startMount({
},
},
}),
{
retries: 3,
}
RETRY_OPTIONS
)
} catch (error) {
console.error('[Mount] Error creating directory:', error)
@@ -794,9 +789,7 @@ export async function startMount({
},
},
}),
{
retries: 3,
}
RETRY_OPTIONS
)
}
@@ -862,9 +855,7 @@ export async function startBisync({
},
},
}),
{
retries: 3,
}
RETRY_OPTIONS
)
if (!r?.jobid) {
@@ -883,9 +874,7 @@ export async function startBisync({
},
},
}),
{
retries: 3,
}
RETRY_OPTIONS
).catch(() => null)
console.log('jobStatus', JSON.stringify(jobStatus, null, 2))
@@ -1201,9 +1190,7 @@ export async function startBatch(inputs: ({ _path: string } & Record<string, any
_async: true,
},
}),
{
retries: 3,
}
RETRY_OPTIONS
)
console.log('[startBatch] job created', { jobid: r.jobid })
@@ -1219,9 +1206,7 @@ export async function startBatch(inputs: ({ _path: string } & Record<string, any
},
},
}),
{
retries: 3,
}
RETRY_OPTIONS
).catch(() => null)
console.log('[startBatch] jobStatus', {
+11 -6
View File
@@ -11,12 +11,15 @@ import createRCDClient, {
type RCDClient,
} from 'rclone-sdk'
import { selectCurrentHost, usePersistedStore } from '../../store/persisted'
import { UserCancelledError } from '../errors'
const RE_RECONNECT = /rclone config reconnect (\S+?):/
// Returns true when the user declined to reconnect (dismissed the prompt or the reconnect attempt
// failed) so callers can abort retries instead of re-running and re-prompting.
async function handleReconnectIfNeeded(errorMessage: string) {
const match = errorMessage.match(RE_RECONNECT)
if (!match) return
if (!match) return false
const remoteName = match[1]
const confirmed = await ask(
`Remote "${remoteName}" needs to be reconnected. This usually means the authentication token has expired.\n\nWould you like to reconnect now?`,
@@ -27,7 +30,7 @@ async function handleReconnectIfNeeded(errorMessage: string) {
cancelLabel: 'Dismiss',
}
)
if (!confirmed) return
if (!confirmed) return true
try {
const { reconnectRemote } = await import('./api')
await reconnectRemote(remoteName)
@@ -35,11 +38,13 @@ async function handleReconnectIfNeeded(errorMessage: string) {
title: 'Reconnected',
kind: 'info',
})
return false
} catch (err) {
await message(err instanceof Error ? err.message : 'Reconnection failed', {
title: 'Reconnect Error',
kind: 'error',
})
return true
}
}
@@ -123,8 +128,8 @@ async function request(mode: 'sync' | 'async', path: string, init: any[]): Promi
const errMsg =
typeof result.error === 'string' ? result.error : JSON.stringify(result.error)
await handleReconnectIfNeeded(errMsg)
throw new Error(errMsg)
const cancelled = await handleReconnectIfNeeded(errMsg)
throw cancelled ? new UserCancelledError(errMsg) : new Error(errMsg)
}
const data = result.data as { error?: unknown } | undefined
@@ -132,8 +137,8 @@ async function request(mode: 'sync' | 'async', path: string, init: any[]): Promi
console.error('[rclone] DATA ERROR', path, { error: data.error })
const errMsg = typeof data.error === 'string' ? data.error : JSON.stringify(data.error)
await handleReconnectIfNeeded(errMsg)
throw new Error(errMsg)
const cancelled = await handleReconnectIfNeeded(errMsg)
throw cancelled ? new UserCancelledError(errMsg) : new Error(errMsg)
}
if (!result.response.ok) {