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 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) 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) 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) 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>/.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 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" // 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) 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) 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) 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.)