UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

404 lines • 17.7 kB
// ========= 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