+
);
diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts
index 5955eb25a..8c0f80e93 100644
--- a/frontend/types/gotypes.d.ts
+++ b/frontend/types/gotypes.d.ts
@@ -782,6 +782,21 @@ declare global {
magnified?: boolean;
};
+ // wshrpc.VDomUrlRequestData
+ type VDomUrlRequestData = {
+ method: string;
+ url: string;
+ headers: {[key: string]: string};
+ body?: string;
+ };
+
+ // wshrpc.VDomUrlRequestResponse
+ type VDomUrlRequestResponse = {
+ statuscode?: number;
+ headers?: {[key: string]: string};
+ body?: string;
+ };
+
type WSCommandType = {
wscommand: string;
} & ( SetBlockTermSizeWSCommand | BlockInputWSCommand | WSRpcCommand );
diff --git a/go.mod b/go.mod
index 7645166bc..4e442886b 100644
--- a/go.mod
+++ b/go.mod
@@ -22,7 +22,7 @@ require (
github.com/skeema/knownhosts v1.3.0
github.com/spf13/cobra v1.8.1
github.com/ubuntu/gowsl v0.0.0-20240906163211-049fd49bd93b
- github.com/wavetermdev/htmltoken v0.1.0
+ github.com/wavetermdev/htmltoken v0.2.0
golang.org/x/crypto v0.28.0
golang.org/x/sys v0.26.0
golang.org/x/term v0.25.0
diff --git a/go.sum b/go.sum
index cb329ca3f..121013e50 100644
--- a/go.sum
+++ b/go.sum
@@ -86,8 +86,8 @@ github.com/ubuntu/decorate v0.0.0-20230125165522-2d5b0a9bb117 h1:XQpsQG5lqRJlx4m
github.com/ubuntu/decorate v0.0.0-20230125165522-2d5b0a9bb117/go.mod h1:mx0TjbqsaDD9DUT5gA1s3hw47U6RIbbIBfvGzR85K0g=
github.com/ubuntu/gowsl v0.0.0-20240906163211-049fd49bd93b h1:wFBKF5k5xbJQU8bYgcSoQ/ScvmYyq6KHUabAuVUjOWM=
github.com/ubuntu/gowsl v0.0.0-20240906163211-049fd49bd93b/go.mod h1:N1CYNinssZru+ikvYTgVbVeSi21thHUTCoJ9xMvWe+s=
-github.com/wavetermdev/htmltoken v0.1.0 h1:RMdA9zTfnYa5jRC4RRG3XNoV5NOP8EDxpaVPjuVz//Q=
-github.com/wavetermdev/htmltoken v0.1.0/go.mod h1:5FM0XV6zNYiNza2iaTcFGj+hnMtgqumFHO31Z8euquk=
+github.com/wavetermdev/htmltoken v0.2.0 h1:sFVPPemlDv7/jg7n4Hx1AEF2m9MVAFjFpELWfhi/DlM=
+github.com/wavetermdev/htmltoken v0.2.0/go.mod h1:5FM0XV6zNYiNza2iaTcFGj+hnMtgqumFHO31Z8euquk=
github.com/wavetermdev/ssh_config v0.0.0-20241027232332-ed124367682d h1:ArHaUBaiQWUqBzM2G/oLlm3Be0kwUMDt9vTNOWIfOd0=
github.com/wavetermdev/ssh_config v0.0.0-20241027232332-ed124367682d/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
diff --git a/pkg/tsgen/tsgen.go b/pkg/tsgen/tsgen.go
index 503e4e890..bf17a2f41 100644
--- a/pkg/tsgen/tsgen.go
+++ b/pkg/tsgen/tsgen.go
@@ -130,6 +130,10 @@ func TypeToTSType(t reflect.Type, tsTypesMap map[reflect.Type]string) (string, [
case reflect.Bool:
return "boolean", nil
case reflect.Slice, reflect.Array:
+ // special case for byte slice, marshals to base64 encoded string
+ if t.Elem().Kind() == reflect.Uint8 {
+ return "string", nil
+ }
elemType, subTypes := TypeToTSType(t.Elem(), tsTypesMap)
if elemType == "" {
return "", nil
diff --git a/pkg/util/utilfn/compare.go b/pkg/util/utilfn/compare.go
index d9c96a24e..6d9d30cbd 100644
--- a/pkg/util/utilfn/compare.go
+++ b/pkg/util/utilfn/compare.go
@@ -58,6 +58,9 @@ func CompareAsFloat64(a, b any) bool {
// Convert various numeric types to float64 for comparison
func ToFloat64(val any) (float64, bool) {
+ if val == nil {
+ return 0, false
+ }
switch v := val.(type) {
case int:
return float64(v), true
@@ -87,3 +90,57 @@ func ToFloat64(val any) (float64, bool) {
return 0, false
}
}
+
+func ToInt64(val any) (int64, bool) {
+ if val == nil {
+ return 0, false
+ }
+ switch v := val.(type) {
+ case int:
+ return int64(v), true
+ case int8:
+ return int64(v), true
+ case int16:
+ return int64(v), true
+ case int32:
+ return int64(v), true
+ case int64:
+ return v, true
+ case uint:
+ return int64(v), true
+ case uint8:
+ return int64(v), true
+ case uint16:
+ return int64(v), true
+ case uint32:
+ return int64(v), true
+ case uint64:
+ return int64(v), true
+ case float32:
+ return int64(v), true
+ case float64:
+ return int64(v), true
+ default:
+ return 0, false
+ }
+}
+
+func ToInt(val any) (int, bool) {
+ i, ok := ToInt64(val)
+ if !ok {
+ return 0, false
+ }
+ return int(i), true
+}
+
+func ToStr(val any) (string, bool) {
+ if val == nil {
+ return "", false
+ }
+ switch v := val.(type) {
+ case string:
+ return v, true
+ default:
+ return "", false
+ }
+}
diff --git a/pkg/vdom/vdom.go b/pkg/vdom/vdom.go
index 0503c46b5..d7c66177a 100644
--- a/pkg/vdom/vdom.go
+++ b/pkg/vdom/vdom.go
@@ -25,8 +25,6 @@ type Hook struct {
Deps []any
}
-type CFunc = func(ctx context.Context, props map[string]any) any
-
func (e *VDomElem) Key() string {
keyVal, ok := e.Props[KeyPropKey]
if !ok {
diff --git a/pkg/vdom/vdom_html.go b/pkg/vdom/vdom_html.go
index f6ab228bb..67721efc2 100644
--- a/pkg/vdom/vdom_html.go
+++ b/pkg/vdom/vdom_html.go
@@ -73,18 +73,6 @@ func finalizeStack(stack []*VDomElem) *VDomElem {
return rtnElem
}
-func attrVal(attr htmltoken.Attribute) (any, error) {
- // if !attr.IsJson {
- // return attr.Val, nil
- // }
- var val any
- err := json.Unmarshal([]byte(attr.Val), &val)
- if err != nil {
- return nil, fmt.Errorf("error parsing json attr %q: %v", attr.Key, err)
- }
- return val, nil
-}
-
// returns value, isjson
func getAttrString(token htmltoken.Token, key string) string {
for _, attr := range token.Attr {
@@ -96,6 +84,19 @@ func getAttrString(token htmltoken.Token, key string) string {
}
func attrToProp(attrVal string, isJson bool, params map[string]any) any {
+ if isJson {
+ var val any
+ err := json.Unmarshal([]byte(attrVal), &val)
+ if err != nil {
+ return nil
+ }
+ unmStrVal, ok := val.(string)
+ if !ok {
+ return val
+ }
+ attrVal = unmStrVal
+ // fallthrough using the json str val
+ }
if strings.HasPrefix(attrVal, Html_ParamPrefix) {
bindKey := attrVal[len(Html_ParamPrefix):]
bindVal, ok := params[bindKey]
@@ -134,7 +135,7 @@ func tokenToElem(token htmltoken.Token, params map[string]any) *VDomElem {
if attr.Key == "" || attr.Val == "" {
continue
}
- propVal := attrToProp(attr.Val, false, params)
+ propVal := attrToProp(attr.Val, attr.IsJson, params)
elem.Props[attr.Key] = propVal
}
return elem
diff --git a/pkg/vdom/vdom_root.go b/pkg/vdom/vdom_root.go
index acbe67fd0..3f26bc660 100644
--- a/pkg/vdom/vdom_root.go
+++ b/pkg/vdom/vdom_root.go
@@ -32,7 +32,7 @@ type Atom struct {
type RootElem struct {
OuterCtx context.Context
Root *Component
- CFuncs map[string]CFunc
+ CFuncs map[string]any
CompMap map[string]*Component // component waveid -> component
EffectWorkQueue []*EffectWorkElem
NeedsRenderMap map[string]bool
@@ -63,7 +63,7 @@ func (r *RootElem) AddEffectWork(id string, effectIndex int) {
func MakeRoot() *RootElem {
return &RootElem{
Root: nil,
- CFuncs: make(map[string]CFunc),
+ CFuncs: make(map[string]any),
CompMap: make(map[string]*Component),
Atoms: make(map[string]*Atom),
}
@@ -112,8 +112,42 @@ func (r *RootElem) SetOuterCtx(ctx context.Context) {
r.OuterCtx = ctx
}
-func (r *RootElem) RegisterComponent(name string, cfunc CFunc) {
+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")
+ }
+ // second can a map, or a struct, or ptr to struct (we'll reflect the value into it)
+ arg2Type := rtype.In(1)
+ if arg2Type.Kind() == reflect.Ptr {
+ arg2Type = arg2Type.Elem()
+ }
+ if arg2Type.Kind() != reflect.Map && arg2Type.Kind() != reflect.Struct {
+ return fmt.Errorf("Component function second argument must be a map or a struct")
+ }
+ return nil
+}
+
+func (r *RootElem) RegisterComponent(name string, cfunc any) error {
+ if err := validateCFunc(cfunc); err != nil {
+ return err
+ }
r.CFuncs[name] = cfunc
+ return nil
}
func (r *RootElem) Render(elem *VDomElem) {
@@ -321,7 +355,19 @@ func getRenderContext(ctx context.Context) *VDomContextVal {
return v.(*VDomContextVal)
}
-func (r *RootElem) renderComponent(cfunc CFunc, elem *VDomElem, comp **Component) {
+func callCFunc(cfunc any, ctx context.Context, props map[string]any) any {
+ rval := reflect.ValueOf(cfunc)
+ arg2Type := rval.Type().In(1)
+ arg2Val := reflect.New(arg2Type)
+ utilfn.ReUnmarshal(arg2Val.Interface(), props)
+ rtnVal := rval.Call([]reflect.Value{reflect.ValueOf(ctx), arg2Val.Elem()})
+ if len(rtnVal) == 0 {
+ return nil
+ }
+ return rtnVal[0].Interface()
+}
+
+func (r *RootElem) renderComponent(cfunc any, elem *VDomElem, comp **Component) {
if (*comp).Children != nil {
for _, child := range (*comp).Children {
r.unmount(&child)
@@ -334,7 +380,7 @@ func (r *RootElem) renderComponent(cfunc CFunc, elem *VDomElem, comp **Component
}
props[ChildrenPropKey] = elem.Children
ctx := r.makeRenderContext(*comp)
- renderedElem := cfunc(ctx, props)
+ renderedElem := callCFunc(cfunc, ctx, props)
rtnElemArr := partToElems(renderedElem)
if len(rtnElemArr) == 0 {
r.unmount(&(*comp).Comp)
diff --git a/pkg/vdom/vdom_test.go b/pkg/vdom/vdom_test.go
index 2be63fa41..42e8214d8 100644
--- a/pkg/vdom/vdom_test.go
+++ b/pkg/vdom/vdom_test.go
@@ -5,7 +5,10 @@ import (
"encoding/json"
"fmt"
"log"
+ "reflect"
"testing"
+
+ "github.com/wavetermdev/waveterm/pkg/util/utilfn"
)
type renderContextKeyType struct{}
@@ -118,3 +121,65 @@ func TestBind(t *testing.T) {
jsonBytes, _ = json.MarshalIndent(elem, "", " ")
log.Printf("%s\n", string(jsonBytes))
}
+
+func TestJsonBind(t *testing.T) {
+ elem := Bind(`
`, nil)
+ if elem == nil {
+ t.Fatalf("elem is nil")
+ }
+ if elem.Tag != "div" {
+ t.Fatalf("elem.Tag: %s (expected 'div')\n", elem.Tag)
+ }
+ if elem.Props == nil || len(elem.Props) != 3 {
+ t.Fatalf("elem.Props: %v\n", elem.Props)
+ }
+ data1Val, ok := elem.Props["data1"]
+ if !ok {
+ t.Fatalf("data1 not found\n")
+ }
+ _, ok = data1Val.(float64)
+ if !ok {
+ t.Fatalf("data1: %T\n", data1Val)
+ }
+ data1Int, ok := utilfn.ToInt(data1Val)
+ if !ok || data1Int != 5 {
+ t.Fatalf("data1: %v\n", data1Val)
+ }
+ data2Val, ok := elem.Props["data2"]
+ if !ok {
+ t.Fatalf("data2 not found\n")
+ }
+ d2type := reflect.TypeOf(data2Val)
+ if d2type.Kind() != reflect.Slice {
+ t.Fatalf("data2: %T\n", data2Val)
+ }
+ data2Arr := data2Val.([]any)
+ if len(data2Arr) != 3 {
+ t.Fatalf("data2: %v\n", data2Val)
+ }
+ d2v2, ok := data2Arr[1].(float64)
+ if !ok || d2v2 != 2 {
+ t.Fatalf("data2: %v\n", data2Val)
+ }
+ data3Val, ok := elem.Props["data3"]
+ if !ok || data3Val == nil {
+ t.Fatalf("data3 not found\n")
+ }
+ d3type := reflect.TypeOf(data3Val)
+ if d3type.Kind() != reflect.Map {
+ t.Fatalf("data3: %T\n", data3Val)
+ }
+ data3Map := data3Val.(map[string]any)
+ if len(data3Map) != 1 {
+ t.Fatalf("data3: %v\n", data3Val)
+ }
+ d3v1, ok := data3Map["a"]
+ if !ok {
+ t.Fatalf("data3: %v\n", data3Val)
+ }
+ mval, ok := utilfn.ToInt(d3v1)
+ if !ok || mval != 1 {
+ t.Fatalf("data3: %v\n", data3Val)
+ }
+ log.Printf("elem: %v\n", elem)
+}
diff --git a/pkg/vdom/vdomclient/streamingresp.go b/pkg/vdom/vdomclient/streamingresp.go
new file mode 100644
index 000000000..6a8456786
--- /dev/null
+++ b/pkg/vdom/vdomclient/streamingresp.go
@@ -0,0 +1,129 @@
+// Copyright 2024, Command Line Inc.
+// SPDX-License-Identifier: Apache-2.0
+
+package vdomclient
+
+import (
+ "bytes"
+ "net/http"
+
+ "github.com/wavetermdev/waveterm/pkg/wshrpc"
+)
+
+const maxChunkSize = 64 * 1024 // 64KB maximum chunk size
+
+// StreamingResponseWriter implements http.ResponseWriter interface to stream response
+// data through a channel rather than buffering it in memory. This is particularly
+// useful for handling large responses like video streams or file downloads.
+type StreamingResponseWriter struct {
+ header http.Header
+ statusCode int
+ respChan chan<- wshrpc.RespOrErrorUnion[wshrpc.VDomUrlRequestResponse]
+ headerSent bool
+ buffer *bytes.Buffer
+}
+
+func NewStreamingResponseWriter(respChan chan<- wshrpc.RespOrErrorUnion[wshrpc.VDomUrlRequestResponse]) *StreamingResponseWriter {
+ return &StreamingResponseWriter{
+ header: make(http.Header),
+ statusCode: http.StatusOK,
+ respChan: respChan,
+ headerSent: false,
+ buffer: bytes.NewBuffer(make([]byte, 0, maxChunkSize)),
+ }
+}
+
+func (w *StreamingResponseWriter) Header() http.Header {
+ return w.header
+}
+
+func (w *StreamingResponseWriter) WriteHeader(statusCode int) {
+ if w.headerSent {
+ return
+ }
+
+ w.statusCode = statusCode
+ w.headerSent = true
+
+ headers := make(map[string]string)
+ for key, values := range w.header {
+ if len(values) > 0 {
+ headers[key] = values[0]
+ }
+ }
+
+ w.respChan <- wshrpc.RespOrErrorUnion[wshrpc.VDomUrlRequestResponse]{
+ Response: wshrpc.VDomUrlRequestResponse{
+ StatusCode: w.statusCode,
+ Headers: headers,
+ },
+ }
+}
+
+// sendChunk sends a single chunk of exactly maxChunkSize (or less)
+func (w *StreamingResponseWriter) sendChunk(data []byte) {
+ if len(data) == 0 {
+ return
+ }
+ chunk := make([]byte, len(data))
+ copy(chunk, data)
+ w.respChan <- wshrpc.RespOrErrorUnion[wshrpc.VDomUrlRequestResponse]{
+ Response: wshrpc.VDomUrlRequestResponse{
+ Body: chunk,
+ },
+ }
+}
+
+func (w *StreamingResponseWriter) Write(data []byte) (int, error) {
+ if !w.headerSent {
+ w.WriteHeader(http.StatusOK)
+ }
+
+ originalLen := len(data)
+
+ // If we already have data in the buffer
+ if w.buffer.Len() > 0 {
+ // Fill the buffer up to maxChunkSize
+ spaceInBuffer := maxChunkSize - w.buffer.Len()
+ if spaceInBuffer > 0 {
+ // How much of the new data can fit in the buffer
+ toBuffer := spaceInBuffer
+ if toBuffer > len(data) {
+ toBuffer = len(data)
+ }
+ w.buffer.Write(data[:toBuffer])
+ data = data[toBuffer:] // Advance data slice
+ }
+
+ // If buffer is full, send it
+ if w.buffer.Len() == maxChunkSize {
+ w.sendChunk(w.buffer.Bytes())
+ w.buffer.Reset()
+ }
+ }
+
+ // Send any full chunks from data
+ for len(data) >= maxChunkSize {
+ w.sendChunk(data[:maxChunkSize])
+ data = data[maxChunkSize:]
+ }
+
+ // Buffer any remaining data
+ if len(data) > 0 {
+ w.buffer.Write(data)
+ }
+
+ return originalLen, nil
+}
+
+func (w *StreamingResponseWriter) Close() error {
+ if !w.headerSent {
+ w.WriteHeader(http.StatusOK)
+ }
+
+ if w.buffer.Len() > 0 {
+ w.sendChunk(w.buffer.Bytes())
+ w.buffer.Reset()
+ }
+ return nil
+}
diff --git a/pkg/vdom/vdomclient/vdomclient.go b/pkg/vdom/vdomclient/vdomclient.go
index 79ee5d743..b21bfdbed 100644
--- a/pkg/vdom/vdomclient/vdomclient.go
+++ b/pkg/vdom/vdomclient/vdomclient.go
@@ -4,15 +4,17 @@
package vdomclient
import (
- "context"
"fmt"
"log"
+ "net/http"
"os"
"sync"
"time"
"github.com/google/uuid"
+ "github.com/gorilla/mux"
"github.com/wavetermdev/waveterm/pkg/vdom"
+ "github.com/wavetermdev/waveterm/pkg/wavebase"
"github.com/wavetermdev/waveterm/pkg/wps"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
@@ -33,42 +35,8 @@ type Client struct {
DoneCh chan struct{}
Opts vdom.VDomBackendOpts
GlobalEventHandler func(client *Client, event vdom.VDomEvent)
-}
-
-type VDomServerImpl struct {
- Client *Client
- BlockId string
-}
-
-func (*VDomServerImpl) WshServerImpl() {}
-
-func (impl *VDomServerImpl) VDomRenderCommand(ctx context.Context, feUpdate vdom.VDomFrontendUpdate) (*vdom.VDomBackendUpdate, error) {
- if feUpdate.Dispose {
- log.Printf("got dispose from frontend\n")
- impl.Client.doShutdown("got dispose from frontend")
- return nil, nil
- }
- if impl.Client.GetIsDone() {
- return nil, nil
- }
- // set atoms
- for _, ss := range feUpdate.StateSync {
- impl.Client.Root.SetAtomVal(ss.Atom, ss.Value, false)
- }
- // run events
- for _, event := range feUpdate.Events {
- if event.WaveId == "" {
- if impl.Client.GlobalEventHandler != nil {
- impl.Client.GlobalEventHandler(impl.Client, event)
- }
- } else {
- impl.Client.Root.Event(event.WaveId, event.EventType, event.EventData)
- }
- }
- if feUpdate.Resync {
- return impl.Client.fullRender()
- }
- return impl.Client.incrementalRender()
+ UrlHandlerMux *mux.Router
+ OverrideUrlHandler http.Handler
}
func (c *Client) GetIsDone() bool {
@@ -92,11 +60,16 @@ func (c *Client) SetGlobalEventHandler(handler func(client *Client, event vdom.V
c.GlobalEventHandler = handler
}
+func (c *Client) SetOverrideUrlHandler(handler http.Handler) {
+ c.OverrideUrlHandler = handler
+}
+
func MakeClient(opts *vdom.VDomBackendOpts) (*Client, error) {
client := &Client{
- Lock: &sync.Mutex{},
- Root: vdom.MakeRoot(),
- DoneCh: make(chan struct{}),
+ Lock: &sync.Mutex{},
+ Root: vdom.MakeRoot(),
+ DoneCh: make(chan struct{}),
+ UrlHandlerMux: mux.NewRouter(),
}
if opts != nil {
client.Opts = *opts
@@ -197,8 +170,8 @@ func makeNullVDom() *vdom.VDomElem {
return &vdom.VDomElem{WaveId: uuid.New().String(), Tag: vdom.WaveNullTag}
}
-func (c *Client) RegisterComponent(name string, cfunc vdom.CFunc) {
- c.Root.RegisterComponent(name, cfunc)
+func (c *Client) RegisterComponent(name string, cfunc any) error {
+ return c.Root.RegisterComponent(name, cfunc)
}
func (c *Client) fullRender() (*vdom.VDomBackendUpdate, error) {
@@ -236,3 +209,14 @@ func (c *Client) incrementalRender() (*vdom.VDomBackendUpdate, error) {
StateSync: c.Root.GetStateSync(false),
}, nil
}
+
+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)
+ c.UrlHandlerMux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
+ http.ServeFile(w, r, fileName)
+ })
+}
diff --git a/pkg/vdom/vdomclient/vdomserverimpl.go b/pkg/vdom/vdomclient/vdomserverimpl.go
new file mode 100644
index 000000000..2d8e352b8
--- /dev/null
+++ b/pkg/vdom/vdomclient/vdomserverimpl.go
@@ -0,0 +1,95 @@
+// Copyright 2024, Command Line Inc.
+// SPDX-License-Identifier: Apache-2.0
+
+package vdomclient
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "log"
+ "net/http"
+
+ "github.com/wavetermdev/waveterm/pkg/vdom"
+ "github.com/wavetermdev/waveterm/pkg/wshrpc"
+)
+
+type VDomServerImpl struct {
+ Client *Client
+ BlockId string
+}
+
+func (*VDomServerImpl) WshServerImpl() {}
+
+func (impl *VDomServerImpl) VDomRenderCommand(ctx context.Context, feUpdate vdom.VDomFrontendUpdate) (*vdom.VDomBackendUpdate, error) {
+ if feUpdate.Dispose {
+ log.Printf("got dispose from frontend\n")
+ impl.Client.doShutdown("got dispose from frontend")
+ return nil, nil
+ }
+ if impl.Client.GetIsDone() {
+ return nil, nil
+ }
+ // set atoms
+ for _, ss := range feUpdate.StateSync {
+ impl.Client.Root.SetAtomVal(ss.Atom, ss.Value, false)
+ }
+ // run events
+ for _, event := range feUpdate.Events {
+ if event.WaveId == "" {
+ if impl.Client.GlobalEventHandler != nil {
+ impl.Client.GlobalEventHandler(impl.Client, event)
+ }
+ } else {
+ impl.Client.Root.Event(event.WaveId, event.EventType, event.EventData)
+ }
+ }
+ if feUpdate.Resync {
+ return impl.Client.fullRender()
+ }
+ return impl.Client.incrementalRender()
+}
+
+func (impl *VDomServerImpl) VDomUrlRequestCommand(ctx context.Context, data wshrpc.VDomUrlRequestData) chan wshrpc.RespOrErrorUnion[wshrpc.VDomUrlRequestResponse] {
+ log.Printf("VDomUrlRequestCommand: url=%q\n", data.URL)
+ respChan := make(chan wshrpc.RespOrErrorUnion[wshrpc.VDomUrlRequestResponse])
+ writer := NewStreamingResponseWriter(respChan)
+
+ go func() {
+ 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
+ var bodyReader *bytes.Reader
+ if data.Body != nil {
+ bodyReader = bytes.NewReader(data.Body)
+ } else {
+ bodyReader = bytes.NewReader([]byte{})
+ }
+
+ httpReq, err := http.NewRequest(data.Method, data.URL, bodyReader)
+ if err != nil {
+ writer.WriteHeader(http.StatusInternalServerError)
+ writer.Write([]byte(err.Error()))
+ return
+ }
+
+ for key, value := range data.Headers {
+ httpReq.Header.Set(key, value)
+ }
+
+ if impl.Client.OverrideUrlHandler != nil {
+ impl.Client.OverrideUrlHandler.ServeHTTP(writer, httpReq)
+ return
+ }
+ impl.Client.UrlHandlerMux.ServeHTTP(writer, httpReq)
+ }()
+
+ return respChan
+}
diff --git a/pkg/wshrpc/wshclient/wshclient.go b/pkg/wshrpc/wshclient/wshclient.go
index 86cbac5c3..536550d67 100644
--- a/pkg/wshrpc/wshclient/wshclient.go
+++ b/pkg/wshrpc/wshclient/wshclient.go
@@ -303,6 +303,11 @@ func VDomRenderCommand(w *wshutil.WshRpc, data vdom.VDomFrontendUpdate, opts *ws
return resp, err
}
+// command "vdomurlrequest", wshserver.VDomUrlRequestCommand
+func VDomUrlRequestCommand(w *wshutil.WshRpc, data wshrpc.VDomUrlRequestData, opts *wshrpc.RpcOpts) chan wshrpc.RespOrErrorUnion[wshrpc.VDomUrlRequestResponse] {
+ return sendRpcRequestResponseStreamHelper[wshrpc.VDomUrlRequestResponse](w, "vdomurlrequest", data, opts)
+}
+
// command "waitforroute", wshserver.WaitForRouteCommand
func WaitForRouteCommand(w *wshutil.WshRpc, data wshrpc.CommandWaitForRouteData, opts *wshrpc.RpcOpts) (bool, error) {
resp, err := sendRpcRequestCallHelper[bool](w, "waitforroute", data, opts)
diff --git a/pkg/wshrpc/wshrpctypes.go b/pkg/wshrpc/wshrpctypes.go
index eefa6c00c..e1e74d1da 100644
--- a/pkg/wshrpc/wshrpctypes.go
+++ b/pkg/wshrpc/wshrpctypes.go
@@ -81,6 +81,7 @@ const (
Command_VDomCreateContext = "vdomcreatecontext"
Command_VDomAsyncInitiation = "vdomasyncinitiation"
Command_VDomRender = "vdomrender"
+ Command_VDomUrlRequest = "vdomurlrequest"
)
type RespOrErrorUnion[T any] struct {
@@ -157,6 +158,7 @@ type WshRpcInterface interface {
// proc
VDomRenderCommand(ctx context.Context, data vdom.VDomFrontendUpdate) (*vdom.VDomBackendUpdate, error)
+ VDomUrlRequestCommand(ctx context.Context, data VDomUrlRequestData) chan RespOrErrorUnion[VDomUrlRequestResponse]
}
// for frontend
@@ -431,6 +433,19 @@ type WaveNotificationOptions struct {
Silent bool `json:"silent,omitempty"`
}
+type VDomUrlRequestData struct {
+ Method string `json:"method"`
+ URL string `json:"url"`
+ Headers map[string]string `json:"headers"`
+ Body []byte `json:"body,omitempty"`
+}
+
+type VDomUrlRequestResponse struct {
+ StatusCode int `json:"statuscode,omitempty"`
+ Headers map[string]string `json:"headers,omitempty"`
+ Body []byte `json:"body,omitempty"`
+}
+
type WaveInfoData struct {
Version string `json:"version"`
BuildTime string `json:"buildtime"`