Files
shiftcraft/.claude/skills/pocketbase-best-practices/rules/sdk-auto-cancellation.md
2026-04-17 23:26:01 +00:00

4.3 KiB

title, impact, impactDescription, tags
title impact impactDescription tags
Understand and Control Auto-Cancellation MEDIUM Prevents race conditions, improves UX for search/typeahead sdk, cancellation, requests, performance

Understand and Control Auto-Cancellation

The SDK automatically cancels duplicate pending requests. This prevents race conditions but requires understanding for proper use in concurrent scenarios.

Incorrect (confused by auto-cancellation):

// These requests will interfere with each other!
async function loadDashboard() {
  // Only the last one executes, others cancelled
  const posts = pb.collection('posts').getList(1, 20);
  const users = pb.collection('posts').getList(1, 10);  // Different params but same path
  const comments = pb.collection('posts').getList(1, 5);

  // posts and users are cancelled, only comments executes
  return Promise.all([posts, users, comments]);  // First two fail!
}

// Realtime combined with polling causes cancellation
pb.collection('posts').subscribe('*', callback);
setInterval(() => {
  pb.collection('posts').getList();  // May cancel realtime!
}, 5000);

Correct (controlling auto-cancellation):

// Disable auto-cancellation for parallel requests
async function loadDashboard() {
  const [posts, users, recent] = await Promise.all([
    pb.collection('posts').getList(1, 20, { requestKey: null }),
    pb.collection('users').getList(1, 10, { requestKey: null }),
    pb.collection('posts').getList(1, 5, { requestKey: 'recent' })
  ]);
  // All requests complete independently
  return { posts, users, recent };
}

// Use unique request keys for different purposes
async function searchPosts(query) {
  return pb.collection('posts').getList(1, 20, {
    filter: pb.filter('title ~ {:q}', { q: query }),
    requestKey: 'post-search'  // Cancels previous searches only
  });
}

async function loadPostDetails(postId) {
  return pb.collection('posts').getOne(postId, {
    requestKey: `post-${postId}`  // Unique per post
  });
}

// Typeahead search - auto-cancellation is helpful here
async function typeaheadSearch(query) {
  // Previous search automatically cancelled when user types more
  return pb.collection('products').getList(1, 10, {
    filter: pb.filter('name ~ {:q}', { q: query })
    // No requestKey = uses default (path-based), previous cancelled
  });
}

// Globally disable auto-cancellation (use carefully)
pb.autoCancellation(false);

// Now all requests are independent
await Promise.all([
  pb.collection('posts').getList(1, 20),
  pb.collection('posts').getList(1, 10),
  pb.collection('posts').getList(1, 5)
]);

// Re-enable
pb.autoCancellation(true);

Manual cancellation:

// Cancel all pending requests
pb.cancelAllRequests();

// Cancel specific request by key
pb.cancelRequest('post-search');

// Example: Cancel on component unmount
function PostList() {
  useEffect(() => {
    loadPosts();

    return () => {
      // Cleanup: cancel pending requests
      pb.cancelRequest('post-list');
    };
  }, []);

  async function loadPosts() {
    const result = await pb.collection('posts').getList(1, 20, {
      requestKey: 'post-list'
    });
    setPosts(result.items);
  }
}

// Handle cancellation in catch
async function fetchWithCancellation() {
  try {
    return await pb.collection('posts').getList();
  } catch (error) {
    if (error.isAbort) {
      // Request was cancelled - this is expected
      console.log('Request cancelled');
      return null;
    }
    throw error;
  }
}

When to use each approach:

Scenario Approach
Search/typeahead Default (let it cancel)
Parallel data loading requestKey: null
Grouped requests Custom requestKey
Component cleanup cancelRequest(key)
Testing/debugging autoCancellation(false)
OAuth2 flow cancel cancelRequest(requestKey) — properly rejects the authWithOAuth2() Promise (JS SDK v0.26.8+)

Note (JS SDK v0.26.8): Calling pb.cancelRequest(requestKey) while authWithOAuth2() is waiting now properly rejects the returned Promise. In earlier versions the manual cancellation did not account for the waiting realtime subscription, so the Promise could hang indefinitely.

Reference: PocketBase Auto-Cancellation