@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
104 lines • 4.86 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 { eisdir, enoent } from "../../errors/fs.js";
import { sliceWindow } from "../../utils/ranges.js";
import { stripSlash } from "../../utils/slash.js";
import { ROOT } from "./scope.js";
/**
* Build a call only some file kinds answer: classify, guard, dispatch, null
* for the rest.
*
* The line calls (`firstLines`, `lastLines`, `lineCount`, `follow`) are
* answered for a backend's data files (a table's rows, a collection's
* documents) and not for its metadata files (a schema, a database.json),
* which the caller reads the usual way. The table names the data kinds once,
* so no answer repeats the check. Each answer is called as
* `answer(accessor, match, path, ...args)`, after `guard`. Mirrors
* `per_kind` in `mirage/core/hierarchy/read.py`.
*/
export function perKind(detect, answers, options = {}) {
return async (accessor, path, ...args) => {
const match = detect(path);
const answer = answers[match.kind];
if (answer === undefined)
return null;
if (options.guard !== undefined)
await options.guard(accessor, match, path.virtual);
return answer(accessor, match, path, ...args);
};
}
/**
* Build a hierarchy read: classify, dispatch, refuse the rest.
*
* Readers own their fetches, guards and rendering, handed what is at the path
* (`entry`) and at its parent (`parent`) as the listings name them; the kit
* owns the classification and the ENOENT funnel for every non-file shape.
* `readers` holds one reader per leaf kind. Mirrors `make_read` in
* `mirage/core/hierarchy/read.py`.
*/
export function makeRead(detect, readers, options = {}) {
const windowed = options.windowed ?? {};
const parented = options.parented === true;
return async function read(accessor, path, entry = null, parent = null, window = {}) {
const match = detect(path);
const windowReader = windowed[match.kind];
const reader = readers[match.kind];
if (parented &&
(windowReader !== undefined || reader !== undefined) &&
parent === null &&
stripSlash(path.vfsPath).includes('/')) {
// Named as the file the caller asked for, the way GNU reports
// `cat missing/f` against the operand rather than its directory.
throw enoent(path.virtual);
}
if (windowReader !== undefined) {
return windowReader(accessor, match, path, entry, parent, window);
}
if (reader === undefined) {
// A directory that exists by construction (the root, or a
// probed=false scope) read as a file is EISDIR. Everything else is
// reported absent: a matched shape alone is no proof the node
// exists, and GNU says "No such file" for a missing name, "Is a
// directory" only for a real one.
if (match.kind === ROOT ||
(match.scope !== null && !match.scope.leaf && !match.scope.probed)) {
throw eisdir(path);
}
throw enoent(path);
}
return reader(accessor, match, path, entry, parent);
};
}
/**
* Build a byte-ranged read over a hierarchy read.
*
* A rendered file has no remote range to ask for — its bytes do not exist
* until the read renders them — so the window is sliced after the fact. A
* stored blob does (discord and slack attachments serve HTTP range
* requests), and downloading the whole file to keep a slice would defeat the
* ranged read; those kinds name a ranged reader in `ranged` and push the
* byte window to the source.
*/
export function makeReadRange(detect, read, ranged) {
return async function readRange(accessor, path, entry = null, parent = null, options) {
const offset = options?.offset ?? 0;
const size = options?.size ?? null;
const match = detect(path);
const fn = ranged[match.kind];
if (fn !== undefined)
return fn(accessor, match, path, entry, parent, offset, size);
return sliceWindow(await read(accessor, path, entry, parent), offset, size);
};
}
//# sourceMappingURL=read.js.map