e53f473fc2
- bootstrap(store) — uses store.cas + store.var - gc(store) — uses store.cas + store.var - render/renderAsync/renderDirect — uses store.cas + store.var for templates - refs/walk/validate/getSchema/putSchema — uses store.cas - wrapEnvelope — uses store.cas + store.var - registerOutputTemplates — uses store.cas + store.var - Remove VariableStore SQLite class from @ocas/core - Zero bun:sqlite imports in core - Update @ocas/fs and @ocas/cli to new signatures - 560 tests pass Fixes #41
95 lines
2.6 KiB
TypeScript
95 lines
2.6 KiB
TypeScript
import { walk } from "./schema.js";
|
|
import type { Hash, OcasStore } from "./types.js";
|
|
|
|
export interface GcStats {
|
|
total: number; // Total CAS nodes before GC
|
|
reachable: number; // Nodes marked as reachable
|
|
collected: number; // Nodes deleted (swept)
|
|
scanned: number; // Variables scanned as roots
|
|
}
|
|
|
|
/**
|
|
* Garbage collection: mark-and-sweep algorithm
|
|
* - Roots: all variable values (global, not scoped)
|
|
* - Mark: recursively walk refs from roots
|
|
* - Sweep: delete unmarked nodes
|
|
* - Schema preservation: schemas of reachable nodes are also marked
|
|
*/
|
|
export function gc(store: OcasStore): GcStats {
|
|
// Get all variables (no filters → global). Omit `limit` so the full
|
|
// variable set is returned for use as gc roots.
|
|
const variables = store.var.list();
|
|
const scanned = variables.length;
|
|
|
|
// Collect unique root hashes from all variables
|
|
const roots = new Set<Hash>();
|
|
for (const variable of variables) {
|
|
roots.add(variable.value);
|
|
}
|
|
|
|
// Mark phase: walk from all roots
|
|
const reachable = new Set<Hash>();
|
|
|
|
for (const rootHash of roots) {
|
|
walk(store, rootHash, (hash, node) => {
|
|
// Mark the node itself
|
|
reachable.add(hash);
|
|
// Mark the schema (type) of the node
|
|
reachable.add(node.type);
|
|
});
|
|
}
|
|
|
|
// Walk the schema chain to ensure bootstrap meta-schema is preserved
|
|
// For each reachable schema, walk its schema chain (not its references)
|
|
const schemasToWalk = new Set<Hash>();
|
|
for (const hash of reachable) {
|
|
const node = store.cas.get(hash);
|
|
if (node) {
|
|
schemasToWalk.add(node.type);
|
|
}
|
|
}
|
|
|
|
for (const schemaHash of schemasToWalk) {
|
|
// Walk the schema's type chain (meta-schema, etc.)
|
|
let current: Hash | null = schemaHash;
|
|
while (current !== null && !reachable.has(current)) {
|
|
reachable.add(current);
|
|
const node = store.cas.get(current);
|
|
if (!node || node.type === current) {
|
|
// Self-referencing or missing node, stop
|
|
break;
|
|
}
|
|
current = node.type;
|
|
}
|
|
}
|
|
|
|
// Preserve all self-referencing nodes (bootstrap meta-schema)
|
|
// These are nodes where type === hash
|
|
const allHashes = store.cas.listAll();
|
|
for (const hash of allHashes) {
|
|
const node = store.cas.get(hash);
|
|
if (node && node.type === hash) {
|
|
reachable.add(hash);
|
|
}
|
|
}
|
|
|
|
// Count total nodes
|
|
const total = allHashes.length;
|
|
|
|
// Sweep phase: delete unmarked nodes
|
|
let collected = 0;
|
|
for (const hash of allHashes) {
|
|
if (!reachable.has(hash)) {
|
|
store.cas.delete(hash);
|
|
collected++;
|
|
}
|
|
}
|
|
|
|
return {
|
|
total,
|
|
reachable: reachable.size,
|
|
collected,
|
|
scanned,
|
|
};
|
|
}
|