filememo

package
v0.6.0 Latest Latest
Warning

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

Go to latest
Published: Oct 1, 2026 License: Apache-2.0 Imports: 3 Imported by: 0

Documentation

Overview

Package filememo is the one answer to "a file read at most once per change".

THE SHAPE IT EXISTS FOR. A great many facts this program needs on the path of a keystroke or a turn live in a small file that almost never changes: the profile's config.json, which is read to answer whether a key is configured on EVERY Enter and whether this is the first prompt on EVERY turn; a project's AGENTS.md and CLAUDE.md, re-read whenever the system prompt's clock goes stale, which is precisely the came-back-from-lunch turn a person is already waiting on. Each of those was an os.ReadFile and a parse per call, and each was written separately because none of them looked expensive on its own.

So there is ONE mechanism, and every one of those callers uses it: a Memo holds the derived value beside the reading of the file it came from, and spends one stat to decide whether it may answer. A stat is a syscall against a page the kernel has cached; a read-and-parse is the file's bytes through encoding/json and an allocation per key.

WHAT MAKES AN ANSWER STALE, stated plainly because a memo that is wrong about this is worse than no memo at all:

  • The file's modification time or its size differs from the reading the held value came from. That is every ordinary write, including every write through an atomic create-temp-and-rename, which lands a file with a fresh mtime and usually a fresh size.
  • The file appearing where there was none, or disappearing. Absence is a value like any other and is held like one, so a profile with no config.json — the ordinary state of a fresh install — is answered without a read too.
  • A caller-supplied stamp moving ([Memo.Stamped]). It is how a package whose own writer runs in this process says "that was me": config's every persisted write bumps a counter, and reading that counter into the freshness key means this process can never serve its own stale write, on any filesystem, whatever its timestamp granularity.

AND WHAT IT DOES NOT SEE: a file rewritten by ANOTHER process to the same size within one tick of the filesystem's timestamp resolution. On every filesystem this program runs on that tick is a nanosecond. It is written down here rather than guarded against, because guarding against it means reading the file.

IT IS NOT THE SURFACE'S OWN HOLDER, and the difference is the contract rather than the code. The surface keeps a thing of the same family for what it draws with, and that one is refreshed by the frame's beat from a single goroutine and never stats anything on the path — it answers with whatever the last beat left, because a frame that blocks is a frame dropped. This one is the opposite bargain: it stats on every read, from any goroutine, and is therefore never behind the disk by more than a write that has not finished. A caller who can afford to be one beat stale wants that one; a caller who must not serve a setting the person just changed wants this one.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Derive

type Derive[T any] func(path string, data []byte, missing bool) (T, error)

Derive turns one file's bytes into the value a caller wanted. It is handed the path it read, the bytes, and whether the file was there at all — because a missing file is very often a legitimate answer (an empty settings object, no project instructions) rather than a failure.

type Memo

type Memo[T any] struct {
	// contains filtered or unexported fields
}

Memo is a derived value per path, held until the file behind it changes.

The zero value is not usable: a memo is made by New, which is what binds it to the one derivation it will ever perform. A memo per derivation rather than a memo per package is deliberate — two callers that parse the same file into two different shapes must not be able to hand each other the wrong one.

It is safe for concurrent use. The lock is held across the read, so two callers arriving on a cold memo at once perform one read rather than two; that is the right trade for files this small and this hot.

func New

func New[T any](derive Derive[T]) *Memo[T]

New binds a memo to its derivation.

func Stamped

func Stamped[T any](stamp func() uint64, derive Derive[T]) *Memo[T]

Stamped is New with an in-process invalidator: a counter the caller's own writer bumps, read into every freshness decision.

IT IS THE ANSWER TO TIMESTAMP GRANULARITY AND NOT A SECOND CACHE. A package that writes the file it reads knows exactly when it did so, and a memo that reads that knowledge cannot serve its own writer a stale value even where the filesystem's clock is too coarse to tell two writes apart.

func (*Memo[T]) Forget

func (m *Memo[T]) Forget()

Forget drops everything held. It exists for the tests that move a file under a memo faster than a filesystem can stamp it, and for a caller that has just learned its whole world moved. NOTHING ON A TURN'S PATH CALLS IT.

func (*Memo[T]) Read

func (m *Memo[T]) Read(path string) (T, error)

Read answers from the file at path, reading it only if it has changed since the held value was derived.

An error from the derivation is held like a value. Retrying a parse failure on every call would be a read per call for a file that is broken until somebody fixes it, which is the exact cost this type exists to remove.

Jump to

Keyboard shortcuts

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