Cursor Best Practices
Purpose: Guidelines for working with cursors
Last Updated: 2024-11-26
Overview
This document provides guidelines and anti-patterns for working with cursors.
Source Code Reference
For cursor implementation details, see:
src/lib/cursor/abstract-cursor.ts- Base cursor classsrc/lib/cursor/types.ts- Cursor interfaces and typessrc/lib/db/api/QueryCursor.ts- PostgreSQL cursor implementation
Design Guidelines
✅ Do
Keep cursors focused on pagination
- Cursors handle position tracking only
- Use streams for transformations
- Separate concerns cleanly
Implement AsyncIterable properly
- Support
for await...ofloops - Handle cleanup in async iterator
- Follow protocol correctly
Manage token state carefully
- Validate tokens before use
- Handle expired/invalid tokens
- Include version information
Handle edge cases
- Empty result sets
- End of data
- Error conditions
- Concurrent modifications
Document token format
- Specify required fields
- Document optional fields
- Version token structure
- Provide examples
❌ Don't
Mix transformation logic
- Keep transformations in streams
- Don't filter/map in cursors
- Maintain separation of concerns
Hold unnecessary state
- Only store cursor position
- Don't cache full results
- Release resources promptly
Ignore error handling
- Always handle fetch errors
- Validate cursor tokens
- Provide meaningful error messages
Skip token validation
- Check token version
- Validate required fields
- Handle corrupted tokens
Make cursors data-source aware beyond necessity
- Keep implementation details private
- Expose clean interface
- Minimize coupling
Performance Considerations
Memory Usage
Good: Stream items, don't collect all
for await (const item of cursor) {
await processItem(item); // Process one at a time
}
Bad: Load everything into memory
const allItems = [];
for await (const item of cursor) {
allItems.push(item);
}
// Now allItems holds everything in memory
Batch Size
Optimal: Balance between roundtrips and memory
const cursor = new MyCursor({
batchSize: 100 // Reasonable default
});
Too small: Many roundtrips
const cursor = new MyCursor({
batchSize: 10 // Too many network calls
});
Too large: High memory usage
const cursor = new MyCursor({
batchSize: 10000 // May cause memory issues
});
Common Pitfalls
Pitfall 1: Mixing Iteration Styles
// ❌ Bad: Mixing for-await with manual fetch
const cursor = new MyCursor(config);
for await (const item of cursor) {
// ...
}
// Don't do this after for-await
const more = await cursor.fetch(); // May not work as expected
// ✅ Good: Choose one style
const cursor = new MyCursor(config);
while (await cursor.hasMore()) {
const batch = await cursor.fetch();
await processBatch(batch);
}
Pitfall 2: Not Checking hasMore()
// ❌ Bad: Fetching without checking
const cursor = new MyCursor(config);
const items = await cursor.fetch(); // May return empty array
// ✅ Good: Check before fetching
const cursor = new MyCursor(config);
if (await cursor.hasMore()) {
const items = await cursor.fetch();
}
Pitfall 3: Ignoring Cursor State
// ❌ Bad: Creating new cursor each time
function fetchNextBatch() {
const cursor = new MyCursor(config); // Starts from beginning!
return cursor.fetch();
}
// ✅ Good: Reuse cursor instance
const cursor = new MyCursor(config);
function fetchNextBatch() {
return cursor.fetch(); // Continues from current position
}
Testing Best Practices
Test Cursor Behavior
describe('Cursor', () => {
it('fetches in batches', async () => {
const cursor = new MyCursor(config);
const batch1 = await cursor.fetch(10);
const batch2 = await cursor.fetch(10);
expect(batch1).toHaveLength(10);
expect(batch2).toHaveLength(10);
expect(batch1[0]).not.toEqual(batch2[0]);
});
it('handles empty results', async () => {
const cursor = new MyCursor(emptyConfig);
expect(await cursor.hasMore()).toBe(false);
expect(await cursor.fetch()).toEqual([]);
});
it('maintains token state', async () => {
const cursor = new MyCursor(config);
await cursor.fetch(10);
const token = cursor.getToken();
expect(token).toBeDefined();
expect(token).toContain('lastId');
});
});
Related Documentation
- Implementation Guide - Creating custom cursors
- Usage Patterns - Common usage examples
- Cursors Architecture - Overview and concepts
Next steps: See Usage Patterns for practical examples.