mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-09 23:10:20 +08:00
feat: enhance cursor-based pagination in API documentation and types
- Added detailed explanations and examples for cursor semantics in `api-types.md`, clarifying the exclusive nature of cursors in pagination. - Updated `CursorPaginationParams` interface to emphasize the cursor's role as an exclusive boundary in responses. - Refactored `BranchMessagesQueryParams` to extend `CursorPaginationParams`, aligning with the new pagination logic. - Modified `MessageService` to implement cursor-based pagination semantics, ensuring accurate message retrieval and response structure. - Enhanced documentation throughout to provide clearer guidance on pagination behavior and usage patterns.
This commit is contained in:
parent
81bb8e7981
commit
7cac5b55f6
@ -88,6 +88,36 @@ The API system supports two pagination modes with composable query parameters.
|
||||
| `SortParams` | `sortBy?`, `sortOrder?` | Sorting (combine as needed) |
|
||||
| `SearchParams` | `search?` | Text search (combine as needed) |
|
||||
|
||||
### Cursor Semantics
|
||||
|
||||
The `cursor` in `CursorPaginationParams` marks an **exclusive boundary** - the cursor item itself is never included in the response.
|
||||
|
||||
**Common patterns:**
|
||||
|
||||
| Pattern | Use Case | Behavior |
|
||||
|---------|----------|----------|
|
||||
| "after cursor" | Forward pagination, new items | Returns items AFTER cursor |
|
||||
| "before cursor" | Backward/historical loading | Returns items BEFORE cursor |
|
||||
|
||||
The specific semantic depends on the API endpoint. For example:
|
||||
- `GET /topics/:id/messages` uses "before cursor" for loading historical messages
|
||||
- Other endpoints may use "after cursor" for forward pagination
|
||||
|
||||
**Example: Loading historical messages**
|
||||
|
||||
```typescript
|
||||
// First request - get most recent messages
|
||||
const res1 = await api.get('/topics/123/messages', { query: { limit: 20 } })
|
||||
// res1: { items: [msg80...msg99], nextCursor: 'msg80-id', activeNodeId: '...' }
|
||||
|
||||
// Load more - get older messages before the cursor
|
||||
const res2 = await api.get('/topics/123/messages', {
|
||||
query: { cursor: res1.nextCursor, limit: 20 }
|
||||
})
|
||||
// res2: { items: [msg60...msg79], nextCursor: 'msg60-id', activeNodeId: '...' }
|
||||
// Note: msg80 is NOT in res2 (cursor is exclusive)
|
||||
```
|
||||
|
||||
### Response Types
|
||||
|
||||
| Type | Fields | Description |
|
||||
|
||||
@ -152,9 +152,18 @@ export interface OffsetPaginationParams {
|
||||
|
||||
/**
|
||||
* Cursor-based pagination parameters (cursor + limit)
|
||||
*
|
||||
* The cursor is typically an opaque reference to a record in the dataset.
|
||||
* The cursor itself is NEVER included in the response - it marks an exclusive boundary.
|
||||
*
|
||||
* Common semantics:
|
||||
* - "after cursor": Returns items AFTER the cursor (forward pagination)
|
||||
* - "before cursor": Returns items BEFORE the cursor (backward/historical pagination)
|
||||
*
|
||||
* The specific semantic depends on the API endpoint. Check endpoint documentation.
|
||||
*/
|
||||
export interface CursorPaginationParams {
|
||||
/** Cursor for next page (undefined for first page) */
|
||||
/** Cursor for pagination boundary (exclusive - cursor item not included in response) */
|
||||
cursor?: string
|
||||
/** Items per page */
|
||||
limit?: number
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
* Includes endpoints for tree visualization and conversation view.
|
||||
*/
|
||||
|
||||
import type { CursorPaginationParams } from '@shared/data/api/apiTypes'
|
||||
import type {
|
||||
BranchMessagesResponse,
|
||||
Message,
|
||||
@ -115,14 +116,16 @@ export interface TreeQueryParams {
|
||||
|
||||
/**
|
||||
* Query parameters for GET /topics/:id/messages
|
||||
*
|
||||
* Uses "before cursor" semantics for loading historical messages:
|
||||
* - First request (no cursor): Returns the most recent `limit` messages
|
||||
* - Subsequent requests: Pass `nextCursor` from previous response as `cursor`
|
||||
* to load older messages towards root
|
||||
* - The cursor message itself is NOT included in the response
|
||||
*/
|
||||
export interface BranchMessagesQueryParams {
|
||||
export interface BranchMessagesQueryParams extends CursorPaginationParams {
|
||||
/** End node ID (defaults to topic.activeNodeId) */
|
||||
nodeId?: string
|
||||
/** Pagination cursor: return messages before this node */
|
||||
beforeNodeId?: string
|
||||
/** Number of messages to return */
|
||||
limit?: number
|
||||
/** Whether to include siblingsGroup in response */
|
||||
includeSiblings?: boolean
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import type { CursorPaginationResponse } from '@shared/data/api/apiTypes'
|
||||
/**
|
||||
* Message Statistics - combines token usage and performance metrics
|
||||
* Replaces the separate `usage` and `metrics` fields
|
||||
@ -474,9 +475,7 @@ export interface BranchMessage {
|
||||
/**
|
||||
* Branch messages response structure
|
||||
*/
|
||||
export interface BranchMessagesResponse {
|
||||
/** Messages in root-to-leaf order */
|
||||
messages: BranchMessage[]
|
||||
export interface BranchMessagesResponse extends CursorPaginationResponse<BranchMessage> {
|
||||
/** Current active node ID */
|
||||
activeNodeId: string | null
|
||||
}
|
||||
|
||||
@ -45,7 +45,7 @@ export const messageHandlers: {
|
||||
const q = (query || {}) as BranchMessagesQueryParams
|
||||
return await messageService.getBranchMessages(params.topicId, {
|
||||
nodeId: q.nodeId,
|
||||
beforeNodeId: q.beforeNodeId,
|
||||
cursor: q.cursor,
|
||||
limit: q.limit,
|
||||
includeSiblings: q.includeSiblings
|
||||
})
|
||||
|
||||
@ -315,13 +315,23 @@ export class MessageService {
|
||||
* Optimized implementation using recursive CTE to fetch only the path
|
||||
* from nodeId to root, avoiding loading all messages for large topics.
|
||||
* Siblings are batch-queried in a single additional query.
|
||||
*
|
||||
* Uses "before cursor" pagination semantics:
|
||||
* - cursor: Message ID marking the pagination boundary (exclusive)
|
||||
* - Returns messages BEFORE the cursor (towards root)
|
||||
* - The cursor message itself is NOT included
|
||||
* - nextCursor points to the oldest message in current batch
|
||||
*
|
||||
* Example flow:
|
||||
* 1. First request (no cursor) → returns msg80-99, nextCursor=msg80.id
|
||||
* 2. Second request (cursor=msg80.id) → returns msg60-79, nextCursor=msg60.id
|
||||
*/
|
||||
async getBranchMessages(
|
||||
topicId: string,
|
||||
options: { nodeId?: string; beforeNodeId?: string; limit?: number; includeSiblings?: boolean } = {}
|
||||
options: { nodeId?: string; cursor?: string; limit?: number; includeSiblings?: boolean } = {}
|
||||
): Promise<BranchMessagesResponse> {
|
||||
const db = dbService.getDb()
|
||||
const { limit = DEFAULT_LIMIT, includeSiblings = true } = options
|
||||
const { cursor, limit = DEFAULT_LIMIT, includeSiblings = true } = options
|
||||
|
||||
// Get topic
|
||||
const [topic] = await db.select().from(topicTable).where(eq(topicTable.id, topicId)).limit(1)
|
||||
@ -334,7 +344,7 @@ export class MessageService {
|
||||
|
||||
// Return empty if no active node
|
||||
if (!nodeId) {
|
||||
return { messages: [], activeNodeId: null }
|
||||
return { items: [], nextCursor: undefined, activeNodeId: null }
|
||||
}
|
||||
|
||||
// Use recursive CTE to get path from nodeId to root (single query)
|
||||
@ -359,19 +369,22 @@ export class MessageService {
|
||||
let startIndex = 0
|
||||
let endIndex = fullPath.length
|
||||
|
||||
if (options.beforeNodeId) {
|
||||
const beforeIndex = fullPath.findIndex((m) => m.id === options.beforeNodeId)
|
||||
if (beforeIndex === -1) {
|
||||
throw DataApiErrorFactory.notFound('Message', options.beforeNodeId)
|
||||
if (cursor) {
|
||||
const cursorIndex = fullPath.findIndex((m) => m.id === cursor)
|
||||
if (cursorIndex === -1) {
|
||||
throw DataApiErrorFactory.notFound('Message (cursor)', cursor)
|
||||
}
|
||||
startIndex = Math.max(0, beforeIndex - limit)
|
||||
endIndex = beforeIndex
|
||||
startIndex = Math.max(0, cursorIndex - limit)
|
||||
endIndex = cursorIndex
|
||||
} else {
|
||||
startIndex = Math.max(0, fullPath.length - limit)
|
||||
}
|
||||
|
||||
const paginatedPath = fullPath.slice(startIndex, endIndex)
|
||||
|
||||
// Calculate nextCursor: if there are more historical messages
|
||||
const nextCursor = startIndex > 0 ? fullPath[startIndex].id : undefined
|
||||
|
||||
// Build result with optional siblings
|
||||
const result: BranchMessage[] = []
|
||||
|
||||
@ -435,7 +448,8 @@ export class MessageService {
|
||||
}
|
||||
|
||||
return {
|
||||
messages: result,
|
||||
items: result,
|
||||
nextCursor,
|
||||
activeNodeId: topic.activeNodeId
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user