spb/spbgit Public MIT
SPB Git — the platform hosting itself
JavaScript 73.9%
CSS 11.7%
Nunjucks 11.6%
Shell 2.7%
1/**2 * ─────────────────────────────────────────────3 * SPB Git — Personal Git Platform4 * ─────────────────────────────────────────────5 * Author : Simon-Pierre Boucher6 * Contact : contact@spboucher.ai7 * File : src/lib/cache.mjs8 * Purpose : Filesystem cache keyed by <repo>@<sha> — busted on push9 * License : MIT © Simon-Pierre Boucher10 * ─────────────────────────────────────────────11 */1213import {14 readFileSync, writeFileSync, mkdirSync, rmSync, existsSync,15 readdirSync, statSync, renameSync,16} from 'node:fs';17import { join, dirname } from 'node:path';18import { randomBytes } from 'node:crypto';19import { safeJoin } from './util.mjs';2021/**22 * Content cache under `cacheDir`. Layout:23 * <cacheDir>/repos/<repo>/<sha>/<kind> per-commit artifacts24 * <cacheDir>/global/<kind> cross-repo artifacts (search index, heatmap)25 * Push hooks call {@link Cache#bustRepo} which nukes the repo subtree.26 */27export class Cache {28 /** @param {string} cacheDir */29 constructor(cacheDir) {30 this.root = cacheDir;31 }3233 /** @param {...string} segments @returns {string} */34 path(...segments) {35 const cleaned = segments.map((s) => String(s).replaceAll('/', '_'));36 return safeJoin(this.root, ...cleaned);37 }3839 /** Per-repo, per-commit key. @returns {string} */40 repoPath(repo, sha, kind) {41 return safeJoin(this.root, 'repos', repo, sha, kind.replaceAll('/', '_'));42 }4344 /** @returns {Buffer|null} */45 getBuffer(path) {46 try {47 return readFileSync(path);48 } catch {49 return null;50 }51 }5253 /** @returns {any|null} */54 getJSON(path) {55 const buf = this.getBuffer(path);56 if (buf === null) return null;57 try {58 return JSON.parse(buf.toString('utf8'));59 } catch {60 return null;61 }62 }6364 /** Atomic write (tmp + rename). */65 set(path, data) {66 mkdirSync(dirname(path), { recursive: true });67 const tmp = `${path}.${randomBytes(4).toString('hex')}.tmp`;68 writeFileSync(tmp, data);69 renameSync(tmp, path);70 }7172 setJSON(path, value) {73 this.set(path, JSON.stringify(value));74 }7576 /**77 * Read-through helper for JSON artifacts.78 * @param {string} path79 * @param {() => Promise<any>|any} compute80 */81 async remember(path, compute) {82 const hit = this.getJSON(path);83 if (hit !== null) return hit;84 const value = await compute();85 if (value !== undefined) this.setJSON(path, value);86 return value;87 }8889 /** Drop every cached artifact for a repo (all shas). @param {string} repo */90 bustRepo(repo) {91 rmSync(safeJoin(this.root, 'repos', repo), { recursive: true, force: true });92 rmSync(safeJoin(this.root, 'global'), { recursive: true, force: true });93 }9495 /** @returns {{files: number, bytes: number}} recursive cache footprint */96 footprint() {97 let files = 0;98 let bytes = 0;99 const walk = (dir) => {100 let entries;101 try {102 entries = readdirSync(dir);103 } catch {104 return;105 }106 for (const entry of entries) {107 const full = join(dir, entry);108 let st;109 try {110 st = statSync(full);111 } catch {112 continue;113 }114 if (st.isDirectory()) walk(full);115 else {116 files += 1;117 bytes += st.size;118 }119 }120 };121 if (existsSync(this.root)) walk(this.root);122 return { files, bytes };123 }124}125