feat: 기능 추가 과정중
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
import type { Result } from "../../application/result.ts";
|
||||
import type {
|
||||
CursorPage,
|
||||
CursorPaginationProfile,
|
||||
CursorPaginationRuntime,
|
||||
} from "../../contracts/cursor-pagination.ts";
|
||||
import { createFailure } from "../../contracts/errors.ts";
|
||||
|
||||
export function createCursorPaginationRuntime<Value>(dependencies: Readonly<{
|
||||
definitionId: string;
|
||||
profile: CursorPaginationProfile;
|
||||
loadPage(
|
||||
cursor: string | null,
|
||||
context: Readonly<{ signal?: AbortSignal }>,
|
||||
): Promise<Result<CursorPage<Value>>>;
|
||||
}>): CursorPaginationRuntime<Value> {
|
||||
validateProfile(dependencies.profile);
|
||||
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 < dependencies.profile.maxPages;
|
||||
pageIndex += 1
|
||||
) {
|
||||
if (context.signal?.aborted) {
|
||||
return failure("REQUEST_ABORTED", "PAGINATION_ABORTED");
|
||||
}
|
||||
const result = await dependencies.loadPage(cursor, context);
|
||||
if (!result.ok) return result;
|
||||
const page = result.value;
|
||||
if (!isValidPage(page, dependencies.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 > dependencies.profile.maxTotalItems ||
|
||||
estimatedBytes(items) > dependencies.profile.maxEstimatedBytes
|
||||
) {
|
||||
return failure(
|
||||
"RESULT_LIMIT_EXCEEDED",
|
||||
"PAGINATION_RESULT_LIMIT",
|
||||
);
|
||||
}
|
||||
if (!page.hasMore) return { ok: true, value: Object.freeze(items) };
|
||||
const nextCursor = 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, dependencies.definitionId, 0, { code }),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function validateProfile(profile: CursorPaginationProfile): void {
|
||||
if (
|
||||
!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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user