perf(backend): Rework sync behaviour (#2050)

This commit is contained in:
Rémi Marseault 2024-11-25 19:34:56 +01:00 committed by GitHub
parent e22ac51d6a
commit 542e5c7d00
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 88 additions and 118 deletions

View file

@ -1,5 +1,4 @@
VITE_QBITTORRENT_TARGET=http://localhost:8080
VITE_BACKEND_TARGET=http://localhost:8081
VITE_USE_MOCK_PROVIDER=false
VITE_FAKE_TORRENTS_COUNT=5

View file

@ -9,7 +9,7 @@ import { backend } from '@/services/backend'
import { useAddTorrentStore, useAppStore, useDialogStore, useLogStore, useMaindataStore, usePreferenceStore, useTorrentStore, useVueTorrentStore } from '@/stores'
import { storeToRefs } from 'pinia'
import { onBeforeMount, onMounted, watch, watchEffect } from 'vue'
import { useI18nUtils } from '@/composables'
import { useBackendSync, useI18nUtils } from '@/composables'
import { toast } from 'vue3-toastify'
const { t } = useI18nUtils()
@ -24,6 +24,10 @@ const preferencesStore = usePreferenceStore()
const vuetorrentStore = useVueTorrentStore()
const { language, uiTitleCustom, uiTitleType, useBitSpeed } = storeToRefs(vuetorrentStore)
const backendSync = useBackendSync(vuetorrentStore, 'vuetorrent_webuiSettings', {
blacklist: ['uiTitleCustom']
})
const checkAuthentication = async () => {
const promise = appStore.fetchAuthStatus()
const timer = setTimeout(() => toast.promise(promise, { pending: t('login.pending') }), 1000)
@ -58,12 +62,6 @@ function addLaunchQueueConsumer() {
}
onBeforeMount(() => {
backend.init(vuetorrentStore.backendUrl)
backend.ping().then(ok => {
if (!backend.isAutoConfig && !ok) {
toast.error(t('toast.backend_unreachable'), { delay: 1000, autoClose: 2500 })
}
})
vuetorrentStore.updateTheme()
vuetorrentStore.setLanguage(language.value)
checkAuthentication()
@ -84,8 +82,16 @@ watch(
await preferencesStore.fetchPreferences()
await logStore.cleanAndFetchLogs()
addTorrentStore.initForm()
backend.ping().then(async ok => {
if (ok) {
await backendSync.loadState()
await backendSync.registerWatcher()
}
})
} else {
maindataStore.stopMaindataSync()
await backendSync.cancelWatcher()
}
},
{

View file

@ -144,10 +144,6 @@ function openDateFormatHelp() {
function openDurationFormatHelp() {
openLink('https://day.js.org/docs/en/durations/format#list-of-all-available-formats')
}
function openBackendHelp() {
openLink('https://github.com/VueTorrent/vuetorrent-backend/wiki/Installation')
}
</script>
<template>
@ -292,16 +288,6 @@ function openBackendHelp() {
append-inner-icon="mdi-help-circle"
@click:appendInner="openDurationFormatHelp" />
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="vueTorrentStore.backendUrl"
:label="t('settings.vuetorrent.general.backendUrl')"
:hint="t('settings.vuetorrent.general.backendUrlHint')"
placeholder="https://YOUR-HOST:PORT/"
append-inner-icon="mdi-help-circle"
@click:appendInner="openBackendHelp" />
</v-col>
</v-row>
</v-list-item>

View file

@ -0,0 +1,53 @@
import { backend } from '@/services/backend'
import { Store } from 'pinia'
import { ref } from 'vue'
export function useBackendSync(store: Store, key: string, config: { blacklist?: string[], whitelist?: string[] } = {}) {
let cancelWatcherCallback = ref<() => void>()
function keyMatchesFilter(k: string) {
return config.whitelist?.includes(k) || !config.blacklist?.includes(k)
}
async function loadState() {
const data = await backend.get(key)
if (data) {
const newState = JSON.parse(data) as Record<string, any>
const temp = {} as Record<string, any>
Object.entries(newState).forEach(([k, v]) => {
if (keyMatchesFilter(k)) {
temp[k] = v
}
})
store.$patch(temp)
}
}
async function saveState() {
const state = {} as Record<string, any>
Object.entries(store.$state).forEach(([k, v]) => {
if (keyMatchesFilter(k)) {
state[k] = v
}
})
await backend.set(key, JSON.stringify(state))
}
async function registerWatcher() {
cancelWatcherCallback.value = store.$subscribe(() => {
saveState()
})
}
async function cancelWatcher() {
cancelWatcherCallback.value && cancelWatcherCallback.value()
}
return {
loadState,
saveState,
registerWatcher,
cancelWatcher
}
}

View file

@ -1,8 +1,9 @@
import { useArrayPagination } from './ArrayPagination'
import { useBackendSync } from './BackendSync'
import { useDialog } from './Dialog'
import { useI18nUtils } from './i18n'
import { useSearchQuery } from './SearchQuery'
import { useTorrentBuilder } from './TorrentBuilder'
import { useTreeBuilder } from './TreeBuilder'
export { useArrayPagination, useDialog, useI18nUtils, useSearchQuery, useTorrentBuilder, useTreeBuilder }
export { useArrayPagination, useBackendSync, useDialog, useI18nUtils, useSearchQuery, useTorrentBuilder, useTreeBuilder }

View file

@ -13,10 +13,9 @@ import VTorrentCardGrid from '@/components/Settings/VueTorrent/TorrentCard/Grid.
import VTorrentCardList from '@/components/Settings/VueTorrent/TorrentCard/List.vue'
import VTorrentCardTable from '@/components/Settings/VueTorrent/TorrentCard/Table.vue'
import WebUI from '@/components/Settings/WebUI.vue'
import { backend } from '@/services/backend'
import { useDialogStore, usePreferenceStore, useVueTorrentStore } from '@/stores'
import { computed, onBeforeUnmount, onMounted, ref, watchEffect } from 'vue'
import { useI18nUtils } from '@/composables'
import { useDialogStore, usePreferenceStore } from '@/stores'
import { computed, onBeforeUnmount, onMounted, ref, watchEffect } from 'vue'
import { useRouter } from 'vue-router'
import { toast } from 'vue3-toastify'
@ -24,7 +23,6 @@ const router = useRouter()
const { t } = useI18nUtils()
const dialogStore = useDialogStore()
const preferenceStore = usePreferenceStore()
const vuetorrentStore = useVueTorrentStore()
const tabs = [
{ text: t('settings.tabs.vuetorrent.title'), value: 'vuetorrent' },
@ -56,10 +54,6 @@ const saveSettings = async () => {
toast.success(t('settings.saveSuccess'))
await preferenceStore.fetchPreferences()
const oldInit = backend.isInitialized && !backend.isAutoConfig
backend.init(vuetorrentStore.backendUrl)
const newInit = backend.isInitialized && !backend.isAutoConfig
if (!preferenceStore.preferences!.alternative_webui_enabled) {
if ('serviceWorker' in navigator) {
const registrations = await navigator.serviceWorker.getRegistrations()
@ -69,15 +63,6 @@ const saveSettings = async () => {
}
location.hash = ''
location.reload()
} else {
if (!oldInit && newInit) {
location.reload()
} else {
const ok = await backend.ping()
if (!backend.isAutoConfig && !ok) {
toast.error(t('toast.backend_unreachable'), { delay: 1000, autoClose: 2500 })
}
}
}
}

View file

@ -1,15 +1,16 @@
import { StorageLikeAsync } from '@vueuse/core'
import axios, { AxiosInstance } from 'axios'
class BackendProvider {
private axios: AxiosInstance
private _isInitialized: boolean = false
private _isAutoConfig: boolean = false
private up: boolean = true
private pingPromise: Promise<boolean> | null = null
private up: boolean = false
constructor() {
let baseURL = `${location.origin}${location.pathname}`
if (!baseURL.endsWith('/')) baseURL += '/'
baseURL += 'backend'
this.axios = axios.create({
baseURL,
withCredentials: true,
headers: {
put: { 'Content-Type': 'application/json' }
@ -17,41 +18,12 @@ class BackendProvider {
})
}
get isInitialized() {
return this._isInitialized
}
get isAutoConfig() {
return this._isAutoConfig
}
init(baseURL?: string) {
if (!baseURL) {
baseURL = `${location.origin}${location.pathname}`
if (baseURL.endsWith('/')) {
baseURL = baseURL.slice(0, -1)
}
baseURL += '/backend'
this._isAutoConfig = true
} else {
this._isAutoConfig = false
}
this.axios.defaults.baseURL = baseURL
this._isInitialized = !!baseURL
}
/**
* Ping the backend to check if it's up
* @returns true if backend is up, false otherwise
*/
async ping(): Promise<boolean> {
if (!this._isInitialized) return false
if (this.pingPromise) {
return this.pingPromise
}
this.pingPromise = this.axios
return await this.axios
.get('/ping')
.then(
res => res.data === 'pong',
@ -59,30 +31,16 @@ class BackendProvider {
)
.then(ok => {
this.up = ok
this.pingPromise = null
return ok
})
return await this.pingPromise
}
private async waitForPing() {
if (this.pingPromise) {
await this.pingPromise
}
}
shouldDiscardCalls() {
return !this._isInitialized || !this.up
}
/**
* Get all values
*/
async getAll(): Promise<Record<string, string>> {
await this.waitForPing()
if (this.shouldDiscardCalls()) return {}
async getAll(): Promise<Record<string, string> | null> {
if (!this.up) return null
return this.axios.get('/config').then(res => res.data)
}
@ -93,8 +51,7 @@ class BackendProvider {
* @returns string or null if key doesn't exists
*/
async get(key: string): Promise<string | null> {
await this.waitForPing()
if (this.shouldDiscardCalls()) return null
if (!this.up) return null
return this.axios.get(`/config/${key}`).then(
res => res.data[key],
@ -109,8 +66,7 @@ class BackendProvider {
* @returns true if value was set, false otherwise
*/
async set(key: string, value: string): Promise<boolean> {
await this.waitForPing()
if (this.shouldDiscardCalls()) return false
if (!this.up) return false
return this.axios.put(`/config/${key}`, { value }).then(
() => true,
@ -124,25 +80,17 @@ class BackendProvider {
* @returns true if value was deleted, false otherwise
*/
async del(key: string): Promise<boolean> {
await this.waitForPing()
if (this.shouldDiscardCalls()) return false
if (!this.up) return false
return this.axios.delete(`/config/${key}`).then(
() => true,
() => false
)
}
async update() {
return this.axios.get('/update')
}
}
export const backend = new BackendProvider()
export const backendStorage: StorageLikeAsync = {
async getItem(key) {
return await backend.get(key)
},
async setItem(key, state) {
await backend.set(key, state)
},
async removeItem(key) {
await backend.del(key)
}
}

View file

@ -10,7 +10,6 @@ import {
TitleOptions,
TorrentProperty
} from '@/constants/vuetorrent'
import { backendStorage } from '@/services/backend'
import { DarkLegacy, LightLegacy } from '@/themes'
import { useMediaQuery } from '@vueuse/core'
import { acceptHMRUpdate, defineStore } from 'pinia'
@ -22,8 +21,6 @@ import { useTheme } from 'vuetify'
export const useVueTorrentStore = defineStore(
'vuetorrent',
() => {
const backendUrl = ref('')
const language = ref('en')
const theme = reactive({
mode: ThemeMode.SYSTEM,
@ -235,7 +232,6 @@ export const useVueTorrentStore = defineStore(
}
return {
backendUrl,
theme,
dateFormat,
durationFormat,
@ -291,7 +287,6 @@ export const useVueTorrentStore = defineStore(
toggleDoneGridProperty,
toggleTableProperty,
$reset: () => {
backendUrl.value = ''
language.value = 'en'
theme.mode = ThemeMode.SYSTEM
theme.light = LightLegacy.id
@ -338,8 +333,7 @@ export const useVueTorrentStore = defineStore(
persistence: {
enabled: true,
storageItems: [
{ storage: localStorage, key: 'webuiSettings' },
{ storage: backendStorage, key: 'webuiSettings', excludePaths: ['backendUrl', 'uiTitleCustom'] }
{ storage: localStorage, key: 'webuiSettings' }
]
}
}

1
src/vite-env.d.ts vendored
View file

@ -6,7 +6,6 @@ interface ImportMetaEnv extends BaseImportMetaEnv {
readonly VITE_PACKAGE_VERSION: string
readonly VITE_QBITTORRENT_TARGET: string
readonly VITE_QBITTORRENT_PORT: number
readonly VITE_USE_MOCK_PROVIDER: string
readonly VITE_FAKE_TORRENTS_COUNT: number

View file

@ -10,7 +10,6 @@ import vuetify from 'vite-plugin-vuetify'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd())
const qBittorrentTarget = env.VITE_QBITTORRENT_TARGET ?? 'http://localhost:8080'
const backendTarget = env.VITE_BACKEND_TARGET ?? 'http://localhost:3000'
return {
base: './',
@ -65,7 +64,7 @@ export default defineConfig(({ mode }) => {
'/backend': {
secure: false,
changeOrigin: true,
target: backendTarget
target: qBittorrentTarget
}
}
},