oficina / sheets Public

Clone
git clone https://amadan.net/oficina/sheets

Plain git — no account needed to clone.

Download

Download this file

1package sheets
2
3import (
4 "html/template"
5 "sync"
6)
7
8// previewCache holds rendered thumbnails in memory.
9//
10// **In memory only, never in a column.** The preview contains no cell
11// text — buildPreview cannot read one — so it is not plaintext even
12// before you seal it, and a column would be safe. The reason not to
13// have one is different and better: a preview column becomes a second
14// thing to remember to seal later, and the plaintext-grep gate that
15// guards the one rule structurally cannot catch it. A picture of a grid
16// contains no fixture words, so that test would pass while the
17// obligation quietly went unmet. An untestable obligation is the
18// argument, not disclosure. (Docs reached this first and it is theirs.)
19//
20// **Invalidation is by construction, not by a call.** An entry is a hit
21// only when the stored nanosecond timestamp equals the one on the row
22// the list just read, so any write that bumps UpdatedAt invalidates it
23// and there is no Forget() for a future write path to forget. Every
24// write path in this app already bumps it: the cell POST, the format
25// POST, import, and the two scriptless paths through publishReport.
26//
27// Two traps, both learned from Docs and both real:
28//
29// - Compare UnixNano int64s, NEVER time.Time with ==. A time parsed
30// out of SQLite and a time.Now().UTC() in memory carry different
31// monotonic and location fields, so == is never true and the cache
32// silently never hits — it goes on rendering every row on every
33// load and looks exactly like a cache that works.
34// - Take the timestamp from the LIST QUERY's row, not from a struct
35// that has been through a write path, which may not have written
36// the field back.
37type previewCache struct {
38 mu sync.RWMutex
39 at map[uint64]previewEntry
40}
41
42type previewEntry struct {
43 nanos int64
44 svg template.HTML
45}
46
47// maxPreviewEntries bounds the map.
48//
49// A cache with no bound is a slow leak keyed on however many sheets an
50// instance accumulates. The eviction is deliberately crude — past the
51// bound the map is dropped whole — because the alternative is an LRU
52// with a linked list, and the thing being protected is a few kilobytes
53// of markup that costs two indexed queries to rebuild.
54const maxPreviewEntries = 512
55
56func newPreviewCache() *previewCache {
57 return &previewCache{at: map[uint64]previewEntry{}}
58}
59
60// get returns the cached thumbnail for a sheet if it was rendered from
61// exactly this version of it.
62func (c *previewCache) get(sheetID uint64, nanos int64) (template.HTML, bool) {
63 if c == nil {
64 return "", false
65 }
66 c.mu.RLock()
67 e, ok := c.at[sheetID]
68 c.mu.RUnlock()
69 if !ok || e.nanos != nanos {
70 return "", false
71 }
72 return e.svg, true
73}
74
75func (c *previewCache) put(sheetID uint64, nanos int64, svg template.HTML) {
76 if c == nil {
77 return
78 }
79 c.mu.Lock()
80 defer c.mu.Unlock()
81 if len(c.at) >= maxPreviewEntries {
82 c.at = map[uint64]previewEntry{}
83 }
84 c.at[sheetID] = previewEntry{nanos: nanos, svg: svg}
85}
86
87// previewFor is what the list calls: cached if it can be, rendered if
88// it cannot.
89func (a *App) previewFor(s SheetSummary) template.HTML {
90 nanos := s.UpdatedAt.UnixNano()
91 if svg, ok := a.previews.get(s.ID, nanos); ok {
92 return svg
93 }
94 svg := previewSVG(a.buildPreview(s.TabID))
95 a.previews.put(s.ID, nanos, svg)
96 return svg
97}
98