@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
75 lines • 3.2 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 { PaginationStalledError } from "./errors.js";
/** Notion's continuation: `next_cursor`, while `has_more` holds. */
export function hasMoreCursor(data) {
const cursor = data.next_cursor;
if (data.has_more !== true || typeof cursor !== 'string' || cursor === '')
return null;
return cursor;
}
/**
* Airtable's continuation: an `offset` on every page but the last. Records
* drop the key on the last page and comments send it as null; both read as
* the end.
*/
export function offsetCursor(data) {
const cursor = data.offset;
return typeof cursor === 'string' && cursor !== '' ? cursor : null;
}
export const HAS_MORE_PAGES = { itemsKey: 'results', nextCursor: hasMoreCursor };
/**
* Collect every item from a cursor-paginated endpoint.
*
* The default reply protocol is the `results` / `has_more` / `next_cursor`
* shape (Notion's); `shape` names another one (an Airtable list is `records`
* plus an `offset` that vanishes on the last page). Where the resume cursor
* goes on the request — a `start_cursor` body field, a query parameter — is
* the caller's, so `fetchPage` owns that merge. Pagination stops when the
* reply stops naming a cursor, and fails with `PaginationStalledError` when
* it names one it already sent. `maxResults` stops early and slices the tail
* of the last page.
*/
export async function cursorItems(fetchPage, maxResults, shape = HAS_MORE_PAGES) {
const collected = [];
for await (const page of cursorPages(fetchPage, shape)) {
for (const item of page)
collected.push(item);
if (maxResults !== undefined && collected.length >= maxResults) {
return collected.slice(0, maxResults);
}
}
return collected;
}
/**
* Each page's items from a cursor-paginated endpoint, the next page fetched
* only when the caller pulls it. Fails with `PaginationStalledError` when the
* reply names a cursor it already sent. Mirrors Python's `cursor_pages`.
*/
export async function* cursorPages(fetchPage, shape = HAS_MORE_PAGES) {
const seen = new Set();
let cursor = null;
for (;;) {
const data = await fetchPage(cursor);
const page = data[shape.itemsKey];
yield Array.isArray(page) ? page : [];
cursor = shape.nextCursor(data);
if (cursor === null)
return;
if (seen.has(cursor))
throw new PaginationStalledError(cursor);
seen.add(cursor);
}
}
//# sourceMappingURL=paginate.js.map