Files
tech-log-frontend/src/adapters/query-cache/cursor-pagination-runtime.ts
T
DongHyeonkaandClaude Opus 5 4bff9ca151 chore: sync the frontend template from 4dc033c to 8157ad4
The product was materialized from the template at `4dc033c` and has stayed
on it through 43 template commits, so it was missing all three rounds of
adapter remediation — including files it never had, such as the shared
`abortable-operation` primitive and the `exact-snapshot` decoder that
later fixes are written against. Taking only the newest round was not
possible for that reason: the delta is coherent only as a whole.

The product had not touched `src/adapters` at all since materialization,
so the 140-file delta applied with a three-way merge and no conflicts.
`package.json` was the single overlap and merged cleanly: the product owns
`name`, the template contributed `check:adapter-inventory`,
`check:remediation-ledger` and the image-resolve-signal type fixture.
All 24 product-owned files — README, index.html, CI workflow, i18n
catalog, home page, generated schemas, evidence scripts, component and
visual snapshots — are byte-identical to `main`.

`template.lock.json` now pins the synced revision and tree.

Verified in this repository, not inherited from the template: six type
projects, lint, nine gates (adapter inventory, remediation ledger,
registries, diagnostics, realtime boundaries, architecture, browser
file/storage boundaries, optional recipes, documentation), the production
build, and 2,054 of 2,073 tests. The 19 failures are all in
`tests/unit/ci-artifact-contract.test.ts` and are the same pre-existing
sandbox RLIMIT, EMFILE, umask and `/tmp` permission behaviour the template
records; four suites that failed once under parallel load pass in
isolation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 12:04:58 +09:00

235 lines
7.7 KiB
TypeScript

import type { Result } from "../../application/result.ts";
import type {
CursorPage,
CursorPaginationProfile,
CursorPaginationRuntime,
} from "../../contracts/cursor-pagination.ts";
import { createFailure } from "../../contracts/errors.ts";
import { snapshotExactObject } from "../../contracts/exact-snapshot.ts";
const ABORTED = Symbol("PAGINATION_ABORTED");
/**
* Resolves as soon as the operation settles or the signal aborts, whichever
* comes first. A late operation result is observed and discarded, never thrown
* as an unhandled rejection.
*
* OPT-NET-01. A loader rejection is *not* an abort. The presence of a signal
* says nothing about why the loader failed, so a rejection is re-thrown exactly
* as it would be with no signal at all; only a signal that has actually
* aborted classifies the outcome as cancellation.
*/
async function raceAbort<Value>(
operation: Promise<Value>,
signal: AbortSignal | undefined,
): Promise<Value | typeof ABORTED> {
operation.catch(() => {});
if (!signal) return await operation;
if (signal.aborted) return ABORTED;
return await new Promise<Value | typeof ABORTED>((resolve, reject) => {
const onAbort = () => resolve(ABORTED);
signal.addEventListener("abort", onAbort, { once: true });
operation.then(
(value) => {
signal.removeEventListener("abort", onAbort);
resolve(value);
},
(reason: unknown) => {
signal.removeEventListener("abort", onAbort);
if (signal.aborted) {
resolve(ABORTED);
return;
}
reject(reason);
},
);
});
}
export function createCursorPaginationRuntime<Value>(dependencies: Readonly<{
definitionId: string;
profile: CursorPaginationProfile;
loadPage(
cursor: string | null,
context: Readonly<{ signal?: AbortSignal }>,
): Promise<Result<CursorPage<Value>>>;
}>): CursorPaginationRuntime<Value> {
// NS-07. The caps are captured once, here. Validating the caller's profile and
// then reading it again on every page let a `maxPages` of 1 become 3 after
// construction, so the request count, item total and byte ceiling that were
// checked were not the ones the loop enforced. The collaborators are captured
// for the same reason.
const profile = snapshotProfile(dependencies.profile);
const definitionId = dependencies.definitionId;
const loadPage = dependencies.loadPage;
if (typeof definitionId !== "string" || typeof loadPage !== "function") {
throw new TypeError("Invalid cursor pagination dependencies.");
}
return Object.freeze({
async loadAll(context) {
const items: Value[] = [];
const cursors = new Set<string>();
let cursor: string | null = null;
let snapshot: string | null | undefined;
for (
let pageIndex = 0;
pageIndex < profile.maxPages;
pageIndex += 1
) {
if (context.signal?.aborted) {
return failure("REQUEST_ABORTED", "PAGINATION_ABORTED");
}
// N-10. A non-cooperative loader may never settle, or may settle after
// abort. Race the signal so `loadAll` is bounded, and re-check before
// observing the page so a late completion is ignored rather than
// accumulated into a successful result.
const raced: Result<CursorPage<Value>> | typeof ABORTED =
await raceAbort<Result<CursorPage<Value>>>(
loadPage(cursor, context),
context.signal,
);
if (raced === ABORTED || context.signal?.aborted) {
return failure("REQUEST_ABORTED", "PAGINATION_ABORTED");
}
const result: Result<CursorPage<Value>> = raced;
if (!result.ok) return result;
const page: CursorPage<Value> = result.value;
if (!isValidPage(page, profile)) {
return failure(
"PAGINATION_CONTRACT_VIOLATION",
"PAGINATION_PAGE_INVALID",
);
}
if (snapshot === undefined) {
snapshot = page.snapshotToken;
} else if (snapshot !== page.snapshotToken) {
return failure(
"PAGINATION_CONTRACT_VIOLATION",
"PAGINATION_SNAPSHOT_CHANGED",
);
}
items.push(...page.items);
if (
items.length > profile.maxTotalItems ||
estimatedBytes(items) > profile.maxEstimatedBytes
) {
return failure(
"RESULT_LIMIT_EXCEEDED",
"PAGINATION_RESULT_LIMIT",
);
}
if (!page.hasMore) return { ok: true, value: Object.freeze(items) };
const nextCursor: string | null = page.nextCursor;
if (!nextCursor || cursors.has(nextCursor)) {
return failure(
"PAGINATION_CONTRACT_VIOLATION",
"PAGINATION_CURSOR_LOOP",
);
}
cursors.add(nextCursor);
cursor = nextCursor;
}
return failure(
"RESULT_LIMIT_EXCEEDED",
"PAGINATION_PAGE_LIMIT",
);
},
});
function failure(
kind:
| "PAGINATION_CONTRACT_VIOLATION"
| "RESULT_LIMIT_EXCEEDED"
| "REQUEST_ABORTED",
code: string,
) {
return {
ok: false as const,
error: createFailure(kind, definitionId, 0, { code }),
};
}
}
/**
* NS-07. Copies the profile into an owned frozen record, reading every field
* exactly once, and validates that copy. An accessor, an inherited or extra
* field, a symbol key or a Proxy trap fails closed rather than becoming a cap
* that can change after it was checked.
*/
function snapshotProfile(source: unknown): CursorPaginationProfile {
const profile = snapshotExactObject(source, {
allowed: [
"profileId",
"maxPages",
"maxTotalItems",
"maxEstimatedBytes",
"maxCursorBytes",
"allowSparsePage",
],
required: [
"profileId",
"maxPages",
"maxTotalItems",
"maxEstimatedBytes",
"maxCursorBytes",
"allowSparsePage",
],
}) as CursorPaginationProfile | null;
if (profile === null || typeof profile.allowSparsePage !== "boolean") {
throw new TypeError("Invalid cursor pagination profile.");
}
validateProfile(profile);
return profile;
}
function validateProfile(profile: CursorPaginationProfile): void {
if (
typeof profile.profileId !== "string" ||
!profile.profileId ||
!Number.isSafeInteger(profile.maxPages) ||
profile.maxPages < 1 ||
profile.maxPages > 100 ||
!Number.isSafeInteger(profile.maxTotalItems) ||
profile.maxTotalItems < 1 ||
!Number.isSafeInteger(profile.maxEstimatedBytes) ||
profile.maxEstimatedBytes < 1 ||
!Number.isSafeInteger(profile.maxCursorBytes) ||
profile.maxCursorBytes < 1 ||
profile.maxCursorBytes > 4_096
) {
throw new TypeError("Invalid cursor pagination profile.");
}
}
function isValidPage<Value>(
page: CursorPage<Value>,
profile: CursorPaginationProfile,
): boolean {
const encoder = new TextEncoder();
return (
Boolean(page) &&
Array.isArray(page.items) &&
typeof page.hasMore === "boolean" &&
page.hasMore === (page.nextCursor !== null) &&
(page.nextCursor === null ||
(typeof page.nextCursor === "string" &&
page.nextCursor.length > 0 &&
encoder.encode(page.nextCursor).byteLength <=
profile.maxCursorBytes)) &&
(page.snapshotToken === null ||
(typeof page.snapshotToken === "string" &&
page.snapshotToken.length > 0 &&
encoder.encode(page.snapshotToken).byteLength <=
profile.maxCursorBytes)) &&
(profile.allowSparsePage || !page.hasMore || page.items.length > 0)
);
}
function estimatedBytes(value: unknown): number {
try {
return new TextEncoder().encode(JSON.stringify(value)).byteLength;
} catch {
return Number.POSITIVE_INFINITY;
}
}