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
96 lines
4.0 KiB
JavaScript
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;
|
|
}
|
|
}
|