UNPKG

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
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 };