Cursor Implementation Guide
Purpose: How to implement custom cursors
Last Updated: 2024-11-26
Overview
This guide shows how to implement custom cursors by extending the abstract cursor pattern.
Source Code Reference
See src/lib/cursor/abstract-cursor.ts for the base implementation.
Implementing a Custom Cursor
Basic Pattern
import { AbstractCursor } from '@/lib/cursor/abstract-cursor';
class MyCursor<T> extends AbstractCursor<T> {
constructor(private config: Config) {
super();
}
protected async fetchBatch(limit: number): Promise<T[]> {
// Implement data source-specific fetching
// Use this.token for position tracking
return await myDataSource.fetch({
after: this.token?.lastId,
limit
});
}
protected updateToken(items: T[]): void {
// Update cursor token based on fetched items
if (items.length > 0) {
const last = items[items.length - 1];
this.token = {
lastId: last.id,
direction: 'forward',
limit: this.batchSize,
timestamp: Date.now(),
version: 1
};
}
}
}
Required Methods
fetchBatch(limit: number)
Purpose: Fetch the next batch of items from your data source.
Implementation:
- Use
this.tokenfor position tracking - Return array of items (empty if no more)
- Should handle errors appropriately
updateToken(items: T[])
Purpose: Update cursor token after fetching items.
Implementation:
- Extract position info from last item
- Set
this.tokenwith updated position - Include metadata (timestamp, version, etc.)
Examples
Example 1: Database Cursor
See src/lib/db/api/QueryCursor.ts for the production PostgreSQL implementation.
Example 2: API Cursor
class ApiCursor<T> extends AbstractCursor<T> {
constructor(
private apiClient: ApiClient,
private endpoint: string
) {
super();
}
protected async fetchBatch(limit: number): Promise<T[]> {
const response = await this.apiClient.get(this.endpoint, {
params: {
limit,
cursor: this.token ? encodeCursor(this.token) : undefined
}
});
return response.data.items;
}
protected updateToken(items: T[]): void {
if (items.length > 0) {
const last = items[items.length - 1];
this.token = {
lastId: last.id,
direction: 'forward',
limit: this.batchSize,
timestamp: Date.now(),
version: 1
};
}
}
}
Best Practices
✅ Do
- Keep cursors focused on pagination only
- Use type-safe token structures
- Handle empty results gracefully
- Implement proper error handling
- Document token format
❌ Don't
- Mix transformation logic into cursors
- Hold unnecessary state
- Ignore error cases
- Skip token validation
- Make cursors data-source aware beyond what's needed
Testing Custom Cursors
describe('MyCursor', () => {
it('fetches items correctly', async () => {
const cursor = new MyCursor(config);
const items = await cursor.fetch(10);
expect(items).toHaveLength(10);
});
it('handles end of data', async () => {
const cursor = new MyCursor(emptyConfig);
expect(await cursor.hasMore()).toBe(false);
});
it('maintains token state', async () => {
const cursor = new MyCursor(config);
await cursor.fetch(10);
const token = cursor.getToken();
expect(token).toBeDefined();
});
});
Related Documentation
- Usage Patterns - How to use cursors
- Best Practices - Guidelines and anti-patterns
- Database Implementation - PostgreSQL example
Next steps: See Usage Patterns for examples of using your cursor.