@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
161 lines • 6.7 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 { underPath } from "../../utils/key_prefix.js";
import { stripSlash } from "../../utils/slash.js";
import { sortedJson } from "../../utils/sort.js";
import { changesUnder } from "../../events/delta.js";
import { virtualOf } from "../../events/payload.js";
import { continueFolder, latestCursor } from "./api.js";
import { DropboxApiError } from "./client.js";
import { dropboxPathOf } from "./paths.js";
function isReset(err) {
return err instanceof DropboxApiError && err.status === 409 && err.summary.startsWith('reset');
}
/**
* A listing row's mount-relative path, or null for the mount root.
*
* Dropbox paths are case-insensitive: `path_display` carries the server's
* casing while `rootPath` carries the user's, so the root is stripped
* case-folded and the rest keeps the server's casing, which is safe because
* `path_lower` is `path_display` lowercased, same length. Mirrors Python
* `_relative`.
*/
function relativeOf(accessor, entry) {
const display = entry.path_display ?? entry.path_lower;
if (display === undefined || display === '')
return null;
const base = accessor.rootPath;
const relative = base !== '' && display.toLowerCase().startsWith(base.toLowerCase())
? display.slice(base.length)
: display;
const stripped = stripSlash(relative);
return stripped === '' ? null : stripped;
}
function encode(cursor, seen) {
return sortedJson({ dropbox: { cursor, seen } });
}
/** The cursor and seen paths a checkpoint holds, null for a baseline. */
function decode(checkpoint) {
if (checkpoint === null)
return null;
const data = JSON.parse(checkpoint);
const state = data.dropbox ?? {};
return [state.cursor ?? '', { ...(state.seen ?? {}) }];
}
function dropBelow(seen, relative) {
for (const key of Object.keys(seen)) {
if (underPath(key, relative)) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete seen[key];
}
}
}
/**
* What changed under a Dropbox event root since the last cursor, for the
* consumer to call when Dropbox pings.
*
* Dropbox's push (a webhook, or `list_folder/longpoll` answering `changes`)
* names only the account, so each ping is answered by one
* `list_folder/continue` from the stored cursor. A baseline takes a cursor at
* the present with `get_latest_cursor` and lists nothing.
*
* A row names its path, so a delete is a DELETE where it was, and a move is
* its old path deleted and its new rows. A row does not say whether a file was
* made or replaced, so the checkpoint keeps each path a pull has reported with
* its `content_hash`: a path seen before is an UPDATE, or nothing when its
* content did not change, and any other is a CREATE. A cursor Dropbox resets
* answers UNKNOWN on the event root and a fresh cursor; a root Dropbox cannot
* find has no cursor, and the pull that finds it again reports the root
* UNKNOWN.
*
* Mirrors Python `DropboxDeltaHook` (`core/dropbox/events.py`).
*/
export class DropboxDeltaHook {
accessor;
constructor(accessor) {
this.accessor = accessor;
}
/** A cursor at the root's present, '' when Dropbox cannot find it. */
async latest(root) {
try {
return await latestCursor(this.accessor.tokenManager, dropboxPathOf(this.accessor, root), {
recursive: true,
});
}
catch (err) {
if (!(err instanceof DropboxApiError) || err.status !== 409)
throw err;
console.debug(`dropbox event root has no cursor: ${err.message}`);
return '';
}
}
async pull(root, checkpoint) {
const state = decode(checkpoint);
const observed = new Date();
const lost = [[FileChangeKind.UNKNOWN, root.virtual]];
if (state === null)
return new Delta({ changes: [], checkpoint: encode(await this.latest(root), {}) });
let [cursor] = state;
const seen = state[1];
if (cursor === '') {
cursor = await this.latest(root);
const changes = cursor !== '' ? changesUnder(root, lost, observed) : [];
return new Delta({ changes, checkpoint: encode(cursor, {}) });
}
let rows;
try {
;
({ entries: rows, cursor } = await continueFolder(this.accessor.tokenManager, cursor));
}
catch (err) {
if (!isReset(err))
throw err;
console.debug(`dropbox cursor reset: ${String(err)}`);
const fresh = await this.latest(root);
return new Delta({
changes: changesUnder(root, lost, observed),
checkpoint: encode(fresh, {}),
});
}
const found = [];
for (const row of rows) {
const relative = relativeOf(this.accessor, row);
if (relative === null)
continue;
const virtual = virtualOf(root, relative);
const tag = row['.tag'];
if (tag === 'deleted') {
dropBelow(seen, relative);
found.push([FileChangeKind.DELETE, virtual]);
continue;
}
const version = tag === 'folder' ? '' : (row.content_hash ?? row.rev ?? '');
const previous = Object.hasOwn(seen, relative) ? seen[relative] : undefined;
seen[relative] = version;
if (previous === undefined)
found.push([FileChangeKind.CREATE, virtual]);
else if (previous !== version)
found.push([FileChangeKind.UPDATE, virtual]);
}
return new Delta({
changes: changesUnder(root, found, observed),
checkpoint: encode(cursor, seen),
});
}
}
export function buildDeltaHook(accessor) {
return new DropboxDeltaHook(accessor);
}
//# sourceMappingURL=events.js.map