UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

228 lines • 9.82 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 { 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