@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
154 lines • 7.47 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 { conditionVersion } from "../../cache/context.js";
import { dropCached, settleAfter, knownVersions, moveCondition, stale, } from "../../cache/context.js";
import { liftLost, lostCount, record, startOp } from "../../observe/context.js";
import { enoent, enotempty, strerrorOf } from "../../errors/fs.js";
import * as kp from "../../utils/key_prefix.js";
import { holdsChild, keepWalk, requireHook, } from "./driver.js";
import { ConditionLostError } from "./errors.js";
/**
* Build file-or-prefix relocation over one driver.
*
* A single object moves with the driver's native file move. A directory
* owns no object of its own, so it moves as a prefix walk; a source that
* is neither is ENOENT rather than the raw store error. The driver must
* carry a native move — a store without one leaves rename unwired,
* which the dispatcher surfaces as ENOTSUP.
*/
export function makeRename(driver, exists) {
const { moveFile, movePrefix } = driver;
if (moveFile === undefined || movePrefix === undefined) {
throw new Error(`${driver.vfs} driver has no native move; leave rename unwired instead of building it`);
}
return async function rename(accessor, src, dst) {
const kpfx = driver.keyPrefixOf(accessor);
const srcKey = kp.apply(kpfx, src.mountPath);
if (srcKey === kp.apply(kpfx, dst.mountPath)) {
// POSIX rename(2): the same existing file succeeds and performs no
// other action. Reaching the move below would instead delete the
// object on any store that accepts the self-copy, and error on the
// ones that reject it (#150).
if (!(await exists(accessor, src)))
throw enoent(src);
return;
}
const timer = startOp();
// Which of the two paths ran, because only the prefix walk moves a
// subtree and capture retracts on the op name. A rejection from
// moveFile leaves this 'rename': the walk never ran, so nothing under
// the prefix can have moved.
let op = 'rename';
// Whether the source still held a child once the walk was done.
let left = false;
// A move is a copy then a delete, so both carry a condition.
const [cond, source] = await moveCondition(src, dst);
const move = async (conn) => {
const srcPfx = kp.applyDir(kpfx, src.mountPath);
let moved;
if (cond !== null) {
const moveFileIf = requireHook(driver.moveFileIf);
if (await moveFileIf(conn, srcKey, kp.apply(kpfx, dst.mountPath), cond, source))
return true;
op = 'rename_prefix';
moved = await requireHook(driver.movePrefixIf)(conn, srcPfx, kp.applyDir(kpfx, dst.mountPath), knownVersions(src, kpfx), knownVersions(dst, kpfx));
}
else {
try {
if (await moveFile(conn, srcKey, kp.apply(kpfx, dst.mountPath)))
return true;
}
catch (err) {
if (err?.code === 'ENOENT')
throw enoent(src);
throw err;
}
// A directory owns no object of its own, so a clean false here is
// the ordinary way into the prefix walk, not an answer about it.
op = 'rename_prefix';
moved = await movePrefix(conn, srcPfx, kp.applyDir(kpfx, dst.mountPath));
}
// The walk lists once, so a key written under the source after its
// page was read stayed behind: the move copied and removed what it
// listed, and the source is not empty.
left = moved && (await holdsChild(driver, conn, srcPfx));
return moved;
};
const settle = (moved) => {
// undefined when the store threw: movePrefix is a paginated walk
// that can fail having already moved keys, so only a clean false,
// where nothing moved at all, is skipped.
if (moved === false)
return Promise.resolve();
// Two records for one op, because a move invalidates the token of
// both paths: src's object left, dst's was replaced by it. Order is
// free -- both are pure retractions and deletions commute -- but it
// stops being free if either carries a token.
record(op, src.virtual, driver.vfs, 0, timer);
record(op, dst.virtual, driver.vfs, 0, timer);
return Promise.resolve();
};
// The path a refusal names and the version its write sent.
const named = (key, err) => {
if (key === kp.apply(kpfx, dst.mountPath))
return [dst, conditionVersion(cond)];
const sent = err.versions.get(key) ?? (key === srcKey ? source : null);
return [
key === srcKey ? src : kp.keyPath(src, kpfx, key),
typeof sent === 'string' ? sent : null,
];
};
const upto = lostCount();
const { conn, close } = await driver.connect(accessor);
let moved;
try {
moved = await settleAfter(async () => {
try {
return await move(conn);
}
finally {
await close();
}
}, settle);
}
catch (err) {
if (!(err instanceof ConditionLostError))
throw err;
const [lost, sent] = named(await keepWalk(src, kpfx, err), err);
// The untouched end of a refused move keeps its version.
if (err.landed)
await dropCached(dst);
else if (lost === dst) {
if (source !== null && source !== '')
await dropCached(src, source);
}
else if (cond?.kind === 'match')
await dropCached(dst, cond.version);
const refusal = await stale(lost, { landed: err.landed, version: sent });
throw Object.assign(refusal, {
recovered: err.recovered,
recoveryError: err.recoveryError !== null ? strerrorOf(err.recoveryError) : null,
recoveryReason: err.recoveryReason,
});
}
if (!moved)
throw enoent(src);
liftLost(src, upto, op !== 'rename');
liftLost(dst, upto, op !== 'rename');
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- move sets it inside the walk
if (left)
throw enotempty(src);
};
}
//# sourceMappingURL=rename.js.map