@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
100 lines • 4.61 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 { lineVersion } from "../../observe/context.js";
import { READ_FINGERPRINT_OPS, RecordIndex, STAMP_FINGERPRINT_OPS, } from "../../observe/record.js";
import { compareCodePoints } from "../../utils/sort.js";
import { KeyLock } from "../lock.js";
const mutationLocks = new WeakMap();
/** Serialize cache fills with mount ownership changes, including remote writes. */
export function mutationLock(cache, fn) {
let lock = mutationLocks.get(cache);
if (lock === undefined) {
lock = new KeyLock();
mutationLocks.set(cache, lock);
}
return lock.withLock('', fn);
}
/**
* Latest backend fingerprint a read recorded for `path`.
*
* Reads only: a write's own record labels the bytes it sent. Backends stamp a
* read record with the content
* identifier they returned (S3 ETag, OneDrive cTag, Postgres sha256).
* Threading it into the cache entry lets a `fresh` mount's `isFresh`
* compare like with like. null means the bytes carry no token, and the
* entry then stores none: an unverifiable copy is dropped and re-read,
* which is what a fabricated one produced anyway on every backend whose
* token is not an md5 of the content.
*
* Only the newest read of the path counts: when it carries no token, neither
* does the entry, whatever an earlier read stamped. The backend did not vouch
* for the bytes stored, and an older read's token would label bytes it never
* described, which a later revert to that token serves as fresh.
*/
export function latestFingerprint(records, path) {
if (records === undefined)
return null;
for (let i = records.length - 1; i >= 0; i--) {
const rec = records[i];
if (rec !== undefined && READ_FINGERPRINT_OPS.has(rec.op) && rec.path === path) {
return rec.fingerprint === null || rec.fingerprint === '' ? null : rec.fingerprint;
}
}
return null;
}
/**
* Keep the version each path last had on the line, on conditional mounts. A
* read that fills no cache (`grep`, `head`) or a write that keeps no bytes
* (`>>`, a resize, a cross-mount `cp`) still names the version it saw, and
* the next line's write on a conditional mount needs it; so does a refusal,
* whose read may have been served from the cache and left no record. Runs
* when the line ends. Mirrors Python's `keep_versions`.
*
* @param nested a nested line's (`eval`, `$(...)`): it keeps only the
* versions of paths still lost; its line keeps the rest. Its read records
* do not count: a concurrent sibling stage records into the same list.
*/
export async function keepVersions(cache, records, cacheFacts, lost, nested = false) {
const counted = nested ? records.filter((rec) => !READ_FINGERPRINT_OPS.has(rec.op)) : records;
const index = new RecordIndex(counted);
const paths = new Set(nested
? []
: counted
.filter((rec) => STAMP_FINGERPRINT_OPS.has(rec.op) && (rec.fingerprint ?? '') !== '')
.map((rec) => rec.path));
if (lost !== null)
for (const key of lost.marks.keys())
if (lost.holds(key))
paths.add(key);
const versions = {};
for (const path of paths) {
const facts = cacheFacts(path);
if (!facts.cacheable || facts.keepsVersions !== true)
continue;
const [, version] = lineVersion(index, lost, path);
if (version !== null && version !== '')
versions[path] = version;
}
if (Object.keys(versions).length === 0)
return;
try {
await mutationLock(cache, () => cache.keepFingerprints(versions));
}
catch (err) {
const msg = err instanceof Error ? err.message : String(err);
const paths = Object.keys(versions).sort(compareCodePoints);
console.warn(`versions not kept for ${String(paths.length)} paths, first ${paths[0] ?? ''}: ${msg}`);
}
}
//# sourceMappingURL=io.js.map