Add js web stuff to landing page + documentation
This commit is contained in:
192
go/jsbundler/ssrcache.go
Normal file
192
go/jsbundler/ssrcache.go
Normal file
@@ -0,0 +1,192 @@
|
||||
package jsbundler
|
||||
|
||||
// Build-time caching + parallelism for public-page SSR (see genssr.go).
|
||||
//
|
||||
// Rendering a page means bundling it with esbuild and running it through a goja
|
||||
// runtime — too slow to repeat for every page on every build when nothing
|
||||
// changed. So each render records the source files esbuild pulled in (from the
|
||||
// metafile) and their content hashes; a later build reuses the cached HTML when
|
||||
// the page's entry and every input file are byte-identical. Cache misses are
|
||||
// rendered concurrently, one goja runtime per worker.
|
||||
//
|
||||
// The cache lives under tmp/ (gitignored) and is purely an optimization: any
|
||||
// read/parse/write error just falls back to rendering.
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// ssrCachePath is the on-disk render cache (gitignored via tmp/).
|
||||
var ssrCachePath = filepath.Join("tmp", "ssr-cache.json")
|
||||
|
||||
// ssrCacheEntry fingerprints one rendered page.
|
||||
type ssrCacheEntry struct {
|
||||
EntryHash string `json:"entry"` // hash of the goja entry source
|
||||
Inputs map[string]string `json:"inputs"` // input file path -> content hash
|
||||
HTML string `json:"html"` // the rendered, baked body
|
||||
RenderJS string `json:"renderJS,omitempty"` // bundled JS, baked for dynamic (ISR) pages
|
||||
}
|
||||
|
||||
// ssrCache is the whole render cache, keyed by page URL path.
|
||||
type ssrCache struct {
|
||||
Engine string `json:"engine"` // EngineHash() at write time
|
||||
Pages map[string]ssrCacheEntry `json:"pages"`
|
||||
}
|
||||
|
||||
func loadSSRCache() ssrCache {
|
||||
data, err := os.ReadFile(ssrCachePath)
|
||||
if err != nil {
|
||||
return ssrCache{}
|
||||
}
|
||||
var c ssrCache
|
||||
if json.Unmarshal(data, &c) != nil || c.Pages == nil {
|
||||
return ssrCache{}
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func saveSSRCache(c ssrCache) {
|
||||
data, err := json.MarshalIndent(c, "", " ")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(ssrCachePath), 0o755); err != nil {
|
||||
return
|
||||
}
|
||||
_ = os.WriteFile(ssrCachePath, data, 0o644)
|
||||
}
|
||||
|
||||
func hashString(s string) string {
|
||||
sum := sha256.Sum256([]byte(s))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
// fileHasher memoizes content hashes within a single build so files shared by
|
||||
// several pages (the vendored Solid runtime, PublicLayout, shared UI) are read
|
||||
// and hashed once. Safe for concurrent use.
|
||||
type fileHasher struct {
|
||||
mu sync.Mutex
|
||||
m map[string]string // path -> hex hash; "" records a read failure
|
||||
}
|
||||
|
||||
func newFileHasher() *fileHasher { return &fileHasher{m: map[string]string{}} }
|
||||
|
||||
// hash returns the file's content hash and whether it was readable.
|
||||
func (f *fileHasher) hash(path string) (string, bool) {
|
||||
f.mu.Lock()
|
||||
if h, ok := f.m[path]; ok {
|
||||
f.mu.Unlock()
|
||||
return h, h != ""
|
||||
}
|
||||
f.mu.Unlock()
|
||||
|
||||
h := ""
|
||||
if data, err := os.ReadFile(path); err == nil {
|
||||
sum := sha256.Sum256(data)
|
||||
h = hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
f.mu.Lock()
|
||||
f.m[path] = h
|
||||
f.mu.Unlock()
|
||||
return h, h != ""
|
||||
}
|
||||
|
||||
// inputsUnchanged reports whether every recorded input still hashes the same.
|
||||
// A page can't gain a new dependency without editing one of these files, so an
|
||||
// all-match means the bundle (and thus the rendered output) is identical.
|
||||
func inputsUnchanged(old map[string]string, h *fileHasher) bool {
|
||||
if len(old) == 0 {
|
||||
return false
|
||||
}
|
||||
for path, want := range old {
|
||||
got, ok := h.hash(path)
|
||||
if !ok || got != want {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func hashInputs(paths []string, h *fileHasher) map[string]string {
|
||||
m := make(map[string]string, len(paths))
|
||||
for _, p := range paths {
|
||||
if sum, ok := h.hash(p); ok {
|
||||
m[p] = sum
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// renderJob / renderResult carry a cache-miss page through the worker pool.
|
||||
type renderJob struct {
|
||||
idx int
|
||||
path string
|
||||
component string
|
||||
entry string
|
||||
}
|
||||
|
||||
type renderResult struct {
|
||||
idx int
|
||||
path string
|
||||
html string
|
||||
js string // bundled render entry (baked for dynamic/ISR pages)
|
||||
inputs []string
|
||||
}
|
||||
|
||||
// renderMisses renders the given jobs concurrently — one goja runtime per
|
||||
// worker, capped at NumCPU — and returns their results in input order. On the
|
||||
// first render error it stops reporting and returns that error.
|
||||
func renderMisses(jobs []renderJob) ([]renderResult, error) {
|
||||
if len(jobs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
limit := runtime.NumCPU()
|
||||
if limit < 1 {
|
||||
limit = 1
|
||||
}
|
||||
if limit > len(jobs) {
|
||||
limit = len(jobs)
|
||||
}
|
||||
|
||||
results := make([]renderResult, len(jobs))
|
||||
sem := make(chan struct{}, limit)
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex
|
||||
var firstErr error
|
||||
|
||||
for k := range jobs {
|
||||
wg.Add(1)
|
||||
go func(k int) {
|
||||
defer wg.Done()
|
||||
sem <- struct{}{}
|
||||
defer func() { <-sem }()
|
||||
|
||||
j := jobs[k]
|
||||
html, js, inputs, err := RenderEntryFull(j.entry, ".")
|
||||
if err != nil {
|
||||
mu.Lock()
|
||||
if firstErr == nil {
|
||||
firstErr = fmt.Errorf("rendering %s (%s): %w", j.path, j.component, err)
|
||||
}
|
||||
mu.Unlock()
|
||||
return
|
||||
}
|
||||
results[k] = renderResult{idx: j.idx, path: j.path, html: html, js: js, inputs: inputs}
|
||||
}(k)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
if firstErr != nil {
|
||||
return nil, firstErr
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
Reference in New Issue
Block a user