N-05: the conditional-validator key was a colon join over components that may themselves contain colons, so two distinct valid bindings could collide and one definition's ETag could be prepared for another. The key is now a validated, byte-bounded fixed tuple encoded with JSON.stringify. N-09: capture localStorage exactly once and compare StorageEvent.storageArea against that object identity, so a pulse from sessionStorage or any other area is rejected instead of matching on key and value alone. The pulse key is registered in the storage registry as CACHE_INVALIDATION_PULSE. N-10: race loadPage against the caller signal and re-check before observing a page, so a non-cooperative loader can neither hold loadAll forever nor have a post-abort completion accumulated into a successful result. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
161 lines
4.1 KiB
TypeScript
161 lines
4.1 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("returns PAGINATION_ABORTED when a non-cooperative page resolves after abort", async () => {
|
|
const controller = new AbortController();
|
|
let releasePage: ((page: unknown) => void) | undefined;
|
|
const loadPage = vi.fn(
|
|
() =>
|
|
new Promise((resolve) => {
|
|
releasePage = resolve as (page: unknown) => void;
|
|
}),
|
|
);
|
|
const runtime = createCursorPaginationRuntime({
|
|
definitionId: "bounded",
|
|
profile,
|
|
loadPage: loadPage as never,
|
|
});
|
|
|
|
const loading = runtime.loadAll({ signal: controller.signal });
|
|
await Promise.resolve();
|
|
controller.abort();
|
|
|
|
const result = await loading;
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
error: { kind: "REQUEST_ABORTED", code: "PAGINATION_ABORTED" },
|
|
});
|
|
|
|
// The late page completion must be ignored, not accumulated.
|
|
releasePage?.({
|
|
ok: true,
|
|
value: {
|
|
items: ["late"],
|
|
nextCursor: null,
|
|
hasMore: false,
|
|
snapshotToken: "snapshot-1",
|
|
},
|
|
});
|
|
await Promise.resolve();
|
|
expect(loadPage).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
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" },
|
|
});
|
|
});
|
|
});
|