2024-07-23 22:16:53 +02:00
|
|
|
// Copyright 2024, Command Line Inc.
|
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
|
|
|
package vdom
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"fmt"
|
2024-11-06 08:07:45 +01:00
|
|
|
"log"
|
2024-07-23 22:16:53 +02:00
|
|
|
"reflect"
|
2024-11-06 08:07:45 +01:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
2024-07-23 22:16:53 +02:00
|
|
|
|
|
|
|
"github.com/google/uuid"
|
2024-10-17 23:50:36 +02:00
|
|
|
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
|
2024-07-23 22:16:53 +02:00
|
|
|
)
|
|
|
|
|
2024-11-06 00:52:59 +01:00
|
|
|
const (
|
|
|
|
BackendUpdate_InitialChunkSize = 50 // Size for initial chunks that contain both TransferElems and StateSync
|
|
|
|
BackendUpdate_ChunkSize = 100 // Size for subsequent chunks
|
|
|
|
)
|
|
|
|
|
2024-07-23 22:16:53 +02:00
|
|
|
type vdomContextKeyType struct{}
|
|
|
|
|
|
|
|
var vdomContextKey = vdomContextKeyType{}
|
|
|
|
|
|
|
|
type VDomContextVal struct {
|
|
|
|
Root *RootElem
|
2024-11-04 21:52:36 +01:00
|
|
|
Comp *ComponentImpl
|
2024-07-23 22:16:53 +02:00
|
|
|
HookIdx int
|
|
|
|
}
|
|
|
|
|
2024-10-17 23:50:36 +02:00
|
|
|
type Atom struct {
|
|
|
|
Val any
|
|
|
|
Dirty bool
|
|
|
|
UsedBy map[string]bool // component waveid -> true
|
|
|
|
}
|
|
|
|
|
2024-07-23 22:16:53 +02:00
|
|
|
type RootElem struct {
|
|
|
|
OuterCtx context.Context
|
2024-11-04 21:52:36 +01:00
|
|
|
Root *ComponentImpl
|
2024-11-06 08:07:45 +01:00
|
|
|
RenderTs int64
|
2024-11-02 18:58:13 +01:00
|
|
|
CFuncs map[string]any
|
2024-11-04 21:52:36 +01:00
|
|
|
CompMap map[string]*ComponentImpl // component waveid -> component
|
2024-07-23 22:16:53 +02:00
|
|
|
EffectWorkQueue []*EffectWorkElem
|
|
|
|
NeedsRenderMap map[string]bool
|
2024-10-17 23:50:36 +02:00
|
|
|
Atoms map[string]*Atom
|
2024-11-06 08:07:45 +01:00
|
|
|
RefOperations []VDomRefOperation
|
2024-07-23 22:16:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
const (
|
|
|
|
WorkType_Render = "render"
|
|
|
|
WorkType_Effect = "effect"
|
|
|
|
)
|
|
|
|
|
|
|
|
type EffectWorkElem struct {
|
|
|
|
Id string
|
|
|
|
EffectIndex int
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *RootElem) AddRenderWork(id string) {
|
|
|
|
if r.NeedsRenderMap == nil {
|
|
|
|
r.NeedsRenderMap = make(map[string]bool)
|
|
|
|
}
|
|
|
|
r.NeedsRenderMap[id] = true
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *RootElem) AddEffectWork(id string, effectIndex int) {
|
|
|
|
r.EffectWorkQueue = append(r.EffectWorkQueue, &EffectWorkElem{Id: id, EffectIndex: effectIndex})
|
|
|
|
}
|
|
|
|
|
|
|
|
func MakeRoot() *RootElem {
|
|
|
|
return &RootElem{
|
|
|
|
Root: nil,
|
2024-11-02 18:58:13 +01:00
|
|
|
CFuncs: make(map[string]any),
|
2024-11-04 21:52:36 +01:00
|
|
|
CompMap: make(map[string]*ComponentImpl),
|
2024-10-17 23:50:36 +02:00
|
|
|
Atoms: make(map[string]*Atom),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *RootElem) GetAtom(name string) *Atom {
|
|
|
|
atom, ok := r.Atoms[name]
|
|
|
|
if !ok {
|
|
|
|
atom = &Atom{UsedBy: make(map[string]bool)}
|
|
|
|
r.Atoms[name] = atom
|
|
|
|
}
|
|
|
|
return atom
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *RootElem) GetAtomVal(name string) any {
|
|
|
|
atom := r.GetAtom(name)
|
|
|
|
return atom.Val
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *RootElem) GetStateSync(full bool) []VDomStateSync {
|
|
|
|
stateSync := make([]VDomStateSync, 0)
|
|
|
|
for atomName, atom := range r.Atoms {
|
|
|
|
if atom.Dirty || full {
|
|
|
|
stateSync = append(stateSync, VDomStateSync{Atom: atomName, Value: atom.Val})
|
|
|
|
atom.Dirty = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return stateSync
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *RootElem) SetAtomVal(name string, val any, markDirty bool) {
|
|
|
|
atom := r.GetAtom(name)
|
|
|
|
if !markDirty {
|
|
|
|
atom.Val = val
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// try to avoid setting the value and marking as dirty if it's the "same"
|
|
|
|
if utilfn.JsonValEqual(val, atom.Val) {
|
|
|
|
return
|
2024-07-23 22:16:53 +02:00
|
|
|
}
|
2024-10-17 23:50:36 +02:00
|
|
|
atom.Val = val
|
|
|
|
atom.Dirty = true
|
2024-07-23 22:16:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (r *RootElem) SetOuterCtx(ctx context.Context) {
|
|
|
|
r.OuterCtx = ctx
|
|
|
|
}
|
|
|
|
|
2024-11-02 18:58:13 +01:00
|
|
|
func validateCFunc(cfunc any) error {
|
|
|
|
if cfunc == nil {
|
|
|
|
return fmt.Errorf("Component function cannot b nil")
|
|
|
|
}
|
|
|
|
rval := reflect.ValueOf(cfunc)
|
|
|
|
if rval.Kind() != reflect.Func {
|
|
|
|
return fmt.Errorf("Component function must be a function")
|
|
|
|
}
|
|
|
|
rtype := rval.Type()
|
|
|
|
if rtype.NumIn() != 2 {
|
|
|
|
return fmt.Errorf("Component function must take exactly 2 arguments")
|
|
|
|
}
|
|
|
|
if rtype.NumOut() != 1 {
|
|
|
|
return fmt.Errorf("Component function must return exactly 1 value")
|
|
|
|
}
|
|
|
|
// first arg must be context.Context
|
|
|
|
if rtype.In(0) != reflect.TypeOf((*context.Context)(nil)).Elem() {
|
|
|
|
return fmt.Errorf("Component function first argument must be context.Context")
|
|
|
|
}
|
2024-11-07 09:07:23 +01:00
|
|
|
// second can a map[string]any, or a struct, or ptr to struct (we'll reflect the value into it)
|
2024-11-02 18:58:13 +01:00
|
|
|
arg2Type := rtype.In(1)
|
|
|
|
if arg2Type.Kind() == reflect.Ptr {
|
|
|
|
arg2Type = arg2Type.Elem()
|
|
|
|
}
|
2024-11-07 09:07:23 +01:00
|
|
|
if arg2Type.Kind() == reflect.Map {
|
|
|
|
if arg2Type.Key().Kind() != reflect.String ||
|
|
|
|
!(arg2Type.Elem().Kind() == reflect.Interface && arg2Type.Elem().NumMethod() == 0) {
|
|
|
|
return fmt.Errorf("Map argument must be map[string]any")
|
|
|
|
}
|
|
|
|
} else if arg2Type.Kind() != reflect.Struct &&
|
|
|
|
!(arg2Type.Kind() == reflect.Interface && arg2Type.NumMethod() == 0) {
|
|
|
|
return fmt.Errorf("Component function second argument must be map[string]any, struct, or any")
|
2024-11-02 18:58:13 +01:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *RootElem) RegisterComponent(name string, cfunc any) error {
|
|
|
|
if err := validateCFunc(cfunc); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2024-07-23 22:16:53 +02:00
|
|
|
r.CFuncs[name] = cfunc
|
2024-11-02 18:58:13 +01:00
|
|
|
return nil
|
2024-07-23 22:16:53 +02:00
|
|
|
}
|
|
|
|
|
2024-10-17 23:50:36 +02:00
|
|
|
func (r *RootElem) Render(elem *VDomElem) {
|
2024-07-23 22:16:53 +02:00
|
|
|
r.render(elem, &r.Root)
|
|
|
|
}
|
|
|
|
|
2024-11-04 21:52:36 +01:00
|
|
|
func (vdf *VDomFunc) CallFn(event VDomEvent) {
|
2024-10-17 23:50:36 +02:00
|
|
|
if vdf.Fn == nil {
|
2024-07-23 22:16:53 +02:00
|
|
|
return
|
|
|
|
}
|
2024-10-17 23:50:36 +02:00
|
|
|
rval := reflect.ValueOf(vdf.Fn)
|
|
|
|
if rval.Kind() != reflect.Func {
|
|
|
|
return
|
|
|
|
}
|
2024-11-04 21:52:36 +01:00
|
|
|
rtype := rval.Type()
|
|
|
|
if rtype.NumIn() == 0 {
|
|
|
|
rval.Call(nil)
|
|
|
|
}
|
|
|
|
if rtype.NumIn() == 1 {
|
|
|
|
if rtype.In(0) == reflect.TypeOf((*VDomEvent)(nil)).Elem() {
|
|
|
|
rval.Call([]reflect.Value{reflect.ValueOf(event)})
|
|
|
|
}
|
|
|
|
}
|
2024-10-17 23:50:36 +02:00
|
|
|
}
|
|
|
|
|
2024-11-04 21:52:36 +01:00
|
|
|
func callVDomFn(fnVal any, data VDomEvent) {
|
2024-07-23 22:16:53 +02:00
|
|
|
if fnVal == nil {
|
|
|
|
return
|
|
|
|
}
|
2024-10-17 23:50:36 +02:00
|
|
|
fn := fnVal
|
|
|
|
if vdf, ok := fnVal.(*VDomFunc); ok {
|
|
|
|
fn = vdf.Fn
|
|
|
|
}
|
|
|
|
if fn == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
rval := reflect.ValueOf(fn)
|
|
|
|
if rval.Kind() != reflect.Func {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
rtype := rval.Type()
|
|
|
|
if rtype.NumIn() == 0 {
|
|
|
|
rval.Call(nil)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if rtype.NumIn() == 1 {
|
|
|
|
rval.Call([]reflect.Value{reflect.ValueOf(data)})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-04 21:52:36 +01:00
|
|
|
func (r *RootElem) Event(id string, propName string, event VDomEvent) {
|
2024-10-17 23:50:36 +02:00
|
|
|
comp := r.CompMap[id]
|
|
|
|
if comp == nil || comp.Elem == nil {
|
2024-07-23 22:16:53 +02:00
|
|
|
return
|
|
|
|
}
|
2024-10-17 23:50:36 +02:00
|
|
|
fnVal := comp.Elem.Props[propName]
|
2024-11-04 21:52:36 +01:00
|
|
|
callVDomFn(fnVal, event)
|
2024-07-23 22:16:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// this will be called by the frontend to say the DOM has been mounted
|
|
|
|
// it will eventually send any updated "refs" to the backend as well
|
2024-10-17 23:50:36 +02:00
|
|
|
func (r *RootElem) RunWork() {
|
2024-07-23 22:16:53 +02:00
|
|
|
workQueue := r.EffectWorkQueue
|
|
|
|
r.EffectWorkQueue = nil
|
|
|
|
// first, run effect cleanups
|
|
|
|
for _, work := range workQueue {
|
|
|
|
comp := r.CompMap[work.Id]
|
|
|
|
if comp == nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
hook := comp.Hooks[work.EffectIndex]
|
|
|
|
if hook.UnmountFn != nil {
|
|
|
|
hook.UnmountFn()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// now run, new effects
|
|
|
|
for _, work := range workQueue {
|
|
|
|
comp := r.CompMap[work.Id]
|
|
|
|
if comp == nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
hook := comp.Hooks[work.EffectIndex]
|
|
|
|
if hook.Fn != nil {
|
|
|
|
hook.UnmountFn = hook.Fn()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// now check if we need a render
|
|
|
|
if len(r.NeedsRenderMap) > 0 {
|
|
|
|
r.NeedsRenderMap = nil
|
|
|
|
r.render(r.Root.Elem, &r.Root)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-04 21:52:36 +01:00
|
|
|
func (r *RootElem) render(elem *VDomElem, comp **ComponentImpl) {
|
2024-07-23 22:16:53 +02:00
|
|
|
if elem == nil || elem.Tag == "" {
|
|
|
|
r.unmount(comp)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
elemKey := elem.Key()
|
|
|
|
if *comp == nil || !(*comp).compMatch(elem.Tag, elemKey) {
|
|
|
|
r.unmount(comp)
|
|
|
|
r.createComp(elem.Tag, elemKey, comp)
|
|
|
|
}
|
|
|
|
(*comp).Elem = elem
|
|
|
|
if elem.Tag == TextTag {
|
|
|
|
r.renderText(elem.Text, comp)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if isBaseTag(elem.Tag) {
|
|
|
|
// simple vdom, fragment, wave element
|
|
|
|
r.renderSimple(elem, comp)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
cfunc := r.CFuncs[elem.Tag]
|
|
|
|
if cfunc == nil {
|
|
|
|
text := fmt.Sprintf("<%s>", elem.Tag)
|
|
|
|
r.renderText(text, comp)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
r.renderComponent(cfunc, elem, comp)
|
|
|
|
}
|
|
|
|
|
2024-11-04 21:52:36 +01:00
|
|
|
func (r *RootElem) unmount(comp **ComponentImpl) {
|
2024-07-23 22:16:53 +02:00
|
|
|
if *comp == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// parent clean up happens first
|
|
|
|
for _, hook := range (*comp).Hooks {
|
|
|
|
if hook.UnmountFn != nil {
|
|
|
|
hook.UnmountFn()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// clean up any children
|
|
|
|
if (*comp).Comp != nil {
|
|
|
|
r.unmount(&(*comp).Comp)
|
|
|
|
}
|
|
|
|
if (*comp).Children != nil {
|
|
|
|
for _, child := range (*comp).Children {
|
|
|
|
r.unmount(&child)
|
|
|
|
}
|
|
|
|
}
|
2024-10-17 23:50:36 +02:00
|
|
|
delete(r.CompMap, (*comp).WaveId)
|
2024-07-23 22:16:53 +02:00
|
|
|
*comp = nil
|
|
|
|
}
|
|
|
|
|
2024-11-04 21:52:36 +01:00
|
|
|
func (r *RootElem) createComp(tag string, key string, comp **ComponentImpl) {
|
|
|
|
*comp = &ComponentImpl{WaveId: uuid.New().String(), Tag: tag, Key: key}
|
2024-10-17 23:50:36 +02:00
|
|
|
r.CompMap[(*comp).WaveId] = *comp
|
2024-07-23 22:16:53 +02:00
|
|
|
}
|
|
|
|
|
2024-11-04 21:52:36 +01:00
|
|
|
func (r *RootElem) renderText(text string, comp **ComponentImpl) {
|
2024-07-23 22:16:53 +02:00
|
|
|
if (*comp).Text != text {
|
|
|
|
(*comp).Text = text
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-04 21:52:36 +01:00
|
|
|
func (r *RootElem) renderChildren(elems []VDomElem, curChildren []*ComponentImpl) []*ComponentImpl {
|
|
|
|
newChildren := make([]*ComponentImpl, len(elems))
|
|
|
|
curCM := make(map[ChildKey]*ComponentImpl)
|
|
|
|
usedMap := make(map[*ComponentImpl]bool)
|
2024-07-23 22:16:53 +02:00
|
|
|
for idx, child := range curChildren {
|
|
|
|
if child.Key != "" {
|
|
|
|
curCM[ChildKey{Tag: child.Tag, Idx: 0, Key: child.Key}] = child
|
|
|
|
} else {
|
|
|
|
curCM[ChildKey{Tag: child.Tag, Idx: idx, Key: ""}] = child
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for idx, elem := range elems {
|
|
|
|
elemKey := elem.Key()
|
2024-11-04 21:52:36 +01:00
|
|
|
var curChild *ComponentImpl
|
2024-07-23 22:16:53 +02:00
|
|
|
if elemKey != "" {
|
|
|
|
curChild = curCM[ChildKey{Tag: elem.Tag, Idx: 0, Key: elemKey}]
|
|
|
|
} else {
|
|
|
|
curChild = curCM[ChildKey{Tag: elem.Tag, Idx: idx, Key: ""}]
|
|
|
|
}
|
|
|
|
usedMap[curChild] = true
|
|
|
|
newChildren[idx] = curChild
|
|
|
|
r.render(&elem, &newChildren[idx])
|
|
|
|
}
|
|
|
|
for _, child := range curChildren {
|
|
|
|
if !usedMap[child] {
|
|
|
|
r.unmount(&child)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return newChildren
|
|
|
|
}
|
|
|
|
|
2024-11-04 21:52:36 +01:00
|
|
|
func (r *RootElem) renderSimple(elem *VDomElem, comp **ComponentImpl) {
|
2024-07-23 22:16:53 +02:00
|
|
|
if (*comp).Comp != nil {
|
|
|
|
r.unmount(&(*comp).Comp)
|
|
|
|
}
|
|
|
|
(*comp).Children = r.renderChildren(elem.Children, (*comp).Children)
|
|
|
|
}
|
|
|
|
|
2024-11-04 21:52:36 +01:00
|
|
|
func (r *RootElem) makeRenderContext(comp *ComponentImpl) context.Context {
|
2024-07-23 22:16:53 +02:00
|
|
|
var ctx context.Context
|
|
|
|
if r.OuterCtx != nil {
|
|
|
|
ctx = r.OuterCtx
|
|
|
|
} else {
|
|
|
|
ctx = context.Background()
|
|
|
|
}
|
|
|
|
ctx = context.WithValue(ctx, vdomContextKey, &VDomContextVal{Root: r, Comp: comp, HookIdx: 0})
|
|
|
|
return ctx
|
|
|
|
}
|
|
|
|
|
|
|
|
func getRenderContext(ctx context.Context) *VDomContextVal {
|
|
|
|
v := ctx.Value(vdomContextKey)
|
|
|
|
if v == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return v.(*VDomContextVal)
|
|
|
|
}
|
|
|
|
|
2024-11-02 18:58:13 +01:00
|
|
|
func callCFunc(cfunc any, ctx context.Context, props map[string]any) any {
|
|
|
|
rval := reflect.ValueOf(cfunc)
|
|
|
|
arg2Type := rval.Type().In(1)
|
2024-11-07 09:07:23 +01:00
|
|
|
|
|
|
|
var arg2Val reflect.Value
|
|
|
|
if arg2Type.Kind() == reflect.Interface && arg2Type.NumMethod() == 0 {
|
|
|
|
// For any/interface{}, pass nil properly
|
|
|
|
arg2Val = reflect.New(arg2Type)
|
2024-11-04 21:52:36 +01:00
|
|
|
} else {
|
2024-11-07 09:07:23 +01:00
|
|
|
arg2Val = reflect.New(arg2Type)
|
|
|
|
// if arg2 is a map, just pass props
|
|
|
|
if arg2Type.Kind() == reflect.Map {
|
|
|
|
arg2Val.Elem().Set(reflect.ValueOf(props))
|
|
|
|
} else {
|
|
|
|
err := utilfn.MapToStruct(props, arg2Val.Interface())
|
|
|
|
if err != nil {
|
|
|
|
fmt.Printf("error unmarshalling props: %v\n", err)
|
|
|
|
}
|
2024-11-04 21:52:36 +01:00
|
|
|
}
|
|
|
|
}
|
2024-11-02 18:58:13 +01:00
|
|
|
rtnVal := rval.Call([]reflect.Value{reflect.ValueOf(ctx), arg2Val.Elem()})
|
|
|
|
if len(rtnVal) == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return rtnVal[0].Interface()
|
|
|
|
}
|
|
|
|
|
2024-11-04 21:52:36 +01:00
|
|
|
func (r *RootElem) renderComponent(cfunc any, elem *VDomElem, comp **ComponentImpl) {
|
2024-07-23 22:16:53 +02:00
|
|
|
if (*comp).Children != nil {
|
|
|
|
for _, child := range (*comp).Children {
|
|
|
|
r.unmount(&child)
|
|
|
|
}
|
|
|
|
(*comp).Children = nil
|
|
|
|
}
|
|
|
|
props := make(map[string]any)
|
|
|
|
for k, v := range elem.Props {
|
|
|
|
props[k] = v
|
|
|
|
}
|
|
|
|
props[ChildrenPropKey] = elem.Children
|
|
|
|
ctx := r.makeRenderContext(*comp)
|
2024-11-02 18:58:13 +01:00
|
|
|
renderedElem := callCFunc(cfunc, ctx, props)
|
2024-07-23 22:16:53 +02:00
|
|
|
rtnElemArr := partToElems(renderedElem)
|
|
|
|
if len(rtnElemArr) == 0 {
|
|
|
|
r.unmount(&(*comp).Comp)
|
|
|
|
return
|
|
|
|
}
|
2024-10-17 23:50:36 +02:00
|
|
|
var rtnElem *VDomElem
|
2024-07-23 22:16:53 +02:00
|
|
|
if len(rtnElemArr) == 1 {
|
|
|
|
rtnElem = &rtnElemArr[0]
|
|
|
|
} else {
|
2024-10-17 23:50:36 +02:00
|
|
|
rtnElem = &VDomElem{Tag: FragmentTag, Children: rtnElemArr}
|
2024-07-23 22:16:53 +02:00
|
|
|
}
|
|
|
|
r.render(rtnElem, &(*comp).Comp)
|
|
|
|
}
|
|
|
|
|
2024-11-06 08:07:45 +01:00
|
|
|
func (r *RootElem) UpdateRef(updateRef VDomRefUpdate) {
|
|
|
|
refId := updateRef.RefId
|
|
|
|
split := strings.SplitN(refId, ":", 2)
|
|
|
|
if len(split) != 2 {
|
|
|
|
log.Printf("invalid ref id: %s\n", refId)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
waveId := split[0]
|
|
|
|
hookIdx, err := strconv.Atoi(split[1])
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("invalid ref id (bad hook idx): %s\n", refId)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
comp := r.CompMap[waveId]
|
|
|
|
if comp == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if hookIdx < 0 || hookIdx >= len(comp.Hooks) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
hook := comp.Hooks[hookIdx]
|
|
|
|
if hook == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
ref, ok := hook.Val.(*VDomRef)
|
|
|
|
if !ok {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
ref.HasCurrent = updateRef.HasCurrent
|
|
|
|
ref.Position = updateRef.Position
|
|
|
|
r.AddRenderWork(waveId)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *RootElem) QueueRefOp(op VDomRefOperation) {
|
|
|
|
r.RefOperations = append(r.RefOperations, op)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *RootElem) GetRefOperations() []VDomRefOperation {
|
|
|
|
ops := r.RefOperations
|
|
|
|
r.RefOperations = nil
|
|
|
|
return ops
|
|
|
|
}
|
|
|
|
|
2024-07-23 22:16:53 +02:00
|
|
|
func convertPropsToVDom(props map[string]any) map[string]any {
|
|
|
|
if len(props) == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
vdomProps := make(map[string]any)
|
|
|
|
for k, v := range props {
|
|
|
|
if v == nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
val := reflect.ValueOf(v)
|
|
|
|
if val.Kind() == reflect.Func {
|
2024-10-17 23:50:36 +02:00
|
|
|
vdomProps[k] = VDomFunc{Type: ObjectType_Func}
|
2024-07-23 22:16:53 +02:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
vdomProps[k] = v
|
|
|
|
}
|
|
|
|
return vdomProps
|
|
|
|
}
|
|
|
|
|
2024-11-04 21:52:36 +01:00
|
|
|
func convertBaseToVDom(c *ComponentImpl) *VDomElem {
|
2024-10-17 23:50:36 +02:00
|
|
|
elem := &VDomElem{WaveId: c.WaveId, Tag: c.Tag}
|
2024-07-23 22:16:53 +02:00
|
|
|
if c.Elem != nil {
|
|
|
|
elem.Props = convertPropsToVDom(c.Elem.Props)
|
|
|
|
}
|
|
|
|
for _, child := range c.Children {
|
|
|
|
childVDom := convertToVDom(child)
|
|
|
|
if childVDom != nil {
|
|
|
|
elem.Children = append(elem.Children, *childVDom)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return elem
|
|
|
|
}
|
|
|
|
|
2024-11-04 21:52:36 +01:00
|
|
|
func convertToVDom(c *ComponentImpl) *VDomElem {
|
2024-07-23 22:16:53 +02:00
|
|
|
if c == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
if c.Tag == TextTag {
|
2024-10-17 23:50:36 +02:00
|
|
|
return &VDomElem{Tag: TextTag, Text: c.Text}
|
2024-07-23 22:16:53 +02:00
|
|
|
}
|
|
|
|
if isBaseTag(c.Tag) {
|
|
|
|
return convertBaseToVDom(c)
|
|
|
|
} else {
|
|
|
|
return convertToVDom(c.Comp)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-04 21:52:36 +01:00
|
|
|
func (r *RootElem) makeVDom(comp *ComponentImpl) *VDomElem {
|
2024-07-23 22:16:53 +02:00
|
|
|
vdomElem := convertToVDom(comp)
|
|
|
|
return vdomElem
|
|
|
|
}
|
|
|
|
|
2024-10-17 23:50:36 +02:00
|
|
|
func (r *RootElem) MakeVDom() *VDomElem {
|
2024-07-23 22:16:53 +02:00
|
|
|
return r.makeVDom(r.Root)
|
|
|
|
}
|
2024-11-04 21:52:36 +01:00
|
|
|
|
|
|
|
func ConvertElemsToTransferElems(elems []VDomElem) []VDomTransferElem {
|
|
|
|
var transferElems []VDomTransferElem
|
|
|
|
textCounter := 0 // Counter for generating unique IDs for #text nodes
|
|
|
|
|
|
|
|
// Helper function to recursively process each VDomElem in preorder
|
2024-11-06 00:52:59 +01:00
|
|
|
var processElem func(elem VDomElem) string
|
|
|
|
processElem = func(elem VDomElem) string {
|
2024-11-04 21:52:36 +01:00
|
|
|
// Handle #text nodes by generating a unique placeholder ID
|
|
|
|
if elem.Tag == "#text" {
|
|
|
|
textId := fmt.Sprintf("text-%d", textCounter)
|
|
|
|
textCounter++
|
|
|
|
transferElems = append(transferElems, VDomTransferElem{
|
|
|
|
WaveId: textId,
|
|
|
|
Tag: elem.Tag,
|
|
|
|
Text: elem.Text,
|
|
|
|
Props: nil,
|
|
|
|
Children: nil,
|
|
|
|
})
|
|
|
|
return textId
|
|
|
|
}
|
|
|
|
|
|
|
|
// Convert children to WaveId references, handling potential #text nodes
|
|
|
|
childrenIds := make([]string, len(elem.Children))
|
|
|
|
for i, child := range elem.Children {
|
2024-11-06 00:52:59 +01:00
|
|
|
childrenIds[i] = processElem(child) // Children are not roots
|
2024-11-04 21:52:36 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Create the VDomTransferElem for the current element
|
|
|
|
transferElem := VDomTransferElem{
|
|
|
|
WaveId: elem.WaveId,
|
|
|
|
Tag: elem.Tag,
|
|
|
|
Props: elem.Props,
|
|
|
|
Children: childrenIds,
|
|
|
|
Text: elem.Text,
|
|
|
|
}
|
|
|
|
transferElems = append(transferElems, transferElem)
|
|
|
|
|
|
|
|
return elem.WaveId
|
|
|
|
}
|
|
|
|
|
|
|
|
// Start processing each top-level element, marking them as roots
|
|
|
|
for _, elem := range elems {
|
2024-11-06 00:52:59 +01:00
|
|
|
processElem(elem)
|
2024-11-04 21:52:36 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return transferElems
|
|
|
|
}
|
2024-11-06 00:52:59 +01:00
|
|
|
|
|
|
|
func DedupTransferElems(elems []VDomTransferElem) []VDomTransferElem {
|
|
|
|
seen := make(map[string]int) // maps WaveId to its index in the result slice
|
|
|
|
var result []VDomTransferElem
|
|
|
|
|
|
|
|
for _, elem := range elems {
|
|
|
|
if idx, exists := seen[elem.WaveId]; exists {
|
|
|
|
// Overwrite the previous element with the latest one
|
|
|
|
result[idx] = elem
|
|
|
|
} else {
|
|
|
|
// Add new element and store its index
|
|
|
|
seen[elem.WaveId] = len(result)
|
|
|
|
result = append(result, elem)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
func (beUpdate *VDomBackendUpdate) CreateTransferElems() {
|
|
|
|
var vdomElems []VDomElem
|
|
|
|
for idx, reUpdate := range beUpdate.RenderUpdates {
|
|
|
|
if reUpdate.VDom == nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
vdomElems = append(vdomElems, *reUpdate.VDom)
|
|
|
|
beUpdate.RenderUpdates[idx].VDomWaveId = reUpdate.VDom.WaveId
|
|
|
|
beUpdate.RenderUpdates[idx].VDom = nil
|
|
|
|
}
|
|
|
|
transferElems := ConvertElemsToTransferElems(vdomElems)
|
|
|
|
transferElems = DedupTransferElems(transferElems)
|
|
|
|
beUpdate.TransferElems = transferElems
|
|
|
|
}
|
|
|
|
|
|
|
|
// SplitBackendUpdate splits a large VDomBackendUpdate into multiple smaller updates
|
|
|
|
// The first update contains all the core fields, while subsequent updates only contain
|
|
|
|
// array elements that need to be appended
|
|
|
|
func SplitBackendUpdate(update *VDomBackendUpdate) []*VDomBackendUpdate {
|
|
|
|
// If the update is small enough, return it as is
|
|
|
|
if len(update.TransferElems) <= BackendUpdate_InitialChunkSize && len(update.StateSync) <= BackendUpdate_InitialChunkSize {
|
|
|
|
return []*VDomBackendUpdate{update}
|
|
|
|
}
|
|
|
|
|
|
|
|
var updates []*VDomBackendUpdate
|
|
|
|
|
|
|
|
// First update contains core fields and initial chunks
|
|
|
|
firstUpdate := &VDomBackendUpdate{
|
|
|
|
Type: update.Type,
|
|
|
|
Ts: update.Ts,
|
|
|
|
BlockId: update.BlockId,
|
|
|
|
Opts: update.Opts,
|
|
|
|
HasWork: update.HasWork,
|
|
|
|
RenderUpdates: update.RenderUpdates,
|
|
|
|
RefOperations: update.RefOperations,
|
|
|
|
Messages: update.Messages,
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add initial chunks of arrays
|
|
|
|
if len(update.TransferElems) > 0 {
|
|
|
|
firstUpdate.TransferElems = update.TransferElems[:min(BackendUpdate_InitialChunkSize, len(update.TransferElems))]
|
|
|
|
}
|
|
|
|
if len(update.StateSync) > 0 {
|
|
|
|
firstUpdate.StateSync = update.StateSync[:min(BackendUpdate_InitialChunkSize, len(update.StateSync))]
|
|
|
|
}
|
|
|
|
|
|
|
|
updates = append(updates, firstUpdate)
|
|
|
|
|
|
|
|
// Create subsequent updates for remaining TransferElems
|
|
|
|
for i := BackendUpdate_InitialChunkSize; i < len(update.TransferElems); i += BackendUpdate_ChunkSize {
|
|
|
|
end := min(i+BackendUpdate_ChunkSize, len(update.TransferElems))
|
|
|
|
updates = append(updates, &VDomBackendUpdate{
|
|
|
|
Type: update.Type,
|
|
|
|
Ts: update.Ts,
|
|
|
|
BlockId: update.BlockId,
|
|
|
|
TransferElems: update.TransferElems[i:end],
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create subsequent updates for remaining StateSync
|
|
|
|
for i := BackendUpdate_InitialChunkSize; i < len(update.StateSync); i += BackendUpdate_ChunkSize {
|
|
|
|
end := min(i+BackendUpdate_ChunkSize, len(update.StateSync))
|
|
|
|
updates = append(updates, &VDomBackendUpdate{
|
|
|
|
Type: update.Type,
|
|
|
|
Ts: update.Ts,
|
|
|
|
BlockId: update.BlockId,
|
|
|
|
StateSync: update.StateSync[i:end],
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return updates
|
|
|
|
}
|