Notes/server/src/nextcloud/topicCache.ts
Claudio Schaad be421d83cb Rename Book to Topic throughout the app
The book/topic field was really always a general-purpose tag, and now
that notes can come from more than just books (camera-scanned pages,
free text), "book" was misleading in the UI as well as the code.
Renames files, functions, variables, the API route (/api/books ->
/api/topics), the note payload field, and the cache TTL env var
(BOOK_CACHE_TTL_MS -> TOPIC_CACHE_TTL_MS) for consistency.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C3DGg1pvmvf81zXs4pk1J7
2026-08-27 18:55:51 +02:00

99 lines
2.8 KiB
TypeScript

import { config } from "../config.js";
import {
createPage,
findRootPage,
listCollectives,
listPages,
type PageInfoDto
} from "./collectivesClient.js";
let collectiveIdPromise: Promise<number> | null = null;
let parentPagePromise: Promise<PageInfoDto> | null = null;
let cachedTopics: Map<string, PageInfoDto> | null = null;
let cachedAt = 0;
function getCollectiveId(): Promise<number> {
if (!collectiveIdPromise) {
collectiveIdPromise = listCollectives().then((collectives) => {
const match = collectives.find((c) => c.name === config.collectiveName);
if (!match) {
const available = collectives.map((c) => c.name).join(", ");
throw new Error(
`Collective "${config.collectiveName}" not found. Available collectives: ${available}`
);
}
return match.id;
});
}
return collectiveIdPromise;
}
export function getParentPage(): Promise<PageInfoDto> {
if (!parentPagePromise) {
parentPagePromise = (async () => {
const collectiveId = await getCollectiveId();
const pages = await listPages(collectiveId);
const root = findRootPage(pages);
if (!root) {
throw new Error(`Could not find root page for collective "${config.collectiveName}"`);
}
const existing = pages.find(
(p) => p.parentId === root.id && p.title === config.parentPageTitle
);
if (existing) {
return existing;
}
return createPage(collectiveId, root.id, config.parentPageTitle);
})();
}
return parentPagePromise;
}
async function refreshTopics(force = false): Promise<Map<string, PageInfoDto>> {
if (!force && cachedTopics && Date.now() - cachedAt < config.topicCacheTtlMs) {
return cachedTopics;
}
const collectiveId = await getCollectiveId();
const parent = await getParentPage();
const pages = await listPages(collectiveId);
const map = new Map<string, PageInfoDto>();
for (const page of pages) {
if (page.parentId === parent.id) {
map.set(page.title, page);
}
}
cachedTopics = map;
cachedAt = Date.now();
return map;
}
export async function listTopics(): Promise<{ title: string }[]> {
const map = await refreshTopics();
return [...map.keys()].sort((a, b) => a.localeCompare(b)).map((title) => ({ title }));
}
export async function getOrCreateTopicPage(title: string): Promise<PageInfoDto> {
let map = await refreshTopics();
let page = map.get(title);
if (page) {
return page;
}
map = await refreshTopics(true);
page = map.get(title);
if (page) {
return page;
}
const collectiveId = await getCollectiveId();
const parent = await getParentPage();
const created = await createPage(collectiveId, parent.id, title);
map.set(created.title, created);
cachedTopics = map;
return created;
}