UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

148 lines • 5.98 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 { jsonBytes } from "../render/json.js"; import { efbig, enoent, isEnoent } from "../../errors/fs.js"; import { makeRead, perKind } from "../hierarchy/read.js"; import { listBases, listRecords, listTables, recordPages } from "./client.js"; import { AirtableApiError } from "./errors.js"; import { normalizeBase, normalizeTable, recordsJsonl } from "./normalize.js"; import { schemaTable } from "./readdir.js"; import { detectScope } from "./scope.js"; /** * Refuse a file its parent listing does not hold. `makeRead` proves every * file's parent by the row the mount hands. A view file is named by its view * id, so under a real `views` directory a wrong name, or another table's view * id, is still the reader's to prove: without this `jq` would read it while * `stat` and `cat` call it absent, and a foreign id would reach the API as a * raw 422. */ function ensureListed(path, entry) { if (entry === null) throw enoent(path); } async function readBaseJson(accessor, match, path) { const baseId = match.slots.base_id ?? ''; for (const base of await listBases(accessor)) { if (base.id === baseId) { return jsonBytes(normalizeBase(base, await listTables(accessor, baseId))); } } throw enoent(path); } async function readTableJson(accessor, match, path) { let table; try { table = await schemaTable(accessor, match); } catch (err) { if (isEnoent(err)) throw enoent(path); throw err; } return jsonBytes(normalizeTable(table, match.slots.base_id ?? '')); } async function renderRecords(accessor, match, path, view, window) { const cap = accessor.maxReadRecords; const skip = window.offset ?? 0; const limit = window.limit ?? null; // A window is a record count pushed into maxRecords. One record past the // cap proves the full answer would exceed it, without paging through the // rest of a large table first. const wanted = limit === null ? cap + 1 : Math.min(skip + limit, cap + 1); let found; try { found = await listRecords(accessor, match.slots.base_id ?? '', match.slots.table_id ?? '', { ...(view !== undefined ? { view } : {}), maxRecords: wanted, }); } catch (err) { if (err instanceof AirtableApiError && err.notFound) throw enoent(path); throw err; } // EFBIG, reported per operand as `<cmd>: <path>: File too large`: the // whole file would render more than this mount allows. if (found.length > cap) throw efbig(path); return recordsJsonl(found.slice(skip)); } function readRecords(accessor, match, path, _entry, _parent, window) { return renderRecords(accessor, match, path, undefined, window); } async function readView(accessor, match, path, entry, _parent, window) { ensureListed(path, entry); return renderRecords(accessor, match, path, match.slots.view_id ?? '', window); } export const read = makeRead(detectScope, { base_json: readBaseJson, table_json: readTableJson, }, { windowed: { records: readRecords, view: readView, }, parented: true, }); /** * Stream a records or view file a page of records at a time, the next page * fetched only when the caller pulls it; any other file comes whole, as * `read` gives it. A table or view past `maxReadRecords` throws EFBIG once * that many records are out, rather than refusing them whole: a stream cannot * know a table's size before it has paged through it, and a reader that stops * first (`head -c`, `grep -l`) never reaches it. Mirrors Python's * `read_stream`. */ export async function* readStream(accessor, path, entry = null, parent = null) { const match = detectScope(path); if (match.kind !== 'records' && match.kind !== 'view') { yield await read(accessor, path, entry, parent); return; } if (parent === null) throw enoent(path); let view; if (match.kind === 'view') { ensureListed(path, entry); view = match.slots.view_id ?? ''; } const cap = accessor.maxReadRecords; let sent = 0; try { for await (const page of recordPages(accessor, match.slots.base_id ?? '', match.slots.table_id ?? '', { ...(view !== undefined ? { view } : {}), maxRecords: cap + 1 })) { const kept = page.slice(0, cap - sent); sent += kept.length; if (kept.length > 0) yield recordsJsonl(kept); if (kept.length < page.length) throw efbig(path); } } catch (err) { if (err instanceof AirtableApiError && err.notFound) throw enoent(path); throw err; } } /** * The first `n` records of a records or view file, by `maxRecords`, each on * its own line. A count past `maxReadRecords` that the table fills throws * EFBIG, as a whole read of that table does. Mirrors `_first_records` in * `mirage/core/airtable/read.py`. */ function firstRecords(accessor, _match, path, n, entry = null, parent = null) { return read(accessor, path, entry, parent, { limit: n }); } export const firstLines = perKind(detectScope, { records: firstRecords, view: firstRecords }); //# sourceMappingURL=read.js.map