cache

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 23, 2026 License: MIT Imports: 27 Imported by: 0

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

View Source
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`.

View Source
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
)
View Source
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
View Source
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.

View Source
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).

View Source
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.

View Source
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.

View Source
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.

View Source
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

View Source
var ErrBadGeneration = errors.New("cache: malformed generation")

ErrBadGeneration is returned by GenerationTime for a malformed value.

View Source
var ErrLockTimeout = errors.New("cache: lock acquisition timed out")

ErrLockTimeout is returned by the locker when the acquisition budget expires.

Functions

func AuthResolutionKey

func AuthResolutionKey(sc Scope) string

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 BodyHash

func BodyHash(body []byte) string

BodyHash is the §8.6 singleflight key component for a request body.

func Cacheable

func Cacheable(req Request, resp Response) (bool, string)

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

func DiscoveryRanWarning(elapsed time.Duration) output.Warning

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 FlightKey

func FlightKey(method, url string, body []byte) string

FlightKey is §8.6's singleflight key: method + url + sha256(body).

func GenerationTime

func GenerationTime(s string) (time.Time, error)

GenerationTime extracts the mint time from a generation.

func KeyFingerprint

func KeyFingerprint(credential string) string

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

func NewGeneration(now time.Time) string

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

func ValidGeneration(s string) bool

ValidGeneration reports whether s is a syntactically valid ULID.

func ValidScopeKey

func ValidScopeKey(name string) bool

ValidScopeKey reports whether name is exactly 16 lowercase hex characters.

func ValidShardRef

func ValidShardRef(ref string) bool

ValidShardRef reports whether a manifest's fields_shard value is a relative path this package is willing to open.

func ValidSlug

func ValidSlug(slug string) bool

ValidSlug reports whether slug is safe to use in a shard file name.

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

func (c Class) Persistable() bool

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.

func NewEntry

func NewEntry(sc Scope, generation string, class Class, req Request, resp Response, now time.Time) Entry

NewEntry builds an entry header. url is stored only after redact.URL (§8.3).

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

func Evaluate

func Evaluate(c Class, fetchedAt, now time.Time) (Freshness, time.Duration)

Evaluate places an entry on the ladder.

A fetchedAt in the future (clock skew, or a file copied between machines) is treated as age zero rather than as a negative age, which would otherwise make an entry immortal on one side of the comparison and expired on the other.

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

func (m *Manifest) Age(now time.Time) time.Duration

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

func (*Manifest) Entity

func (m *Manifest) Entity(slug string, kind EntityKind) (EntityRef, bool)

Entity finds a collection or global by slug.

func (*Manifest) Freshness

func (m *Manifest) Freshness(now time.Time) (Freshness, time.Duration)

Freshness places the manifest on the §8.5 ladder.

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

type Probe func(ctx context.Context) (fingerprint string, err error)

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.

  1. 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.
  2. Fire the Level-1 probe and wait up to budget (default 250 ms).
  3. Hash matches -> stale-served, bump confirmed_at.
  4. Hash differs -> discard, re-discover, re-issue, revalidated.
  5. 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.

func (Scope) Anonymous

func (s Scope) Anonymous() bool

Anonymous reports whether the scope carries no credential.

func (Scope) String

func (s Scope) String() string

String returns the scope key, which is what every path and log line uses.

func (Scope) Valid

func (s Scope) Valid() bool

Valid reports whether the scope key is a well-formed 16-hex directory name. Scope lookup only ever resolves an exact 16-hex name, which is what makes §8.7's rename-to-trash GC invisible to readers.

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

func (s *Session) AddWarnings(w ...output.Warning)

AddWarnings collects cache warnings for the envelope. Safe from any goroutine.

func (*Session) AllowRediscovery

func (s *Session) AllowRediscovery() bool

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

func (s *Session) DiscoveryMode() string

DiscoveryMode returns the meta.cache.discovery value for this invocation.

func (*Session) Do

func (s *Session) Do(key string, fn func() (any, error)) (val any, shared bool, err error)

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

func (s *Session) Doc(collection, key string) (any, bool)

Doc reads the RAM document memo. The RAM layer may hold document data; the disk layer may not (§8.6).

func (*Session) FlushCollection

func (s *Session) FlushCollection(collection string)

FlushCollection drops the RAM memo for a collection. Any successful write must call this immediately (§8.6).

func (*Session) Identity

func (s *Session) Identity() (any, bool)

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

func (s *Session) LoadManifest() (*Manifest, bool)

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) Manifest

func (s *Session) Manifest() *Manifest

Manifest returns the shared read-only manifest, or nil.

func (*Session) MarkNegative

func (s *Session) MarkNegative(key string)

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) Negative

func (s *Session) Negative(key string) bool

Negative reports whether key already produced a 404/501 in this process.

func (*Session) Options

func (s *Session) Options() SessionOptions

Options returns the per-invocation cache flags.

func (*Session) PersistWrites

func (s *Session) PersistWrites() bool

PersistWrites reports whether this session may write to disk. --no-cache discovers in-process but persists nothing; --refresh does write.

func (*Session) PutDoc

func (s *Session) PutDoc(collection, key string, v any)

PutDoc memoises a document for the rest of the process.

func (*Session) Rediscovered

func (s *Session) Rediscovered() bool

Rediscovered reports whether the Level-3 guard has already fired.

func (*Session) RevalidateRead

func (s *Session) RevalidateRead(ctx context.Context, probe Probe, now time.Time) Revalidation

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

func (s *Session) RevalidateWrite(ctx context.Context, probe Probe, now time.Time) Revalidation

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) Scope

func (s *Session) Scope() Scope

Scope returns the session's scope.

func (*Session) SetDiscoveryMode

func (s *Session) SetDiscoveryMode(mode string)

SetDiscoveryMode records how the manifest was obtained (output.Cache*).

func (*Session) SetIdentity

func (s *Session) SetIdentity(v any)

SetIdentity records the resolved identity in memory.

func (*Session) SetManifest

func (s *Session) SetManifest(m *Manifest)

SetManifest publishes the manifest for the rest of the process.

func (*Session) SetTopology

func (s *Session) SetTopology(fingerprint string)

SetTopology records the Level-1 probe result.

func (*Session) Store

func (s *Session) Store() *Store

Store returns the disk layer, which is nil-safe and may be disabled.

func (*Session) Topology

func (s *Session) Topology() (string, bool)

Topology returns the memoised Level-1 probe result, so a command touching three collections issues one /api/access, not three.

func (*Session) Warnings

func (s *Session) Warnings() []output.Warning

Warnings returns a copy of the collected warnings, de-duplicated by code+message so a fan-out over 49 collections cannot emit 49 identical cache_unreadable lines.

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 New

func New(root string) *Store

New returns a Store rooted at the cache directory ($CACHE from §4.1).

func (*Store) AuthResolutionPath

func (s *Store) AuthResolutionPath() string

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

func (s *Store) Clear(scope string, now time.Time) error

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

func (s *Store) ClearAll() error

ClearAll removes the whole cache root, including auth-resolution.json (§7.0(e): `pay cache clear --all` removes it).

func (*Store) Enabled

func (s *Store) Enabled() bool

Enabled reports whether this store may touch the filesystem.

func (*Store) EpochDir

func (s *Store) EpochDir() string

EpochDir returns $CACHE/v1.

func (*Store) GC

func (s *Store) GC(now time.Time) (GCStats, []output.Warning)

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) LockPath

func (s *Store) LockPath(sc Scope) string

LockPath returns $CACHE/v1/<scope>/.lock.

func (*Store) LookupAuthResolution

func (s *Store) LookupAuthResolution(sc Scope) (AuthResolution, bool, *output.Warning)

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

func (s *Store) ManifestPath(sc Scope) string

ManifestPath returns $CACHE/v1/<scope>/manifest.json.

func (*Store) MaybeGC

func (s *Store) MaybeGC(now time.Time) []output.Warning

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

func (s *Store) PutAuthResolution(sc Scope, slug string, now time.Time) (bool, *output.Warning)

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

func (s *Store) ReadManifest(sc Scope) (*Manifest, bool, *output.Warning)

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) Root

func (s *Store) Root() string

Root returns the cache root ($CACHE). It is exposed for `pay cache path`.

func (*Store) ScopeDir

func (s *Store) ScopeDir(sc Scope) string

ScopeDir returns $CACHE/v1/<scope>.

func (*Store) Scopes

func (s *Store) Scopes() []ScopeInfo

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

func (s *Store) Touch(sc Scope, now time.Time) (bool, *output.Warning)

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

func (s *Store) WriteSet(sc Scope, set Set, now time.Time) (bool, []output.Warning)

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

func TTLFor

func TTLFor(c Class) (TTL, bool)

TTLFor returns the §8.5 row for a class. An unknown class is reported as not persistable with a zero freshness window, so a future caller that invents a class cannot accidentally get permanent caching.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL