UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

100 lines • 4.61 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 { 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