@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
122 lines • 4.45 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 { FileStat, FileType } from "../types.js";
import { isEnotdir, isMissingPath } from "../errors/fs.js";
import { childSpec } from "./key_prefix.js";
import { rstripSlash } from "./slash.js";
/**
* A remnant cascade met an entry the session can see.
*
* Thrown by `removeRemnants` before the visible entry is touched: the
* caller's not-empty refusal is then simply true in the session's own
* view, so every arm answers this by re-raising that original refusal
* rather than deleting visible data.
*/
export class VisibleRemnant extends Error {
code = 'ENOTEMPTY';
virtual;
virtualPath;
constructor(virtual) {
super(`ENOTEMPTY: directory not empty, '${virtual}'`);
this.name = 'VisibleRemnant';
this.virtual = virtual;
this.virtualPath = virtual;
}
}
/**
* One listing entry's bare child name. Cold object-store listings mark
* a directory with a trailing slash, and some backends report whole
* paths rather than names; both normalize to the last component.
*/
export function entryName(entry) {
const trimmed = rstripSlash(entry);
return trimmed.slice(trimmed.lastIndexOf('/') + 1);
}
/**
* Whether any listed name is visible as a child of `base`.
*
* The one emptiness predicate every remnant arm judges with, fed every
* name source its plane can enumerate (the backend listing, and on the
* ops plane the namespace's merged children too), so "visibly empty"
* cannot mean different things at different entry points.
*/
export function visibleBelow(base, names, allowed) {
const root = rstripSlash(base);
for (const name of names) {
if (allowed(`${root}/${entryName(name)}`))
return true;
}
return false;
}
/**
* Remove one directory and everything under it, children first,
* revalidating visibility at every step.
*
* The walk lists raw, but the moment any entry is visible the whole
* cascade aborts with `VisibleRemnant` before that entry is touched:
* between the caller's classification and each deletion another writer
* may have created something visible, and destroying it would turn an
* ostensibly empty rmdir into data loss. An entry that vanishes
* mid-walk is a completed removal (a prefix-store directory disappears
* with its last child), not an error. Every op goes through the
* channel, so the plane's other protections refuse exactly as they
* would a first-class op; the caller answers any cascade failure with
* its original refusal.
*/
export async function removeRemnants(channel, allowed, spec) {
let entries;
try {
entries = await channel.readdir(spec);
}
catch (err) {
if (isMissingPath(err) || isEnotdir(err))
return;
throw err;
}
for (const entry of entries) {
const name = entryName(entry);
const child = childSpec(spec, name);
if (allowed(child.virtual))
throw new VisibleRemnant(child.virtual);
let row;
try {
row = await channel.stat(child);
}
catch (err) {
if (isMissingPath(err) || isEnotdir(err))
continue;
throw err;
}
if (row instanceof FileStat && row.type === FileType.DIRECTORY) {
await removeRemnants(channel, allowed, child);
}
else {
try {
await channel.unlink(child);
}
catch (err) {
if (!(isMissingPath(err) || isEnotdir(err)))
throw err;
}
}
}
try {
await channel.rmdir(spec);
}
catch (err) {
if (!(isMissingPath(err) || isEnotdir(err)))
throw err;
}
}
//# sourceMappingURL=remnants.js.map