-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Open
Description
Current Behavior
The Planet feature (community project sharing) makes API calls to the server (ServerInterface.js) without any client-side rate limiting, request throttling, or caching strategy. Currently:
- Every scroll to 75% of page height triggers loadMoreProjects() without throttling
- Search input triggers API calls with only a 250ms debounce
- No caching of downloaded projects in IndexedDB for offline access
- No retry logic with exponential backoff on failures
- Projects are re-downloaded when revisiting the Planet page
Relevant files:
- planet/js/ServerInterface.js - All API calls go through request() method without rate limiting
- planet/js/GlobalPlanet.js - loadMoreProjects() and search() have no request throttling
- js/planetInterface.js - Entry point for Planet functionality
Desired Behavior
Implement a robust client-side API management layer:
-
Request Throttling
- Add minimum delay between consecutive API calls (e.g., 500ms)
- Prevent duplicate concurrent requests to the same endpoint
- Queue requests when rate limit is reached
-
Local Caching with IndexedDB
- Cache project metadata and thumbnails locally
- Cache full project data for recently viewed projects
- Implement cache expiration (e.g., 24 hours for metadata)
- Enable browsing previously viewed projects offline
-
Retry Logic with Exponential Backoff
- Retry failed requests with increasing delays (1s, 2s, 4s...)
- Maximum retry limit (e.g., 3 attempts)
- Show user-friendly error after all retries fail
-
Request Deduplication
- Prevent the same project from being downloaded multiple times simultaneously
- Coalesce multiple requests for the same resource
Screenshots / Mockups
N/A - This is a backend/infrastructure improvement
Implementation
Proposed approach:
- Create a new
RequestManagerclass inplanet/js/:
class RequestManager {
constructor(options = {}) {
this.minDelay = options.minDelay || 500;
this.maxRetries = options.maxRetries || 3;
this.pendingRequests = new Map();
this.lastRequestTime = 0;
}
async throttledRequest(key, requestFn) {
// Check for pending duplicate request
if (this.pendingRequests.has(key)) {
return this.pendingRequests.get(key);
}
// Apply throttling
const now = Date.now();
const timeSinceLastRequest = now - this.lastRequestTime;
if (timeSinceLastRequest < this.minDelay) {
await new Promise(r => setTimeout(r, this.minDelay - timeSinceLastRequest));
}
// Execute with retry logic
const promise = this.executeWithRetry(requestFn);
this.pendingRequests.set(key, promise);
this.lastRequestTime = Date.now();
try {
return await promise;
} finally {
this.pendingRequests.delete(key);
}
}
async executeWithRetry(requestFn, attempt = 0) {
try {
return await requestFn();
} catch (error) {
if (attempt < this.maxRetries) {
const delay = Math.pow(2, attempt) * 1000;
await new Promise(r => setTimeout(r, delay));
return this.executeWithRetry(requestFn, attempt + 1);
}
throw error;
}
}
}Metadata
Metadata
Assignees
Labels
No labels