Files
lazysecrets/internal/ui/app/cache.go
T
kebandClaude Opus 5.5 db8ce90ee7
CI / Checks (pull_request) Successful in 39s
PR title / Conventional title (pull_request) Successful in 1s
fix: the details pane follows its secret's list when it reloads
A cached list is shown at once and reloaded in the background; the
selected secret's details stayed as first shown, old web page
included, until another secret was selected. When the reloaded list
has it changed, the details pane takes the new listing (a value shown
stays shown) and describes it afresh: its cached details came from
the old one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 14:50:59 +02:00

83 lines
2.7 KiB
Go

package app
import (
"maps"
"slices"
"time"
tea "charm.land/bubbletea/v2"
"git.kebcloud.no/keb/lazysecrets/internal/provider"
)
// cacheTTL is how long cached metadata is shown without reloading. Older
// entries are still shown immediately, then reloaded in the background.
const cacheTTL = 2 * time.Minute
// cache holds metadata: never secret values or errors. It's kept between
// runs in the cache file (persist.go).
type cache struct {
containers map[string]entry[[]provider.Container] // by source key
objects map[string]entry[[]provider.Object] // by container key (ID|type)
details map[string]entry[provider.Object] // Describe results by ID@version
// cursor remembers the selected child per parent key, so returning to
// a subscription or vault puts the cursor back where it was.
cursor map[string]string
}
type entry[T any] struct {
value T
at time.Time
}
func newCache() cache {
return cache{
containers: map[string]entry[[]provider.Container]{},
objects: map[string]entry[[]provider.Object]{},
details: map[string]entry[provider.Object]{},
cursor: map[string]string{},
}
}
func (e entry[T]) fresh(now time.Time) bool { return now.Sub(e.at) < cacheTTL }
// detailKey includes the account: the result carries its container, and
// later requests for the object are routed by it.
func detailKey(o provider.Object) string {
return containerKey(o.Container) + "|" + o.ID + "@" + o.Version
}
func (c cache) rememberCursor(parent, child string) {
if parent != "" && child != "" {
c.cursor[parent] = child
}
}
// refreshShown shows the selected secret's newer metadata when its list
// reloaded with it changed from before (a cached list refreshed in the
// background), and describes it afresh: its cached details came from
// the old listing.
func (m *Model) refreshShown(before []provider.Object) tea.Cmd {
s, ok := m.secrets.Selected()
if !ok || m.secrets.SelectedID() != m.selSecret {
return nil // a new selection is shown as usual
}
i := slices.IndexFunc(before, func(o provider.Object) bool { return o.ID == s.ID })
if i < 0 || sameListing(before[i], s) {
return nil
}
m.detail.Refresh(s)
delete(m.cache.details, detailKey(s))
return m.describe(s)
}
// sameListing reports whether a and b list the same: what a list
// says of an object, not what describing it adds.
func sameListing(a, b provider.Object) bool {
enabled := func(e *bool) bool { return e == nil || *e }
return a.Name == b.Name && a.Version == b.Version && enabled(a.Enabled) == enabled(b.Enabled) &&
a.Updated.Equal(b.Updated) && a.Expires.Equal(b.Expires) && a.ContentType == b.ContentType &&
maps.Equal(a.Tags, b.Tags) && a.Reference == b.Reference && a.URL == b.URL &&
slices.Equal(a.Attrs, b.Attrs)
}