e66987f092
Extracts gallery.js's hand-rolled inflightId pattern into a new useInflightToken composable; adopts in every store that previously had no guard against late-response overwrites or wrong-image URL interpolation. Two operator-impacting bugs the audit (workflow wf_bbe3fdb1-e62) flagged: - modal.removeTag rolled back the chip rail unconditionally even when only the secondary dismiss POST had failed — UI lied until refresh. And all tag-mutation URLs interpolated currentImageId AFTER an await, so a fast prev/next could route DELETE/POST to the wrong image. Both fixed: split try/catch (dismiss failure surfaces a warning, doesn't roll back the delete); imageId captured at call-time and used in URLs throughout. - suggestions.accept dereferenced currentImageId after the awaited POST /api/tags, so the subsequent /suggestions/accept could apply A's chosen tag to image B AND push it to B's allowlist. Fixed by capturing imageId at click-time + inflight guard on load(). Same shape across artist / downloads / artistDirectory / tagDirectory / posts stores: rapid filter/nav changes used to interleave responses (last-writer-wins). Now the late response is discarded and the most-recent request wins. Filter-change-during- search no longer drops the second fetch because the loading flag was still true from the first. gallery.js's inflightId removed in favor of the shared composable so the pattern stays consistent.
87 lines
2.8 KiB
JavaScript
87 lines
2.8 KiB
JavaScript
import { defineStore } from 'pinia'
|
|
import { ref, computed } from 'vue'
|
|
import { useApi } from '../composables/useApi.js'
|
|
import { useInflightToken } from '../composables/useInflightToken.js'
|
|
import { usePostsStore } from './posts.js'
|
|
|
|
const PAGE = 60
|
|
|
|
export const useArtistStore = defineStore('artist', () => {
|
|
const api = useApi()
|
|
const overview = ref(null)
|
|
const images = ref([])
|
|
const nextCursor = ref(null)
|
|
const loading = ref(false)
|
|
const imagesLoading = ref(false)
|
|
const error = ref(null)
|
|
const notFound = ref(false)
|
|
let started = false
|
|
// Rapid artist-to-artist navigation used to render the previous
|
|
// artist's overview/images briefly when the second load resolved
|
|
// after the third. Audit 2026-06-02.
|
|
const inflight = useInflightToken()
|
|
|
|
async function load (slug) {
|
|
// Cross-artist reset: clear this store AND the posts store so the new
|
|
// artist doesn't briefly render with the previous artist's content
|
|
// when the user is on the Posts tab. (Gallery tab uses this artist
|
|
// store's own images list — cleared above.)
|
|
inflight.cancel()
|
|
overview.value = null
|
|
images.value = []
|
|
nextCursor.value = null
|
|
notFound.value = false
|
|
started = false
|
|
error.value = null
|
|
loading.value = true
|
|
usePostsStore().$reset?.()
|
|
const t = inflight.claim()
|
|
try {
|
|
const body = await api.get(`/api/artist/${encodeURIComponent(slug)}`)
|
|
if (!t.isCurrent()) return
|
|
overview.value = body
|
|
await loadMoreImages(slug)
|
|
} catch (e) {
|
|
if (!t.isCurrent()) return
|
|
if (e.status === 404) notFound.value = true
|
|
else error.value = e.message
|
|
} finally {
|
|
if (t.isCurrent()) loading.value = false
|
|
}
|
|
}
|
|
|
|
async function loadMoreImages (slug) {
|
|
if (imagesLoading.value) return
|
|
if (started && nextCursor.value === null) return
|
|
imagesLoading.value = true
|
|
const t = inflight.claim()
|
|
try {
|
|
const params = { limit: PAGE }
|
|
if (nextCursor.value) params.cursor = nextCursor.value
|
|
const body = await api.get(
|
|
`/api/artist/${encodeURIComponent(slug)}/images`, { params }
|
|
)
|
|
if (!t.isCurrent()) return
|
|
images.value.push(...body.images)
|
|
nextCursor.value = body.next_cursor
|
|
started = true
|
|
} catch (e) {
|
|
if (!t.isCurrent()) return
|
|
error.value = e.message
|
|
} finally {
|
|
if (t.isCurrent()) imagesLoading.value = false
|
|
}
|
|
}
|
|
|
|
const hasMoreImages = computed(() => !started || nextCursor.value !== null)
|
|
const postCount = computed(() => overview.value?.post_count ?? null)
|
|
const imageCount = computed(() => overview.value?.image_count ?? null)
|
|
const lastAdded = computed(() => overview.value?.date_range?.max ?? null)
|
|
|
|
return {
|
|
overview, images, loading, imagesLoading, error, notFound,
|
|
hasMoreImages, postCount, imageCount, lastAdded,
|
|
load, loadMoreImages,
|
|
}
|
|
})
|