// storage.mbt — three-tier JSON storage (mirrors Python `storage.py`)
//
// Layout:
// /meta_index.json in-memory index snapshot (categories + entries)
// //.json one file per entry (optionally encrypted)
//
// The in-memory index is held in a `@ref.Ref` cell so that by-value method
// receivers still share the same underlying map: a `Storage` value (and any
// copy of it) mutates the one shared index, which is what makes
// `sys.remember(...)` / `sys.recall(...)` update state in place.
///|
const SEP : String = "/"
///|
/// Three-tier JSON-backed memory store.
pub struct Storage {
root : String
crypto : CryptoProvider
index_path : String
entries : @ref.Ref[Map[String, MemoryEntry]]
}
///|
fn join(a : String, b : String) -> String {
a + SEP + b
}
///|
fn ensure_dir(path : String) -> Unit {
if !@fs.path_exists(path) {
@fs.create_dir(path) catch {
_ => ()
}
}
}
///|
fn Storage::ensure_dirs(self : Storage) -> Unit {
ensure_dir(self.root)
for t in all_types() {
ensure_dir(join(self.root, t.value()))
}
}
///|
fn Storage::entry_path(self : Storage, entry : MemoryEntry) -> String {
join(join(self.root, entry.mtype), entry.id + ".json")
}
///|
fn Storage::load_index(self : Storage) -> Unit {
if !@fs.path_exists(self.index_path) {
return
}
try {
let raw = @fs.read_file_to_string(self.index_path)
let dec = self.crypto.decrypt(raw)
let j = @json.parse(dec)
match j {
Object(m) =>
match m.get("entries") {
Some(Object(ents)) =>
for v in ents.values() {
match MemoryEntry::from_json(v) {
Some(e) => self.entries.val.set(e.id, e)
None => ()
}
}
_ => ()
}
_ => ()
}
} catch {
_ => ()
}
}
///|
fn Storage::save_index(self : Storage) -> Unit raise @fs.IOError {
let cats : Array[Json] = []
for t in all_types() {
let tv = t.value()
let of_type = self.entries.val
.values()
.filter(fn(e) { e.mtype == tv })
.to_array()
let count = of_type.length()
let updated = if count > 0 {
let mut mx = of_type[0].timestamp
for e in of_type {
if e.timestamp > mx {
mx = e.timestamp
}
}
mx.to_double()
} else {
0.0
}
let cat : Map[String, Json] = Map([], capacity=0)
cat.set("type", Json::string(tv))
cat.set("path", Json::string(join(self.root, tv)))
cat.set("entry_count", Json::number(count.to_double()))
cat.set("updated_at", Json::number(updated))
cats.push(Json::object(cat))
}
let ents : Map[String, Json] = Map([], capacity=0)
for e in self.entries.val.values() {
ents.set(e.id, e.to_json())
}
let root_map : Map[String, Json] = Map([], capacity=0)
root_map.set("categories", Json::array(cats))
root_map.set("entries", Json::object(ents))
let s = Json::object(root_map).stringify(indent=2)
@fs.write_string_to_file(self.index_path, self.crypto.encrypt(s))
}
///|
/// Open (or create) a store rooted at `root`.
pub fn Storage::new(
root : String,
crypto? : CryptoProvider = CryptoProvider::disabled(),
) -> Storage {
let s = {
root,
crypto,
index_path: join(root, "meta_index.json"),
entries: @ref.Ref(Map([], capacity=0)),
}
s.ensure_dirs()
s.load_index()
// Best-effort initial index write; persistence is retried on first `put`.
s.save_index() catch {
_ => ()
}
s
}
///|
/// Persist an entry (file + index).
pub fn Storage::put(
self : Storage,
entry : MemoryEntry,
) -> Unit raise @fs.IOError {
let path = self.entry_path(entry)
let raw = entry.to_json().stringify(indent=2)
@fs.write_string_to_file(path, self.crypto.encrypt(raw))
self.entries.val.set(entry.id, entry)
self.save_index()
}
///|
/// Fetch an entry by id (from the in-memory index).
pub fn Storage::get(self : Storage, entry_id : String) -> MemoryEntry? {
self.entries.val.get(entry_id)
}
///|
/// Delete an entry by id. Returns true if something was removed.
pub fn Storage::delete(
self : Storage,
entry_id : String,
) -> Bool raise @fs.IOError {
match self.entries.val.get(entry_id) {
None => false
Some(e) => {
let path = self.entry_path(e)
if @fs.path_exists(path) {
@fs.remove_file(path) catch {
_ => ()
}
}
self.entries.val.remove(entry_id)
self.save_index()
true
}
}
}
///|
/// All entries.
pub fn Storage::all_entries(self : Storage) -> Array[MemoryEntry] {
self.entries.val.values().to_array()
}
///|
/// Entries of a given type.
pub fn Storage::query_by_type(
self : Storage,
mtype : MemoryType,
) -> Array[MemoryEntry] {
self.entries.val
.values()
.filter(fn(e) { e.mtype == mtype.value() })
.to_array()
}
///|
/// Aggregate statistics (mirrors Python `Storage.stats`).
pub fn Storage::stats(self : Storage) -> Map[String, Json] {
let entries = self.entries.val.values().to_array()
let by_type : Map[String, Int] = Map([], capacity=0)
let mut active = 0
let mut deprecated = 0
for e in entries {
by_type.set(e.mtype, by_type.get_or_default(e.mtype, 0) + 1)
if e.status == MemoryStatus::Active.value() {
active = active + 1
} else if e.status == MemoryStatus::Deprecated.value() {
deprecated = deprecated + 1
}
}
let by_type_json : Map[String, Json] = Map([], capacity=0)
for k, v in by_type {
by_type_json.set(k, Json::number(v.to_double()))
}
let res : Map[String, Json] = Map([], capacity=0)
res.set("total", Json::number(entries.length().to_double()))
res.set("by_type", Json::object(by_type_json))
res.set("active", Json::number(active.to_double()))
res.set("deprecated", Json::number(deprecated.to_double()))
res
}