UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

295 lines • 11.8 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 { rstripSlash } from "../../utils/slash.js"; import { mutationLock } from "../file/io.js"; import { LookupStatus, } from "./config.js"; import { IndexCacheStore } from "./store.js"; /** A mount-owned index view; delayed backend writes retain their original owner. */ export class IndexView extends IndexCacheStore { inner; cache; prefix; owns; unverified = false; locked; readTtl; onGone; mayServeListing; excludedPrefixes; noteWritten; commandListed; constructor(inner, cache, prefix, owns, options = {}) { super(inner.clock); this.inner = inner; this.cache = cache; this.prefix = prefix; this.owns = owns; this.locked = options.locked ?? false; this.readTtl = options.readTtl; this.onGone = options.onGone; this.mayServeListing = options.mayServeListing; this.noteWritten = options.noteWritten; this.commandListed = options.listedThisCommand; this.excludedPrefixes = options.excludedPrefixes ?? (() => []); } listedThisCommand(folder) { return this.commandListed?.(folder) ?? false; } /** The store this view writes through. */ get store() { return this.inner; } get ttl() { return this.readTtl === undefined ? this.inner.ttl : Math.min(this.inner.ttl, this.readTtl); } fence(fn) { return this.locked ? fn() : mutationLock(this.cache, fn); } /** Cap the expiry, preserving the store default when it is shorter. */ deadline(expiredAt) { if (expiredAt !== null && expiredAt !== undefined) return this.cap(expiredAt); if (this.readTtl === undefined || this.inner.ttl <= this.readTtl) return expiredAt; return new Date(this.clock.now() * 1000 + this.readTtl * 1000); } /** Shorten an explicit expiry to this mount's bound. */ cap(at) { if (this.readTtl === undefined) return at; const bound = this.clock.now() * 1000 + this.readTtl * 1000; return at.getTime() > bound ? new Date(bound) : at; } scopeSnapshot(snapshot) { return { entries: new Map([...snapshot.entries].filter(([path]) => this.owns(path))), children: new Map([...snapshot.children] .filter(([path]) => this.owns(path)) .map(([path, keys]) => [path, keys.filter((key) => this.owns(key))])), version: snapshot.version ?? null, }; } seed(entries, children, expiresAt, version = null) { if (!this.owns(this.prefix)) return; const snapshot = this.scopeSnapshot({ entries, children, version }); this.inner.seed(snapshot.entries, snapshot.children, this.cap(expiresAt), snapshot.version ?? null); for (const folder of snapshot.children.keys()) this.noted(folder); } entries() { return this.fence(async () => { if (!this.owns(this.prefix)) return new Map(); const entries = await this.inner.entries(); return new Map([...entries].filter(([path]) => this.owns(path))); }); } getMany(vfsPaths) { return this.fence(async () => { const paths = [...new Set(vfsPaths)].filter((path) => this.owns(path)); if (paths.length === 0) return new Map(); const rows = await this.inner.getMany(paths); const kept = new Map(); for (const path of paths) { const entry = rows.get(path); if (this.owns(path) && entry !== undefined) kept.set(path, entry); } return kept; }); } async get(path) { // Index lookups may flush queued state; keep them inside the write fence too. return this.fence(async () => { if (!this.owns(path)) return { status: LookupStatus.NOT_FOUND }; const result = await this.inner.get(path); return this.owns(path) ? result : { status: LookupStatus.NOT_FOUND }; }); } async listDir(path, child) { const result = await this.fencedListDir(path, child); // Asked outside the fence, since a gate may reach the backend, and only // about a listing the store has: a NOT_FOUND must stay one. A refusal // leaves the listing stored for the re-list to diff. if (this.mayServeListing !== undefined && (result.entries != null || result.partialEntries != null) && !(await this.mayServeListing(path, result.version ?? null))) { return { status: LookupStatus.EXPIRED }; } return result; } fencedListDir(path, child) { return this.fence(async () => { if (!this.owns(path)) return { status: LookupStatus.NOT_FOUND }; if (this.unverified) return { status: LookupStatus.EXPIRED }; const result = await this.inner.listDir(path, child); if (!this.owns(path)) return { status: LookupStatus.NOT_FOUND }; // A child of a folder this mount owns is its own too, unless a nested // mount sits at that very name. const nested = new Set(this.excludedPrefixes()); const kept = (keys) => keys.filter((key) => !nested.has(key) || this.owns(key)); return { ...result, ...(result.entries == null ? {} : { entries: kept(result.entries) }), ...(result.partialEntries == null ? {} : { partialEntries: kept(result.partialEntries) }), }; }); } put(path, entry) { return this.fence(async () => { if (this.owns(path)) await this.inner.put(path, entry); }); } addEntry(path, entry, expiredAt) { return this.fence(async () => { if (this.owns(path)) await this.inner.addEntry(path, entry, this.deadline(expiredAt)); }); } replaceIfUnchanged(path, predecessor, entry) { return this.fence(() => this.owns(path) ? this.inner.replaceIfUnchanged(path, predecessor, entry) : Promise.resolve(false)); } setDir(path, entries, expiredAt, options = {}) { return this.storeDir(path, entries, expiredAt, false, options.window === true, options.excluded ?? [], options.version ?? null).then(async (gone) => { await this.reportGone(gone); return gone; }); } setPartialDir(path, entries, expiredAt) { return this.storeDir(path, entries, expiredAt, true, false).then(() => undefined); } storeDir(path, entries, expiredAt, partial, window, excluded = [], version = null) { return this.fence(async () => { if (!this.owns(path)) return []; const prefix = rstripSlash(path) + '/'; const owned = entries.filter(([name]) => this.owns(prefix + name)); const deadline = this.deadline(expiredAt); if (partial) { await this.inner.setPartialDir(path, owned, deadline); this.noted(path); return []; } const gone = await this.inner.setDir(path, owned, deadline, { window, excluded: [...excluded, ...this.excludedPrefixes()], version, }); this.noted(path); return gone.filter((child) => this.owns(child.path)); }); } // After the store holds it, never before: a reader trusting the note must // find the listing the note is about. noted(path) { this.noteWritten?.(path); } // Outside the fence: cleanup evicts file-cache entries, and the mount table // can change after the write, so ownership is asked again at cleanup time. async reportGone(gone) { if (this.onGone === undefined) return; const owned = gone.filter((child) => this.owns(child.path)); if (owned.length > 0) await this.onGone(owned); } refillToken() { return this.fence(async () => { if (this.unverified && this.owns(this.prefix)) { await this.inner.invalidatePrefix(this.prefix, this.excludedPrefixes(), true); this.unverified = false; } return this.inner.refillToken(); }); } invalidateMissing(paths) { return this.fence(async () => { const owned = paths.filter((path) => this.owns(path)); if (owned.length === 0) return false; try { if (this.unverified) await this.inner.invalidatePrefix(this.prefix, this.excludedPrefixes(), true); const result = await this.inner.invalidateMissing(owned); this.unverified = false; return result; } catch (err) { this.unverified = true; throw err; } }); } async refillDir(path, entries, token, expiredAt, options = {}) { const gone = await this.fence(async () => { if (this.unverified || !this.owns(path)) return null; const prefix = rstripSlash(path) + '/'; const owned = entries.filter(([name]) => this.owns(prefix + name)); const result = await this.inner.refillDir(path, owned, token, this.deadline(expiredAt), { ...options, excluded: [...(options.excluded ?? []), ...this.excludedPrefixes()], }); if (result === null) return null; this.noted(path); return result.filter((child) => this.owns(child.path)); }); if (gone !== null) await this.reportGone(gone); return gone; } invalidateEntry(vfsPath) { return this.fence(async () => { if (this.owns(vfsPath)) await this.inner.invalidateEntry(vfsPath); }); } invalidateDir(path) { return this.fence(async () => { if (this.owns(path)) await this.inner.invalidateDir(path); }); } invalidatePrefix(path, excluded = [], remember = false) { return this.fence(async () => { if (this.owns(path)) await this.inner.invalidatePrefix(path, [...excluded, ...this.excludedPrefixes()], remember); }); } // No ownership filter: answering false for a path a nested mount owns would // keep that subtree cached, the non-conservative way. holdsSubtree(path) { return this.fence(() => this.inner.holdsSubtree(path)); } invalidate() { return this.fence(async () => { if (this.owns(this.prefix)) await this.inner.invalidate(); }); } clear() { return this.invalidatePrefix(this.prefix); } } //# sourceMappingURL=view.js.map