Skip to content

Conversation

@tnederlof
Copy link
Collaborator

Overview

Implements request deduplication for the API client to eliminate duplicate concurrent requests, addressing issue #80.

Changes

  • api/client.ts: Added promise-sharing deduplication mechanism for GET requests

    • Implements 2-second deduplication window (matching SWR default)
    • Normalizes URLs with sorted query parameters for consistent cache keys
    • Automatic cleanup on promise settlement with failsafe timeout
    • Zero dependencies, TypeScript-safe implementation
  • GlobalState.tsx: Updated to use api.getJSON instead of raw fetch

  • Product.tsx: Updated to use api.getJSON instead of raw fetch

  • E2E Tests: Added comprehensive tests to verify no duplicate concurrent requests

Implementation Details

Follows OSS best practices from TanStack Query, SWR, and Apollo Client:

  1. Store in-flight promises in a Map keyed by normalized GET URL
  2. Return same promise for concurrent identical requests
  3. Clean up promises after completion or 2s timeout
  4. Only deduplicate GET requests (mutations unaffected)

Testing

  • ✅ All new E2E tests pass (3 deduplication scenarios)
  • ✅ All existing E2E tests pass (37 total)
  • ✅ Backend tests pass (51 tests)
  • ✅ TypeScript build passes with strict mode
  • ✅ ESLint and Prettier checks pass

Verification

Console logs from E2E tests confirm:

  • /products?include_delivery_summary=true: 1 request (was 2)
  • /api/categories: 1 request (was 2)
  • /products/{id}: 1 request (was 2)

Fixes #80

- Add promise-sharing deduplication for GET requests in api/client.ts
- Implement 2-second deduplication window with automatic cleanup
- Normalize URLs with sorted query params for consistent cache keys
- Update GlobalState.tsx to use api.getJSON for products endpoint
- Update Product.tsx to use api.getJSON for product details endpoint
- Add comprehensive E2E tests to verify no duplicate concurrent requests

Fixes #80

Amp-Thread-ID: https://ampcode.com/threads/T-3c3a93a0-1e22-4e7c-9093-f155c6b44368
Co-authored-by: Amp <[email protected]>
@tnederlof tnederlof self-assigned this Oct 24, 2025
Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

This PR refactors the API client to add request deduplication for GET requests and consolidates fetch logic across the application. The changes replace manual fetch calls in GlobalState.tsx and Product.tsx with the centralized api.getJSON method, removing hardcoded API URLs.

Key changes:

  • Implements GET request deduplication with a 2-second window using an in-flight request cache
  • Adds query parameter normalization to ensure consistent cache keys
  • Exposes a public getJSON method for typed API calls
  • Refactors existing fetch calls to use the new centralized client

Additional considerations:
The deduplication implementation is generally sound with proper cleanup handling via both finally blocks and failsafe timers. However, the type safety concern with Promise<unknown> storage and the potential race condition should be addressed to ensure robustness. The functionality itself should work correctly for typical use cases where the same endpoint consistently returns the same data shape.

View this review on Amp


const DEDUPE_INTERVAL_MS = 2000

type InFlightEntry = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type safety issue: Storing Promise<unknown> but casting to Promise<T> when reusing can lead to type mismatches. If two different parts of the code request the same endpoint with different expected return types within the deduplication window, the second caller will receive incorrectly typed data from the first request's promise, potentially causing runtime errors.

}

// Shared JSON request with GET-deduping
private async request<T>(endpoint: string, options: globalThis.RequestInit = {}): Promise<T> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary .toString() call. The options.method is already a string type (from RequestInit), and the fallback 'GET' is also a string. This can be simplified to const method = (options.method || 'GET').toUpperCase()

Suggested change
private async request<T>(endpoint: string, options: globalThis.RequestInit = {}): Promise<T> {
const method = (options.method || 'GET').toUpperCase()

if (method === 'GET') {
const key = this.buildKey(endpoint, method)
const now = Date.now()
const existing = this.inFlight.get(key)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential race condition: If two identical GET requests are initiated simultaneously before either is added to the inFlight map, both will create separate fetch calls, defeating the deduplication mechanism. Consider checking and setting the map entry atomically, or document this as an acceptable limitation for client-side deduplication.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Deduplicate concurrent API requests from the front end

2 participants