mirror of
https://github.com/stablyai/orca.git
synced 2026-10-07 00:02:29 +00:00
fix: guard runtime pairing async state (#3597)
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
/* eslint-disable max-lines -- Why: the generated URL cache, grant list, and
|
||||
settings form stay together so revocation and cache invalidation remain
|
||||
auditable. */
|
||||
import { Check, Copy, Loader2, RefreshCw } from 'lucide-react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { useMountedRef } from '@/hooks/useMountedRef'
|
||||
import type { RuntimeAccessGrant } from '../../../../shared/runtime-access-grants'
|
||||
import { Button } from '../ui/button'
|
||||
import { Input } from '../ui/input'
|
||||
@@ -112,6 +116,7 @@ export function RuntimePairingUrlGenerator({
|
||||
const [isGeneratingPairing, setIsGeneratingPairing] = useState(false)
|
||||
const networkInterfaceLoadIdRef = useRef(0)
|
||||
const accessGrantLoadIdRef = useRef(0)
|
||||
const mountedRef = useMountedRef()
|
||||
|
||||
useEffect(() => {
|
||||
if (copiedTarget === null) {
|
||||
@@ -119,57 +124,71 @@ export function RuntimePairingUrlGenerator({
|
||||
}
|
||||
const target = copiedTarget
|
||||
const timeout = window.setTimeout(() => {
|
||||
setCopiedTarget((current) => (current === target ? null : current))
|
||||
if (mountedRef.current) {
|
||||
setCopiedTarget((current) => (current === target ? null : current))
|
||||
}
|
||||
}, 1400)
|
||||
return () => window.clearTimeout(timeout)
|
||||
}, [copiedTarget])
|
||||
}, [copiedTarget, mountedRef])
|
||||
|
||||
const loadRuntimeAccessGrants = useCallback(
|
||||
async (options: { showToastOnError?: boolean } = {}): Promise<void> => {
|
||||
const loadId = accessGrantLoadIdRef.current + 1
|
||||
accessGrantLoadIdRef.current = loadId
|
||||
setIsLoadingAccessGrants(true)
|
||||
if (mountedRef.current) {
|
||||
setIsLoadingAccessGrants(true)
|
||||
}
|
||||
try {
|
||||
const result = await window.api.mobile.listRuntimeAccessGrants()
|
||||
if (loadId === accessGrantLoadIdRef.current) {
|
||||
if (mountedRef.current && loadId === accessGrantLoadIdRef.current) {
|
||||
setRuntimeAccessGrants(result.grants)
|
||||
}
|
||||
} catch (error) {
|
||||
if (loadId === accessGrantLoadIdRef.current && options.showToastOnError) {
|
||||
if (
|
||||
mountedRef.current &&
|
||||
loadId === accessGrantLoadIdRef.current &&
|
||||
options.showToastOnError
|
||||
) {
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : 'Failed to load shared access grants.'
|
||||
)
|
||||
}
|
||||
} finally {
|
||||
if (loadId === accessGrantLoadIdRef.current) {
|
||||
if (mountedRef.current && loadId === accessGrantLoadIdRef.current) {
|
||||
setIsLoadingAccessGrants(false)
|
||||
}
|
||||
}
|
||||
},
|
||||
[]
|
||||
[mountedRef]
|
||||
)
|
||||
|
||||
const loadNetworkInterfaces = useCallback(
|
||||
async (options: { showToastOnError?: boolean } = {}): Promise<void> => {
|
||||
const loadId = networkInterfaceLoadIdRef.current + 1
|
||||
networkInterfaceLoadIdRef.current = loadId
|
||||
setRefreshingNetworkInterfaces(true)
|
||||
if (mountedRef.current) {
|
||||
setRefreshingNetworkInterfaces(true)
|
||||
}
|
||||
try {
|
||||
const result = await window.api.mobile.listNetworkInterfaces()
|
||||
if (loadId === networkInterfaceLoadIdRef.current) {
|
||||
if (mountedRef.current && loadId === networkInterfaceLoadIdRef.current) {
|
||||
setNetworkInterfaces(result.interfaces)
|
||||
}
|
||||
} catch {
|
||||
if (loadId === networkInterfaceLoadIdRef.current && options.showToastOnError) {
|
||||
if (
|
||||
mountedRef.current &&
|
||||
loadId === networkInterfaceLoadIdRef.current &&
|
||||
options.showToastOnError
|
||||
) {
|
||||
toast.error('Failed to refresh network interfaces.')
|
||||
}
|
||||
} finally {
|
||||
if (loadId === networkInterfaceLoadIdRef.current) {
|
||||
if (mountedRef.current && loadId === networkInterfaceLoadIdRef.current) {
|
||||
setRefreshingNetworkInterfaces(false)
|
||||
}
|
||||
}
|
||||
},
|
||||
[]
|
||||
[mountedRef]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -190,9 +209,11 @@ export function RuntimePairingUrlGenerator({
|
||||
runtimePairingUrlCache.runtimePairingUrl = null
|
||||
runtimePairingUrlCache.webClientUrl = null
|
||||
runtimePairingUrlCache.runtimePairingDeviceId = null
|
||||
setRuntimePairingUrl(null)
|
||||
setWebClientUrl(null)
|
||||
setRuntimePairingDeviceId(null)
|
||||
if (mountedRef.current) {
|
||||
setRuntimePairingUrl(null)
|
||||
setWebClientUrl(null)
|
||||
setRuntimePairingDeviceId(null)
|
||||
}
|
||||
}
|
||||
|
||||
const generateRuntimePairingUrl = async (): Promise<void> => {
|
||||
@@ -205,21 +226,31 @@ export function RuntimePairingUrlGenerator({
|
||||
})
|
||||
if (!result.available) {
|
||||
clearGeneratedUrls()
|
||||
toast.error('Runtime pairing is unavailable.')
|
||||
if (mountedRef.current) {
|
||||
toast.error('Runtime pairing is unavailable.')
|
||||
}
|
||||
return
|
||||
}
|
||||
runtimePairingUrlCache.runtimePairingUrl = result.pairingUrl
|
||||
runtimePairingUrlCache.webClientUrl = result.webClientUrl
|
||||
runtimePairingUrlCache.runtimePairingDeviceId = result.deviceId
|
||||
setRuntimePairingUrl(result.pairingUrl)
|
||||
setWebClientUrl(result.webClientUrl)
|
||||
setRuntimePairingDeviceId(result.deviceId)
|
||||
if (mountedRef.current) {
|
||||
setRuntimePairingUrl(result.pairingUrl)
|
||||
setWebClientUrl(result.webClientUrl)
|
||||
setRuntimePairingDeviceId(result.deviceId)
|
||||
}
|
||||
await loadRuntimeAccessGrants()
|
||||
toast.success(result.webClientUrl ? 'Generated web client URL.' : 'Generated pairing URL.')
|
||||
if (mountedRef.current) {
|
||||
toast.success(result.webClientUrl ? 'Generated web client URL.' : 'Generated pairing URL.')
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : 'Failed to generate pairing URL.')
|
||||
if (mountedRef.current) {
|
||||
toast.error(error instanceof Error ? error.message : 'Failed to generate pairing URL.')
|
||||
}
|
||||
} finally {
|
||||
setIsGeneratingPairing(false)
|
||||
if (mountedRef.current) {
|
||||
setIsGeneratingPairing(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,31 +259,45 @@ export function RuntimePairingUrlGenerator({
|
||||
try {
|
||||
const result = await window.api.mobile.revokeRuntimeAccess({ deviceId: grant.deviceId })
|
||||
if (!result.revoked) {
|
||||
toast.error('Shared access was already revoked.')
|
||||
if (mountedRef.current) {
|
||||
toast.error('Shared access was already revoked.')
|
||||
}
|
||||
await loadRuntimeAccessGrants()
|
||||
return
|
||||
}
|
||||
setRuntimeAccessGrants((current) =>
|
||||
current.filter((entry) => entry.deviceId !== grant.deviceId)
|
||||
)
|
||||
if (mountedRef.current) {
|
||||
setRuntimeAccessGrants((current) =>
|
||||
current.filter((entry) => entry.deviceId !== grant.deviceId)
|
||||
)
|
||||
}
|
||||
if (runtimePairingDeviceId === grant.deviceId) {
|
||||
clearGeneratedUrls()
|
||||
}
|
||||
toast.success('Shared access revoked.')
|
||||
if (mountedRef.current) {
|
||||
toast.success('Shared access revoked.')
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : 'Failed to revoke shared access.')
|
||||
if (mountedRef.current) {
|
||||
toast.error(error instanceof Error ? error.message : 'Failed to revoke shared access.')
|
||||
}
|
||||
} finally {
|
||||
setRevokingGrantId(null)
|
||||
if (mountedRef.current) {
|
||||
setRevokingGrantId(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const copyGeneratedUrl = async (target: 'web' | 'pairing', value: string): Promise<void> => {
|
||||
try {
|
||||
await window.api.ui.writeClipboardText(value)
|
||||
setCopiedTarget(target)
|
||||
toast.success(target === 'web' ? 'Copied web client URL.' : 'Copied pairing URL.')
|
||||
if (mountedRef.current) {
|
||||
setCopiedTarget(target)
|
||||
toast.success(target === 'web' ? 'Copied web client URL.' : 'Copied pairing URL.')
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : 'Failed to copy URL.')
|
||||
if (mountedRef.current) {
|
||||
toast.error(error instanceof Error ? error.message : 'Failed to copy URL.')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user