@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
200 lines • 9.16 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 { conditioned, deleteCondition, knownVersions, settleAfter, stale, } from "../../cache/context.js";
import { liftLost, lostCount, record, startOp } from "../../observe/context.js";
import { eisdir, enoent, enotempty } from "../../errors/fs.js";
import * as kp from "../../utils/key_prefix.js";
import { rstripSlash } from "../../utils/slash.js";
import { isDir } from "../../utils/stat_view.js";
import { holdsChild, keepWalk, refused, requireHook, } from "./driver.js";
import { ConditionLostError } from "./errors.js";
import { makeStat } from "./stat.js";
/** Build single-key deletion over one driver. */
export function makeUnlink(driver) {
const stat = makeStat(driver);
return async function unlink(accessor, path) {
// unlink(2) answers ENOENT for a missing name and EISDIR for a
// directory; a store's delete is silent for both, and only the
// command builders check first (see `makeRmdir`).
const found = await stat(accessor, path);
if (isDir(found))
throw eisdir(path);
const key = kp.apply(driver.keyPrefixOf(accessor), path.mountPath);
// A delete needs no prior read, only that nobody wrote since.
const cond = await deleteCondition(path, found.fingerprint ?? null);
const timer = startOp();
const settle = () => {
// Also when the delete threw: one that throws part-way has already
// removed keys, and a pin that outlives the object it names fails
// the next snapshot load.
record('unlink', path.virtual, driver.vfs, 0, timer);
return Promise.resolve();
};
// The connect is outside, because a connection that never opened
// removed nothing.
const { conn, close } = await driver.connect(accessor);
const upto = lostCount();
try {
await settleAfter(async () => {
try {
if (cond !== null)
await requireHook(driver.deleteIf)(conn, key, cond);
else
await driver.deleteFile(conn, key);
}
finally {
await close();
}
}, settle);
}
catch (err) {
if (err instanceof ConditionLostError)
throw await refused(path, err, cond);
throw err;
}
liftLost(path, upto);
};
}
/**
* Build recursive prefix deletion over one driver.
*
* Serves both the `rmR` and `rmdir` slots: on a keyed store an empty
* directory is its marker object, so removing it and removing a subtree
* are the same prefix delete.
*/
export function makeRemovePrefix(driver) {
return async function removePrefix(accessor, path) {
const kpfx = driver.keyPrefixOf(accessor);
const pfx = kp.applyDir(kpfx, path.mountPath);
// Keeps and reports a key changed since the agent (or walk) saw it.
const conditional = conditioned(path, 'delete');
const timer = startOp();
const settle = () => {
// A prefix delete is a paginated walk, so a failure mid-walk has
// already removed keys, as in unlink.
record('rm_r', path.virtual, driver.vfs, 0, timer);
return Promise.resolve();
};
const { conn, close } = await driver.connect(accessor);
const upto = lostCount();
let left;
try {
left = await settleAfter(async () => {
try {
if (conditional) {
await requireHook(driver.deletePrefixIf)(conn, pfx, knownVersions(path, kpfx));
}
else
await driver.deletePrefix(conn, pfx);
// The walk lists once, so a key written under the prefix after its
// page was read stays; as GNU rm's last rmdir meets an entry
// created after its read, that is ENOTEMPTY.
return await holdsChild(driver, conn, pfx);
}
finally {
await close();
}
}, settle);
}
catch (err) {
if (!(err instanceof ConditionLostError))
throw err;
const key = await keepWalk(path, kpfx, err);
const sent = err.versions.get(key);
throw await stale(kp.keyPath(path, kpfx, key), {
version: typeof sent === 'string' ? sent : null,
});
}
liftLost(path, upto, true);
if (left)
throw enotempty(path);
};
}
/**
* Build POSIX `rmdir` over one driver: refuse a non-empty prefix.
*
* Not `makeRemovePrefix`. On a keyed store an empty directory is its
* zero-byte marker object, so a prefix delete removes an empty directory
* correctly and a *non-empty* one recursively, which is `rm -r`, not
* `rmdir`. Sharing one function between the two slots made `rmdir` destroy
* a whole subtree for every caller that does not pre-check emptiness
* itself, and the command builders are the only callers that do: FUSE,
* `ws.vfs` and the sandbox runtimes all reach the op directly.
*
* The listing is the same one `readdir` reads, so the two agree on what a
* child is: a `marker` entry is the prefix's own marker (or a key the
* delimiter listing could not classify) and proves only that the directory
* exists, while any `f` or `d` entry is a child and makes this ENOTEMPTY.
* The walk stops at the first child rather than listing the whole directory
* to answer a yes/no question.
*
* A prefix holding no key at all -- not even the marker -- is a directory
* the store does not have, and rmdir(2) reports ENOENT for it.
* `makeRemovePrefix` stays silent there on purpose, because `rm -r` owns
* that refusal through its own `-f` handling.
*/
export function makeRmdir(driver) {
return async function rmdir(accessor, path) {
const pfx = kp.applyDir(driver.keyPrefixOf(accessor), path.mountPath);
const isRoot = rstripSlash(path.mountPath) === '';
const timer = startOp();
const { conn, close } = await driver.connect(accessor);
let sawKey = false;
let hasChild = false;
let deleted = false;
try {
for await (const child of driver.listChildren(conn, pfx)) {
sawKey = true;
if (child.kind !== 'marker') {
hasChild = true;
break;
}
}
// The marker only, never the prefix: a child created between the
// listing above and this delete would be taken down with it, which
// is the subtree loss this function exists to stop in a smaller
// window. A root holding no key has no marker and falls through as
// the no-op the prefix delete already was.
if (!hasChild && sawKey) {
await driver.deleteFile(conn, pfx);
deleted = true;
// A child written between the listing and the delete still holds
// the directory, so rmdir(2) is ENOTEMPTY and the marker goes back:
// a refused rmdir changes nothing.
hasChild = await holdsChild(driver, conn, pfx);
if (hasChild && driver.markersSupported !== false) {
await driver.put(conn, pfx, new Uint8Array());
}
}
}
finally {
// Gated on the delete having run, not on "did not throw": the
// keyless root above deletes nothing and throws nothing, and a
// listing that fails after its first marker reaches here having
// deleted nothing either. Recording in those cases would retract a
// pin for an object no one touched. A delete that rejects is the
// one case `unlink` treats the other way; here it is immaterial,
// because what rmdir removes is the "d/" marker and a pin can only
// ever name the "d" object beside it.
if (deleted)
record('rmdir', path.virtual, driver.vfs, 0, timer);
await close();
}
if (hasChild)
throw enotempty(path);
if (!sawKey && !isRoot)
throw enoent(path);
};
}
//# sourceMappingURL=remove.js.map