Documentation
¶
Overview ¶
Package cache provides caching utilities for the foundation layer.
Index ¶
- func AccessFingerprint(scope ...string) string
- func CountGenKey(tenant, resource string) string
- func CountKey(tenant, resource, generation, accessFP, filterHash string) string
- func Decode[T any](data []byte, version int) (val T, ok bool, err error)
- func Encode[T any](value T, version int) ([]byte, error)
- func FilterHash(filter types.Filter) string
- func ReadThrough[T any](ctx context.Context, c interfaces.ByteCache, sf *singleflight.Group, ...) (T, error)
- func SuggestKey(tenant, resource, field, accessFP, prefix string, limit int) string
- type Envelope
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func AccessFingerprint ¶
AccessFingerprint hashes an access SCOPE — the exact resolved-context values the query's authz predicate applies (e.g. the set of accessible group ids plus an access level, or a tenant id) — into a compact fingerprint. It is order-insensitive (the scope is a set: the parts are sorted first), so the same access hashes equally regardless of argument order, and ANY change to the scope rotates the fingerprint — which is how an authz change invalidates cached entries (by key rotation, never reach-in). The caller MUST pass the same values the WHERE clause applies (never raw client input), so the key can never be spoofed narrower than the real access boundary.
func CountGenKey ¶
CountGenKey is the key holding the current generation token for a (tenant, resource)'s count cache. A resource write rotates the token stored here (one Set), coarse-busting the whole bucket in a single O(1) operation. tenant and resource are identifiers with no ':' so the readable form is unambiguous.
func CountKey ¶
CountKey builds the total_count cache key: the "count:" namespace then a digest of (tenant, resource, generation, access_fp, filter_hash). The generation component is what makes a coarse bust O(1): rotating the generation (see CountGenKey) changes every count key for the (tenant, resource) at once, orphaning all prior entries across every access_fp/filter_hash.
func Decode ¶
Decode deserializes bytes into a versioned envelope. Returns (value, true, nil) on success, (zero, false, nil) on version mismatch (stale-schema miss), and (zero, false, err) on corrupt/invalid JSON.
func FilterHash ¶
FilterHash hashes a list-query filter into a stable key component, reusing the query-fingerprint (filter-only — no sort). Two structurally-identical filters hash equally (so a total is reused across page flips for one filter); a different filter hashes differently. The encoding is order-preserving, so two SEMANTICALLY-equal but differently-ordered composite filters hash differently — a cache-hit-rate cost, never a correctness bug (a distinct key is still scoped correctly).
func ReadThrough ¶
func ReadThrough[T any]( ctx context.Context, c interfaces.ByteCache, sf *singleflight.Group, key string, ttl time.Duration, version int, load func() (T, error), ) (T, error)
ReadThrough is the shared read-through (cache-aside) primitive for a value of type T, folding the versioned envelope (Encode/Decode) and single-flight (a stampede of concurrent misses on one key collapses to ONE load) into one reuse point for every SCOPED cache decorator (suggest, count, …). Its shape mirrors the repos-tier caching decorator, which predates this primitive and still holds its own copy of the envelope + stampede logic; consolidating that decorator onto ReadThrough is a future DRY cleanup.
Flow: a version-matching hit returns the decoded value WITHOUT calling load; otherwise a single load (per key, across concurrent callers) runs, its result is encoded and stored with ttl, and returned. A load error propagates and nothing is cached (a cache Get/Set failure degrades to the live load, never an error).
T is stored and returned BY VALUE (the envelope round-trips a value), so concurrent callers that share the single-flight result never alias a mutable pointer — pass a value type (e.g. types.Page[...] or a small count struct), not a pointer.
func SuggestKey ¶
SuggestKey builds the value-suggestion cache key: the "suggest:" namespace then a digest of (tenant, resource, field, access_fp, prefix, limit). The limit is part of the key because the cached page is limit-bounded — two callers with the same prefix but different limits must NOT share an entry (a smaller-limit caller would otherwise read a larger-limit page, or vice-versa). The user-controlled prefix is folded into the digest, so it can never inject a delimiter to collide with another key.
Types ¶
type Envelope ¶
type Envelope[T any] struct { // Data is the cached value payload. Data T `json:"data"` // CachedAt records when the envelope was written, for observability and TTL reasoning. CachedAt time.Time `json:"cached_at"` // Version is the schema version the value was encoded with; a mismatch on // Decode is treated as a miss so stale-schema entries self-evict. Version int `json:"version"` }
Envelope wraps a cached value with metadata for version-aware caching. When the envelope's version doesn't match the expected version, the cached entry is treated as a miss — stale-schema entries are silently evicted.
Serialization is JSON, deliberately, not proto/binary: T is an arbitrary type parameter (repository entities: Ent models, domain structs), NOT a proto.Message, so proto.Marshal cannot encode it. A cache of one *concrete* proto response type can use proto encoding; that pattern does not generalize to Envelope[T]. The companion cursor codec (repos/repository/cursor.go) drops reflection where it can (a fixed 2-field shape); the generic payload here cannot, and a binary-header-plus-JSON-payload variant would churn the cache wire format on a hot read path for a small, version-bump-only gain — so JSON stays.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package decorators provides cache decorators for cross-cutting concerns.
|
Package decorators provides cache decorators for cross-cutting concerns. |