// Package cache wraps rueidis with the semantic-key convention used by the // analytics service. Keys follow cache::: // so a workspace can be invalidated without scanning unrelated entries. package cache import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "time" "github.com/redis/rueidis" ) type Cache struct { client rueidis.Client } func New(client rueidis.Client) *Cache { return &Cache{client: client} } // Key builds a deterministic cache key for the given (kind, workspace, params). // Params must JSON-serialize stably -- use a struct or a sorted map. func Key(kind, workspaceID string, params any) (string, error) { raw, err := json.Marshal(params) if err != nil { return "", fmt.Errorf("cache key marshal: %w", err) } sum := sha256.Sum256(raw) return fmt.Sprintf("cache:%s:%s:%s", kind, workspaceID, hex.EncodeToString(sum[:16])), nil } // Get returns (value, true) on hit and (nil, false) on miss. Any redis error // is treated as a miss -- the caller falls through to the underlying source. func (c *Cache) Get(ctx context.Context, key string) ([]byte, bool) { res := c.client.Do(ctx, c.client.B().Get().Key(key).Build()) b, err := res.AsBytes() if err != nil { if errors.Is(err, rueidis.Nil) { return nil, false } return nil, false } return b, true } // Set writes the value with a TTL. func (c *Cache) Set(ctx context.Context, key string, value []byte, ttl time.Duration) error { return c.client.Do(ctx, c.client.B().Set().Key(key).Value(rueidis.BinaryString(value)).Ex(ttl).Build()).Error() }