refactor(server): extract SortablePager for generic pagination
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { CursorError } from "./cursor.js";
|
||||
import { SortablePager, type SortSpec } from "./sortable-pager.js";
|
||||
|
||||
const KEYS = ["created_at", "title", "updated_at"] as const;
|
||||
type Key = (typeof KEYS)[number];
|
||||
|
||||
interface Row {
|
||||
id: string;
|
||||
created_at: number;
|
||||
title: string | null;
|
||||
updated_at: number;
|
||||
}
|
||||
|
||||
const pager = new SortablePager<Row, Key>({
|
||||
validKeys: KEYS,
|
||||
defaultSort: [{ key: "updated_at", direction: "desc" }],
|
||||
label: "rows",
|
||||
getId: (row) => row.id,
|
||||
getSortValue: (row, key) => row[key],
|
||||
});
|
||||
|
||||
const rowA: Row = { id: "a", created_at: 1, title: "alpha", updated_at: 30 };
|
||||
const rowB: Row = { id: "b", created_at: 2, title: "bravo", updated_at: 20 };
|
||||
const rowC: Row = { id: "c", created_at: 3, title: null, updated_at: 30 };
|
||||
|
||||
describe("SortablePager.normalizeSort", () => {
|
||||
it("falls back to default when sort is undefined", () => {
|
||||
expect(pager.normalizeSort(undefined)).toEqual([{ key: "updated_at", direction: "desc" }]);
|
||||
});
|
||||
|
||||
it("falls back to default when sort is empty", () => {
|
||||
expect(pager.normalizeSort([])).toEqual([{ key: "updated_at", direction: "desc" }]);
|
||||
});
|
||||
|
||||
it("dedupes by key, keeping the first occurrence", () => {
|
||||
const sort: SortSpec<Key>[] = [
|
||||
{ key: "title", direction: "asc" },
|
||||
{ key: "title", direction: "desc" },
|
||||
{ key: "updated_at", direction: "desc" },
|
||||
];
|
||||
expect(pager.normalizeSort(sort)).toEqual([
|
||||
{ key: "title", direction: "asc" },
|
||||
{ key: "updated_at", direction: "desc" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("SortablePager.compare", () => {
|
||||
it("orders by primary sort direction", () => {
|
||||
const sort: SortSpec<Key>[] = [{ key: "updated_at", direction: "desc" }];
|
||||
expect(pager.compare(rowA, rowB, sort)).toBeLessThan(0);
|
||||
expect(pager.compare(rowB, rowA, sort)).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("falls through to secondary sort when primary ties", () => {
|
||||
const sort: SortSpec<Key>[] = [
|
||||
{ key: "updated_at", direction: "desc" },
|
||||
{ key: "title", direction: "asc" },
|
||||
];
|
||||
expect(pager.compare(rowA, rowC, sort)).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("breaks final ties with id", () => {
|
||||
const sort: SortSpec<Key>[] = [{ key: "updated_at", direction: "desc" }];
|
||||
const twin: Row = { id: "z", created_at: 99, title: "alpha", updated_at: 30 };
|
||||
expect(pager.compare(rowA, twin, sort)).toBeLessThan(0);
|
||||
});
|
||||
|
||||
it("treats null as less than any value", () => {
|
||||
const sort: SortSpec<Key>[] = [{ key: "title", direction: "asc" }];
|
||||
expect(pager.compare(rowC, rowA, sort)).toBeLessThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("SortablePager cursor roundtrip", () => {
|
||||
const sort: SortSpec<Key>[] = [{ key: "updated_at", direction: "desc" }];
|
||||
|
||||
it("encodes and decodes through the underlying codec", () => {
|
||||
const token = pager.encode(rowA, sort);
|
||||
const decoded = pager.decode(token, sort);
|
||||
expect(decoded.id).toBe("a");
|
||||
expect(decoded.values).toEqual({ updated_at: 30 });
|
||||
});
|
||||
|
||||
it("translates codec errors with the configured label", () => {
|
||||
expect(() => pager.decode("@@@", sort)).toThrow(new CursorError("Invalid rows cursor"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("SortablePager.compareWithCursor", () => {
|
||||
const sort: SortSpec<Key>[] = [{ key: "updated_at", direction: "desc" }];
|
||||
|
||||
it("places rows ahead of the cursor as positive", () => {
|
||||
const token = pager.encode(rowB, sort);
|
||||
const cursor = pager.decode(token, sort);
|
||||
expect(pager.compareWithCursor(rowA, cursor, sort)).toBeLessThan(0);
|
||||
expect(pager.compareWithCursor(rowC, cursor, sort)).toBeLessThan(0);
|
||||
});
|
||||
|
||||
it("returns 0 only for the cursor's own row", () => {
|
||||
const token = pager.encode(rowA, sort);
|
||||
const cursor = pager.decode(token, sort);
|
||||
expect(pager.compareWithCursor(rowA, cursor, sort)).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
import {
|
||||
CursorError,
|
||||
type CursorSortValue,
|
||||
type DecodedCursor,
|
||||
decodeCursor,
|
||||
encodeCursor,
|
||||
} from "./cursor.js";
|
||||
|
||||
export interface SortSpec<K extends string> {
|
||||
key: K;
|
||||
direction: "asc" | "desc";
|
||||
}
|
||||
|
||||
export interface SortablePagerConfig<TItem, K extends string> {
|
||||
validKeys: readonly K[];
|
||||
defaultSort: readonly SortSpec<K>[];
|
||||
label: string;
|
||||
getId: (item: TItem) => string;
|
||||
getSortValue: (item: TItem, key: K) => CursorSortValue;
|
||||
}
|
||||
|
||||
export class SortablePager<TItem, K extends string> {
|
||||
constructor(private readonly config: SortablePagerConfig<TItem, K>) {}
|
||||
|
||||
normalizeSort(sort: readonly SortSpec<K>[] | undefined): SortSpec<K>[] {
|
||||
if (!sort || sort.length === 0) {
|
||||
return [...this.config.defaultSort];
|
||||
}
|
||||
const deduped: SortSpec<K>[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const entry of sort) {
|
||||
if (seen.has(entry.key)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(entry.key);
|
||||
deduped.push(entry);
|
||||
}
|
||||
return deduped.length > 0 ? deduped : [...this.config.defaultSort];
|
||||
}
|
||||
|
||||
compare(left: TItem, right: TItem, sort: readonly SortSpec<K>[]): number {
|
||||
for (const spec of sort) {
|
||||
const leftValue = this.config.getSortValue(left, spec.key);
|
||||
const rightValue = this.config.getSortValue(right, spec.key);
|
||||
const base = compareValues(leftValue, rightValue);
|
||||
if (base === 0) {
|
||||
continue;
|
||||
}
|
||||
return spec.direction === "asc" ? base : -base;
|
||||
}
|
||||
return this.config.getId(left).localeCompare(this.config.getId(right));
|
||||
}
|
||||
|
||||
compareWithCursor(item: TItem, cursor: DecodedCursor<K>, sort: readonly SortSpec<K>[]): number {
|
||||
for (const spec of sort) {
|
||||
const leftValue = this.config.getSortValue(item, spec.key);
|
||||
const rightValue = cursor.values[spec.key] ?? null;
|
||||
const base = compareValues(leftValue, rightValue);
|
||||
if (base === 0) {
|
||||
continue;
|
||||
}
|
||||
return spec.direction === "asc" ? base : -base;
|
||||
}
|
||||
return this.config.getId(item).localeCompare(cursor.id);
|
||||
}
|
||||
|
||||
encode(item: TItem, sort: readonly SortSpec<K>[]): string {
|
||||
return encodeCursor(item, sort, this.config.getId, this.config.getSortValue);
|
||||
}
|
||||
|
||||
decode(token: string, sort: readonly SortSpec<K>[]): DecodedCursor<K> {
|
||||
return decodeCursor(token, sort, this.config.validKeys, this.config.label);
|
||||
}
|
||||
}
|
||||
|
||||
export { CursorError };
|
||||
|
||||
export function compareValues(left: CursorSortValue, right: CursorSortValue): number {
|
||||
if (left === right) {
|
||||
return 0;
|
||||
}
|
||||
if (left === null) {
|
||||
return -1;
|
||||
}
|
||||
if (right === null) {
|
||||
return 1;
|
||||
}
|
||||
if (typeof left === "number" && typeof right === "number") {
|
||||
return left < right ? -1 : 1;
|
||||
}
|
||||
return String(left).localeCompare(String(right));
|
||||
}
|
||||
@@ -35,7 +35,8 @@ import {
|
||||
import type { TerminalManager } from "../terminal/terminal-manager.js";
|
||||
import { TerminalSessionController } from "../terminal/terminal-session-controller.js";
|
||||
import type { TerminalStreamFrame } from "../shared/terminal-stream-protocol.js";
|
||||
import { CursorError, decodeCursor, encodeCursor } from "./pagination/cursor.js";
|
||||
import { CursorError } from "./pagination/cursor.js";
|
||||
import { SortablePager, type SortSpec } from "./pagination/sortable-pager.js";
|
||||
import { TTSManager } from "./agent/tts-manager.js";
|
||||
import { STTManager } from "./agent/stt-manager.js";
|
||||
import type { SpeechToTextProvider, TextToSpeechProvider } from "./speech/speech-provider.js";
|
||||
@@ -473,11 +474,6 @@ interface AgentUpdatesSubscriptionState {
|
||||
isBootstrapping: boolean;
|
||||
pendingUpdatesByAgentId: Map<string, AgentUpdatePayload>;
|
||||
}
|
||||
interface FetchAgentsCursor {
|
||||
sort: FetchAgentsRequestSort[];
|
||||
values: Record<string, string | number | null>;
|
||||
id: string;
|
||||
}
|
||||
type FetchWorkspacesRequestMessage = Extract<
|
||||
SessionInboundMessage,
|
||||
{ type: "fetch_workspaces_request" }
|
||||
@@ -502,12 +498,6 @@ interface WorkspaceUpdatesSubscriptionState {
|
||||
pendingUpdatesByWorkspaceId: Map<string, WorkspaceUpdatePayload>;
|
||||
lastEmittedByWorkspaceId: Map<string, WorkspaceUpdatePayload>;
|
||||
}
|
||||
interface FetchWorkspacesCursor {
|
||||
sort: FetchWorkspacesRequestSort[];
|
||||
values: Record<string, string | number | null>;
|
||||
id: string;
|
||||
}
|
||||
|
||||
function summarizeFetchWorkspacesEntries(entries: Iterable<FetchWorkspacesResponseEntry>): {
|
||||
count: number;
|
||||
projectIds: string[];
|
||||
@@ -5744,26 +5734,6 @@ export class Session {
|
||||
return this.isProviderVisibleToClient(payload.provider) ? payload : null;
|
||||
}
|
||||
|
||||
private normalizeFetchAgentsSort(
|
||||
sort: FetchAgentsRequestSort[] | undefined,
|
||||
): FetchAgentsRequestSort[] {
|
||||
const fallback: FetchAgentsRequestSort[] = [{ key: "updated_at", direction: "desc" }];
|
||||
if (!sort || sort.length === 0) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const deduped: FetchAgentsRequestSort[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const entry of sort) {
|
||||
if (seen.has(entry.key)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(entry.key);
|
||||
deduped.push(entry);
|
||||
}
|
||||
return deduped.length > 0 ? deduped : fallback;
|
||||
}
|
||||
|
||||
private getStatusPriority(agent: AgentSnapshotPayload): number {
|
||||
const attentionReason = agent.attentionReason ?? null;
|
||||
const hasPendingPermission = (agent.pendingPermissions?.length ?? 0) > 0;
|
||||
@@ -5782,115 +5752,6 @@ export class Session {
|
||||
return 4;
|
||||
}
|
||||
|
||||
private getFetchAgentsSortValue(
|
||||
entry: FetchAgentsResponseEntry,
|
||||
key: FetchAgentsRequestSort["key"],
|
||||
): string | number | null {
|
||||
switch (key) {
|
||||
case "status_priority":
|
||||
return this.getStatusPriority(entry.agent);
|
||||
case "created_at":
|
||||
return Date.parse(entry.agent.createdAt);
|
||||
case "updated_at":
|
||||
return Date.parse(entry.agent.updatedAt);
|
||||
case "title":
|
||||
return entry.agent.title?.toLocaleLowerCase() ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
private getFetchAgentsSortValueFromAgent(
|
||||
agent: AgentSnapshotPayload,
|
||||
key: FetchAgentsRequestSort["key"],
|
||||
): string | number | null {
|
||||
switch (key) {
|
||||
case "status_priority":
|
||||
return this.getStatusPriority(agent);
|
||||
case "created_at":
|
||||
return Date.parse(agent.createdAt);
|
||||
case "updated_at":
|
||||
return Date.parse(agent.updatedAt);
|
||||
case "title":
|
||||
return agent.title?.toLocaleLowerCase() ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
private compareSortValues(left: string | number | null, right: string | number | null): number {
|
||||
if (left === right) {
|
||||
return 0;
|
||||
}
|
||||
if (left === null) {
|
||||
return -1;
|
||||
}
|
||||
if (right === null) {
|
||||
return 1;
|
||||
}
|
||||
if (typeof left === "number" && typeof right === "number") {
|
||||
return left < right ? -1 : 1;
|
||||
}
|
||||
return String(left).localeCompare(String(right));
|
||||
}
|
||||
|
||||
private compareFetchAgentsAgents(
|
||||
left: AgentSnapshotPayload,
|
||||
right: AgentSnapshotPayload,
|
||||
sort: FetchAgentsRequestSort[],
|
||||
): number {
|
||||
for (const spec of sort) {
|
||||
const leftValue = this.getFetchAgentsSortValueFromAgent(left, spec.key);
|
||||
const rightValue = this.getFetchAgentsSortValueFromAgent(right, spec.key);
|
||||
const base = this.compareSortValues(leftValue, rightValue);
|
||||
if (base === 0) {
|
||||
continue;
|
||||
}
|
||||
return spec.direction === "asc" ? base : -base;
|
||||
}
|
||||
return left.id.localeCompare(right.id);
|
||||
}
|
||||
|
||||
private encodeFetchAgentsCursor(
|
||||
entry: FetchAgentsResponseEntry,
|
||||
sort: FetchAgentsRequestSort[],
|
||||
): string {
|
||||
return encodeCursor(
|
||||
entry,
|
||||
sort,
|
||||
(e) => e.agent.id,
|
||||
(e, key) => this.getFetchAgentsSortValue(e, key),
|
||||
);
|
||||
}
|
||||
|
||||
private decodeFetchAgentsCursor(
|
||||
cursor: string,
|
||||
sort: FetchAgentsRequestSort[],
|
||||
): FetchAgentsCursor {
|
||||
try {
|
||||
return decodeCursor(cursor, sort, FETCH_AGENTS_SORT_KEYS, "fetch_agents");
|
||||
} catch (error) {
|
||||
if (error instanceof CursorError) {
|
||||
throw new SessionRequestError("invalid_cursor", error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private compareAgentWithCursor(
|
||||
agent: AgentSnapshotPayload,
|
||||
cursor: FetchAgentsCursor,
|
||||
sort: FetchAgentsRequestSort[],
|
||||
): number {
|
||||
for (const spec of sort) {
|
||||
const leftValue = this.getFetchAgentsSortValueFromAgent(agent, spec.key);
|
||||
const rightValue =
|
||||
cursor.values[spec.key] !== undefined ? (cursor.values[spec.key] ?? null) : null;
|
||||
const base = this.compareSortValues(leftValue, rightValue);
|
||||
if (base === 0) {
|
||||
continue;
|
||||
}
|
||||
return spec.direction === "asc" ? base : -base;
|
||||
}
|
||||
return agent.id.localeCompare(cursor.id);
|
||||
}
|
||||
|
||||
private async buildActiveProjectPlacementsByWorkspaceCwd(): Promise<
|
||||
Map<string, ProjectPlacementPayload>
|
||||
> {
|
||||
@@ -5976,7 +5837,7 @@ export class Session {
|
||||
? { ...request.filter, includeArchived: true }
|
||||
: request.filter;
|
||||
const scope = request.type === "fetch_agents_request" ? request.scope : undefined;
|
||||
const sort = this.normalizeFetchAgentsSort(request.sort);
|
||||
const sort = this.agentsPager.normalizeSort(request.sort);
|
||||
|
||||
let agents = await this.listAgentPayloads({
|
||||
labels: filter?.labels,
|
||||
@@ -6008,12 +5869,12 @@ export class Session {
|
||||
};
|
||||
|
||||
let candidates = [...agents];
|
||||
candidates.sort((left, right) => this.compareFetchAgentsAgents(left, right, sort));
|
||||
candidates.sort((left, right) => this.agentsPager.compare(left, right, sort));
|
||||
const cursorToken = request.page?.cursor;
|
||||
if (cursorToken) {
|
||||
const cursor = this.decodeFetchAgentsCursor(cursorToken, sort);
|
||||
const cursor = this.decodeAgentCursor(cursorToken, sort);
|
||||
candidates = candidates.filter(
|
||||
(agent) => this.compareAgentWithCursor(agent, cursor, sort) > 0,
|
||||
(agent) => this.agentsPager.compareWithCursor(agent, cursor, sort) > 0,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6030,7 +5891,7 @@ export class Session {
|
||||
const hasMore = matchedEntries.length > limit;
|
||||
const nextCursor =
|
||||
hasMore && pagedEntries.length > 0
|
||||
? this.encodeFetchAgentsCursor(pagedEntries[pagedEntries.length - 1], sort)
|
||||
? this.agentsPager.encode(pagedEntries[pagedEntries.length - 1].agent, sort)
|
||||
: null;
|
||||
|
||||
return {
|
||||
@@ -6051,6 +5912,75 @@ export class Session {
|
||||
done: 4,
|
||||
};
|
||||
|
||||
private readonly agentsPager = new SortablePager<
|
||||
AgentSnapshotPayload,
|
||||
FetchAgentsRequestSort["key"]
|
||||
>({
|
||||
validKeys: FETCH_AGENTS_SORT_KEYS,
|
||||
defaultSort: [{ key: "updated_at", direction: "desc" }],
|
||||
label: "fetch_agents",
|
||||
getId: (agent) => agent.id,
|
||||
getSortValue: (agent, key) => {
|
||||
switch (key) {
|
||||
case "status_priority":
|
||||
return this.getStatusPriority(agent);
|
||||
case "created_at":
|
||||
return Date.parse(agent.createdAt);
|
||||
case "updated_at":
|
||||
return Date.parse(agent.updatedAt);
|
||||
case "title":
|
||||
return agent.title?.toLocaleLowerCase() ?? "";
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
private readonly workspacesPager = new SortablePager<
|
||||
WorkspaceDescriptorPayload,
|
||||
FetchWorkspacesRequestSort["key"]
|
||||
>({
|
||||
validKeys: FETCH_WORKSPACES_SORT_KEYS,
|
||||
defaultSort: [{ key: "activity_at", direction: "desc" }],
|
||||
label: "fetch_workspaces",
|
||||
getId: (workspace) => workspace.id,
|
||||
getSortValue: (workspace, key) => {
|
||||
switch (key) {
|
||||
case "status_priority":
|
||||
return this.workspaceStatePriority[workspace.status];
|
||||
case "activity_at":
|
||||
return workspace.activityAt ? Date.parse(workspace.activityAt) : null;
|
||||
case "name":
|
||||
return workspace.name.toLocaleLowerCase();
|
||||
case "project_id":
|
||||
return workspace.projectId.toLocaleLowerCase();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
private decodeAgentCursor(token: string, sort: SortSpec<FetchAgentsRequestSort["key"]>[]) {
|
||||
try {
|
||||
return this.agentsPager.decode(token, sort);
|
||||
} catch (error) {
|
||||
if (error instanceof CursorError) {
|
||||
throw new SessionRequestError("invalid_cursor", error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private decodeWorkspaceCursor(
|
||||
token: string,
|
||||
sort: SortSpec<FetchWorkspacesRequestSort["key"]>[],
|
||||
) {
|
||||
try {
|
||||
return this.workspacesPager.decode(token, sort);
|
||||
} catch (error) {
|
||||
if (error instanceof CursorError) {
|
||||
throw new SessionRequestError("invalid_cursor", error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private deriveWorkspaceStateBucket(agent: AgentSnapshotPayload): WorkspaceStateBucket {
|
||||
const pendingPermissionCount = agent.pendingPermissions?.length ?? 0;
|
||||
if (pendingPermissionCount > 0 || agent.attentionReason === "permission") {
|
||||
@@ -6332,102 +6262,6 @@ export class Session {
|
||||
);
|
||||
}
|
||||
|
||||
private normalizeFetchWorkspacesSort(
|
||||
sort: FetchWorkspacesRequestSort[] | undefined,
|
||||
): FetchWorkspacesRequestSort[] {
|
||||
const fallback: FetchWorkspacesRequestSort[] = [{ key: "activity_at", direction: "desc" }];
|
||||
if (!sort || sort.length === 0) {
|
||||
return fallback;
|
||||
}
|
||||
const deduped: FetchWorkspacesRequestSort[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const entry of sort) {
|
||||
if (seen.has(entry.key)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(entry.key);
|
||||
deduped.push(entry);
|
||||
}
|
||||
return deduped.length > 0 ? deduped : fallback;
|
||||
}
|
||||
|
||||
private getFetchWorkspacesSortValue(
|
||||
workspace: WorkspaceDescriptorPayload,
|
||||
key: FetchWorkspacesRequestSort["key"],
|
||||
): string | number | null {
|
||||
switch (key) {
|
||||
case "status_priority":
|
||||
return this.workspaceStatePriority[workspace.status];
|
||||
case "activity_at":
|
||||
return workspace.activityAt ? Date.parse(workspace.activityAt) : null;
|
||||
case "name":
|
||||
return workspace.name.toLocaleLowerCase();
|
||||
case "project_id":
|
||||
return workspace.projectId.toLocaleLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
private compareFetchWorkspacesEntries(
|
||||
left: WorkspaceDescriptorPayload,
|
||||
right: WorkspaceDescriptorPayload,
|
||||
sort: FetchWorkspacesRequestSort[],
|
||||
): number {
|
||||
for (const spec of sort) {
|
||||
const leftValue = this.getFetchWorkspacesSortValue(left, spec.key);
|
||||
const rightValue = this.getFetchWorkspacesSortValue(right, spec.key);
|
||||
const base = this.compareSortValues(leftValue, rightValue);
|
||||
if (base === 0) {
|
||||
continue;
|
||||
}
|
||||
return spec.direction === "asc" ? base : -base;
|
||||
}
|
||||
return left.id.localeCompare(right.id);
|
||||
}
|
||||
|
||||
private encodeFetchWorkspacesCursor(
|
||||
entry: FetchWorkspacesResponseEntry,
|
||||
sort: FetchWorkspacesRequestSort[],
|
||||
): string {
|
||||
return encodeCursor(
|
||||
entry,
|
||||
sort,
|
||||
(e) => e.id,
|
||||
(e, key) => this.getFetchWorkspacesSortValue(e, key),
|
||||
);
|
||||
}
|
||||
|
||||
private decodeFetchWorkspacesCursor(
|
||||
cursor: string,
|
||||
sort: FetchWorkspacesRequestSort[],
|
||||
): FetchWorkspacesCursor {
|
||||
try {
|
||||
return decodeCursor(cursor, sort, FETCH_WORKSPACES_SORT_KEYS, "fetch_workspaces");
|
||||
} catch (error) {
|
||||
if (error instanceof CursorError) {
|
||||
throw new SessionRequestError("invalid_cursor", error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private compareWorkspaceWithCursor(
|
||||
workspace: WorkspaceDescriptorPayload,
|
||||
cursor: FetchWorkspacesCursor,
|
||||
sort: FetchWorkspacesRequestSort[],
|
||||
): number {
|
||||
for (const spec of sort) {
|
||||
const leftValue = this.getFetchWorkspacesSortValue(workspace, spec.key);
|
||||
const rightValue =
|
||||
cursor.values[spec.key] !== undefined ? (cursor.values[spec.key] ?? null) : null;
|
||||
const base = this.compareSortValues(leftValue, rightValue);
|
||||
if (base === 0) {
|
||||
continue;
|
||||
}
|
||||
return spec.direction === "asc" ? base : -base;
|
||||
}
|
||||
return workspace.id.localeCompare(cursor.id);
|
||||
}
|
||||
|
||||
private matchesWorkspaceFilter(input: {
|
||||
workspace: WorkspaceDescriptorPayload;
|
||||
filter: FetchWorkspacesRequestFilter | undefined;
|
||||
@@ -6467,18 +6301,18 @@ export class Session {
|
||||
pageInfo: FetchWorkspacesResponsePageInfo;
|
||||
}> {
|
||||
const filter = request.filter;
|
||||
const sort = this.normalizeFetchWorkspacesSort(request.sort);
|
||||
const sort = this.workspacesPager.normalizeSort(request.sort);
|
||||
let entries = await this.listWorkspaceDescriptors();
|
||||
const listedCount = entries.length;
|
||||
entries = entries.filter((workspace) => this.matchesWorkspaceFilter({ workspace, filter }));
|
||||
const filteredCount = entries.length;
|
||||
entries.sort((left, right) => this.compareFetchWorkspacesEntries(left, right, sort));
|
||||
entries.sort((left, right) => this.workspacesPager.compare(left, right, sort));
|
||||
|
||||
const cursorToken = request.page?.cursor;
|
||||
if (cursorToken) {
|
||||
const cursor = this.decodeFetchWorkspacesCursor(cursorToken, sort);
|
||||
const cursor = this.decodeWorkspaceCursor(cursorToken, sort);
|
||||
entries = entries.filter(
|
||||
(workspace) => this.compareWorkspaceWithCursor(workspace, cursor, sort) > 0,
|
||||
(workspace) => this.workspacesPager.compareWithCursor(workspace, cursor, sort) > 0,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6487,7 +6321,7 @@ export class Session {
|
||||
const hasMore = entries.length > limit;
|
||||
const nextCursor =
|
||||
hasMore && pagedEntries.length > 0
|
||||
? this.encodeFetchWorkspacesCursor(pagedEntries[pagedEntries.length - 1], sort)
|
||||
? this.workspacesPager.encode(pagedEntries[pagedEntries.length - 1], sort)
|
||||
: null;
|
||||
|
||||
this.sessionLogger.debug(
|
||||
|
||||
Reference in New Issue
Block a user