@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
228 lines • 9.82 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 { Delta, FileChangeKind } from "../../types.js";
import { changesUnder, folderPath, justMade, lostAt, movedBelow, settle, } from "../../events/delta.js";
import { virtualOf } from "../../events/payload.js";
import { GoogleApiError } from "../google/client.js";
import { FOLDER_MIME, MIME_TO_EXT } from "../google/drive.js";
import { feedCheckpoint, feedState } from "../google/events.js";
import { uniqueSharedDriveName } from "./readdir.js";
import { rootContext } from "./resolve.js";
/**
* Whether a changed item is a folder the hook places by name; a shared drive's
* root is named by the root listing instead. Mirrors Python's `_is_folder`.
*/
function isFolder(item) {
if (item.mimeType !== FOLDER_MIME)
return false;
return !(item.driveId !== undefined && item.driveId !== '' && (item.parents ?? []).length === 0);
}
/** The name the listing gives `item`. */
function filenameOf(item) {
return item.name + (MIME_TO_EXT[item.mimeType ?? ''] ?? '');
}
/**
* Where items sit on a Drive mount, found by their parents up to the mount
* root, for one read. Each folder looked up is kept (name and parent, by id)
* in `folders`, which the checkpoint carries to the next read. Mirrors
* Python's `_Places`.
*/
class Places {
folders;
drive;
top;
scoped;
drives = null;
driveRoots = new Set();
constructor(drive, top, scoped, folders) {
this.drive = drive;
this.top = top;
this.scoped = scoped;
this.folders = folders;
}
/**
* The folders the checkpoint carries: every one looked up but a shared
* drive's root, whose name the root listing assigns afresh whenever My
* Drive's own top entries change.
*/
kept() {
return Object.fromEntries(Object.entries(this.folders).filter(([id]) => !this.driveRoots.has(id)));
}
/** The mount path of the folder an item sits in, '' for the root, null outside. */
async under(parents) {
const parent = parents[0];
if (parent === undefined)
return null;
if (parent === this.top)
return '';
return this.folder(parent);
}
/** The mount path of a folder, null when it is outside the mount. */
folder(folderId) {
return folderPath(folderId, this.top, this.folders, (id) => this.lookUp(id));
}
/**
* A folder's name and parent, null when it is gone; a shared drive's root
* sits under an unscoped mount's root by the name its listing gives it, and
* nowhere on a scoped one.
*/
async lookUp(folderId) {
let item;
try {
item = await this.drive.getFile(folderId);
}
catch (err) {
if (!(err instanceof GoogleApiError) || err.status !== 404)
throw err;
console.debug(`drive folder ${folderId} is gone: ${err.message}`);
return null;
}
const parents = item.parents ?? [];
if (item.driveId !== undefined && item.driveId !== '' && parents.length === 0) {
this.driveRoots.add(folderId);
const name = this.scoped ? '' : await this.driveName(folderId);
return [name, name !== '' ? this.top : ''];
}
return [item.name, parents[0] ?? ''];
}
/** The name the root listing gives a shared drive, '' when it is no longer listed. */
async driveName(driveId) {
if (this.drives === null) {
const taken = new Set((await this.drive.listFiles({ folderId: 'root' })).map(filenameOf));
this.drives = new Map();
for (const shared of await this.drive.listSharedDrives()) {
const name = uniqueSharedDriveName(shared.name, taken);
taken.add(name);
this.drives.set(shared.id, name);
}
}
return this.drives.get(driveId) ?? '';
}
}
/**
* Checkpointed delta over Drive's change feed.
*
* Drive pings a channel with no body, so a ping reads `changes.list` from the
* page token the last read handed out. A change names an item by id, with its
* name and parents, 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, trashed or no longer shared
* leaves it. An item seen for the first time is created when it is as Drive
* made it (its created and modified times agree) and updated otherwise; one
* removed before any read
* placed it names nothing, and is left to the index's expiry. On an unscoped
* mount, whose root lists the shared drives, a shared drive made, renamed or
* left reports the root as unknown. 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, and a shared drive is named afresh each read, as the root
* listing may rename it. An item the feed lists more than once is taken at its
* last change.
*
* Mirrors Python `GDriveDeltaHook` (`core/gdrive/events.py`).
*/
export class GDriveDeltaHook {
accessor;
constructor(accessor) {
this.accessor = accessor;
}
async pull(root, checkpoint) {
const drive = this.accessor.drive;
const observed = new Date();
const [folderId] = await rootContext(this.accessor);
const scoped = folderId !== 'root';
const state = feedState(checkpoint);
if (state === null) {
const token = await drive.startPageToken();
const top = scoped ? folderId : (await drive.getFile('root')).id;
return new Delta({ changes: [], checkpoint: encode(token, top, {}, {}) });
}
const [found, token] = await drive.listChanges(state.token);
const top = String(state.root);
const places = new Places(drive, top, scoped, { ...(state.folders ?? {}) });
const seen = { ...(state.seen ?? {}) };
const changes = [];
let unknown = false;
const latest = new Map();
for (const change of found) {
if (change.changeType === 'drive') {
unknown ||= !scoped;
continue;
}
const itemId = change.fileId;
if (typeof itemId !== 'string')
continue;
const item = change.file ?? null;
const gone = change.removed === true || item === null || item.trashed === true;
if (itemId === top) {
if (gone)
changes.push([FileChangeKind.DELETE, virtualOf(root, '')]);
continue;
}
latest.delete(itemId);
latest.set(itemId, [itemId, item, gone]);
}
const entries = [...latest.values()];
for (const [itemId] of entries) {
if (!Object.hasOwn(seen, itemId) && Object.hasOwn(places.folders, itemId)) {
const placed = await places.folder(itemId);
if (placed !== null)
seen[itemId] = virtualOf(root, placed);
}
}
for (const [itemId, item, gone] of entries) {
if (item !== null && isFolder(item)) {
places.folders[itemId] = [item.name, item.parents?.[0] ?? ''];
}
else if (gone) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete places.folders[itemId];
}
}
const moves = new Map();
for (const [itemId, item, gone] of entries) {
let where = null;
if (item !== null) {
const base = await places.under(item.parents ?? []);
if (base !== null)
where = virtualOf(root, `${base}/${filenameOf(item)}`);
}
if (gone && where !== null && !Object.hasOwn(seen, itemId))
seen[itemId] = where;
const now = gone ? null : where;
const made = item !== null && justMade(item.createdTime, item.modifiedTime);
const was = settle(changes, seen, itemId, now, made);
if (was !== null && was !== now)
moves.set(was, now);
}
movedBelow(seen, moves, new Set(entries.map(([itemId]) => itemId)));
const events = changesUnder(root, changes, observed);
if (unknown)
events.push(lostAt(root, observed));
return new Delta({
changes: events,
checkpoint: encode(token, top, places.kept(), seen),
});
}
}
/** Encode the state the next read starts from. Mirrors Python's `_checkpoint`. */
function encode(token, top, folders, seen) {
return feedCheckpoint({ token, root: top, folders, seen });
}
export function buildDeltaHook(accessor) {
return new GDriveDeltaHook(accessor);
}
//# sourceMappingURL=events.js.map