feat(front): cache objects and make them available across components #2448 #2390

2448-complete-tags
Flupsi 2025-06-06 17:38:29 +02:00
rodzic df32d822e9
commit 8cbee81149
3 zmienionych plików z 108 dodań i 20 usunięć

Wyświetl plik

@ -1,5 +1,5 @@
<script setup lang="ts">
import type { Track, Album, Artist, Library, ArtistCredit } from '~/types'
import type { Track, Artist, Library, ArtistCredit } from '~/types'
import { momentFormat } from '~/utils/filters'
import { computed, reactive, ref, watch } from 'vue'
@ -7,6 +7,7 @@ import { useI18n } from 'vue-i18n'
import { useRouter, useRoute } from 'vue-router'
import { sum } from 'lodash-es'
import { useStore } from '~/store'
import { useDataStore } from '~/ui/stores/data'
import { useQueue } from '~/composables/audio/queue'
import axios from 'axios'
@ -35,13 +36,15 @@ interface Props {
}
const store = useStore()
const dataStore = useDataStore()
const emit = defineEmits<Events>()
const props = defineProps<Props>()
const object = ref<Album | null>(null)
const artist = ref<Artist | null>(null)
const artistCredit = ref([] as ArtistCredit[])
const object = computed(() => dataStore.get("album", props.id).value)
const artistCredit = computed(() => object.value?.artist_credit ?? [])
const artist = computed(() => dataStore.get("artist", artistCredit.value[0].artist.id))
const libraries = ref([] as Library[])
const paginateBy = ref(50)
@ -75,20 +78,10 @@ const {
const isLoading = ref(false)
const fetchData = async () => {
isLoading.value = true
const albumResponse = await axios.get(`albums/${props.id}/`, { params: { refresh: 'true' } })
artistCredit.value = albumResponse.data.artist_credit
// fetch the first artist of the album
const artistResponse = await axios.get(`artists/${albumResponse.data.artist_credit[0].artist.id}/`)
artist.value = artistResponse.data
object.value = albumResponse.data
if (object.value) {
if (!object.value)
return
else
object.value.tracks = []
}
fetchTracks()
isLoading.value = false
@ -128,7 +121,7 @@ const fetchTracks = async () => {
}
}
watch(() => props.id, fetchData, { immediate: true })
watch(() => [props.id, object.value], fetchData, { immediate: true })
const router = useRouter()
const route = useRoute()
@ -154,6 +147,7 @@ const remove = async () => {
/>
<Header
v-if="object"
:key="object.title /*Re-render component when title changes after an update*/"
:h1="object.title"
page-heading
>
@ -272,7 +266,7 @@ const remove = async () => {
</Layout>
</Header>
<div style="flex 1;">
<div style="flex: 1;">
<router-view
v-if="object"
:key="route.fullPath"

Wyświetl plik

@ -7,6 +7,7 @@ import { isEqual, clone } from 'lodash-es'
import { useI18n } from 'vue-i18n'
import { useStore } from '~/store'
import { useRoute } from 'vue-router'
import { useDataStore } from '~/ui/stores/data'
import axios from 'axios'
@ -34,14 +35,18 @@ const props = withDefaults(defineProps<Props>(), {
licenses: () => []
})
// Since the object may be reactive (self-updating), we need a clone to compare changes
const originalObject = Object.assign({}, props.object)
const { t } = useI18n()
const configs = useEditConfigs()
const store = useStore()
const dataStore = useDataStore()
const route = useRoute()
const config = computed(() => configs[props.objectType])
const currentState = computed(() => config.value.fields.reduce((state: ReviewState, field) => {
state[field.id] = { value: field.getValue(props.object) }
state[field.id] = { value: field.getValue(originalObject) }
return state
}, {}))
@ -123,6 +128,9 @@ const submit = async () => {
})
submittedMutation.value = response.data
// Immediately re-fetch the updated object into the store
dataStore.get(props.objectType, props.object.id!.toString(), { immediate: true })
} catch (error) {
errors.value = (error as BackendError).backendErrors
}

Wyświetl plik

@ -0,0 +1,86 @@
import { defineStore } from 'pinia'
import { ref, computed, type Ref } from 'vue'
import axios from 'axios'
import type { Album, Artist, Track } from '~/types'
/**
* Fetch an item from the API.
* - Rate limiting: Caches the result for 1 second to prevent over-fetching and request duplication (override with { immediate: true})
* - Sharing reactive objects across components: Avoid data duplication, and auto-update the Ui whenever an updated version of the data is re-fetched
* - Strongly typed results
* - TODO: Errors and Loading states (`undefined` can mean an error occurred, or the request is still pending)
*
* **Example**
* ```ts
* import { useDataStore } from '~/ui/stores/data'
*
* const data = useDataStore()
*
* artist15 = data.get("artist", "15") // Ref<Artist | undefined>
* const album23 = data.get("album", "23") // Ref<Album | undefined>
* ```
* As soon as you re-fetch data, all references to the same object in all components using this store will be updated.
*
* Note: Pinia does not support destructuring.
* Do not write `{ get } = useDataStore()`
*/
export const useDataStore
= defineStore('data', () => {
// Type map that associates cache keys with their corresponding types
type ItemType = {
artist: Artist
album: Album
track: Track
// Add new types here (channel: Channel...)
}
type Items<I extends keyof ItemType> = Record<number | string, {
result: Ref<ItemType[I] | undefined>;
timestamp: number;
}>;
type Cache = {
[I in keyof ItemType]: Items<I>
}
const cache: Cache = {
artist: {},
album: {},
track: {}
}
/** Inspect the cache with the Vue Devtools (Pinia tab); read-only */
const data = computed(() => cache)
/**
* @param type - either 'artist' or 'album' etc.
* @param id - The ID of the item to fetch.
* @param immediate - Whether to re-fetch immediately (default: only re-fetch data older than 1 second)
* @returns an auto-updating reference to `undefined` if there is an error or the item is not yet loaded, or the actual item once it is loaded.
*
* Tip: Re-run after 1 second to refresh the data.
*/
const get = <I extends keyof ItemType>(type: I, id: number | string, options?: { immediate?: boolean }) => {
// Override limited typescript inference (Remove assertion once typescript can infer correctly)
const items = cache[type] as Items<I>
// Initialize the object if it doesn't exist
if (!items[id])
items[id] = { result: ref(undefined) as Ref<ItemType[I] | undefined>, timestamp: 0 }
// Re-fetch if immediate is true or the item is not cached or older than 1 second
if (options?.immediate || items[id].timestamp < Date.now() - 1000)
axios.get<ItemType[I]>(`${type}s/${id}/`, { params: { refresh: 'true' } }).then(({ data }) => {
items[id].result.value = data;
items[id].timestamp = Date.now();
})
return items[id].result
}
return {
data,
get
}
})