trellis
Version:
Agentic State Engine — event-sourced causal graph with branching, decision traces, and realtime sync for AI-native applications
198 lines (195 loc) • 5.76 kB
JavaScript
import {
__esm
} from "./chunk-2ESYSVXG.js";
// src/vcs/blob-store.ts
import {
createReadStream as fsCreateReadStream,
existsSync,
mkdirSync,
readdirSync,
readFileSync,
rmSync,
statSync,
writeFileSync
} from "fs";
import { join } from "path";
import { createHash } from "crypto";
var BlobStore;
var init_blob_store = __esm({
"src/vcs/blob-store.ts"() {
"use strict";
BlobStore = class {
blobDir;
metaPath;
constructor(trellisDir) {
this.blobDir = join(trellisDir, "blobs");
this.metaPath = join(trellisDir, "blob-meta.json");
if (!existsSync(this.blobDir)) {
mkdirSync(this.blobDir, { recursive: true });
}
}
/**
* Store content and return its SHA-256 hash.
* Idempotent — storing the same content twice is a no-op.
*/
async put(content) {
const hash = await this.hash(content);
const blobPath = join(this.blobDir, hash);
if (!existsSync(blobPath)) {
writeFileSync(blobPath, content);
}
return hash;
}
/**
* Synchronous put — uses Bun's sync crypto if available.
*/
putSync(content) {
const hash = this.hashSync(content);
const blobPath = join(this.blobDir, hash);
if (!existsSync(blobPath)) {
writeFileSync(blobPath, content);
}
return hash;
}
/**
* Retrieve content by hash. Returns null if not found.
*/
get(hash) {
const blobPath = join(this.blobDir, hash);
if (!existsSync(blobPath)) {
return null;
}
return readFileSync(blobPath);
}
/**
* Check if a blob exists.
*/
has(hash) {
return existsSync(join(this.blobDir, hash));
}
/**
* Byte length of a stored blob, or null if not found. Cheap stat — does not
* read the content. Used to answer Range/Content-Length without loading bytes.
*/
size(hash) {
const blobPath = join(this.blobDir, hash);
if (!existsSync(blobPath)) return null;
return statSync(blobPath).size;
}
/**
* Stream a blob (optionally a single byte range) from disk without buffering
* the whole file into memory. `start`/`end` are inclusive byte offsets, matching
* HTTP Range semantics. Returns null if the blob does not exist.
*/
createReadStream(hash, range) {
const blobPath = join(this.blobDir, hash);
if (!existsSync(blobPath)) return null;
return range ? fsCreateReadStream(blobPath, { start: range.start, end: range.end }) : fsCreateReadStream(blobPath);
}
/**
* Compute SHA-256 hash of content (async).
*/
async hash(content) {
const hashBuffer = await crypto.subtle.digest(
"SHA-256",
content
);
return this.hexFromBuffer(hashBuffer);
}
/**
* Compute SHA-256 hash of content (sync).
* Uses node:crypto for cross-runtime compatibility.
*/
hashSync(content) {
return createHash("sha256").update(content).digest("hex");
}
/**
* List stored blob hashes (sha256 hex filenames). Order is filesystem order.
*/
listHashes() {
try {
const HASH_RE = /^[a-f0-9]{64}$/;
return readdirSync(this.blobDir).filter(
(f) => HASH_RE.test(f)
);
} catch {
return [];
}
}
/** Optional display metadata (filename / mime) keyed by content hash. */
getMeta(hash) {
return this.readMetaMap()[hash];
}
setMeta(hash, meta) {
const map = this.readMetaMap();
const prev = map[hash] ?? {};
map[hash] = {
...prev,
...meta,
// Prefer a real filename over a later empty overwrite.
name: meta.name?.trim() || prev.name,
contentType: meta.contentType?.trim() || prev.contentType
};
this.writeMetaMap(map);
}
/**
* Delete a blob by hash. Returns true when a stored blob was removed.
* Also prunes any display metadata keyed by the same hash.
*/
delete(hash) {
const blobPath = join(this.blobDir, hash);
if (!existsSync(blobPath)) return false;
rmSync(blobPath, { force: true });
const map = this.readMetaMap();
if (hash in map) {
delete map[hash];
this.writeMetaMap(map);
}
return true;
}
/**
* Returns the number of blobs stored.
*/
count() {
return this.listHashes().length;
}
/**
* Returns the total size of all blobs in bytes.
*/
totalSize() {
try {
const files = readdirSync(this.blobDir);
return files.reduce((sum, f) => {
try {
return sum + statSync(join(this.blobDir, f)).size;
} catch {
return sum;
}
}, 0);
} catch {
return 0;
}
}
readMetaMap() {
try {
if (!existsSync(this.metaPath)) return {};
const parsed = JSON.parse(readFileSync(this.metaPath, "utf8"));
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
return parsed;
} catch {
return {};
}
}
writeMetaMap(map) {
writeFileSync(this.metaPath, JSON.stringify(map));
}
hexFromBuffer(buffer) {
return Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
}
};
}
});
export {
BlobStore,
init_blob_store
};