UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

260 lines • 10.7 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 } from "../../types.js"; import { LISTING_TRUST_WINDOW, ROW_READ_BATCH } from "./constants.js"; import { soleCommandStarted } from "./scope.js"; const WRITES_BY_PREFIX = new Map(); const ROW_WRITES_BY_PREFIX = new Map(); // Each row written within the last window, with its write count and time. const ROWS_WRITTEN_BY_PREFIX = new Map(); // The open memos on each prefix, which share its write count. const HOLDERS_BY_PREFIX = new Map(); function positions(kept) { const keys = kept.listing.entries ?? kept.listing.partialEntries ?? []; kept.positions ??= new Map(keys.map((key, at) => [key, at])); return kept.positions; } /** * A listing as `read` answers it: a kept one copied, so a caller's edit never * reaches it, or, given `child`, narrowed to that key where it is listed. */ function answer(listing, child, kept) { if (child === undefined && kept === null) return listing; const named = child !== undefined && (kept === null ? (listing.entries ?? listing.partialEntries ?? []).includes(child) : positions(kept).has(child)); const keys = (entries) => child === undefined ? [...entries] : named ? [child] : []; const copy = { ...listing }; if (copy.entries != null) copy.entries = keys(copy.entries); if (copy.partialEntries != null) copy.partialEntries = keys(copy.partialEntries); return copy; } /** * The listings one store read for the running commands. * * A read is served again only to the command that made it, only while no * listing write to the key prefix has finished since the read began, for * under LISTING_TRUST_WINDOW seconds, and never past the listing's own * expiry. Row reads batch at most ROW_READ_BATCH siblings and share this * lifetime, a row the batch did not find is left to a lookup of its own, * and a row-only write drops that row alone. Each fill drops the oldest * reads no command can be served any more, so a read stays held for about * one window. The key prefix's write count lives while an open memo holds * the prefix. On the fallback storage, while more than one command is live, * there is no sole stamp and every read goes to Redis. */ export class ListingMemo { keyPrefix; clock; reads = new Map(); closed = false; reference = new WeakRef(this); /** * @param keyPrefix the store's key prefix, whose writes count. * @param clock the store's clock, whose wall time a listing's expiry is * compared with. */ constructor(keyPrefix, clock) { this.keyPrefix = keyPrefix; this.clock = clock; const holders = HOLDERS_BY_PREFIX.get(keyPrefix) ?? new Set(); holders.add(this.reference); HOLDERS_BY_PREFIX.set(keyPrefix, holders); } written() { return WRITES_BY_PREFIX.get(this.keyPrefix) ?? 0; } live(kept) { return (kept.writesBefore === this.written() && performance.now() - kept.readAt < LISTING_TRUST_WINDOW * 1000 && this.clock.now() < kept.expiresAt); } /** Whether this command's live listings already name every observed child key. */ allows(paths) { const stamp = soleCommandStarted(); if (stamp === null) return false; for (const path of paths) { const folder = path.slice(0, path.lastIndexOf('/')) || '/'; const kept = this.reads.get(`${String(stamp)}:${folder}`); if (kept === undefined || !this.live(kept)) return false; if (!positions(kept).has(path)) return false; } return true; } /** Whether a live listing any open memo on this prefix keeps leaves out an observed child key. */ contradicts(paths) { const observed = new Map(); for (const path of paths) { const folder = path.slice(0, path.lastIndexOf('/')) || '/'; observed.set(folder, [...(observed.get(folder) ?? []), path]); } const holders = HOLDERS_BY_PREFIX.get(this.keyPrefix); for (const reference of holders ?? []) { const memo = reference.deref(); if (memo === undefined) { holders?.delete(reference); continue; } for (const [key, kept] of memo.reads) { const named = observed.get(key.slice(key.indexOf(':') + 1)); if (named === undefined || !memo.live(kept)) continue; if (named.some((path) => !positions(kept).has(path))) return true; } } return false; } /** * A folder's listing, read from Redis at most once per command. * * @param folder the listed folder's key. * @param fetch reads the listing from Redis and answers it with its * expiry, or with null when there is no live listing to keep. * @param child the one key a lookup asks about, to narrow the entries to. */ async read(folder, fetch, child) { const stamp = soleCommandStarted(); if (stamp === null) return answer((await fetch())[0], child, null); const key = `${String(stamp)}:${folder}`; const kept = this.reads.get(key); if (kept !== undefined && this.live(kept)) return answer(kept.listing, child, kept); const writes = this.written(); const readAt = performance.now(); const [listing, expiresAt] = await fetch(); if (expiresAt === null || this.written() !== writes || soleCommandStarted() !== stamp) { return answer(listing, child, null); } for (const [held, read] of this.reads) { if (this.live(read)) break; this.reads.delete(held); } const read = { writesBefore: writes, readAt, expiresAt, listing, rowWrites: -1, rows: new Map(), positions: null, }; this.reads.delete(key); this.reads.set(key, read); return answer(listing, child, read); } /** Batch a listed row with its siblings within the listing's lifetime. */ async row(path, fetch) { const stamp = soleCommandStarted(); if (stamp === null) return null; const folder = path.slice(0, path.lastIndexOf('/')) || '/'; const key = `${String(stamp)}:${folder}`; const kept = this.reads.get(key); if (kept === undefined || !this.live(kept)) return null; const writes = ROW_WRITES_BY_PREFIX.get(this.keyPrefix) ?? 0; if (kept.rowWrites !== writes) { for (const [done, write] of ROWS_WRITTEN_BY_PREFIX.get(this.keyPrefix) ?? []) { if (write.count > kept.rowWrites) kept.rows.delete(done); } kept.rowWrites = writes; } let row = kept.rows.get(path); if (row === undefined) { const position = positions(kept).get(path); if (position === undefined) return null; const paths = kept.listing.entries ?? kept.listing.partialEntries ?? []; const start = Math.floor(position / ROW_READ_BATCH) * ROW_READ_BATCH; const missing = paths.slice(start, start + ROW_READ_BATCH).filter((p) => !kept.rows.has(p)); const fetched = await fetch(missing); if (this.live(kept) && (ROW_WRITES_BY_PREFIX.get(this.keyPrefix) ?? 0) === writes && soleCommandStarted() === stamp) { for (const p of missing) kept.rows.set(p, fetched.get(p) ?? null); } row = fetched.get(path) ?? null; } return row === null ? null : new FileStat(structuredClone(row)); } /** Count a finished write of one row, keeping its siblings' reads. */ rowWrote(path) { const count = (ROW_WRITES_BY_PREFIX.get(this.keyPrefix) ?? 0) + 1; ROW_WRITES_BY_PREFIX.set(this.keyPrefix, count); let written = ROWS_WRITTEN_BY_PREFIX.get(this.keyPrefix); if (written === undefined) { written = new Map(); ROWS_WRITTEN_BY_PREFIX.set(this.keyPrefix, written); } const at = performance.now(); written.delete(path); written.set(path, { count, at }); for (const [held, write] of written) { if (at - write.at < LISTING_TRUST_WINDOW * 1000) break; written.delete(held); } } /** Runs `fn` and counts a listing write once it is over, whether it threw or not. */ async writing(fn) { try { return await fn(); } finally { this.wrote(); } } /** Counts a finished listing write to the key prefix. */ wrote() { WRITES_BY_PREFIX.set(this.keyPrefix, this.written() + 1); } /** * Drops the kept listings and releases the key prefix. The last open memo * on a prefix drops its write count, so a host that cycles through * prefixes keeps none it no longer uses. */ close() { this.reads.clear(); if (this.closed) return; this.closed = true; const holders = HOLDERS_BY_PREFIX.get(this.keyPrefix); if (holders !== undefined) { holders.delete(this.reference); for (const reference of holders) { if (reference.deref() === undefined) holders.delete(reference); } if (holders.size > 0) return; } HOLDERS_BY_PREFIX.delete(this.keyPrefix); WRITES_BY_PREFIX.delete(this.keyPrefix); ROW_WRITES_BY_PREFIX.delete(this.keyPrefix); ROWS_WRITTEN_BY_PREFIX.delete(this.keyPrefix); } } //# sourceMappingURL=listing_memo.js.map