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
99 lines
2.8 KiB
TypeScript
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;
|
|
}
|