mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-02 18:39:05 +01:00
vdom 7 (#1180)
This commit is contained in:
parent
6ed812c8ea
commit
eeda49bbde
@ -81,9 +81,14 @@ func StyleTag(ctx context.Context, props map[string]any) any {
|
||||
`, nil)
|
||||
}
|
||||
|
||||
func BgItemTag(ctx context.Context, props map[string]any) any {
|
||||
type BgItemProps struct {
|
||||
Bg string
|
||||
Label string
|
||||
}
|
||||
|
||||
func BgItemTag(ctx context.Context, props BgItemProps) any {
|
||||
clickFn := func() {
|
||||
log.Printf("bg item clicked %q\n", props["bg"])
|
||||
log.Printf("bg item clicked %q\n", props.Bg)
|
||||
blockInfo, err := wshclient.BlockInfoCommand(GlobalVDomClient.RpcClient, GlobalVDomClient.RpcContext.BlockId, nil)
|
||||
if err != nil {
|
||||
log.Printf("error getting block info: %v\n", err)
|
||||
@ -92,7 +97,7 @@ func BgItemTag(ctx context.Context, props map[string]any) any {
|
||||
log.Printf("block info: tabid=%q\n", blockInfo.TabId)
|
||||
err = wshclient.SetMetaCommand(GlobalVDomClient.RpcClient, wshrpc.CommandSetMetaData{
|
||||
ORef: waveobj.ORef{OType: "tab", OID: blockInfo.TabId},
|
||||
Meta: map[string]any{"bg": props["bg"]},
|
||||
Meta: map[string]any{"bg": props.Bg},
|
||||
}, nil)
|
||||
if err != nil {
|
||||
log.Printf("error setting meta: %v\n", err)
|
||||
@ -100,8 +105,8 @@ func BgItemTag(ctx context.Context, props map[string]any) any {
|
||||
// wshclient.SetMetaCommand(GlobalVDomClient.RpcClient)
|
||||
}
|
||||
params := map[string]any{
|
||||
"bg": props["bg"],
|
||||
"label": props["label"],
|
||||
"bg": props.Bg,
|
||||
"label": props.Label,
|
||||
"clickHandler": clickFn,
|
||||
}
|
||||
return vdom.Bind(`
|
||||
@ -141,7 +146,10 @@ func MakeVDom() *vdom.VDomElem {
|
||||
<wave:markdown text="*quick vdom application to set background colors*"/>
|
||||
</div>
|
||||
<div>
|
||||
<AllBgItemsTag/>
|
||||
<AllBgItemsTag/>
|
||||
</div>
|
||||
<div>
|
||||
<img style="width: 100%; height: 100%; max-width: 300px; max-height: 300px; object-fit: contain;" src="vdom:///test.png"/>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
@ -169,6 +177,7 @@ func htmlRun(cmd *cobra.Command, args []string) error {
|
||||
client.RegisterComponent("StyleTag", StyleTag)
|
||||
client.RegisterComponent("BgItemTag", BgItemTag)
|
||||
client.RegisterComponent("AllBgItemsTag", AllBgItemsTag)
|
||||
client.RegisterFileHandler("/test.png", "~/Downloads/IMG_1939.png")
|
||||
client.SetRootElem(MakeVDom())
|
||||
err = client.CreateVDomContext(&vdom.VDomTarget{NewBlock: htmlCmdNewBlock})
|
||||
if err != nil {
|
||||
|
100
emain/emain-vdomhandler.ts
Normal file
100
emain/emain-vdomhandler.ts
Normal file
@ -0,0 +1,100 @@
|
||||
import { protocol } from "electron";
|
||||
import { RpcApi } from "../frontend/app/store/wshclientapi";
|
||||
import { base64ToArray } from "../frontend/util/util";
|
||||
import { ElectronWshClient } from "./emain-wsh";
|
||||
|
||||
export function setupVdomUrlHandler() {
|
||||
protocol.handle("vdom", async (request) => {
|
||||
// Only handle GET requests for now
|
||||
if (request.method !== "GET") {
|
||||
return new Response(null, {
|
||||
status: 405,
|
||||
headers: {
|
||||
"Content-Type": "text/plain",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const parts = request.url.split("/");
|
||||
const uuid = parts[2];
|
||||
// simple error checking for uuid
|
||||
if (!uuid || uuid.length !== 36) {
|
||||
return new Response(null, {
|
||||
status: 400,
|
||||
headers: {
|
||||
"Content-Type": "text/plain",
|
||||
},
|
||||
});
|
||||
}
|
||||
const path = "/" + parts.slice(3).join("/");
|
||||
|
||||
// Convert Headers object to plain object
|
||||
const headers: Record<string, string> = {};
|
||||
for (const [key, value] of request.headers.entries()) {
|
||||
headers[key] = value;
|
||||
}
|
||||
|
||||
const data: VDomUrlRequestData = {
|
||||
method: "GET",
|
||||
url: path,
|
||||
headers: headers,
|
||||
};
|
||||
|
||||
try {
|
||||
const respStream = RpcApi.VDomUrlRequestCommand(ElectronWshClient, data, {
|
||||
route: `proc:${uuid}`,
|
||||
});
|
||||
|
||||
// Get iterator for the stream
|
||||
const iterator = respStream[Symbol.asyncIterator]();
|
||||
|
||||
// Get first chunk to extract headers and status
|
||||
const firstChunk = await iterator.next();
|
||||
if (firstChunk.done) {
|
||||
throw new Error("No response received from backend");
|
||||
}
|
||||
|
||||
const firstResp = firstChunk.value as VDomUrlRequestResponse;
|
||||
const statusCode = firstResp.statuscode ?? 200;
|
||||
const responseHeaders = firstResp.headers ?? {};
|
||||
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
try {
|
||||
// Enqueue the body from the first chunk if it exists
|
||||
if (firstResp.body) {
|
||||
controller.enqueue(base64ToArray(firstResp.body));
|
||||
}
|
||||
|
||||
// Process the rest of the stream
|
||||
while (true) {
|
||||
const chunk = await iterator.next();
|
||||
if (chunk.done) break;
|
||||
|
||||
const resp = chunk.value as VDomUrlRequestResponse;
|
||||
if (resp.body) {
|
||||
controller.enqueue(base64ToArray(resp.body));
|
||||
}
|
||||
}
|
||||
controller.close();
|
||||
} catch (err) {
|
||||
controller.error(err);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
status: statusCode,
|
||||
headers: responseHeaders,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("VDOM URL handler error:", err);
|
||||
return new Response(null, {
|
||||
status: 500,
|
||||
headers: {
|
||||
"Content-Type": "text/plain",
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as electron from "electron";
|
||||
import { setupVdomUrlHandler } from "emain/emain-vdomhandler";
|
||||
import { FastAverageColor } from "fast-average-color";
|
||||
import fs from "fs";
|
||||
import * as child_process from "node:child_process";
|
||||
@ -708,6 +709,7 @@ async function appMain() {
|
||||
const ready = await getWaveSrvReady();
|
||||
console.log("wavesrv ready signal received", ready, Date.now() - startTs, "ms");
|
||||
await electronApp.whenReady();
|
||||
setupVdomUrlHandler();
|
||||
configureAuthKeyRequestInjection(electron.session.defaultSession);
|
||||
const fullConfig = await services.FileService.GetFullConfig();
|
||||
ensureHotSpareTab(fullConfig);
|
||||
@ -721,7 +723,6 @@ async function appMain() {
|
||||
console.log("error initializing wshrpc", e);
|
||||
}
|
||||
await configureAutoUpdater();
|
||||
|
||||
setGlobalIsStarting(false);
|
||||
if (fullConfig?.settings?.["window:maxtabcachesize"] != null) {
|
||||
setMaxTabCacheSize(fullConfig.settings["window:maxtabcachesize"]);
|
||||
|
@ -405,7 +405,7 @@
|
||||
&.block-no-highlight,
|
||||
&.block-preview {
|
||||
.block-mask {
|
||||
border: 2px solid rgba(255, 255, 255, 0.1);
|
||||
border: 2px solid rgba(255, 255, 255, 0.1) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -252,6 +252,11 @@ class RpcApiType {
|
||||
return client.wshRpcCall("vdomrender", data, opts);
|
||||
}
|
||||
|
||||
// command "vdomurlrequest" [responsestream]
|
||||
VDomUrlRequestCommand(client: WshClient, data: VDomUrlRequestData, opts?: RpcOpts): AsyncGenerator<VDomUrlRequestResponse, void, boolean> {
|
||||
return client.wshRpcStream("vdomurlrequest", data, opts);
|
||||
}
|
||||
|
||||
// command "waitforroute" [call]
|
||||
WaitForRouteCommand(client: WshClient, data: CommandWaitForRouteData, opts?: RpcOpts): Promise<boolean> {
|
||||
return client.wshRpcCall("waitforroute", data, opts);
|
||||
|
@ -46,24 +46,8 @@
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
|
||||
.term-htmlelem-focus {
|
||||
height: 0;
|
||||
width: 0;
|
||||
input {
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.term-htmlelem-content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
flex-grow: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
.block-content {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -592,4 +592,12 @@ export class VDomModel {
|
||||
}
|
||||
return feUpdate;
|
||||
}
|
||||
|
||||
getBackendRouteId(): string {
|
||||
const fullRoute = globalStore.get(this.backendRoute);
|
||||
if (fullRoute == null || !fullRoute.startsWith("proc:")) {
|
||||
return null;
|
||||
}
|
||||
return fullRoute?.split(":")[1];
|
||||
}
|
||||
}
|
||||
|
@ -46,6 +46,33 @@ export function validateAndWrapCss(model: VDomModel, cssText: string, wrapperCla
|
||||
if (node.type === "IdSelector") {
|
||||
node.name = convertVDomId(model, node.name);
|
||||
}
|
||||
|
||||
// Transform url(#id) references in filter and mask properties (svg)
|
||||
if (node.type === "Declaration" && ["filter", "mask"].includes(node.property)) {
|
||||
if (node.value && node.value.type === "Value" && "children" in node.value) {
|
||||
const urlNode = node.value.children
|
||||
.toArray()
|
||||
.find(
|
||||
(child: CssNode): child is CssNode & { value: string } =>
|
||||
child && child.type === "Url" && typeof (child as any).value === "string"
|
||||
);
|
||||
if (urlNode && urlNode.value && urlNode.value.startsWith("#")) {
|
||||
urlNode.value = "#" + convertVDomId(model, urlNode.value.substring(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
// transform url(vdom:///foo.jpg) => url(vdom://blockId/foo.jpg)
|
||||
if (node.type === "Url") {
|
||||
const url = node.value;
|
||||
if (url != null && url.startsWith("vdom://")) {
|
||||
const absUrl = url.substring(7);
|
||||
if (!absUrl.startsWith("/")) {
|
||||
list.remove(item);
|
||||
} else {
|
||||
node.value = "vdom://" + model.blockId + url.substring(7);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
const sanitizedCss = csstree.generate(ast);
|
||||
@ -56,3 +83,56 @@ export function validateAndWrapCss(model: VDomModel, cssText: string, wrapperCla
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function cssTransformStyleValue(model: VDomModel, property: string, value: string): string {
|
||||
try {
|
||||
const ast = csstree.parse(value, { context: "value" });
|
||||
csstree.walk(ast, {
|
||||
enter(node) {
|
||||
// Transform url(#id) in filter/mask properties
|
||||
if (node.type === "Url" && (property === "filter" || property === "mask")) {
|
||||
if (node.value.startsWith("#")) {
|
||||
node.value = `#${convertVDomId(model, node.value.substring(1))}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Transform vdom:/// URLs
|
||||
if (node.type === "Url" && node.value.startsWith("vdom:///")) {
|
||||
const absUrl = node.value.substring(7);
|
||||
if (absUrl.startsWith("/")) {
|
||||
node.value = `vdom://${model.blockId}${absUrl}`;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return csstree.generate(ast);
|
||||
} catch (error) {
|
||||
console.error("Error processing style value:", error);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
export function validateAndWrapReactStyle(model: VDomModel, style: Record<string, any>): Record<string, any> {
|
||||
const sanitizedStyle: Record<string, any> = {};
|
||||
let updated = false;
|
||||
for (const [property, value] of Object.entries(style)) {
|
||||
if (value == null || value === "") {
|
||||
continue;
|
||||
}
|
||||
if (typeof value !== "string") {
|
||||
sanitizedStyle[property] = value; // For non-string values, just copy as-is
|
||||
continue;
|
||||
}
|
||||
if (value.includes("vdom://") || value.includes("url(#")) {
|
||||
updated = true;
|
||||
sanitizedStyle[property] = cssTransformStyleValue(model, property, value);
|
||||
} else {
|
||||
sanitizedStyle[property] = value;
|
||||
}
|
||||
}
|
||||
if (!updated) {
|
||||
return style;
|
||||
}
|
||||
return sanitizedStyle;
|
||||
}
|
||||
|
@ -2,4 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
.view-vdom {
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
@ -10,7 +10,12 @@ import * as jotai from "jotai";
|
||||
import * as React from "react";
|
||||
|
||||
import { BlockNodeModel } from "@/app/block/blocktypes";
|
||||
import { convertVDomId, getTextChildren, validateAndWrapCss } from "@/app/view/vdom/vdom-utils";
|
||||
import {
|
||||
convertVDomId,
|
||||
getTextChildren,
|
||||
validateAndWrapCss,
|
||||
validateAndWrapReactStyle,
|
||||
} from "@/app/view/vdom/vdom-utils";
|
||||
import "./vdom.less";
|
||||
|
||||
const TextTag = "#text";
|
||||
@ -68,6 +73,50 @@ const AllowedSimpleTags: { [tagName: string]: boolean } = {
|
||||
code: true,
|
||||
};
|
||||
|
||||
const AllowedSvgTags = {
|
||||
// SVG tags
|
||||
svg: true,
|
||||
circle: true,
|
||||
ellipse: true,
|
||||
line: true,
|
||||
path: true,
|
||||
polygon: true,
|
||||
polyline: true,
|
||||
rect: true,
|
||||
g: true,
|
||||
text: true,
|
||||
tspan: true,
|
||||
textPath: true,
|
||||
use: true,
|
||||
defs: true,
|
||||
linearGradient: true,
|
||||
radialGradient: true,
|
||||
stop: true,
|
||||
clipPath: true,
|
||||
mask: true,
|
||||
pattern: true,
|
||||
image: true,
|
||||
marker: true,
|
||||
symbol: true,
|
||||
filter: true,
|
||||
feBlend: true,
|
||||
feColorMatrix: true,
|
||||
feComponentTransfer: true,
|
||||
feComposite: true,
|
||||
feConvolveMatrix: true,
|
||||
feDiffuseLighting: true,
|
||||
feDisplacementMap: true,
|
||||
feFlood: true,
|
||||
feGaussianBlur: true,
|
||||
feImage: true,
|
||||
feMerge: true,
|
||||
feMorphology: true,
|
||||
feOffset: true,
|
||||
feSpecularLighting: true,
|
||||
feTile: true,
|
||||
feTurbulence: true,
|
||||
};
|
||||
|
||||
const IdAttributes = {
|
||||
id: true,
|
||||
for: true,
|
||||
@ -81,6 +130,18 @@ const IdAttributes = {
|
||||
list: true,
|
||||
};
|
||||
|
||||
const SvgUrlIdAttributes = {
|
||||
"clip-path": true,
|
||||
mask: true,
|
||||
filter: true,
|
||||
fill: true,
|
||||
stroke: true,
|
||||
"marker-start": true,
|
||||
"marker-mid": true,
|
||||
"marker-end": true,
|
||||
"text-decoration": true,
|
||||
};
|
||||
|
||||
function convertVDomFunc(model: VDomModel, fnDecl: VDomFunc, compId: string, propName: string): (e: any) => void {
|
||||
return (e: any) => {
|
||||
if ((propName == "onKeyDown" || propName == "onKeyDownCapture") && fnDecl["#keys"]) {
|
||||
@ -193,28 +254,62 @@ function convertProps(elem: VDomElem, model: VDomModel): [GenericPropsType, Set<
|
||||
}
|
||||
}
|
||||
}
|
||||
// fallthrough to set props[key] = val
|
||||
val = validateAndWrapReactStyle(model, val);
|
||||
props[key] = val;
|
||||
continue;
|
||||
}
|
||||
if (IdAttributes[key]) {
|
||||
props[key] = convertVDomId(model, val);
|
||||
continue;
|
||||
}
|
||||
if (AllowedSvgTags[elem.tag]) {
|
||||
if ((elem.tag == "use" && key == "href") || (elem.tag == "textPath" && key == "href")) {
|
||||
if (val == null || !val.startsWith("#")) {
|
||||
continue;
|
||||
}
|
||||
props[key] = convertVDomId(model, "#" + val.substring(1));
|
||||
continue;
|
||||
}
|
||||
if (SvgUrlIdAttributes[key]) {
|
||||
if (val == null || !val.startsWith("url(#") || !val.endsWith(")")) {
|
||||
continue;
|
||||
}
|
||||
props[key] = "url(#" + convertVDomId(model, val.substring(4, val.length - 1)) + ")";
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (key == "src" && val != null && val.startsWith("vdom://")) {
|
||||
// we're going to convert vdom:///foo.jpg to vdom://blockid/foo.jpg. if it doesn't start with "/" it is not valid
|
||||
const vdomUrl = val.substring(7);
|
||||
if (!vdomUrl.startsWith("/")) {
|
||||
continue;
|
||||
}
|
||||
const backendRouteId = model.getBackendRouteId();
|
||||
if (backendRouteId == null) {
|
||||
continue;
|
||||
}
|
||||
props[key] = "vdom://" + backendRouteId + vdomUrl;
|
||||
continue;
|
||||
}
|
||||
props[key] = val;
|
||||
}
|
||||
return [props, atomKeys];
|
||||
}
|
||||
|
||||
function convertChildren(elem: VDomElem, model: VDomModel): (string | JSX.Element)[] {
|
||||
let childrenComps: (string | JSX.Element)[] = [];
|
||||
if (elem.children == null) {
|
||||
return childrenComps;
|
||||
if (elem.children == null || elem.children.length == 0) {
|
||||
return null;
|
||||
}
|
||||
let childrenComps: (string | JSX.Element)[] = [];
|
||||
for (let child of elem.children) {
|
||||
if (child == null) {
|
||||
continue;
|
||||
}
|
||||
childrenComps.push(convertElemToTag(child, model));
|
||||
}
|
||||
if (childrenComps.length == 0) {
|
||||
return null;
|
||||
}
|
||||
return childrenComps;
|
||||
}
|
||||
|
||||
@ -286,7 +381,7 @@ function VDomTag({ elem, model }: { elem: VDomElem; model: VDomModel }) {
|
||||
if (elem.tag == StyleTagName) {
|
||||
return <StyleTag elem={elem} model={model} />;
|
||||
}
|
||||
if (!AllowedSimpleTags[elem.tag]) {
|
||||
if (!AllowedSimpleTags[elem.tag] && !AllowedSvgTags[elem.tag]) {
|
||||
return <div>{"Invalid Tag <" + elem.tag + ">"}</div>;
|
||||
}
|
||||
let childrenComps = convertChildren(elem, model);
|
||||
@ -345,7 +440,7 @@ function VDomView({ blockId, model }: VDomViewProps) {
|
||||
model.viewRef = viewRef;
|
||||
const vdomClass = "vdom-" + blockId;
|
||||
return (
|
||||
<div className={clsx("vdom-view", vdomClass)} ref={viewRef}>
|
||||
<div className={clsx("view-vdom", vdomClass)} ref={viewRef}>
|
||||
<VDomRoot model={model} />
|
||||
</div>
|
||||
);
|
||||
|
15
frontend/types/gotypes.d.ts
vendored
15
frontend/types/gotypes.d.ts
vendored
@ -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 );
|
||||
|
2
go.mod
2
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
|
||||
|
4
go.sum
4
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=
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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(`<div data1={5} data2={[1,2,3]} data3={{"a": 1}}/>`, 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)
|
||||
}
|
||||
|
129
pkg/vdom/vdomclient/streamingresp.go
Normal file
129
pkg/vdom/vdomclient/streamingresp.go
Normal file
@ -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
|
||||
}
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
95
pkg/vdom/vdomclient/vdomserverimpl.go
Normal file
95
pkg/vdom/vdomclient/vdomserverimpl.go
Normal file
@ -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
|
||||
}
|
@ -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)
|
||||
|
@ -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"`
|
||||
|
Loading…
Reference in New Issue
Block a user