@@ -6,75 +6,45 @@ import (
66 "encoding/hex"
77 "encoding/json"
88 "fmt"
9- "sort"
109)
1110
1211// Cache provides an abstract interface for caching content to local disk.
1312// Implementations should handle storing and retrieving cached components
1413// using fingerprints for cache invalidation.
14+ // Cache operations fail open: if caching fails, the compute function is still called.
1515type Cache [T any ] interface {
1616 // GetOrCompute retrieves cached content for the given fingerprint, or computes it using the provided function.
1717 // If the content is found in cache, it is returned directly.
1818 // If not found, the compute function is called, its result is cached, and then returned.
1919 // The fingerprint can be any struct that will be serialized deterministically for cache key generation.
20- // Returns an error if the cache operation or compute function fails.
20+ // Cache failures do not block computation - if caching fails, compute is called anyway.
21+ // Returns an error only if the compute function fails.
2122 GetOrCompute (ctx context.Context , fingerprint any , compute func (ctx context.Context ) (T , error )) (T , error )
2223}
2324
2425// fingerprintToHash converts any struct to a deterministic string representation for use as a cache key.
26+ // For structs, json.Marshal uses struct field order, not JSON tag order. To ensure deterministic
27+ // hashing regardless of struct field order, we convert to a map which json.Marshal sorts by key.
2528func fingerprintToHash (fingerprint any ) (string , error ) {
26- // Serialize to JSON with sorted keys for deterministic output
29+ // Marshal to JSON
2730 data , err := json .Marshal (fingerprint )
2831 if err != nil {
2932 return "" , fmt .Errorf ("failed to marshal fingerprint: %w" , err )
3033 }
3134
32- // Parse back to ensure consistent key ordering
33- var obj any
34- if err := json .Unmarshal (data , & obj ); err != nil {
35+ // Unmarshal to map to ensure key ordering
36+ var m map [ string ] any
37+ if err := json .Unmarshal (data , & m ); err != nil {
3538 return "" , fmt .Errorf ("failed to unmarshal fingerprint: %w" , err )
3639 }
3740
38- // Sort keys deterministically
39- normalized := normalizeForFingerprint (obj )
40-
41- // Re-marshal with normalized structure
42- normalizedData , err := json .Marshal (normalized )
41+ // Marshal map (map keys are sorted by json.Marshal)
42+ normalizedData , err := json .Marshal (m )
4343 if err != nil {
4444 return "" , fmt .Errorf ("failed to marshal normalized fingerprint: %w" , err )
4545 }
4646
47- // Hash the result for a consistent, reasonably-sized key
47+ // Hash for consistent, reasonably-sized key
4848 hash := sha256 .Sum256 (normalizedData )
4949 return hex .EncodeToString (hash [:]), nil
5050}
51-
52- // normalizeForFingerprint recursively sorts map keys to ensure deterministic serialization.
53- func normalizeForFingerprint (obj any ) any {
54- switch v := obj .(type ) {
55- case map [string ]any :
56- // Sort keys
57- keys := make ([]string , 0 , len (v ))
58- for k := range v {
59- keys = append (keys , k )
60- }
61- sort .Strings (keys )
62-
63- // Create ordered map
64- result := make (map [string ]any , len (v ))
65- for _ , k := range keys {
66- result [k ] = normalizeForFingerprint (v [k ])
67- }
68- return result
69- case []any :
70- // Normalize each element in the slice
71- result := make ([]any , len (v ))
72- for i , item := range v {
73- result [i ] = normalizeForFingerprint (item )
74- }
75- return result
76- default :
77- // Primitive types are returned as-is
78- return v
79- }
80- }
0 commit comments