108 lines
3.5 KiB
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 "" }
|