diff --git a/pkg/app/cache.go b/pkg/app/cache.go index ee96b61c..bb8e15e7 100644 --- a/pkg/app/cache.go +++ b/pkg/app/cache.go @@ -1,25 +1,10 @@ package app import ( - "context" "sync" - "time" - - "github.com/maxence-charriere/go-app/v9/pkg/cache" ) -// PreRenderCache is the interface that describes a cache that stores -// pre-rendered resources. -type PreRenderCache interface { - // Get returns the item at the given path. - Get(ctx context.Context, path string) (PreRenderedItem, bool) - - // Set stored the item at the given path. - Set(ctx context.Context, i PreRenderedItem) -} - -// PreRenderedItem represent an item that is stored in a PreRenderCache. -type PreRenderedItem struct { +type cacheItem struct { // The request path. Path string @@ -29,69 +14,32 @@ type PreRenderedItem struct { // The response content encoding. ContentEncoding string - // The cache control. - CacheControl string - // The response body. Body []byte } -// Len return the body length. -func (r PreRenderedItem) Size() int { - return len(r.Body) -} - -// NewPreRenderLRUCache creates an in memory LRU cache that stores items for the -// given duration. If provided, on eviction functions are called when item are -// evicted. -func NewPreRenderLRUCache(size int, itemTTL time.Duration, onEvict ...func(path string, i PreRenderedItem)) PreRenderCache { - return &preRenderLRUCache{ - LRU: cache.LRU{ - MaxSize: size, - ItemTTL: itemTTL, - OnEvict: func(path string, i cache.Item) { - item := i.(PreRenderedItem) - for _, fn := range onEvict { - fn(path, item) - } - }, - }, - } - -} - -type preRenderLRUCache struct { - cache.LRU -} - -func (c *preRenderLRUCache) Get(ctx context.Context, path string) (PreRenderedItem, bool) { - i, ok := c.LRU.Get(ctx, path) - if !ok { - return PreRenderedItem{}, false - } - return i.(PreRenderedItem), true +func (i cacheItem) Len() int { + return len(i.Body) } -func (c *preRenderLRUCache) Set(ctx context.Context, i PreRenderedItem) { - c.LRU.Set(ctx, i.Path, i) -} - -type preRenderCache struct { +type memoryCache struct { mu sync.RWMutex - items map[string]PreRenderedItem + items map[string]cacheItem } -func newPreRenderCache(size int) *preRenderCache { - return &preRenderCache{ - items: make(map[string]PreRenderedItem, size), +func newMemoryCache(size int) *memoryCache { + return &memoryCache{ + items: make(map[string]cacheItem, size), } } -func (c *preRenderCache) Set(ctx context.Context, i PreRenderedItem) { + +func (c *memoryCache) Set(i cacheItem) { c.mu.Lock() c.items[i.Path] = i c.mu.Unlock() } -func (c *preRenderCache) Get(ctx context.Context, path string) (PreRenderedItem, bool) { + +func (c *memoryCache) Get(path string) (cacheItem, bool) { c.mu.Lock() i, ok := c.items[path] c.mu.Unlock() diff --git a/pkg/app/cache_test.go b/pkg/app/cache_test.go index 77ecc662..3e571b07 100644 --- a/pkg/app/cache_test.go +++ b/pkg/app/cache_test.go @@ -1,136 +1,27 @@ package app import ( - "context" "testing" - "time" "github.com/stretchr/testify/require" ) -func TestPreRenderLRUCache(t *testing.T) { - testPreRenderCache(t, NewPreRenderLRUCache(100, time.Second)) -} - -func TestPreRenderCache(t *testing.T) { - testPreRenderCache(t, newPreRenderCache(1)) -} - -func testPreRenderCache(t *testing.T, c PreRenderCache) { - ctx := context.TODO() +func TestMemoryCache(t *testing.T) { + c := newMemoryCache(5) - i := PreRenderedItem{ + i := cacheItem{ Path: "/test", ContentType: "text/html", ContentEncoding: "gzip", Body: []byte("test"), } - ic, ok := c.Get(ctx, i.Path) + ic, ok := c.Get(i.Path) require.Zero(t, ic) require.False(t, ok) - c.Set(ctx, i) - ic, ok = c.Get(ctx, i.Path) + c.Set(i) + ic, ok = c.Get(i.Path) require.True(t, ok) require.Equal(t, i, ic) } - -func TestPreRenderLRUCacheExpire(t *testing.T) { - ctx := context.TODO() - evictCalled := false - onEvict := func(string, PreRenderedItem) { evictCalled = true } - - c := NewPreRenderLRUCache(16, -time.Second, onEvict).(*preRenderLRUCache) - - items := []PreRenderedItem{ - { - Path: "/test1", - Body: []byte("test"), - }, - { - Path: "/test2", - Body: []byte("test"), - }, - { - Path: "/test3", - Body: []byte("test"), - }, - { - Path: "/test4", - Body: []byte("test"), - }, - } - - for _, i := range items { - c.Set(ctx, i) - } - require.Equal(t, 4, c.Len()) - require.Equal(t, 16, c.Size()) - - for _, i := range items { - ic, ok := c.Get(ctx, i.Path) - require.Zero(t, ic) - require.False(t, ok) - } - require.Equal(t, 4, c.Len()) - require.Equal(t, 16, c.Size()) - - c.Set(ctx, PreRenderedItem{ - Path: "/test5", - Body: []byte("test"), - }) - require.Equal(t, 1, c.Len()) - require.Equal(t, 4, c.Size()) - require.False(t, evictCalled) -} - -func TestPreRenderLRUCacheEvict(t *testing.T) { - ctx := context.TODO() - - evictCount := 0 - evictSize := 0 - onEvict := func(path string, i PreRenderedItem) { - evictCount++ - evictSize += i.Size() - } - - c := NewPreRenderLRUCache(8, time.Second, onEvict).(*preRenderLRUCache) - - items := []PreRenderedItem{ - { - Path: "/test1", - Body: []byte("test"), - }, - { - Path: "/test2", - Body: []byte("test"), - }, - } - - for _, i := range items { - c.Set(ctx, i) - } - require.Equal(t, 2, c.Len()) - require.Equal(t, 8, c.Size()) - require.Equal(t, 0, evictCount) - require.Equal(t, 0, evictSize) - - c.Set(ctx, PreRenderedItem{ - Path: "/test3", - Body: []byte("test"), - }) - require.Equal(t, 2, c.Len()) - require.Equal(t, 8, c.Size()) - require.Equal(t, 1, evictCount) - require.Equal(t, 4, evictSize) - - c.Set(ctx, PreRenderedItem{ - Path: "/test4", - Body: []byte("testbig"), - }) - require.Equal(t, 1, c.Len()) - require.Equal(t, 7, c.Size()) - require.Equal(t, 3, evictCount) - require.Equal(t, 12, evictSize) -} diff --git a/pkg/app/http.go b/pkg/app/http.go index 63104041..09df99fe 100644 --- a/pkg/app/http.go +++ b/pkg/app/http.go @@ -2,7 +2,6 @@ package app import ( "bytes" - "context" "crypto/sha1" "encoding/json" "fmt" @@ -152,15 +151,6 @@ type Handler struct { // DNS+TCP+TLS for HTTPS origins). Preconnect []string - // The cache that stores pre-rendered pages. - // - // Default: A LRU cache that keeps pages up to 24h and have a maximum size - // of 8MB. - PreRenderCache PreRenderCache - - // The Control-Cache header value for pre-rendered resources. - PreRenderCacheControl string - // The static resources that are accessible from custom paths. Files that // are proxied by default are /robots.txt, /sitemap.xml and /ads.txt. ProxyResources []ProxyResource @@ -196,10 +186,11 @@ type Handler struct { // worker template is not supported and will be closed. ServiceWorkerTemplate string - once sync.Once - etag string - pwaResources PreRenderCache - proxyResources map[string]ProxyResource + once sync.Once + etag string + proxyResources map[string]ProxyResource + cachedProxyResources *memoryCache + cachedPWAResources *memoryCache } func (h *Handler) init() { @@ -213,7 +204,7 @@ func (h *Handler) init() { h.initIcon() h.initPWA() h.initPageContent() - h.initPreRenderedResources() + h.initPWAResources() h.initProxyResources() } @@ -319,46 +310,38 @@ func (h *Handler) initPageContent() { } -func (h *Handler) initPreRenderedResources() { - h.pwaResources = newPreRenderCache(5) - ctx := context.TODO() +func (h *Handler) initPWAResources() { + h.cachedPWAResources = newMemoryCache(5) - h.pwaResources.Set(ctx, PreRenderedItem{ + h.cachedPWAResources.Set(cacheItem{ Path: "/wasm_exec.js", ContentType: "application/javascript", Body: []byte(wasmExecJS), }) - h.pwaResources.Set(ctx, PreRenderedItem{ + h.cachedPWAResources.Set(cacheItem{ Path: "/app.js", ContentType: "application/javascript", Body: h.makeAppJS(), }) - h.pwaResources.Set(ctx, PreRenderedItem{ + h.cachedPWAResources.Set(cacheItem{ Path: "/app-worker.js", ContentType: "application/javascript", Body: h.makeAppWorkerJS(), }) - h.pwaResources.Set(ctx, PreRenderedItem{ + h.cachedPWAResources.Set(cacheItem{ Path: "/manifest.webmanifest", ContentType: "application/manifest+json", Body: h.makeManifestJSON(), }) - h.pwaResources.Set(ctx, PreRenderedItem{ + h.cachedPWAResources.Set(cacheItem{ Path: "/app.css", ContentType: "text/css", Body: []byte(appCSS), }) - - if h.PreRenderCache == nil { - h.PreRenderCache = NewPreRenderLRUCache( - defaultPreRenderCacheSize, - defaultPreRenderCacheTTL, - ) - } } func (h *Handler) makeAppJS() []byte { @@ -494,6 +477,7 @@ func (h *Handler) makeManifestJSON() []byte { } func (h *Handler) initProxyResources() { + h.cachedProxyResources = newMemoryCache(len(h.ProxyResources)) resources := make(map[string]ProxyResource) for _, r := range h.ProxyResources { @@ -579,13 +563,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } - if res, ok := h.pwaResources.Get(r.Context(), path); ok { - h.servePreRenderedItem(w, res) - return - } - - if res, ok := h.PreRenderCache.Get(r.Context(), path); ok { - h.servePreRenderedItem(w, res) + if res, ok := h.cachedPWAResources.Get(path); ok { + h.serveCachedItem(w, res) return } @@ -597,18 +576,14 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.servePage(w, r) } -func (h *Handler) servePreRenderedItem(w http.ResponseWriter, i PreRenderedItem) { - w.Header().Set("Content-Length", strconv.Itoa(i.Size())) +func (h *Handler) serveCachedItem(w http.ResponseWriter, i cacheItem) { + w.Header().Set("Content-Length", strconv.Itoa(i.Len())) w.Header().Set("Content-Type", i.ContentType) if i.ContentEncoding != "" { w.Header().Set("Content-Encoding", i.ContentEncoding) } - if i.CacheControl != "" { - w.Header().Set("Cache-Control", i.CacheControl) - } - w.WriteHeader(http.StatusOK) w.Write(i.Body) } @@ -627,6 +602,11 @@ func (h *Handler) serveProxyResource(resource ProxyResource, w http.ResponseWrit u = h.Resources.Static() + resource.ResourcePath } + if i, ok := h.cachedProxyResources.Get(resource.Path); ok { + h.serveCachedItem(w, i) + return + } + res, err := http.Get(u) if err != nil { w.WriteHeader(http.StatusInternalServerError) @@ -657,14 +637,14 @@ func (h *Handler) serveProxyResource(resource ProxyResource, w http.ResponseWrit return } - item := PreRenderedItem{ + item := cacheItem{ Path: resource.Path, ContentType: res.Header.Get("Content-Type"), ContentEncoding: res.Header.Get("Content-Encoding"), Body: body, } - h.PreRenderCache.Set(r.Context(), item) - h.servePreRenderedItem(w, item) + h.cachedProxyResources.Set(item) + h.serveCachedItem(w, item) } func (h *Handler) servePage(w http.ResponseWriter, r *http.Request) { @@ -842,14 +822,10 @@ func (h *Handler) servePage(w http.ResponseWriter, r *http.Request) { body, )) - item := PreRenderedItem{ - Path: page.URL().Path, - Body: b.Bytes(), - ContentType: "text/html", - CacheControl: h.PreRenderCacheControl, - } - h.PreRenderCache.Set(r.Context(), item) - h.servePreRenderedItem(w, item) + w.Header().Set("Content-Length", strconv.Itoa(b.Len())) + w.Header().Set("Content-Type", "text/html") + w.WriteHeader(http.StatusOK) + w.Write(b.Bytes()) } func (h *Handler) resolvePackagePath(path string) string { diff --git a/pkg/app/state.go b/pkg/app/state.go index 7a536c35..42353438 100644 --- a/pkg/app/state.go +++ b/pkg/app/state.go @@ -494,8 +494,3 @@ type persistentState struct { func (s *persistentState) isExpired(now time.Time) bool { return s.ExpiresAt != time.Time{} && now.After(s.ExpiresAt) } - -type broadcastState struct { - StoreID string `json:""` - Value json.RawMessage `json:",omitempty"` -}