Savvi Studio

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:

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...of loops
  • 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');
  });
});

Next steps: See Usage Patterns for practical examples.