Merge branch 'mai/knuth/phase-5b-cache' (phase 5b slice C: timelineCache → cache.TTLCache)
This commit is contained in:
@@ -393,7 +393,7 @@ func (s *Server) handleCalDAVTodoAction(w http.ResponseWriter, r *http.Request,
|
||||
s.dashboard.InvalidateAll()
|
||||
}
|
||||
if s.timeline != nil {
|
||||
s.timeline.invalidateAll()
|
||||
s.timeline.InvalidateAll()
|
||||
}
|
||||
// Always re-render the tasks section so HTMX (or a plain redirect for
|
||||
// non-HTMX clients) sees the post-write state.
|
||||
|
||||
@@ -634,7 +634,7 @@ func (s *Server) dashboardTaskWrite(w http.ResponseWriter, r *http.Request, acti
|
||||
return
|
||||
}
|
||||
s.dashboard.InvalidateAll()
|
||||
s.timeline.invalidateAll()
|
||||
s.timeline.InvalidateAll()
|
||||
// Re-render whichever surface the request came from. HTMX sets HX-Target
|
||||
// to the swap target's id; the timeline surface uses #timeline-section.
|
||||
// Non-HTMX clients fall through to the dashboard re-render.
|
||||
|
||||
@@ -51,7 +51,7 @@ func (s *Server) handleLinksAdd(w http.ResponseWriter, r *http.Request, path str
|
||||
}
|
||||
}
|
||||
// New dated link → bust the timeline cache so the row surfaces on next view.
|
||||
s.timeline.invalidateAll()
|
||||
s.timeline.InvalidateAll()
|
||||
s.renderDocumentsSection(w, r, it, nil, banner)
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ func (s *Server) handleLinksRemove(w http.ResponseWriter, r *http.Request, path
|
||||
// Bust the dashboard + timeline caches: a removed dated link should
|
||||
// disappear from both surfaces on next render.
|
||||
s.dashboard.InvalidateAll()
|
||||
s.timeline.invalidateAll()
|
||||
s.timeline.InvalidateAll()
|
||||
// When the delete came from the timeline (HX-Target = timeline-section),
|
||||
// re-render the timeline so the row vanishes in place instead of trying to
|
||||
// swap a Documents fragment into it.
|
||||
|
||||
@@ -44,7 +44,7 @@ type Server struct {
|
||||
MCP http.Handler // nil → /mcp/ routes return 404 (off cleanly)
|
||||
Version string // build-time -ldflags injection; surfaced on /admin
|
||||
dashboard *cache.TTLCache[*dashboardPayload]
|
||||
timeline *timelineCache
|
||||
timeline *cache.TTLCache[*TimelinePayload]
|
||||
adminHealth *adminHealthCache
|
||||
}
|
||||
|
||||
@@ -267,7 +267,7 @@ func New(s *store.Store, logger *slog.Logger) (*Server, error) {
|
||||
pages: pages,
|
||||
Logger: logger,
|
||||
dashboard: cache.NewTTL[*dashboardPayload](dashboardCacheTTL),
|
||||
timeline: newTimelineCache(timelineCacheTTL),
|
||||
timeline: cache.NewTTL[*TimelinePayload](timelineCacheTTL),
|
||||
adminHealth: newAdminHealthCache(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/m/projax/internal/aggregate"
|
||||
@@ -13,7 +12,8 @@ import (
|
||||
)
|
||||
|
||||
// Timeline cache TTL — looser than the dashboard's 60s because /timeline is
|
||||
// browse-y rather than action-y. Per filter+window key.
|
||||
// browse-y rather than action-y. Per filter+window key. Phase 5b unified
|
||||
// onto internal/cache.TTLCache[V].
|
||||
const timelineCacheTTL = 90 * time.Second
|
||||
|
||||
// timelineKindTodo / Event / Doc / Creation are the four filterable row
|
||||
@@ -33,57 +33,6 @@ const (
|
||||
timelineDefaultFutureDays = 90
|
||||
)
|
||||
|
||||
// timelineCache holds aggregated payloads per (filter, window, order) key.
|
||||
type timelineCache struct {
|
||||
ttl time.Duration
|
||||
mu sync.Mutex
|
||||
rows map[string]cachedTimeline
|
||||
}
|
||||
|
||||
type cachedTimeline struct {
|
||||
at time.Time
|
||||
payload *TimelinePayload
|
||||
}
|
||||
|
||||
func newTimelineCache(ttl time.Duration) *timelineCache {
|
||||
return &timelineCache{ttl: ttl, rows: map[string]cachedTimeline{}}
|
||||
}
|
||||
|
||||
func (c *timelineCache) get(key string) (*TimelinePayload, bool) {
|
||||
if c == nil {
|
||||
return nil, false
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
v, ok := c.rows[key]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
if time.Since(v.at) > c.ttl {
|
||||
delete(c.rows, key)
|
||||
return nil, false
|
||||
}
|
||||
return v.payload, true
|
||||
}
|
||||
|
||||
func (c *timelineCache) set(key string, p *TimelinePayload) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.rows[key] = cachedTimeline{at: time.Now(), payload: p}
|
||||
}
|
||||
|
||||
func (c *timelineCache) invalidateAll() {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.rows = map[string]cachedTimeline{}
|
||||
}
|
||||
|
||||
// TimelineRow is one entry on the chronological spine. Re-exported from
|
||||
// internal/aggregate so MCP + web share one row shape. (Pre-Phase-5a
|
||||
// web/timeline.go defined its own copy; the aggregator package now owns
|
||||
@@ -224,16 +173,16 @@ func (s *Server) handleTimeline(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
key := q.cacheKey()
|
||||
if r.URL.Query().Get("refresh") == "1" {
|
||||
s.timeline.invalidateAll()
|
||||
s.timeline.InvalidateAll()
|
||||
}
|
||||
payload, hit := s.timeline.get(key)
|
||||
payload, hit := s.timeline.Get(key)
|
||||
if !hit {
|
||||
built, err := s.buildTimeline(r.Context(), q, now)
|
||||
if err != nil {
|
||||
s.fail(w, r, err)
|
||||
return
|
||||
}
|
||||
s.timeline.set(key, built)
|
||||
s.timeline.Set(key, built)
|
||||
payload = built
|
||||
}
|
||||
display := *payload
|
||||
|
||||
Reference in New Issue
Block a user