119 lines
4.5 KiB
TypeScript
119 lines
4.5 KiB
TypeScript
import type { ArticleMeta } from '~/components/ArticleCard.vue'
|
|
|
|
export interface ReaderArticle extends ArticleMeta {
|
|
content?: string
|
|
}
|
|
|
|
/* Requests in flight on the client, so two panes asking for the same article
|
|
only trouble the API once. (Not touched during SSR, where only the page in
|
|
view is ever fetched.) */
|
|
const pending = new Map<string, Promise<ReaderArticle | null>>()
|
|
|
|
/** What a language version answers to from its address: `fr:bayes-theorem`. */
|
|
export function versionKey(identity: string, lang: string): string {
|
|
return `${lang}:${identity}`
|
|
}
|
|
|
|
/** The article a document belongs to: its own slug unless it translates one. */
|
|
export function identityOf(article: { slug: string; baseSlug?: string | null }): string {
|
|
return article.baseSlug || article.slug
|
|
}
|
|
|
|
/**
|
|
* The text of the pages the open book holds: one entry per article version,
|
|
* in shared state so the book, the route hosts and the middleware all see the
|
|
* same copy. A document answers both to its stored slug (what the panes and
|
|
* tabs are filed under) and, for language versions, to the `lang:identity`
|
|
* key its /wiki/<lang>/<slug> address asks for. The /wiki routes' middleware
|
|
* is what usually fills it, before anything renders — so the server's first
|
|
* render already has the page in view written out.
|
|
*/
|
|
export function useArticleLibrary() {
|
|
const api = `${useRuntimeConfig().public.apiBase}/api`
|
|
const shelf = useState<Record<string, ReaderArticle>>('article-book:shelf', () => ({}))
|
|
/* request key -> the status of the request that failed to bring it in */
|
|
const unreadable = useState<Record<string, number>>('article-book:unreadable', () => ({}))
|
|
|
|
function get(slug: string): ReaderArticle | null {
|
|
return shelf.value[slug] ?? null
|
|
}
|
|
|
|
/** the version an address like /wiki/fr/bayes-theorem asks for, if held */
|
|
function getVersion(identity: string, lang: string): ReaderArticle | null {
|
|
return shelf.value[versionKey(identity, lang)] ?? null
|
|
}
|
|
|
|
function put(key: string, article: ReaderArticle) {
|
|
delete unreadable.value[key]
|
|
delete unreadable.value[versionKey(identityOf(article), article.language ?? 'en')]
|
|
shelf.value[key] = article
|
|
/* a version also answers to its document's own slug, where it was fetched
|
|
from and where the panes keep it */
|
|
shelf.value[article.slug] = article
|
|
}
|
|
|
|
function forget(slug: string) {
|
|
const article = shelf.value[slug]
|
|
if (article) delete shelf.value[versionKey(identityOf(article), article.language ?? 'en')]
|
|
delete shelf.value[slug]
|
|
delete unreadable.value[slug]
|
|
}
|
|
|
|
async function fetchOntoShelf(key: string, url: string): Promise<ReaderArticle | null> {
|
|
const running = pending.get(key)
|
|
if (running) return running
|
|
|
|
const request = (async () => {
|
|
try {
|
|
const article = await $fetch<ReaderArticle>(url)
|
|
put(key, article)
|
|
return article
|
|
} catch (err) {
|
|
/* remember why, and let the page say so in its own words */
|
|
const status = (err as { statusCode?: number; status?: number }) ?? {}
|
|
unreadable.value[key] = status.statusCode ?? status.status ?? 0
|
|
return null
|
|
} finally {
|
|
pending.delete(key)
|
|
}
|
|
})()
|
|
|
|
pending.set(key, request)
|
|
return request
|
|
}
|
|
|
|
/** Bring a document onto the shelf, once. */
|
|
async function load(slug: string): Promise<ReaderArticle | null> {
|
|
const known = shelf.value[slug]
|
|
if (known) return known
|
|
return fetchOntoShelf(slug, `${api}/articles/slug/${encodeURIComponent(slug)}`)
|
|
}
|
|
|
|
/** Bring one language version onto the shelf, once. */
|
|
async function loadVersion(identity: string, lang: string): Promise<ReaderArticle | null> {
|
|
const key = versionKey(identity, lang)
|
|
const known = shelf.value[key]
|
|
if (known) return known
|
|
return fetchOntoShelf(
|
|
key,
|
|
`${api}/articles/slug/${encodeURIComponent(identity)}?lang=${encodeURIComponent(lang)}`
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Read a page again without waiting for it: a page already open is served
|
|
* from the shelf at once, but an edit made elsewhere should still find its
|
|
* way onto the paper.
|
|
*/
|
|
function revalidate(slug: string, lang?: string) {
|
|
const key = lang ? versionKey(slug, lang) : slug
|
|
if (import.meta.server || pending.has(key)) return
|
|
const url = lang
|
|
? `${api}/articles/slug/${encodeURIComponent(slug)}?lang=${encodeURIComponent(lang)}`
|
|
: `${api}/articles/slug/${encodeURIComponent(slug)}`
|
|
void fetchOntoShelf(key, url)
|
|
}
|
|
|
|
return { get, getVersion, put, forget, load, loadVersion, revalidate, unreadable }
|
|
}
|