class

Hwaro::Core::Build::Cache

Inherits Reference < Object

Build cache manager for tracking file changes

Constants

CACHE_FILE = ".hwaro_cache.json"

Default cache filename - uses dot prefix to hide from directory listings and 'hwaro_' prefix to identify it as project-specific cache

RACY_WINDOW_MS = 2000_i64

Racy-git: an mtime only stands in for a file's bytes once it is safely older than the moment the stamp was taken. Filesystems stamp with a coarse clock (ext4 uses the kernel tick, 1-10ms; HFS+ 1s, FAT 2s), so a same-size rewrite inside the tick a stamp was taken in keeps the stamped mtime, and the next warm build served the old output (#857: two builds in one tick, as specs, serve and scripts run them). A stamp that is not stable_mtime? is not kept; the next build re-hashes the file instead.

SERVE_CACHE_FILE = ".hwaro/serve_cache.json"

hwaro serve --cache keeps its own cache, inside the .hwaro/ workspace next to the .hwaro/serve tree it describes. A cache remembers ONE output directory, and a --cache build whose cache names another one takes the cold path — so a shared file made every hwaro build --cache after a serve session wipe public/ and regenerate every OG image and resized image variant.

UNSTABLE_MTIME = Int64::MIN

Stored in place of an mtime that was not stable when recorded. Never a real mtime, so the next check re-hashes the file.

Constructors

new(enabled : Bool = true, cache_path : String = CACHE_FILE)
Source

Class methods

compute_config_hash(config : Models::Config, env : String | Nil = nil) : String

Compute a checksum for the effective (env-merged, env-substituted) config plus the active env name and resolved base_url. Hashing the parsed config.raw rather than the raw config.toml bytes means an env override file (config.<env>.toml), changed ${ENV_VAR} substitutions, or a --base-url override all invalidate the per-page cache — none of which the file-bytes hash above can detect — while a formatting-only edit to config.toml no longer forces a full rebuild.

Source
compute_options_hash(options : Config::Options::BuildOptions) : String

Fingerprint the CLI options that change what a page RENDERS TO.

Every other cache hash covers files (sources, templates, config, data). None of them move when the same sources are built with a different flag, so build --cache --minify after a plain build --cache kept serving the unminified HTML it had already rendered — for as long as the pages stayed untouched. Flags that only affect which pages are built (--drafts, --include-future) or how fast the build runs (--jobs, --stream, --no-parallel) are deliberately excluded: they never change a rendered page's bytes, and folding them in would force needless cold rebuilds.

Source
compute_templates_hash(templates : Hash(String, String)) : String

Compute a combined checksum for a set of template files. Fields are length-prefixed (see DigestUtils) so adjacent name/content pairs can't produce the same byte stream across boundaries, which would have failed to invalidate.

Source
discard_if_tracking(output_dir : String, cache_path : String = CACHE_FILE) : Nil

A build WITHOUT --cache rewrites output_dir and never touches the cache file, which keeps describing the tree as it was: the next --cache build then keeps the new output (drafts, minified or other-base-url pages) and skips every page whose hashes still match. Drop the file when it describes this output directory; a cache for another one is none of this build's business.

Source
output_dir_key(output_dir : String) : String

Workspace-independent identity for an output directory: relative to the project root when it lives inside it, absolute otherwise. Makes -o dist vs -o preview distinguishable while /ci/a/dist and /ci/b/dist compare equal.

Source
path_for(serve_mode : Bool) : String

The cache file a build with these settings reads and writes.

Source
stable_mtime?(mtime_ms : Int64, now_ms : Int64) : Bool

now_ms must be read BEFORE the file is stat'ed and hashed.

Source

Instance methods

changed?(file_path : String, output_path : String = "", cascade_hash : String = "", template_hash : String | Nil = nil, extra_outputs : Array(String) = [] of String, assets_hash : String = "", git_hash : String = "", relations_hash : String = "") : Bool

Check if a file has changed since last build. template_hash is the page's template closure fingerprint; nil skips the comparison (non-page entries, or dependency tracking off). extra_outputs are secondary sibling output files (see [outputs]) that must also still exist on disk — a manually deleted index.json forces a rebuild just like a deleted index.html does.

Source
check_render_inputs(digest : String) : Bool

Compare the current render-inputs digest (computed over render_input_keys) with the last build's. A change invalidates every entry, like a config edit: the reads are global, and a cached page cannot say which of them it made. Returns true when it did.

Source
clear

Clear all cache entries

Source
compute_file_hash(file_path : String) : String

MD5 checksum of a file's content (streamed by Digest#file)

Source
copy_stamp_trusted?(dest : String, src_mtime_ms : Int64, now_ms : Int64) : Bool

May a copy whose size and mtime equal its source's skip the byte compare? Not while the source mtime is racy (#857), nor on the first check after a copy was made inside that window (#859): the copy carries the source mtime, so a same-size rewrite in that tick matches it exactly. That check takes dest off the list, since the caller then compares the bytes or copies again.

Source
current_output_files

Every output file the entries that SURVIVED this build record. The Finalize phase subtracts it from the stale list: a path harvested from a discarded entry and then written again is not stale at all.

Source
derived_paths_for(file_path : String) : Array(String)

Files this page wrote BESIDES its own output on the last build — its aliases redirect stubs and a section's /page/N/ pagination pages (see CacheEntry#derived_paths). They are produced only by a render, so a caller that needs to know whether something else has since overwritten one has to ask for them by name.

Source
enabled?

Check if caching is enabled

Source
invalidate(file_path : String)

Remove entry from cache

Source
invalidate_on_disk

Write-ahead invalidation: the file on disk stops describing the output directory the moment a build starts rewriting it, and only a successful Finalize save (forced by the dirty flag) vouches for it again. A build that dies mid-render otherwise leaves the previous build's hashes next to half-rewritten pages, and reverting the edit that broke it makes every hash match again — a warm build then trusts output it never produced.

Source
load

Load cache from disk

Source
output_paths_for(file_path : String) : Array(String)

Secondary output files (beyond the primary HTML output) recorded for file_path on the last build, or [] when the entry has none (or doesn't exist). Used to detect a manually deleted sibling format file and to locate stale files when a page's source is removed (see Builder#stale_outputs_for_removed).

Source
page_set_changed?(fingerprint : String) : Bool

Has the global page set (content page metadata that listings render — path/url/title/date/weight/draft/section) changed since last build?

Source
previous_generated_outputs

The source-less generated outputs the LAST build recorded, and the setter the current build's Finalize phase persists through. Paths are stored relative to the output directory; callers rejoin them.

Source
prune_entries_not_in(live_paths : Set(String)) : Array(String)

Drop every entry whose source path is NOT in live_paths and return the output files those entries recorded.

A --cache build keeps the output directory between runs (a cold build wipes it), and nothing else notices that a page went away: the render phase only walks pages that still exist. So rm content/posts/old.md && hwaro build --cache left /posts/old/ in public/ — still served, still deployed — and the same held for a renamed source, a post flipped to draft, one that passed its expires date, and render = false. Each entry knows the primary file it wrote plus its [outputs] siblings, so the entries no live page claims name exactly the files to remove.

Callers filter the result against the outputs the CURRENT site claims before deleting: a source renamed onto the same output URL (foo.md -> foo/index.md) leaves an orphan entry pointing at a file this very build rewrote.

Source
record_generated_outputs(paths : Array(String)) : Nil
Source
record_render_inputs(digest : String, keys : Array(String)) : Nil

Persist this build's render-inputs digest and the read keys it covers; marks the cache dirty only when either moved.

Source
record_set_fingerprints(page_set : String, section_set : String) : Nil

Record the current page/section-set fingerprints so the next build can detect a change; marks the cache dirty when either value moves.

Source
render_input_keys

The render reads the last build recorded (see CacheMetadata#render_input_keys).

Source
render_inputs_hash

The render-inputs digest the last build recorded.

Source
save

Save cache to disk using atomic write (temp file + rename) to prevent corruption from partial writes (e.g. disk full, crash). No-op when nothing changed since load — a warm all-hits build otherwise re-serializes every entry just to write identical bytes.

Source
section_set_changed?(fingerprint : String) : Bool

Has the section set (section metadata that nav/menus render) changed?

Source
set_global_checksums(template_hash : String, config_hash : String, invalidate_on_template_change : Bool = true, output_dir : String = "")

Set the current build's template and config checksums. A config change always invalidates all entries. A template change invalidates all entries only when invalidate_on_template_change is true — with template dependency tracking active, the builder passes false and per-page closure hashes (see changed?) decide which pages a template edit actually affects.

Source
stats

Get cache statistics

Source
take_orphaned_outputs

Output files recorded by an entry this build then replaced — see @orphaned_outputs. Drained: a second call returns nothing.

Source
tracks_output_dir?(output_dir : String) : Bool

Does this cache describe what is sitting in output_dir right now?

A --cache build keeps the output directory and prunes only what the cache remembers writing there, so it is only as good as that memory. With no usable record — no cache file, a corrupt one (whose warning promises a rebuild from scratch), a legacy one, or one last written for a DIFFERENT output directory — a page deleted since the tree was written stayed published, with its alias stubs and tag pages, on every later warm build. The Initialize phase takes the cold path instead when this is false. Asked before set_global_checksums, which re-stamps output_dir.

Source
update(file_path : String, output_path : String = "", cascade_hash : String = "", template_hash : String | Nil = nil, output_paths : Array(String) = [] of String, assets_hash : String = "", git_hash : String = "", derived_paths : Array(String) = [] of String, relations_hash : String = "", source_read : Tuple(Int64, Int64, String | Nil) | Nil = nil)

Update cache entry for a file. template_hash is the page's template closure fingerprint; nil stores the global templates checksum (non-page entries). output_paths are the secondary sibling output files this page emitted (see [outputs]); empty when the feature isn't in use. source_read is the page's Page#source_read: the entry then describes the bytes the render read, not whatever the file holds by now (#859). A racy read stores its own digest; a file whose stamp moved since the read stores a hash that never matches, so the next build re-renders it. Thread-safe: protected by mutex for concurrent parallel builds.

Source