package entities import ( "context" "log/slog" "time" ) // Refresher fetches entities from ha-gateway on startup (blocking, fail // loud — without an initial entity list nothing can resolve) and then // periodically in the background (non-blocking; a failed periodic refresh // only logs and keeps serving the last-known-good snapshot, since going // down over a transient ha-gateway hiccup would be worse than serving // slightly stale entity names). type Refresher struct { client *Client resolver *Resolver interval time.Duration log *slog.Logger } // NewRefresher constructs a Refresher. client fetches entities, resolver is // the snapshot it populates, interval controls the periodic refresh cadence. func NewRefresher(client *Client, resolver *Resolver, interval time.Duration, log *slog.Logger) *Refresher { return &Refresher{client: client, resolver: resolver, interval: interval, log: log} } // Start performs the blocking initial fetch, then launches a background // goroutine for periodic refreshes until ctx is cancelled. It returns an // error only from the initial fetch — the caller should fail startup on it. func (r *Refresher) Start(ctx context.Context) error { if err := r.refreshOnce(ctx); err != nil { return err } go r.loop(ctx) return nil } func (r *Refresher) refreshOnce(ctx context.Context) error { list, err := r.client.FetchAll(ctx) if err != nil { return err } r.resolver.Replace(list) return nil } func (r *Refresher) loop(ctx context.Context) { ticker := time.NewTicker(r.interval) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: if err := r.refreshOnce(ctx); err != nil { r.log.Error("periodic entity refresh failed, keeping stale data", "err", err) } } } }