プリフェッチ実装(#140) (#256)

Merge branch 'main' into feature/140

#140

Merge remote-tracking branch 'origin/main' into feature/140

#140

#140

#140

#140

#140

Merge remote-tracking branch 'origin/main' into feature/140

#140

#140

#140

#140

#140

#140

#140

#140

#140

#140

#140

Merge remote-tracking branch 'origin/main' into feature/140

Merge remote-tracking branch 'origin/main' into feature/140

#140 ぼちぼち

Merge remote-tracking branch 'origin/main' into feature/140

#140

#140

#140

Co-authored-by: miteruzo <miteruzo@naver.com>
Reviewed-on: #256
This commit was merged in pull request #256.
This commit is contained in:
2026-02-11 13:27:28 +09:00
parent 1a776e348a
commit eb975e5301
30 changed files with 517 additions and 488 deletions
+12 -2
View File
@@ -3,9 +3,12 @@ import toCamel from 'camelcase-keys'
import { API_BASE_URL } from '@/config'
import type { AxiosError, AxiosRequestConfig } from 'axios'
type Opt = {
params?: Record<string, unknown>
headers?: Record<string, string> }
params?: AxiosRequestConfig['params']
headers?: Record<string, string>
responseType?: 'blob' }
const client = axios.create ({ baseURL: API_BASE_URL })
@@ -23,6 +26,8 @@ const apiP = async <T> (
opt?: Opt,
): Promise<T> => {
const res = await client[method] (path, body ?? { }, withUserCode (opt))
if (opt?.responseType === 'blob')
return res.data as T
return toCamel (res.data as any, { deep: true }) as T
}
@@ -32,6 +37,8 @@ export const apiGet = async <T> (
opt?: Opt,
): Promise<T> => {
const res = await client.get (path, withUserCode (opt))
if (opt?.responseType === 'blob')
return res.data as T
return toCamel (res.data as any, { deep: true }) as T
}
@@ -63,3 +70,6 @@ export const apiDelete = async (
): Promise<void> => {
await client.delete (path, withUserCode (opt))
}
export const isApiError = (err: unknown): err is AxiosError => axios.isAxiosError (err)
+14 -3
View File
@@ -1,6 +1,6 @@
import { apiDelete, apiGet, apiPost } from '@/lib/api'
import type { Post } from '@/types'
import type { Post, PostTagChange } from '@/types'
export const fetchPosts = async (
@@ -13,8 +13,8 @@ export const fetchPosts = async (
): Promise<{
posts: Post[]
count: number
nextCursor: string }> => await apiGet ('/posts', {
params: {
nextCursor: string }> =>
await apiGet ('/posts', { params: {
tags,
match,
...(page && { page }),
@@ -25,6 +25,17 @@ export const fetchPosts = async (
export const fetchPost = async (id: string): Promise<Post> => await apiGet (`/posts/${ id }`)
export const fetchPostChanges = async (
{ id, page, limit }: {
id?: string
page: number
limit: number },
): Promise<{
changes: PostTagChange[]
count: number }> =>
await apiGet ('/posts/changes', { params: { ...(id && { id }), page, limit } })
export const toggleViewedFlg = async (id: string, viewed: boolean): Promise<void> => {
await (viewed ? apiPost : apiDelete) (`/posts/${ id }/viewed`)
}
+84 -10
View File
@@ -1,12 +1,69 @@
import { QueryClient } from '@tanstack/react-query'
import { match } from 'path-to-regexp'
import { fetchPost, fetchPosts } from '@/lib/posts'
import { postsKeys } from '@/lib/queryKeys'
import { fetchPost, fetchPosts, fetchPostChanges } from '@/lib/posts'
import { postsKeys, tagsKeys, wikiKeys } from '@/lib/queryKeys'
import { fetchTagByName } from '@/lib/tags'
import { fetchWikiPage,
fetchWikiPageByTitle,
fetchWikiPages } from '@/lib/wiki'
type Prefetcher = (qc: QueryClient, url: URL) => Promise<void>
const mPost = match<{ id: string }> ('/posts/:id')
const mWiki = match<{ title: string }> ('/wiki/:title')
const prefetchWikiPagesIndex: Prefetcher = async (qc, url) => {
const title = url.searchParams.get ('title') ?? ''
await qc.prefetchQuery ({
queryKey: wikiKeys.index ({ title }),
queryFn: () => fetchWikiPages ({ title }) })
}
const prefetchWikiPageShow: Prefetcher = async (qc, url) => {
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 }),
queryFn: () => fetchWikiPageByTitle (title, { version }) })
if (wikiPage)
{
const effectiveId = String (wikiPage.id ?? '')
await qc.prefetchQuery ({
queryKey: wikiKeys.show (effectiveId, { }),
queryFn: () => fetchWikiPage (effectiveId, { } ) })
if (wikiPage.body)
{
await qc.prefetchQuery ({
queryKey: wikiKeys.index ({ }),
queryFn: () => fetchWikiPages ({ }) })
}
}
const effectiveTitle = wikiPage?.title ?? title
await qc.prefetchQuery ({
queryKey: tagsKeys.show (effectiveTitle),
queryFn: () => fetchTagByName (effectiveTitle) })
if (version)
return
const p = { tags: effectiveTitle, match: 'all', page: 1, limit: 8 } as const
await qc.prefetchQuery ({
queryKey: postsKeys.index (p),
queryFn: () => fetchPosts (p) })
}
const prefetchPostsIndex: Prefetcher = async (qc, url) => {
@@ -14,6 +71,7 @@ const prefetchPostsIndex: Prefetcher = async (qc, url) => {
const m = url.searchParams.get ('match') === 'any' ? 'any' : 'all'
const page = Number (url.searchParams.get ('page') || 1)
const limit = Number (url.searchParams.get ('limit') || 20)
await qc.prefetchQuery ({
queryKey: postsKeys.index ({ tags, match: m, page, limit }),
queryFn: () => fetchPosts ({ tags, match: m, page, limit }) })
@@ -26,24 +84,40 @@ const prefetchPostShow: Prefetcher = async (qc, url) => {
return
const { id } = m.params
await qc.prefetchQuery ({
queryKey: postsKeys.show (id),
queryFn: () => fetchPost (id) })
}
export const routePrefetchers: {
test: (u: URL) => boolean
run: Prefetcher }[] = [
const prefetchPostChanges: Prefetcher = async (qc, url) => {
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: postsKeys.changes ({ ...(id && { id }), page, limit }),
queryFn: () => fetchPostChanges ({ ...(id && { id }), page, limit }) })
}
export const routePrefetchers: { test: (u: URL) => boolean; run: Prefetcher }[] = [
{ test: u => u.pathname === '/' || u.pathname === '/posts', run: prefetchPostsIndex },
{ test: u => Boolean (mPost (u.pathname)), run: prefetchPostShow }]
{ test: u => (!(['/posts/new', '/posts/changes'].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 }]
export const prefetchForURL = async (qc: QueryClient, urlLike: string): Promise<void> => {
const u = new URL (urlLike, location.origin)
const jobs = routePrefetchers.filter (r => r.test (u)).map (r => r.run (qc, u))
if (jobs.length === 0)
const r = routePrefetchers.find (x => x.test (u))
if (!(r))
return
await Promise.all (jobs)
await r.run (qc, u)
}
+10 -3
View File
@@ -3,8 +3,15 @@ export const postsKeys = {
index: (p: { tags: string; match: 'any' | 'all'; page: number; limit: number }) =>
['posts', 'index', p] as const,
show: (id: string) => ['posts', id] as const,
related: (id: string) => ['related', id] as const }
related: (id: string) => ['related', id] as const,
changes: (p: { id?: string; page: number; limit: number }) =>
['posts', 'changes', p] as const }
export const tagsKeys = {
root: ['tags'] as const,
show: (name: string) => ['tags', name] as const }
export const wikiKeys = {
root: ['wiki'] as const,
show: (title: string, p: { version: string }) => ['wiki', title, p] as const }
root: ['wiki'] as const,
index: (p: { title?: string }) => ['wiki', 'index', p] as const,
show: (title: string, p: { version?: string }) => ['wiki', title, p] as const }
+10 -2
View File
@@ -3,5 +3,13 @@ import { apiGet } from '@/lib/api'
import type { Tag } from '@/types'
export const fetchTagByName = async (name: string): Promise<Tag> =>
await apiGet (`/tags/name/${ name }`)
export const fetchTagByName = async (name: string): Promise<Tag | null> => {
try
{
return await apiGet (`/tags/name/${ name }`)
}
catch
{
return null
}
}
+21 -4
View File
@@ -3,12 +3,29 @@ import { apiGet } from '@/lib/api'
import type { WikiPage } from '@/types'
export const fetchWikiPage = async (id: string): Promise<WikiPage> =>
await apiGet (`/wiki/${ id }`)
export const fetchWikiPages = async (
{ title }: { title?: string },
): Promise<WikiPage[]> =>
await apiGet ('/wiki', { params: { title } })
export const fetchWikiPage = async (
id: string,
{ version }: { version?: string },
): Promise<WikiPage> =>
await apiGet (`/wiki/${ id }`, { params: version ? { version } : { } })
export const fetchWikiPageByTitle = async (
title: string,
{ version }: { version?: string },
): Promise<WikiPage> =>
await apiGet (`/wiki/title/${ title }`, { params: version ? { version } : { } })
): Promise<WikiPage | null> => {
try
{
return await apiGet (`/wiki/title/${ encodeURIComponent (title) }`, { params: { version } })
}
catch
{
return null
}
}