UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

192 lines • 7.38 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 { FileChangeKind, FileEvent, PathSpec } from "../types.js"; import { rekey, underPath } from "../utils/key_prefix.js"; import { rstripSlash } from "../utils/slash.js"; export function specFor(root, virtual) { return PathSpec.fromStrPath(virtual, rekey(root.virtual, root.vfsPath, virtual)); } /** * The event saying what changed under `root` is not known: UNKNOWN on the * root, which the hub reads as "re-inventory everything below". Mirrors * Python `lost_at`. */ export function lostAt(root, observed) { return new FileEvent({ kind: FileChangeKind.UNKNOWN, path: root, timestamp: observed }); } /** * How one item's paths moved: each path it left, each it reached, and each it * kept that `updated` says changed. Mirrors Python `changes_between`. */ export function changesBetween(before, after, updated) { const kept = new Set(before.filter((path) => after.includes(path))); const changes = []; for (const path of before) if (!kept.has(path)) changes.push([FileChangeKind.DELETE, path]); for (const path of after) if (!kept.has(path)) changes.push([FileChangeKind.CREATE, path]); for (const path of after) if (kept.has(path) && updated(path)) changes.push([FileChangeKind.UPDATE, path]); return changes; } /** * The changes a subscription to `root` reports (`eventsUnder`), from each change and * the virtual path it is at. Mirrors Python `changes_under`. */ export function changesUnder(root, changes, observed) { const events = []; for (const [kind, virtual] of changes) { events.push(new FileEvent({ kind, path: specFor(root, virtual), timestamp: observed })); } return eventsUnder(root, events); } /** * The events a subscription to `root` reports: each at or below it as itself, and * each at a directory above it as the root's own, since that directory removed * removes the root, and any other change to it may have moved the root * (UNKNOWN). A move out of the root is a delete where it was. Mirrors Python * `events_under`. */ export function eventsUnder(root, events) { const top = rstripSlash(root.virtual); const kept = []; for (const event of events) { const virtual = event.path.virtual; const previous = event.previousPath; if (underPath(virtual, top)) { kept.push(event); } else if (previous !== null && underPath(previous.virtual, top)) { kept.push(new FileEvent({ kind: FileChangeKind.DELETE, path: previous, timestamp: event.timestamp, })); } else if (top.startsWith(`${rstripSlash(virtual)}/`)) { const gone = event.kind === FileChangeKind.DELETE; kept.push(new FileEvent({ kind: gone ? event.kind : FileChangeKind.UNKNOWN, path: root, timestamp: event.timestamp, })); } } return kept; } /** * Record how one item of a feed that names items by id moved from where the * last read placed it to `now`, and return where it was. * * An item seen before leaves its old path when it moved and is updated in * place otherwise; one not seen before is created when it is as the service * made it and updated otherwise, since where it was is not known. * Mirrors Python `settle`. */ export function settle(changes, seen, itemId, now, made) { const was = Object.hasOwn(seen, itemId) ? (seen[itemId] ?? null) : null; // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete seen[itemId]; if (now === null) { if (was !== null) changes.push([FileChangeKind.DELETE, was]); return was; } seen[itemId] = now; if (was === null) { changes.push([made ? FileChangeKind.CREATE : FileChangeKind.UPDATE, now]); } else if (was !== now) { changes.push([FileChangeKind.DELETE, was]); changes.push([FileChangeKind.CREATE, now]); } else { changes.push([FileChangeKind.UPDATE, now]); } return was; } /** * Carry each kept path below a folder that moved to the folder's new path, or * forget it when the folder left. * * The moves of one read apply at once, each path by the deepest folder it was * below, so a folder that takes another's old name does not pull in what the * other held. Paths the read placed itself are where their items are now and * stay as they are. Mirrors Python `moved_below`. */ export function movedBelow(seen, moves, placed) { for (const [itemId, path] of Object.entries(seen)) { if (placed.has(itemId)) continue; let was = null; for (const old of moves.keys()) { if (path.startsWith(`${old}/`) && (was === null || old.length > was.length)) was = old; } if (was === null) continue; const now = moves.get(was) ?? null; if (now === null) { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete seen[itemId]; } else { seen[itemId] = now + path.slice(was.length); } } } /** * Whether an item is as it was made: its creation and last modification * times, both the service's own clock, agree. Mirrors Python `just_made`. */ export function justMade(created, modified) { if (created === null || created === undefined || created === '') return false; if (modified === null || modified === undefined || modified === '') return false; return Date.parse(created) === Date.parse(modified); } /** * The path of a folder below the folder `top` by each folder's name and * parent, '' for `top` itself, null when its parents never reach `top`. Each * folder looked up is added to `folders`; a parent of '' is outside. Mirrors * Python `folder_path`. */ export async function folderPath(folderId, top, folders, lookUp) { const names = []; const visited = new Set(); let current = folderId; while (current !== top) { if (visited.has(current)) return null; visited.add(current); let entry = Object.hasOwn(folders, current) ? folders[current] : undefined; if (entry === undefined) { const found = await lookUp(current); if (found === null) return null; folders[current] = found; entry = found; } if (entry[1] === '') return null; names.push(entry[0]); current = entry[1]; } return names.reverse().join('/'); } //# sourceMappingURL=delta.js.map