(null);
+ React.useEffect(() => {
+ async function fetchAndSanitizeCss() {
+ try {
+ const response = await fetch(src);
+ if (!response.ok) {
+ console.error(`Failed to load CSS from ${src}`);
+ return;
+ }
+ const cssText = await response.text();
+ const wrapperClassName = "vdom-" + model.blockId;
+ const sanitizedCss = validateAndWrapCss(model, cssText, wrapperClassName);
+ if (sanitizedCss) {
+ setStyleContent(sanitizedCss);
+ } else {
+ console.error("Failed to sanitize CSS");
+ }
+ } catch (error) {
+ console.error("Error fetching CSS:", error);
+ }
+ }
+ fetchAndSanitizeCss();
+ }, [src, model]);
+ if (!styleContent) {
+ return null;
+ }
+ return ;
+}
+
function VDomTag({ elem, model }: { elem: VDomElem; model: VDomModel }) {
const props = useVDom(model, elem);
if (elem.tag == WaveNullTag) {
@@ -382,6 +413,9 @@ function VDomTag({ elem, model }: { elem: VDomElem; model: VDomModel }) {
if (elem.tag == StyleTagName) {
return ;
}
+ if (elem.tag == WaveStyleTagName) {
+ return ;
+ }
if (!AllowedSimpleTags[elem.tag] && !AllowedSvgTags[elem.tag]) {
return {"Invalid Tag <" + elem.tag + ">"}
;
}
diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts
index 44bfcc9ba..d59b5c028 100644
--- a/frontend/types/gotypes.d.ts
+++ b/frontend/types/gotypes.d.ts
@@ -652,6 +652,7 @@ declare global {
opts?: VDomBackendOpts;
haswork?: boolean;
renderupdates?: VDomRenderUpdate[];
+ transferelems?: VDomTransferElem[];
statesync?: VDomStateSync[];
refoperations?: VDomRefOperation[];
messages?: VDomMessage[];
@@ -773,7 +774,8 @@ declare global {
type VDomRenderUpdate = {
updatetype: "root"|"append"|"replace"|"remove"|"insert";
waveid?: string;
- vdom: VDomElem;
+ vdomwaveid?: string;
+ vdom?: VDomElem;
index?: number;
};
@@ -789,6 +791,15 @@ declare global {
magnified?: boolean;
};
+ // vdom.VDomTransferElem
+ type VDomTransferElem = {
+ waveid?: string;
+ tag: string;
+ props?: {[key: string]: any};
+ children?: string[];
+ text?: string;
+ };
+
// wshrpc.VDomUrlRequestData
type VDomUrlRequestData = {
method: string;
diff --git a/pkg/vdom/vdom.go b/pkg/vdom/vdom.go
index a3cc04a7f..945dbdcb5 100644
--- a/pkg/vdom/vdom.go
+++ b/pkg/vdom/vdom.go
@@ -35,6 +35,11 @@ type styleAttrWrapper struct {
Val any
}
+type classAttrWrapper struct {
+ ClassName string
+ Cond bool
+}
+
type styleAttrMapWrapper struct {
StyleAttrMap map[string]any
}
@@ -82,6 +87,47 @@ func mergeStyleAttr(props *map[string]any, styleAttr styleAttrWrapper) {
styleMap[styleAttr.StyleAttr] = styleAttr.Val
}
+func mergeClassAttr(props *map[string]any, classAttr classAttrWrapper) {
+ if *props == nil {
+ *props = make(map[string]any)
+ }
+ if classAttr.Cond {
+ if (*props)["className"] == nil {
+ (*props)["className"] = classAttr.ClassName
+ return
+ }
+ classVal, ok := (*props)["className"].(string)
+ if !ok {
+ return
+ }
+ // check if class already exists (must split, contains won't work)
+ splitArr := strings.Split(classVal, " ")
+ for _, class := range splitArr {
+ if class == classAttr.ClassName {
+ return
+ }
+ }
+ (*props)["className"] = classVal + " " + classAttr.ClassName
+ } else {
+ classVal, ok := (*props)["className"].(string)
+ if !ok {
+ return
+ }
+ splitArr := strings.Split(classVal, " ")
+ for i, class := range splitArr {
+ if class == classAttr.ClassName {
+ splitArr = append(splitArr[:i], splitArr[i+1:]...)
+ break
+ }
+ }
+ if len(splitArr) == 0 {
+ delete(*props, "className")
+ } else {
+ (*props)["className"] = strings.Join(splitArr, " ")
+ }
+ }
+}
+
func E(tag string, parts ...any) *VDomElem {
rtn := &VDomElem{Tag: tag}
for _, part := range parts {
@@ -103,12 +149,54 @@ func E(tag string, parts ...any) *VDomElem {
}
continue
}
+ if classAttr, ok := part.(classAttrWrapper); ok {
+ mergeClassAttr(&rtn.Props, classAttr)
+ continue
+ }
elems := partToElems(part)
rtn.Children = append(rtn.Children, elems...)
}
return rtn
}
+func Class(name string) classAttrWrapper {
+ return classAttrWrapper{ClassName: name, Cond: true}
+}
+
+func ClassIf(cond bool, name string) classAttrWrapper {
+ return classAttrWrapper{ClassName: name, Cond: cond}
+}
+
+func ClassIfElse(cond bool, name string, elseName string) classAttrWrapper {
+ if cond {
+ return classAttrWrapper{ClassName: name, Cond: true}
+ }
+ return classAttrWrapper{ClassName: elseName, Cond: true}
+}
+
+func If(cond bool, part any) any {
+ if cond {
+ return part
+ }
+ return nil
+}
+
+func IfElse(cond bool, part any, elsePart any) any {
+ if cond {
+ return part
+ }
+ return elsePart
+}
+
+func ForEach[T any](items []T, fn func(T) any) []any {
+ var elems []any
+ for _, item := range items {
+ fnResult := fn(item)
+ elems = append(elems, fnResult)
+ }
+ return elems
+}
+
func Props(props any) map[string]any {
m, err := utilfn.StructToMap(props)
if err != nil {
@@ -173,6 +261,31 @@ func UseState[T any](ctx context.Context, initialVal T) (T, func(T)) {
return rtnVal, setVal
}
+func UseStateWithFn[T any](ctx context.Context, initialVal T) (T, func(T), func(func(T) T)) {
+ vc, hookVal := getHookFromCtx(ctx)
+ if !hookVal.Init {
+ hookVal.Init = true
+ hookVal.Val = initialVal
+ }
+ var rtnVal T
+ rtnVal, ok := hookVal.Val.(T)
+ if !ok {
+ panic("UseState hook value is not a state (possible out of order or conditional hooks)")
+ }
+
+ setVal := func(newVal T) {
+ hookVal.Val = newVal
+ vc.Root.AddRenderWork(vc.Comp.WaveId)
+ }
+
+ setFuncVal := func(updateFunc func(T) T) {
+ hookVal.Val = updateFunc(hookVal.Val.(T))
+ vc.Root.AddRenderWork(vc.Comp.WaveId)
+ }
+
+ return rtnVal, setVal, setFuncVal
+}
+
func UseAtom[T any](ctx context.Context, atomName string) (T, func(T)) {
vc, hookVal := getHookFromCtx(ctx)
if !hookVal.Init {
@@ -212,6 +325,19 @@ func UseVDomRef(ctx context.Context) *VDomRef {
return refVal
}
+func UseRef[T any](ctx context.Context, val T) *VDomSimpleRef[T] {
+ _, hookVal := getHookFromCtx(ctx)
+ if !hookVal.Init {
+ hookVal.Init = true
+ hookVal.Val = &VDomSimpleRef[T]{Current: val}
+ }
+ refVal, ok := hookVal.Val.(*VDomSimpleRef[T])
+ if !ok {
+ panic("UseRef hook value is not a ref (possible out of order or conditional hooks)")
+ }
+ return refVal
+}
+
func UseId(ctx context.Context) string {
vc := getRenderContext(ctx)
if vc == nil {
diff --git a/pkg/vdom/vdom_root.go b/pkg/vdom/vdom_root.go
index b59f5dd37..954edd9af 100644
--- a/pkg/vdom/vdom_root.go
+++ b/pkg/vdom/vdom_root.go
@@ -12,6 +12,11 @@ import (
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
)
+const (
+ BackendUpdate_InitialChunkSize = 50 // Size for initial chunks that contain both TransferElems and StateSync
+ BackendUpdate_ChunkSize = 100 // Size for subsequent chunks
+)
+
type vdomContextKeyType struct{}
var vdomContextKey = vdomContextKeyType{}
@@ -470,14 +475,13 @@ func ConvertElemsToTransferElems(elems []VDomElem) []VDomTransferElem {
textCounter := 0 // Counter for generating unique IDs for #text nodes
// Helper function to recursively process each VDomElem in preorder
- var processElem func(elem VDomElem, isRoot bool) string
- processElem = func(elem VDomElem, isRoot bool) string {
+ var processElem func(elem VDomElem) string
+ processElem = func(elem VDomElem) string {
// Handle #text nodes by generating a unique placeholder ID
if elem.Tag == "#text" {
textId := fmt.Sprintf("text-%d", textCounter)
textCounter++
transferElems = append(transferElems, VDomTransferElem{
- Root: isRoot,
WaveId: textId,
Tag: elem.Tag,
Text: elem.Text,
@@ -490,12 +494,11 @@ func ConvertElemsToTransferElems(elems []VDomElem) []VDomTransferElem {
// Convert children to WaveId references, handling potential #text nodes
childrenIds := make([]string, len(elem.Children))
for i, child := range elem.Children {
- childrenIds[i] = processElem(child, false) // Children are not roots
+ childrenIds[i] = processElem(child) // Children are not roots
}
// Create the VDomTransferElem for the current element
transferElem := VDomTransferElem{
- Root: isRoot,
WaveId: elem.WaveId,
Tag: elem.Tag,
Props: elem.Props,
@@ -509,8 +512,99 @@ func ConvertElemsToTransferElems(elems []VDomElem) []VDomTransferElem {
// Start processing each top-level element, marking them as roots
for _, elem := range elems {
- processElem(elem, true)
+ processElem(elem)
}
return transferElems
}
+
+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
+}
diff --git a/pkg/vdom/vdom_types.go b/pkg/vdom/vdom_types.go
index e3b3c00e9..ae77fcd8a 100644
--- a/pkg/vdom/vdom_types.go
+++ b/pkg/vdom/vdom_types.go
@@ -33,7 +33,6 @@ type VDomElem struct {
// the over the wire format for a vdom element
type VDomTransferElem struct {
- Root bool `json:"root,omitempty"`
WaveId string `json:"waveid,omitempty"` // required, except for #text nodes
Tag string `json:"tag"`
Props map[string]any `json:"props,omitempty"`
@@ -86,6 +85,7 @@ type VDomBackendUpdate struct {
Opts *VDomBackendOpts `json:"opts,omitempty"`
HasWork bool `json:"haswork,omitempty"`
RenderUpdates []VDomRenderUpdate `json:"renderupdates,omitempty"`
+ TransferElems []VDomTransferElem `json:"transferelems,omitempty"`
StateSync []VDomStateSync `json:"statesync,omitempty"`
RefOperations []VDomRefOperation `json:"refoperations,omitempty"`
Messages []VDomMessage `json:"messages,omitempty"`
@@ -118,6 +118,10 @@ type VDomRef struct {
HasCurrent bool `json:"hascurrent,omitempty"`
}
+type VDomSimpleRef[T any] struct {
+ Current T `json:"current"`
+}
+
type DomRect struct {
Top float64 `json:"top"`
Left float64 `json:"left"`
@@ -176,10 +180,11 @@ type VDomBackendOpts struct {
}
type VDomRenderUpdate struct {
- UpdateType string `json:"updatetype" tstype:"\"root\"|\"append\"|\"replace\"|\"remove\"|\"insert\""`
- WaveId string `json:"waveid,omitempty"`
- VDom VDomElem `json:"vdom"`
- Index *int `json:"index,omitempty"`
+ UpdateType string `json:"updatetype" tstype:"\"root\"|\"append\"|\"replace\"|\"remove\"|\"insert\""`
+ WaveId string `json:"waveid,omitempty"`
+ VDomWaveId string `json:"vdomwaveid,omitempty"`
+ VDom *VDomElem `json:"vdom,omitempty"` // these get removed for transfer (encoded to transferelems)
+ Index *int `json:"index,omitempty"`
}
type VDomRefOperation struct {
diff --git a/pkg/vdom/vdomclient/vdomclient.go b/pkg/vdom/vdomclient/vdomclient.go
index 97972aa43..ee26e0ecc 100644
--- a/pkg/vdom/vdomclient/vdomclient.go
+++ b/pkg/vdom/vdomclient/vdomclient.go
@@ -6,6 +6,8 @@ package vdomclient
import (
"context"
"fmt"
+ "io"
+ "io/fs"
"log"
"net/http"
"os"
@@ -207,7 +209,7 @@ func (c *Client) fullRender() (*vdom.VDomBackendUpdate, error) {
HasWork: len(c.Root.EffectWorkQueue) > 0,
Opts: &c.Opts,
RenderUpdates: []vdom.VDomRenderUpdate{
- {UpdateType: "root", VDom: *renderedVDom},
+ {UpdateType: "root", VDom: renderedVDom},
},
StateSync: c.Root.GetStateSync(true),
}, nil
@@ -224,7 +226,7 @@ func (c *Client) incrementalRender() (*vdom.VDomBackendUpdate, error) {
Ts: time.Now().UnixMilli(),
BlockId: c.RpcContext.BlockId,
RenderUpdates: []vdom.VDomRenderUpdate{
- {UpdateType: "root", VDom: *renderedVDom},
+ {UpdateType: "root", VDom: renderedVDom},
},
StateSync: c.Root.GetStateSync(false),
}, nil
@@ -234,9 +236,111 @@ func (c *Client) RegisterUrlPathHandler(path string, handler http.Handler) {
c.UrlHandlerMux.Handle(path, handler)
}
-func (c *Client) RegisterFileHandler(path string, fileName string) {
- fileName = wavebase.ExpandHomeDirSafe(fileName)
+type FileHandlerOption struct {
+ FilePath string // optional file path on disk
+ Data []byte // optional byte slice content
+ Reader io.Reader // optional reader for content
+ File fs.File // optional embedded or opened file
+ MimeType string // optional mime type
+}
+
+func determineMimeType(option FileHandlerOption) (string, []byte) {
+ // If MimeType is set, use it directly
+ if option.MimeType != "" {
+ return option.MimeType, nil
+ }
+
+ // Detect from Data if available, no need to buffer
+ if option.Data != nil {
+ return http.DetectContentType(option.Data), nil
+ }
+
+ // Detect from FilePath, no buffering necessary
+ if option.FilePath != "" {
+ filePath := wavebase.ExpandHomeDirSafe(option.FilePath)
+ file, err := os.Open(filePath)
+ if err != nil {
+ return "application/octet-stream", nil // Fallback on error
+ }
+ defer file.Close()
+
+ // Read first 512 bytes for MIME detection
+ buf := make([]byte, 512)
+ _, err = file.Read(buf)
+ if err != nil && err != io.EOF {
+ return "application/octet-stream", nil
+ }
+ return http.DetectContentType(buf), nil
+ }
+
+ // Buffer for File (fs.File), since it lacks Seek
+ if option.File != nil {
+ buf := make([]byte, 512)
+ n, err := option.File.Read(buf)
+ if err != nil && err != io.EOF {
+ return "application/octet-stream", nil
+ }
+ return http.DetectContentType(buf[:n]), buf[:n]
+ }
+
+ // Buffer for Reader (io.Reader), same as File
+ if option.Reader != nil {
+ buf := make([]byte, 512)
+ n, err := option.Reader.Read(buf)
+ if err != nil && err != io.EOF {
+ return "application/octet-stream", nil
+ }
+ return http.DetectContentType(buf[:n]), buf[:n]
+ }
+
+ // Default MIME type if none specified
+ return "application/octet-stream", nil
+}
+
+func (c *Client) RegisterFileHandler(path string, option FileHandlerOption) {
c.UrlHandlerMux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
- http.ServeFile(w, r, fileName)
+ // Determine MIME type and get buffered data if needed
+ contentType, bufferedData := determineMimeType(option)
+ w.Header().Set("Content-Type", contentType)
+
+ if option.FilePath != "" {
+ // Serve file from path
+ filePath := wavebase.ExpandHomeDirSafe(option.FilePath)
+ http.ServeFile(w, r, filePath)
+ } else if option.Data != nil {
+ // Set content length and serve content from in-memory data
+ w.Header().Set("Content-Length", fmt.Sprintf("%d", len(option.Data)))
+ w.WriteHeader(http.StatusOK) // Ensure headers are sent before writing body
+ if _, err := w.Write(option.Data); err != nil {
+ http.Error(w, "Failed to serve content", http.StatusInternalServerError)
+ }
+ } else if option.File != nil {
+ // Write buffered data if available, then continue with remaining File content
+ if bufferedData != nil {
+ w.Header().Set("Content-Length", fmt.Sprintf("%d", len(bufferedData)))
+ if _, err := w.Write(bufferedData); err != nil {
+ http.Error(w, "Failed to serve content", http.StatusInternalServerError)
+ return
+ }
+ }
+ // Serve remaining content from File
+ if _, err := io.Copy(w, option.File); err != nil {
+ http.Error(w, "Failed to serve content", http.StatusInternalServerError)
+ }
+ } else if option.Reader != nil {
+ // Write buffered data if available, then continue with remaining Reader content
+ if bufferedData != nil {
+ if _, err := w.Write(bufferedData); err != nil {
+ http.Error(w, "Failed to serve content", http.StatusInternalServerError)
+ return
+ }
+ }
+ // Serve remaining content from Reader
+ if _, err := io.Copy(w, option.Reader); err != nil {
+ http.Error(w, "Failed to serve content", http.StatusInternalServerError)
+ }
+ } else {
+ http.Error(w, "No content available", http.StatusNotFound)
+ }
})
}
diff --git a/pkg/vdom/vdomclient/vdomserverimpl.go b/pkg/vdom/vdomclient/vdomserverimpl.go
index 69dc34c38..f79584e17 100644
--- a/pkg/vdom/vdomclient/vdomserverimpl.go
+++ b/pkg/vdom/vdomclient/vdomserverimpl.go
@@ -21,15 +21,31 @@ type VDomServerImpl struct {
func (*VDomServerImpl) WshServerImpl() {}
-func (impl *VDomServerImpl) VDomRenderCommand(ctx context.Context, feUpdate vdom.VDomFrontendUpdate) (*vdom.VDomBackendUpdate, error) {
+func (impl *VDomServerImpl) VDomRenderCommand(ctx context.Context, feUpdate vdom.VDomFrontendUpdate) chan wshrpc.RespOrErrorUnion[*vdom.VDomBackendUpdate] {
+ respChan := make(chan wshrpc.RespOrErrorUnion[*vdom.VDomBackendUpdate], 5)
+
+ defer func() {
+ if r := recover(); r != nil {
+ log.Printf("panic in VDomRenderCommand: %v\n", r)
+ respChan <- wshrpc.RespOrErrorUnion[*vdom.VDomBackendUpdate]{
+ Error: fmt.Errorf("internal error: %v", r),
+ }
+ close(respChan)
+ }
+ }()
+
if feUpdate.Dispose {
+ defer close(respChan)
log.Printf("got dispose from frontend\n")
impl.Client.doShutdown("got dispose from frontend")
- return nil, nil
+ return respChan
}
+
if impl.Client.GetIsDone() {
- return nil, nil
+ close(respChan)
+ return respChan
}
+
// set atoms
for _, ss := range feUpdate.StateSync {
impl.Client.Root.SetAtomVal(ss.Atom, ss.Value, false)
@@ -44,10 +60,37 @@ func (impl *VDomServerImpl) VDomRenderCommand(ctx context.Context, feUpdate vdom
impl.Client.Root.Event(event.WaveId, event.EventType, event)
}
}
+
+ var update *vdom.VDomBackendUpdate
+ var err error
+
if feUpdate.Resync || true {
- return impl.Client.fullRender()
+ update, err = impl.Client.fullRender()
+ } else {
+ update, err = impl.Client.incrementalRender()
}
- return impl.Client.incrementalRender()
+ update.CreateTransferElems()
+
+ if err != nil {
+ respChan <- wshrpc.RespOrErrorUnion[*vdom.VDomBackendUpdate]{
+ Error: err,
+ }
+ close(respChan)
+ return respChan
+ }
+
+ // Split the update into chunks and send them sequentially
+ updates := vdom.SplitBackendUpdate(update)
+ go func() {
+ defer close(respChan)
+ for _, splitUpdate := range updates {
+ respChan <- wshrpc.RespOrErrorUnion[*vdom.VDomBackendUpdate]{
+ Response: splitUpdate,
+ }
+ }
+ }()
+
+ return respChan
}
func (impl *VDomServerImpl) VDomUrlRequestCommand(ctx context.Context, data wshrpc.VDomUrlRequestData) chan wshrpc.RespOrErrorUnion[wshrpc.VDomUrlRequestResponse] {
@@ -56,13 +99,14 @@ func (impl *VDomServerImpl) VDomUrlRequestCommand(ctx context.Context, data wshr
writer := NewStreamingResponseWriter(respChan)
go func() {
+ defer close(respChan) // Declared first, so it executes last
+ defer writer.Close() // Ensures writer is closed before the channel is closed
+
defer func() {
if r := recover(); r != nil {
- // On panic, send 500 status code
writer.WriteHeader(http.StatusInternalServerError)
writer.Write([]byte(fmt.Sprintf("internal server error: %v", r)))
}
- close(respChan)
}()
// Create an HTTP request from the RPC request data
diff --git a/pkg/wshrpc/wshclient/wshclient.go b/pkg/wshrpc/wshclient/wshclient.go
index 536550d67..c36f8d1c7 100644
--- a/pkg/wshrpc/wshclient/wshclient.go
+++ b/pkg/wshrpc/wshclient/wshclient.go
@@ -298,9 +298,8 @@ func VDomCreateContextCommand(w *wshutil.WshRpc, data vdom.VDomCreateContext, op
}
// command "vdomrender", wshserver.VDomRenderCommand
-func VDomRenderCommand(w *wshutil.WshRpc, data vdom.VDomFrontendUpdate, opts *wshrpc.RpcOpts) (*vdom.VDomBackendUpdate, error) {
- resp, err := sendRpcRequestCallHelper[*vdom.VDomBackendUpdate](w, "vdomrender", data, opts)
- return resp, err
+func VDomRenderCommand(w *wshutil.WshRpc, data vdom.VDomFrontendUpdate, opts *wshrpc.RpcOpts) chan wshrpc.RespOrErrorUnion[*vdom.VDomBackendUpdate] {
+ return sendRpcRequestResponseStreamHelper[*vdom.VDomBackendUpdate](w, "vdomrender", data, opts)
}
// command "vdomurlrequest", wshserver.VDomUrlRequestCommand
diff --git a/pkg/wshrpc/wshrpctypes.go b/pkg/wshrpc/wshrpctypes.go
index e1e74d1da..29894ded8 100644
--- a/pkg/wshrpc/wshrpctypes.go
+++ b/pkg/wshrpc/wshrpctypes.go
@@ -157,7 +157,7 @@ type WshRpcInterface interface {
VDomAsyncInitiationCommand(ctx context.Context, data vdom.VDomAsyncInitiationRequest) error
// proc
- VDomRenderCommand(ctx context.Context, data vdom.VDomFrontendUpdate) (*vdom.VDomBackendUpdate, error)
+ VDomRenderCommand(ctx context.Context, data vdom.VDomFrontendUpdate) chan RespOrErrorUnion[*vdom.VDomBackendUpdate]
VDomUrlRequestCommand(ctx context.Context, data VDomUrlRequestData) chan RespOrErrorUnion[VDomUrlRequestResponse]
}