orchard
mirrorEvery site I host, in one repo, along with the Cloudflare Tunnel and Caddy that front them. It's all Go, Vite, and SQLite, and it runs on a desktop at home with nothing listening on an inbound port.
blogbuncaddycloudflare-tunneldockergogolanghomelabhtml-templatemonorepoself-hostedseosqlitestatic-sitetypstuptime-monitoringviteweb-analytics
1package tools
2
3import "sync"
4
5// A widget is the structured half of an answer. The model still writes prose
6// about a ticker or a forecast, but the numbers behind it read far better as a
7// chart than as a sentence, and the model is not good at either drawing one or
8// at reciting fourteen figures without dropping one.
9//
10// What is recorded here is the subject and not the data. The frontend fetches
11// the readings itself from /api/widget/..., so switching a chart from a day to
12// a year does not need another turn, and reopening a week old conversation
13// draws today's price rather than the one that was on screen when it was asked.
14type Widget struct {
15 Kind string `json:"kind"`
16
17 // ticker
18 Symbol string `json:"symbol,omitempty"`
19
20 // weather. Zip is what pollen.com is keyed on and Country decides whether
21 // there is any pollen to ask for at all.
22 Place string `json:"place,omitempty"`
23 Lat float64 `json:"lat,omitempty"`
24 Lon float64 `json:"lon,omitempty"`
25 Zip string `json:"zip,omitempty"`
26 Country string `json:"country,omitempty"`
27
28 Label string `json:"label,omitempty"`
29}
30
31// Sink collects the widgets one turn produced. A turn runs its tools on
32// goroutines, so this locks, and it is created per turn and hung on the Deps
33// copy rather than on the shared one, for the same reason the session is.
34type Sink struct {
35 mu sync.Mutex
36 list []Widget
37 seen map[string]bool
38}
39
40func NewSink() *Sink { return &Sink{seen: map[string]bool{}} }
41
42// Add ignores a repeat. A model that asks for VTI twice in one turn should not
43// get two identical charts stacked up.
44func (s *Sink) Add(w Widget) {
45 if s == nil {
46 return
47 }
48 s.mu.Lock()
49 defer s.mu.Unlock()
50 key := w.Kind + "\x00" + w.Symbol + "\x00" + w.Place
51 if s.seen[key] {
52 return
53 }
54 s.seen[key] = true
55 s.list = append(s.list, w)
56}
57
58func (s *Sink) List() []Widget {
59 if s == nil {
60 return nil
61 }
62 s.mu.Lock()
63 defer s.mu.Unlock()
64 return append([]Widget(nil), s.list...)
65}