diff options
Diffstat (limited to 'docs/design/cache-helper-design.org')
| -rw-r--r-- | docs/design/cache-helper-design.org | 165 |
1 files changed, 0 insertions, 165 deletions
diff --git a/docs/design/cache-helper-design.org b/docs/design/cache-helper-design.org deleted file mode 100644 index 5de0f348c..000000000 --- a/docs/design/cache-helper-design.org +++ /dev/null @@ -1,165 +0,0 @@ -#+TITLE: Cache Helper Design Addendum -#+AUTHOR: Craig Jennings -#+DATE: 2026-05-10 - -* Status - -Phase 5 design addendum to [[file:utility-consolidation.org][utility-consolidation.org]]. Specifies the cache API to extract before any code moves. - -* Problem - -Two modules carry a parallel cache implementation today: - -- =modules/org-agenda-config.el= caches the agenda file list. -- =modules/org-refile-config.el= caches refile targets. - -Both share the same shape (lines map between modules): - -| Element | Purpose | -|---------|---------| -| =VAR-cache= | the cached value | -| =VAR-cache-time= | float-time when last built | -| =VAR-cache-ttl= | seconds to retain (default 3600) | -| =VAR-building= | non-nil while an async build is in progress | -| =cj/build-VAR (&optional force-rebuild)= | "use cache if valid, else rebuild" | - -The build function does: - -1. Check whether cache is valid: cache and cache-time set, FORCE-REBUILD nil, age < TTL. -2. If valid: assign to the consumer's variable, log cache-hit, return. -3. If a background build is running: log "waiting...", continue (no second build). -4. Otherwise: set building flag, run the rebuild closure inside =condition-case=, on success update cache+time, on error log message; the =unwind-protect= clears the building flag. - -The consumer's variable (e.g. =org-agenda-files=, =org-refile-targets=) is updated as a side effect. - -A separate cache pattern exists in =modules/modeline-config.el= for VC info, but it's *buffer-local*, *key-based* (the file path), and not TTL-based. It will not migrate to the same helper -- see "Out of scope" below. - -* Goals - -- One source of truth for the TTL+building build pattern. -- Consumers shrink from ~30 lines of cache plumbing to a single =cj/cache-value-or-rebuild= call. -- Behavior preserved for agenda and refile. -- Tests cover rebuild / TTL hit / TTL miss / nil cache value / build error / building-flag cleanup. - -* Out of scope - -- Modeline VC cache (=cj/modeline-vc-cache-*=). Buffer-local + key-based + non-TTL invalidation. Different lifecycle. The spec calls this out explicitly: consider only after global cache behavior is stable. Defer to a future round. -- Generic memoization. We're not introducing function memoization or LRU. Just the specific "rebuild a long-running computation behind a TTL" pattern. - -* API - -** =cj/cache-make= - -#+begin_src emacs-lisp -(cj/cache-make &key ttl) -#+end_src - -Return a fresh cache state object. TTL is in seconds; defaults to 3600. - -The state is a plist with keys =:value=, =:time=, =:ttl=, =:building=. Consumers store the state in a single =defvar=. - -** =cj/cache-valid-p= - -#+begin_src emacs-lisp -(cj/cache-valid-p cache) -#+end_src - -Return non-nil when CACHE has a non-nil value, a non-nil time, and (now - time) < ttl. - -** =cj/cache-value-or-rebuild= - -#+begin_src emacs-lisp -(cj/cache-value-or-rebuild cache build-fn - &key force-rebuild - on-hit - on-build-start - on-build-success - on-build-error) -#+end_src - -The main entry point. Returns the cached value when valid (and FORCE-REBUILD is nil); otherwise calls BUILD-FN to compute a new value, updates CACHE, and returns the result. - -The four optional callbacks let the consumer log without the helper printing on its behalf: - -- =on-hit (value)= -- the helper found a valid cache. -- =on-build-start ()= -- about to call BUILD-FN. -- =on-build-success (value)= -- BUILD-FN returned cleanly. -- =on-build-error (err)= -- BUILD-FN signaled. After this fires the helper rethrows so the caller sees the error. - -The =:building= flag is set before BUILD-FN runs and cleared inside an =unwind-protect= regardless of outcome. The flag is exposed read-only via =cj/cache-building-p= so callers can log "build in progress" without poking at the plist directly. - -** =cj/cache-building-p= - -#+begin_src emacs-lisp -(cj/cache-building-p cache) -#+end_src - -Return non-nil when a build is currently in progress on CACHE. Used by callers that want to log "waiting for background build" before invoking =cj/cache-value-or-rebuild=. - -** =cj/cache-invalidate= - -#+begin_src emacs-lisp -(cj/cache-invalidate cache) -#+end_src - -Reset the cache to "no value, no time". TTL is preserved. - -* Consumer Shape (after migration) - -The agenda module's ~30 lines of cache plumbing become roughly: - -#+begin_src emacs-lisp -(defvar cj/--org-agenda-files-cache (cj/cache-make :ttl 3600)) - -(defun cj/build-org-agenda-list (&optional force-rebuild) - "..." - (interactive "P") - (when (cj/cache-building-p cj/--org-agenda-files-cache) - (cj/log-silently "Waiting for background agenda build to complete...")) - (let ((files - (cj/cache-value-or-rebuild - cj/--org-agenda-files-cache - (lambda () (cj/--scan-org-agenda-files)) - :force-rebuild force-rebuild - :on-hit (lambda (v) (cj/log-silently - "Using cached agenda files (%d files)" (length v))) - :on-build-start (lambda () (cj/log-silently - "Rebuilding agenda files (slow)...")) - :on-build-success (lambda (v) (cj/log-silently - "Built agenda files (%d files)" - (length v))) - :on-build-error (lambda (err) (cj/log-silently - "Agenda build failed: %s" err))))) - (setq org-agenda-files files) - files)) -#+end_src - -The =cj/--scan-org-agenda-files= helper holds the slow filesystem walk; the existing in-place expression body moves there with no behavior change. - -* Migration order - -Per the spec: agenda first, refile second. Each is its own commit: - -1. Add =modules/cj-cache.el= with the API and tests. No call-site changes. -2. Migrate =org-agenda-config.el= to the helper. Verify behavior with the existing async-cache tests. -3. Migrate =org-refile-config.el= the same way. - -* Testing - -For the helper itself, =tests/test-cj-cache.el=: - -- Normal: hit returns cached value; miss calls BUILD-FN. -- TTL: build at t=0, request at t=ttl-1 hits; request at t=ttl+1 rebuilds. -- FORCE-REBUILD wins over a valid cache. -- nil from BUILD-FN is stored (cache-valid-p returns t). This matches today's behavior -- a build that legitimately produces nil should not loop. *Decision point*: the current implementations actually treat "cache value nil" as "cache invalid" (line 131 of agenda, line 66 of refile both check =(and cache cache-time ...)=). Preserve that to avoid a behavior change: the new helper's =cj/cache-valid-p= treats a nil :value as "not valid". That's the safer default; consumers that need "nil is a real value" can migrate to a sentinel later. -- :building flag is set during BUILD-FN, cleared after success. -- :building flag is cleared even when BUILD-FN signals. -- Each callback fires once per appropriate path (hit / start / success / error). - -For the migrated consumers: the existing async-cache and rebuild tests run unchanged after the migration. No new test files for agenda/refile are required as part of Phase 5 itself; they got their tests when the original cache was added. - -* Risks - -- *Behavior drift on cache-hit logging.* The current code logs "Using cached agenda files (N files)" via =cj/log-silently=. The migration preserves that exact message via =:on-hit=. Verify by tail-ing =*Messages*= during a manual smoke test. -- *Building-flag leak.* The current code uses =unwind-protect= to clear =VAR-building= even on error. The helper does the same. The test "building flag cleared on error" pins this contract. -- *Async timer interaction.* Both modules schedule background builds via =run-with-idle-timer=. The migration leaves those scheduling forms in the consumer; only the cache-or-build core moves. No changes to startup timing. |
