122 lines
3.0 KiB
TypeScript
122 lines
3.0 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
|
|
import { createCursorPaginationRuntime } from "../../src/adapters/query-cache/cursor-pagination-runtime.ts";
|
|
|
|
const profile = {
|
|
profileId: "bounded-cursor-v1",
|
|
maxPages: 3,
|
|
maxTotalItems: 4,
|
|
maxEstimatedBytes: 1_024,
|
|
maxCursorBytes: 64,
|
|
allowSparsePage: false,
|
|
} as const;
|
|
|
|
describe("bounded cursor pagination runtime", () => {
|
|
it("loads a stable finite chain without exposing cursors in its value", async () => {
|
|
const loadPage = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
value: {
|
|
items: ["one"],
|
|
nextCursor: "cursor-2",
|
|
hasMore: true,
|
|
snapshotToken: "snapshot-a",
|
|
},
|
|
})
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
value: {
|
|
items: ["two"],
|
|
nextCursor: null,
|
|
hasMore: false,
|
|
snapshotToken: "snapshot-a",
|
|
},
|
|
});
|
|
const runtime = createCursorPaginationRuntime({
|
|
definitionId: "LIST_ALL",
|
|
profile,
|
|
loadPage,
|
|
});
|
|
|
|
await expect(runtime.loadAll({})).resolves.toEqual({
|
|
ok: true,
|
|
value: ["one", "two"],
|
|
});
|
|
expect(loadPage.mock.calls.map(([cursor]) => cursor)).toEqual([
|
|
null,
|
|
"cursor-2",
|
|
]);
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
page: {
|
|
items: [],
|
|
nextCursor: "next",
|
|
hasMore: true,
|
|
snapshotToken: null,
|
|
},
|
|
code: "PAGINATION_PAGE_INVALID",
|
|
},
|
|
{
|
|
page: {
|
|
items: ["one"],
|
|
nextCursor: null,
|
|
hasMore: true,
|
|
snapshotToken: null,
|
|
},
|
|
code: "PAGINATION_PAGE_INVALID",
|
|
},
|
|
])("rejects invalid page invariants", async ({ page, code }) => {
|
|
const runtime = createCursorPaginationRuntime({
|
|
definitionId: "LIST_ALL",
|
|
profile,
|
|
loadPage: async () => ({ ok: true, value: page }),
|
|
});
|
|
await expect(runtime.loadAll({})).resolves.toMatchObject({
|
|
ok: false,
|
|
error: { kind: "PAGINATION_CONTRACT_VIOLATION", code },
|
|
});
|
|
});
|
|
|
|
it("rejects cursor loops and snapshot drift", async () => {
|
|
const loop = createCursorPaginationRuntime({
|
|
definitionId: "LIST_ALL",
|
|
profile,
|
|
loadPage: async () => ({
|
|
ok: true,
|
|
value: {
|
|
items: ["one"],
|
|
nextCursor: "same",
|
|
hasMore: true,
|
|
snapshotToken: null,
|
|
},
|
|
}),
|
|
});
|
|
await expect(loop.loadAll({})).resolves.toMatchObject({
|
|
ok: false,
|
|
error: { code: "PAGINATION_CURSOR_LOOP" },
|
|
});
|
|
|
|
let page = 0;
|
|
const drift = createCursorPaginationRuntime({
|
|
definitionId: "LIST_ALL",
|
|
profile,
|
|
loadPage: async () => ({
|
|
ok: true,
|
|
value: {
|
|
items: [String(page)],
|
|
nextCursor: page++ === 0 ? "next" : null,
|
|
hasMore: page === 1,
|
|
snapshotToken: page === 1 ? "snapshot-a" : "snapshot-b",
|
|
},
|
|
}),
|
|
});
|
|
await expect(drift.loadAll({})).resolves.toMatchObject({
|
|
ok: false,
|
|
error: { code: "PAGINATION_SNAPSHOT_CHANGED" },
|
|
});
|
|
});
|
|
});
|