package cache import ( "context" "encoding/json" "math/rand" "sync" "time" "golang.org/x/sync/singleflight" ) const ( defaultJitterRatio = 0.1 ) var ( jitterMu sync.Mutex jitterRand = rand.New(rand.NewSource(time.Now().UnixNano())) ) type payloadEnvelope[T any] struct { Empty bool `json:"empty,omitempty"` Value T `json:"value,omitempty"` } func LoadJSON[T any]( ctx context.Context, c Cache, group *singleflight.Group, key string, ttl time.Duration, loader func(context.Context) (T, error), ) (T, error) { value, _, err := loadJSONInternal(ctx, c, group, key, ttl, 0, func(loadCtx context.Context) (T, bool, error) { value, err := loader(loadCtx) return value, true, err }) return value, err } func LoadJSONWithEmpty[T any]( ctx context.Context, c Cache, group *singleflight.Group, key string, ttl time.Duration, emptyTTL time.Duration, loader func(context.Context) (T, bool, error), ) (T, bool, error) { return loadJSONInternal(ctx, c, group, key, ttl, emptyTTL, loader) } func loadJSONInternal[T any]( ctx context.Context, c Cache, group *singleflight.Group, key string, ttl time.Duration, emptyTTL time.Duration, loader func(context.Context) (T, bool, error), ) (T, bool, error) { var zero T if c == nil { return loader(ctx) } if raw, err := c.Get(ctx, key); err == nil { var envelope payloadEnvelope[T] if unmarshalErr := json.Unmarshal(raw, &envelope); unmarshalErr == nil { if envelope.Empty { return zero, false, nil } return envelope.Value, true, nil } } result, err, _ := group.Do(key, func() (interface{}, error) { value, found, loadErr := loader(ctx) if loadErr != nil { return nil, loadErr } envelope := payloadEnvelope[T]{ Empty: !found, Value: value, } if raw, marshalErr := json.Marshal(envelope); marshalErr == nil { cacheTTL := ttl if !found { cacheTTL = emptyTTL } if cacheTTL > 0 { _ = c.Set(ctx, key, raw, ApplyJitter(cacheTTL)) } } return envelope, nil }) if err != nil { return zero, false, err } envelope, ok := result.(payloadEnvelope[T]) if !ok { return zero, false, nil } if envelope.Empty { return zero, false, nil } return envelope.Value, true, nil } func ApplyJitter(ttl time.Duration) time.Duration { if ttl <= 0 { return ttl } maxJitter := int64(float64(ttl) * defaultJitterRatio) if maxJitter <= 0 { return ttl } jitterMu.Lock() defer jitterMu.Unlock() return ttl + time.Duration(jitterRand.Int63n(maxJitter+1)) }