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 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 => { 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) }