checkpoint -- generic updates, wave object store, new setup for initialization, atoms, etc. lots of progress

This commit is contained in:
sawka
2024-05-27 13:59:58 -07:00
parent 6d3f76cb74
commit b87786febf
14 changed files with 646 additions and 195 deletions
+5 -4
View File
@@ -9,6 +9,7 @@ import { Workspace } from "@/app/workspace/workspace";
import { globalStore, atoms } from "@/store/global";
import "../../public/style.less";
import { CenteredDiv } from "./element/quickelems";
const App = () => {
return (
@@ -19,16 +20,16 @@ const App = () => {
};
const AppInner = () => {
const client = jotai.useAtomValue(atoms.clientAtom);
const windowData = jotai.useAtomValue(atoms.windowData);
const client = jotai.useAtomValue(atoms.client);
const windowData = jotai.useAtomValue(atoms.waveWindow);
if (client == null || windowData == null) {
return (
<div className="mainapp">
<div>invalid configuration, client or window was not loaded</div>
<div className="titlebar"></div>
<CenteredDiv>invalid configuration, client or window was not loaded</CenteredDiv>
</div>
);
}
return (
<div className="mainapp">
<div className="titlebar"></div>
+43 -121
View File
@@ -1,54 +1,66 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as React from "react";
import * as jotai from "jotai";
import * as jotaiUtils from "jotai/utils";
import { v4 as uuidv4 } from "uuid";
import * as rxjs from "rxjs";
import type { WailsEvent } from "@wailsio/runtime/types/events";
import { Events } from "@wailsio/runtime";
import { produce } from "immer";
import { BlockService } from "@/bindings/blockservice";
import { ObjectService } from "@/bindings/objectservice";
import * as wstore from "@/gopkg/wstore";
import { Call as $Call } from "@wailsio/runtime";
import * as WOS from "./wos";
const globalStore = jotai.createStore();
const blockDataMap = new Map<string, jotai.Atom<wstore.Block>>();
const urlParams = new URLSearchParams(window.location.search);
const globalWindowId = urlParams.get("windowid");
const globalClientId = urlParams.get("clientid");
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 uiContext: UIContext = {
windowid: get(atoms.windowId),
};
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.getStaticObjectValue(WOS.makeORef("client", clientId), get);
});
const windowDataAtom: jotai.Atom<WaveWindow> = jotai.atom((get) => {
const windowId = get(windowIdAtom);
if (windowId == null) {
return null;
}
return WOS.getStaticObjectValue(WOS.makeORef("window", windowId), get);
});
const workspaceAtom: jotai.Atom<Workspace> = jotai.atom((get) => {
const windowData = get(windowDataAtom);
if (windowData == null) {
return null;
}
return WOS.getStaticObjectValue(WOS.makeORef("workspace", windowData.workspaceid), get);
});
const atoms = {
blockDataMap: blockDataMap,
clientAtom: jotai.atom(null) as jotai.PrimitiveAtom<wstore.Client>,
// initialized in wave.ts (will not be null inside of application)
windowId: jotai.atom<string>(null) as jotai.PrimitiveAtom<string>,
windowData: jotai.atom<WaveWindow>(null) as jotai.PrimitiveAtom<WaveWindow>,
windowId: windowIdAtom,
clientId: clientIdAtom,
uiContext: uiContextAtom,
client: clientAtom,
waveWindow: windowDataAtom,
workspace: workspaceAtom,
blockDataMap: blockDataMap,
};
type SubjectWithRef<T> = rxjs.Subject<T> & { refCount: number; release: () => void };
const blockSubjects = new Map<string, SubjectWithRef<any>>();
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<any> {
let subject = blockSubjects.get(blockId);
if (subject == null) {
@@ -95,94 +107,4 @@ function useBlockAtom<T>(blockId: string, name: string, makeFn: () => jotai.Atom
return atom as jotai.Atom<T>;
}
function GetObject<T>(oref: string): Promise<T> {
let prtn = $Call.ByName(
"github.com/wavetermdev/thenextwave/pkg/service/objectservice.ObjectService.GetObject",
oref
);
return prtn;
}
function GetClientObject(): Promise<Client> {
let prtn = $Call.ByName(
"github.com/wavetermdev/thenextwave/pkg/service/objectservice.ObjectService.GetClientObject"
);
return prtn;
}
type WaveObjectValue<T> = {
pendingPromise: Promise<any>;
dataAtom: jotai.PrimitiveAtom<{ value: T; loading: boolean }>;
};
const waveObjectValueCache = new Map<string, WaveObjectValue<any>>();
function clearWaveObjectCache() {
waveObjectValueCache.clear();
}
function createWaveValueObject<T>(oref: string): WaveObjectValue<T> {
const wov = { pendingPromise: null, dataAtom: null };
wov.dataAtom = jotai.atom({ value: null, loading: true });
let startTs = Date.now();
let localPromise = GetObject<T>(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<T>(oref: string): [T, boolean] {
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 atomVal = jotai.useAtomValue(wov.dataAtom);
return [atomVal.value, atomVal.loading];
}
function useWaveObject<T>(oref: string): [T, boolean, (T) => void] {
console.log("useWaveObject", oref);
let wov = waveObjectValueCache.get(oref);
if (wov == null) {
wov = createWaveValueObject(oref);
waveObjectValueCache.set(oref, wov);
}
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,
blockDataMap,
useBlockAtom,
GetObject,
GetClientObject,
useWaveObject,
useWaveObjectValue,
clearWaveObjectCache,
};
export { globalStore, atoms, getBlockSubject, blockDataMap, useBlockAtom, WOS };
+251
View File
@@ -0,0 +1,251 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
// WaveObjectStore
import * as React from "react";
import * as jotai from "jotai";
import { Events } from "@wailsio/runtime";
import { Call as $Call } from "@wailsio/runtime";
import { globalStore, atoms } from "./global";
type WaveObjectDataItemType<T extends WaveObj> = {
value: T;
loading: boolean;
};
type WaveObjectValue<T extends WaveObj> = {
pendingPromise: Promise<T>;
dataAtom: jotai.PrimitiveAtom<WaveObjectDataItemType<T>>;
refCount: number;
holdTime: number;
};
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 isBlank(str: string): boolean {
return str == null || str == "";
}
function isBlankNum(num: number): boolean {
return num == null || isNaN(num) || num == 0;
}
function isValidWaveObj(val: WaveObj): boolean {
if (val == null) {
return false;
}
if (isBlank(val.otype) || isBlank(val.oid)) {
return false;
}
if (!val.deleted && isBlankNum(val.version)) {
return false;
}
return true;
}
function makeORef(otype: string, oid: string): string {
if (isBlank(otype) || isBlank(oid)) {
return null;
}
return `${otype}:${oid}`;
}
function GetObject<T>(oref: string): Promise<T> {
let prtn = $Call.ByName(
"github.com/wavetermdev/thenextwave/pkg/service/objectservice.ObjectService.GetObject",
oref
);
return prtn;
}
const waveObjectValueCache = new Map<string, WaveObjectValue<any>>();
function clearWaveObjectCache() {
waveObjectValueCache.clear();
}
const defaultHoldTime = 5000; // 5-seconds
function createWaveValueObject<T extends WaveObj>(oref: string, shouldFetch: boolean): WaveObjectValue<T> {
const wov = { pendingPromise: null, dataAtom: null, refCount: 0, holdTime: Date.now() + 5000 };
wov.dataAtom = jotai.atom({ value: null, loading: true });
if (!shouldFetch) {
return wov;
}
let startTs = Date.now();
let localPromise = GetObject<T>(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, Date.now() - startTs + "ms");
});
return wov;
}
function loadAndPinWaveObject<T>(oref: string): Promise<T> {
let wov = waveObjectValueCache.get(oref);
if (wov == null) {
wov = createWaveValueObject(oref, true);
waveObjectValueCache.set(oref, wov);
}
wov.refCount++;
if (wov.pendingPromise == null) {
const dataValue = globalStore.get(wov.dataAtom);
return Promise.resolve(dataValue.value);
}
return wov.pendingPromise;
}
function useWaveObjectValue<T>(oref: string): [T, boolean] {
let wov = waveObjectValueCache.get(oref);
if (wov == null) {
wov = createWaveValueObject(oref, true);
waveObjectValueCache.set(oref, wov);
}
React.useEffect(() => {
wov.refCount++;
return () => {
wov.refCount--;
};
}, [oref]);
const atomVal = jotai.useAtomValue(wov.dataAtom);
return [atomVal.value, atomVal.loading];
}
function useWaveObject<T>(oref: string): [T, boolean, (T) => void] {
let wov = waveObjectValueCache.get(oref);
if (wov == null) {
wov = createWaveValueObject(oref, true);
waveObjectValueCache.set(oref, wov);
}
React.useEffect(() => {
wov.refCount++;
return () => {
wov.refCount--;
};
}, [oref]);
const [atomVal, setAtomVal] = jotai.useAtom(wov.dataAtom);
const simpleSet = (val: T) => {
setAtomVal({ value: val, loading: false });
};
return [atomVal.value, atomVal.loading, simpleSet];
}
function updateWaveObject(val: WaveObj) {
if (val == null) {
return;
}
if (!isValidWaveObj(val)) {
console.log("invalid wave object", val);
return;
}
let oref = makeORef(val.otype, val.oid);
let wov = waveObjectValueCache.get(oref);
if (wov == null) {
wov = createWaveValueObject(oref, false);
waveObjectValueCache.set(oref, wov);
}
if (val.deleted) {
globalStore.set(wov.dataAtom, { value: null, loading: false });
} else {
let curValue: WaveObjectDataItemType<WaveObj> = globalStore.get(wov.dataAtom);
if (curValue.value != null && curValue.value.version >= val.version) {
return;
}
globalStore.set(wov.dataAtom, { value: val, loading: false });
}
wov.holdTime = Date.now() + defaultHoldTime;
return;
}
function updateWaveObjects(vals: WaveObj[]) {
for (let val of vals) {
updateWaveObject(val);
}
}
function cleanWaveObjectCache() {
let now = Date.now();
for (let [oref, wov] of waveObjectValueCache) {
if (wov.refCount == 0 && wov.holdTime < now) {
waveObjectValueCache.delete(oref);
}
}
}
Events.On("waveobj:update", (event: any) => {
const data: WaveObj[] = event?.data;
if (data == null) {
return;
}
if (!Array.isArray(data)) {
console.log("invalid waveobj:update, not an array", data);
return;
}
if (data.length == 0) {
return;
}
updateWaveObjects(data);
});
function wrapObjectServiceCall<T>(fnName: string, ...args: any[]): Promise<T> {
const uiContext = globalStore.get(atoms.uiContext);
let prtn = $Call.ByName(
"github.com/wavetermdev/thenextwave/pkg/service/objectservice.ObjectService." + fnName,
uiContext,
...args
);
prtn = prtn.then((val) => {
if (val.updates) {
updateWaveObjects(val.updates);
}
return val;
});
return prtn;
}
function AddTabToWorkspace(tabName: string, activateTab: boolean): Promise<{ tabId: string }> {
return wrapObjectServiceCall("AddTabToWorkspace", tabName, activateTab);
}
function getStaticObjectValue<T>(oref: string, getFn: jotai.Getter): T {
let wov = waveObjectValueCache.get(oref);
if (wov == null) {
return null;
}
const atomVal = getFn(wov.dataAtom);
return atomVal.value;
}
export {
makeORef,
useWaveObject,
useWaveObjectValue,
loadAndPinWaveObject,
clearWaveObjectCache,
updateWaveObject,
updateWaveObjects,
cleanWaveObjectCache,
getStaticObjectValue,
AddTabToWorkspace,
};
+2 -2
View File
@@ -5,13 +5,13 @@ 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 * as WOS from "@/store/wos";
import "./tab.less";
import { CenteredLoadingDiv } from "../element/quickelems";
const TabContent = ({ tabId }: { tabId: string }) => {
const [tabData, tabLoading] = gdata.useWaveObjectValue<Tab>(gdata.makeORef("tab", tabId));
const [tabData, tabLoading] = WOS.useWaveObjectValue<Tab>(WOS.makeORef("tab", tabId));
if (tabLoading) {
return <CenteredLoadingDiv />;
}
+11 -20
View File
@@ -12,22 +12,20 @@ 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 * as WOS from "@/store/wos";
import { CenteredLoadingDiv, CenteredDiv } from "../element/quickelems";
function Tab({ tabId }: { tabId: string }) {
const windowData = jotai.useAtomValue(atoms.windowData);
const [tabData, tabLoading] = gdata.useWaveObjectValue<Tab>(gdata.makeORef("tab", tabId));
import "./workspace.less";
function Tab({ tabId }: { tabId: string }) {
const windowData = jotai.useAtomValue(atoms.waveWindow);
const [tabData, tabLoading] = WOS.useWaveObjectValue<Tab>(WOS.makeORef("tab", tabId));
function setActiveTab(tabId: string) {
if (tabId == null) {
return;
}
// TODO
}
return (
<div
className={clsx("tab", { active: tabData != null && windowData.activetabid === tabData.oid })}
@@ -40,12 +38,9 @@ function Tab({ tabId }: { tabId: string }) {
function TabBar({ workspace, waveWindow }: { workspace: Workspace; waveWindow: WaveWindow }) {
function handleAddTab() {
const newTabId = uuidv4();
const newTabName = "Tab " + (tabData.length + 1);
setTabData([...tabData, { name: newTabName, tabid: newTabId, blockids: [] }]);
setActiveTab(newTabId);
const newTabName = `Tab-${workspace.tabids.length + 1}`;
WOS.AddTabToWorkspace(newTabName, true);
}
const tabIds = workspace?.tabids ?? [];
return (
<div className="tab-bar">
@@ -60,7 +55,7 @@ function TabBar({ workspace, waveWindow }: { workspace: Workspace; waveWindow: W
}
function Widgets() {
const windowData = jotai.useAtomValue(atoms.windowData);
const windowData = jotai.useAtomValue(atoms.waveWindow);
const activeTabId = windowData.activetabid;
async function createBlock(blockDef: wstore.BlockDef) {
@@ -122,18 +117,14 @@ function Widgets() {
}
function WorkspaceElem() {
const windowData = jotai.useAtomValue(atoms.windowData);
const workspaceId = windowData?.workspaceid;
const windowData = jotai.useAtomValue(atoms.waveWindow);
const activeTabId = windowData?.activetabid;
const [ws, wsLoading] = gdata.useWaveObjectValue<Workspace>(gdata.makeORef("workspace", workspaceId));
if (wsLoading) {
return <CenteredLoadingDiv />;
}
const ws = jotai.useAtomValue(atoms.workspace);
return (
<div className="workspace">
<TabBar workspace={ws} waveWindow={windowData} />
<div className="workspace-tabcontent">
<TabContent key={workspaceId} tabId={activeTabId} />
<TabContent key={windowData.workspaceid} tabId={activeTabId} />
<Widgets />
</div>
</div>
+6
View File
@@ -2,6 +2,10 @@
// SPDX-License-Identifier: Apache-2.0
declare global {
type UIContext = {
windowid: string;
};
type ORef = {
otype: string;
oid: string;
@@ -79,6 +83,8 @@ declare global {
type WaveObj = {
otype: string;
oid: string;
version: number;
deleted?: boolean;
};
type WaveWindow = {
+8 -6
View File
@@ -7,14 +7,16 @@ import { App } from "./app/app";
import { loadFonts } from "./util/fontutil";
import { ClientService } from "@/bindings/clientservice";
import { Client } from "@/gopkg/wstore";
import { globalStore, atoms, GetClientObject, GetObject, makeORef } from "@/store/global";
import { globalStore, atoms } from "@/store/global";
import * as WOS from "@/store/wos";
import * as wailsRuntime from "@wailsio/runtime";
import * as wstore from "@/gopkg/wstore";
import * as gdata from "@/store/global";
import { immerable } from "immer";
const urlParams = new URLSearchParams(window.location.search);
const windowId = urlParams.get("windowid");
globalStore.set(atoms.windowId, windowId);
const clientId = urlParams.get("clientid");
wstore.Block.prototype[immerable] = true;
wstore.Tab.prototype[immerable] = true;
@@ -30,10 +32,10 @@ wstore.WinSize.prototype[immerable] = true;
loadFonts();
document.addEventListener("DOMContentLoaded", async () => {
const client = await GetClientObject();
globalStore.set(atoms.clientAtom, client);
const window = await GetObject<WaveWindow>(makeORef("window", windowId));
globalStore.set(atoms.windowData, window);
// ensures client/window are loaded into the cache before rendering
await WOS.loadAndPinWaveObject<Client>(WOS.makeORef("client", clientId));
const waveWindow = await WOS.loadAndPinWaveObject<WaveWindow>(WOS.makeORef("window", windowId));
await WOS.loadAndPinWaveObject<Workspace>(WOS.makeORef("workspace", waveWindow.workspaceid));
let reactElem = React.createElement(App, null, null);
let elem = document.getElementById("main");
let root = createRoot(elem);