hatch-surf/internal/face/cache.go
Chris Anderson 4bf37faf4f Add face service package for AWS Rekognition integration
- Client interface and MockClient for testing
- Service layer with IndexPhoto and SearchFaces methods
- CacheService interface and MemoryCache implementation
- Configuration with environment variable support
- Error types and user-facing messages
- Unit tests for service layer

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-25 21:11:45 +02:00

154 lines
3.4 KiB
Go

package face
import (
"context"
"encoding/json"
"sync"
"time"
)
// CacheService defines the interface for caching face search results
type CacheService interface {
// Get retrieves a cached value by key
Get(ctx context.Context, key string) (string, error)
// Set stores a value with expiration
Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error
// Delete removes cached values
Delete(ctx context.Context, keys ...string) error
// GetJSON retrieves and unmarshals a cached JSON value
GetJSON(ctx context.Context, key string, dest interface{}) error
// SetJSON marshals and stores a JSON value
SetJSON(ctx context.Context, key string, value interface{}, expiration time.Duration) error
}
// MemoryCache is an in-memory cache implementation for testing and development
type MemoryCache struct {
mu sync.RWMutex
entries map[string]*cacheEntry
}
type cacheEntry struct {
value string
expiresAt time.Time
}
// NewMemoryCache creates a new in-memory cache
func NewMemoryCache() *MemoryCache {
return &MemoryCache{
entries: make(map[string]*cacheEntry),
}
}
// Get retrieves a value from the cache
func (c *MemoryCache) Get(ctx context.Context, key string) (string, error) {
c.mu.RLock()
defer c.mu.RUnlock()
entry, ok := c.entries[key]
if !ok {
return "", nil
}
if time.Now().After(entry.expiresAt) {
return "", nil
}
return entry.value, nil
}
// Set stores a value in the cache
func (c *MemoryCache) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error {
c.mu.Lock()
defer c.mu.Unlock()
var strValue string
switch v := value.(type) {
case string:
strValue = v
default:
bytes, err := json.Marshal(v)
if err != nil {
return err
}
strValue = string(bytes)
}
c.entries[key] = &cacheEntry{
value: strValue,
expiresAt: time.Now().Add(expiration),
}
return nil
}
// Delete removes values from the cache
func (c *MemoryCache) Delete(ctx context.Context, keys ...string) error {
c.mu.Lock()
defer c.mu.Unlock()
for _, key := range keys {
delete(c.entries, key)
}
return nil
}
// GetJSON retrieves and unmarshals a cached JSON value
func (c *MemoryCache) GetJSON(ctx context.Context, key string, dest interface{}) error {
value, err := c.Get(ctx, key)
if err != nil {
return err
}
if value == "" {
return nil
}
return json.Unmarshal([]byte(value), dest)
}
// SetJSON marshals and stores a JSON value
func (c *MemoryCache) SetJSON(ctx context.Context, key string, value interface{}, expiration time.Duration) error {
return c.Set(ctx, key, value, expiration)
}
// Cleanup removes expired entries (call periodically)
func (c *MemoryCache) Cleanup() {
c.mu.Lock()
defer c.mu.Unlock()
now := time.Now()
for key, entry := range c.entries {
if now.After(entry.expiresAt) {
delete(c.entries, key)
}
}
}
// CacheKeys returns common cache key patterns
type CacheKeys struct{}
// Search returns the cache key for search results
func (CacheKeys) Search(spaceID string, imageHash string) string {
return "search:" + spaceID + ":" + imageHash
}
// Photo returns the cache key for photo metadata
func (CacheKeys) Photo(photoID string) string {
return "photo:" + photoID
}
// Face returns the cache key for face embeddings
func (CacheKeys) Face(faceID string) string {
return "face:" + faceID
}
// Indexed returns the cache key for indexed photos
func (CacheKeys) Indexed(photoID string) string {
return "indexed:" + photoID
}