package main import ( "context" "fmt" "io" "net/http" "os" "path/filepath" "strings" "time" ) // Client is an HTTP client for the PokeAPI with disk caching and concurrency limiting. type Client struct { baseURL string httpClient *http.Client cacheDir string sem chan struct{} // concurrency limiter } // NewClient creates a new PokeAPI client. func NewClient(baseURL, cacheDir string, concurrency int) *Client { return &Client{ baseURL: strings.TrimRight(baseURL, "/"), httpClient: &http.Client{ Timeout: 2 * time.Minute, }, cacheDir: cacheDir, sem: make(chan struct{}, concurrency), } } // Get fetches the given endpoint, using disk cache when available. func (c *Client) Get(ctx context.Context, endpoint string) ([]byte, error) { // Check cache first (no semaphore needed for disk reads) safeName := strings.NewReplacer("/", "_", "?", "_").Replace(endpoint) + ".json" cachePath := filepath.Join(c.cacheDir, safeName) if data, err := os.ReadFile(cachePath); err == nil { return data, nil } // Acquire semaphore for HTTP request select { case c.sem <- struct{}{}: defer func() { <-c.sem }() case <-ctx.Done(): return nil, ctx.Err() } url := c.baseURL + "/" + endpoint var data []byte maxRetries := 3 for attempt := range maxRetries { req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, fmt.Errorf("creating request for %s: %w", endpoint, err) } resp, err := c.httpClient.Do(req) if err != nil { if attempt < maxRetries-1 { backoff := time.Duration(1<= 500 { backoff := time.Duration(1<