waveterm/pkg/vdom/vdomclient/vdomclient.go

349 lines
10 KiB
Go
Raw Normal View History

2024-10-17 23:50:36 +02:00
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package vdomclient
import (
"context"
2024-10-17 23:50:36 +02:00
"fmt"
"io"
"io/fs"
2024-10-17 23:50:36 +02:00
"log"
2024-11-02 18:58:13 +01:00
"net/http"
2024-10-17 23:50:36 +02:00
"os"
"sync"
"time"
"unicode"
2024-10-17 23:50:36 +02:00
"github.com/google/uuid"
2024-11-02 18:58:13 +01:00
"github.com/gorilla/mux"
2024-10-17 23:50:36 +02:00
"github.com/wavetermdev/waveterm/pkg/vdom"
2024-11-02 18:58:13 +01:00
"github.com/wavetermdev/waveterm/pkg/wavebase"
2024-10-17 23:50:36 +02:00
"github.com/wavetermdev/waveterm/pkg/wps"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
"github.com/wavetermdev/waveterm/pkg/wshutil"
)
type Client struct {
2024-10-24 07:47:29 +02:00
Lock *sync.Mutex
2024-10-17 23:50:36 +02:00
Root *vdom.RootElem
RootElem *vdom.VDomElem
RpcClient *wshutil.WshRpc
RpcContext *wshrpc.RpcContext
ServerImpl *VDomServerImpl
IsDone bool
RouteId string
2024-10-24 07:47:29 +02:00
VDomContextBlockId string
2024-10-17 23:50:36 +02:00
DoneReason string
DoneCh chan struct{}
Opts vdom.VDomBackendOpts
GlobalEventHandler func(client *Client, event vdom.VDomEvent)
2024-11-02 18:58:13 +01:00
UrlHandlerMux *mux.Router
OverrideUrlHandler http.Handler
2024-10-17 23:50:36 +02:00
}
2024-10-24 07:47:29 +02:00
func (c *Client) GetIsDone() bool {
c.Lock.Lock()
defer c.Lock.Unlock()
return c.IsDone
}
2024-10-17 23:50:36 +02:00
func (c *Client) doShutdown(reason string) {
2024-10-24 07:47:29 +02:00
c.Lock.Lock()
defer c.Lock.Unlock()
if c.IsDone {
return
}
c.DoneReason = reason
c.IsDone = true
close(c.DoneCh)
2024-10-17 23:50:36 +02:00
}
func (c *Client) SetGlobalEventHandler(handler func(client *Client, event vdom.VDomEvent)) {
c.GlobalEventHandler = handler
}
2024-11-02 18:58:13 +01:00
func (c *Client) SetOverrideUrlHandler(handler http.Handler) {
c.OverrideUrlHandler = handler
}
func MakeClient(opts *vdom.VDomBackendOpts) *Client {
2024-10-17 23:50:36 +02:00
client := &Client{
2024-11-02 18:58:13 +01:00
Lock: &sync.Mutex{},
Root: vdom.MakeRoot(),
DoneCh: make(chan struct{}),
UrlHandlerMux: mux.NewRouter(),
2024-10-17 23:50:36 +02:00
}
if opts != nil {
client.Opts = *opts
}
return client
}
func (client *Client) Connect() error {
2024-10-17 23:50:36 +02:00
jwtToken := os.Getenv(wshutil.WaveJwtTokenVarName)
if jwtToken == "" {
return fmt.Errorf("no %s env var set", wshutil.WaveJwtTokenVarName)
2024-10-17 23:50:36 +02:00
}
rpcCtx, err := wshutil.ExtractUnverifiedRpcContext(jwtToken)
if err != nil {
return fmt.Errorf("error extracting rpc context from %s: %v", wshutil.WaveJwtTokenVarName, err)
2024-10-17 23:50:36 +02:00
}
client.RpcContext = rpcCtx
if client.RpcContext == nil || client.RpcContext.BlockId == "" {
return fmt.Errorf("no block id in rpc context")
2024-10-17 23:50:36 +02:00
}
client.ServerImpl = &VDomServerImpl{BlockId: client.RpcContext.BlockId, Client: client}
sockName, err := wshutil.ExtractUnverifiedSocketName(jwtToken)
if err != nil {
return fmt.Errorf("error extracting socket name from %s: %v", wshutil.WaveJwtTokenVarName, err)
2024-10-17 23:50:36 +02:00
}
rpcClient, err := wshutil.SetupDomainSocketRpcClient(sockName, client.ServerImpl)
if err != nil {
return fmt.Errorf("error setting up domain socket rpc client: %v", err)
2024-10-17 23:50:36 +02:00
}
client.RpcClient = rpcClient
authRtn, err := wshclient.AuthenticateCommand(client.RpcClient, jwtToken, &wshrpc.RpcOpts{NoResponse: true})
if err != nil {
return fmt.Errorf("error authenticating rpc connection: %v", err)
2024-10-17 23:50:36 +02:00
}
client.RouteId = authRtn.RouteId
return nil
2024-10-17 23:50:36 +02:00
}
func (c *Client) SetRootElem(elem *vdom.VDomElem) {
c.RootElem = elem
}
2024-10-24 07:47:29 +02:00
func (c *Client) CreateVDomContext(target *vdom.VDomTarget) error {
blockORef, err := wshclient.VDomCreateContextCommand(
c.RpcClient,
vdom.VDomCreateContext{Target: target},
&wshrpc.RpcOpts{Route: wshutil.MakeFeBlockRouteId(c.RpcContext.BlockId)},
)
2024-10-17 23:50:36 +02:00
if err != nil {
return err
}
2024-10-24 07:47:29 +02:00
c.VDomContextBlockId = blockORef.OID
log.Printf("created vdom context: %v\n", blockORef)
gotRoute, err := wshclient.WaitForRouteCommand(c.RpcClient, wshrpc.CommandWaitForRouteData{
RouteId: wshutil.MakeFeBlockRouteId(blockORef.OID),
WaitMs: 4000,
}, &wshrpc.RpcOpts{Timeout: 5000})
if err != nil {
return fmt.Errorf("error waiting for vdom context route: %v", err)
}
if !gotRoute {
return fmt.Errorf("vdom context route could not be established")
}
wshclient.EventSubCommand(c.RpcClient, wps.SubscriptionRequest{Event: wps.Event_BlockClose, Scopes: []string{
blockORef.String(),
2024-10-17 23:50:36 +02:00
}}, nil)
c.RpcClient.EventListener.On("blockclose", func(event *wps.WaveEvent) {
c.doShutdown("got blockclose event")
})
return nil
}
2024-10-24 07:47:29 +02:00
func (c *Client) SendAsyncInitiation() error {
if c.VDomContextBlockId == "" {
return fmt.Errorf("no vdom context block id")
}
if c.GetIsDone() {
return fmt.Errorf("client is done")
}
return wshclient.VDomAsyncInitiationCommand(
c.RpcClient,
vdom.MakeAsyncInitiationRequest(c.RpcContext.BlockId),
&wshrpc.RpcOpts{Route: wshutil.MakeFeBlockRouteId(c.VDomContextBlockId)},
)
2024-10-17 23:50:36 +02:00
}
func (c *Client) SetAtomVals(m map[string]any) {
for k, v := range m {
c.Root.SetAtomVal(k, v, true)
}
}
func (c *Client) SetAtomVal(name string, val any) {
c.Root.SetAtomVal(name, val, true)
}
func (c *Client) GetAtomVal(name string) any {
return c.Root.GetAtomVal(name)
}
func makeNullVDom() *vdom.VDomElem {
return &vdom.VDomElem{WaveId: uuid.New().String(), Tag: vdom.WaveNullTag}
}
func DefineComponent[P any](client *Client, name string, renderFn func(ctx context.Context, props P) any) vdom.Component[P] {
if name == "" {
panic("Component name cannot be empty")
}
if !unicode.IsUpper(rune(name[0])) {
panic("Component name must start with an uppercase letter")
}
client.RegisterComponent(name, renderFn)
return func(props P) *vdom.VDomElem {
return vdom.E(name, vdom.Props(props))
}
}
2024-11-02 18:58:13 +01:00
func (c *Client) RegisterComponent(name string, cfunc any) error {
return c.Root.RegisterComponent(name, cfunc)
2024-10-25 22:45:00 +02:00
}
2024-10-17 23:50:36 +02:00
func (c *Client) fullRender() (*vdom.VDomBackendUpdate, error) {
c.Root.RunWork()
c.Root.Render(c.RootElem)
renderedVDom := c.Root.MakeVDom()
if renderedVDom == nil {
renderedVDom = makeNullVDom()
}
return &vdom.VDomBackendUpdate{
Type: "backendupdate",
Ts: time.Now().UnixMilli(),
BlockId: c.RpcContext.BlockId,
HasWork: len(c.Root.EffectWorkQueue) > 0,
2024-10-17 23:50:36 +02:00
Opts: &c.Opts,
RenderUpdates: []vdom.VDomRenderUpdate{
{UpdateType: "root", VDom: renderedVDom},
2024-10-17 23:50:36 +02:00
},
RefOperations: c.Root.GetRefOperations(),
StateSync: c.Root.GetStateSync(true),
2024-10-17 23:50:36 +02:00
}, nil
}
func (c *Client) incrementalRender() (*vdom.VDomBackendUpdate, error) {
c.Root.RunWork()
renderedVDom := c.Root.MakeVDom()
if renderedVDom == nil {
renderedVDom = makeNullVDom()
}
return &vdom.VDomBackendUpdate{
Type: "backendupdate",
Ts: time.Now().UnixMilli(),
BlockId: c.RpcContext.BlockId,
RenderUpdates: []vdom.VDomRenderUpdate{
{UpdateType: "root", VDom: renderedVDom},
2024-10-17 23:50:36 +02:00
},
RefOperations: c.Root.GetRefOperations(),
StateSync: c.Root.GetStateSync(false),
2024-10-17 23:50:36 +02:00
}, nil
}
2024-11-02 18:58:13 +01:00
func (c *Client) RegisterUrlPathHandler(path string, handler http.Handler) {
c.UrlHandlerMux.Handle(path, handler)
}
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) {
2024-11-02 18:58:13 +01:00
c.UrlHandlerMux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
// 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)
}
2024-11-02 18:58:13 +01:00
})
}