Files
go-jdenticon/internal/engine/singleflight.go
Kevin McIntyre d9e84812ff Initial release: Go Jdenticon library v0.1.0
- Core library with SVG and PNG generation
- CLI tool with generate and batch commands
- Cross-platform path handling for Windows compatibility
- Comprehensive test suite with integration tests
2026-01-03 23:41:48 -05:00

104 lines
2.7 KiB
Go

package engine
import (
"context"
"fmt"
"github.com/ungluedlabs/go-jdenticon/internal/util"
)
// Generate creates an identicon with the specified hash and size
// This method includes caching and singleflight to prevent duplicate work
func (g *Generator) Generate(ctx context.Context, hash string, size float64) (*Icon, error) {
// Basic validation
if hash == "" {
return nil, fmt.Errorf("jdenticon: engine: generation failed: hash cannot be empty")
}
if size <= 0 {
return nil, fmt.Errorf("jdenticon: engine: generation failed: invalid size: %f", size)
}
// Check icon size limits
if g.maxIconSize > 0 && int(size) > g.maxIconSize {
return nil, fmt.Errorf("jdenticon: engine: generation failed: icon size %d exceeds maximum allowed size %d", int(size), g.maxIconSize)
}
// Validate hash format
if !util.IsValidHash(hash) {
return nil, fmt.Errorf("jdenticon: engine: generation failed: invalid hash format: %s", hash)
}
// Check for context cancellation before proceeding
if err := ctx.Err(); err != nil {
return nil, err
}
// Generate cache key
key := g.cacheKey(hash, size)
// Check cache first (with read lock)
g.mu.RLock()
if cached, ok := g.cache.Get(key); ok {
g.mu.RUnlock()
g.metrics.recordHit()
return cached, nil
}
g.mu.RUnlock()
// Use singleflight to prevent multiple concurrent generations for the same key
result, err, _ := g.sf.Do(key, func() (interface{}, error) {
// Check cache again inside singleflight (another goroutine might have populated it)
g.mu.RLock()
if cached, ok := g.cache.Get(key); ok {
g.mu.RUnlock()
g.metrics.recordHit()
return cached, nil
}
g.mu.RUnlock()
// Generate the icon
icon, err := g.generateIcon(ctx, hash, size)
if err != nil {
return nil, err
}
// Store in cache (with write lock)
g.mu.Lock()
g.cache.Add(key, icon)
g.mu.Unlock()
g.metrics.recordMiss()
return icon, nil
})
if err != nil {
return nil, err
}
return result.(*Icon), nil
}
// GenerateWithoutCache creates an identicon without using cache
// This method is useful for testing or when caching is not desired
func (g *Generator) GenerateWithoutCache(ctx context.Context, hash string, size float64) (*Icon, error) {
// Basic validation
if hash == "" {
return nil, fmt.Errorf("jdenticon: engine: generation failed: hash cannot be empty")
}
if size <= 0 {
return nil, fmt.Errorf("jdenticon: engine: generation failed: invalid size: %f", size)
}
// Validate hash format
if !util.IsValidHash(hash) {
return nil, fmt.Errorf("jdenticon: engine: generation failed: invalid hash format: %s", hash)
}
// Check for context cancellation
if err := ctx.Err(); err != nil {
return nil, err
}
return g.generateIcon(ctx, hash, size)
}