new wshrpc mechanism (#112)

lots of changes. new wshrpc implementation. unify websocket, web,
blockcontroller, domain sockets, and terminal inputs to all use the new
rpc system.

lots of moving files around to deal with circular dependencies

use new wshrpc as a client in wsh cmd
This commit is contained in:
Mike Sawka
2024-07-17 15:24:43 -07:00
committed by GitHub
parent b178434c0a
commit 01b5d71709
49 changed files with 1810 additions and 1996 deletions
-1
View File
@@ -420,7 +420,6 @@ const BlockFrame = React.memo((props: BlockFrameProps) => {
});
function blockViewToIcon(view: string): string {
console.log("blockViewToIcon", view);
if (view == "term") {
return "terminal";
}
+8 -2
View File
@@ -5,6 +5,7 @@ import { LayoutTreeAction, LayoutTreeActionType, LayoutTreeInsertNodeAction, new
import { getLayoutStateAtomForTab } from "@/faraday/lib/layoutAtom";
import { layoutTreeStateReducer } from "@/faraday/lib/layoutState";
import { handleIncomingRpcMessage } from "@/app/store/wshrpc";
import * as layoututil from "@/util/layoututil";
import { produce } from "immer";
import * as jotai from "jotai";
@@ -27,8 +28,8 @@ let globalClientId: string = null;
if (typeof window !== "undefined") {
// this if statement allows us to use the code in nodejs as well
const urlParams = new URLSearchParams(window.location.search);
globalWindowId = urlParams.get("windowid") || "74eba2d0-22fc-4221-82ad-d028dd496342";
globalClientId = urlParams.get("clientid") || "f4bc1713-a364-41b3-a5c4-b000ba10d622";
globalWindowId = urlParams.get("windowid");
globalClientId = urlParams.get("clientid");
}
const windowIdAtom = jotai.atom(null) as jotai.PrimitiveAtom<string>;
const clientIdAtom = jotai.atom(null) as jotai.PrimitiveAtom<string>;
@@ -223,6 +224,11 @@ function handleWSEventMessage(msg: WSEventType) {
}
return;
}
if (msg.eventtype == "rpc") {
const rpcMsg: RpcMessage = msg.data;
handleIncomingRpcMessage(rpcMsg);
return;
}
if (msg.eventtype == "layoutaction") {
const layoutAction: WSLayoutActionData = msg.data;
if (layoutAction.actiontype == LayoutTreeActionType.InsertNode) {
+5 -10
View File
@@ -13,14 +13,9 @@ class BlockServiceType {
SaveTerminalState(arg2: string, arg3: string, arg4: string, arg5: number): Promise<void> {
return WOS.callBackendService("block", "SaveTerminalState", Array.from(arguments))
}
// send command to block
SendCommand(cmd: string, arg3: BlockCommand): Promise<void> {
return WOS.callBackendService("block", "SendCommand", Array.from(arguments))
}
}
export const BlockService = new BlockServiceType()
export const BlockService = new BlockServiceType();
// clientservice.ClientService (client)
class ClientServiceType {
@@ -44,7 +39,7 @@ class ClientServiceType {
}
}
export const ClientService = new ClientServiceType()
export const ClientService = new ClientServiceType();
// fileservice.FileService (file)
class FileServiceType {
@@ -71,7 +66,7 @@ class FileServiceType {
}
}
export const FileService = new FileServiceType()
export const FileService = new FileServiceType();
// objectservice.ObjectService (object)
class ObjectServiceType {
@@ -126,7 +121,7 @@ class ObjectServiceType {
}
}
export const ObjectService = new ObjectServiceType()
export const ObjectService = new ObjectServiceType();
// windowservice.WindowService (window)
class WindowServiceType {
@@ -150,5 +145,5 @@ class WindowServiceType {
}
}
export const WindowService = new WindowServiceType()
export const WindowService = new WindowServiceType();
+47
View File
@@ -3,8 +3,10 @@
// WaveObjectStore
import { sendRpcCommand } from "@/app/store/wshrpc";
import * as jotai from "jotai";
import * as React from "react";
import { v4 as uuidv4 } from "uuid";
import { atoms, getBackendHostPort, globalStore } from "./global";
import * as services from "./services";
@@ -103,6 +105,50 @@ function callBackendService(service: string, method: string, args: any[], noUICo
return prtn;
}
function callWshServerRpc(
command: string,
data: any,
meta: WshServerCommandMeta,
opts: WshRpcCommandOpts
): Promise<any> {
let msg: RpcMessage = {
command: command,
data: data,
};
if (!opts?.noresponse) {
msg.reqid = uuidv4();
}
if (opts?.timeout) {
msg.timeout = opts.timeout;
}
if (meta.commandtype != "call") {
throw new Error("unimplemented wshserver commandtype " + meta.commandtype);
}
const rpcGen = sendRpcCommand(msg);
if (rpcGen == null) {
return null;
}
let resolveFn: (value: any) => void;
let rejectFn: (reason?: any) => void;
const prtn = new Promise((resolve, reject) => {
resolveFn = resolve;
rejectFn = reject;
});
const respMsg = rpcGen.next(true); // pass true to force termination of rpc after 1 response (not streaing)
respMsg.then((msg: IteratorResult<RpcMessage, void>) => {
if (msg.value == null) {
resolveFn(null);
}
let respMsg: RpcMessage = msg.value as RpcMessage;
if (respMsg.error != null) {
rejectFn(new Error(respMsg.error));
return;
}
resolveFn(respMsg.data);
});
return prtn;
}
const waveObjectValueCache = new Map<string, WaveObjectValue<any>>();
function clearWaveObjectCache() {
@@ -320,6 +366,7 @@ function setObjectValue<T extends WaveObj>(value: T, setFn?: jotai.Setter, pushT
export {
callBackendService,
callWshServerRpc,
cleanWaveObjectCache,
clearWaveObjectCache,
getObjectValue,
+3 -58
View File
@@ -3,19 +3,9 @@
import * as jotai from "jotai";
import { sprintf } from "sprintf-js";
import { v4 as uuidv4 } from "uuid";
const MaxWebSocketSendSize = 1024 * 1024; // 1MB
type RpcEntry = {
reqId: string;
startTs: number;
method: string;
resolve: (any) => void;
reject: (any) => void;
promise: Promise<any>;
};
type JotaiStore = {
get: <Value>(atom: jotai.Atom<Value>) => Value;
set: <Value>(atom: jotai.WritableAtom<Value, [Value], void>, value: Value) => void;
@@ -35,7 +25,6 @@ class WSControl {
authKey: string;
baseHostPort: string;
lastReconnectTime: number = 0;
rpcMap: Map<string, RpcEntry> = new Map(); // reqId -> RpcEntry
jotaiStore: JotaiStore;
constructor(
@@ -169,10 +158,6 @@ class WSControl {
this.reconnectTimes = 0;
return;
}
if (eventData.type == "rpcresp") {
this.handleRpcResp(eventData);
return;
}
if (this.messageCallback) {
try {
this.messageCallback(eventData);
@@ -189,60 +174,20 @@ class WSControl {
this.wsConn.send(JSON.stringify({ type: "ping", stime: Date.now() }));
}
handleRpcResp(data: any) {
let reqId = data.reqid;
let rpcEntry = this.rpcMap.get(reqId);
if (rpcEntry == null) {
console.log("rpcresp for unknown reqid", reqId);
return;
}
this.rpcMap.delete(reqId);
console.log("rpcresp", rpcEntry.method, Math.round(performance.now() - rpcEntry.startTs) + "ms");
if (data.error != null) {
rpcEntry.reject(data.error);
} else {
rpcEntry.resolve(data.data);
}
}
doRpc(method: string, params: any[]): Promise<any> {
if (!this.isOpen()) {
return Promise.reject("not connected");
}
let reqId = uuidv4();
let req = { type: "rpc", method: method, params: params, reqid: reqId };
let rpcEntry: RpcEntry = {
method: method,
startTs: performance.now(),
reqId: reqId,
resolve: null,
reject: null,
promise: null,
};
let rpcPromise = new Promise((resolve, reject) => {
rpcEntry.resolve = resolve;
rpcEntry.reject = reject;
});
rpcEntry.promise = rpcPromise;
this.rpcMap.set(reqId, rpcEntry);
this.wsConn.send(JSON.stringify(req));
return rpcPromise;
}
sendMessage(data: any) {
sendMessage(data: WSCommandType) {
if (!this.isOpen()) {
return;
}
let msg = JSON.stringify(data);
const byteSize = new Blob([msg]).size;
if (byteSize > MaxWebSocketSendSize) {
console.log("ws message too large", byteSize, data.type, msg.substring(0, 100));
console.log("ws message too large", byteSize, data.wscommand, msg.substring(0, 100));
return;
}
this.wsConn.send(msg);
}
pushMessage(data: any) {
pushMessage(data: WSCommandType) {
if (!this.isOpen()) {
this.msgQueue.push(data);
return;
+88
View File
@@ -0,0 +1,88 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { globalWS } from "./global";
type RpcEntry = {
reqId: string;
startTs: number;
command: string;
msgFn: (msg: RpcMessage) => void;
};
let openRpcs = new Map<string, RpcEntry>();
async function* rpcResponseGenerator(
command: string,
reqid: string,
timeout: number
): AsyncGenerator<RpcMessage, void, boolean> {
const msgQueue: RpcMessage[] = [];
let signalFn: () => void;
let signalPromise = new Promise<void>((resolve) => (signalFn = resolve));
let timeoutId: NodeJS.Timeout = null;
if (timeout > 0) {
timeoutId = setTimeout(() => {
msgQueue.push({ resid: reqid, error: "EC-TIME: timeout waiting for response" });
signalFn();
}, timeout);
}
const msgFn = (msg: RpcMessage) => {
msgQueue.push(msg);
signalFn();
// reset signal promise
signalPromise = new Promise<void>((resolve) => (signalFn = resolve));
};
openRpcs.set(reqid, {
reqId: reqid,
startTs: Date.now(),
command: command,
msgFn: msgFn,
});
try {
while (true) {
while (msgQueue.length > 0) {
const msg = msgQueue.shift()!;
const shouldTerminate = yield msg;
if (shouldTerminate || !msg.cont) {
return;
}
}
await signalPromise;
}
} finally {
openRpcs.delete(reqid);
if (timeoutId != null) {
clearTimeout(timeoutId);
}
}
}
function sendRpcCommand(msg: RpcMessage): AsyncGenerator<RpcMessage, void, boolean> {
let wsMsg: WSRpcCommand = { wscommand: "rpc", message: msg };
globalWS.pushMessage(wsMsg);
if (msg.reqid == null) {
return null;
}
return rpcResponseGenerator(msg.command, msg.reqid, msg.timeout);
}
function handleIncomingRpcMessage(msg: RpcMessage) {
const isRequest = msg.command != null || msg.reqid != null;
if (isRequest) {
console.log("rpc request not supported", msg);
return;
}
if (msg.resid == null) {
console.log("rpc response missing resid", msg);
return;
}
const entry = openRpcs.get(msg.resid);
if (entry == null) {
console.log("rpc response generator not found", msg);
return;
}
entry.msgFn(msg);
}
export { handleIncomingRpcMessage, sendRpcCommand };
+72
View File
@@ -0,0 +1,72 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
// generated by cmd/generate/main-generate.go
import * as WOS from "./wos";
// WshServerCommandToDeclMap
class WshServerType {
// command "controller:input" [call]
BlockInputCommand(data: CommandBlockInputData, opts?: WshRpcCommandOpts): Promise<void> {
const meta: WshServerCommandMeta = {commandtype: "call"};
return WOS.callWshServerRpc("controller:input", data, meta, opts);
}
// command "controller:restart" [call]
BlockRestartCommand(data: CommandBlockRestartData, opts?: WshRpcCommandOpts): Promise<void> {
const meta: WshServerCommandMeta = {commandtype: "call"};
return WOS.callWshServerRpc("controller:restart", data, meta, opts);
}
// command "createblock" [call]
CreateBlockCommand(data: CommandCreateBlockData, opts?: WshRpcCommandOpts): Promise<ORef> {
const meta: WshServerCommandMeta = {commandtype: "call"};
return WOS.callWshServerRpc("createblock", data, meta, opts);
}
// command "file:append" [call]
AppendFileCommand(data: CommandAppendFileData, opts?: WshRpcCommandOpts): Promise<void> {
const meta: WshServerCommandMeta = {commandtype: "call"};
return WOS.callWshServerRpc("file:append", data, meta, opts);
}
// command "file:appendijson" [call]
AppendIJsonCommand(data: CommandAppendIJsonData, opts?: WshRpcCommandOpts): Promise<void> {
const meta: WshServerCommandMeta = {commandtype: "call"};
return WOS.callWshServerRpc("file:appendijson", data, meta, opts);
}
// command "getmeta" [call]
GetMetaCommand(data: CommandGetMetaData, opts?: WshRpcCommandOpts): Promise<MetaType> {
const meta: WshServerCommandMeta = {commandtype: "call"};
return WOS.callWshServerRpc("getmeta", data, meta, opts);
}
// command "message" [call]
MessageCommand(data: CommandMessageData, opts?: WshRpcCommandOpts): Promise<void> {
const meta: WshServerCommandMeta = {commandtype: "call"};
return WOS.callWshServerRpc("message", data, meta, opts);
}
// command "resolveids" [call]
ResolveIdsCommand(data: CommandResolveIdsData, opts?: WshRpcCommandOpts): Promise<CommandResolveIdsRtnData> {
const meta: WshServerCommandMeta = {commandtype: "call"};
return WOS.callWshServerRpc("resolveids", data, meta, opts);
}
// command "setmeta" [call]
SetMetaCommand(data: CommandSetMetaData, opts?: WshRpcCommandOpts): Promise<void> {
const meta: WshServerCommandMeta = {commandtype: "call"};
return WOS.callWshServerRpc("setmeta", data, meta, opts);
}
// command "setview" [call]
BlockSetViewCommand(data: CommandBlockSetViewData, opts?: WshRpcCommandOpts): Promise<void> {
const meta: WshServerCommandMeta = {commandtype: "call"};
return WOS.callWshServerRpc("setview", data, meta, opts);
}
}
export const WshServer = new WshServerType();
+6 -36
View File
@@ -1,20 +1,10 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import {
WOS,
atoms,
getEventORefSubject,
globalStore,
sendWSCommand,
useBlockAtom,
useSettingsAtom,
} from "@/store/global";
import { WOS, atoms, getEventORefSubject, globalStore, useBlockAtom, useSettingsAtom } from "@/store/global";
import * as services from "@/store/services";
import * as keyutil from "@/util/keyutil";
import { FitAddon } from "@xterm/addon-fit";
import type { ITheme } from "@xterm/xterm";
import { Terminal } from "@xterm/xterm";
import clsx from "clsx";
import { produce } from "immer";
import * as jotai from "jotai";
@@ -23,6 +13,7 @@ import { IJsonView } from "./ijson";
import { TermStickers } from "./termsticker";
import { TermWrap } from "./termwrap";
import { WshServer } from "@/app/store/wshserver";
import "public/xterm.css";
import "./term.less";
@@ -54,23 +45,6 @@ function getThemeFromCSSVars(el: Element): ITheme {
return theme;
}
function handleResize(fitAddon: FitAddon, blockId: string, term: Terminal) {
if (term == null) {
return;
}
const oldRows = term.rows;
const oldCols = term.cols;
fitAddon.fit();
if (oldRows !== term.rows || oldCols !== term.cols) {
const wsCommand: SetBlockTermSizeWSCommand = {
wscommand: "setblocktermsize",
blockid: blockId,
termsize: { rows: term.rows, cols: term.cols },
};
sendWSCommand(wsCommand);
}
}
const keyMap = {
Enter: "\r",
Backspace: "\x7f",
@@ -177,14 +151,12 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
if (keyutil.checkKeyPressed(waveEvent, "Cmd:Escape")) {
event.preventDefault();
event.stopPropagation();
const metaCmd: BlockSetMetaCommand = { command: "setmeta", meta: { "term:mode": "html" } };
services.BlockService.SendCommand(this.blockId, metaCmd);
WshServer.SetMetaCommand({ oref: WOS.makeORef("block", blockId), meta: { "term:mode": null } });
return false;
}
if (shellProcStatusRef.current != "running" && keyutil.checkKeyPressed(waveEvent, "Enter")) {
// restart
const restartCmd: BlockRestartCommand = { command: "controller:restart", blockid: blockId };
services.BlockService.SendCommand(blockId, restartCmd);
WshServer.BlockRestartCommand({ blockid: blockId });
return false;
}
}
@@ -224,8 +196,7 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
const waveEvent = keyutil.adaptFromReactOrNativeKeyEvent(event);
if (keyutil.checkKeyPressed(waveEvent, "Cmd:Escape")) {
// reset term:mode
const metaCmd: BlockSetMetaCommand = { command: "setmeta", meta: { "term:mode": null } };
services.BlockService.SendCommand(blockId, metaCmd);
WshServer.SetMetaCommand({ oref: WOS.makeORef("block", blockId), meta: { "term:mode": null } });
return false;
}
const asciiVal = keyboardEventToASCII(event);
@@ -233,8 +204,7 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
return false;
}
const b64data = btoa(asciiVal);
const inputCmd: BlockInputCommand = { command: "controller:input", inputdata64: b64data, blockid: blockId };
services.BlockService.SendCommand(blockId, inputCmd);
WshServer.BlockInputCommand({ blockid: blockId, inputdata64: b64data });
return true;
};
+2 -7
View File
@@ -1,8 +1,8 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { WshServer } from "@/app/store/wshserver";
import { createBlock, getBackendHostPort } from "@/store/global";
import * as services from "@/store/services";
import clsx from "clsx";
import * as jotai from "jotai";
import * as React from "react";
@@ -98,12 +98,7 @@ function TermSticker({ sticker, config }: { sticker: StickerType; config: Sticke
console.log("clickHandler", sticker.clickcmd, sticker.clickblockdef);
if (sticker.clickcmd) {
const b64data = btoa(sticker.clickcmd);
const inputCmd: BlockInputCommand = {
command: "controller:input",
inputdata64: b64data,
blockid: config.blockId,
};
services.BlockService.SendCommand(config.blockId, inputCmd);
WshServer.BlockInputCommand({ blockid: config.blockId, inputdata64: b64data });
}
if (sticker.clickblockdef) {
createBlock(sticker.clickblockdef);
+2 -11
View File
@@ -1,6 +1,7 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { WshServer } from "@/app/store/wshserver";
import { PLATFORM, fetchWaveFile, getFileSubject, sendWSCommand } from "@/store/global";
import * as services from "@/store/services";
import { base64ToArray } from "@/util/util";
@@ -75,17 +76,7 @@ export class TermWrap {
handleTermData(data: string) {
const b64data = btoa(data);
if (b64data.length < 512) {
const wsCmd: BlockInputWSCommand = { wscommand: "blockinput", blockid: this.blockId, inputdata64: b64data };
sendWSCommand(wsCmd);
} else {
const inputCmd: BlockInputCommand = {
command: "controller:input",
blockid: this.blockId,
inputdata64: b64data,
};
services.BlockService.SendCommand(this.blockId, inputCmd);
}
WshServer.BlockInputCommand({ blockid: this.blockId, inputdata64: b64data });
}
addFocusListener(focusFn: () => void) {
+98 -18
View File
@@ -107,6 +107,73 @@ declare global {
meta: MetaType;
};
// wshrpc.CommandAppendFileData
type CommandAppendFileData = {
zoneid: string;
filename: string;
data64: string;
};
// wshrpc.CommandAppendIJsonData
type CommandAppendIJsonData = {
zoneid: string;
filename: string;
data: MetaType;
};
// wshrpc.CommandBlockInputData
type CommandBlockInputData = {
blockid: string;
inputdata64?: string;
signame?: string;
termsize?: TermSize;
};
// wshrpc.CommandBlockRestartData
type CommandBlockRestartData = {
blockid: string;
};
// wshrpc.CommandBlockSetViewData
type CommandBlockSetViewData = {
blockid: string;
view: string;
};
// wshrpc.CommandCreateBlockData
type CommandCreateBlockData = {
tabid: string;
blockdef: BlockDef;
rtopts: RuntimeOpts;
};
// wshrpc.CommandGetMetaData
type CommandGetMetaData = {
oref: ORef;
};
// wshrpc.CommandMessageData
type CommandMessageData = {
oref: ORef;
message: string;
};
// wshrpc.CommandResolveIdsData
type CommandResolveIdsData = {
ids: string[];
};
// wshrpc.CommandResolveIdsRtnData
type CommandResolveIdsRtnData = {
resolvedids: {[key: string]: ORef};
};
// wshrpc.CommandSetMetaData
type CommandSetMetaData = {
oref: ORef;
meta: MetaType;
};
// wshutil.CreateBlockCommand
type CreateBlockCommand = {
command: "createblock";
@@ -115,18 +182,6 @@ declare global {
rtopts?: RuntimeOpts;
};
// wconfig.DateTimeConfigType
type DateTimeConfigType = {
locale: string;
format: DateTimeFormatConfigType;
};
// wconfig.DateTimeFormatConfigType
type DateTimeFormatConfigType = {
dateStyle: number;
timeStyle: number;
};
// wstore.FileDef
type FileDef = {
filetype?: string;
@@ -185,10 +240,7 @@ declare global {
};
// waveobj.ORef
type ORef = {
otype: string;
oid: string;
};
type ORef = string;
// wstore.Point
type Point = {
@@ -202,6 +254,18 @@ declare global {
ids: string[];
};
// wshutil.RpcMessage
type RpcMessage = {
command?: string;
reqid?: string;
resid?: string;
timeout?: number;
cont?: boolean;
error?: string;
datatype?: string;
data?: any;
};
// wstore.RuntimeOpts
type RuntimeOpts = {
termsize?: TermSize;
@@ -218,7 +282,6 @@ declare global {
// wconfig.SettingsConfigType
type SettingsConfigType = {
mimetypes: {[key: string]: MimeTypeConfigType};
datetime: DateTimeConfigType;
term: TerminalConfigType;
widgets: WidgetsConfigType[];
blockheader: BlockHeaderOpts;
@@ -273,7 +336,7 @@ declare global {
type WSCommandType = {
wscommand: string;
} & ( SetBlockTermSizeWSCommand | BlockInputWSCommand );
} & ( SetBlockTermSizeWSCommand | BlockInputWSCommand | WSRpcCommand );
// eventbus.WSEventType
type WSEventType = {
@@ -297,6 +360,12 @@ declare global {
blockid: string;
};
// webcmd.WSRpcCommand
type WSRpcCommand = {
wscommand: "rpc";
message: RpcMessage;
};
// wconfig.WatcherUpdate
type WatcherUpdate = {
file: string;
@@ -380,6 +449,17 @@ declare global {
meta: MetaType;
};
// wshrpc.WshRpcCommandOpts
type WshRpcCommandOpts = {
timeout: number;
noresponse: boolean;
};
// wshrpc.WshServerCommandMeta
type WshServerCommandMeta = {
commandtype: string;
};
}
export {}
+2
View File
@@ -1,6 +1,7 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { WshServer } from "@/app/store/wshserver";
import { atoms, getApi, globalStore, globalWS, initWS, setPlatform } from "@/store/global";
import * as services from "@/store/services";
import * as WOS from "@/store/wos";
@@ -25,6 +26,7 @@ loadFonts();
(window as any).globalWS = globalWS;
(window as any).WOS = WOS;
(window as any).globalStore = globalStore;
(window as any).WshServer = WshServer;
document.title = `The Next Wave (${windowId.substring(0, 8)})`;