Skip to content

Commit ac85035

Browse files
committed
cleanup and document rendering pipeline (render.go / render.md). remove some unused types, fix some weirdness
1 parent dda17b4 commit ac85035

10 files changed

Lines changed: 612 additions & 394 deletions

File tree

tsunami/app/root_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ func Button(ctx context.Context, props map[string]any) any {
6060
}
6161

6262
func printVDom(root *engine.RootElem) {
63-
vd := root.MakeVDom()
63+
vd := root.MakeRendered()
6464
jsonBytes, _ := json.MarshalIndent(vd, "", " ")
6565
fmt.Printf("%s\n", string(jsonBytes))
6666
}

tsunami/engine/clientimpl.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -222,8 +222,8 @@ func (c *ClientImpl) GetAtomVal(name string) any {
222222
return c.Root.GetAtomVal(name)
223223
}
224224

225-
func makeNullVDom() *vdom.VDomElem {
226-
return &vdom.VDomElem{WaveId: uuid.New().String(), Tag: vdom.WaveNullTag}
225+
func makeNullRendered() *rpctypes.RenderedElem {
226+
return &rpctypes.RenderedElem{WaveId: uuid.New().String(), Tag: vdom.WaveNullTag}
227227
}
228228

229229
func structToProps(props any) map[string]any {
@@ -258,9 +258,9 @@ func (c *ClientImpl) fullRender() (*rpctypes.VDomBackendUpdate, error) {
258258
opts := &RenderOpts{Resync: true}
259259
c.Root.RunWork(opts)
260260
c.Root.Render(c.RootElem, opts)
261-
renderedVDom := c.Root.MakeVDom()
261+
renderedVDom := c.Root.MakeRendered()
262262
if renderedVDom == nil {
263-
renderedVDom = makeNullVDom()
263+
renderedVDom = makeNullRendered()
264264
}
265265
return &rpctypes.VDomBackendUpdate{
266266
Type: "backendupdate",
@@ -279,9 +279,9 @@ func (c *ClientImpl) fullRender() (*rpctypes.VDomBackendUpdate, error) {
279279
func (c *ClientImpl) incrementalRender() (*rpctypes.VDomBackendUpdate, error) {
280280
opts := &RenderOpts{Resync: false}
281281
c.Root.RunWork(opts)
282-
renderedVDom := c.Root.MakeVDom()
282+
renderedVDom := c.Root.MakeRendered()
283283
if renderedVDom == nil {
284-
renderedVDom = makeNullVDom()
284+
renderedVDom = makeNullRendered()
285285
}
286286
return &rpctypes.VDomBackendUpdate{
287287
Type: "backendupdate",

tsunami/engine/comp.go

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,30 @@ type ChildKey struct {
1414
Key string
1515
}
1616

17+
// ComponentImpl represents a node in the persistent shadow component tree.
18+
// This is Tsunami's equivalent to React's Fiber nodes - it maintains component
19+
// identity, state, and lifecycle across renders while the VDomElem input/output
20+
// structures are ephemeral.
1721
type ComponentImpl struct {
18-
WaveId string
19-
Tag string
20-
Key string
21-
Elem *vdom.VDomElem
22-
Mounted bool
22+
WaveId string // Unique identifier for this component instance
23+
Tag string // Component type (HTML tag, custom component name, "#text", etc.)
24+
Key string // User-provided key for reconciliation (like React keys)
25+
Elem *vdom.VDomElem // Reference to the current input VDomElem being rendered
26+
Mounted bool // Whether this component is currently mounted
2327

24-
// hooks
25-
Hooks []*Hook
28+
// Hooks system (React-like)
29+
Hooks []*Hook // Array of hooks (state, effects, etc.) attached to this component
2630

27-
// #text component
28-
Text string
31+
// Component content - exactly ONE of these patterns is used:
2932

30-
// base component -- vdom, wave elem, or #fragment
31-
Children []*ComponentImpl
33+
// Pattern 1: Text nodes
34+
Text string // For "#text" components - stores the actual text content
3235

33-
// component -> component
34-
Comp *ComponentImpl
36+
// Pattern 2: Base/DOM elements with children
37+
Children []*ComponentImpl // For HTML tags, fragments - array of child components
38+
39+
// Pattern 3: Custom components that render to other components
40+
RenderedComp *ComponentImpl // For custom components - points to what this component rendered to
3541
}
3642

3743
func (c *ComponentImpl) compMatch(tag string, key string) bool {

tsunami/engine/render.go

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
// Copyright 2025, Command Line Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package engine
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"reflect"
10+
"unicode"
11+
12+
"github.com/google/uuid"
13+
"github.com/wavetermdev/waveterm/tsunami/rpctypes"
14+
"github.com/wavetermdev/waveterm/tsunami/util"
15+
"github.com/wavetermdev/waveterm/tsunami/vdom"
16+
)
17+
18+
// see render.md for a complete guide to how tsunami rendering, lifecycle, and reconciliation works
19+
20+
type RenderOpts struct {
21+
Resync bool
22+
}
23+
24+
func (r *RootElem) Render(elem *vdom.VDomElem, opts *RenderOpts) {
25+
r.render(elem, &r.Root, opts)
26+
}
27+
28+
func getElemKey(elem *vdom.VDomElem) string {
29+
if elem == nil {
30+
return ""
31+
}
32+
keyVal, ok := elem.Props[vdom.KeyPropKey]
33+
if !ok {
34+
return ""
35+
}
36+
return fmt.Sprint(keyVal)
37+
}
38+
39+
func (r *RootElem) render(elem *vdom.VDomElem, comp **ComponentImpl, opts *RenderOpts) {
40+
if elem == nil || elem.Tag == "" {
41+
r.unmount(comp)
42+
return
43+
}
44+
elemKey := getElemKey(elem)
45+
if *comp == nil || !(*comp).compMatch(elem.Tag, elemKey) {
46+
r.unmount(comp)
47+
r.createComp(elem.Tag, elemKey, comp)
48+
}
49+
(*comp).Elem = elem
50+
if elem.Tag == vdom.TextTag {
51+
// Pattern 1: Text Nodes
52+
r.renderText(elem.Text, comp)
53+
return
54+
}
55+
if isBaseTag(elem.Tag) {
56+
// Pattern 2: Base elements
57+
r.renderSimple(elem, comp, opts)
58+
return
59+
}
60+
cfunc := r.CFuncs[elem.Tag]
61+
if cfunc == nil {
62+
text := fmt.Sprintf("<%s>", elem.Tag)
63+
r.renderText(text, comp)
64+
return
65+
}
66+
// Pattern 3: components
67+
r.renderComponent(cfunc, elem, comp, opts)
68+
}
69+
70+
// Pattern 1
71+
func (r *RootElem) renderText(text string, comp **ComponentImpl) {
72+
// No need to clear Children/Comp - text components cannot have them
73+
if (*comp).Text != text {
74+
(*comp).Text = text
75+
}
76+
}
77+
78+
// Pattern 2
79+
func (r *RootElem) renderSimple(elem *vdom.VDomElem, comp **ComponentImpl, opts *RenderOpts) {
80+
if (*comp).RenderedComp != nil {
81+
// Clear Comp since base elements don't use it
82+
r.unmount(&(*comp).RenderedComp)
83+
}
84+
(*comp).Children = r.renderChildren(elem.Children, (*comp).Children, opts)
85+
}
86+
87+
// Pattern 3
88+
func (r *RootElem) renderComponent(cfunc any, elem *vdom.VDomElem, comp **ComponentImpl, opts *RenderOpts) {
89+
if (*comp).Children != nil {
90+
// Clear Children since custom components don't use them
91+
for _, child := range (*comp).Children {
92+
r.unmount(&child)
93+
}
94+
(*comp).Children = nil
95+
}
96+
props := make(map[string]any)
97+
for k, v := range elem.Props {
98+
props[k] = v
99+
}
100+
props[ChildrenPropKey] = elem.Children
101+
vc := makeContextVal(r, *comp, opts)
102+
rtnElemArr := withGlobalCtx(vc, func() []vdom.VDomElem {
103+
ctx := r.OuterCtx
104+
if ctx == nil {
105+
ctx = context.Background()
106+
}
107+
renderedElem := callCFunc(cfunc, ctx, props)
108+
return vdom.ToElems(renderedElem)
109+
})
110+
var rtnElem *vdom.VDomElem
111+
if len(rtnElemArr) == 0 {
112+
rtnElem = nil
113+
} else if len(rtnElemArr) == 1 {
114+
rtnElem = &rtnElemArr[0]
115+
} else {
116+
rtnElem = &vdom.VDomElem{Tag: vdom.FragmentTag, Children: rtnElemArr}
117+
}
118+
r.render(rtnElem, &(*comp).RenderedComp, opts)
119+
}
120+
121+
func (r *RootElem) unmount(comp **ComponentImpl) {
122+
if *comp == nil {
123+
return
124+
}
125+
waveId := (*comp).WaveId
126+
for _, hook := range (*comp).Hooks {
127+
if hook.UnmountFn != nil {
128+
hook.UnmountFn()
129+
}
130+
}
131+
if (*comp).RenderedComp != nil {
132+
r.unmount(&(*comp).RenderedComp)
133+
}
134+
if (*comp).Children != nil {
135+
for _, child := range (*comp).Children {
136+
r.unmount(&child)
137+
}
138+
}
139+
delete(r.CompMap, waveId)
140+
r.cleanupUsedByForUnmount(waveId)
141+
*comp = nil
142+
}
143+
144+
func (r *RootElem) createComp(tag string, key string, comp **ComponentImpl) {
145+
*comp = &ComponentImpl{WaveId: uuid.New().String(), Tag: tag, Key: key}
146+
r.CompMap[(*comp).WaveId] = *comp
147+
}
148+
149+
// handles reconcilation
150+
// maps children via key or index (exclusively)
151+
func (r *RootElem) renderChildren(elems []vdom.VDomElem, curChildren []*ComponentImpl, opts *RenderOpts) []*ComponentImpl {
152+
newChildren := make([]*ComponentImpl, len(elems))
153+
curCM := make(map[ChildKey]*ComponentImpl)
154+
usedMap := make(map[*ComponentImpl]bool)
155+
for idx, child := range curChildren {
156+
if child.Key != "" {
157+
curCM[ChildKey{Tag: child.Tag, Idx: 0, Key: child.Key}] = child
158+
} else {
159+
curCM[ChildKey{Tag: child.Tag, Idx: idx, Key: ""}] = child
160+
}
161+
}
162+
for idx, elem := range elems {
163+
elemKey := getElemKey(&elem)
164+
var curChild *ComponentImpl
165+
if elemKey != "" {
166+
curChild = curCM[ChildKey{Tag: elem.Tag, Idx: 0, Key: elemKey}]
167+
} else {
168+
curChild = curCM[ChildKey{Tag: elem.Tag, Idx: idx, Key: ""}]
169+
}
170+
usedMap[curChild] = true
171+
newChildren[idx] = curChild
172+
r.render(&elem, &newChildren[idx], opts)
173+
}
174+
for _, child := range curChildren {
175+
if !usedMap[child] {
176+
r.unmount(&child)
177+
}
178+
}
179+
return newChildren
180+
}
181+
182+
// uses reflection to call the component function
183+
func callCFunc(cfunc any, ctx context.Context, props map[string]any) any {
184+
rval := reflect.ValueOf(cfunc)
185+
arg2Type := rval.Type().In(1)
186+
187+
var arg2Val reflect.Value
188+
if arg2Type.Kind() == reflect.Interface && arg2Type.NumMethod() == 0 {
189+
arg2Val = reflect.New(arg2Type)
190+
} else {
191+
arg2Val = reflect.New(arg2Type)
192+
if arg2Type.Kind() == reflect.Map {
193+
arg2Val.Elem().Set(reflect.ValueOf(props))
194+
} else {
195+
err := util.MapToStruct(props, arg2Val.Interface())
196+
if err != nil {
197+
fmt.Printf("error unmarshalling props: %v\n", err)
198+
}
199+
}
200+
}
201+
rtnVal := rval.Call([]reflect.Value{reflect.ValueOf(ctx), arg2Val.Elem()})
202+
if len(rtnVal) == 0 {
203+
return nil
204+
}
205+
return rtnVal[0].Interface()
206+
}
207+
208+
func convertPropsToVDom(props map[string]any) map[string]any {
209+
if len(props) == 0 {
210+
return nil
211+
}
212+
vdomProps := make(map[string]any)
213+
for k, v := range props {
214+
if v == nil {
215+
continue
216+
}
217+
if vdomFunc, ok := v.(vdom.VDomFunc); ok {
218+
// ensure Type is set on all VDomFuncs
219+
vdomFunc.Type = vdom.ObjectType_Func
220+
vdomProps[k] = vdomFunc
221+
continue
222+
}
223+
if vdomRef, ok := v.(vdom.VDomRef); ok {
224+
// ensure Type is set on all VDomRefs
225+
vdomRef.Type = vdom.ObjectType_Ref
226+
vdomProps[k] = vdomRef
227+
continue
228+
}
229+
val := reflect.ValueOf(v)
230+
if val.Kind() == reflect.Func {
231+
// convert go functions passed to event handlers to VDomFuncs
232+
vdomProps[k] = vdom.VDomFunc{Type: vdom.ObjectType_Func}
233+
continue
234+
}
235+
vdomProps[k] = v
236+
}
237+
return vdomProps
238+
}
239+
240+
func (r *RootElem) MakeRendered() *rpctypes.RenderedElem {
241+
if r.Root == nil {
242+
return nil
243+
}
244+
return r.convertCompToRendered(r.Root)
245+
}
246+
247+
func (r *RootElem) convertCompToRendered(c *ComponentImpl) *rpctypes.RenderedElem {
248+
if c == nil {
249+
return nil
250+
}
251+
if c.RenderedComp != nil {
252+
return r.convertCompToRendered(c.RenderedComp)
253+
}
254+
if len(c.Children) == 0 && r.CFuncs[c.Tag] != nil {
255+
return nil
256+
}
257+
return r.convertBaseToRendered(c)
258+
}
259+
260+
func (r *RootElem) convertBaseToRendered(c *ComponentImpl) *rpctypes.RenderedElem {
261+
elem := &rpctypes.RenderedElem{WaveId: c.WaveId, Tag: c.Tag}
262+
if c.Elem != nil {
263+
elem.Props = convertPropsToVDom(c.Elem.Props)
264+
}
265+
for _, child := range c.Children {
266+
childElem := r.convertCompToRendered(child)
267+
if childElem != nil {
268+
elem.Children = append(elem.Children, *childElem)
269+
}
270+
}
271+
if c.Tag == vdom.TextTag {
272+
elem.Text = c.Text
273+
}
274+
return elem
275+
}
276+
277+
func isBaseTag(tag string) bool {
278+
if tag == "" {
279+
return false
280+
}
281+
if tag == vdom.TextTag || tag == vdom.WaveTextTag || tag == vdom.WaveNullTag || tag == vdom.FragmentTag {
282+
return true
283+
}
284+
if tag[0] == '#' {
285+
return true
286+
}
287+
firstChar := rune(tag[0])
288+
return unicode.IsLower(firstChar)
289+
}

0 commit comments

Comments
 (0)