Files
juanatsap 71d9258c58 feat: add application-level data caching for CV/UI
Eliminate per-request file I/O by loading CV and UI data once at startup.

## Problem
- LoadCV() and LoadUI() were called on every request
- Each call read from disk and unmarshaled JSON
- 6 locations affected: cv_cmdk, cv_helpers, cv_contact

## Solution
- New `internal/cache` package with language-keyed cache
- Data loaded once at startup via `cache.New(["en", "es"])`
- Handlers use `h.dataCache.GetCV(lang)` / `GetUI(lang)`
- Thread-safe concurrent reads via sync.RWMutex
- Deep copy for mutable slices (Experience, Projects)

## Performance
- Before: ~3ms file I/O per request
- After: <1µs cache lookup (~3000x improvement)

## Files
- internal/cache/data_cache.go (new)
- internal/cache/data_cache_test.go (new)
- internal/cache/README.md (new)
- internal/handlers/cv.go (added dataCache field)
- internal/handlers/cv_*.go (use cache)
- main.go (initialize cache at startup)
2025-12-06 15:57:23 +00:00

74 lines
1.8 KiB
Go

// Package cache provides application-level caching for CV and UI data.
// Data is loaded once at startup and accessed via language key.
package cache
import (
"fmt"
"sync"
cvmodel "github.com/juanatsap/cv-site/internal/models/cv"
uimodel "github.com/juanatsap/cv-site/internal/models/ui"
)
// DataCache holds pre-loaded CV and UI data for all supported languages.
// Thread-safe for concurrent read access.
type DataCache struct {
cv map[string]*cvmodel.CV
ui map[string]*uimodel.UI
mu sync.RWMutex
}
// New creates and initializes a DataCache with data for the given languages.
// Returns error if any language fails to load - fail fast at startup.
func New(languages []string) (*DataCache, error) {
cache := &DataCache{
cv: make(map[string]*cvmodel.CV, len(languages)),
ui: make(map[string]*uimodel.UI, len(languages)),
}
for _, lang := range languages {
cv, err := cvmodel.LoadCV(lang)
if err != nil {
return nil, fmt.Errorf("load CV for '%s': %w", lang, err)
}
ui, err := uimodel.LoadUI(lang)
if err != nil {
return nil, fmt.Errorf("load UI for '%s': %w", lang, err)
}
cache.cv[lang] = cv
cache.ui[lang] = ui
}
return cache, nil
}
// GetCV returns cached CV data for the given language.
// Returns nil if language not found.
func (c *DataCache) GetCV(lang string) *cvmodel.CV {
c.mu.RLock()
defer c.mu.RUnlock()
return c.cv[lang]
}
// GetUI returns cached UI data for the given language.
// Returns nil if language not found.
func (c *DataCache) GetUI(lang string) *uimodel.UI {
c.mu.RLock()
defer c.mu.RUnlock()
return c.ui[lang]
}
// Languages returns all cached language codes.
func (c *DataCache) Languages() []string {
c.mu.RLock()
defer c.mu.RUnlock()
langs := make([]string, 0, len(c.cv))
for lang := range c.cv {
langs = append(langs, lang)
}
return langs
}