Antonio Mika
·
2026-10-10
1package pgs
2
3import (
4 "context"
5 "fmt"
6 "io"
7 "log/slog"
8 "strings"
9 "sync"
10 "time"
11
12 "github.com/hashicorp/golang-lru/v2/expirable"
13 "github.com/picosh/pico/pkg/shared"
14 "github.com/picosh/utils/pipe"
15)
16
17type PicoPubsub interface {
18 io.Reader
19 io.Writer
20 io.Closer
21}
22
23type PubsubPipe struct {
24 Pipe *pipe.ReconnectReadWriteCloser
25}
26
27func (p *PubsubPipe) Read(b []byte) (int, error) {
28 return p.Pipe.Read(b)
29}
30
31func (p *PubsubPipe) Write(b []byte) (int, error) {
32 return p.Pipe.Write(b)
33}
34
35func (p *PubsubPipe) Close() error {
36 return p.Pipe.Close()
37}
38
39func NewPubsubPipe(pipe *pipe.ReconnectReadWriteCloser) *PubsubPipe {
40 return &PubsubPipe{
41 Pipe: pipe,
42 }
43}
44
45type PubsubChan struct {
46 Chan chan []byte
47}
48
49func (p *PubsubChan) Read(b []byte) (int, error) {
50 n := copy(b, <-p.Chan)
51 return n, nil
52}
53
54func (p *PubsubChan) Write(b []byte) (int, error) {
55 p.Chan <- b
56 return len(b), nil
57}
58
59func (p *PubsubChan) Close() error {
60 close(p.Chan)
61 return nil
62}
63
64func NewPubsubChan() *PubsubChan {
65 return &PubsubChan{
66 Chan: make(chan []byte),
67 }
68}
69
70func getSurrogateKey(userName, projectName string) string {
71 return fmt.Sprintf("%s-%s", userName, projectName)
72}
73
74func CreatePubCacheDrain(ctx context.Context, logger *slog.Logger) *pipe.ReconnectReadWriteCloser {
75 info := shared.NewPicoPipeClient()
76 send := pipe.NewReconnectReadWriteCloser(
77 ctx,
78 logger,
79 info,
80 "pub to cache-drain",
81 "pub cache-drain -b=false",
82 100,
83 -1,
84 )
85 return send
86}
87
88func CreateSubCacheDrain(ctx context.Context, logger *slog.Logger) *pipe.ReconnectReadWriteCloser {
89 info := shared.NewPicoPipeClient()
90 send := pipe.NewReconnectReadWriteCloser(
91 ctx,
92 logger,
93 info,
94 "sub to cache-drain",
95 "sub cache-drain -k",
96 100,
97 -1,
98 )
99 return send
100}
101
102// purgeCache send a pipe pub to the pgs web instance which purges
103// cached entries for a given subdomain (like "fakeuser-www-proj"). We set a
104// "surrogate-key: <subdomain>" header on every pgs response which ensures all
105// cached assets for a given subdomain are grouped under a single key (which is
106// separate from the "GET-https-example.com-/path" key used for serving files
107// from the cache).
108func purgeCache(cfg *Config, writer io.Writer, surrogate string) error {
109 cfg.Logger.Info("purging cache", "surrogate", surrogate)
110 _, err := writer.Write([]byte(surrogate + "\n"))
111 return err
112}
113
114func purgeAllCache(cfg *Config, writer io.Writer) error {
115 return purgeCache(cfg, writer, "*")
116}
117
118// SiteCache holds the http cache's responses and indexes their keys by site,
119// so purging one site doesn't scan the whole cache.
120type SiteCache struct {
121 *expirable.LRU[string, []byte]
122 mu sync.Mutex
123 sites map[string]map[string]struct{}
124}
125
126func NewSiteCache(size int, ttl time.Duration, onEvict func(key string, value []byte)) *SiteCache {
127 c := &SiteCache{sites: map[string]map[string]struct{}{}}
128 c.LRU = expirable.NewLRU(size, func(key string, value []byte) {
129 c.unindex(key)
130 if onEvict != nil {
131 onEvict(key, value)
132 }
133 }, ttl)
134 return c
135}
136
137// cacheKeySite returns the site a PgsCacheKey belongs to.
138func cacheKeySite(key string) string {
139 site, _, _ := strings.Cut(key, "__")
140 return site
141}
142
143func (c *SiteCache) Add(key string, value []byte) bool {
144 evicted := c.LRU.Add(key, value)
145 site := cacheKeySite(key)
146 c.mu.Lock()
147 defer c.mu.Unlock()
148 if c.sites[site] == nil {
149 c.sites[site] = map[string]struct{}{}
150 }
151 c.sites[site][key] = struct{}{}
152 return evicted
153}
154
155// PurgeSite removes every cached response for a site. The index lock is
156// released before touching the LRU, whose eviction callback takes it.
157func (c *SiteCache) PurgeSite(site string) {
158 c.mu.Lock()
159 keys := c.sites[site]
160 delete(c.sites, site)
161 c.mu.Unlock()
162 for key := range keys {
163 c.Remove(key)
164 }
165}
166
167func (c *SiteCache) unindex(key string) {
168 site := cacheKeySite(key)
169 c.mu.Lock()
170 defer c.mu.Unlock()
171 if keys, ok := c.sites[site]; ok {
172 delete(keys, key)
173 if len(keys) == 0 {
174 delete(c.sites, site)
175 }
176 }
177}