First commit

This commit is contained in:
2026-09-30 01:49:35 +02:00
commit 6edd03aace
109 changed files with 27020 additions and 0 deletions

View 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 }
}