diff --git a/frontend/app/block/block.tsx b/frontend/app/block/block.tsx index b0e61840..4a56d1fa 100644 --- a/frontend/app/block/block.tsx +++ b/frontend/app/block/block.tsx @@ -3,7 +3,7 @@ import * as React from "react"; import * as jotai from "jotai"; -import { atoms, blockDataMap, removeBlockFromTab } from "@/store/global"; +import { atoms, blockDataMap } from "@/store/global"; import { TerminalView } from "@/app/view/term"; import { PreviewView } from "@/app/view/preview"; @@ -17,7 +17,7 @@ const Block = ({ tabId, blockId }: { tabId: string; blockId: string }) => { const [dims, setDims] = React.useState({ width: 0, height: 0 }); function handleClose() { - removeBlockFromTab(tabId, blockId); + // TODO } React.useEffect(() => { diff --git a/frontend/app/store/global.ts b/frontend/app/store/global.ts index 3089e757..eed83efe 100644 --- a/frontend/app/store/global.ts +++ b/frontend/app/store/global.ts @@ -15,28 +15,40 @@ import * as wstore from "@/gopkg/wstore"; import { Call as $Call } from "@wailsio/runtime"; const globalStore = jotai.createStore(); - -const tabId1 = uuidv4(); - -const tabArr: wstore.Tab[] = [new wstore.Tab({ name: "Tab 1", tabid: tabId1, blockids: [] })]; const blockDataMap = new Map>(); -const blockAtomCache = new Map>>(); const atoms = { - activeTabId: jotai.atom(tabId1), - tabsAtom: jotai.atom(tabArr), blockDataMap: blockDataMap, clientAtom: jotai.atom(null) as jotai.PrimitiveAtom, // initialized in wave.ts (will not be null inside of application) windowId: jotai.atom(null) as jotai.PrimitiveAtom, - windowData: jotai.atom(null) as jotai.PrimitiveAtom, + windowData: jotai.atom(null) as jotai.PrimitiveAtom, }; type SubjectWithRef = rxjs.Subject & { refCount: number; release: () => void }; const blockSubjects = new Map>(); +function isBlank(str: string): boolean { + return str == null || str == ""; +} + +function makeORef(otype: string, oid: string): string { + if (isBlank(otype) || isBlank(oid)) { + return null; + } + return `${otype}:${oid}`; +} + +function splitORef(oref: string): [string, string] { + let parts = oref.split(":"); + if (parts.length != 2) { + throw new Error("invalid oref"); + } + return [parts[0], parts[1]]; +} + function getBlockSubject(blockId: string): SubjectWithRef { let subject = blockSubjects.get(blockId); if (subject == null) { @@ -69,20 +81,6 @@ Events.On("block:ptydata", (event: any) => { subject.next(data); }); -function addBlockIdToTab(tabId: string, blockId: string) { - let tabArr = globalStore.get(atoms.tabsAtom); - const newTabArr = produce(tabArr, (draft) => { - const tab = draft.find((tab) => tab.tabid == tabId); - tab.blockids.push(blockId); - }); - globalStore.set(atoms.tabsAtom, newTabArr); -} - -function removeBlock(blockId: string) { - blockDataMap.delete(blockId); - blockAtomCache.delete(blockId); -} - function useBlockAtom(blockId: string, name: string, makeFn: () => jotai.Atom): jotai.Atom { let blockCache = blockAtomCache.get(blockId); if (blockCache == null) { @@ -97,18 +95,7 @@ function useBlockAtom(blockId: string, name: string, makeFn: () => jotai.Atom return atom as jotai.Atom; } -function removeBlockFromTab(tabId: string, blockId: string) { - let tabArr = globalStore.get(atoms.tabsAtom); - const newTabArr = produce(tabArr, (draft) => { - const tab = draft.find((tab) => tab.tabid == tabId); - tab.blockids = tab.blockids.filter((id) => id !== blockId); - }); - globalStore.set(atoms.tabsAtom, newTabArr); - removeBlock(blockId); - BlockService.CloseBlock(blockId); -} - -function GetObject(oref: string): Promise { +function GetObject(oref: string): Promise { let prtn = $Call.ByName( "github.com/wavetermdev/thenextwave/pkg/service/objectservice.ObjectService.GetObject", oref @@ -116,84 +103,85 @@ function GetObject(oref: string): Promise { return prtn; } -type WaveObjectHookData = { - oref: string; -}; +function GetClientObject(): Promise { + let prtn = $Call.ByName( + "github.com/wavetermdev/thenextwave/pkg/service/objectservice.ObjectService.GetClientObject" + ); + return prtn; +} type WaveObjectValue = { pendingPromise: Promise; - value: T; - loading: boolean; + dataAtom: jotai.PrimitiveAtom<{ value: T; loading: boolean }>; }; const waveObjectValueCache = new Map>(); -let waveObjectAtomCache = new WeakMap>(); function clearWaveObjectCache() { waveObjectValueCache.clear(); - waveObjectAtomCache = new WeakMap>(); } -function createWaveObjectAtom(oref: string): jotai.Atom<[T, boolean]> { - let cacheVal: WaveObjectValue = waveObjectValueCache.get(oref); - if (cacheVal == null) { - cacheVal = { pendingPromise: null, value: null, loading: true }; - cacheVal.pendingPromise = GetObject(oref).then((val) => { - cacheVal.value = val; - cacheVal.loading = false; - cacheVal.pendingPromise = null; - }); - waveObjectValueCache.set(oref, cacheVal); - } - return jotai.atom( - (get) => { - return [cacheVal.value, cacheVal.loading]; - }, - (get, set, newVal: T) => { - cacheVal.value = newVal; +function createWaveValueObject(oref: string): WaveObjectValue { + const wov = { pendingPromise: null, dataAtom: null }; + wov.dataAtom = jotai.atom({ value: null, loading: true }); + let startTs = Date.now(); + let localPromise = GetObject(oref); + wov.pendingPromise = localPromise; + localPromise.then((val) => { + if (wov.pendingPromise != localPromise) { + return; } - ); + const [otype, oid] = splitORef(oref); + if (val != null) { + if (val["otype"] != otype) { + throw new Error("GetObject returned wrong type"); + } + if (val["oid"] != oid) { + throw new Error("GetObject returned wrong id"); + } + } + wov.pendingPromise = null; + globalStore.set(wov.dataAtom, { value: val, loading: false }); + console.log("GetObject resolved", oref, val, Date.now() - startTs + "ms"); + }); + return wov; } function useWaveObjectValue(oref: string): [T, boolean] { - const objRef = React.useRef(null); - if (objRef.current == null) { - objRef.current = { oref: oref }; + console.log("useWaveObjectValue", oref); + let wov = waveObjectValueCache.get(oref); + if (wov == null) { + console.log("creating new wov", oref); + wov = createWaveValueObject(oref); + waveObjectValueCache.set(oref, wov); } - const objHookData = objRef.current; - let objAtom = waveObjectAtomCache.get(objHookData); - if (objAtom == null) { - objAtom = createWaveObjectAtom(oref); - waveObjectAtomCache.set(objHookData, objAtom); - } - const atomVal = jotai.useAtomValue(objAtom); - return [atomVal[0], atomVal[1]]; + const atomVal = jotai.useAtomValue(wov.dataAtom); + return [atomVal.value, atomVal.loading]; } function useWaveObject(oref: string): [T, boolean, (T) => void] { - const objRef = React.useRef(null); - if (objRef.current == null) { - objRef.current = { oref: oref }; + console.log("useWaveObject", oref); + let wov = waveObjectValueCache.get(oref); + if (wov == null) { + wov = createWaveValueObject(oref); + waveObjectValueCache.set(oref, wov); } - const objHookData = objRef.current; - let objAtom = waveObjectAtomCache.get(objHookData); - if (objAtom == null) { - objAtom = createWaveObjectAtom(oref); - waveObjectAtomCache.set(objHookData, objAtom); - } - const [atomVal, setAtomVal] = jotai.useAtom(objAtom); - return [atomVal[0], atomVal[1], setAtomVal]; + const [atomVal, setAtomVal] = jotai.useAtom(wov.dataAtom); + const simpleSet = (val: T) => { + setAtomVal({ value: val, loading: false }); + }; + return [atomVal.value, atomVal.loading, simpleSet]; } export { globalStore, + makeORef, atoms, getBlockSubject, - addBlockIdToTab, blockDataMap, useBlockAtom, - removeBlockFromTab, GetObject, + GetClientObject, useWaveObject, useWaveObjectValue, clearWaveObjectCache, diff --git a/frontend/app/tab/tab.tsx b/frontend/app/tab/tab.tsx index 1944ba47..9799be16 100644 --- a/frontend/app/tab/tab.tsx +++ b/frontend/app/tab/tab.tsx @@ -5,12 +5,16 @@ import * as React from "react"; import * as jotai from "jotai"; import { Block } from "@/app/block/block"; import { atoms } from "@/store/global"; +import * as gdata from "@/store/global"; import "./tab.less"; +import { CenteredLoadingDiv } from "../element/quickelems"; const TabContent = ({ tabId }: { tabId: string }) => { - const tabs = jotai.useAtomValue(atoms.tabsAtom); - const tabData = tabs.find((tab) => tab.tabid === tabId); + const [tabData, tabLoading] = gdata.useWaveObjectValue(gdata.makeORef("tab", tabId)); + if (tabLoading) { + return ; + } if (!tabData) { return
Tab not found
; } diff --git a/frontend/app/workspace/workspace.tsx b/frontend/app/workspace/workspace.tsx index 0e296e5e..69f76909 100644 --- a/frontend/app/workspace/workspace.tsx +++ b/frontend/app/workspace/workspace.tsx @@ -5,32 +5,40 @@ import * as React from "react"; import * as jotai from "jotai"; import { TabContent } from "@/app/tab/tab"; import { clsx } from "clsx"; -import { atoms, addBlockIdToTab, blockDataMap } from "@/store/global"; +import { atoms, blockDataMap } from "@/store/global"; import { v4 as uuidv4 } from "uuid"; import { BlockService } from "@/bindings/blockservice"; import { ClientService } from "@/bindings/clientservice"; import { Workspace } from "@/gopkg/wstore"; import * as wstore from "@/gopkg/wstore"; import * as jotaiUtil from "jotai/utils"; +import * as gdata from "@/store/global"; import "./workspace.less"; import { CenteredLoadingDiv, CenteredDiv } from "../element/quickelems"; -function Tab({ tab }: { tab: wstore.Tab }) { - const [activeTab, setActiveTab] = jotai.useAtom(atoms.activeTabId); +function Tab({ tabId }: { tabId: string }) { + const windowData = jotai.useAtomValue(atoms.windowData); + const [tabData, tabLoading] = gdata.useWaveObjectValue(gdata.makeORef("tab", tabId)); + + function setActiveTab(tabId: string) { + if (tabId == null) { + return; + } + // TODO + } + return ( -
setActiveTab(tab.tabid)}> - {tab.name} +
setActiveTab(tabData?.oid)} + > + {tabData?.name ?? "..."}
); } -function TabBar() { - const [tabData, setTabData] = jotai.useAtom(atoms.tabsAtom); - const [activeTab, setActiveTab] = jotai.useAtom(atoms.activeTabId); - const tabs = jotai.useAtomValue(atoms.tabsAtom); - const client = jotai.useAtomValue(atoms.clientAtom); - +function TabBar({ workspace, waveWindow }: { workspace: Workspace; waveWindow: WaveWindow }) { function handleAddTab() { const newTabId = uuidv4(); const newTabName = "Tab " + (tabData.length + 1); @@ -38,10 +46,11 @@ function TabBar() { setActiveTab(newTabId); } + const tabIds = workspace?.tabids ?? []; return (
- {tabs.map((tab, idx) => { - return ; + {tabIds.map((tabid, idx) => { + return ; })}
handleAddTab()}> @@ -51,7 +60,8 @@ function TabBar() { } function Widgets() { - const activeTabId = jotai.useAtomValue(atoms.activeTabId); + const windowData = jotai.useAtomValue(atoms.windowData); + const activeTabId = windowData.activetabid; async function createBlock(blockDef: wstore.BlockDef) { const rtOpts: wstore.RuntimeOpts = new wstore.RuntimeOpts({ termsize: { rows: 25, cols: 80 } }); @@ -113,27 +123,15 @@ function Widgets() { function WorkspaceElem() { const windowData = jotai.useAtomValue(atoms.windowData); - const activeTabId = jotai.useAtomValue(atoms.activeTabId); - const workspaceId = windowData.workspaceid; - const wsAtom = React.useMemo(() => { - return jotaiUtil.loadable( - jotai.atom(async (get) => { - const ws = await ClientService.GetWorkspace(workspaceId); - return ws; - }) - ); - }, [workspaceId]); - const wsLoadable = jotai.useAtomValue(wsAtom); - if (wsLoadable.state === "loading") { + const workspaceId = windowData?.workspaceid; + const activeTabId = windowData?.activetabid; + const [ws, wsLoading] = gdata.useWaveObjectValue(gdata.makeORef("workspace", workspaceId)); + if (wsLoading) { return ; } - if (wsLoadable.state === "hasError") { - return Error: {wsLoadable.error?.toString()}; - } - const ws: Workspace = wsLoadable.data; return (
- +
diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index c7111e1c..aae4f6e2 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -81,7 +81,7 @@ declare global { oid: string; }; - type Window = { + type WaveWindow = { otype: string; oid: string; version: number; diff --git a/frontend/wave.ts b/frontend/wave.ts index 992eed3c..c06689bc 100644 --- a/frontend/wave.ts +++ b/frontend/wave.ts @@ -7,7 +7,7 @@ import { App } from "./app/app"; import { loadFonts } from "./util/fontutil"; import { ClientService } from "@/bindings/clientservice"; import { Client } from "@/gopkg/wstore"; -import { globalStore, atoms } from "@/store/global"; +import { globalStore, atoms, GetClientObject, GetObject, makeORef } from "@/store/global"; import * as wailsRuntime from "@wailsio/runtime"; import * as wstore from "@/gopkg/wstore"; import { immerable } from "immer"; @@ -30,9 +30,9 @@ wstore.WinSize.prototype[immerable] = true; loadFonts(); document.addEventListener("DOMContentLoaded", async () => { - const client = await ClientService.GetClientData(); + const client = await GetClientObject(); globalStore.set(atoms.clientAtom, client); - const window = await ClientService.GetWindow(windowId); + const window = await GetObject(makeORef("window", windowId)); globalStore.set(atoms.windowData, window); let reactElem = React.createElement(App, null, null); let elem = document.getElementById("main"); diff --git a/pkg/service/objectservice/objectservice.go b/pkg/service/objectservice/objectservice.go index 96995599..64476bec 100644 --- a/pkg/service/objectservice/objectservice.go +++ b/pkg/service/objectservice/objectservice.go @@ -25,6 +25,16 @@ func parseORef(oref string) (*waveobj.ORef, error) { return &waveobj.ORef{OType: fields[0], OID: fields[1]}, nil } +func (svc *ObjectService) GetClientObject() (any, error) { + ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) + defer cancelFn() + client, err := wstore.DBGetSingleton[*wstore.Client](ctx) + if err != nil { + return nil, fmt.Errorf("error getting client: %w", err) + } + return waveobj.ToJsonMap(client) +} + func (svc *ObjectService) GetObject(orefStr string) (any, error) { oref, err := parseORef(orefStr) if err != nil { @@ -36,7 +46,7 @@ func (svc *ObjectService) GetObject(orefStr string) (any, error) { if err != nil { return nil, fmt.Errorf("error getting object: %w", err) } - return obj, nil + return waveobj.ToJsonMap(obj) } func (svc *ObjectService) GetObjects(orefStrArr []string) (any, error) { diff --git a/pkg/waveobj/waveobj.go b/pkg/waveobj/waveobj.go index 0b1a5421..0b965a9e 100644 --- a/pkg/waveobj/waveobj.go +++ b/pkg/waveobj/waveobj.go @@ -124,7 +124,7 @@ func SetVersion(waveObj WaveObj, version int) { reflect.ValueOf(waveObj).Elem().FieldByIndex(desc.VersionField.Index).SetInt(int64(version)) } -func ToJson(w WaveObj) ([]byte, error) { +func ToJsonMap(w WaveObj) (map[string]any, error) { m := make(map[string]any) dconfig := &mapstructure.DecoderConfig{ Result: &m, @@ -139,6 +139,16 @@ func ToJson(w WaveObj) ([]byte, error) { return nil, err } m[OTypeKeyName] = w.GetOType() + m[OIDKeyName] = GetOID(w) + m[VersionKeyName] = GetVersion(w) + return m, nil +} + +func ToJson(w WaveObj) ([]byte, error) { + m, err := ToJsonMap(w) + if err != nil { + return nil, err + } return json.Marshal(m) } @@ -253,10 +263,18 @@ func typeToTSType(t reflect.Type) (string, []reflect.Type) { } } +var tsRenameMap = map[string]string{ + "Window": "WaveWindow", +} + func generateTSTypeInternal(rtype reflect.Type) (string, []reflect.Type) { var buf bytes.Buffer waveObjType := reflect.TypeOf((*WaveObj)(nil)).Elem() - buf.WriteString(fmt.Sprintf("type %s = {\n", rtype.Name())) + tsTypeName := rtype.Name() + if tsRename, ok := tsRenameMap[tsTypeName]; ok { + tsTypeName = tsRename + } + buf.WriteString(fmt.Sprintf("type %s = {\n", tsTypeName)) var isWaveObj bool if rtype.Implements(waveObjType) || reflect.PointerTo(rtype).Implements(waveObjType) { isWaveObj = true diff --git a/pkg/wstore/wstore.go b/pkg/wstore/wstore.go index 3e54986d..163316df 100644 --- a/pkg/wstore/wstore.go +++ b/pkg/wstore/wstore.go @@ -209,8 +209,8 @@ func EnsureInitialData() error { return fmt.Errorf("error inserting workspace: %w", err) } tab := &Tab{ - OID: uuid.New().String(), - Name: "Tab 1", + OID: tabId, + Name: "Tab-1", BlockIds: []string{}, } err = DBInsert(ctx, tab)