-
Notifications
You must be signed in to change notification settings - Fork 585
/
Copy pathuse-app-install.ts
139 lines (123 loc) · 4.43 KB
/
use-app-install.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
import {useMutation} from '@tanstack/react-query'
import {useEffect} from 'react'
import {useInterval, usePrevious} from 'react-use'
import {toast} from 'sonner'
import {arrayIncludes} from 'ts-extras'
import {AppState, AppStateOrLoading, trpcClient, trpcReact} from '@/trpc/trpc'
import {t} from '@/utils/i18n'
// TODO: consider adding `stopped` and `unknown`
/** States where we want to frequently poll (on the order of seconds) */
export const pollStates = [
'installing',
'uninstalling',
'updating',
'starting',
'restarting',
'stopping',
] as const satisfies readonly AppState[]
export function useUninstallAllApps() {
const apps = trpcReact.apps.list.useQuery().data
const ctx = trpcReact.useContext()
const mut = useMutation(
async () => {
for (const app of apps ?? []) {
await trpcClient.apps.uninstall.mutate({appId: app.id})
}
},
{
onSuccess: () => {
toast(t('apps.uninstalled-all.success'))
ctx.invalidate()
},
},
)
return () => mut.mutate()
}
// TODO: rename to something that covers more than install
export function useAppInstall(id: string) {
const ctx = trpcReact.useContext()
const appStateQ = trpcReact.apps.state.useQuery({appId: id})
const refreshAppStates = () => {
// Invalidate this app's state
ctx.apps.state.invalidate({appId: id})
// Invalidate list of apps on desktop
ctx.apps.list.invalidate()
// Invalidate latest app opens
ctx.user.get.invalidate()
}
const makeOptimisticOnMutate = (optimisticState: (typeof pollStates)[number]) => () => {
// Optimistic because actions do not return until complete
// see: https://create.t3.gg/en/usage/trpc#optimistic-updates
ctx.apps.state.cancel()
ctx.apps.state.setData({appId: id}, {state: optimisticState, progress: 0})
// Make sure apps list reflects the change in time. This is necessary
// because a request to, say, install an app does not return until the
// action is complete. TODO: Refactor the backend to set the state, return
// early and run the actual action asynchronously.
setTimeout(() => ctx.apps.list.invalidate(), 2000)
}
const startMut = trpcReact.apps.start.useMutation({
onMutate: makeOptimisticOnMutate('starting'),
onSettled: refreshAppStates,
})
const stopMut = trpcReact.apps.stop.useMutation({
onMutate: makeOptimisticOnMutate('stopping'),
onSettled: refreshAppStates,
})
const installMut = trpcReact.apps.install.useMutation({
onMutate: makeOptimisticOnMutate('installing'),
onSettled: refreshAppStates,
})
const uninstallMut = trpcReact.apps.uninstall.useMutation({
onMutate: makeOptimisticOnMutate('uninstalling'),
onSettled: refreshAppStates,
})
const restartMut = trpcReact.apps.restart.useMutation({
onMutate: makeOptimisticOnMutate('restarting'),
onSettled: refreshAppStates,
})
const appState = appStateQ.data?.state
const progress = appStateQ.data?.progress
// Poll for install status if we're installing or uninstalling
const shouldPollForStatus = appState && arrayIncludes(pollStates, appState)
useInterval(appStateQ.refetch, shouldPollForStatus ? 2000 : null)
// Also refresh app states when polling ends in case this tab isn't the one
// owning the mutation and hence isn't notified when it settles
const prevShouldPollForStatus = usePrevious(shouldPollForStatus)
useEffect(() => {
if (!shouldPollForStatus && prevShouldPollForStatus === true) {
refreshAppStates()
}
}, [shouldPollForStatus, prevShouldPollForStatus])
const start = async () => startMut.mutate({appId: id})
const stop = async () => stopMut.mutate({appId: id})
const install = async (alternatives?: Record<string, string>) => {
return installMut.mutate({appId: id, alternatives})
}
const getAppsToUninstallFirst = async () => {
const appsToUninstallFirst = await trpcClient.apps.dependents.query(id)
// We expect to have an array, even if it's empty
if (!appsToUninstallFirst) throw new Error(t('apps.uninstall.failed-to-get-required-apps'))
return appsToUninstallFirst
}
const uninstall = async () => {
const uninstallTheseFirst = await getAppsToUninstallFirst()
if (uninstallTheseFirst.length > 0) {
return {uninstallTheseFirst}
}
uninstallMut.mutate({appId: id})
}
const restart = async () => restartMut.mutate({appId: id})
// Ready means the app can be installed
const state: AppStateOrLoading = appStateQ.isLoading ? 'loading' : (appState ?? 'not-installed')
return {
start,
stop,
restart,
install,
getAppsToUninstallFirst,
uninstall,
progress,
state,
} as const
}