waveterm/frontend/app/store/global.ts

359 lines
12 KiB
TypeScript

// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { LayoutTreeAction, LayoutTreeActionType, LayoutTreeInsertNodeAction, newLayoutNode } from "@/faraday/index";
import { getLayoutStateAtomForTab } from "@/faraday/lib/layoutAtom";
import { layoutTreeStateReducer } from "@/faraday/lib/layoutState";
import { produce } from "immer";
import * as jotai from "jotai";
import * as rxjs from "rxjs";
import * as services from "./services";
import * as WOS from "./wos";
import { WSControl } from "./ws";
// TODO remove the window dependency completely
// we should have the initialization be more orderly -- proceed directly from wave.ts instead of on its own.
const globalStore = jotai.createStore();
let globalWindowId: string = null;
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";
}
const windowIdAtom = jotai.atom(null) as jotai.PrimitiveAtom<string>;
const clientIdAtom = jotai.atom(null) as jotai.PrimitiveAtom<string>;
globalStore.set(windowIdAtom, globalWindowId);
globalStore.set(clientIdAtom, globalClientId);
const uiContextAtom = jotai.atom((get) => {
const windowData = get(windowDataAtom);
const uiContext: UIContext = {
windowid: get(atoms.windowId),
activetabid: windowData?.activetabid,
};
return uiContext;
}) as jotai.Atom<UIContext>;
const clientAtom: jotai.Atom<Client> = jotai.atom((get) => {
const clientId = get(clientIdAtom);
if (clientId == null) {
return null;
}
return WOS.getObjectValue(WOS.makeORef("client", clientId), get);
});
const windowDataAtom: jotai.Atom<WaveWindow> = jotai.atom((get) => {
const windowId = get(windowIdAtom);
if (windowId == null) {
return null;
}
const rtn = WOS.getObjectValue<WaveWindow>(WOS.makeORef("window", windowId), get);
return rtn;
});
const workspaceAtom: jotai.Atom<Workspace> = jotai.atom((get) => {
const windowData = get(windowDataAtom);
if (windowData == null) {
return null;
}
return WOS.getObjectValue(WOS.makeORef("workspace", windowData.workspaceid), get);
});
const settingsConfigAtom = jotai.atom(null) as jotai.PrimitiveAtom<SettingsConfigType>;
const tabAtom: jotai.Atom<Tab> = jotai.atom((get) => {
const windowData = get(windowDataAtom);
if (windowData == null) {
return null;
}
return WOS.getObjectValue(WOS.makeORef("tab", windowData.activetabid), get);
});
const activeTabIdAtom: jotai.Atom<string> = jotai.atom((get) => {
const windowData = get(windowDataAtom);
if (windowData == null) {
return null;
}
return windowData.activetabid;
});
const atoms = {
// initialized in wave.ts (will not be null inside of application)
windowId: windowIdAtom,
clientId: clientIdAtom,
uiContext: uiContextAtom,
client: clientAtom,
waveWindow: windowDataAtom,
workspace: workspaceAtom,
settingsConfigAtom: settingsConfigAtom,
tabAtom: tabAtom,
activeTabId: activeTabIdAtom,
};
// key is "eventType" or "eventType|oref"
const eventSubjects = new Map<string, SubjectWithRef<WSEventType>>();
const fileSubjects = new Map<string, SubjectWithRef<WSFileEventData>>();
function getSubjectInternal(subjectKey: string): SubjectWithRef<WSEventType> {
let subject = eventSubjects.get(subjectKey);
if (subject == null) {
subject = new rxjs.Subject<any>() as any;
subject.refCount = 0;
subject.release = () => {
subject.refCount--;
if (subject.refCount === 0) {
subject.complete();
eventSubjects.delete(subjectKey);
}
};
eventSubjects.set(subjectKey, subject);
}
subject.refCount++;
return subject;
}
function getEventSubject(eventType: string): SubjectWithRef<WSEventType> {
return getSubjectInternal(eventType);
}
function getEventORefSubject(eventType: string, oref: string): SubjectWithRef<WSEventType> {
return getSubjectInternal(eventType + "|" + oref);
}
function getFileSubject(zoneId: string, fileName: string): SubjectWithRef<WSFileEventData> {
const subjectKey = zoneId + "|" + fileName;
let subject = fileSubjects.get(subjectKey);
if (subject == null) {
subject = new rxjs.Subject<any>() as any;
subject.refCount = 0;
subject.release = () => {
subject.refCount--;
if (subject.refCount === 0) {
subject.complete();
fileSubjects.delete(subjectKey);
}
};
fileSubjects.set(subjectKey, subject);
}
subject.refCount++;
return subject;
}
const blockCache = new Map<string, Map<string, any>>();
function useBlockCache<T>(blockId: string, name: string, makeFn: () => T): T {
let blockMap = blockCache.get(blockId);
if (blockMap == null) {
blockMap = new Map<string, any>();
blockCache.set(blockId, blockMap);
}
let value = blockMap.get(name);
if (value == null) {
value = makeFn();
blockMap.set(name, value);
}
return value as T;
}
const settingsAtomCache = new Map<string, jotai.Atom<any>>();
function useSettingsAtom<T>(name: string, settingsFn: (settings: SettingsConfigType) => T): jotai.Atom<T> {
let atom = settingsAtomCache.get(name);
if (atom == null) {
atom = jotai.atom((get) => {
const settings = get(settingsConfigAtom);
if (settings == null) {
return null;
}
return settingsFn(settings);
}) as jotai.Atom<T>;
settingsAtomCache.set(name, atom);
}
return atom as jotai.Atom<T>;
}
const blockAtomCache = new Map<string, Map<string, jotai.Atom<any>>>();
function useBlockAtom<T>(blockId: string, name: string, makeFn: () => jotai.Atom<T>): jotai.Atom<T> {
let blockCache = blockAtomCache.get(blockId);
if (blockCache == null) {
blockCache = new Map<string, jotai.Atom<any>>();
blockAtomCache.set(blockId, blockCache);
}
let atom = blockCache.get(name);
if (atom == null) {
atom = makeFn();
blockCache.set(name, atom);
console.log("New BlockAtom", blockId, name);
}
return atom as jotai.Atom<T>;
}
function getBackendHostPort(): string {
// TODO deal with dev/production
return "http://127.0.0.1:8190";
}
function getBackendWSHostPort(): string {
return "ws://127.0.0.1:8191";
}
let globalWS: WSControl = null;
function handleWSEventMessage(msg: WSEventType) {
if (msg.eventtype == null) {
console.log("unsupported event", msg);
return;
}
if (msg.eventtype == "config") {
const data: WatcherUpdate = msg.data;
globalStore.set(settingsConfigAtom, data.update);
console.log("config", data);
return;
}
if (msg.eventtype == "blockfile") {
const fileData: WSFileEventData = msg.data;
const fileSubject = getFileSubject(fileData.zoneid, fileData.filename);
if (fileSubject != null) {
fileSubject.next(fileData);
}
return;
}
if (msg.eventtype == "layoutaction") {
const layoutAction: WSLayoutAction = msg.data;
if (layoutAction.actiontype == LayoutTreeActionType.InsertNode) {
const insertNodeAction: LayoutTreeInsertNodeAction<TabLayoutData> = {
type: LayoutTreeActionType.InsertNode,
node: newLayoutNode<TabLayoutData>(undefined, undefined, undefined, {
blockId: layoutAction.blockid,
}),
};
runLayoutAction(layoutAction.tabid, insertNodeAction);
} else {
console.log("unsupported layout action", layoutAction);
}
return;
}
// we send to two subjects just eventType and eventType|oref
// we don't use getORefSubject here because we don't want to create a new subject
const eventSubject = eventSubjects.get(msg.eventtype);
if (eventSubject != null) {
eventSubject.next(msg);
}
const eventOrefSubject = eventSubjects.get(msg.eventtype + "|" + msg.oref);
if (eventOrefSubject != null) {
eventOrefSubject.next(msg);
}
}
function handleWSMessage(msg: any) {
if (msg == null) {
return;
}
if (msg.eventtype != null) {
handleWSEventMessage(msg);
}
}
function initWS() {
globalWS = new WSControl(getBackendWSHostPort(), globalStore, globalWindowId, "", (msg) => {
handleWSMessage(msg);
});
globalWS.connectNow("initWS");
}
function sendWSCommand(command: WSCommandType) {
globalWS.pushMessage(command);
}
// more code that could be moved into an init
// here we want to set up a "waveobj:update" handler
const waveobjUpdateSubject = getEventSubject("waveobj:update");
waveobjUpdateSubject.subscribe((msg: WSEventType) => {
const update: WaveObjUpdate = msg.data;
WOS.updateWaveObject(update);
});
/**
* Get the preload api.
*/
function getApi(): ElectronApi {
return (window as any).api;
}
function runLayoutAction(tabId: string, action: LayoutTreeAction) {
const layoutStateAtom = getLayoutStateAtomForTab(tabId, WOS.getWaveObjectAtom<Tab>(WOS.makeORef("tab", tabId)));
const curState = globalStore.get(layoutStateAtom);
globalStore.set(layoutStateAtom, layoutTreeStateReducer(curState, action));
}
async function createBlock(blockDef: BlockDef) {
const rtOpts: RuntimeOpts = { termsize: { rows: 25, cols: 80 } };
const blockId = await services.ObjectService.CreateBlock(blockDef, rtOpts);
const insertNodeAction: LayoutTreeInsertNodeAction<TabLayoutData> = {
type: LayoutTreeActionType.InsertNode,
node: newLayoutNode<TabLayoutData>(undefined, undefined, undefined, { blockId }),
};
const activeTabId = globalStore.get(atoms.uiContext).activetabid;
runLayoutAction(activeTabId, insertNodeAction);
}
// when file is not found, returns {data: null, fileInfo: null}
async function fetchWaveFile(
zoneId: string,
fileName: string,
offset?: number
): Promise<{ data: Uint8Array; fileInfo: WaveFile }> {
let usp = new URLSearchParams();
usp.set("zoneid", zoneId);
usp.set("name", fileName);
if (offset != null) {
usp.set("offset", offset.toString());
}
const resp = await fetch(getBackendHostPort() + "/wave/file?" + usp.toString());
if (!resp.ok) {
if (resp.status === 404) {
return { data: null, fileInfo: null };
}
throw new Error("error getting wave file: " + resp.statusText);
}
if (resp.status == 204) {
return { data: null, fileInfo: null };
}
let fileInfo64 = resp.headers.get("X-ZoneFileInfo");
if (fileInfo64 == null) {
throw new Error(`missing zone file info for ${zoneId}:${fileName}`);
}
let fileInfo = JSON.parse(atob(fileInfo64));
const data = await resp.arrayBuffer();
return { data: new Uint8Array(data), fileInfo };
}
function setBlockFocus(blockId: string) {
let winData = globalStore.get(atoms.waveWindow);
if (winData.activeblockid === blockId) {
return;
}
winData = produce(winData, (draft) => {
draft.activeblockid = blockId;
});
WOS.setObjectValue(winData, globalStore.set, true);
}
export {
WOS,
atoms,
createBlock,
fetchWaveFile,
getApi,
getBackendHostPort,
getEventORefSubject,
getEventSubject,
getFileSubject,
globalStore,
globalWS,
initWS,
sendWSCommand,
setBlockFocus,
useBlockAtom,
useBlockCache,
useSettingsAtom,
};