phase 0-2 complete
This commit is contained in:
134
internal/server/store.go
Normal file
134
internal/server/store.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/blakeridgway/heloha/internal/radar"
|
||||
)
|
||||
|
||||
const maxFrames = 12
|
||||
|
||||
type productKey struct{ site, product string }
|
||||
|
||||
type frameRing struct {
|
||||
frames [maxFrames]*radar.RadarProduct
|
||||
head int // next write slot
|
||||
count int // filled slots (0..maxFrames)
|
||||
}
|
||||
|
||||
func (r *frameRing) push(p *radar.RadarProduct) {
|
||||
// Deduplicate: skip if same or older scan time as the newest stored frame.
|
||||
if r.count > 0 {
|
||||
newest := r.frames[(r.head-1+maxFrames)%maxFrames]
|
||||
if !newest.Time.Before(p.Time) {
|
||||
return
|
||||
}
|
||||
}
|
||||
r.frames[r.head] = p
|
||||
r.head = (r.head + 1) % maxFrames
|
||||
if r.count < maxFrames {
|
||||
r.count++
|
||||
}
|
||||
}
|
||||
|
||||
// get returns the frame at index i where 0=oldest, count-1=newest.
|
||||
func (r *frameRing) get(i int) *radar.RadarProduct {
|
||||
if i < 0 || i >= r.count {
|
||||
return nil
|
||||
}
|
||||
slot := (r.head - r.count + i + maxFrames) % maxFrames
|
||||
return r.frames[slot]
|
||||
}
|
||||
|
||||
// RadarStore holds multi-site, multi-product frame history.
|
||||
type RadarStore struct {
|
||||
mu sync.RWMutex
|
||||
rings map[productKey]*frameRing
|
||||
}
|
||||
|
||||
func NewRadarStore() *RadarStore {
|
||||
return &RadarStore{rings: make(map[productKey]*frameRing)}
|
||||
}
|
||||
|
||||
func (s *RadarStore) Set(site, product string, p *radar.RadarProduct) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
k := productKey{site: site, product: product}
|
||||
if s.rings[k] == nil {
|
||||
s.rings[k] = &frameRing{}
|
||||
}
|
||||
s.rings[k].push(p)
|
||||
}
|
||||
|
||||
// GetLatest returns the newest frame for a given site and product, or nil.
|
||||
func (s *RadarStore) GetLatest(site, product string) *radar.RadarProduct {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
k := productKey{site: site, product: product}
|
||||
r := s.rings[k]
|
||||
if r == nil || r.count == 0 {
|
||||
return nil
|
||||
}
|
||||
return r.get(r.count - 1)
|
||||
}
|
||||
|
||||
// GetAllLatest returns the newest frame for every site for the given product.
|
||||
func (s *RadarStore) GetAllLatest(product string) []*radar.RadarProduct {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
out := make([]*radar.RadarProduct, 0, 16)
|
||||
for k, r := range s.rings {
|
||||
if k.product == product && r.count > 0 {
|
||||
out = append(out, r.get(r.count-1))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// GetAllAtFrame returns one frame per site at the given age index (0=oldest, N-1=newest).
|
||||
func (s *RadarStore) GetAllAtFrame(product string, frameIdx int) []*radar.RadarProduct {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
out := make([]*radar.RadarProduct, 0, 16)
|
||||
for k, r := range s.rings {
|
||||
if k.product != product {
|
||||
continue
|
||||
}
|
||||
// Map frameIdx relative to this ring's count.
|
||||
// frameIdx 0 = oldest across all rings → use offset from back.
|
||||
p := r.get(frameIdx)
|
||||
if p != nil {
|
||||
out = append(out, p)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// FrameInfo is one entry in the frames API response.
|
||||
type FrameInfo struct {
|
||||
Index int `json:"index"`
|
||||
Time time.Time `json:"time"`
|
||||
AgeSeconds int `json:"age_seconds"`
|
||||
}
|
||||
|
||||
// FrameMeta returns frame metadata ordered oldest→newest using KTLX as the reference.
|
||||
func (s *RadarStore) FrameMeta(product string) (int, []FrameInfo) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
r := s.rings[productKey{site: "ktlx", product: product}]
|
||||
if r == nil || r.count == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
now := time.Now()
|
||||
out := make([]FrameInfo, r.count)
|
||||
for i := 0; i < r.count; i++ {
|
||||
p := r.get(i)
|
||||
out[i] = FrameInfo{
|
||||
Index: i,
|
||||
Time: p.Time,
|
||||
AgeSeconds: int(now.Sub(p.Time).Seconds()),
|
||||
}
|
||||
}
|
||||
return r.count, out
|
||||
}
|
||||
Reference in New Issue
Block a user