@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
265 lines • 11.3 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 { changesUnder, folderPath, justMade, movedBelow, settle } from "../../events/delta.js";
import { Delta, FileChangeKind } from "../../types.js";
import { rstripSlash, stripSlash } from "../../utils/slash.js";
import { sortedJson } from "../../utils/sort.js";
import { encodedPath, GraphError, graphGet } from "./client.js";
import { parentId } from "./drive.js";
/**
* Checkpointed delta over Graph's `/delta` feed, one per drive.
*
* Graph pings a subscription with no description of the change, so a ping
* reads each drive's feed from the link the last read handed out. The feed
* names an item by id with its name and parent, never its path, so the hook
* places it by its parents up to the mount root, keeping each folder it has
* looked up (name and parent, by id) in the checkpoint, where the folders' own
* changes keep them current; an item whose parents never reach the root is
* outside the mount. The checkpoint also keeps the path of each item a read
* placed: one that moves leaves that path, with everything below a folder, and
* one deleted leaves it. An item seen for the first time is created when it is
* as Graph made it (its created and modified times agree) and updated
* otherwise; one deleted before any
* read placed it names nothing, and is left to the index's expiry. Each read
* first applies the feed's folder renames and moves, so an item listed before
* its folder is still placed where the folder is now. A feed Graph asks to
* resync reports its mount root as unknown and starts again from the present,
* forgetting every place it kept. Each read finds the drives the root covers
* again: a library added since is created and read from then on, one gone is
* deleted, and one renamed moves.
*
* Mirrors Python `GraphDeltaHook` (`core/msgraph/events.py`).
*/
export class GraphDeltaHook {
config;
targets;
constructor(config, targets) {
this.config = config;
this.targets = targets;
}
async pull(root, checkpoint) {
const observed = new Date();
const state = decode(checkpoint);
const targets = new Map((await this.targets(root)).map((t) => [t.drive, t]));
if (state === null) {
const drives = {};
for (const [drive, target] of targets)
drives[drive] = await this.baseline(target);
return new Delta({ changes: [], checkpoint: encode(drives) });
}
const changes = [];
const drives = state.drives;
for (const drive of Object.keys(drives).filter((d) => !targets.has(d))) {
const gone = drives[drive];
if (gone !== undefined)
changes.push([FileChangeKind.DELETE, gone.virtual]);
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete drives[drive];
}
for (const [drive, target] of targets) {
const kept = Object.hasOwn(drives, drive) ? drives[drive] : undefined;
if (kept === undefined) {
drives[drive] = await this.baseline(target);
changes.push([FileChangeKind.CREATE, target.virtual]);
continue;
}
const old = kept.virtual;
if (old !== target.virtual) {
changes.push([FileChangeKind.DELETE, old]);
changes.push([FileChangeKind.CREATE, target.virtual]);
movedBelow(kept.seen, new Map([[old, target.virtual]]), new Set());
kept.virtual = target.virtual;
}
await this.read(drive, kept, changes);
}
return new Delta({
changes: changesUnder(root, changes, observed),
checkpoint: encode(drives),
});
}
/**
* A drive's state at its present: the feed link that lists nothing yet, the
* mount root's id, and nothing placed.
*/
async baseline(target) {
const top = await this.top(target.drive, target.folder);
return {
virtual: target.virtual,
folder: target.folder,
top: top ?? '',
link: await this.latest(target.drive),
folders: {},
seen: {},
};
}
/**
* Read one drive's feed into `changes` and move `kept` on.
*
* A mount rooted below the drive root names its root by path, so each read
* finds that folder again: one deleted, replaced or moved away (its own move
* or an ancestor's) reports the root as unknown and starts placing items
* afresh under whatever the path is now.
*/
async read(drive, kept, changes) {
const virtual = kept.virtual;
const folder = kept.folder ?? '';
let items;
try {
;
[items, kept.link] = await this.feed(kept.link);
}
catch (err) {
if (!(err instanceof GraphError) || err.status !== 410)
throw err;
console.debug(`graph feed of ${drive} must resync: ${err.message}`);
changes.push([FileChangeKind.UNKNOWN, virtual]);
kept.top = (await this.top(drive, folder)) ?? '';
kept.link = await this.latest(drive);
kept.folders = {};
kept.seen = {};
return;
}
if (folder !== '') {
const top = (await this.top(drive, folder)) ?? '';
if (top !== kept.top) {
changes.push([FileChangeKind.UNKNOWN, virtual]);
kept.top = top;
kept.folders = {};
kept.seen = {};
}
}
const { top, folders, seen } = kept;
if (top === '')
return;
const lookUp = (id) => this.lookUp(drive, id);
const placed = items.filter((item) => typeof item.id === 'string' && item.id !== top && item.root === undefined);
for (const item of placed) {
if (!Object.hasOwn(seen, item.id) && Object.hasOwn(folders, item.id)) {
const path = await folderPath(item.id, top, folders, lookUp);
if (path !== null)
seen[item.id] = join(virtual, path);
}
}
for (const item of placed) {
if (item.deleted !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete folders[item.id];
}
else if (item.folder !== undefined) {
folders[item.id] = [item.name ?? '', parentId(item)];
}
}
const moves = new Map();
for (const item of placed) {
const parent = parentId(item);
let now = null;
if (item.deleted === undefined && parent !== '') {
const base = parent === top ? '' : await folderPath(parent, top, folders, lookUp);
if (base !== null) {
const name = item.name ?? '';
now = join(virtual, base !== '' ? `${base}/${name}` : name);
}
}
const made = justMade(item.createdDateTime, item.lastModifiedDateTime);
const was = settle(changes, seen, item.id, now, made);
if (was !== null && was !== now)
moves.set(was, now);
}
movedBelow(seen, moves, new Set(placed.map((item) => item.id)));
}
/**
* Every item a feed link lists, once each at its last row, and the link the
* next read starts from. Graph may list an item more than once across pages;
* its last row is its state.
*/
async feed(link) {
const items = new Map();
let url = link;
for (;;) {
const data = await graphGet(this.config, url);
const value = data.value;
for (const item of Array.isArray(value) ? value : []) {
if (typeof item.id !== 'string')
continue;
items.delete(item.id);
items.set(item.id, item);
}
const delta = data['@odata.deltaLink'];
if (typeof delta === 'string' && delta !== '')
return [[...items.values()], delta];
url = String(data['@odata.nextLink']);
}
}
/** A feed link at the drive's present, which lists nothing. */
async latest(drive) {
const data = await graphGet(this.config, `${drive}/root/delta`, { token: 'latest' });
return String(data['@odata.deltaLink']);
}
/** The id of the folder the mount root is, null when no folder is at its path. */
async top(drive, folder) {
const path = stripSlash(folder);
const url = path !== '' ? `${drive}/root:/${encodedPath(path)}` : `${drive}/root`;
try {
const data = await graphGet(this.config, url, { $select: 'id' });
return String(data.id);
}
catch (err) {
if (!(err instanceof GraphError) || err.status !== 404)
throw err;
console.debug(`graph mount root ${path} is gone: ${err.message}`);
return null;
}
}
/**
* A folder's name and parent id, null when it is gone; the drive root has
* no parent on a mount rooted below it.
*/
async lookUp(drive, folderId) {
let item;
try {
item = (await graphGet(this.config, `${drive}/items/${encodeURIComponent(folderId)}`, {
$select: 'id,name,parentReference,root',
}));
}
catch (err) {
if (!(err instanceof GraphError) || err.status !== 404)
throw err;
console.debug(`graph folder ${folderId} is gone: ${err.message}`);
return null;
}
if (item.root !== undefined)
return ['', ''];
return [item.name ?? '', parentId(item)];
}
}
/** The virtual path of a path below a mount root. Mirrors Python's `_join`. */
function join(virtual, relative) {
if (relative === '')
return virtual;
return `${rstripSlash(virtual)}/${relative}`;
}
function encode(drives) {
return sortedJson({ graph: { drives } });
}
/** What a checkpoint holds, null for a baseline. Mirrors Python's `_decode`. */
function decode(checkpoint) {
if (checkpoint === null)
return null;
const data = JSON.parse(checkpoint);
const drives = data?.graph?.drives;
if (drives === null || drives === undefined || typeof drives !== 'object')
return null;
return { drives: drives };
}
//# sourceMappingURL=events.js.map