@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
295 lines • 11.8 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 { 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