blob: 9aad51a3d0a1bb3de869369ace75bb26f56dc9cd (
plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
|
;;; cj-cache-lib.el --- Generic TTL cache with build-guard -*- lexical-binding: t; -*-
;; Author: Craig Jennings <c@cjennings.net>
;;; Commentary:
;; Generic "rebuild a long-running computation behind a TTL" cache,
;; with a building-flag guard that prevents duplicate concurrent
;; rebuilds in async-build scenarios.
;;
;; Used by org-agenda-config and org-refile-config which previously
;; carried parallel hand-rolled implementations of this exact shape.
;; See docs/design/cache-helper-design.org for the API contract,
;; consumer migration shape, and rationale for the deliberate "nil
;; cached value reads as invalid" decision.
;;
;; Out of scope: buffer-local key-based caches like the modeline VC
;; cache (different lifecycle).
;;; Code:
(require 'cl-lib)
(defun cj/cache-make (&rest plist)
"Return a fresh cache state.
PLIST keywords:
- =:ttl= seconds to retain a built value (default 3600)."
(let ((ttl (or (plist-get plist :ttl) 3600)))
(list :value nil :time nil :ttl ttl :building nil)))
(defun cj/cache-valid-p (cache)
"Return non-nil when CACHE has a fresh, non-nil value within its TTL.
A nil cached value reads as invalid by design -- a build that legitimately
returns nil rebuilds on the next request, matching the prior agenda/refile
contract."
(let ((value (plist-get cache :value))
(time (plist-get cache :time))
(ttl (plist-get cache :ttl)))
(and value
time
(< (- (float-time) time) ttl))))
(defun cj/cache-building-p (cache)
"Return non-nil when a build is currently in progress on CACHE."
(plist-get cache :building))
(defun cj/cache-invalidate (cache)
"Clear CACHE's value and timestamp. TTL is preserved."
(plist-put cache :value nil)
(plist-put cache :time nil))
(cl-defun cj/cache-value-or-rebuild (cache build-fn
&key force-rebuild
on-hit
on-build-start
on-build-success
on-build-error)
"Return CACHE's value, calling BUILD-FN to rebuild when invalid.
When CACHE is valid and FORCE-REBUILD is nil, return the stored value
and call ON-HIT (if given) with the value. Otherwise call BUILD-FN,
store its result, and return it.
The four callbacks let the consumer log without this helper printing on
its behalf:
- ON-HIT (value)
- ON-BUILD-START ()
- ON-BUILD-SUCCESS (value)
- ON-BUILD-ERROR (err)
The :building flag is set before BUILD-FN runs and cleared inside an
`unwind-protect' regardless of outcome. Errors from BUILD-FN are
rethrown after ON-BUILD-ERROR fires."
(cond
((and (not force-rebuild) (cj/cache-valid-p cache))
(let ((value (plist-get cache :value)))
(when on-hit (funcall on-hit value))
value))
(t
(when on-build-start (funcall on-build-start))
(plist-put cache :building t)
(unwind-protect
(condition-case err
(let ((value (funcall build-fn)))
(plist-put cache :value value)
(plist-put cache :time (float-time))
(when on-build-success (funcall on-build-success value))
value)
(error
(when on-build-error (funcall on-build-error err))
(signal (car err) (cdr err))))
(plist-put cache :building nil)))))
(provide 'cj-cache-lib)
;;; cj-cache-lib.el ends here
|