ファイル
btrc-hub/frontend/src/lib/prefetchers.ts
T
2026-10-19 23:08:59 +09:00

339 行
12 KiB
TypeScript

import { QueryClient } from '@tanstack/react-query'
import { match } from 'path-to-regexp'
import { getHelpArticle,
getHelpChanges,
getHelpDiff,
getHelpEditionContent,
getHelpSearchEntries,
resolveHelpLanguage } from '@/lib/help'
import { localeFromPath, localeLanguage, unlocalisedPath } from '@/lib/locales'
import { fetchPost, fetchPosts, fetchPostChanges } from '@/lib/posts'
import { helpKeys, postsKeys, tagsKeys, wikiKeys } from '@/lib/queryKeys'
import { fetchNicoTags, fetchTag, fetchTagChanges, fetchTags } from '@/lib/tags'
import { fetchWikiAutolinkCandidates,
fetchWikiPageByReference,
fetchWikiTag,
fetchWikiPages } from '@/lib/wiki'
import type { LocaleCode } from '@/lib/locales'
import type { Category, FetchPostsOrder, FetchTagsOrder, WikiTagReference } from '@/types'
type Prefetcher = (qc: QueryClient, url: URL, locale: LocaleCode) => Promise<void>
const mPost = match<{ id: string }> ('/posts/:id')
const mWiki = match<{ title: string }> ('/wiki/:title')
const mTag = match<{ id: string }> ('/tags/:id')
const mHelp = match<{ identifier: string }> ('/help/:identifier')
const mHelpDiff = match<{ identifier: string }> ('/help/:identifier/diff')
const boolFromQuery = (value: string | null): boolean =>
['1', 'true', 'on', 'yes', ''].includes ((value ?? '').toLowerCase ())
const prefetchHelpIndex: Prefetcher = async (qc, _url, locale) => {
await qc.prefetchQuery ({
queryKey: helpKeys.index (localeLanguage (locale)),
queryFn: () => getHelpSearchEntries (localeLanguage (locale)),
staleTime: Infinity })
}
const prefetchHelpShow: Prefetcher = async (qc, url, locale) => {
const matched = mHelp (url.pathname)
if (!(matched))
return
const { identifier } = matched.params
const article = await qc.fetchQuery ({
queryKey: helpKeys.article (identifier),
queryFn: () => getHelpArticle (identifier),
staleTime: Infinity })
const language = resolveHelpLanguage (article, localeLanguage (locale))
if (language == null)
return
const edition = url.searchParams.get ('edition') ?? article?.languages[language]?.currentEdition
if (edition == null)
return
await qc.prefetchQuery ({
queryKey: helpKeys.edition (identifier, language, edition),
queryFn: () => getHelpEditionContent (identifier, language, edition),
staleTime: Infinity })
}
const prefetchHelpChanges: Prefetcher = async (qc, _url, locale) => {
await qc.prefetchQuery ({
queryKey: helpKeys.changes (localeLanguage (locale)),
queryFn: () => getHelpChanges (localeLanguage (locale)),
staleTime: Infinity })
}
const prefetchHelpDiff: Prefetcher = async (qc, url, locale) => {
const matched = mHelpDiff (url.pathname)
const fromValues = url.searchParams.getAll ('from')
const toValues = url.searchParams.getAll ('to')
const [from] = fromValues
const [to] = toValues
if (!(matched) || fromValues.length !== 1 || toValues.length !== 1 || !(from) || !(to))
return
const { identifier } = matched.params
const article = await qc.fetchQuery ({
queryKey: helpKeys.article (identifier),
queryFn: () => getHelpArticle (identifier),
staleTime: Infinity })
const language = resolveHelpLanguage (article, localeLanguage (locale))
if (language == null)
return
await qc.prefetchQuery ({
queryKey: helpKeys.diff (identifier, language, from, to),
queryFn: () => getHelpDiff (identifier, language, from, to),
staleTime: Infinity })
}
const prefetchWikiPagesIndex: Prefetcher = async (qc, url, locale) => {
const title = url.searchParams.get ('title') ?? ''
await qc.prefetchQuery ({
queryKey: wikiKeys.index ({ title }, locale),
queryFn: () => fetchWikiPages ({ title }, locale) })
}
const prefetchWikiPageShow: Prefetcher = async (qc, url, locale) => {
const m = mWiki (url.pathname)
if (!(m))
return
const title = decodeURIComponent (m.params.title)
const version = url.searchParams.get ('version') || undefined
const wikiPage = await qc.fetchQuery ({
queryKey: wikiKeys.show (title, { version }, locale),
queryFn: () => fetchWikiPageByReference (title, { version }, locale) })
if (wikiPage?.body)
{
const contentLanguage = wikiPage.languageCode
?? (wikiPage.resourceType === 'tag_description' ? localeLanguage (locale) : 'und')
const language = contentLanguage === 'und' ? 'ja' : contentLanguage
await qc.prefetchQuery ({
queryKey: wikiKeys.autolinkCandidates (language, locale),
queryFn: () => fetchWikiAutolinkCandidates (language, locale) })
}
const effectiveTitle = wikiPage?.title ?? title
const tagReference: WikiTagReference = (wikiPage != null || !/^\d+$/.test (title))
? { name: effectiveTitle } : { wikiId: title }
const tag = await qc.fetchQuery ({
queryKey: wikiKeys.tag (tagReference, locale),
queryFn: () => fetchWikiTag (tagReference, locale) })
if (version)
return
const p = {
tags: wikiPage?.title ?? tag?.name ?? title,
match: 'all',
page: 1,
limit: 8,
url: '',
title: '',
originalCreatedFrom: '',
originalCreatedTo: '',
createdFrom: '',
createdTo: '',
updatedFrom: '',
updatedTo: '',
order: 'original_created_at:desc' } as const
await qc.prefetchQuery ({
queryKey: postsKeys.index (p, locale),
queryFn: () => fetchPosts (p, locale) })
}
const prefetchPostsIndex: Prefetcher = async (qc, url, locale) => {
const tags = url.searchParams.get ('tags') ?? ''
const qURL = url.searchParams.get ('url') ?? ''
const title = url.searchParams.get ('title') ?? ''
const originalCreatedFrom = url.searchParams.get ('original_created_from') ?? ''
const originalCreatedTo = url.searchParams.get ('original_created_to') ?? ''
const createdFrom = url.searchParams.get ('created_from') ?? ''
const createdTo = url.searchParams.get ('created_to') ?? ''
const updatedFrom = url.searchParams.get ('updated_from') ?? ''
const updatedTo = url.searchParams.get ('updated_to') ?? ''
const m: 'all' | 'any' = url.searchParams.get ('match') === 'any' ? 'any' : 'all'
const page = Number (url.searchParams.get ('page') || 1)
const limit = Number (url.searchParams.get ('limit') || 20)
const order = (url.searchParams.get ('order') ?? 'original_created_at:desc') as FetchPostsOrder
const keys = {
tags, match: m, page, limit, url: qURL, title,
...(url.searchParams.get ('tag_refs') && {
tagReferences: url.searchParams.get ('tag_refs')! }),
originalCreatedFrom, originalCreatedTo, createdFrom, createdTo,
updatedFrom, updatedTo, order }
await qc.prefetchQuery ({
queryKey: postsKeys.index (keys, locale),
queryFn: () => fetchPosts (keys, locale) })
}
const prefetchPostShow: Prefetcher = async (qc, url, locale) => {
const m = mPost (url.pathname)
if (!(m))
return
const { id } = m.params
await qc.prefetchQuery ({
queryKey: postsKeys.show (id, locale),
queryFn: () => fetchPost (id, locale) })
}
const prefetchPostChanges: Prefetcher = async (qc, url, locale) => {
const id = url.searchParams.get ('id')
const tag = url.searchParams.get ('tag')
const externalTag = url.searchParams.get ('external_tag')
const page = Number (url.searchParams.get ('page') || 1)
const limit = Number (url.searchParams.get ('limit') || 20)
if (tag)
{
await qc.prefetchQuery ({
queryKey: tagsKeys.show (tag, locale),
queryFn: () => fetchTag (tag, locale) })
}
await qc.prefetchQuery ({
queryKey: postsKeys.changes ({ ...(id && { id }),
...(tag && { tag }),
...(externalTag && { externalTag }),
page, limit }, locale),
queryFn: () => fetchPostChanges ({ ...(id && { id }),
...(tag && { tag }),
...(externalTag && { externalTag }),
page, limit }, locale) })
}
const prefetchTagsIndex: Prefetcher = async (qc, url, locale) => {
const postRaw = url.searchParams.get ('post')
const post = postRaw ? Number (postRaw) : null
const name = url.searchParams.get ('name') ?? ''
const category = (url.searchParams.get ('category') || null) as Category | null
const postCountGTE = Number (url.searchParams.get ('post_count_gte') || 1)
const postCountLTERaw = url.searchParams.get ('post_count_lte')
const postCountLTE = postCountLTERaw ? Number (postCountLTERaw) : null
const createdFrom = url.searchParams.get ('created_from') ?? ''
const createdTo = url.searchParams.get ('created_to') ?? ''
const updatedFrom = url.searchParams.get ('updated_from') ?? ''
const updatedTo = url.searchParams.get ('updated_to') ?? ''
const deprecated = url.searchParams.has ('deprecated')
? boolFromQuery (url.searchParams.get ('deprecated'))
: null
const page = Number (url.searchParams.get ('page') || 1)
const limit = Number (url.searchParams.get ('limit') || 20)
const order = (url.searchParams.get ('order') ?? 'post_count:desc') as FetchTagsOrder
const keys = {
post, name, category, postCountGTE, postCountLTE, createdFrom, createdTo,
updatedFrom, updatedTo, deprecated, page, limit, order }
await qc.prefetchQuery ({
queryKey: tagsKeys.index (keys, locale),
queryFn: () => fetchTags (keys, locale) })
}
const prefetchNicoTagsIndex: Prefetcher = async (qc, url, locale) => {
const keys = {
name: url.searchParams.get ('name') ?? '',
linkedTag: url.searchParams.get ('linked_tag') ?? '',
linkStatus: (url.searchParams.get ('link_status') || 'all') as
'all' | 'linked' | 'unlinked',
page: Number (url.searchParams.get ('page') || 1),
limit: Number (url.searchParams.get ('limit') || 20),
order: (url.searchParams.get ('order') || 'updated_at:desc') as
'name:asc' | 'name:desc' | 'created_at:asc' | 'created_at:desc'
| 'updated_at:asc' | 'updated_at:desc' }
await qc.prefetchQuery ({
queryKey: tagsKeys.nicoIndex (keys, locale),
queryFn: () => fetchNicoTags (keys, locale) })
}
const prefetchTagShow: Prefetcher = async (qc, url, locale) => {
const m = mTag (url.pathname)
if (!(m))
return
const { id } = m.params
await qc.prefetchQuery ({
queryKey: tagsKeys.show (id, locale),
queryFn: () => fetchTag (id, locale) })
}
const prefetchTagChanges: Prefetcher = async (qc, url, locale) => {
const id = url.searchParams.get ('id')
const page = Number (url.searchParams.get ('page') || 1)
const limit = Number (url.searchParams.get ('limit') || 20)
await qc.prefetchQuery ({
queryKey: tagsKeys.changes ({ ...(id && { id }), page, limit }, locale),
queryFn: () => fetchTagChanges ({ ...(id && { id }), page, limit }, locale) })
}
export const routePrefetchers: { test: (u: URL) => boolean; run: Prefetcher }[] = [
{ test: u => /^\/help\/?$/.test (u.pathname), run: prefetchHelpIndex },
{ test: u => /^\/help\/changes\/?$/.test (u.pathname), run: prefetchHelpChanges },
{ test: u => Boolean (mHelpDiff (u.pathname)), run: prefetchHelpDiff },
{ test: u => {
const matched = mHelp (u.pathname)
return Boolean (matched && matched.params.identifier !== 'changes')
}, run: prefetchHelpShow },
{ test: u => ['/', '/posts', '/posts/search'].includes (u.pathname),
run: prefetchPostsIndex },
{ test: u => (!(['/posts/new', '/posts/changes', '/posts/search'].includes (u.pathname))
&& Boolean (mPost (u.pathname))),
run: prefetchPostShow },
{ test: u => u.pathname === '/posts/changes', run: prefetchPostChanges },
{ test: u => u.pathname === '/wiki', run: prefetchWikiPagesIndex },
{ test: u => (!(['/wiki/new', '/wiki/changes'].includes (u.pathname))
&& Boolean (mWiki (u.pathname))),
run: prefetchWikiPageShow },
{ test: u => u.pathname === '/tags', run: prefetchTagsIndex },
{ test: u => ['/tags/nico', '/nico/tags'].includes (u.pathname),
run: prefetchNicoTagsIndex },
{ test: u => (!(['/tags/nico', '/tags/changes'].includes (u.pathname))
&& Boolean (mTag (u.pathname))),
run: prefetchTagShow },
{ test: u => u.pathname === '/tags/changes', run: prefetchTagChanges }]
export const prefetchForURL = async (qc: QueryClient, urlLike: string): Promise<void> => {
const u = new URL (urlLike, location.origin)
if (u.origin !== location.origin)
return
const locale = localeFromPath (u.pathname) ?? 'ja'
u.pathname = unlocalisedPath (u.pathname)
const r = routePrefetchers.find (x => x.test (u))
if (!(r))
return
await r.run (qc, u, locale)
}