Files
cmkiss/js/cache.js
T
biosiasandClaude Sonnet 5 1dc5e16d14 Flatten cmjs/ into the repo root (AI)
The repo is already named cmjs, so the nested cmjs/ subfolder was
redundant. Moved css/, js/, strings/, templates/, and the gitignored
articles/ up to the repo root and removed the empty cmjs/ directory,
so the whole repo (or just css/js/strings/templates/articles) can be
dropped into an existing site and embedded as-is — no wrapper folder
required.

This also fixes a real bug: js/config.js's MARKED_URL previously
reached outside cmjs/ (../../js/marked.min.js) to find the vendored
marked.min.js, which already lived at the repo root as a sibling of
cmjs/ rather than inside it. Now that both files sit in the same js/
folder, MARKED_URL is a same-directory reference and no longer depends
on anything outside this one folder tree. BASE_URL's own formula is
unchanged (config.js is still exactly one level below whatever holds
articles/, strings/, and templates/ — previously that was cmjs/, now
it's the repo root).

Updated index.html, .gitignore (/cmjs/articles/ -> /articles/), and
every path reference throughout README.md (project structure diagram,
embedding instructions, nginx config, discovery/caching docs) to match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXSVPS3UmmASiiprPfcnUd
2026-09-12 15:20:19 +02:00

96 lines
4.0 KiB
JavaScript

// Persistent cache for discovered articles, keyed by each article file's own
// path. Raw IndexedDB (no wrapper library — see
// .claude/nginx-sitemap-crawler-plan.md L4/L5 on the zero-dependency,
// long-stable-API requirement). If IndexedDB is unavailable (private
// browsing, old browser, blocked), every operation below silently becomes a
// no-op so discovery.js gets live-fetch behaviour for free without needing
// its own availability branching (plan doc R6).
// Bump this whenever discovery.js's derived article record shape changes.
// The version is baked into the DB name so a code deploy invalidates every
// cached entry immediately, instead of stale-shaped records lingering until
// each file's mtime happens to change — which, on a "deploy and forget"
// site, could be years.
const CACHE_SCHEMA_VERSION = 3;
const DB_NAME = `cmjs-cache-v${CACHE_SCHEMA_VERSION}`;
const STORE_NAME = 'articleFiles';
let cacheAvailable = true;
let dbPromise = null;
function openDb() {
if (!dbPromise) {
dbPromise = new Promise((resolve, reject) => {
if (typeof indexedDB === 'undefined') { reject(new Error('IndexedDB unavailable')); return; }
const request = indexedDB.open(DB_NAME, 1);
request.onupgradeneeded = () => {
request.result.createObjectStore(STORE_NAME, { keyPath: 'path' });
};
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
request.onblocked = () => reject(new Error('IndexedDB open blocked'));
}).catch(error => { cacheAvailable = false; throw error; });
}
return dbPromise;
}
// Opens a store and resolves/rejects with the given IDBRequest's own outcome
// — the standard pattern, so result timing never depends on transaction
// lifecycle details.
async function runRequest(mode, makeRequest) {
if (!cacheAvailable) return undefined;
try {
const db = await openDb();
const store = db.transaction(STORE_NAME, mode).objectStore(STORE_NAME);
return await new Promise((resolve, reject) => {
const request = makeRequest(store);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
} catch {
cacheAvailable = false;
return undefined;
}
}
export function isCacheAvailable() {
return cacheAvailable;
}
// Returns every cached { path, mtime, article } record as a Map keyed by
// path — one bulk read instead of a separate IndexedDB round-trip per
// article file, which matters once there are hundreds of them: discovery.js
// reads this once up front rather than doing a point lookup per file during
// indexing. Empty Map on a miss or if the cache is unavailable.
export async function getAllCacheEntries() {
const records = await runRequest('readonly', store => store.getAll());
return new Map((records || []).map(record => [record.path, record]));
}
export async function setCacheEntry(path, entry) {
await runRequest('readwrite', store => store.put({ ...entry, path }));
}
// Removes any cached entry whose path is no longer in the live directory
// listing (renamed/deleted slugs), so the cache doesn't grow unbounded.
export async function pruneCache(validPaths) {
if (!cacheAvailable) return;
const valid = new Set(validPaths);
try {
const db = await openDb();
const store = db.transaction(STORE_NAME, 'readwrite').objectStore(STORE_NAME);
await new Promise((resolve, reject) => {
const cursorRequest = store.openCursor();
cursorRequest.onsuccess = event => {
const cursor = event.target.result;
if (!cursor) { resolve(); return; }
if (!valid.has(cursor.value.path)) cursor.delete();
cursor.continue();
};
cursorRequest.onerror = () => reject(cursorRequest.error);
});
} catch {
cacheAvailable = false;
}
}