ZDDC/zddc/internal/apps/embed.go
ZDDC ed7a7fc9c0 perf(server): ETag + max-age=0 on embedded HTML responses
The apps subsystem previously sent Cache-Control: public, max-age=300|3600,
must-revalidate but no ETag. With must-revalidate and no validator, the
browser cannot return 304 — it has to refetch the full body once max-age
expires. For mdedit that's 920 KB on every reload after an hour.

Add a content-addressed ETag (sha256 hex prefix, 32 chars) to:
- apps/handler.go's serveBody + serveEmbedded (both paths now emit ETag
  + handle If-None-Match short-circuit to 304)
- handler/directory.go's embedded:browse fallback (mirror behavior so
  the bare-directory landing serves the same way)

Drop max-age to 0 with must-revalidate: every page load revalidates,
but a matching ETag returns 304 with empty body. Steady-state cost of
a reload drops from N KB to a few hundred bytes. When the binary is
redeployed, the ETag changes (content hash) and the next request
returns 200 with the new bytes.

Tests in apps/handler_test.go cover both paths:
- TestServer_Embedded_ConditionalGET: full GET, matching INM, stale INM
- TestEmbeddedETag_Stable: same bytes → same ETag, different → different

Live smoke (curl against zddc-server -root /tmp/empty):
  GET /            → 200, ETag set, body = 80919 bytes (landing.html)
  GET / + INM:tag  → 304 Not Modified, empty body
2026-05-04 07:49:17 -05:00

89 lines
2.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package apps
import (
"crypto/sha256"
"encoding/hex"
_ "embed"
"sync"
)
// Embedded fallback: the five tool HTMLs from the time the binary was
// built. Used as a last-resort served-bytes when (cache miss) AND
// (upstream unreachable) AND (no operator override) — see handler.go.
//
// The files are populated by the top-level build.sh, which copies the
// freshly-built dist/<tool>.html into ./embedded/ before `go build` runs.
// Empty placeholder files are checked in so the package compiles when no
// build has been run yet (CI bootstrap, fresh clone, etc.); at runtime an
// empty embedded body is treated as "no embedded fallback available."
//go:embed embedded/archive.html
var embeddedArchive []byte
//go:embed embedded/transmittal.html
var embeddedTransmittal []byte
//go:embed embedded/classifier.html
var embeddedClassifier []byte
//go:embed embedded/mdedit.html
var embeddedMdedit []byte
//go:embed embedded/index.html
var embeddedLanding []byte
//go:embed embedded/browse.html
var embeddedBrowse []byte
// EmbeddedBytes returns the embedded HTML for app, or nil if either app is
// not one of the canonical names or the embedded slot is empty (no build
// has populated it).
func EmbeddedBytes(app string) []byte {
var b []byte
switch app {
case "archive":
b = embeddedArchive
case "transmittal":
b = embeddedTransmittal
case "classifier":
b = embeddedClassifier
case "mdedit":
b = embeddedMdedit
case "landing":
b = embeddedLanding
case "browse":
b = embeddedBrowse
default:
return nil
}
if len(b) == 0 {
return nil
}
return b
}
// EmbeddedETag returns a strong ETag (sha256-hex prefix, 32 chars) for the
// app's embedded bytes. Computed lazily on first call per-app and memoized
// — the embedded slot is fixed for the binary's lifetime, so the ETag
// changes only when the binary is redeployed. Empty slot returns "".
//
// Used by apps.Server.serveEmbedded to issue conditional-GET-friendly
// responses: with this ETag + Cache-Control: max-age=0, must-revalidate,
// every page load revalidates and gets a 304 unless the binary has been
// updated. Saves re-transmitting 50920 KB tool HTMLs on every reload.
func EmbeddedETag(app string) string {
if v, ok := etagCacheByApp.Load(app); ok {
return v.(string)
}
body := EmbeddedBytes(app)
if body == nil {
return ""
}
sum := sha256.Sum256(body)
etag := hex.EncodeToString(sum[:])[:32]
etagCacheByApp.Store(app, etag)
return etag
}
// etagCacheByApp memoizes EmbeddedETag results keyed by app name.
var etagCacheByApp sync.Map