Documentation
¶
Overview ¶
Package cache implements PayCLI's on-disk discovery cache (§8) and the in-process memo layer that sits in front of it (§8.6).
Three invariants shape everything in this package:
- The cache is scoped by *credential*, not just by URL (§8.1). `/api/access` returns 10 collections anonymously and 49 authenticated, so a scope key that did not bind the credential would serve a truncated inventory in which 39 collections appear not to exist.
- Exactly one predicate, Cacheable, may authorise a disk write (§8.3). Document data, write responses, non-200s and non-JSON never touch disk.
- Any failure on the read path is a *miss*, never a command failure (§8.3), so `rm -rf $(pay cache path)` only ever costs latency (§4.1).
Nothing in this package calls time.Now: every entry point that needs the clock takes it as an argument, per §3.1.
Index ¶
- Constants
- Variables
- func AuthResolutionKey(sc Scope) string
- func BodyHash(body []byte) string
- func Cacheable(req Request, resp Response) (bool, string)
- func DiscoveryRanWarning(elapsed time.Duration) output.Warning
- func FlightKey(method, url string, body []byte) string
- func GenerationTime(s string) (time.Time, error)
- func KeyFingerprint(credential string) string
- func NewGeneration(now time.Time) string
- func ShardName(slug string, kind EntityKind) string
- func ValidGeneration(s string) bool
- func ValidScopeKey(name string) bool
- func ValidShardRef(ref string) bool
- func ValidSlug(slug string) bool
- type AuthResolution
- type CensusHeader
- type Class
- type EntityKind
- type EntityRef
- type Entry
- type Freshness
- type GCStats
- type Manifest
- type ManifestFingerprint
- type ManifestMeta
- type Probe
- type Request
- type Response
- type Revalidation
- type Scope
- type ScopeInfo
- type ScopeInput
- type Session
- func (s *Session) AddWarnings(w ...output.Warning)
- func (s *Session) AllowRediscovery() bool
- func (s *Session) DiscoveryMode() string
- func (s *Session) Do(key string, fn func() (any, error)) (val any, shared bool, err error)
- func (s *Session) Doc(collection, key string) (any, bool)
- func (s *Session) FlushCollection(collection string)
- func (s *Session) Identity() (any, bool)
- func (s *Session) LoadManifest() (*Manifest, bool)
- func (s *Session) Manifest() *Manifest
- func (s *Session) MarkNegative(key string)
- func (s *Session) Negative(key string) bool
- func (s *Session) Options() SessionOptions
- func (s *Session) PersistWrites() bool
- func (s *Session) PutDoc(collection, key string, v any)
- func (s *Session) Rediscovered() bool
- func (s *Session) RevalidateRead(ctx context.Context, probe Probe, now time.Time) Revalidation
- func (s *Session) RevalidateWrite(ctx context.Context, probe Probe, now time.Time) Revalidation
- func (s *Session) Scope() Scope
- func (s *Session) SetDiscoveryMode(mode string)
- func (s *Session) SetIdentity(v any)
- func (s *Session) SetManifest(m *Manifest)
- func (s *Session) SetTopology(fingerprint string)
- func (s *Session) Store() *Store
- func (s *Session) Topology() (string, bool)
- func (s *Session) Warnings() []output.Warning
- type SessionOptions
- type Set
- type Shard
- type Store
- func (s *Store) AuthResolutionPath() string
- func (s *Store) CensusPath(sc Scope) string
- func (s *Store) Clear(scope string, now time.Time) error
- func (s *Store) ClearAll() error
- func (s *Store) Enabled() bool
- func (s *Store) EpochDir() string
- func (s *Store) GC(now time.Time) (GCStats, []output.Warning)
- func (s *Store) LockPath(sc Scope) string
- func (s *Store) LookupAuthResolution(sc Scope) (AuthResolution, bool, *output.Warning)
- func (s *Store) ManifestPath(sc Scope) string
- func (s *Store) MaybeGC(now time.Time) []output.Warning
- func (s *Store) PutAuthResolution(sc Scope, slug string, now time.Time) (bool, *output.Warning)
- func (s *Store) ReadCensus(sc Scope) ([]byte, bool, *output.Warning)
- func (s *Store) ReadGraphQLType(sc Scope, typeName, schemaSHA256 string, now time.Time) (*Entry, bool, *output.Warning)
- func (s *Store) ReadManifest(sc Scope) (*Manifest, bool, *output.Warning)
- func (s *Store) ReadShard(sc Scope, m *Manifest, slug string, kind EntityKind) (*Shard, bool, *output.Warning)
- func (s *Store) Root() string
- func (s *Store) ScopeDir(sc Scope) string
- func (s *Store) Scopes() []ScopeInfo
- func (s *Store) Touch(sc Scope, now time.Time) (bool, *output.Warning)
- func (s *Store) UpdateCensus(sc Scope, merge func(current []byte) (any, error)) (bool, *output.Warning)
- func (s *Store) WriteCensus(sc Scope, body any) (bool, *output.Warning)
- func (s *Store) WriteSet(sc Scope, set Set, now time.Time) (bool, []output.Warning)
- type TTL
Constants ¶
const ( // GCMaxAge is the 30-day collection threshold. GCMaxAge = 30 * 24 * time.Hour // GCStampMaxAge forces a sweep when the sentinel is this old. GCStampMaxAge = 24 * time.Hour // DefaultGCDivisor is §8.7's 1-in-50 write-path probability. DefaultGCDivisor = 50 )
§8.7 GC.
On any write, with probability 1/50 or when the sentinel mtime is older than 24 h, sweep $CACHE/v1 and collect scope directories whose manifest.json -> meta.confirmed_at (the single authoritative copy, §8.2) is older than 30 days, plus foreign epoch directories. Never on the read path: a sweep on read would put a multi-second stat storm in front of `pay --help`.
const ( // AnonKeyFingerprint is the §8.1 sentinel used in anonymous mode. It is a // literal, not a hash, so an anonymous scope can never collide with a // credentialled one (§8.3 rule 6 relies on this). AnonKeyFingerprint = "anon" // NoHeadersFingerprint is the §8.1 sentinel for "no extra headers". NoHeadersFingerprint = "none" // FingerprintLen is the §5 "16 hex everywhere" rule: credentials.json, // manifest.meta.key_fingerprint, `pay auth status`, `pay cache ls` and the // §8.1 scope input all use the same 16 characters. FingerprintLen = 16 )
const ( // Epoch is the cache-format epoch. A directory under any other vN is // garbage-collected rather than read (§8.2). Epoch = "v1" // FilePerm is the mode of every cache file; DirPerm of every directory // (§4.1: files 0600, directories 0700). FilePerm fs.FileMode = 0o600 DirPerm = fsatomic.DirPerm )
§8.2 on-disk layout.
$CACHE/v1/auth-resolution.json §7.0(e) resolved auth-collection slug $CACHE/v1/<scope>/manifest.json the index (§7.8.1), meta+fingerprint folded in $CACHE/v1/<scope>/fields/<slug>.json one field shard per entity (§7.8.2) $CACHE/v1/<scope>/fields/_global_<slug>.json $CACHE/v1/<scope>/graphql/<Type>.json per-type raw introspection $CACHE/v1/<scope>/census.json §7.10c block census (aggregate, no documents) $CACHE/v1/<scope>/.lock
const ( ReasonCacheable = "cacheable" ReasonNoCache = "cache_disabled" ReasonNonIdempotent = "non_idempotent_method" ReasonStatusNot200 = "status_not_200" ReasonContentTypeNotJSON = "content_type_not_json" ReasonClassNotPersistable = "class_not_persistable" ReasonIdentityResponse = "identity_response" ReasonAnonymousIntoScope = "anonymous_into_authenticated_scope" ReasonRedirectHostChanged = "redirect_changed_host" ReasonBodyIncomplete = "body_incomplete" ReasonUnknownClass = "unknown_class" )
Reasons returned by Cacheable. ReasonCacheable is the only one that permits a disk write.
const ( // WarnCacheUnreadable accompanies every read-path degradation (§8.3). WarnCacheUnreadable = "cache_unreadable" // WarnCacheWriteFailed accompanies a skipped cache write (§8.7). A skipped // write is never silent: without this a read-only $CACHE makes every // invocation silently re-pay discovery forever. WarnCacheWriteFailed = "cache_write_failed" // WarnRevalidationTimedOut is §8.5 ladder step 5. WarnRevalidationTimedOut = "revalidation_timed_out" // WarnDiscoveryRan is §8.5's cold-cache warning. WarnDiscoveryRan = "discovery_ran" )
Warning codes emitted by the cache layer. §10's output.Warn* constants cover the shared layers; these four are §8's own (§8.3, §8.5, §8.7).
const DefaultLockTimeout = 5 * time.Second
DefaultLockTimeout is §8.7's 5 s flock acquisition timeout. On timeout the write is skipped and the command continues: a cache failure must never fail a command, and a stale lock from a crashed process must never wedge the CLI.
const (
// GenerationLen is the canonical ULID length.
GenerationLen = 26
)
A generation is a ULID minted once per discovery run and stamped on every artefact that run writes: manifest.json, every fields/<slug>.json and every graphql/<Type>.json (§8.2).
It is what makes lock-free reads safe. A reader that observes a shard from writer A and an index from writer B sees mismatched generations and treats it as a miss; without it the torn read is structurally undetectable (§8.7).
ULID rather than a random hex string because the embedded millisecond timestamp makes "which run wrote this" answerable from the value alone, and it sorts lexicographically by mint time.
const ManifestVersion = 2
ManifestVersion is the §7.8.1 layout version this build writes and reads. A file carrying any other value is a cache miss, not a parse error.
const RevalidationBudget = 250 * time.Millisecond
RevalidationBudget is §8.5's 250 ms: the longest a stale-but-valid entry may hold output unwritten while the Level-1 probe runs.
Variables ¶
var ErrBadGeneration = errors.New("cache: malformed generation")
ErrBadGeneration is returned by GenerationTime for a malformed value.
var ErrLockTimeout = errors.New("cache: lock acquisition timed out")
ErrLockTimeout is returned by the locker when the acquisition budget expires.
Functions ¶
func AuthResolutionKey ¶
AuthResolutionKey hashes (normURL, keyFP) the way §7.0(e) specifies. It is independent of graphql_path and of the extra-header set: the auth collection a credential belongs to cannot change because a bypass header was added.
func Cacheable ¶
Cacheable is §8.3: the only function authorised to permit a disk write.
Rule 1 reads "any non-GET response" in §8.3, but §8.2 caches graphql/<Type>.json, which can only be obtained by POSTing an introspection query. The two are reconciled the way §6.1 already reconciles them for retries: the gate is the *idempotency-safe* predicate (GET, read-only POST /api/graphql, or POST with the method-override header), not the literal method. A real write is never cacheable under either reading.
func DiscoveryRanWarning ¶
DiscoveryRanWarning is §8.5's cold-cache warning.
§8.5 shows an "elapsed_ms" key on this warning; output.Warning (§10.1) has no such field and inventing one here would desynchronise the envelope schema, so the elapsed time is folded into the message instead.
func GenerationTime ¶
GenerationTime extracts the mint time from a generation.
func KeyFingerprint ¶
KeyFingerprint is the §5 credential fingerprint: the first 16 hex characters of sha256("paycli-key-v1\x00" + credential). An empty credential (anonymous mode) yields the literal "anon".
The credential itself is hashed and immediately discarded; it is never stored in a scope, a manifest, a log line or an error message (§5.3).
func NewGeneration ¶
NewGeneration mints a ULID for a discovery run: 48 bits of millisecond timestamp followed by 80 bits of cryptographic randomness.
now is passed in rather than read from the clock because §3.1 reserves time.Now for internal/cli/app.go.
func ShardName ¶
func ShardName(slug string, kind EntityKind) string
ShardName returns the manifest-relative shard path for an entity, matching §7.8.1's fields_shard values ("fields/pages.json", "fields/_global_nav.json").
func ValidGeneration ¶
ValidGeneration reports whether s is a syntactically valid ULID.
func ValidScopeKey ¶
ValidScopeKey reports whether name is exactly 16 lowercase hex characters.
func ValidShardRef ¶
ValidShardRef reports whether a manifest's fields_shard value is a relative path this package is willing to open.
Types ¶
type AuthResolution ¶
type AuthResolution struct {
AuthCollection string `json:"auth_collection"`
ResolvedAt time.Time `json:"resolved_at"`
CLIMajor int `json:"cli_major"`
}
AuthResolution is one §7.0(e) record: the auth-collection slug resolved for a (normURL, keyFP) pair. The pair itself is hashed, so the file never reveals which servers or credentials a user has.
type CensusHeader ¶ added in v0.3.0
type CensusHeader struct {
CensusVersion int `json:"census_version"`
Scope string `json:"scope"`
UpdatedAt time.Time `json:"updated_at"`
}
CensusHeader is the part of census.json the cache verifies.
type Class ¶
type Class string
Class is a §8.5 data class. Every cache entry carries its class, and the class alone decides both the TTL and whether the entry may ever reach disk.
const ( // ClassDiscoveryManifest is the §7.8.1 index: topology, capabilities, the // collection/global inventory. ClassDiscoveryManifest Class = "discovery_manifest" // ClassFieldShard is a §7.8.2 shard. §8.5 has no separate row for shards; // they are written and invalidated as part of the manifest set (§8.7) and // therefore share the manifest's TTL. ClassFieldShard Class = "field_shard" // ClassPermissions is the per-identity /api/access projection. ClassPermissions Class = "permissions" // ClassGraphQLType is one per-type introspection blob. ClassGraphQLType Class = "graphql_type" // ClassWhereInput is a _where input type. ClassWhereInput Class = "where_input" // ClassGraphQLMode is the GraphQL mode / capability probe result. ClassGraphQLMode Class = "graphql_mode" // ClassIdentity is /api/{auth}/me. Memory only: the body contains a // plaintext API key (§8.3 rule 5, verified). ClassIdentity Class = "identity" // ClassSkillsManifest covers the skills manifest and release metadata. ClassSkillsManifest Class = "skills_manifest" // ClassBlockCensus is §7.10c's block census: an AGGREGATE of observed // block rows (counts, JSON types, a bounded set of short scalar values and // document references). It holds no document, so §8.3 rule 4 does not // apply; its fresh window is the GraphQL-schema row's, because both // describe the project's block schema and change only on a deploy. ClassBlockCensus Class = "block_census" // ClassDocument is document data of any kind. Never cached to disk // (§8.3 rule 4): Payload content is mutable by definition and stale // content served to an agent causes wrong edits. ClassDocument Class = "document" )
func Classes ¶
func Classes() []Class
Classes returns every known class, sorted, for `pay explain` and tests.
func (Class) Persistable ¶
Persistable reports whether a class may be written to disk (§8.3).
type EntityKind ¶
type EntityKind string
EntityKind distinguishes the two shard namespaces.
const ( KindCollection EntityKind = "collection" KindGlobal EntityKind = "global" )
type EntityRef ¶
type EntityRef struct {
Slug string `json:"slug"`
FieldsCount int `json:"fields_count"`
FieldsSHA256 string `json:"fields_sha256"`
FieldsShard string `json:"fields_shard"`
}
EntityRef is the subset of a collection/global entry the cache must verify: which shard holds its fields and what that shard must hash to.
type Entry ¶
type Entry struct {
Scope string `json:"scope"`
Generation string `json:"generation"`
Method string `json:"method"`
URL string `json:"url"`
Status int `json:"status"`
ContentType string `json:"content_type"`
FetchedAt time.Time `json:"fetched_at"`
TTL int64 `json:"ttl"`
CLIMajor int `json:"cli_major"`
Class Class `json:"class"`
SchemaSHA256 string `json:"schema_sha256,omitempty"`
Body json.RawMessage `json:"body"`
}
Entry is §8.3's mandatory self-describing header plus the payload. Every field of the header is verified on read; a mismatch is a miss and the entry is deleted, because unlike a shard an entry is never part of an atomically published set.
type Freshness ¶
type Freshness string
Freshness is the position of an entry on the §8.5 staleness ladder.
const ( // FreshnessFresh: use with zero network, meta.cache.discovery = "hit". FreshnessFresh Freshness = "fresh" // FreshnessStale: past fresh but within hard max — run the ladder. FreshnessStale Freshness = "stale" // FreshnessExpired: past hard max — a miss. FreshnessExpired Freshness = "expired" )
type GCStats ¶
type GCStats struct {
ScopesCollected int `json:"scopes_collected"`
EpochsCollected int `json:"epochs_collected"`
TrashRemoved int `json:"trash_removed"`
Errors int `json:"errors"`
}
GCStats reports what a sweep collected. `pay doctor` prints it.
type Manifest ¶
type Manifest struct {
Raw []byte `json:"-"`
Path string `json:"-"`
ManifestVersion int `json:"manifest_version"`
Generation string `json:"generation"`
CLIVersion string `json:"cli_version"`
GeneratedAt time.Time `json:"generated_at"`
ExpiresAt time.Time `json:"expires_at"`
Meta ManifestMeta `json:"meta"`
Fingerprint ManifestFingerprint `json:"fingerprint"`
Collections []EntityRef `json:"collections"`
Globals []EntityRef `json:"globals"`
}
Manifest is the verified index. Raw is the exact bytes read from disk, which is what discovery decodes into its own richer struct.
func (*Manifest) Age ¶
Age is the manifest's age against meta.confirmed_at, which §8.2 names as the single authoritative timestamp (created_at and generated_at record when the data was *produced*; confirmed_at records when it was last known good).
type ManifestFingerprint ¶
type ManifestFingerprint struct {
TopologySHA256 string `json:"topology_sha256"`
SchemaSHA256 string `json:"schema_sha256"`
CheckedAt time.Time `json:"checked_at"`
ServerIdentity string `json:"server_identity"`
}
ManifestFingerprint is manifest.json -> fingerprint (§8.4 levels 1 and 2).
type ManifestMeta ¶
type ManifestMeta struct {
Scope string `json:"scope"`
Profiles []string `json:"profiles"`
BaseURL string `json:"base_url"`
APIPath string `json:"api_path"`
GraphQLPath string `json:"graphql_path"`
HeaderNames []string `json:"header_names"`
KeyFingerprint string `json:"key_fingerprint"`
CreatedAt time.Time `json:"created_at"`
ConfirmedAt time.Time `json:"confirmed_at"`
}
ManifestMeta is manifest.json -> meta. Header *values* are never present: only the sorted names, so `pay cache ls` can explain why two profiles do or do not share a scope (§8.1).
type Probe ¶
Probe is the §8.4 Level-1 topology probe: it returns topology_sha256 for the current identity. It is supplied by internal/discovery so this package never makes a network call itself.
type Request ¶
type Request struct {
Method string
URL string
// Class is what the response carries. Call sites never decide whether to
// cache; they only say what kind of thing they fetched.
Class Class
// IdempotencySafe is §6.1's predicate: GET, POST /api/graphql carrying a
// read-only query, or POST with X-Payload-HTTP-Method-Override: GET.
IdempotencySafe bool
// Anonymous is true when the request carried no credential.
Anonymous bool
// ScopeKeyFingerprint is the scope the answer would be filed under.
ScopeKeyFingerprint string
// NoCache is --no-cache / PAY_NO_CACHE=1: discover in-process, write
// nothing to disk (§8.5).
NoCache bool
}
Request is the request half of the Cacheable predicate.
type Response ¶
type Response struct {
Status int
ContentType string
Body []byte
// RedirectedHost is true when a redirect changed host (§8.3 rule 7).
RedirectedHost bool
// ShapeErr is the caller's strict json.Unmarshal error, if any. Bodies
// arrive Transfer-Encoding: chunked with no Content-Length, so a truncated
// body can still look like valid JSON to a permissive decoder (§8.3 rule 8).
ShapeErr error
}
Response is the response half of the Cacheable predicate.
type Revalidation ¶
type Revalidation struct {
// Discovery is the meta.cache.discovery value: output.CacheStaleServed or
// output.CacheRevalidated.
Discovery string
// Refresh is true when the caller must discard the held response,
// re-discover and re-issue the operation (ladder step 4).
Refresh bool
// Confirm is true when the caller should bump meta.confirmed_at
// (ladder step 3).
Confirm bool
// Warning is the revalidation_timed_out warning, set only on step 5.
Warning *output.Warning
// Err is the probe's error, if any. Informational: a probe error is a
// stale-serve, never a command failure.
Err error
// Fingerprint is the topology hash the probe returned, when it completed.
Fingerprint string
}
Revalidation is the outcome of the §8.5 ladder.
func Revalidate ¶
func Revalidate(ctx context.Context, cachedFingerprint string, probe Probe, budget time.Duration) Revalidation
Revalidate runs §8.5's five-step ladder for a stale entry.
- The caller has already computed the response from the stale manifest and is holding it unwritten — that is the caller's half of the contract, and it is what makes the advertised warm latency honest.
- Fire the Level-1 probe and wait up to budget (default 250 ms).
- Hash matches -> stale-served, bump confirmed_at.
- Hash differs -> discard, re-discover, re-issue, revalidated.
- Timeout/error -> stale-served plus the revalidation_timed_out warning.
The probe goroutine writes to a buffered channel, so a timed-out probe finishes and exits on its own rather than leaking or blocking the command.
type Scope ¶
type Scope struct {
// Key is the 16-hex scope directory name.
Key string
// NormURL is lowercase(scheme)://lowercase(host)[:port]+api_path.
NormURL string
// GraphQLPath is the normalised GraphQL endpoint path.
GraphQLPath string
// KeyFingerprint is the 16-hex credential fingerprint, or "anon".
KeyFingerprint string
// HeaderNames are the sorted, de-duplicated, lowercased extra-header names.
// Values are never present, here or anywhere else on disk.
HeaderNames []string
// HeadersFingerprint is the 16-hex hash of the sorted name:value pairs, or
// the literal "none".
HeadersFingerprint string
}
Scope is a computed cache scope. It holds no secret material: the credential and every header value have already been reduced to 16-hex fingerprints.
func NewScope ¶
func NewScope(in ScopeInput) (Scope, error)
NewScope computes the §8.1 scope key.
scope = sha256hex(normURL \0 graphqlPath \0 keyFP \0 headersFP)[:16]
A malformed base URL is the one hard error: it is a configuration problem (exit 9), not a cache miss.
type ScopeInfo ¶
type ScopeInfo struct {
Scope string `json:"scope"`
Path string `json:"path"`
Profiles []string `json:"profiles"`
BaseURL string `json:"base_url"`
APIPath string `json:"api_path"`
GraphQLPath string `json:"graphql_path"`
HeaderNames []string `json:"header_names"`
KeyFP string `json:"key_fingerprint"`
Generation string `json:"generation"`
CreatedAt *time.Time `json:"created_at"`
ConfirmedAt *time.Time `json:"confirmed_at"`
Collections int `json:"collections"`
Globals int `json:"globals"`
Shards int `json:"shards"`
Bytes int64 `json:"bytes"`
Readable bool `json:"readable"`
}
ScopeInfo is one row of `pay cache ls` (§9). It carries no secret material: only fingerprints and header *names* (§8.1).
type ScopeInput ¶
type ScopeInput struct {
// BaseURL is the profile's resolved base URL. Userinfo is stripped by
// config.Resolve (§4.2); it is stripped again here, defensively.
BaseURL string
// APIPath is the Payload REST prefix, e.g. "/api". It is part of normURL
// because two Payload projects on one host can use different routes.api.
APIPath string
// GraphQLPath is the resolved GraphQL endpoint, e.g. "/api/graphql". It is
// an input because it determines capabilities.graphql.mode and every
// GraphQL-derived field schema.
GraphQLPath string
// Credential is the API key (api-key mode) or the JWT (jwt mode), empty in
// anonymous mode. Hashed immediately, never retained.
Credential string
// KeyFingerprint short-circuits Credential when the caller already holds
// the §5 fingerprint (internal/secret computes it once per process).
KeyFingerprint string
// Headers is the [profiles.X.headers] table AFTER ${ENV} interpolation.
// Values are hashed and discarded; only the sorted lowercase names survive,
// into Scope.HeaderNames and manifest meta.header_names (§5.3).
Headers map[string]string
}
ScopeInput is everything §8.1 feeds into the scope key. Exactly one of Credential / KeyFingerprint is needed; Credential is hashed on the spot.
authCollectionSlug is deliberately absent: §8.1 states it is a derived function of (normURL, credential) and including it would make the scope path uncomputable until after the answer it stores was already known. The profile *name* is absent for the same stated reason — two profiles pointing at the same server with the same key share one discovery.
type Session ¶
type Session struct {
// contains filtered or unexported fields
}
Session is the in-process cache built once in PersistentPreRun and threaded on the context (§8.6). It holds the decoded manifest, the resolved identity (memory only), the Level-1 probe result, a singleflight group, a negative memo for 404/501 paths, a document memo, and the §8.4(b) re-entrancy guard.
The manifest is immutable after construction and shared by read-only pointer; only the mutable memos are guarded.
func NewSession ¶
func NewSession(store *Store, sc Scope, opts SessionOptions) *Session
NewSession builds the in-process layer for one invocation.
func (*Session) AddWarnings ¶
AddWarnings collects cache warnings for the envelope. Safe from any goroutine.
func (*Session) AllowRediscovery ¶
AllowRediscovery is §8.4(b): at most one Level-3 re-discovery per scope per process. The first call returns true; every later call returns false, and the caller must fail with schema_stale (exit 10) rather than re-discover again — no ping-pong between a genuinely stale scope and a genuinely invalid path.
func (*Session) DiscoveryMode ¶
DiscoveryMode returns the meta.cache.discovery value for this invocation.
func (*Session) Do ¶
Do runs fn once per key for the lifetime of the session, sharing the result with every concurrent caller. shared reports whether the result came from another caller's in-flight call.
A tiny singleflight rather than golang.org/x/sync: §3.2 pins the dependency list at seven direct modules, and this is twenty lines.
func (*Session) Doc ¶
Doc reads the RAM document memo. The RAM layer may hold document data; the disk layer may not (§8.6).
func (*Session) FlushCollection ¶
FlushCollection drops the RAM memo for a collection. Any successful write must call this immediately (§8.6).
func (*Session) Identity ¶
Identity is the resolved /me result. Memory only, for the whole process lifetime: §8.3 rule 5 forbids it ever reaching disk.
func (*Session) LoadManifest ¶
LoadManifest reads the manifest from disk once per process, collecting any cache_unreadable warning. --refresh skips the read entirely so a forced refresh never races its own stale copy.
func (*Session) MarkNegative ¶
MarkNegative records a 404/501 path so a fan-out over 49 collections hits payload-migrations's 501 once rather than 49 times. Negative results are memory-only by §8.3 rule 2.
func (*Session) Options ¶
func (s *Session) Options() SessionOptions
Options returns the per-invocation cache flags.
func (*Session) PersistWrites ¶
PersistWrites reports whether this session may write to disk. --no-cache discovers in-process but persists nothing; --refresh does write.
func (*Session) Rediscovered ¶
Rediscovered reports whether the Level-3 guard has already fired.
func (*Session) RevalidateRead ¶
RevalidateRead runs the §8.5 staleness ladder for a stale manifest on the read path: the caller is holding its computed response unwritten, this call spends at most RevalidationBudget deciding what to do with it, and the resulting discovery mode, confirmed_at bump and warning are all applied to the session.
func (*Session) RevalidateWrite ¶
RevalidateWrite is §8.5's "serve-stale is read-path only": a write whose client-side validation depends on cached field names waits for the Level-1 probe rather than capping it at 250 ms. The wait is still bounded, by the command's own --timeout/--deadline on ctx.
A probe that fails here forces a re-discovery rather than a stale serve: for a write, "I could not confirm the schema" and "the schema changed" must have the same consequence.
func (*Session) SetDiscoveryMode ¶
SetDiscoveryMode records how the manifest was obtained (output.Cache*).
func (*Session) SetIdentity ¶
SetIdentity records the resolved identity in memory.
func (*Session) SetManifest ¶
SetManifest publishes the manifest for the rest of the process.
func (*Session) SetTopology ¶
SetTopology records the Level-1 probe result.
type SessionOptions ¶
type SessionOptions struct {
// NoCache is --no-cache / PAY_NO_CACHE=1: discovery still runs in-process,
// nothing is persisted. It never disables client-side validation.
NoCache bool
// Refresh is --refresh: force a re-discovery, and do write it.
Refresh bool
}
SessionOptions carries the per-invocation cache flags (§8.5).
type Set ¶
type Set struct {
// Generation is the run's ULID. It must match the manifest's own.
Generation string
// Manifest is the §7.8.1 index: a struct, a map, or raw JSON bytes.
Manifest any
// Shards maps a manifest-relative shard path ("fields/pages.json") to its
// §7.8.2 body.
Shards map[string]any
// GraphQL maps a GraphQL type name to its introspection Entry body.
GraphQL map[string]Entry
}
Set is everything one discovery run publishes. Shards and GraphQL blobs are written first and manifest.json is renamed last, so the index — which names and checksums every shard — only becomes visible after everything it points at exists (§8.7).
type Shard ¶
type Shard struct {
Raw []byte `json:"-"`
Path string `json:"-"`
Generation string `json:"generation"`
Slug string `json:"slug"`
SHA256 string `json:"sha256"`
}
Shard is a verified §7.8.2 field shard.
type Store ¶
type Store struct {
// LockTimeout overrides DefaultLockTimeout (tests use a shorter one).
LockTimeout time.Duration
// GCDivisor is the 1/N write-path GC probability from §8.7 (default 50).
GCDivisor int
// DisableGC turns the opportunistic sweep off entirely.
DisableGC bool
// contains filtered or unexported fields
}
Store is the on-disk cache. A Store with an empty root is a valid, permanently disabled store: every read is a miss and every write is a silent no-op, which is exactly what `--no-cache` needs.
func (*Store) AuthResolutionPath ¶
AuthResolutionPath returns $CACHE/v1/auth-resolution.json. It lives beside the scope directories, not inside one, so it survives scope GC (§7.0(e)).
func (*Store) CensusPath ¶ added in v0.3.0
CensusPath returns $CACHE/v1/<scope>/census.json.
func (*Store) Clear ¶
Clear removes one scope, using the same rename-then-remove dance as GC so a concurrent reader sees a clean ENOENT rather than a half-deleted tree (§8.7).
func (*Store) ClearAll ¶
ClearAll removes the whole cache root, including auth-resolution.json (§7.0(e): `pay cache clear --all` removes it).
func (*Store) GC ¶
GC sweeps the cache root. It never returns an error: every failure is a skipped collection that the next sweep retries, and a cache problem never fails a command (§8.7).
Nothing is unlinked in place. A scope directory is first renamed to <scope>.trash-<ulid> — atomic, and invisible to scope lookup, which only ever resolves an exact 16-hex name — and then removed best effort. On Windows, unlinking a file another process has open fails outright, so the rename is what makes GC safe there; on unix it additionally guarantees a mid-traversal reader gets a clean ENOENT, which §8.3 already defines as a miss.
func (*Store) LookupAuthResolution ¶
LookupAuthResolution returns the cached auth-collection slug for this scope. Like every other read path it degrades to a miss (§8.3).
func (*Store) ManifestPath ¶
ManifestPath returns $CACHE/v1/<scope>/manifest.json.
func (*Store) MaybeGC ¶
MaybeGC runs the opportunistic sweep described in §8.7. It is called from the write path only, after the lock is released.
func (*Store) PutAuthResolution ¶
PutAuthResolution records the resolved slug. The whole map is rewritten atomically; a concurrent writer can lose an unrelated entry, which costs one extra Stage -1 bootstrap and never a wrong answer.
func (*Store) ReadCensus ¶ added in v0.3.0
ReadCensus returns the raw census for a scope. A missing file is a plain miss; anything else that prevents reading it is a miss plus a cache_unreadable warning (§8.3).
func (*Store) ReadGraphQLType ¶
func (s *Store) ReadGraphQLType(sc Scope, typeName, schemaSHA256 string, now time.Time) (*Entry, bool, *output.Warning)
ReadGraphQLType returns the cached introspection blob for one GraphQL type. schemaSHA256 is the §8.2 key that prevents a graphql blob from disagreeing with the manifest that references it; pass "" to skip that check.
func (*Store) ReadManifest ¶
ReadManifest loads and verifies the index for a scope.
It never returns an error. Per §8.3 every failure on the read path — ENOENT, EACCES, a short read, malformed JSON, an unknown manifest_version, a scope mismatch — is a miss, optionally carrying a cache_unreadable warning. A plain "not cached yet" is a miss with no warning: a cold start is not an anomaly.
func (*Store) ReadShard ¶
func (s *Store) ReadShard(sc Scope, m *Manifest, slug string, kind EntityKind) (*Shard, bool, *output.Warning)
ReadShard loads one entity's field shard and verifies it against the index.
A shard whose generation or sha256 does not match the index's fields_shard/fields_sha256 entry is a miss **for that entity only** (§8.2): PayCLI re-discovers rather than mixing two runs. The file is not deleted — it may well be a *newer* generation whose index has not been renamed into place yet, and deleting it would corrupt a concurrent writer's set.
func (*Store) Scopes ¶
Scopes lists every scope directory under the current epoch, newest first. Unreadable scopes are reported with Readable=false rather than skipped, so `pay cache ls` can explain a permissions problem instead of hiding it.
func (*Store) Touch ¶
Touch bumps meta.confirmed_at after a successful §8.5 step-3 revalidation. It rewrites the index in place, atomically, under the same lock the writer uses; nothing else in the file changes.
func (*Store) UpdateCensus ¶ added in v0.3.0
func (s *Store) UpdateCensus(sc Scope, merge func(current []byte) (any, error)) (bool, *output.Warning)
UpdateCensus is WriteCensus as a read-modify-write under the scope lock: merge receives the census on disk as it is NOW (nil when there is none or it cannot be read for this scope) and returns the body to write. Agents run tools in parallel, and a census built from a copy read before another process wrote its own entities would otherwise drop them.
A merge that returns (nil, nil) writes nothing and reports false.
func (*Store) WriteCensus ¶ added in v0.3.0
WriteCensus persists a census atomically under the scope lock. body must marshal to an object whose "scope" is this scope; a census for another scope is refused rather than written where the next reader would reject it.
Like every cache write it never fails a command: it returns false plus a cache_write_failed warning (§8.7).
func (*Store) WriteSet ¶
WriteSet publishes a discovery run atomically.
It returns ok=false plus a cache_write_failed warning for every failure mode, and never an error: §8.7 requires that a read-only or full $CACHE degrade to "slower", never to "broken", and requires that the degradation be visible rather than silent.
type TTL ¶
type TTL struct {
Fresh time.Duration
HardMax time.Duration
// Persist reports whether this class may ever be written to disk.
Persist bool
}
TTL is one row of the §8.5 table. HardMax == 0 means there is no serve-stale window at all: past Fresh the entry is simply expired. (§8.5 writes "—" for the identity and skills rows.)