@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
404 lines • 17.7 kB
JavaScript
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import { FileType } from "../../types.js";
import { compareCodePoints } from "../../utils/sort.js";
import { underPath } from "../../utils/key_prefix.js";
import { KeyLock } from "../lock.js";
import { LookupStatus, } from "./config.js";
import { IndexCacheStore } from "./store.js";
export class RAMIndexCacheStore extends IndexCacheStore {
ttl;
entryMap = new Map();
children = new Map();
folderFlags = new Map();
expiry = new Map();
partial = new Set();
windowed = new Set();
tombstones = new Map();
versions = new Map();
observation = 0;
lock = new KeyLock();
constructor(options = {}) {
super(options.clock);
this.ttl = options.ttl ?? 600;
}
seed(entries, children, expiresAt, version = null) {
for (const [path, entry] of entries)
this.entryMap.set(path, entry);
for (const [path, keys] of children) {
this.children.set(path, [...keys]);
this.expiry.set(path, expiresAt.getTime());
this.partial.delete(path);
this.windowed.delete(path);
this.stamp(path, version);
}
for (const [path, keys] of children) {
this.folderFlags.set(path, new Map(keys.map((child) => [
child,
this.children.has(child) || this.entryMap.get(child)?.type === FileType.DIRECTORY,
])));
}
for (const path of children.keys()) {
this.rememberDirectory(path);
}
}
stamp(vfsPath, version) {
if (version === null)
this.versions.delete(vfsPath);
else
this.versions.set(vfsPath, version);
}
entries() {
return Promise.resolve(new Map(this.entryMap));
}
get(vfsPath) {
const entry = this.entryMap.get(vfsPath);
if (entry === undefined)
return Promise.resolve({ status: LookupStatus.NOT_FOUND });
return Promise.resolve({ entry: entry });
}
put(vfsPath, entry) {
const folder = vfsPath.slice(0, vfsPath.lastIndexOf('/')) || '/';
return this.lock.withLock(folder, () => {
if (this.entryMap.get(vfsPath)?.type === FileType.DIRECTORY &&
entry.type !== FileType.DIRECTORY)
this.bury(folder, [vfsPath]);
this.entryMap.set(vfsPath, entry);
return Promise.resolve();
});
}
addEntry(vfsPath, entry, expiredAt) {
const folder = vfsPath.slice(0, vfsPath.lastIndexOf('/')) || '/';
return this.lock.withLock(folder, () => {
const now = this.clock.now() * 1000;
const children = this.children.get(folder) ?? [];
const exp = this.expiry.get(folder);
const wasFolder = this.entryMap.get(vfsPath)?.type === FileType.DIRECTORY ||
this.folderFlags.get(folder)?.get(vfsPath) === true;
const stale = !this.children.has(folder) || exp === undefined || now >= exp;
if (stale)
this.bury(folder, children);
if (wasFolder && entry.type !== FileType.DIRECTORY) {
this.bury(folder, [vfsPath]);
this.partial.add(folder);
this.windowed.delete(folder);
this.versions.delete(folder);
}
this.entryMap.set(vfsPath, entry);
if (stale) {
const lifetime = now + this.ttl * 1000;
this.children.set(folder, [vfsPath]);
this.folderFlags.set(folder, new Map());
this.expiry.set(folder, expiredAt == null ? lifetime : Math.min(lifetime, expiredAt.getTime()));
this.partial.add(folder);
this.windowed.delete(folder);
this.versions.delete(folder);
}
else if (!children.includes(vfsPath)) {
const at = children.findIndex((child) => compareCodePoints(child, vfsPath) > 0);
children.splice(at === -1 ? children.length : at, 0, vfsPath);
}
this.folderFlags.get(folder)?.set(vfsPath, entry.type === FileType.DIRECTORY);
return Promise.resolve();
});
}
replaceIfUnchanged(path, predecessor, entry) {
return this.lock.withLock(path, () => {
const current = this.entryMap.get(path);
if (current === undefined || JSON.stringify(current) !== predecessor)
return Promise.resolve(false);
this.entryMap.set(path, entry);
return Promise.resolve(true);
});
}
listDir(vfsPath, child) {
const exp = this.expiry.get(vfsPath);
if (exp === undefined)
return Promise.resolve({ status: LookupStatus.NOT_FOUND });
if (this.clock.now() * 1000 >= exp)
return Promise.resolve({ status: LookupStatus.EXPIRED });
// A folder's flags name exactly the children its listing holds.
const children = child === undefined
? (this.children.get(vfsPath) ?? [])
: this.folderFlags.get(vfsPath)?.has(child) === true
? [child]
: [];
const version = this.versions.get(vfsPath) ?? null;
if (this.partial.has(vfsPath))
return Promise.resolve({ partialEntries: children, version });
return Promise.resolve({ entries: children, version, window: this.windowed.has(vfsPath) });
}
setDir(vfsPath, entries, expiredAt, options = {}) {
return this.storeDir(vfsPath, entries, expiredAt, false, options.window !== true, options.excluded ?? [], options.version ?? null).then((result) => result ?? []);
}
setPartialDir(vfsPath, entries, expiredAt) {
return this.storeDir(vfsPath, entries, expiredAt, true, false).then(() => undefined);
}
storeDir(vfsPath, entries, expiredAt, partial, evict, excluded = [], version = null, refill = null) {
return this.lock.withLock(vfsPath, async () => {
if (refill !== null && refill !== String(this.observation))
return null;
const now = this.clock.now() * 1000;
const exp = expiredAt ? expiredAt.getTime() : now + this.ttl * 1000;
const prefix = vfsPath === '/' ? '/' : `${vfsPath}/`;
const rows = new Map();
for (const [name, entry] of entries)
rows.set(prefix + name, entry);
const childKeys = [...rows.keys()];
if (partial)
this.bury(vfsPath, [
...(this.children.get(vfsPath) ?? []),
...[...rows.keys()].filter((key) => this.children.has(key) || this.entryMap.get(key)?.type === FileType.DIRECTORY),
]);
const buried = new Map();
if (!partial) {
for (const child of this.tombstones.get(vfsPath) ?? [])
buried.set(child.path, child.folder);
this.tombstones.delete(vfsPath);
}
const candidates = new Set([
...(this.children.get(vfsPath) ?? []),
...buried.keys(),
...rows.keys(),
]);
const currentFolders = this.folderFlags.get(vfsPath);
const gone = evict
? [...candidates]
.filter((key) => (!rows.has(key) ||
(rows.get(key)?.type === FileType.FILE &&
(buried.get(key) === true ||
currentFolders?.get(key) === true ||
this.children.has(key) ||
this.entryMap.get(key)?.type === FileType.DIRECTORY))) &&
!excluded.some((prefix) => underPath(key, prefix)))
.map((key) => this.evict(key, buried.get(key) === true || currentFolders?.get(key) === true, excluded))
: [];
for (const [path, row] of rows)
this.entryMap.set(path, row);
this.children.set(vfsPath, childKeys);
this.folderFlags.set(vfsPath, new Map([...rows].map(([key, row]) => [key, row.type === FileType.DIRECTORY])));
this.rememberDirectory(vfsPath);
this.expiry.set(vfsPath, exp);
this.stamp(vfsPath, partial ? null : version);
if (partial)
this.partial.add(vfsPath);
else
this.partial.delete(vfsPath);
if (partial || evict)
this.windowed.delete(vfsPath);
else
this.windowed.add(vfsPath);
return Promise.resolve(gone);
});
}
/** Drop a child a complete listing no longer names. */
evict(key, buriedFolder = false, excluded = []) {
const entry = this.entryMap.get(key);
this.entryMap.delete(key);
const folder = buriedFolder || this.children.has(key) || entry?.type === FileType.DIRECTORY;
if (folder)
this.dropPrefix(key, false, excluded);
return { path: key, folder };
}
bury(vfsPath, paths) {
const buried = new Map((this.tombstones.get(vfsPath) ?? []).map((child) => [child.path, child.folder]));
for (const child of paths) {
buried.set(child, buried.get(child) === true ||
this.folderFlags.get(vfsPath)?.get(child) === true ||
this.children.has(child) ||
this.entryMap.get(child)?.type === FileType.DIRECTORY);
}
if (buried.size > 0)
this.tombstones.set(vfsPath, [...buried].map(([path, folder]) => ({ path, folder })));
}
rememberDirectory(vfsPath) {
vfsPath = vfsPath.replace(/\/+$/, '');
if (vfsPath === '')
return;
const parent = vfsPath.slice(0, vfsPath.lastIndexOf('/')) || '/';
const flags = this.folderFlags.get(parent);
if (flags?.has(vfsPath))
flags.set(vfsPath, true);
const saved = this.tombstones.get(parent) ?? [];
if (!saved.some((child) => child.path === vfsPath))
return;
const remembered = saved.map((child) => child.path === vfsPath ? { path: child.path, folder: true } : child);
this.tombstones.set(parent, remembered);
}
refillToken() {
return Promise.resolve(String(this.observation));
}
async invalidateMissing(paths) {
let changed = false;
const now = this.clock.now() * 1000;
for (const path of paths) {
const folder = path.slice(0, path.lastIndexOf('/')) || '/';
await this.lock.withLock(folder, () => {
const expiry = this.expiry.get(folder);
if (expiry === undefined || now >= expiry) {
this.observation += 1;
return Promise.resolve();
}
if (this.partial.has(folder) || this.windowed.has(folder)) {
if (this.folderFlags.get(folder)?.has(path) !== true)
this.observation += 1;
return Promise.resolve();
}
if (this.folderFlags.get(folder)?.has(path) === true)
return Promise.resolve();
this.expiry.set(folder, 0);
this.versions.delete(folder);
changed = true;
this.observation += 1;
return Promise.resolve();
});
}
return changed;
}
refillDir(path, entries, token, expiredAt, options = {}) {
return this.storeDir(path, entries, expiredAt, options.partial === true, options.partial !== true && options.window !== true, options.excluded ?? [], options.version ?? null, token);
}
invalidateEntry(vfsPath) {
const folder = vfsPath.slice(0, vfsPath.lastIndexOf('/')) || '/';
return this.lock.withLock(folder, () => {
this.bury(folder, [
...(this.children.get(folder) ?? []),
...(this.children.has(vfsPath) || this.entryMap.get(vfsPath)?.type === FileType.DIRECTORY
? [vfsPath]
: []),
]);
this.entryMap.delete(vfsPath);
const children = this.children.get(folder);
if (children !== undefined) {
this.children.set(folder, children.filter((child) => child !== vfsPath));
this.partial.add(folder);
this.windowed.delete(folder);
this.versions.delete(folder);
this.folderFlags.get(folder)?.delete(vfsPath);
}
return Promise.resolve();
});
}
invalidateDir(vfsPath) {
return this.lock.withLock(vfsPath, () => {
this.bury(vfsPath, this.children.get(vfsPath) ?? []);
for (const child of this.children.get(vfsPath) ?? [])
this.entryMap.delete(child);
this.expiry.delete(vfsPath);
this.children.delete(vfsPath);
this.folderFlags.delete(vfsPath);
this.partial.delete(vfsPath);
this.windowed.delete(vfsPath);
this.versions.delete(vfsPath);
return Promise.resolve();
});
}
// Forgetting what is cached is not evidence that anything went away, so an
// existing tombstone survives for the next complete listing.
invalidatePrefix(vfsPath, excluded = [], remember = false) {
this.observation += 1;
if (remember) {
for (const [folder, children] of this.children) {
if (underPath(folder, vfsPath) && !excluded.some((prefix) => underPath(folder, prefix))) {
this.bury(folder, children);
}
}
}
this.dropPrefix(vfsPath, true, excluded);
return Promise.resolve();
}
holdsSubtree(vfsPath) {
for (const key of this.children.keys()) {
if (underPath(key, vfsPath))
return Promise.resolve(true);
}
return Promise.resolve(false);
}
dropPrefix(vfsPath, keepTombstones = false, excluded = []) {
if (!keepTombstones) {
for (const key of [...this.tombstones.keys()]) {
if (underPath(key, vfsPath) && !excluded.some((prefix) => underPath(key, prefix)))
this.tombstones.delete(key);
}
}
for (const key of [...this.entryMap.keys()]) {
if (underPath(key, vfsPath) && !excluded.some((prefix) => underPath(key, prefix)))
this.entryMap.delete(key);
}
for (const key of [...this.children.keys()]) {
if (underPath(key, vfsPath) && !excluded.some((prefix) => underPath(key, prefix))) {
this.children.delete(key);
this.folderFlags.delete(key);
}
}
for (const key of [...this.expiry.keys()]) {
if (underPath(key, vfsPath) && !excluded.some((prefix) => underPath(key, prefix))) {
this.expiry.delete(key);
this.partial.delete(key);
this.windowed.delete(key);
this.versions.delete(key);
}
}
}
invalidate() {
this.observation += 1;
const past = this.clock.now() * 1000 - 1000;
for (const key of this.expiry.keys())
this.expiry.set(key, past);
return Promise.resolve();
}
clear() {
this.observation += 1;
this.entryMap.clear();
this.children.clear();
this.folderFlags.clear();
this.expiry.clear();
this.partial.clear();
this.windowed.clear();
this.tombstones.clear();
this.versions.clear();
this.lock.clear();
return Promise.resolve();
}
}
/**
* The empty, throwaway store a `read: fresh` check stats through.
*
* The listing gate and the read probe pass it to say a request is what they
* want. A root stat asks the backend for its head only through this store.
* Through any other index it names no version and reads nothing, so a getattr
* of the root never sends a request or reads the index. A backend that lists
* a parent to answer a miss may ask for the one path instead, since the store
* is dropped right after.
*
* Mirrors Python's `ListingCheckStore`.
*/
export class ListingCheckStore extends RAMIndexCacheStore {
hintedRows = new Map();
hints;
constructor(options = {}) {
super(options.hints === undefined ? {} : { clock: options.hints.clock });
this.hints = options.hints ?? null;
}
/** Offer the mount row as a lead that the backend must confirm live. */
async hint(vfsPath) {
if (this.hints === null)
return null;
const entry = (await this.hints.get(vfsPath)).entry ?? null;
if (entry !== null)
this.hintedRows.set(vfsPath, JSON.stringify(entry));
return entry;
}
}
//# sourceMappingURL=ram.js.map