UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

122 lines • 4.45 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 { 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