Files
kjol/go/rsc/server.go

108 lines
3.5 KiB
Go

//go:build !(js && wasm)
package rsc
import (
"encoding/gob"
"net/http"
"sort"
"sync"
"kjol/vdom"
)
// registry maps a server-component name to its factory (created by generated
// code — one Register per //gowasm:server component). The factory builds the
// component's signals + render closure; here it's re-run per request (stateless).
var registry = map[string]func() func() *vdom.VNode{}
func Register(name string, factory func() func() *vdom.VNode) { registry[name] = factory }
// renderMu serializes server renders because the vdom signal Collector is a
// process-global (see vdom.BeginCollect). Fine for this scale.
var renderMu sync.Mutex
// Handler is the single /rsc endpoint. It restores the component's signals from
// the request, applies the event (if any), re-renders, and returns the new
// signal snapshot + tree.
func Handler(w http.ResponseWriter, r *http.Request) {
var req Request
if gob.NewDecoder(r.Body).Decode(&req) != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
factory := registry[req.Name]
if factory == nil {
http.NotFound(w, r)
return
}
renderMu.Lock()
c := vdom.BeginCollect(req.State) // restore prior signal values (nil on mount)
render := factory() // signals created + restored here
vdom.EndCollect()
if req.Kind == "event" {
// Reproduce the tree the client currently shows (same state) to find the
// handler by node id, then invoke it (mutating signals).
_, handlers := renderIDs(render())
if hm := handlers[req.NodeID]; hm != nil {
if h := hm[req.Event]; h != nil {
h(serverEvent{value: req.Value})
}
}
}
tree, _ := renderIDs(render()) // render the result (reflects the mutation)
resp := Response{State: c.Snapshot(), Tree: tree}
renderMu.Unlock()
w.Header().Set("Content-Type", "application/octet-stream")
_ = gob.NewEncoder(w).Encode(resp)
}
// renderIDs serializes a VNode tree to SNode, assigning a stable (traversal
// order) id to each node with handlers and building the handler table.
func renderIDs(root *vdom.VNode) (*SNode, map[int]map[string]func(vdom.Event)) {
handlers := map[int]map[string]func(vdom.Event){}
id := 0
var walk func(*vdom.VNode) *SNode
walk = func(v *vdom.VNode) *SNode {
sn := &SNode{Tag: v.Tag, Text: v.Text, HTML: v.HTML, Attrs: v.Attrs}
if len(v.Events) > 0 {
id++
sn.ID = id
hm := map[string]func(vdom.Event){}
for ev, h := range v.Events {
sn.Events = append(sn.Events, ev)
hm[ev] = h
}
sort.Strings(sn.Events)
handlers[id] = hm
}
if v.HTML == "" {
for _, k := range v.Children {
sn.Kids = append(sn.Kids, walk(k))
}
}
return sn
}
return walk(root), handlers
}
// serverEvent is the vdom.Event a server component sees when the client replays a
// handler invocation. Only the target's value survives the round trip (it is all
// the client sends); everything else — key, pointer coordinates, the DOM target,
// the DataTransfer — is meaningless on the server and reads as its zero value.
type serverEvent struct{ value string }
func (e serverEvent) PreventDefault() {}
func (e serverEvent) StopPropagation() {}
func (e serverEvent) Value() string { return e.value }
func (e serverEvent) Checked() bool { return false }
func (e serverEvent) Key() string { return "" }
func (e serverEvent) ClientX() int { return 0 }
func (e serverEvent) ClientY() int { return 0 }
func (e serverEvent) Target() any { return nil }
func (e serverEvent) SetData(_, _ string) {}
func (e serverEvent) GetData(string) string { return "" }