determine if user cancelled an operation
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user