//go:build js && wasm package rsc import ( "bytes" "encoding/gob" "syscall/js" "kjol/vdom" ) // Mount connects to a server component by name and returns a render function. // The generated client stub for a //gowasm:server component just calls this, so // the call site is identical to a client component. It POSTs {name, state, // event} to /rsc and swaps (reconciles) the returned render into the DOM. State // is opaque and round-trips through the client — the server keeps nothing. func Mount(name string) func() *vdom.VNode { tree := vdom.NewSignal[*vdom.VNode](nil) var state [][]byte var post func(kind string, nodeID int, event, value string) var convert func(*SNode) *vdom.VNode convert = func(sn *SNode) *vdom.VNode { if sn == nil { return nil } if sn.Tag == "" { return vdom.Text(sn.Text) } mods := make([]vdom.Mod, 0, len(sn.Attrs)+len(sn.Events)+len(sn.Kids)+1) for k, v := range sn.Attrs { mods = append(mods, vdom.Attr(k, v)) } if sn.HTML != "" { mods = append(mods, vdom.Raw(sn.HTML)) } for _, ev := range sn.Events { event, id := ev, sn.ID mods = append(mods, vdom.OnEvent(ev, func(e vdom.Event) { post("event", id, event, e.Value()) })) } for _, k := range sn.Kids { if c := convert(k); c != nil { mods = append(mods, c) } } return vdom.El(sn.Tag, mods...) } onArr := js.FuncOf(func(this js.Value, args []js.Value) any { arr := js.Global().Get("Uint8Array").New(args[0]) b := make([]byte, arr.Get("length").Int()) js.CopyBytesToGo(b, arr) var resp Response if gob.NewDecoder(bytes.NewReader(b)).Decode(&resp) == nil { state = resp.State tree.Set(convert(resp.Tree)) // re-render -> reconcile into the DOM } return nil }) onResp := js.FuncOf(func(this js.Value, args []js.Value) any { args[0].Call("arrayBuffer").Call("then", onArr) return nil }) loc := js.Global().Get("location") url := loc.Get("protocol").String() + "//" + loc.Get("host").String() + "/rsc" post = func(kind string, nodeID int, event, value string) { var buf bytes.Buffer if gob.NewEncoder(&buf).Encode(Request{Kind: kind, Name: name, State: state, NodeID: nodeID, Event: event, Value: value}) != nil { return } body := js.Global().Get("Uint8Array").New(buf.Len()) js.CopyBytesToJS(body, buf.Bytes()) opts := js.Global().Get("Object").New() opts.Set("method", "POST") opts.Set("body", body) js.Global().Call("fetch", url, opts).Call("then", onResp) } started := false return func() *vdom.VNode { if !started { started = true post("mount", 0, "", "") } if t := tree.Get(); t != nil { return t } return vdom.Div(vdom.Attr("class", "text-muted"), vdom.Text("loading server component…")) } }