First commit
This commit is contained in:
118
frontend/app/composables/useArticleLibrary.ts
Normal file
118
frontend/app/composables/useArticleLibrary.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
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 }
|
||||
}
|
||||
Reference in New Issue
Block a user