From 2317ce87f39df1795b5a5d1ff955b70cc0693ab1 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 21 May 2024 21:15:11 -0700 Subject: [PATCH 01/11] checkpoint --- db/db.go | 3 + db/migrations-wstore/000001_init.up.sql | 1 - main.go | 6 ++ pkg/service/blockservice/blockservice.go | 9 ++- pkg/wstore/wstore.go | 32 ++++++++ pkg/wstore/wstore_dbops.go | 97 ++++++++++++++++++++++++ pkg/wstore/wstore_dbsetup.go | 66 ++++++++++++++++ 7 files changed, 212 insertions(+), 2 deletions(-) create mode 100644 pkg/wstore/wstore_dbops.go diff --git a/db/db.go b/db/db.go index b04d199a..b13b72fb 100644 --- a/db/db.go +++ b/db/db.go @@ -7,3 +7,6 @@ import "embed" //go:embed migrations-blockstore/*.sql var BlockstoreMigrationFS embed.FS + +//go:embed migrations-wstore/*.sql +var WStoreMigrationFS embed.FS diff --git a/db/migrations-wstore/000001_init.up.sql b/db/migrations-wstore/000001_init.up.sql index 8880f644..b8a3e927 100644 --- a/db/migrations-wstore/000001_init.up.sql +++ b/db/migrations-wstore/000001_init.up.sql @@ -15,6 +15,5 @@ CREATE TABLE db_tab ( CREATE TABLE db_block ( blockid varchar(36) PRIMARY KEY, - tabid varchar(36) NOT NULL, -- the tab this block belongs to data json NOT NULL ); diff --git a/main.go b/main.go index 27ee260b..6995a702 100644 --- a/main.go +++ b/main.go @@ -17,6 +17,7 @@ import ( "github.com/wavetermdev/thenextwave/pkg/service/blockservice" "github.com/wavetermdev/thenextwave/pkg/service/fileservice" "github.com/wavetermdev/thenextwave/pkg/wavebase" + "github.com/wavetermdev/thenextwave/pkg/wstore" "github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/events" @@ -104,6 +105,11 @@ func main() { log.Printf("error initializing blockstore: %v\n", err) return } + err = wstore.InitWStore() + if err != nil { + log.Printf("error initializing wstore: %v\n", err) + return + } app := application.New(application.Options{ Name: "NextWave", diff --git a/pkg/service/blockservice/blockservice.go b/pkg/service/blockservice/blockservice.go index 81df911f..0815d814 100644 --- a/pkg/service/blockservice/blockservice.go +++ b/pkg/service/blockservice/blockservice.go @@ -4,8 +4,10 @@ package blockservice import ( + "context" "fmt" "strings" + "time" "github.com/wavetermdev/thenextwave/pkg/blockcontroller" "github.com/wavetermdev/thenextwave/pkg/util/utilfn" @@ -41,7 +43,12 @@ func (bs *BlockService) CloseBlock(blockId string) { } func (bs *BlockService) GetBlockData(blockId string) (map[string]any, error) { - blockData := wstore.BlockMap.Get(blockId) + ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second) + defer cancelFn() + blockData, err := wstore.BlockGet(ctx, blockId) + if err != nil { + return nil, fmt.Errorf("error getting block data: %w", err) + } if blockData == nil { return nil, nil } diff --git a/pkg/wstore/wstore.go b/pkg/wstore/wstore.go index f1c5466f..62265770 100644 --- a/pkg/wstore/wstore.go +++ b/pkg/wstore/wstore.go @@ -4,6 +4,7 @@ package wstore import ( + "context" "fmt" "sync" @@ -23,6 +24,7 @@ type Client struct { type Workspace struct { Lock *sync.Mutex `json:"-"` WorkspaceId string `json:"workspaceid"` + Name string `json:"name"` TabIds []string `json:"tabids"` } @@ -118,3 +120,33 @@ func CreateWorkspace() (*Workspace, error) { } return ws, nil } + +func EnsureWorkspace(ctx context.Context) error { + wsCount, err := WorkspaceCount(ctx) + if err != nil { + return fmt.Errorf("error getting workspace count: %w", err) + } + if wsCount > 0 { + return nil + } + ws := &Workspace{ + Lock: &sync.Mutex{}, + WorkspaceId: uuid.New().String(), + Name: "default", + } + err = WorkspaceInsert(ctx, ws) + if err != nil { + return fmt.Errorf("error inserting workspace: %w", err) + } + tab := &Tab{ + Lock: &sync.Mutex{}, + TabId: uuid.New().String(), + Name: "Tab 1", + BlockIds: []string{}, + } + err = TabInsert(ctx, tab, ws.WorkspaceId) + if err != nil { + return fmt.Errorf("error inserting tab: %w", err) + } + return nil +} diff --git a/pkg/wstore/wstore_dbops.go b/pkg/wstore/wstore_dbops.go new file mode 100644 index 00000000..3840c893 --- /dev/null +++ b/pkg/wstore/wstore_dbops.go @@ -0,0 +1,97 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package wstore + +import ( + "context" + "fmt" + + "github.com/google/uuid" +) + +func WorkspaceCount(ctx context.Context) (int, error) { + return WithTxRtn(ctx, func(tx *TxWrap) (int, error) { + query := "SELECT count(*) FROM workspace" + return tx.GetInt(query), nil + }) +} + +func WorkspaceInsert(ctx context.Context, ws *Workspace) error { + if ws.WorkspaceId == "" { + ws.WorkspaceId = uuid.New().String() + } + return WithTx(ctx, func(tx *TxWrap) error { + query := "INSERT INTO workspace (workspaceid, data) VALUES (?, ?)" + tx.Exec(query, ws.WorkspaceId, TxJson(tx, ws)) + return nil + }) +} + +func WorkspaceGet(ctx context.Context, workspaceId string) (*Workspace, error) { + return WithTxRtn(ctx, func(tx *TxWrap) (*Workspace, error) { + query := "SELECT data FROM workspace WHERE workspaceid = ?" + jsonData := tx.GetString(query, workspaceId) + return TxReadJson[Workspace](tx, jsonData), nil + }) +} + +func WorkspaceUpdate(ctx context.Context, ws *Workspace) error { + return WithTx(ctx, func(tx *TxWrap) error { + query := "UPDATE workspace SET data = ? WHERE workspaceid = ?" + tx.Exec(query, TxJson(tx, ws), ws.WorkspaceId) + return nil + }) +} + +func addTabToWorkspace(ctx context.Context, workspaceId string, tabId string) error { + return WithTx(ctx, func(tx *TxWrap) error { + ws, err := WorkspaceGet(tx.Context(), workspaceId) + if err != nil { + return err + } + if ws == nil { + return fmt.Errorf("workspace not found: %s", workspaceId) + } + ws.TabIds = append(ws.TabIds, tabId) + return WorkspaceUpdate(tx.Context(), ws) + }) +} + +func TabInsert(ctx context.Context, tab *Tab, workspaceId string) error { + if tab.TabId == "" { + tab.TabId = uuid.New().String() + } + return WithTx(ctx, func(tx *TxWrap) error { + query := "INSERT INTO tab (tabid, data) VALUES (?, ?)" + tx.Exec(query, tab.TabId, TxJson(tx, tab)) + return addTabToWorkspace(tx.Context(), workspaceId, tab.TabId) + }) +} + +func BlockGet(ctx context.Context, blockId string) (*Block, error) { + return WithTxRtn(ctx, func(tx *TxWrap) (*Block, error) { + query := "SELECT data FROM block WHERE blockid = ?" + jsonData := tx.GetString(query, blockId) + return TxReadJson[Block](tx, jsonData), nil + }) +} + +func BlockDelete(ctx context.Context, blockId string) error { + return WithTx(ctx, func(tx *TxWrap) error { + query := "DELETE FROM block WHERE blockid = ?" + tx.Exec(query, blockId) + return nil + }) +} + +func BlockInsert(ctx context.Context, block *Block) error { + if block.BlockId == "" { + block.BlockId = uuid.New().String() + } + return WithTx(ctx, func(tx *TxWrap) error { + query := "INSERT INTO block (blockid, data) VALUES (?, ?)" + tx.Exec(query, block.BlockId, TxJson(tx, block)) + return nil + }) +} diff --git a/pkg/wstore/wstore_dbsetup.go b/pkg/wstore/wstore_dbsetup.go index 1b97352f..3e58417d 100644 --- a/pkg/wstore/wstore_dbsetup.go +++ b/pkg/wstore/wstore_dbsetup.go @@ -5,14 +5,20 @@ package wstore import ( "context" + "encoding/json" "fmt" "log" "path" "time" + "github.com/golang-migrate/migrate/v4" + "github.com/golang-migrate/migrate/v4/source/iofs" "github.com/jmoiron/sqlx" "github.com/sawka/txwrap" "github.com/wavetermdev/thenextwave/pkg/wavebase" + + sqlite3migrate "github.com/golang-migrate/migrate/v4/database/sqlite3" + dbfs "github.com/wavetermdev/thenextwave/db" ) const WStoreDBName = "waveterm.db" @@ -55,3 +61,63 @@ func MakeDB(ctx context.Context) (*sqlx.DB, error) { func MigrateWStore() error { return nil } + +func MakeWStoreMigrate() (*migrate.Migrate, error) { + fsVar, err := iofs.New(dbfs.WStoreMigrationFS, "migrations-wstore") + if err != nil { + return nil, fmt.Errorf("opening iofs: %w", err) + } + mdriver, err := sqlite3migrate.WithInstance(globalDB.DB, &sqlite3migrate.Config{}) + if err != nil { + return nil, fmt.Errorf("making blockstore migration driver: %w", err) + } + m, err := migrate.NewWithInstance("iofs", fsVar, "sqlite3", mdriver) + if err != nil { + return nil, fmt.Errorf("making blockstore migration db[%s]: %w", GetDBName(), err) + } + return m, nil +} + +func GetMigrateVersion(m *migrate.Migrate) (uint, bool, error) { + if m == nil { + var err error + m, err = MakeWStoreMigrate() + if err != nil { + return 0, false, err + } + } + curVersion, dirty, err := m.Version() + if err == migrate.ErrNilVersion { + return 0, false, nil + } + return curVersion, dirty, err +} + +func WithTx(ctx context.Context, fn func(tx *TxWrap) error) error { + return txwrap.WithTx(ctx, globalDB, fn) +} + +func WithTxRtn[RT any](ctx context.Context, fn func(tx *TxWrap) (RT, error)) (RT, error) { + return txwrap.WithTxRtn(ctx, globalDB, fn) +} + +func TxJson(tx *TxWrap, v any) string { + barr, err := json.Marshal(v) + if err != nil { + tx.SetErr(fmt.Errorf("json marshal (%T): %w", v, err)) + return "" + } + return string(barr) +} + +func TxReadJson[T any](tx *TxWrap, jsonData string) *T { + if jsonData == "" { + return nil + } + var v T + err := json.Unmarshal([]byte(jsonData), &v) + if err != nil { + tx.SetErr(fmt.Errorf("json unmarshal (%T): %w", v, err)) + } + return &v +} From 8173bc3c6170aab46efe656777ff7eb9a12d94b6 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 22 May 2024 09:23:16 -0700 Subject: [PATCH 02/11] update blockservice to use wstore types --- frontend/app/store/global.ts | 3 ++- frontend/app/workspace/workspace.tsx | 3 ++- pkg/service/blockservice/blockservice.go | 20 ++++---------------- tsconfig.json | 1 + 4 files changed, 9 insertions(+), 18 deletions(-) diff --git a/frontend/app/store/global.ts b/frontend/app/store/global.ts index 3c10cfa7..3217eedf 100644 --- a/frontend/app/store/global.ts +++ b/frontend/app/store/global.ts @@ -9,13 +9,14 @@ import type { WailsEvent } from "@wailsio/runtime/types/events"; import { Events } from "@wailsio/runtime"; import { produce } from "immer"; import { BlockService } from "@/bindings/blockservice"; +import * as wstore from "@/gopkg/wstore"; const globalStore = jotai.createStore(); const tabId1 = uuidv4(); const tabArr: TabData[] = [{ name: "Tab 1", tabid: tabId1, blockIds: [] }]; -const blockDataMap = new Map>(); +const blockDataMap = new Map>(); const blockAtomCache = new Map>>(); const atoms = { diff --git a/frontend/app/workspace/workspace.tsx b/frontend/app/workspace/workspace.tsx index e61c67f5..afe029c6 100644 --- a/frontend/app/workspace/workspace.tsx +++ b/frontend/app/workspace/workspace.tsx @@ -8,6 +8,7 @@ import { clsx } from "clsx"; import { atoms, addBlockIdToTab, blockDataMap } from "@/store/global"; import { v4 as uuidv4 } from "uuid"; import { BlockService } from "@/bindings/blockservice"; +import * as wstore from "@/gopkg/wstore"; import "./workspace.less"; @@ -49,7 +50,7 @@ function Widgets() { async function createBlock(blockDef: BlockDef) { const rtOpts = { termsize: { rows: 25, cols: 80 } }; - const rtnBlock: BlockData = (await BlockService.CreateBlock(blockDef, rtOpts)) as BlockData; + const rtnBlock: wstore.Block = await BlockService.CreateBlock(blockDef, rtOpts); const newBlockAtom = jotai.atom(rtnBlock); blockDataMap.set(rtnBlock.blockid, newBlockAtom); addBlockIdToTab(activeTabId, rtnBlock.blockid); diff --git a/pkg/service/blockservice/blockservice.go b/pkg/service/blockservice/blockservice.go index 0815d814..dfd3ef2e 100644 --- a/pkg/service/blockservice/blockservice.go +++ b/pkg/service/blockservice/blockservice.go @@ -16,7 +16,7 @@ import ( type BlockService struct{} -func (bs *BlockService) CreateBlock(bdefMap map[string]any, rtOptsMap map[string]any) (map[string]any, error) { +func (bs *BlockService) CreateBlock(bdefMap map[string]any, rtOptsMap map[string]any) (*wstore.Block, error) { var bdef wstore.BlockDef err := utilfn.JsonMapToStruct(bdefMap, &bdef) if err != nil { @@ -31,33 +31,21 @@ func (bs *BlockService) CreateBlock(bdefMap map[string]any, rtOptsMap map[string if err != nil { return nil, fmt.Errorf("error creating block: %w", err) } - rtnMap, err := utilfn.StructToJsonMap(blockData) - if err != nil { - return nil, fmt.Errorf("error marshalling BlockData: %w", err) - } - return rtnMap, nil + return blockData, nil } func (bs *BlockService) CloseBlock(blockId string) { blockcontroller.CloseBlock(blockId) } -func (bs *BlockService) GetBlockData(blockId string) (map[string]any, error) { +func (bs *BlockService) GetBlockData(blockId string) (*wstore.Block, error) { ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second) defer cancelFn() blockData, err := wstore.BlockGet(ctx, blockId) if err != nil { return nil, fmt.Errorf("error getting block data: %w", err) } - if blockData == nil { - return nil, nil - } - rtnMap, err := utilfn.StructToJsonMap(blockData) - if err != nil { - return nil, fmt.Errorf("error marshalling BlockData: %w", err) - } - return rtnMap, nil - + return blockData, nil } func (bs *BlockService) SendCommand(blockId string, cmdMap map[string]any) error { diff --git a/tsconfig.json b/tsconfig.json index 5a3c5ec7..c58021d8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,6 +20,7 @@ "@/store/*": ["frontend/app/store/*"], "@/element/*": ["frontend/app/element/*"], "@/bindings/*": ["frontend/bindings/github.com/wavetermdev/thenextwave/pkg/service/*"], + "@/gopkg/*": ["frontend/bindings/github.com/wavetermdev/thenextwave/pkg/*"], } } } From 134ba3c34c11ade9bc71f3a95173a02138ab7529 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 24 May 2024 15:08:24 -0600 Subject: [PATCH 03/11] checkpoint on integratng wstore. moved to wails data structures, got immer working again, Window object, transitioned to generic DB ops, lots more --- db/migrations-wstore/000001_init.up.sql | 5 + frontend/app/app.tsx | 10 ++ frontend/app/block/block.tsx | 1 + frontend/app/element/quickelems.tsx | 6 +- frontend/app/store/global.ts | 13 +- frontend/app/tab/tab.tsx | 2 +- frontend/app/view/preview.tsx | 3 +- frontend/app/workspace/workspace.tsx | 49 ++++-- frontend/types/custom.d.ts | 34 +--- frontend/wave.ts | 27 ++- main.go | 46 ++++- pkg/blockcontroller/blockcontroller.go | 83 ++++++--- pkg/blockstore/dbsetup.go | 64 +------ pkg/service/blockservice/blockservice.go | 23 ++- pkg/service/clientservice/clientservice.go | 56 ++++++ pkg/util/migrateutil/migrateutil.go | 67 ++++++++ pkg/wstore/wstore.go | 110 +++++++++--- pkg/wstore/wstore_dbops.go | 191 ++++++++++++++------- pkg/wstore/wstore_dbsetup.go | 41 +---- 19 files changed, 542 insertions(+), 289 deletions(-) create mode 100644 pkg/service/clientservice/clientservice.go create mode 100644 pkg/util/migrateutil/migrateutil.go diff --git a/db/migrations-wstore/000001_init.up.sql b/db/migrations-wstore/000001_init.up.sql index b8a3e927..957bc803 100644 --- a/db/migrations-wstore/000001_init.up.sql +++ b/db/migrations-wstore/000001_init.up.sql @@ -3,6 +3,11 @@ CREATE TABLE db_client ( data json NOT NULL ); +CREATE TABLE db_window ( + windowid varchar(36) PRIMARY KEY, + data json NOT NULL +); + CREATE TABLE db_workspace ( workspaceid varchar(36) PRIMARY KEY, data json NOT NULL diff --git a/frontend/app/app.tsx b/frontend/app/app.tsx index ab41992d..eb8d5a8a 100644 --- a/frontend/app/app.tsx +++ b/frontend/app/app.tsx @@ -19,6 +19,16 @@ const App = () => { }; const AppInner = () => { + const client = jotai.useAtomValue(atoms.clientAtom); + const windowData = jotai.useAtomValue(atoms.windowData); + if (client == null || windowData == null) { + return ( +
+
invalid configuration, client or window was not loaded
+
+ ); + } + return (
diff --git a/frontend/app/block/block.tsx b/frontend/app/block/block.tsx index fe7af24b..b0e61840 100644 --- a/frontend/app/block/block.tsx +++ b/frontend/app/block/block.tsx @@ -31,6 +31,7 @@ const Block = ({ tabId, blockId }: { tabId: string; blockId: string }) => { setDims({ width: newWidth, height: newHeight }); } }, [blockRef.current]); + let blockElem: JSX.Element = null; const blockAtom = blockDataMap.get(blockId); const blockData = jotai.useAtomValue(blockAtom); diff --git a/frontend/app/element/quickelems.tsx b/frontend/app/element/quickelems.tsx index 5de52a25..2777acfe 100644 --- a/frontend/app/element/quickelems.tsx +++ b/frontend/app/element/quickelems.tsx @@ -3,6 +3,10 @@ import "./quickelems.less"; +function CenteredLoadingDiv() { + return loading...; +} + function CenteredDiv({ children }: { children: React.ReactNode }) { return (
@@ -11,4 +15,4 @@ function CenteredDiv({ children }: { children: React.ReactNode }) { ); } -export { CenteredDiv as CenteredDiv }; +export { CenteredDiv, CenteredLoadingDiv }; diff --git a/frontend/app/store/global.ts b/frontend/app/store/global.ts index 3217eedf..f615f4b9 100644 --- a/frontend/app/store/global.ts +++ b/frontend/app/store/global.ts @@ -15,14 +15,19 @@ const globalStore = jotai.createStore(); const tabId1 = uuidv4(); -const tabArr: TabData[] = [{ name: "Tab 1", tabid: tabId1, blockIds: [] }]; +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), + 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, }; type SubjectWithRef = rxjs.Subject & { refCount: number; release: () => void }; @@ -65,7 +70,7 @@ 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); + tab.blockids.push(blockId); }); globalStore.set(atoms.tabsAtom, newTabArr); } @@ -93,7 +98,7 @@ 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); + tab.blockids = tab.blockids.filter((id) => id !== blockId); }); globalStore.set(atoms.tabsAtom, newTabArr); removeBlock(blockId); diff --git a/frontend/app/tab/tab.tsx b/frontend/app/tab/tab.tsx index 6b762936..1944ba47 100644 --- a/frontend/app/tab/tab.tsx +++ b/frontend/app/tab/tab.tsx @@ -16,7 +16,7 @@ const TabContent = ({ tabId }: { tabId: string }) => { } return (
- {tabData.blockIds.map((blockId: string) => { + {tabData.blockids.map((blockId: string) => { return (
diff --git a/frontend/app/view/preview.tsx b/frontend/app/view/preview.tsx index 3390cd89..e3ecaad8 100644 --- a/frontend/app/view/preview.tsx +++ b/frontend/app/view/preview.tsx @@ -9,6 +9,7 @@ import { FileService, FileInfo, FullFile } from "@/bindings/fileservice"; import * as util from "@/util/util"; import { CenteredDiv } from "../element/quickelems"; import { DirectoryTable } from "@/element/directorytable"; +import * as wstore from "@/gopkg/wstore"; import "./view.less"; @@ -61,7 +62,7 @@ function DirectoryPreview({ contentAtom }: { contentAtom: jotai.Atom = blockDataMap.get(blockId); + const blockDataAtom: jotai.Atom = blockDataMap.get(blockId); const fileNameAtom = useBlockAtom(blockId, "preview:filename", () => jotai.atom((get) => { return get(blockDataAtom)?.meta?.file; diff --git a/frontend/app/workspace/workspace.tsx b/frontend/app/workspace/workspace.tsx index afe029c6..0e296e5e 100644 --- a/frontend/app/workspace/workspace.tsx +++ b/frontend/app/workspace/workspace.tsx @@ -8,11 +8,15 @@ import { clsx } from "clsx"; import { atoms, addBlockIdToTab, 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 "./workspace.less"; +import { CenteredLoadingDiv, CenteredDiv } from "../element/quickelems"; -function Tab({ tab }: { tab: TabData }) { +function Tab({ tab }: { tab: wstore.Tab }) { const [activeTab, setActiveTab] = jotai.useAtom(atoms.activeTabId); return (
setActiveTab(tab.tabid)}> @@ -25,11 +29,12 @@ 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 handleAddTab() { const newTabId = uuidv4(); const newTabName = "Tab " + (tabData.length + 1); - setTabData([...tabData, { name: newTabName, tabid: newTabId, blockIds: [] }]); + setTabData([...tabData, { name: newTabName, tabid: newTabId, blockids: [] }]); setActiveTab(newTabId); } @@ -48,8 +53,8 @@ function TabBar() { function Widgets() { const activeTabId = jotai.useAtomValue(atoms.activeTabId); - async function createBlock(blockDef: BlockDef) { - const rtOpts = { termsize: { rows: 25, cols: 80 } }; + async function createBlock(blockDef: wstore.BlockDef) { + const rtOpts: wstore.RuntimeOpts = new wstore.RuntimeOpts({ termsize: { rows: 25, cols: 80 } }); const rtnBlock: wstore.Block = await BlockService.CreateBlock(blockDef, rtOpts); const newBlockAtom = jotai.atom(rtnBlock); blockDataMap.set(rtnBlock.blockid, newBlockAtom); @@ -57,25 +62,25 @@ function Widgets() { } async function clickTerminal() { - const termBlockDef = { + const termBlockDef = new wstore.BlockDef({ controller: "shell", view: "term", - }; + }); createBlock(termBlockDef); } async function clickPreview(fileName: string) { - const markdownDef = { + const markdownDef = new wstore.BlockDef({ view: "preview", meta: { file: fileName }, - }; + }); createBlock(markdownDef); } async function clickPlot() { - const plotDef = { + const plotDef = new wstore.BlockDef({ view: "plot", - }; + }); createBlock(plotDef); } @@ -106,17 +111,35 @@ function Widgets() { ); } -function Workspace() { +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") { + return ; + } + if (wsLoadable.state === "hasError") { + return Error: {wsLoadable.error?.toString()}; + } + const ws: Workspace = wsLoadable.data; return (
- +
); } -export { Workspace }; +export { WorkspaceElem as Workspace }; diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index f3f95c4c..d12626e2 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -1,38 +1,6 @@ // Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -declare global { - type MetaDataType = Record; - - type TabData = { - name: string; - tabid: string; - blockIds: string[]; - }; - - type BlockData = { - blockid: string; - blockdef: BlockDef; - controller: string; - controllerstatus: string; - view: string; - meta?: MetaDataType; - }; - - type FileDef = { - filetype?: string; - path?: string; - url?: string; - content?: string; - meta?: MetaDataType; - }; - - type BlockDef = { - controller?: string; - view: string; - files?: FileDef[]; - meta?: MetaDataType; - }; -} +declare global {} export {}; diff --git a/frontend/wave.ts b/frontend/wave.ts index 06c44d83..992eed3c 100644 --- a/frontend/wave.ts +++ b/frontend/wave.ts @@ -5,10 +5,35 @@ import * as React from "react"; import { createRoot } from "react-dom/client"; 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 * as wailsRuntime from "@wailsio/runtime"; +import * as wstore from "@/gopkg/wstore"; +import { immerable } from "immer"; + +const urlParams = new URLSearchParams(window.location.search); +const windowId = urlParams.get("windowid"); +globalStore.set(atoms.windowId, windowId); + +wstore.Block.prototype[immerable] = true; +wstore.Tab.prototype[immerable] = true; +wstore.Client.prototype[immerable] = true; +wstore.Window.prototype[immerable] = true; +wstore.Workspace.prototype[immerable] = true; +wstore.BlockDef.prototype[immerable] = true; +wstore.RuntimeOpts.prototype[immerable] = true; +wstore.FileDef.prototype[immerable] = true; +wstore.Point.prototype[immerable] = true; +wstore.WinSize.prototype[immerable] = true; loadFonts(); -document.addEventListener("DOMContentLoaded", () => { +document.addEventListener("DOMContentLoaded", async () => { + const client = await ClientService.GetClientData(); + globalStore.set(atoms.clientAtom, client); + const window = await ClientService.GetWindow(windowId); + globalStore.set(atoms.windowData, window); let reactElem = React.createElement(App, null, null); let elem = document.getElementById("main"); let root = createRoot(elem); diff --git a/main.go b/main.go index 6995a702..22e10f59 100644 --- a/main.go +++ b/main.go @@ -6,15 +6,18 @@ package main // Note, main.go needs to be in the root of the project for the go:embed directive to work. import ( + "context" "embed" "log" "net/http" "runtime" "strings" + "time" "github.com/wavetermdev/thenextwave/pkg/blockstore" "github.com/wavetermdev/thenextwave/pkg/eventbus" "github.com/wavetermdev/thenextwave/pkg/service/blockservice" + "github.com/wavetermdev/thenextwave/pkg/service/clientservice" "github.com/wavetermdev/thenextwave/pkg/service/fileservice" "github.com/wavetermdev/thenextwave/pkg/wavebase" "github.com/wavetermdev/thenextwave/pkg/wstore" @@ -33,10 +36,10 @@ func createAppMenu(app *application.App) *application.Menu { menu := application.NewMenu() menu.AddRole(application.AppMenu) fileMenu := menu.AddSubmenu("File") - newWindow := fileMenu.Add("New Window") - newWindow.OnClick(func(appContext *application.Context) { - createWindow(app) - }) + // newWindow := fileMenu.Add("New Window") + // newWindow.OnClick(func(appContext *application.Context) { + // createWindow(app) + // }) closeWindow := fileMenu.Add("Close Window") closeWindow.OnClick(func(appContext *application.Context) { app.CurrentWindow().Close() @@ -48,7 +51,7 @@ func createAppMenu(app *application.App) *application.Menu { return menu } -func createWindow(app *application.App) { +func createWindow(windowData *wstore.Window, app *application.App) { window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ Title: "Wave Terminal", Mac: application.MacWindow{ @@ -56,13 +59,18 @@ func createWindow(app *application.App) { Backdrop: application.MacBackdropTranslucent, TitleBar: application.MacTitleBarHiddenInset, }, - BackgroundColour: application.NewRGB(27, 38, 54), - URL: "/public/index.html", + BackgroundColour: application.NewRGB(0, 0, 0), + URL: "/public/index.html?windowid=" + windowData.WindowId, + X: windowData.Pos.X, + Y: windowData.Pos.Y, + Width: windowData.WinSize.Width, + Height: windowData.WinSize.Height, }) eventbus.RegisterWailsWindow(window) window.On(events.Common.WindowClosing, func(event *application.WindowEvent) { eventbus.UnregisterWailsWindow(window.ID()) }) + window.Show() } type waveAssetHandler struct { @@ -110,6 +118,11 @@ func main() { log.Printf("error initializing wstore: %v\n", err) return } + err = wstore.EnsureInitialData() + if err != nil { + log.Printf("error ensuring initial data: %v\n", err) + return + } app := application.New(application.Options{ Name: "NextWave", @@ -117,6 +130,7 @@ func main() { Services: []application.Service{ application.NewService(&fileservice.FileService{}), application.NewService(&blockservice.BlockService{}), + application.NewService(&clientservice.ClientService{}), }, Icon: appIcon, Assets: application.AssetOptions{ @@ -130,7 +144,23 @@ func main() { app.SetMenu(menu) eventbus.RegisterWailsApp(app) - createWindow(app) + setupCtx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second) + defer cancelFn() + client, err := wstore.DBGetSingleton[wstore.Client](setupCtx) + if err != nil { + log.Printf("error getting client data: %v\n", err) + return + } + mainWindow, err := wstore.DBGet[wstore.Window](setupCtx, client.MainWindowId) + if err != nil { + log.Printf("error getting main window: %v\n", err) + return + } + if mainWindow == nil { + log.Printf("no main window data\n") + return + } + createWindow(mainWindow, app) eventbus.Start() defer eventbus.Shutdown() diff --git a/pkg/blockcontroller/blockcontroller.go b/pkg/blockcontroller/blockcontroller.go index 53ccded9..182e07ef 100644 --- a/pkg/blockcontroller/blockcontroller.go +++ b/pkg/blockcontroller/blockcontroller.go @@ -4,12 +4,14 @@ package blockcontroller import ( + "context" "encoding/base64" "encoding/json" "fmt" "io" "log" "sync" + "time" "github.com/creack/pty" "github.com/google/uuid" @@ -24,6 +26,8 @@ const ( BlockController_Cmd = "cmd" ) +const DefaultTimeout = 2 * time.Second + var globalLock = &sync.Mutex{} var blockControllerMap = make(map[string]*BlockController) @@ -32,11 +36,18 @@ type BlockController struct { BlockId string BlockDef *wstore.BlockDef InputCh chan BlockCommand + Status string ShellProc *shellexec.ShellProc ShellInputCh chan *InputCommand } +func (bc *BlockController) WithLock(f func()) { + bc.Lock.Lock() + defer bc.Lock.Unlock() + f() +} + func jsonDeepCopy(val map[string]any) (map[string]any, error) { barr, err := json.Marshal(val) if err != nil { @@ -50,10 +61,9 @@ func jsonDeepCopy(val map[string]any) (map[string]any, error) { return rtn, nil } -func CreateBlock(bdef *wstore.BlockDef, rtOpts *wstore.RuntimeOpts) (*wstore.Block, error) { +func CreateBlock(ctx context.Context, bdef *wstore.BlockDef, rtOpts *wstore.RuntimeOpts) (*wstore.Block, error) { blockId := uuid.New().String() blockData := &wstore.Block{ - Lock: &sync.Mutex{}, BlockId: blockId, BlockDef: bdef, Controller: bdef.Controller, @@ -65,7 +75,10 @@ func CreateBlock(bdef *wstore.BlockDef, rtOpts *wstore.RuntimeOpts) (*wstore.Blo if err != nil { return nil, fmt.Errorf("error copying meta: %w", err) } - wstore.BlockMap.Set(blockId, blockData) + err = wstore.DBInsert(ctx, blockData) + if err != nil { + return nil, fmt.Errorf("error inserting block: %w", err) + } if blockData.Controller != "" { StartBlockController(blockId, blockData) } @@ -179,10 +192,10 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts) error { func (bc *BlockController) Run(bdata *wstore.Block) { defer func() { - bdata.WithLock(func() { + bc.WithLock(func() { // if the controller had an error status, don't change it - if bdata.ControllerStatus == "running" { - bdata.ControllerStatus = "done" + if bc.Status == "running" { + bc.Status = "done" } }) eventbus.SendEvent(application.WailsEvent{ @@ -193,8 +206,8 @@ func (bc *BlockController) Run(bdata *wstore.Block) { defer globalLock.Unlock() delete(blockControllerMap, bc.BlockId) }() - bdata.WithLock(func() { - bdata.ControllerStatus = "running" + bc.WithLock(func() { + bc.Status = "running" }) // only controller is "shell" for now @@ -221,9 +234,6 @@ func (bc *BlockController) Run(bdata *wstore.Block) { func StartBlockController(blockId string, bdata *wstore.Block) { if bdata.Controller != BlockController_Shell { log.Printf("unknown controller %q\n", bdata.Controller) - bdata.WithLock(func() { - bdata.ControllerStatus = "error" - }) return } globalLock.Lock() @@ -234,6 +244,7 @@ func StartBlockController(blockId string, bdata *wstore.Block) { bc := &BlockController{ Lock: &sync.Mutex{}, BlockId: blockId, + Status: "init", InputCh: make(chan BlockCommand), } blockControllerMap[blockId] = bc @@ -246,31 +257,47 @@ func GetBlockController(blockId string) *BlockController { return blockControllerMap[blockId] } -func ProcessStaticCommand(blockId string, cmdGen BlockCommand) { +func ProcessStaticCommand(blockId string, cmdGen BlockCommand) error { + ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) + defer cancelFn() switch cmd := cmdGen.(type) { case *MessageCommand: log.Printf("MESSAGE: %s | %q\n", blockId, cmd.Message) + return nil case *SetViewCommand: log.Printf("SETVIEW: %s | %q\n", blockId, cmd.View) - block := wstore.BlockMap.Get(blockId) - if block != nil { - block.WithLock(func() { - block.View = cmd.View - }) + block, err := wstore.DBGet[wstore.Block](ctx, blockId) + if err != nil { + return fmt.Errorf("error getting block: %w", err) } + block.View = cmd.View + err = wstore.DBUpdate[wstore.Block](ctx, block) + if err != nil { + return fmt.Errorf("error updating block: %w", err) + } + return nil case *SetMetaCommand: log.Printf("SETMETA: %s | %v\n", blockId, cmd.Meta) - block := wstore.BlockMap.Get(blockId) - if block != nil { - block.WithLock(func() { - for k, v := range cmd.Meta { - if v == nil { - delete(block.Meta, k) - continue - } - block.Meta[k] = v - } - }) + block, err := wstore.DBGet[wstore.Block](ctx, blockId) + if err != nil { + return fmt.Errorf("error getting block: %w", err) } + if block == nil { + return nil + } + for k, v := range cmd.Meta { + if v == nil { + delete(block.Meta, k) + continue + } + block.Meta[k] = v + } + err = wstore.DBUpdate(ctx, block) + if err != nil { + return fmt.Errorf("error updating block: %w", err) + } + return nil + default: + return fmt.Errorf("unknown command type %T", cmdGen) } } diff --git a/pkg/blockstore/dbsetup.go b/pkg/blockstore/dbsetup.go index c6e87974..a4937ab9 100644 --- a/pkg/blockstore/dbsetup.go +++ b/pkg/blockstore/dbsetup.go @@ -13,11 +13,9 @@ import ( "path" "time" + "github.com/wavetermdev/thenextwave/pkg/util/migrateutil" "github.com/wavetermdev/thenextwave/pkg/wavebase" - "github.com/golang-migrate/migrate/v4" - sqlite3migrate "github.com/golang-migrate/migrate/v4/database/sqlite3" - "github.com/golang-migrate/migrate/v4/source/iofs" "github.com/jmoiron/sqlx" _ "github.com/mattn/go-sqlite3" "github.com/sawka/txwrap" @@ -40,7 +38,7 @@ func InitBlockstore() error { if err != nil { return err } - err = MigrateBlockstore() + err = migrateutil.Migrate("blockstore", globalDB.DB, dbfs.BlockstoreMigrationFS, "migrations-blockstore") if err != nil { return err } @@ -79,61 +77,3 @@ func WithTx(ctx context.Context, fn func(tx *TxWrap) error) error { func WithTxRtn[RT any](ctx context.Context, fn func(tx *TxWrap) (RT, error)) (RT, error) { return txwrap.WithTxRtn(ctx, globalDB, fn) } - -func MakeBlockstoreMigrate() (*migrate.Migrate, error) { - fsVar, err := iofs.New(dbfs.BlockstoreMigrationFS, "migrations-blockstore") - if err != nil { - return nil, fmt.Errorf("opening iofs: %w", err) - } - mdriver, err := sqlite3migrate.WithInstance(globalDB.DB, &sqlite3migrate.Config{}) - if err != nil { - return nil, fmt.Errorf("making blockstore migration driver: %w", err) - } - m, err := migrate.NewWithInstance("iofs", fsVar, "sqlite3", mdriver) - if err != nil { - return nil, fmt.Errorf("making blockstore migration db[%s]: %w", GetDBName(), err) - } - return m, nil -} - -func MigrateBlockstore() error { - log.Printf("migrate blockstore\n") - m, err := MakeBlockstoreMigrate() - if err != nil { - return err - } - curVersion, dirty, err := GetMigrateVersion(m) - if dirty { - return fmt.Errorf("cannot migrate up, database is dirty") - } - if err != nil { - return fmt.Errorf("cannot get current migration version: %v", err) - } - err = m.Up() - if err != nil && err != migrate.ErrNoChange { - return fmt.Errorf("migrating blockstore: %w", err) - } - newVersion, _, err := GetMigrateVersion(m) - if err != nil { - return fmt.Errorf("cannot get new migration version: %v", err) - } - if newVersion != curVersion { - log.Printf("[db] blockstore migration done, version %d -> %d\n", curVersion, newVersion) - } - return nil -} - -func GetMigrateVersion(m *migrate.Migrate) (uint, bool, error) { - if m == nil { - var err error - m, err = MakeBlockstoreMigrate() - if err != nil { - return 0, false, err - } - } - curVersion, dirty, err := m.Version() - if err == migrate.ErrNilVersion { - return 0, false, nil - } - return curVersion, dirty, err -} diff --git a/pkg/service/blockservice/blockservice.go b/pkg/service/blockservice/blockservice.go index dfd3ef2e..20e08fc5 100644 --- a/pkg/service/blockservice/blockservice.go +++ b/pkg/service/blockservice/blockservice.go @@ -10,24 +10,23 @@ import ( "time" "github.com/wavetermdev/thenextwave/pkg/blockcontroller" - "github.com/wavetermdev/thenextwave/pkg/util/utilfn" "github.com/wavetermdev/thenextwave/pkg/wstore" ) type BlockService struct{} -func (bs *BlockService) CreateBlock(bdefMap map[string]any, rtOptsMap map[string]any) (*wstore.Block, error) { - var bdef wstore.BlockDef - err := utilfn.JsonMapToStruct(bdefMap, &bdef) - if err != nil { - return nil, fmt.Errorf("error unmarshalling BlockDef: %w", err) +const DefaultTimeout = 2 * time.Second + +func (bs *BlockService) CreateBlock(bdef *wstore.BlockDef, rtOpts *wstore.RuntimeOpts) (*wstore.Block, error) { + ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second) + defer cancelFn() + if bdef == nil { + return nil, fmt.Errorf("block definition is nil") } - var rtOpts wstore.RuntimeOpts - err = utilfn.JsonMapToStruct(rtOptsMap, &rtOpts) - if err != nil { - return nil, fmt.Errorf("error unmarshalling RuntimeOpts: %w", err) + if rtOpts == nil { + return nil, fmt.Errorf("runtime options is nil") } - blockData, err := blockcontroller.CreateBlock(&bdef, &rtOpts) + blockData, err := blockcontroller.CreateBlock(ctx, bdef, rtOpts) if err != nil { return nil, fmt.Errorf("error creating block: %w", err) } @@ -41,7 +40,7 @@ func (bs *BlockService) CloseBlock(blockId string) { func (bs *BlockService) GetBlockData(blockId string) (*wstore.Block, error) { ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second) defer cancelFn() - blockData, err := wstore.BlockGet(ctx, blockId) + blockData, err := wstore.DBGet[wstore.Block](ctx, blockId) if err != nil { return nil, fmt.Errorf("error getting block data: %w", err) } diff --git a/pkg/service/clientservice/clientservice.go b/pkg/service/clientservice/clientservice.go new file mode 100644 index 00000000..e86c5fe4 --- /dev/null +++ b/pkg/service/clientservice/clientservice.go @@ -0,0 +1,56 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package clientservice + +import ( + "context" + "fmt" + "time" + + "github.com/wavetermdev/thenextwave/pkg/wstore" +) + +type ClientService struct{} + +const DefaultTimeout = 2 * time.Second + +func (cs *ClientService) GetClientData() (*wstore.Client, error) { + ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) + defer cancelFn() + clientData, err := wstore.DBGetSingleton[wstore.Client](ctx) + if err != nil { + return nil, fmt.Errorf("error getting client data: %w", err) + } + return clientData, nil +} + +func (cs *ClientService) GetWorkspace(workspaceId string) (*wstore.Workspace, error) { + ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) + defer cancelFn() + ws, err := wstore.DBGet[wstore.Workspace](ctx, workspaceId) + if err != nil { + return nil, fmt.Errorf("error getting workspace: %w", err) + } + return ws, nil +} + +func (cs *ClientService) GetTab(tabId string) (*wstore.Tab, error) { + ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) + defer cancelFn() + tab, err := wstore.DBGet[wstore.Tab](ctx, tabId) + if err != nil { + return nil, fmt.Errorf("error getting tab: %w", err) + } + return tab, nil +} + +func (cs *ClientService) GetWindow(windowId string) (*wstore.Window, error) { + ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) + defer cancelFn() + window, err := wstore.DBGet[wstore.Window](ctx, windowId) + if err != nil { + return nil, fmt.Errorf("error getting window: %w", err) + } + return window, nil +} diff --git a/pkg/util/migrateutil/migrateutil.go b/pkg/util/migrateutil/migrateutil.go new file mode 100644 index 00000000..c27f5a32 --- /dev/null +++ b/pkg/util/migrateutil/migrateutil.go @@ -0,0 +1,67 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package migrateutil + +import ( + "database/sql" + "fmt" + "io/fs" + "log" + + "github.com/golang-migrate/migrate/v4" + "github.com/golang-migrate/migrate/v4/source/iofs" + + sqlite3migrate "github.com/golang-migrate/migrate/v4/database/sqlite3" +) + +func GetMigrateVersion(m *migrate.Migrate) (uint, bool, error) { + curVersion, dirty, err := m.Version() + if err == migrate.ErrNilVersion { + return 0, false, nil + } + return curVersion, dirty, err +} + +func MakeMigrate(storeName string, db *sql.DB, migrationFS fs.FS, migrationsName string) (*migrate.Migrate, error) { + fsVar, err := iofs.New(migrationFS, migrationsName) + if err != nil { + return nil, fmt.Errorf("opening fs: %w", err) + } + mdriver, err := sqlite3migrate.WithInstance(db, &sqlite3migrate.Config{}) + if err != nil { + return nil, fmt.Errorf("making %s migration driver: %w", storeName, err) + } + m, err := migrate.NewWithInstance("iofs", fsVar, "sqlite3", mdriver) + if err != nil { + return nil, fmt.Errorf("making %s migration: %w", storeName, err) + } + return m, nil +} + +func Migrate(storeName string, db *sql.DB, migrationFS fs.FS, migrationsName string) error { + log.Printf("migrate %s\n", storeName) + m, err := MakeMigrate(storeName, db, migrationFS, migrationsName) + if err != nil { + return err + } + curVersion, dirty, err := GetMigrateVersion(m) + if dirty { + return fmt.Errorf("%s, migrate up, database is dirty", storeName) + } + if err != nil { + return fmt.Errorf("%s, cannot get current migration version: %v", storeName, err) + } + err = m.Up() + if err != nil && err != migrate.ErrNoChange { + return fmt.Errorf("migrating %s: %w", storeName, err) + } + newVersion, _, err := GetMigrateVersion(m) + if err != nil { + return fmt.Errorf("%s, cannot get new migration version: %v", storeName, err) + } + if newVersion != curVersion { + log.Printf("[db] %s migration done, version %d -> %d\n", storeName, curVersion, newVersion) + } + return nil +} diff --git a/pkg/wstore/wstore.go b/pkg/wstore/wstore.go index 62265770..9fb6cd1d 100644 --- a/pkg/wstore/wstore.go +++ b/pkg/wstore/wstore.go @@ -7,6 +7,7 @@ import ( "context" "fmt" "sync" + "time" "github.com/google/uuid" "github.com/wavetermdev/thenextwave/pkg/shellexec" @@ -18,7 +19,28 @@ var TabMap = ds.NewSyncMap[*Tab]() var BlockMap = ds.NewSyncMap[*Block]() type Client struct { - DefaultWorkspaceId string `json:"defaultworkspaceid"` + ClientId string `json:"clientid"` + MainWindowId string `json:"mainwindowid"` +} + +func (c Client) GetId() string { + return c.ClientId +} + +// stores the ui-context of the window +// workspaceid, active tab, active block within each tab, window size, etc. +type Window struct { + WindowId string `json:"windowid"` + WorkspaceId string `json:"workspaceid"` + ActiveTabId string `json:"activetabid"` + ActiveBlockMap map[string]string `json:"activeblockmap"` // map from tabid to blockid + Pos Point `json:"pos"` + WinSize WinSize `json:"winsize"` + LastFocusTs int64 `json:"lastfocusts"` +} + +func (w Window) GetId() string { + return w.WindowId } type Workspace struct { @@ -28,6 +50,10 @@ type Workspace struct { TabIds []string `json:"tabids"` } +func (ws Workspace) GetId() string { + return ws.WorkspaceId +} + func (ws *Workspace) WithLock(f func()) { ws.Lock.Lock() defer ws.Lock.Unlock() @@ -41,6 +67,10 @@ type Tab struct { BlockIds []string `json:"blockids"` } +func (tab Tab) GetId() string { + return tab.TabId +} + func (tab *Tab) WithLock(f func()) { tab.Lock.Lock() defer tab.Lock.Unlock() @@ -67,25 +97,31 @@ type RuntimeOpts struct { WinSize WinSize `json:"winsize,omitempty"` } +type Point struct { + X int `json:"x"` + Y int `json:"y"` +} + type WinSize struct { Width int `json:"width"` Height int `json:"height"` } type Block struct { - Lock *sync.Mutex `json:"-"` - BlockId string `json:"blockid"` - BlockDef *BlockDef `json:"blockdef"` - Controller string `json:"controller"` - ControllerStatus string `json:"controllerstatus"` - View string `json:"view"` - Meta map[string]any `json:"meta,omitempty"` - RuntimeOpts *RuntimeOpts `json:"runtimeopts,omitempty"` + BlockId string `json:"blockid"` + BlockDef *BlockDef `json:"blockdef"` + Controller string `json:"controller"` + View string `json:"view"` + Meta map[string]any `json:"meta,omitempty"` + RuntimeOpts *RuntimeOpts `json:"runtimeopts,omitempty"` } +func (b Block) GetId() string { + return b.BlockId +} + +// TODO remove func (b *Block) WithLock(f func()) { - b.Lock.Lock() - defer b.Lock.Unlock() f() } @@ -121,30 +157,60 @@ func CreateWorkspace() (*Workspace, error) { return ws, nil } -func EnsureWorkspace(ctx context.Context) error { - wsCount, err := WorkspaceCount(ctx) +func EnsureInitialData() error { + ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second) + defer cancelFn() + clientCount, err := DBGetCount[Client](ctx) if err != nil { - return fmt.Errorf("error getting workspace count: %w", err) + return fmt.Errorf("error getting client count: %w", err) } - if wsCount > 0 { + if clientCount > 0 { return nil } - ws := &Workspace{ - Lock: &sync.Mutex{}, - WorkspaceId: uuid.New().String(), - Name: "default", + windowId := uuid.New().String() + workspaceId := uuid.New().String() + tabId := uuid.New().String() + client := &Client{ + ClientId: uuid.New().String(), + MainWindowId: windowId, } - err = WorkspaceInsert(ctx, ws) + err = DBInsert(ctx, client) + if err != nil { + return fmt.Errorf("error inserting client: %w", err) + } + window := &Window{ + WindowId: windowId, + WorkspaceId: workspaceId, + ActiveTabId: tabId, + ActiveBlockMap: make(map[string]string), + Pos: Point{ + X: 100, + Y: 100, + }, + WinSize: WinSize{ + Width: 800, + Height: 600, + }, + } + err = DBInsert(ctx, window) + if err != nil { + return fmt.Errorf("error inserting window: %w", err) + } + ws := &Workspace{ + WorkspaceId: workspaceId, + Name: "default", + TabIds: []string{tabId}, + } + err = DBInsert(ctx, ws) if err != nil { return fmt.Errorf("error inserting workspace: %w", err) } tab := &Tab{ - Lock: &sync.Mutex{}, TabId: uuid.New().String(), Name: "Tab 1", BlockIds: []string{}, } - err = TabInsert(ctx, tab, ws.WorkspaceId) + err = DBInsert(ctx, tab) if err != nil { return fmt.Errorf("error inserting tab: %w", err) } diff --git a/pkg/wstore/wstore_dbops.go b/pkg/wstore/wstore_dbops.go index 3840c893..003e319a 100644 --- a/pkg/wstore/wstore_dbops.go +++ b/pkg/wstore/wstore_dbops.go @@ -6,92 +6,155 @@ package wstore import ( "context" "fmt" - - "github.com/google/uuid" + "reflect" ) -func WorkspaceCount(ctx context.Context) (int, error) { +const Table_Client = "db_client" +const Table_Workspace = "db_workspace" +const Table_Tab = "db_tab" +const Table_Block = "db_block" +const Table_Window = "db_window" + +// can replace with struct tags in the future +type ObjectWithId interface { + GetId() string +} + +// can replace these with struct tags in the future +var idColumnName = map[string]string{ + Table_Client: "clientid", + Table_Workspace: "workspaceid", + Table_Tab: "tabid", + Table_Block: "blockid", + Table_Window: "windowid", +} + +var tableToType = map[string]reflect.Type{ + Table_Client: reflect.TypeOf(Client{}), + Table_Workspace: reflect.TypeOf(Workspace{}), + Table_Tab: reflect.TypeOf(Tab{}), + Table_Block: reflect.TypeOf(Block{}), + Table_Window: reflect.TypeOf(Window{}), +} + +var typeToTable map[reflect.Type]string + +func init() { + typeToTable = make(map[reflect.Type]string) + for k, v := range tableToType { + typeToTable[v] = k + } +} + +func DBGetCount[T ObjectWithId](ctx context.Context) (int, error) { return WithTxRtn(ctx, func(tx *TxWrap) (int, error) { - query := "SELECT count(*) FROM workspace" + var valInstance T + table := typeToTable[reflect.TypeOf(valInstance)] + if table == "" { + return 0, fmt.Errorf("unknown table type: %T", valInstance) + } + query := fmt.Sprintf("SELECT count(*) FROM %s", table) return tx.GetInt(query), nil }) } -func WorkspaceInsert(ctx context.Context, ws *Workspace) error { - if ws.WorkspaceId == "" { - ws.WorkspaceId = uuid.New().String() - } - return WithTx(ctx, func(tx *TxWrap) error { - query := "INSERT INTO workspace (workspaceid, data) VALUES (?, ?)" - tx.Exec(query, ws.WorkspaceId, TxJson(tx, ws)) - return nil +func DBGetSingleton[T ObjectWithId](ctx context.Context) (*T, error) { + return WithTxRtn(ctx, func(tx *TxWrap) (*T, error) { + var rtn T + query := fmt.Sprintf("SELECT data FROM %s LIMIT 1", typeToTable[reflect.TypeOf(rtn)]) + jsonData := tx.GetString(query) + return TxReadJson[T](tx, jsonData), nil }) } -func WorkspaceGet(ctx context.Context, workspaceId string) (*Workspace, error) { - return WithTxRtn(ctx, func(tx *TxWrap) (*Workspace, error) { - query := "SELECT data FROM workspace WHERE workspaceid = ?" - jsonData := tx.GetString(query, workspaceId) - return TxReadJson[Workspace](tx, jsonData), nil - }) -} - -func WorkspaceUpdate(ctx context.Context, ws *Workspace) error { - return WithTx(ctx, func(tx *TxWrap) error { - query := "UPDATE workspace SET data = ? WHERE workspaceid = ?" - tx.Exec(query, TxJson(tx, ws), ws.WorkspaceId) - return nil - }) -} - -func addTabToWorkspace(ctx context.Context, workspaceId string, tabId string) error { - return WithTx(ctx, func(tx *TxWrap) error { - ws, err := WorkspaceGet(tx.Context(), workspaceId) - if err != nil { - return err +func DBGet[T ObjectWithId](ctx context.Context, id string) (*T, error) { + return WithTxRtn(ctx, func(tx *TxWrap) (*T, error) { + var rtn T + table := typeToTable[reflect.TypeOf(rtn)] + if table == "" { + return nil, fmt.Errorf("unknown table type: %T", rtn) } - if ws == nil { - return fmt.Errorf("workspace not found: %s", workspaceId) + query := fmt.Sprintf("SELECT data FROM %s WHERE %s = ?", table, idColumnName[table]) + jsonData := tx.GetString(query, id) + return TxReadJson[T](tx, jsonData), nil + }) +} + +type idDataType struct { + Id string + Data string +} + +func DBSelectMap[T ObjectWithId](ctx context.Context, ids []string) (map[string]*T, error) { + return WithTxRtn(ctx, func(tx *TxWrap) (map[string]*T, error) { + var valInstance T + table := typeToTable[reflect.TypeOf(valInstance)] + if table == "" { + return nil, fmt.Errorf("unknown table type: %T", &valInstance) } - ws.TabIds = append(ws.TabIds, tabId) - return WorkspaceUpdate(tx.Context(), ws) + var rows []idDataType + query := fmt.Sprintf("SELECT %s, data FROM %s WHERE %s IN (SELECT value FROM json_each(?))", idColumnName[table], table, idColumnName[table]) + tx.Select(&rows, query, ids) + rtnMap := make(map[string]*T) + for _, row := range rows { + if row.Id == "" || row.Data == "" { + continue + } + r := TxReadJson[T](tx, row.Data) + if r == nil { + continue + } + rtnMap[(*r).GetId()] = r + } + return rtnMap, nil }) } -func TabInsert(ctx context.Context, tab *Tab, workspaceId string) error { - if tab.TabId == "" { - tab.TabId = uuid.New().String() - } +func DBDelete[T ObjectWithId](ctx context.Context, id string) error { return WithTx(ctx, func(tx *TxWrap) error { - query := "INSERT INTO tab (tabid, data) VALUES (?, ?)" - tx.Exec(query, tab.TabId, TxJson(tx, tab)) - return addTabToWorkspace(tx.Context(), workspaceId, tab.TabId) - }) -} - -func BlockGet(ctx context.Context, blockId string) (*Block, error) { - return WithTxRtn(ctx, func(tx *TxWrap) (*Block, error) { - query := "SELECT data FROM block WHERE blockid = ?" - jsonData := tx.GetString(query, blockId) - return TxReadJson[Block](tx, jsonData), nil - }) -} - -func BlockDelete(ctx context.Context, blockId string) error { - return WithTx(ctx, func(tx *TxWrap) error { - query := "DELETE FROM block WHERE blockid = ?" - tx.Exec(query, blockId) + var rtn T + table := typeToTable[reflect.TypeOf(rtn)] + if table == "" { + return fmt.Errorf("unknown table type: %T", rtn) + } + query := fmt.Sprintf("DELETE FROM %s WHERE %s = ?", table, idColumnName[table]) + tx.Exec(query, id) return nil }) } -func BlockInsert(ctx context.Context, block *Block) error { - if block.BlockId == "" { - block.BlockId = uuid.New().String() +func DBUpdate[T ObjectWithId](ctx context.Context, val *T) error { + if val == nil { + return fmt.Errorf("cannot update nil value") + } + if (*val).GetId() == "" { + return fmt.Errorf("cannot update %T value with empty id", val) } return WithTx(ctx, func(tx *TxWrap) error { - query := "INSERT INTO block (blockid, data) VALUES (?, ?)" - tx.Exec(query, block.BlockId, TxJson(tx, block)) + table := typeToTable[reflect.TypeOf(*val)] + if table == "" { + return fmt.Errorf("unknown table type: %T", *val) + } + query := fmt.Sprintf("UPDATE %s SET data = ? WHERE %s = ?", table, idColumnName[table]) + tx.Exec(query, TxJson(tx, val), (*val).GetId()) + return nil + }) +} + +func DBInsert[T ObjectWithId](ctx context.Context, val *T) error { + if val == nil { + return fmt.Errorf("cannot insert nil value") + } + if (*val).GetId() == "" { + return fmt.Errorf("cannot insert %T value with empty id", val) + } + return WithTx(ctx, func(tx *TxWrap) error { + table := typeToTable[reflect.TypeOf(*val)] + if table == "" { + return fmt.Errorf("unknown table type: %T", *val) + } + query := fmt.Sprintf("INSERT INTO %s (%s, data) VALUES (?, ?)", table, idColumnName[table]) + tx.Exec(query, (*val).GetId(), TxJson(tx, val)) return nil }) } diff --git a/pkg/wstore/wstore_dbsetup.go b/pkg/wstore/wstore_dbsetup.go index 3e58417d..40ae8530 100644 --- a/pkg/wstore/wstore_dbsetup.go +++ b/pkg/wstore/wstore_dbsetup.go @@ -11,13 +11,11 @@ import ( "path" "time" - "github.com/golang-migrate/migrate/v4" - "github.com/golang-migrate/migrate/v4/source/iofs" "github.com/jmoiron/sqlx" "github.com/sawka/txwrap" + "github.com/wavetermdev/thenextwave/pkg/util/migrateutil" "github.com/wavetermdev/thenextwave/pkg/wavebase" - sqlite3migrate "github.com/golang-migrate/migrate/v4/database/sqlite3" dbfs "github.com/wavetermdev/thenextwave/db" ) @@ -35,7 +33,7 @@ func InitWStore() error { if err != nil { return err } - err = MigrateWStore() + err = migrateutil.Migrate("wstore", globalDB.DB, dbfs.WStoreMigrationFS, "migrations-wstore") if err != nil { return err } @@ -58,41 +56,6 @@ func MakeDB(ctx context.Context) (*sqlx.DB, error) { return rtn, nil } -func MigrateWStore() error { - return nil -} - -func MakeWStoreMigrate() (*migrate.Migrate, error) { - fsVar, err := iofs.New(dbfs.WStoreMigrationFS, "migrations-wstore") - if err != nil { - return nil, fmt.Errorf("opening iofs: %w", err) - } - mdriver, err := sqlite3migrate.WithInstance(globalDB.DB, &sqlite3migrate.Config{}) - if err != nil { - return nil, fmt.Errorf("making blockstore migration driver: %w", err) - } - m, err := migrate.NewWithInstance("iofs", fsVar, "sqlite3", mdriver) - if err != nil { - return nil, fmt.Errorf("making blockstore migration db[%s]: %w", GetDBName(), err) - } - return m, nil -} - -func GetMigrateVersion(m *migrate.Migrate) (uint, bool, error) { - if m == nil { - var err error - m, err = MakeWStoreMigrate() - if err != nil { - return 0, false, err - } - } - curVersion, dirty, err := m.Version() - if err == migrate.ErrNilVersion { - return 0, false, nil - } - return curVersion, dirty, err -} - func WithTx(ctx context.Context, fn func(tx *TxWrap) error) error { return txwrap.WithTx(ctx, globalDB, fn) } From 4ba78a18044f17f848d769c2e9cf1d2bf8879a17 Mon Sep 17 00:00:00 2001 From: sawka Date: Sat, 25 May 2024 18:37:05 -0600 Subject: [PATCH 04/11] working on waveobj -- oids --- go.mod | 1 + go.sum | 2 + pkg/waveobj/waveobj.go | 269 ++++++++++++++++++++++++++++++++++++ pkg/waveobj/waveobj_test.go | 23 +++ pkg/wstore/wstore.go | 4 + 5 files changed, 299 insertions(+) create mode 100644 pkg/waveobj/waveobj.go create mode 100644 pkg/waveobj/waveobj_test.go diff --git a/go.mod b/go.mod index 7ea697ba..e630cfd5 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/google/uuid v1.4.0 github.com/jmoiron/sqlx v1.4.0 github.com/mattn/go-sqlite3 v1.14.22 + github.com/mitchellh/mapstructure v1.5.0 github.com/sawka/txwrap v0.2.0 github.com/wailsapp/wails/v3 v3.0.0-alpha.0 github.com/wavetermdev/waveterm/wavesrv v0.0.0-20240508181017-d07068c09d94 diff --git a/go.sum b/go.sum index 217bbb7f..a346f06c 100644 --- a/go.sum +++ b/go.sum @@ -91,6 +91,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= diff --git a/pkg/waveobj/waveobj.go b/pkg/waveobj/waveobj.go new file mode 100644 index 00000000..9486000c --- /dev/null +++ b/pkg/waveobj/waveobj.go @@ -0,0 +1,269 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package waveobj + +import ( + "bytes" + "encoding/json" + "fmt" + "reflect" + "strings" + "sync" + + "github.com/mitchellh/mapstructure" +) + +const ( + OTypeKeyName = "otype" + OIDKeyName = "oid" +) + +type waveObjDesc struct { + RType reflect.Type + OIDField reflect.StructField +} + +var globalLock = &sync.Mutex{} +var waveObjMap = make(map[string]*waveObjDesc) +var waveObj WaveObj +var waveObjRType = reflect.TypeOf(&waveObj).Elem() + +func RegisterType(w WaveObj) { + globalLock.Lock() + defer globalLock.Unlock() + oidType := w.GetOType() + if waveObjMap[oidType] != nil { + panic(fmt.Sprintf("duplicate WaveObj registration: %T", w)) + } + rtype := reflect.TypeOf(w) + field := findOIDField(rtype) + if field == nil { + panic(fmt.Sprintf("cannot register WaveObj without OID field -- mark with tag `waveobj:\"oid\"`")) + } + waveObjMap[oidType] = &waveObjDesc{ + RType: rtype, + OIDField: *field, + } +} + +func findOIDField(rtype reflect.Type) *reflect.StructField { + for idx := 0; idx < rtype.NumField(); idx++ { + field := rtype.Field(idx) + if field.PkgPath != "" { + // private + continue + } + waveObjTag := field.Tag.Get("waveobj") + if waveObjTag == "oid" { + if field.Type.Kind() != reflect.String { + panic(fmt.Sprintf("in %v marked oid field is not type 'string'", rtype)) + } + return &field + } + } + return nil +} + +func getObjDescForOIDType(oidType string) *waveObjDesc { + globalLock.Lock() + defer globalLock.Unlock() + return waveObjMap[oidType] +} + +type WaveObj interface { + GetOType() string +} + +func ToJson(w WaveObj) ([]byte, error) { + m := make(map[string]any) + err := mapstructure.Decode(w, &m) + if err != nil { + return nil, err + } + desc := getObjDescForOIDType(w.GetOType()) + if desc == nil { + return nil, fmt.Errorf("otype %q (%T) not registered", w.GetOType(), w) + } + m[OTypeKeyName] = w.GetOType() + m[OIDKeyName] = reflect.ValueOf(w).FieldByIndex(desc.OIDField.Index).String() + return json.Marshal(m) +} + +func FromJson(data []byte) (WaveObj, error) { + var m map[string]any + err := json.Unmarshal(data, &m) + if err != nil { + return nil, err + } + otype, ok := m[OTypeKeyName].(string) + if !ok { + return nil, fmt.Errorf("missing otype") + } + oid, ok := m[OIDKeyName].(string) + if !ok { + return nil, fmt.Errorf("missing oid") + } + desc := getObjDescForOIDType(otype) + if desc == nil { + return nil, fmt.Errorf("unknown oid type: %s", otype) + } + objVal := reflect.New(desc.RType) + oidField := objVal.FieldByIndex(desc.OIDField.Index) + oidField.SetString(oid) + obj := objVal.Interface().(WaveObj) + err = mapstructure.Decode(m, obj) + if err != nil { + return nil, err + } + return obj, nil +} + +func FromJsonGen[T WaveObj](data []byte) (T, error) { + obj, err := FromJson(data) + if err != nil { + var zero T + return zero, err + } + rtn, ok := obj.(T) + if !ok { + var zero T + return zero, fmt.Errorf("type mismatch got %T, expected %T", obj, zero) + } + return rtn, nil +} + +func getTSFieldName(field reflect.StructField) string { + jsonTag := field.Tag.Get("json") + if jsonTag != "" { + parts := strings.Split(jsonTag, ",") + namePart := parts[0] + if namePart != "" { + if namePart == "-" { + return "" + } + return namePart + } + // if namePart is empty, still uses default + } + return field.Name +} + +func isFieldOmitEmpty(field reflect.StructField) bool { + jsonTag := field.Tag.Get("json") + if jsonTag != "" { + parts := strings.Split(jsonTag, ",") + if len(parts) > 1 { + for _, part := range parts[1:] { + if part == "omitempty" { + return true + } + } + } + } + return false +} + +func typeToTSType(t reflect.Type) (string, []reflect.Type) { + switch t.Kind() { + case reflect.String: + return "string", nil + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Float32, reflect.Float64: + return "number", nil + case reflect.Bool: + return "boolean", nil + case reflect.Slice, reflect.Array: + elemType, subTypes := typeToTSType(t.Elem()) + if elemType == "" { + return "", nil + } + return fmt.Sprintf("%s[]", elemType), subTypes + case reflect.Map: + if t.Key().Kind() != reflect.String { + return "", nil + } + elemType, subTypes := typeToTSType(t.Elem()) + if elemType == "" { + return "", nil + } + return fmt.Sprintf("{[key: string]: %s}", elemType), subTypes + case reflect.Struct: + return t.Name(), []reflect.Type{t} + case reflect.Ptr: + return typeToTSType(t.Elem()) + case reflect.Interface: + return "any", nil + default: + return "", nil + } +} + +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())) + if rtype.Implements(waveObjType) || reflect.PointerTo(rtype).Implements(waveObjType) { + buf.WriteString(fmt.Sprintf(" %s: string;\n", OTypeKeyName)) + buf.WriteString(fmt.Sprintf(" %s: string;\n", OIDKeyName)) + } + var subTypes []reflect.Type + for i := 0; i < rtype.NumField(); i++ { + field := rtype.Field(i) + if field.PkgPath != "" { + continue + } + fieldName := getTSFieldName(field) + if fieldName == "" { + continue + } + optMarker := "" + if isFieldOmitEmpty(field) { + optMarker = "?" + } + tsTypeTag := field.Tag.Get("tstype") + if tsTypeTag != "" { + buf.WriteString(fmt.Sprintf(" %s%s: %s;\n", fieldName, optMarker, tsTypeTag)) + continue + } + tsType, fieldSubTypes := typeToTSType(field.Type) + if tsType == "" { + continue + } + subTypes = append(subTypes, fieldSubTypes...) + buf.WriteString(fmt.Sprintf(" %s%s: %s;\n", fieldName, optMarker, tsType)) + } + buf.WriteString("}\n") + return buf.String(), subTypes +} + +func GenerateWaveObjTSType() string { + var buf bytes.Buffer + buf.WriteString("type WaveObj {\n") + buf.WriteString(" otype: string;\n") + buf.WriteString(" oid: string;\n") + buf.WriteString("}\n") + return buf.String() +} + +func GenerateTSType(rtype reflect.Type, tsTypesMap map[reflect.Type]string) { + if rtype == nil { + return + } + if rtype.Kind() == reflect.Ptr { + rtype = rtype.Elem() + } + if _, ok := tsTypesMap[rtype]; ok { + return + } + if rtype == waveObjRType { + tsTypesMap[rtype] = GenerateWaveObjTSType() + return + } + tsType, subTypes := generateTSTypeInternal(rtype) + tsTypesMap[rtype] = tsType + for _, subType := range subTypes { + GenerateTSType(subType, tsTypesMap) + } +} diff --git a/pkg/waveobj/waveobj_test.go b/pkg/waveobj/waveobj_test.go new file mode 100644 index 00000000..e3fd4a7b --- /dev/null +++ b/pkg/waveobj/waveobj_test.go @@ -0,0 +1,23 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package waveobj + +import ( + "log" + "reflect" + "testing" + + "github.com/wavetermdev/thenextwave/pkg/wstore" +) + +func TestGenerate(t *testing.T) { + log.Printf("Testing Generate\n") + tsMap := make(map[reflect.Type]string) + var waveObj WaveObj + GenerateTSType(reflect.TypeOf(&waveObj).Elem(), tsMap) + GenerateTSType(reflect.TypeOf(wstore.Block{}), tsMap) + for k, v := range tsMap { + log.Printf("Type: %v, TS:\n%s\n", k, v) + } +} diff --git a/pkg/wstore/wstore.go b/pkg/wstore/wstore.go index 9fb6cd1d..14210469 100644 --- a/pkg/wstore/wstore.go +++ b/pkg/wstore/wstore.go @@ -116,6 +116,10 @@ type Block struct { RuntimeOpts *RuntimeOpts `json:"runtimeopts,omitempty"` } +func (b *Block) GetOType() string { + return "block" +} + func (b Block) GetId() string { return b.BlockId } From b1aaba2a37513ccbee214330d9bb5d6c808baccb Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 26 May 2024 11:59:14 -0700 Subject: [PATCH 05/11] moving hard to OID model --- db/migrations-wstore/000001_init.up.sql | 15 +- main.go | 6 +- pkg/blockcontroller/blockcontroller.go | 8 +- pkg/service/blockservice/blockservice.go | 2 +- pkg/service/clientservice/clientservice.go | 8 +- pkg/waveobj/waveobj.go | 196 +++++++++++++-------- pkg/waveobj/waveobj_test.go | 13 +- pkg/wstore/wstore.go | 106 +++++------ pkg/wstore/wstore_dbops.go | 181 ++++++++----------- pkg/wstore/wstore_dbsetup.go | 22 --- 10 files changed, 277 insertions(+), 280 deletions(-) diff --git a/db/migrations-wstore/000001_init.up.sql b/db/migrations-wstore/000001_init.up.sql index 957bc803..7fa0c3bd 100644 --- a/db/migrations-wstore/000001_init.up.sql +++ b/db/migrations-wstore/000001_init.up.sql @@ -1,24 +1,29 @@ CREATE TABLE db_client ( - clientid varchar(36) PRIMARY KEY, -- unnecessary, but useful to have a PK + oid varchar(36) PRIMARY KEY, + version int NOT NULL, data json NOT NULL ); CREATE TABLE db_window ( - windowid varchar(36) PRIMARY KEY, + oid varchar(36) PRIMARY KEY, + version int NOT NULL, data json NOT NULL ); CREATE TABLE db_workspace ( - workspaceid varchar(36) PRIMARY KEY, + oid varchar(36) PRIMARY KEY, + version int NOT NULL, data json NOT NULL ); CREATE TABLE db_tab ( - tabid varchar(36) PRIMARY KEY, + oid varchar(36) PRIMARY KEY, + version int NOT NULL, data json NOT NULL ); CREATE TABLE db_block ( - blockid varchar(36) PRIMARY KEY, + oid varchar(36) PRIMARY KEY, + version int NOT NULL, data json NOT NULL ); diff --git a/main.go b/main.go index 22e10f59..9c253235 100644 --- a/main.go +++ b/main.go @@ -60,7 +60,7 @@ func createWindow(windowData *wstore.Window, app *application.App) { TitleBar: application.MacTitleBarHiddenInset, }, BackgroundColour: application.NewRGB(0, 0, 0), - URL: "/public/index.html?windowid=" + windowData.WindowId, + URL: "/public/index.html?windowid=" + windowData.OID, X: windowData.Pos.X, Y: windowData.Pos.Y, Width: windowData.WinSize.Width, @@ -146,12 +146,12 @@ func main() { setupCtx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second) defer cancelFn() - client, err := wstore.DBGetSingleton[wstore.Client](setupCtx) + client, err := wstore.DBGetSingleton[*wstore.Client](setupCtx) if err != nil { log.Printf("error getting client data: %v\n", err) return } - mainWindow, err := wstore.DBGet[wstore.Window](setupCtx, client.MainWindowId) + mainWindow, err := wstore.DBGet[*wstore.Window](setupCtx, client.MainWindowId) if err != nil { log.Printf("error getting main window: %v\n", err) return diff --git a/pkg/blockcontroller/blockcontroller.go b/pkg/blockcontroller/blockcontroller.go index 182e07ef..bb8ade23 100644 --- a/pkg/blockcontroller/blockcontroller.go +++ b/pkg/blockcontroller/blockcontroller.go @@ -64,7 +64,7 @@ func jsonDeepCopy(val map[string]any) (map[string]any, error) { func CreateBlock(ctx context.Context, bdef *wstore.BlockDef, rtOpts *wstore.RuntimeOpts) (*wstore.Block, error) { blockId := uuid.New().String() blockData := &wstore.Block{ - BlockId: blockId, + OID: blockId, BlockDef: bdef, Controller: bdef.Controller, View: bdef.View, @@ -266,19 +266,19 @@ func ProcessStaticCommand(blockId string, cmdGen BlockCommand) error { return nil case *SetViewCommand: log.Printf("SETVIEW: %s | %q\n", blockId, cmd.View) - block, err := wstore.DBGet[wstore.Block](ctx, blockId) + block, err := wstore.DBGet[*wstore.Block](ctx, blockId) if err != nil { return fmt.Errorf("error getting block: %w", err) } block.View = cmd.View - err = wstore.DBUpdate[wstore.Block](ctx, block) + err = wstore.DBUpdate(ctx, block) if err != nil { return fmt.Errorf("error updating block: %w", err) } return nil case *SetMetaCommand: log.Printf("SETMETA: %s | %v\n", blockId, cmd.Meta) - block, err := wstore.DBGet[wstore.Block](ctx, blockId) + block, err := wstore.DBGet[*wstore.Block](ctx, blockId) if err != nil { return fmt.Errorf("error getting block: %w", err) } diff --git a/pkg/service/blockservice/blockservice.go b/pkg/service/blockservice/blockservice.go index 20e08fc5..615cd7f6 100644 --- a/pkg/service/blockservice/blockservice.go +++ b/pkg/service/blockservice/blockservice.go @@ -40,7 +40,7 @@ func (bs *BlockService) CloseBlock(blockId string) { func (bs *BlockService) GetBlockData(blockId string) (*wstore.Block, error) { ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second) defer cancelFn() - blockData, err := wstore.DBGet[wstore.Block](ctx, blockId) + blockData, err := wstore.DBGet[*wstore.Block](ctx, blockId) if err != nil { return nil, fmt.Errorf("error getting block data: %w", err) } diff --git a/pkg/service/clientservice/clientservice.go b/pkg/service/clientservice/clientservice.go index e86c5fe4..5d0a0c6d 100644 --- a/pkg/service/clientservice/clientservice.go +++ b/pkg/service/clientservice/clientservice.go @@ -18,7 +18,7 @@ const DefaultTimeout = 2 * time.Second func (cs *ClientService) GetClientData() (*wstore.Client, error) { ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) defer cancelFn() - clientData, err := wstore.DBGetSingleton[wstore.Client](ctx) + clientData, err := wstore.DBGetSingleton[*wstore.Client](ctx) if err != nil { return nil, fmt.Errorf("error getting client data: %w", err) } @@ -28,7 +28,7 @@ func (cs *ClientService) GetClientData() (*wstore.Client, error) { func (cs *ClientService) GetWorkspace(workspaceId string) (*wstore.Workspace, error) { ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) defer cancelFn() - ws, err := wstore.DBGet[wstore.Workspace](ctx, workspaceId) + ws, err := wstore.DBGet[*wstore.Workspace](ctx, workspaceId) if err != nil { return nil, fmt.Errorf("error getting workspace: %w", err) } @@ -38,7 +38,7 @@ func (cs *ClientService) GetWorkspace(workspaceId string) (*wstore.Workspace, er func (cs *ClientService) GetTab(tabId string) (*wstore.Tab, error) { ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) defer cancelFn() - tab, err := wstore.DBGet[wstore.Tab](ctx, tabId) + tab, err := wstore.DBGet[*wstore.Tab](ctx, tabId) if err != nil { return nil, fmt.Errorf("error getting tab: %w", err) } @@ -48,7 +48,7 @@ func (cs *ClientService) GetTab(tabId string) (*wstore.Tab, error) { func (cs *ClientService) GetWindow(windowId string) (*wstore.Window, error) { ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) defer cancelFn() - window, err := wstore.DBGet[wstore.Window](ctx, windowId) + window, err := wstore.DBGet[*wstore.Window](ctx, windowId) if err != nil { return nil, fmt.Errorf("error getting window: %w", err) } diff --git a/pkg/waveobj/waveobj.go b/pkg/waveobj/waveobj.go index 9486000c..fb773fdf 100644 --- a/pkg/waveobj/waveobj.go +++ b/pkg/waveobj/waveobj.go @@ -15,78 +15,123 @@ import ( ) const ( - OTypeKeyName = "otype" - OIDKeyName = "oid" + OTypeKeyName = "otype" + OIDKeyName = "oid" + VersionKeyName = "version" + + OIDGoFieldName = "OID" + VersionGoFieldName = "Version" ) -type waveObjDesc struct { - RType reflect.Type - OIDField reflect.StructField -} - -var globalLock = &sync.Mutex{} -var waveObjMap = make(map[string]*waveObjDesc) -var waveObj WaveObj -var waveObjRType = reflect.TypeOf(&waveObj).Elem() - -func RegisterType(w WaveObj) { - globalLock.Lock() - defer globalLock.Unlock() - oidType := w.GetOType() - if waveObjMap[oidType] != nil { - panic(fmt.Sprintf("duplicate WaveObj registration: %T", w)) - } - rtype := reflect.TypeOf(w) - field := findOIDField(rtype) - if field == nil { - panic(fmt.Sprintf("cannot register WaveObj without OID field -- mark with tag `waveobj:\"oid\"`")) - } - waveObjMap[oidType] = &waveObjDesc{ - RType: rtype, - OIDField: *field, - } -} - -func findOIDField(rtype reflect.Type) *reflect.StructField { - for idx := 0; idx < rtype.NumField(); idx++ { - field := rtype.Field(idx) - if field.PkgPath != "" { - // private - continue - } - waveObjTag := field.Tag.Get("waveobj") - if waveObjTag == "oid" { - if field.Type.Kind() != reflect.String { - panic(fmt.Sprintf("in %v marked oid field is not type 'string'", rtype)) - } - return &field - } - } - return nil -} - -func getObjDescForOIDType(oidType string) *waveObjDesc { - globalLock.Lock() - defer globalLock.Unlock() - return waveObjMap[oidType] -} - type WaveObj interface { - GetOType() string + GetOType() string // should not depend on object state (should work with nil value) +} + +type waveObjDesc struct { + RType reflect.Type + OIDField reflect.StructField + VersionField reflect.StructField +} + +var waveObjMap = sync.Map{} +var waveObjRType = reflect.TypeOf((*WaveObj)(nil)).Elem() + +func RegisterType[T WaveObj]() { + var waveObj T + otype := waveObj.GetOType() + if otype == "" { + panic(fmt.Sprintf("otype is empty for %T", waveObj)) + } + rtype := reflect.TypeOf(waveObj) + if rtype.Kind() != reflect.Ptr { + panic(fmt.Sprintf("wave object must be a pointer for %T", waveObj)) + } + oidField, found := rtype.Elem().FieldByName(OIDGoFieldName) + if !found { + panic(fmt.Sprintf("missing OID field for %T", waveObj)) + } + if oidField.Type.Kind() != reflect.String { + panic(fmt.Sprintf("OID field must be string for %T", waveObj)) + } + if oidField.Tag.Get("json") != OIDKeyName { + panic(fmt.Sprintf("OID field json tag must be %q for %T", OIDKeyName, waveObj)) + } + versionField, found := rtype.Elem().FieldByName(VersionGoFieldName) + if !found { + panic(fmt.Sprintf("missing Version field for %T", waveObj)) + } + if versionField.Type.Kind() != reflect.Int { + panic(fmt.Sprintf("Version field must be int for %T", waveObj)) + } + if versionField.Tag.Get("json") != VersionKeyName { + panic(fmt.Sprintf("Version field json tag must be %q for %T", VersionKeyName, waveObj)) + } + _, found = waveObjMap.Load(otype) + if found { + panic(fmt.Sprintf("otype %q already registered", otype)) + } + waveObjMap.Store(otype, &waveObjDesc{ + RType: rtype, + OIDField: oidField, + VersionField: versionField, + }) +} + +func getWaveObjDesc(otype string) *waveObjDesc { + desc, _ := waveObjMap.Load(otype) + if desc == nil { + return nil + } + return desc.(*waveObjDesc) +} + +func GetOID(waveObj WaveObj) string { + desc := getWaveObjDesc(waveObj.GetOType()) + if desc == nil { + return "" + } + return reflect.ValueOf(waveObj).Elem().FieldByIndex(desc.OIDField.Index).String() +} + +func SetOID(waveObj WaveObj, oid string) { + desc := getWaveObjDesc(waveObj.GetOType()) + if desc == nil { + return + } + reflect.ValueOf(waveObj).Elem().FieldByIndex(desc.OIDField.Index).SetString(oid) +} + +func GetVersion(waveObj WaveObj) int { + desc := getWaveObjDesc(waveObj.GetOType()) + if desc == nil { + return 0 + } + return int(reflect.ValueOf(waveObj).Elem().FieldByIndex(desc.VersionField.Index).Int()) +} + +func SetVersion(waveObj WaveObj, version int) { + desc := getWaveObjDesc(waveObj.GetOType()) + if desc == nil { + return + } + reflect.ValueOf(waveObj).Elem().FieldByIndex(desc.VersionField.Index).SetInt(int64(version)) } func ToJson(w WaveObj) ([]byte, error) { m := make(map[string]any) - err := mapstructure.Decode(w, &m) + dconfig := &mapstructure.DecoderConfig{ + Result: &m, + TagName: "json", + } + decoder, err := mapstructure.NewDecoder(dconfig) if err != nil { return nil, err } - desc := getObjDescForOIDType(w.GetOType()) - if desc == nil { - return nil, fmt.Errorf("otype %q (%T) not registered", w.GetOType(), w) + err = decoder.Decode(w) + if err != nil { + return nil, err } m[OTypeKeyName] = w.GetOType() - m[OIDKeyName] = reflect.ValueOf(w).FieldByIndex(desc.OIDField.Index).String() return json.Marshal(m) } @@ -100,23 +145,24 @@ func FromJson(data []byte) (WaveObj, error) { if !ok { return nil, fmt.Errorf("missing otype") } - oid, ok := m[OIDKeyName].(string) - if !ok { - return nil, fmt.Errorf("missing oid") - } - desc := getObjDescForOIDType(otype) + desc := getWaveObjDesc(otype) if desc == nil { - return nil, fmt.Errorf("unknown oid type: %s", otype) + return nil, fmt.Errorf("unknown otype: %s", otype) } - objVal := reflect.New(desc.RType) - oidField := objVal.FieldByIndex(desc.OIDField.Index) - oidField.SetString(oid) - obj := objVal.Interface().(WaveObj) - err = mapstructure.Decode(m, obj) + wobj := reflect.Zero(desc.RType).Interface().(WaveObj) + dconfig := &mapstructure.DecoderConfig{ + Result: &wobj, + TagName: "json", + } + decoder, err := mapstructure.NewDecoder(dconfig) if err != nil { return nil, err } - return obj, nil + err = decoder.Decode(m) + if err != nil { + return nil, err + } + return wobj, nil } func FromJsonGen[T WaveObj](data []byte) (T, error) { @@ -204,9 +250,12 @@ 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())) + var isWaveObj bool if rtype.Implements(waveObjType) || reflect.PointerTo(rtype).Implements(waveObjType) { + isWaveObj = true buf.WriteString(fmt.Sprintf(" %s: string;\n", OTypeKeyName)) buf.WriteString(fmt.Sprintf(" %s: string;\n", OIDKeyName)) + buf.WriteString(fmt.Sprintf(" %s: number;\n", VersionKeyName)) } var subTypes []reflect.Type for i := 0; i < rtype.NumField(); i++ { @@ -218,6 +267,9 @@ func generateTSTypeInternal(rtype reflect.Type) (string, []reflect.Type) { if fieldName == "" { continue } + if isWaveObj && (fieldName == OTypeKeyName || fieldName == OIDKeyName || fieldName == VersionKeyName) { + continue + } optMarker := "" if isFieldOmitEmpty(field) { optMarker = "?" diff --git a/pkg/waveobj/waveobj_test.go b/pkg/waveobj/waveobj_test.go index e3fd4a7b..d7db8059 100644 --- a/pkg/waveobj/waveobj_test.go +++ b/pkg/waveobj/waveobj_test.go @@ -7,16 +7,23 @@ import ( "log" "reflect" "testing" - - "github.com/wavetermdev/thenextwave/pkg/wstore" ) +type TestBlock struct { + BlockId string `json:"blockid" waveobj:"oid"` + Name string `json:"name"` +} + +func (TestBlock) GetOType() string { + return "block" +} + func TestGenerate(t *testing.T) { log.Printf("Testing Generate\n") tsMap := make(map[reflect.Type]string) var waveObj WaveObj GenerateTSType(reflect.TypeOf(&waveObj).Elem(), tsMap) - GenerateTSType(reflect.TypeOf(wstore.Block{}), tsMap) + GenerateTSType(reflect.TypeOf(TestBlock{}), tsMap) for k, v := range tsMap { log.Printf("Type: %v, TS:\n%s\n", k, v) } diff --git a/pkg/wstore/wstore.go b/pkg/wstore/wstore.go index 14210469..d71e9267 100644 --- a/pkg/wstore/wstore.go +++ b/pkg/wstore/wstore.go @@ -6,31 +6,41 @@ package wstore import ( "context" "fmt" - "sync" "time" "github.com/google/uuid" "github.com/wavetermdev/thenextwave/pkg/shellexec" "github.com/wavetermdev/thenextwave/pkg/util/ds" + "github.com/wavetermdev/thenextwave/pkg/waveobj" ) var WorkspaceMap = ds.NewSyncMap[*Workspace]() var TabMap = ds.NewSyncMap[*Tab]() var BlockMap = ds.NewSyncMap[*Block]() +func init() { + waveobj.RegisterType[*Client]() + waveobj.RegisterType[*Window]() + waveobj.RegisterType[*Workspace]() + waveobj.RegisterType[*Tab]() + waveobj.RegisterType[*Block]() +} + type Client struct { - ClientId string `json:"clientid"` + OID string `json:"oid"` + Version int `json:"version"` MainWindowId string `json:"mainwindowid"` } -func (c Client) GetId() string { - return c.ClientId +func (*Client) GetOType() string { + return "client" } // stores the ui-context of the window // workspaceid, active tab, active block within each tab, window size, etc. type Window struct { - WindowId string `json:"windowid"` + OID string `json:"oid"` + Version int `json:"version"` WorkspaceId string `json:"workspaceid"` ActiveTabId string `json:"activetabid"` ActiveBlockMap map[string]string `json:"activeblockmap"` // map from tabid to blockid @@ -39,42 +49,30 @@ type Window struct { LastFocusTs int64 `json:"lastfocusts"` } -func (w Window) GetId() string { - return w.WindowId +func (*Window) GetOType() string { + return "window" } type Workspace struct { - Lock *sync.Mutex `json:"-"` - WorkspaceId string `json:"workspaceid"` - Name string `json:"name"` - TabIds []string `json:"tabids"` + OID string `json:"oid"` + Version int `json:"version"` + Name string `json:"name"` + TabIds []string `json:"tabids"` } -func (ws Workspace) GetId() string { - return ws.WorkspaceId -} - -func (ws *Workspace) WithLock(f func()) { - ws.Lock.Lock() - defer ws.Lock.Unlock() - f() +func (*Workspace) GetOType() string { + return "workspace" } type Tab struct { - Lock *sync.Mutex `json:"-"` - TabId string `json:"tabid"` - Name string `json:"name"` - BlockIds []string `json:"blockids"` + OID string `json:"oid"` + Version int `json:"version"` + Name string `json:"name"` + BlockIds []string `json:"blockids"` } -func (tab Tab) GetId() string { - return tab.TabId -} - -func (tab *Tab) WithLock(f func()) { - tab.Lock.Lock() - defer tab.Lock.Unlock() - f() +func (*Tab) GetOType() string { + return "tab" } type FileDef struct { @@ -108,7 +106,8 @@ type WinSize struct { } type Block struct { - BlockId string `json:"blockid"` + OID string `json:"oid"` + Version int `json:"version"` BlockDef *BlockDef `json:"blockdef"` Controller string `json:"controller"` View string `json:"view"` @@ -116,45 +115,32 @@ type Block struct { RuntimeOpts *RuntimeOpts `json:"runtimeopts,omitempty"` } -func (b *Block) GetOType() string { +func (*Block) GetOType() string { return "block" } -func (b Block) GetId() string { - return b.BlockId -} - -// TODO remove -func (b *Block) WithLock(f func()) { - f() -} - func CreateTab(workspaceId string, name string) (*Tab, error) { tab := &Tab{ - Lock: &sync.Mutex{}, - TabId: uuid.New().String(), + OID: uuid.New().String(), Name: name, BlockIds: []string{}, } - TabMap.Set(tab.TabId, tab) + TabMap.Set(tab.OID, tab) ws := WorkspaceMap.Get(workspaceId) if ws == nil { return nil, fmt.Errorf("workspace not found: %q", workspaceId) } - ws.WithLock(func() { - ws.TabIds = append(ws.TabIds, tab.TabId) - }) + ws.TabIds = append(ws.TabIds, tab.OID) return tab, nil } func CreateWorkspace() (*Workspace, error) { ws := &Workspace{ - Lock: &sync.Mutex{}, - WorkspaceId: uuid.New().String(), - TabIds: []string{}, + OID: uuid.New().String(), + TabIds: []string{}, } - WorkspaceMap.Set(ws.WorkspaceId, ws) - _, err := CreateTab(ws.WorkspaceId, "Tab 1") + WorkspaceMap.Set(ws.OID, ws) + _, err := CreateTab(ws.OID, "Tab 1") if err != nil { return nil, err } @@ -164,7 +150,7 @@ func CreateWorkspace() (*Workspace, error) { func EnsureInitialData() error { ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second) defer cancelFn() - clientCount, err := DBGetCount[Client](ctx) + clientCount, err := DBGetCount[*Client](ctx) if err != nil { return fmt.Errorf("error getting client count: %w", err) } @@ -175,7 +161,7 @@ func EnsureInitialData() error { workspaceId := uuid.New().String() tabId := uuid.New().String() client := &Client{ - ClientId: uuid.New().String(), + OID: uuid.New().String(), MainWindowId: windowId, } err = DBInsert(ctx, client) @@ -183,7 +169,7 @@ func EnsureInitialData() error { return fmt.Errorf("error inserting client: %w", err) } window := &Window{ - WindowId: windowId, + OID: windowId, WorkspaceId: workspaceId, ActiveTabId: tabId, ActiveBlockMap: make(map[string]string), @@ -201,16 +187,16 @@ func EnsureInitialData() error { return fmt.Errorf("error inserting window: %w", err) } ws := &Workspace{ - WorkspaceId: workspaceId, - Name: "default", - TabIds: []string{tabId}, + OID: workspaceId, + Name: "default", + TabIds: []string{tabId}, } err = DBInsert(ctx, ws) if err != nil { return fmt.Errorf("error inserting workspace: %w", err) } tab := &Tab{ - TabId: uuid.New().String(), + OID: uuid.New().String(), Name: "Tab 1", BlockIds: []string{}, } diff --git a/pkg/wstore/wstore_dbops.go b/pkg/wstore/wstore_dbops.go index 003e319a..3ae15123 100644 --- a/pkg/wstore/wstore_dbops.go +++ b/pkg/wstore/wstore_dbops.go @@ -6,155 +6,124 @@ package wstore import ( "context" "fmt" - "reflect" + + "github.com/wavetermdev/thenextwave/pkg/waveobj" ) -const Table_Client = "db_client" -const Table_Workspace = "db_workspace" -const Table_Tab = "db_tab" -const Table_Block = "db_block" -const Table_Window = "db_window" - -// can replace with struct tags in the future -type ObjectWithId interface { - GetId() string +func waveObjTableName(w waveobj.WaveObj) string { + return "db_" + w.GetOType() } -// can replace these with struct tags in the future -var idColumnName = map[string]string{ - Table_Client: "clientid", - Table_Workspace: "workspaceid", - Table_Tab: "tabid", - Table_Block: "blockid", - Table_Window: "windowid", +func tableNameGen[T waveobj.WaveObj]() string { + var zeroObj T + return "db_" + zeroObj.GetOType() } -var tableToType = map[string]reflect.Type{ - Table_Client: reflect.TypeOf(Client{}), - Table_Workspace: reflect.TypeOf(Workspace{}), - Table_Tab: reflect.TypeOf(Tab{}), - Table_Block: reflect.TypeOf(Block{}), - Table_Window: reflect.TypeOf(Window{}), -} - -var typeToTable map[reflect.Type]string - -func init() { - typeToTable = make(map[reflect.Type]string) - for k, v := range tableToType { - typeToTable[v] = k - } -} - -func DBGetCount[T ObjectWithId](ctx context.Context) (int, error) { +func DBGetCount[T waveobj.WaveObj](ctx context.Context) (int, error) { return WithTxRtn(ctx, func(tx *TxWrap) (int, error) { - var valInstance T - table := typeToTable[reflect.TypeOf(valInstance)] - if table == "" { - return 0, fmt.Errorf("unknown table type: %T", valInstance) - } + table := tableNameGen[T]() query := fmt.Sprintf("SELECT count(*) FROM %s", table) return tx.GetInt(query), nil }) } -func DBGetSingleton[T ObjectWithId](ctx context.Context) (*T, error) { - return WithTxRtn(ctx, func(tx *TxWrap) (*T, error) { - var rtn T - query := fmt.Sprintf("SELECT data FROM %s LIMIT 1", typeToTable[reflect.TypeOf(rtn)]) - jsonData := tx.GetString(query) - return TxReadJson[T](tx, jsonData), nil - }) -} - -func DBGet[T ObjectWithId](ctx context.Context, id string) (*T, error) { - return WithTxRtn(ctx, func(tx *TxWrap) (*T, error) { - var rtn T - table := typeToTable[reflect.TypeOf(rtn)] - if table == "" { - return nil, fmt.Errorf("unknown table type: %T", rtn) - } - query := fmt.Sprintf("SELECT data FROM %s WHERE %s = ?", table, idColumnName[table]) - jsonData := tx.GetString(query, id) - return TxReadJson[T](tx, jsonData), nil - }) -} - type idDataType struct { - Id string - Data string + OId string + Version int + Data []byte } -func DBSelectMap[T ObjectWithId](ctx context.Context, ids []string) (map[string]*T, error) { - return WithTxRtn(ctx, func(tx *TxWrap) (map[string]*T, error) { - var valInstance T - table := typeToTable[reflect.TypeOf(valInstance)] - if table == "" { - return nil, fmt.Errorf("unknown table type: %T", &valInstance) +func DBGetSingleton[T waveobj.WaveObj](ctx context.Context) (T, error) { + return WithTxRtn(ctx, func(tx *TxWrap) (T, error) { + table := tableNameGen[T]() + query := fmt.Sprintf("SELECT oid, version, data FROM %s LIMIT 1", table) + var row idDataType + tx.Get(&row, query) + rtn, err := waveobj.FromJsonGen[T](row.Data) + if err != nil { + return rtn, err } + waveobj.SetVersion(rtn, row.Version) + return rtn, nil + }) +} + +func DBGet[T waveobj.WaveObj](ctx context.Context, id string) (T, error) { + return WithTxRtn(ctx, func(tx *TxWrap) (T, error) { + table := tableNameGen[T]() + query := fmt.Sprintf("SELECT oid, version, data FROM %s WHERE oid = ?", table) + var row idDataType + tx.Get(&row, query, id) + rtn, err := waveobj.FromJsonGen[T](row.Data) + if err != nil { + return rtn, err + } + waveobj.SetVersion(rtn, row.Version) + return rtn, nil + }) +} + +func DBSelectMap[T waveobj.WaveObj](ctx context.Context, ids []string) (map[string]T, error) { + return WithTxRtn(ctx, func(tx *TxWrap) (map[string]T, error) { + table := tableNameGen[T]() var rows []idDataType - query := fmt.Sprintf("SELECT %s, data FROM %s WHERE %s IN (SELECT value FROM json_each(?))", idColumnName[table], table, idColumnName[table]) + query := fmt.Sprintf("SELECT oid, version, data FROM %s WHERE oid IN (SELECT value FROM json_each(?))", table) tx.Select(&rows, query, ids) - rtnMap := make(map[string]*T) + rtnMap := make(map[string]T) for _, row := range rows { - if row.Id == "" || row.Data == "" { + if row.OId == "" || len(row.Data) == 0 { continue } - r := TxReadJson[T](tx, row.Data) - if r == nil { - continue + waveObj, err := waveobj.FromJsonGen[T](row.Data) + if err != nil { + return nil, err } - rtnMap[(*r).GetId()] = r + waveobj.SetVersion(waveObj, row.Version) + rtnMap[row.OId] = waveObj } return rtnMap, nil }) } -func DBDelete[T ObjectWithId](ctx context.Context, id string) error { +func DBDelete[T waveobj.WaveObj](ctx context.Context, id string) error { return WithTx(ctx, func(tx *TxWrap) error { - var rtn T - table := typeToTable[reflect.TypeOf(rtn)] - if table == "" { - return fmt.Errorf("unknown table type: %T", rtn) - } - query := fmt.Sprintf("DELETE FROM %s WHERE %s = ?", table, idColumnName[table]) + table := tableNameGen[T]() + query := fmt.Sprintf("DELETE FROM %s WHERE oid = ?", table) tx.Exec(query, id) return nil }) } -func DBUpdate[T ObjectWithId](ctx context.Context, val *T) error { - if val == nil { - return fmt.Errorf("cannot update nil value") - } - if (*val).GetId() == "" { +func DBUpdate(ctx context.Context, val waveobj.WaveObj) error { + oid := waveobj.GetOID(val) + if oid == "" { return fmt.Errorf("cannot update %T value with empty id", val) } + jsonData, err := waveobj.ToJson(val) + if err != nil { + return err + } return WithTx(ctx, func(tx *TxWrap) error { - table := typeToTable[reflect.TypeOf(*val)] - if table == "" { - return fmt.Errorf("unknown table type: %T", *val) - } - query := fmt.Sprintf("UPDATE %s SET data = ? WHERE %s = ?", table, idColumnName[table]) - tx.Exec(query, TxJson(tx, val), (*val).GetId()) + table := waveObjTableName(val) + query := fmt.Sprintf("UPDATE %s SET data = ?, version = version+1 WHERE oid = ?", table) + tx.Exec(query, jsonData, oid) return nil }) } -func DBInsert[T ObjectWithId](ctx context.Context, val *T) error { - if val == nil { - return fmt.Errorf("cannot insert nil value") - } - if (*val).GetId() == "" { +func DBInsert[T waveobj.WaveObj](ctx context.Context, val T) error { + oid := waveobj.GetOID(val) + if oid == "" { return fmt.Errorf("cannot insert %T value with empty id", val) } + jsonData, err := waveobj.ToJson(val) + if err != nil { + return err + } return WithTx(ctx, func(tx *TxWrap) error { - table := typeToTable[reflect.TypeOf(*val)] - if table == "" { - return fmt.Errorf("unknown table type: %T", *val) - } - query := fmt.Sprintf("INSERT INTO %s (%s, data) VALUES (?, ?)", table, idColumnName[table]) - tx.Exec(query, (*val).GetId(), TxJson(tx, val)) + table := waveObjTableName(val) + query := fmt.Sprintf("INSERT INTO %s (oid, version, data) VALUES (?, ?, ?)", table) + tx.Exec(query, oid, 1, jsonData) return nil }) } diff --git a/pkg/wstore/wstore_dbsetup.go b/pkg/wstore/wstore_dbsetup.go index 40ae8530..d1141490 100644 --- a/pkg/wstore/wstore_dbsetup.go +++ b/pkg/wstore/wstore_dbsetup.go @@ -5,7 +5,6 @@ package wstore import ( "context" - "encoding/json" "fmt" "log" "path" @@ -63,24 +62,3 @@ func WithTx(ctx context.Context, fn func(tx *TxWrap) error) error { func WithTxRtn[RT any](ctx context.Context, fn func(tx *TxWrap) (RT, error)) (RT, error) { return txwrap.WithTxRtn(ctx, globalDB, fn) } - -func TxJson(tx *TxWrap, v any) string { - barr, err := json.Marshal(v) - if err != nil { - tx.SetErr(fmt.Errorf("json marshal (%T): %w", v, err)) - return "" - } - return string(barr) -} - -func TxReadJson[T any](tx *TxWrap, jsonData string) *T { - if jsonData == "" { - return nil - } - var v T - err := json.Unmarshal([]byte(jsonData), &v) - if err != nil { - tx.SetErr(fmt.Errorf("json unmarshal (%T): %w", v, err)) - } - return &v -} From 95ce1cc86da55f07d46e789888df2bde425b74e3 Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 26 May 2024 23:05:11 -0700 Subject: [PATCH 06/11] checkpoint on new objectservice --- cmd/generate/main-generate.go | 26 ++++++ cmd/{ => wsh}/main-wsh.go | 0 frontend/app/store/global.ts | 96 +++++++++++++++++++- frontend/types/custom.d.ts | 93 ++++++++++++++++++- main.go | 2 + pkg/service/objectservice/objectservice.go | 55 ++++++++++++ pkg/waveobj/waveobj.go | 39 ++++---- pkg/wstore/wstore.go | 23 +++-- pkg/wstore/wstore_dbops.go | 100 ++++++++++++++++----- 9 files changed, 387 insertions(+), 47 deletions(-) create mode 100644 cmd/generate/main-generate.go rename cmd/{ => wsh}/main-wsh.go (100%) create mode 100644 pkg/service/objectservice/objectservice.go diff --git a/cmd/generate/main-generate.go b/cmd/generate/main-generate.go new file mode 100644 index 00000000..54b4bd6f --- /dev/null +++ b/cmd/generate/main-generate.go @@ -0,0 +1,26 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "fmt" + "reflect" + + "github.com/wavetermdev/thenextwave/pkg/waveobj" + "github.com/wavetermdev/thenextwave/pkg/wstore" +) + +func main() { + tsTypesMap := make(map[reflect.Type]string) + var waveObj waveobj.WaveObj + waveobj.GenerateTSType(reflect.TypeOf(waveobj.ORef{}), tsTypesMap) + waveobj.GenerateTSType(reflect.TypeOf(&waveObj).Elem(), tsTypesMap) + for _, rtype := range wstore.AllWaveObjTypes() { + waveobj.GenerateTSType(rtype, tsTypesMap) + } + for _, ts := range tsTypesMap { + fmt.Print(ts) + fmt.Print("\n") + } +} diff --git a/cmd/main-wsh.go b/cmd/wsh/main-wsh.go similarity index 100% rename from cmd/main-wsh.go rename to cmd/wsh/main-wsh.go diff --git a/frontend/app/store/global.ts b/frontend/app/store/global.ts index f615f4b9..3089e757 100644 --- a/frontend/app/store/global.ts +++ b/frontend/app/store/global.ts @@ -1,15 +1,18 @@ // Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 +import * as React from "react"; import * as jotai from "jotai"; -import { atomFamily } from "jotai/utils"; +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"; const globalStore = jotai.createStore(); @@ -105,4 +108,93 @@ function removeBlockFromTab(tabId: string, blockId: string) { BlockService.CloseBlock(blockId); } -export { globalStore, atoms, getBlockSubject, addBlockIdToTab, blockDataMap, useBlockAtom, removeBlockFromTab }; +function GetObject(oref: string): Promise { + let prtn = $Call.ByName( + "github.com/wavetermdev/thenextwave/pkg/service/objectservice.ObjectService.GetObject", + oref + ); + return prtn; +} + +type WaveObjectHookData = { + oref: string; +}; + +type WaveObjectValue = { + pendingPromise: Promise; + 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 useWaveObjectValue(oref: string): [T, boolean] { + const objRef = React.useRef(null); + if (objRef.current == null) { + objRef.current = { oref: oref }; + } + 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]]; +} + +function useWaveObject(oref: string): [T, boolean, (T) => void] { + const objRef = React.useRef(null); + if (objRef.current == null) { + objRef.current = { oref: oref }; + } + 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]; +} + +export { + globalStore, + atoms, + getBlockSubject, + addBlockIdToTab, + blockDataMap, + useBlockAtom, + removeBlockFromTab, + GetObject, + useWaveObject, + useWaveObjectValue, + clearWaveObjectCache, +}; diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index d12626e2..c7111e1c 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -1,6 +1,97 @@ // Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -declare global {} +declare global { + type ORef = { + otype: string; + oid: string; + }; + + type Block = { + otype: string; + oid: string; + version: number; + blockdef: BlockDef; + controller: string; + view: string; + meta?: { [key: string]: any }; + runtimeopts?: RuntimeOpts; + }; + + type BlockDef = { + controller: string; + view?: string; + files?: { [key: string]: FileDef }; + meta?: { [key: string]: any }; + }; + + type FileDef = { + filetype?: string; + path?: string; + url?: string; + content?: string; + meta?: { [key: string]: any }; + }; + + type TermSize = { + rows: number; + cols: number; + }; + + type Client = { + otype: string; + oid: string; + version: number; + mainwindowid: string; + }; + + type Tab = { + otype: string; + oid: string; + version: number; + name: string; + blockids: string[]; + }; + + type Point = { + x: number; + y: number; + }; + + type WinSize = { + width: number; + height: number; + }; + + type Workspace = { + otype: string; + oid: string; + version: number; + name: string; + tabids: string[]; + }; + + type RuntimeOpts = { + termsize?: TermSize; + winsize?: WinSize; + }; + + type WaveObj = { + otype: string; + oid: string; + }; + + type Window = { + otype: string; + oid: string; + version: number; + workspaceid: string; + activetabid: string; + activeblockmap: { [key: string]: string }; + pos: Point; + winsize: WinSize; + lastfocusts: number; + }; +} export {}; diff --git a/main.go b/main.go index 9c253235..7b976f80 100644 --- a/main.go +++ b/main.go @@ -19,6 +19,7 @@ import ( "github.com/wavetermdev/thenextwave/pkg/service/blockservice" "github.com/wavetermdev/thenextwave/pkg/service/clientservice" "github.com/wavetermdev/thenextwave/pkg/service/fileservice" + "github.com/wavetermdev/thenextwave/pkg/service/objectservice" "github.com/wavetermdev/thenextwave/pkg/wavebase" "github.com/wavetermdev/thenextwave/pkg/wstore" @@ -131,6 +132,7 @@ func main() { application.NewService(&fileservice.FileService{}), application.NewService(&blockservice.BlockService{}), application.NewService(&clientservice.ClientService{}), + application.NewService(&objectservice.ObjectService{}), }, Icon: appIcon, Assets: application.AssetOptions{ diff --git a/pkg/service/objectservice/objectservice.go b/pkg/service/objectservice/objectservice.go new file mode 100644 index 00000000..96995599 --- /dev/null +++ b/pkg/service/objectservice/objectservice.go @@ -0,0 +1,55 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package objectservice + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/wavetermdev/thenextwave/pkg/waveobj" + "github.com/wavetermdev/thenextwave/pkg/wstore" +) + +type ObjectService struct{} + +const DefaultTimeout = 2 * time.Second + +func parseORef(oref string) (*waveobj.ORef, error) { + fields := strings.Split(oref, ":") + if len(fields) != 2 { + return nil, fmt.Errorf("invalid object reference: %q", oref) + } + return &waveobj.ORef{OType: fields[0], OID: fields[1]}, nil +} + +func (svc *ObjectService) GetObject(orefStr string) (any, error) { + oref, err := parseORef(orefStr) + if err != nil { + return nil, err + } + ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) + defer cancelFn() + obj, err := wstore.DBGetORef(ctx, *oref) + if err != nil { + return nil, fmt.Errorf("error getting object: %w", err) + } + return obj, nil +} + +func (svc *ObjectService) GetObjects(orefStrArr []string) (any, error) { + ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) + defer cancelFn() + + var orefArr []waveobj.ORef + for _, orefStr := range orefStrArr { + orefObj, err := parseORef(orefStr) + if err != nil { + return nil, err + } + orefArr = append(orefArr, *orefObj) + } + return wstore.DBSelectORefs(ctx, orefArr) +} diff --git a/pkg/waveobj/waveobj.go b/pkg/waveobj/waveobj.go index fb773fdf..0b1a5421 100644 --- a/pkg/waveobj/waveobj.go +++ b/pkg/waveobj/waveobj.go @@ -23,6 +23,11 @@ const ( VersionGoFieldName = "Version" ) +type ORef struct { + OType string `json:"otype"` + OID string `json:"oid"` +} + type WaveObj interface { GetOType() string // should not depend on object state (should work with nil value) } @@ -36,35 +41,37 @@ type waveObjDesc struct { var waveObjMap = sync.Map{} var waveObjRType = reflect.TypeOf((*WaveObj)(nil)).Elem() -func RegisterType[T WaveObj]() { - var waveObj T +func RegisterType(rtype reflect.Type) { + if rtype.Kind() != reflect.Ptr { + panic(fmt.Sprintf("wave object must be a pointer for %v", rtype)) + } + if !rtype.Implements(waveObjRType) { + panic(fmt.Sprintf("wave object must implement WaveObj for %v", rtype)) + } + waveObj := reflect.Zero(rtype).Interface().(WaveObj) otype := waveObj.GetOType() if otype == "" { - panic(fmt.Sprintf("otype is empty for %T", waveObj)) - } - rtype := reflect.TypeOf(waveObj) - if rtype.Kind() != reflect.Ptr { - panic(fmt.Sprintf("wave object must be a pointer for %T", waveObj)) + panic(fmt.Sprintf("otype is empty for %v", rtype)) } oidField, found := rtype.Elem().FieldByName(OIDGoFieldName) if !found { - panic(fmt.Sprintf("missing OID field for %T", waveObj)) + panic(fmt.Sprintf("missing OID field for %v", rtype)) } if oidField.Type.Kind() != reflect.String { - panic(fmt.Sprintf("OID field must be string for %T", waveObj)) + panic(fmt.Sprintf("OID field must be string for %v", rtype)) } if oidField.Tag.Get("json") != OIDKeyName { - panic(fmt.Sprintf("OID field json tag must be %q for %T", OIDKeyName, waveObj)) + panic(fmt.Sprintf("OID field json tag must be %q for %v", OIDKeyName, rtype)) } versionField, found := rtype.Elem().FieldByName(VersionGoFieldName) if !found { - panic(fmt.Sprintf("missing Version field for %T", waveObj)) + panic(fmt.Sprintf("missing Version field for %v", rtype)) } if versionField.Type.Kind() != reflect.Int { - panic(fmt.Sprintf("Version field must be int for %T", waveObj)) + panic(fmt.Sprintf("Version field must be int for %v", rtype)) } if versionField.Tag.Get("json") != VersionKeyName { - panic(fmt.Sprintf("Version field json tag must be %q for %T", VersionKeyName, waveObj)) + panic(fmt.Sprintf("Version field json tag must be %q for %v", VersionKeyName, rtype)) } _, found = waveObjMap.Load(otype) if found { @@ -286,16 +293,16 @@ func generateTSTypeInternal(rtype reflect.Type) (string, []reflect.Type) { subTypes = append(subTypes, fieldSubTypes...) buf.WriteString(fmt.Sprintf(" %s%s: %s;\n", fieldName, optMarker, tsType)) } - buf.WriteString("}\n") + buf.WriteString("};\n") return buf.String(), subTypes } func GenerateWaveObjTSType() string { var buf bytes.Buffer - buf.WriteString("type WaveObj {\n") + buf.WriteString("type WaveObj = {\n") buf.WriteString(" otype: string;\n") buf.WriteString(" oid: string;\n") - buf.WriteString("}\n") + buf.WriteString("};\n") return buf.String() } diff --git a/pkg/wstore/wstore.go b/pkg/wstore/wstore.go index d71e9267..3e54986d 100644 --- a/pkg/wstore/wstore.go +++ b/pkg/wstore/wstore.go @@ -6,6 +6,7 @@ package wstore import ( "context" "fmt" + "reflect" "time" "github.com/google/uuid" @@ -19,11 +20,9 @@ var TabMap = ds.NewSyncMap[*Tab]() var BlockMap = ds.NewSyncMap[*Block]() func init() { - waveobj.RegisterType[*Client]() - waveobj.RegisterType[*Window]() - waveobj.RegisterType[*Workspace]() - waveobj.RegisterType[*Tab]() - waveobj.RegisterType[*Block]() + for _, rtype := range AllWaveObjTypes() { + waveobj.RegisterType(rtype) + } } type Client struct { @@ -36,6 +35,16 @@ func (*Client) GetOType() string { return "client" } +func AllWaveObjTypes() []reflect.Type { + return []reflect.Type{ + reflect.TypeOf(&Client{}), + reflect.TypeOf(&Window{}), + reflect.TypeOf(&Workspace{}), + reflect.TypeOf(&Tab{}), + reflect.TypeOf(&Block{}), + } +} + // stores the ui-context of the window // workspaceid, active tab, active block within each tab, window size, etc. type Window struct { @@ -147,6 +156,10 @@ func CreateWorkspace() (*Workspace, error) { return ws, nil } +func GetObject(otype string, oid string) (waveobj.WaveObj, error) { + return nil, nil +} + func EnsureInitialData() error { ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second) defer cancelFn() diff --git a/pkg/wstore/wstore_dbops.go b/pkg/wstore/wstore_dbops.go index 3ae15123..393ac628 100644 --- a/pkg/wstore/wstore_dbops.go +++ b/pkg/wstore/wstore_dbops.go @@ -14,9 +14,18 @@ func waveObjTableName(w waveobj.WaveObj) string { return "db_" + w.GetOType() } +func tableNameFromOType(otype string) string { + return "db_" + otype +} + func tableNameGen[T waveobj.WaveObj]() string { var zeroObj T - return "db_" + zeroObj.GetOType() + return tableNameFromOType(zeroObj.GetOType()) +} + +func getOTypeGen[T waveobj.WaveObj]() string { + var zeroObj T + return zeroObj.GetOType() } func DBGetCount[T waveobj.WaveObj](ctx context.Context) (int, error) { @@ -33,13 +42,26 @@ type idDataType struct { Data []byte } +func genericCastWithErr[T any](v any, err error) (T, error) { + if err != nil { + var zeroVal T + return zeroVal, err + } + return v.(T), err +} + func DBGetSingleton[T waveobj.WaveObj](ctx context.Context) (T, error) { - return WithTxRtn(ctx, func(tx *TxWrap) (T, error) { - table := tableNameGen[T]() + rtn, err := DBGetSingletonByType(ctx, getOTypeGen[T]()) + return genericCastWithErr[T](rtn, err) +} + +func DBGetSingletonByType(ctx context.Context, otype string) (waveobj.WaveObj, error) { + return WithTxRtn(ctx, func(tx *TxWrap) (waveobj.WaveObj, error) { + table := tableNameFromOType(otype) query := fmt.Sprintf("SELECT oid, version, data FROM %s LIMIT 1", table) var row idDataType tx.Get(&row, query) - rtn, err := waveobj.FromJsonGen[T](row.Data) + rtn, err := waveobj.FromJson(row.Data) if err != nil { return rtn, err } @@ -49,12 +71,17 @@ func DBGetSingleton[T waveobj.WaveObj](ctx context.Context) (T, error) { } func DBGet[T waveobj.WaveObj](ctx context.Context, id string) (T, error) { - return WithTxRtn(ctx, func(tx *TxWrap) (T, error) { - table := tableNameGen[T]() + rtn, err := DBGetORef(ctx, waveobj.ORef{OType: getOTypeGen[T](), OID: id}) + return genericCastWithErr[T](rtn, err) +} + +func DBGetORef(ctx context.Context, oref waveobj.ORef) (waveobj.WaveObj, error) { + return WithTxRtn(ctx, func(tx *TxWrap) (waveobj.WaveObj, error) { + table := tableNameFromOType(oref.OType) query := fmt.Sprintf("SELECT oid, version, data FROM %s WHERE oid = ?", table) var row idDataType - tx.Get(&row, query, id) - rtn, err := waveobj.FromJsonGen[T](row.Data) + tx.Get(&row, query, oref.OID) + rtn, err := waveobj.FromJson(row.Data) if err != nil { return rtn, err } @@ -63,31 +90,58 @@ func DBGet[T waveobj.WaveObj](ctx context.Context, id string) (T, error) { }) } -func DBSelectMap[T waveobj.WaveObj](ctx context.Context, ids []string) (map[string]T, error) { - return WithTxRtn(ctx, func(tx *TxWrap) (map[string]T, error) { - table := tableNameGen[T]() - var rows []idDataType +func dbSelectOIDs(ctx context.Context, otype string, oids []string) ([]waveobj.WaveObj, error) { + return WithTxRtn(ctx, func(tx *TxWrap) ([]waveobj.WaveObj, error) { + table := tableNameFromOType(otype) query := fmt.Sprintf("SELECT oid, version, data FROM %s WHERE oid IN (SELECT value FROM json_each(?))", table) - tx.Select(&rows, query, ids) - rtnMap := make(map[string]T) + var rows []idDataType + tx.Select(&rows, query, oids) + rtn := make([]waveobj.WaveObj, 0, len(rows)) for _, row := range rows { - if row.OId == "" || len(row.Data) == 0 { - continue - } - waveObj, err := waveobj.FromJsonGen[T](row.Data) + waveObj, err := waveobj.FromJson(row.Data) if err != nil { return nil, err } waveobj.SetVersion(waveObj, row.Version) - rtnMap[row.OId] = waveObj + rtn = append(rtn, waveObj) } - return rtnMap, nil + return rtn, nil }) } -func DBDelete[T waveobj.WaveObj](ctx context.Context, id string) error { +func DBSelectORefs(ctx context.Context, orefs []waveobj.ORef) ([]waveobj.WaveObj, error) { + oidsByType := make(map[string][]string) + for _, oref := range orefs { + oidsByType[oref.OType] = append(oidsByType[oref.OType], oref.OID) + } + return WithTxRtn(ctx, func(tx *TxWrap) ([]waveobj.WaveObj, error) { + rtn := make([]waveobj.WaveObj, 0, len(orefs)) + for otype, oids := range oidsByType { + rtnArr, err := dbSelectOIDs(tx.Context(), otype, oids) + if err != nil { + return nil, err + } + rtn = append(rtn, rtnArr...) + } + return rtn, nil + }) +} + +func DBSelectMap[T waveobj.WaveObj](ctx context.Context, ids []string) (map[string]T, error) { + rtnArr, err := dbSelectOIDs(ctx, getOTypeGen[T](), ids) + if err != nil { + return nil, err + } + rtnMap := make(map[string]T) + for _, obj := range rtnArr { + rtnMap[waveobj.GetOID(obj)] = obj.(T) + } + return rtnMap, nil +} + +func DBDelete(ctx context.Context, otype string, id string) error { return WithTx(ctx, func(tx *TxWrap) error { - table := tableNameGen[T]() + table := tableNameFromOType(otype) query := fmt.Sprintf("DELETE FROM %s WHERE oid = ?", table) tx.Exec(query, id) return nil @@ -111,7 +165,7 @@ func DBUpdate(ctx context.Context, val waveobj.WaveObj) error { }) } -func DBInsert[T waveobj.WaveObj](ctx context.Context, val T) error { +func DBInsert(ctx context.Context, val waveobj.WaveObj) error { oid := waveobj.GetOID(val) if oid == "" { return fmt.Errorf("cannot insert %T value with empty id", val) From 6d3f76cb74297cf10d7c8969f49339169560ad26 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 27 May 2024 00:47:10 -0700 Subject: [PATCH 07/11] fe now rendering workspace/tab from db. got useWaveObject working. need to work on updates --- frontend/app/block/block.tsx | 4 +- frontend/app/store/global.ts | 154 ++++++++++----------- frontend/app/tab/tab.tsx | 8 +- frontend/app/workspace/workspace.tsx | 60 ++++---- frontend/types/custom.d.ts | 2 +- frontend/wave.ts | 6 +- pkg/service/objectservice/objectservice.go | 12 +- pkg/waveobj/waveobj.go | 22 ++- pkg/wstore/wstore.go | 4 +- 9 files changed, 145 insertions(+), 127 deletions(-) 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) From b87786febf0833d07b9b771a664140f206ffb9a4 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 27 May 2024 13:59:58 -0700 Subject: [PATCH 08/11] checkpoint -- generic updates, wave object store, new setup for initialization, atoms, etc. lots of progress --- frontend/app/app.tsx | 9 +- frontend/app/store/global.ts | 164 ++++---------- frontend/app/store/wos.ts | 251 +++++++++++++++++++++ frontend/app/tab/tab.tsx | 4 +- frontend/app/workspace/workspace.tsx | 31 +-- frontend/types/custom.d.ts | 6 + frontend/wave.ts | 14 +- main.go | 7 +- pkg/blockcontroller/blockcontroller.go | 3 +- pkg/service/objectservice/objectservice.go | 70 +++++- pkg/waveobj/waveobj.go | 55 +++++ pkg/wstore/wstore.go | 177 +++++++++++++-- pkg/wstore/wstore_dbops.go | 30 ++- pkg/wstore/wstore_dbsetup.go | 20 +- 14 files changed, 646 insertions(+), 195 deletions(-) create mode 100644 frontend/app/store/wos.ts diff --git a/frontend/app/app.tsx b/frontend/app/app.tsx index eb8d5a8a..67681faa 100644 --- a/frontend/app/app.tsx +++ b/frontend/app/app.tsx @@ -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 (
-
invalid configuration, client or window was not loaded
+
+ invalid configuration, client or window was not loaded
); } - return (
diff --git a/frontend/app/store/global.ts b/frontend/app/store/global.ts index eed83efe..cf2dc10d 100644 --- a/frontend/app/store/global.ts +++ b/frontend/app/store/global.ts @@ -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>(); +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; +const clientIdAtom = jotai.atom(null) as jotai.PrimitiveAtom; +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; +const clientAtom: jotai.Atom = jotai.atom((get) => { + const clientId = get(clientIdAtom); + if (clientId == null) { + return null; + } + return WOS.getStaticObjectValue(WOS.makeORef("client", clientId), get); +}); +const windowDataAtom: jotai.Atom = jotai.atom((get) => { + const windowId = get(windowIdAtom); + if (windowId == null) { + return null; + } + return WOS.getStaticObjectValue(WOS.makeORef("window", windowId), get); +}); +const workspaceAtom: jotai.Atom = 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, - // 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, + windowId: windowIdAtom, + clientId: clientIdAtom, + uiContext: uiContextAtom, + client: clientAtom, + waveWindow: windowDataAtom, + workspace: workspaceAtom, + blockDataMap: blockDataMap, }; 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) { @@ -95,94 +107,4 @@ function useBlockAtom(blockId: string, name: string, makeFn: () => jotai.Atom return atom as jotai.Atom; } -function GetObject(oref: string): Promise { - let prtn = $Call.ByName( - "github.com/wavetermdev/thenextwave/pkg/service/objectservice.ObjectService.GetObject", - oref - ); - return prtn; -} - -function GetClientObject(): Promise { - let prtn = $Call.ByName( - "github.com/wavetermdev/thenextwave/pkg/service/objectservice.ObjectService.GetClientObject" - ); - return prtn; -} - -type WaveObjectValue = { - pendingPromise: Promise; - dataAtom: jotai.PrimitiveAtom<{ value: T; loading: boolean }>; -}; - -const waveObjectValueCache = new Map>(); - -function clearWaveObjectCache() { - waveObjectValueCache.clear(); -} - -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] { - 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(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 }; diff --git a/frontend/app/store/wos.ts b/frontend/app/store/wos.ts new file mode 100644 index 00000000..bd845f60 --- /dev/null +++ b/frontend/app/store/wos.ts @@ -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 = { + value: T; + loading: boolean; +}; + +type WaveObjectValue = { + pendingPromise: Promise; + dataAtom: jotai.PrimitiveAtom>; + 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(oref: string): Promise { + let prtn = $Call.ByName( + "github.com/wavetermdev/thenextwave/pkg/service/objectservice.ObjectService.GetObject", + oref + ); + return prtn; +} + +const waveObjectValueCache = new Map>(); + +function clearWaveObjectCache() { + waveObjectValueCache.clear(); +} + +const defaultHoldTime = 5000; // 5-seconds + +function createWaveValueObject(oref: string, shouldFetch: boolean): WaveObjectValue { + 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(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(oref: string): Promise { + 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(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(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 = 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(fnName: string, ...args: any[]): Promise { + 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(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, +}; diff --git a/frontend/app/tab/tab.tsx b/frontend/app/tab/tab.tsx index 9799be16..97aa653f 100644 --- a/frontend/app/tab/tab.tsx +++ b/frontend/app/tab/tab.tsx @@ -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(gdata.makeORef("tab", tabId)); + const [tabData, tabLoading] = WOS.useWaveObjectValue(WOS.makeORef("tab", tabId)); if (tabLoading) { return ; } diff --git a/frontend/app/workspace/workspace.tsx b/frontend/app/workspace/workspace.tsx index 69f76909..b00428c9 100644 --- a/frontend/app/workspace/workspace.tsx +++ b/frontend/app/workspace/workspace.tsx @@ -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(gdata.makeORef("tab", tabId)); +import "./workspace.less"; +function Tab({ tabId }: { tabId: string }) { + const windowData = jotai.useAtomValue(atoms.waveWindow); + const [tabData, tabLoading] = WOS.useWaveObjectValue(WOS.makeORef("tab", tabId)); function setActiveTab(tabId: string) { if (tabId == null) { return; } // TODO } - return (
@@ -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(gdata.makeORef("workspace", workspaceId)); - if (wsLoading) { - return ; - } + const ws = jotai.useAtomValue(atoms.workspace); return (
- +
diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index aae4f6e2..da6b0c94 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -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 = { diff --git a/frontend/wave.ts b/frontend/wave.ts index c06689bc..c9d116ac 100644 --- a/frontend/wave.ts +++ b/frontend/wave.ts @@ -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(makeORef("window", windowId)); - globalStore.set(atoms.windowData, window); + // ensures client/window are loaded into the cache before rendering + await WOS.loadAndPinWaveObject(WOS.makeORef("client", clientId)); + const waveWindow = await WOS.loadAndPinWaveObject(WOS.makeORef("window", windowId)); + await WOS.loadAndPinWaveObject(WOS.makeORef("workspace", waveWindow.workspaceid)); let reactElem = React.createElement(App, null, null); let elem = document.getElementById("main"); let root = createRoot(elem); diff --git a/main.go b/main.go index 7b976f80..007106e0 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,7 @@ package main import ( "context" "embed" + "fmt" "log" "net/http" "runtime" @@ -53,6 +54,10 @@ func createAppMenu(app *application.App) *application.Menu { } func createWindow(windowData *wstore.Window, app *application.App) { + client, err := wstore.DBGetSingleton[*wstore.Client](context.Background()) + if err != nil { + panic(fmt.Errorf("error getting client data: %w", err)) + } window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ Title: "Wave Terminal", Mac: application.MacWindow{ @@ -61,7 +66,7 @@ func createWindow(windowData *wstore.Window, app *application.App) { TitleBar: application.MacTitleBarHiddenInset, }, BackgroundColour: application.NewRGB(0, 0, 0), - URL: "/public/index.html?windowid=" + windowData.OID, + URL: "/public/index.html?windowid=" + windowData.OID + "&clientid=" + client.OID, X: windowData.Pos.X, Y: windowData.Pos.Y, Width: windowData.WinSize.Width, diff --git a/pkg/blockcontroller/blockcontroller.go b/pkg/blockcontroller/blockcontroller.go index bb8ade23..70d03c7a 100644 --- a/pkg/blockcontroller/blockcontroller.go +++ b/pkg/blockcontroller/blockcontroller.go @@ -62,6 +62,7 @@ func jsonDeepCopy(val map[string]any) (map[string]any, error) { } func CreateBlock(ctx context.Context, bdef *wstore.BlockDef, rtOpts *wstore.RuntimeOpts) (*wstore.Block, error) { + // TODO blockId := uuid.New().String() blockData := &wstore.Block{ OID: blockId, @@ -86,13 +87,13 @@ func CreateBlock(ctx context.Context, bdef *wstore.BlockDef, rtOpts *wstore.Runt } func CloseBlock(blockId string) { + // TODO bc := GetBlockController(blockId) if bc == nil { return } bc.Close() close(bc.InputCh) - wstore.BlockMap.Delete(blockId) } func (bc *BlockController) setShellProc(shellProc *shellexec.ShellProc) error { diff --git a/pkg/service/objectservice/objectservice.go b/pkg/service/objectservice/objectservice.go index 64476bec..e0eae455 100644 --- a/pkg/service/objectservice/objectservice.go +++ b/pkg/service/objectservice/objectservice.go @@ -25,16 +25,6 @@ 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 { @@ -46,7 +36,8 @@ func (svc *ObjectService) GetObject(orefStr string) (any, error) { if err != nil { return nil, fmt.Errorf("error getting object: %w", err) } - return waveobj.ToJsonMap(obj) + rtn, err := waveobj.ToJsonMap(obj) + return rtn, err } func (svc *ObjectService) GetObjects(orefStrArr []string) (any, error) { @@ -63,3 +54,60 @@ func (svc *ObjectService) GetObjects(orefStrArr []string) (any, error) { } return wstore.DBSelectORefs(ctx, orefArr) } + +func updatesRtn(ctx context.Context, rtnVal map[string]any) (any, error) { + updates := wstore.ContextGetUpdates(ctx) + if len(updates) == 0 { + return nil, nil + } + var rtn []any + for _, obj := range updates { + if obj == nil { + continue + } + jmap, err := waveobj.ToJsonMap(obj) + if err != nil { + return nil, fmt.Errorf("error converting object to JSON: %w", err) + } + rtn = append(rtn, jmap) + } + if rtnVal == nil { + rtnVal = make(map[string]any) + } + rtnVal["updates"] = rtn + return rtnVal, nil +} + +func (svc *ObjectService) AddTabToWorkspace(uiContext wstore.UIContext, tabName string, activateTab bool) (any, error) { + ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) + defer cancelFn() + ctx = wstore.ContextWithUpdates(ctx) + windowData, err := wstore.DBMustGet[*wstore.Window](ctx, uiContext.WindowId) + if err != nil { + return nil, fmt.Errorf("error getting window: %w", err) + } + tab, err := wstore.CreateTab(ctx, windowData.WorkspaceId, tabName) + if err != nil { + return nil, fmt.Errorf("error creating tab: %w", err) + } + if activateTab { + err = wstore.SetActiveTab(ctx, uiContext.WindowId, tab.OID) + if err != nil { + return nil, fmt.Errorf("error setting active tab: %w", err) + } + } + rtn := make(map[string]any) + rtn["tabid"] = waveobj.GetOID(tab) + return updatesRtn(ctx, rtn) +} + +func (svc *ObjectService) SetActiveTab(uiContext wstore.UIContext, tabId string) (any, error) { + ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) + defer cancelFn() + ctx = wstore.ContextWithUpdates(ctx) + err := wstore.SetActiveTab(ctx, uiContext.WindowId, tabId) + if err != nil { + return nil, fmt.Errorf("error setting active tab: %w", err) + } + return updatesRtn(ctx, nil) +} diff --git a/pkg/waveobj/waveobj.go b/pkg/waveobj/waveobj.go index 0b965a9e..38245773 100644 --- a/pkg/waveobj/waveobj.go +++ b/pkg/waveobj/waveobj.go @@ -18,6 +18,7 @@ const ( OTypeKeyName = "otype" OIDKeyName = "oid" VersionKeyName = "version" + DeletedKeyName = "deleted" OIDGoFieldName = "OID" VersionGoFieldName = "Version" @@ -32,6 +33,15 @@ type WaveObj interface { GetOType() string // should not depend on object state (should work with nil value) } +type WaveObjTombstone struct { + OType string `json:"otype"` + OID string `json:"oid"` +} + +func (w *WaveObjTombstone) GetOType() string { + return w.OType +} + type waveObjDesc struct { RType reflect.Type OIDField reflect.StructField @@ -93,6 +103,9 @@ func getWaveObjDesc(otype string) *waveObjDesc { } func GetOID(waveObj WaveObj) string { + if tomb, ok := waveObj.(*WaveObjTombstone); ok { + return tomb.OID + } desc := getWaveObjDesc(waveObj.GetOType()) if desc == nil { return "" @@ -101,6 +114,10 @@ func GetOID(waveObj WaveObj) string { } func SetOID(waveObj WaveObj, oid string) { + if tomb, ok := waveObj.(*WaveObjTombstone); ok { + tomb.OID = oid + return + } desc := getWaveObjDesc(waveObj.GetOType()) if desc == nil { return @@ -109,6 +126,9 @@ func SetOID(waveObj WaveObj, oid string) { } func GetVersion(waveObj WaveObj) int { + if _, ok := waveObj.(*WaveObjTombstone); ok { + return 0 + } desc := getWaveObjDesc(waveObj.GetOType()) if desc == nil { return 0 @@ -117,6 +137,9 @@ func GetVersion(waveObj WaveObj) int { } func SetVersion(waveObj WaveObj, version int) { + if _, ok := waveObj.(*WaveObjTombstone); ok { + return + } desc := getWaveObjDesc(waveObj.GetOType()) if desc == nil { return @@ -138,6 +161,10 @@ func ToJsonMap(w WaveObj) (map[string]any, error) { if err != nil { return nil, err } + if _, ok := w.(*WaveObjTombstone); ok { + m[DeletedKeyName] = true + return m, nil + } m[OTypeKeyName] = w.GetOType() m[OIDKeyName] = GetOID(w) m[VersionKeyName] = GetVersion(w) @@ -152,12 +179,39 @@ func ToJson(w WaveObj) ([]byte, error) { return json.Marshal(m) } +func getMapBoolVal(m map[string]any, key string) bool { + val, ok := m[key].(bool) + if !ok { + return false + } + return val +} + +func getMapStringVal(m map[string]any, key string) string { + val, ok := m[key].(string) + if !ok { + return "" + } + return val +} + +func IsTombstone(w WaveObj) bool { + _, ok := w.(*WaveObjTombstone) + return ok +} + func FromJson(data []byte) (WaveObj, error) { var m map[string]any err := json.Unmarshal(data, &m) if err != nil { return nil, err } + if getMapBoolVal(m, DeletedKeyName) { + return &WaveObjTombstone{ + OType: getMapStringVal(m, OTypeKeyName), + OID: getMapStringVal(m, OIDKeyName), + }, nil + } otype, ok := m[OTypeKeyName].(string) if !ok { return nil, fmt.Errorf("missing otype") @@ -320,6 +374,7 @@ func GenerateWaveObjTSType() string { buf.WriteString("type WaveObj = {\n") buf.WriteString(" otype: string;\n") buf.WriteString(" oid: string;\n") + buf.WriteString(" version: number;\n") buf.WriteString("};\n") return buf.String() } diff --git a/pkg/wstore/wstore.go b/pkg/wstore/wstore.go index 163316df..8a9d7ec5 100644 --- a/pkg/wstore/wstore.go +++ b/pkg/wstore/wstore.go @@ -4,20 +4,19 @@ package wstore import ( + "bytes" "context" "fmt" + "log" "reflect" "time" "github.com/google/uuid" "github.com/wavetermdev/thenextwave/pkg/shellexec" - "github.com/wavetermdev/thenextwave/pkg/util/ds" "github.com/wavetermdev/thenextwave/pkg/waveobj" ) -var WorkspaceMap = ds.NewSyncMap[*Workspace]() -var TabMap = ds.NewSyncMap[*Tab]() -var BlockMap = ds.NewSyncMap[*Block]() +var waveObjUpdateKey = struct{}{} func init() { for _, rtype := range AllWaveObjTypes() { @@ -25,6 +24,122 @@ func init() { } } +type contextUpdatesType struct { + UpdatesStack []map[waveobj.ORef]waveobj.WaveObj +} + +func dumpUpdateStack(updates *contextUpdatesType) { + log.Printf("dumpUpdateStack len:%d\n", len(updates.UpdatesStack)) + for idx, update := range updates.UpdatesStack { + var buf bytes.Buffer + buf.WriteString(fmt.Sprintf(" [%d]:", idx)) + for k := range update { + buf.WriteString(fmt.Sprintf(" %s:%s", k.OType, k.OID)) + } + buf.WriteString("\n") + log.Print(buf.String()) + } +} + +func ContextWithUpdates(ctx context.Context) context.Context { + updatesVal := ctx.Value(waveObjUpdateKey) + if updatesVal != nil { + return ctx + } + return context.WithValue(ctx, waveObjUpdateKey, &contextUpdatesType{ + UpdatesStack: []map[waveobj.ORef]waveobj.WaveObj{make(map[waveobj.ORef]waveobj.WaveObj)}, + }) +} + +func ContextGetUpdates(ctx context.Context) map[waveobj.ORef]waveobj.WaveObj { + updatesVal := ctx.Value(waveObjUpdateKey) + if updatesVal == nil { + return nil + } + updates := updatesVal.(*contextUpdatesType) + if len(updates.UpdatesStack) == 1 { + return updates.UpdatesStack[0] + } + rtn := make(map[waveobj.ORef]waveobj.WaveObj) + for _, update := range updates.UpdatesStack { + for k, v := range update { + rtn[k] = v + } + } + return rtn +} + +func ContextGetUpdate(ctx context.Context, oref waveobj.ORef) waveobj.WaveObj { + updatesVal := ctx.Value(waveObjUpdateKey) + if updatesVal == nil { + return nil + } + updates := updatesVal.(*contextUpdatesType) + for idx := len(updates.UpdatesStack) - 1; idx >= 0; idx-- { + if obj, ok := updates.UpdatesStack[idx][oref]; ok { + return obj + } + } + return nil +} + +func ContextAddUpdate(ctx context.Context, obj waveobj.WaveObj) { + updatesVal := ctx.Value(waveObjUpdateKey) + if updatesVal == nil { + return + } + updates := updatesVal.(*contextUpdatesType) + oref := waveobj.ORef{ + OType: obj.GetOType(), + OID: waveobj.GetOID(obj), + } + updates.UpdatesStack[len(updates.UpdatesStack)-1][oref] = obj +} + +func ContextUpdatesBeginTx(ctx context.Context) context.Context { + updatesVal := ctx.Value(waveObjUpdateKey) + if updatesVal == nil { + return ctx + } + updates := updatesVal.(*contextUpdatesType) + updates.UpdatesStack = append(updates.UpdatesStack, make(map[waveobj.ORef]waveobj.WaveObj)) + return ctx +} + +func ContextUpdatesCommitTx(ctx context.Context) { + updatesVal := ctx.Value(waveObjUpdateKey) + if updatesVal == nil { + return + } + updates := updatesVal.(*contextUpdatesType) + if len(updates.UpdatesStack) <= 1 { + panic(fmt.Errorf("no updates transaction to commit")) + } + // merge the last two updates + curUpdateMap := updates.UpdatesStack[len(updates.UpdatesStack)-1] + prevUpdateMap := updates.UpdatesStack[len(updates.UpdatesStack)-2] + for k, v := range curUpdateMap { + prevUpdateMap[k] = v + } + updates.UpdatesStack = updates.UpdatesStack[:len(updates.UpdatesStack)-1] +} + +func ContextUpdatesRollbackTx(ctx context.Context) { + updatesVal := ctx.Value(waveObjUpdateKey) + if updatesVal == nil { + return + } + updates := updatesVal.(*contextUpdatesType) + if len(updates.UpdatesStack) <= 1 { + panic(fmt.Errorf("no updates transaction to rollback")) + } + updates.UpdatesStack = updates.UpdatesStack[:len(updates.UpdatesStack)-1] +} + +type UIContext struct { + WindowId string `json:"windowid"` +} + type Client struct { OID string `json:"oid"` Version int `json:"version"` @@ -128,39 +243,51 @@ func (*Block) GetOType() string { return "block" } -func CreateTab(workspaceId string, name string) (*Tab, error) { - tab := &Tab{ - OID: uuid.New().String(), - Name: name, - BlockIds: []string{}, - } - TabMap.Set(tab.OID, tab) - ws := WorkspaceMap.Get(workspaceId) - if ws == nil { - return nil, fmt.Errorf("workspace not found: %q", workspaceId) - } - ws.TabIds = append(ws.TabIds, tab.OID) - return tab, nil +func CreateTab(ctx context.Context, workspaceId string, name string) (*Tab, error) { + return WithTxRtn(ctx, func(tx *TxWrap) (*Tab, error) { + ws, _ := DBGet[*Workspace](tx.Context(), workspaceId) + if ws == nil { + return nil, fmt.Errorf("workspace not found: %q", workspaceId) + } + tab := &Tab{ + OID: uuid.New().String(), + Name: name, + BlockIds: []string{}, + } + ws.TabIds = append(ws.TabIds, tab.OID) + DBInsert(tx.Context(), tab) + DBUpdate(tx.Context(), ws) + return tab, nil + }) } -func CreateWorkspace() (*Workspace, error) { +func CreateWorkspace(ctx context.Context) (*Workspace, error) { ws := &Workspace{ OID: uuid.New().String(), TabIds: []string{}, } - WorkspaceMap.Set(ws.OID, ws) - _, err := CreateTab(ws.OID, "Tab 1") - if err != nil { - return nil, err - } + DBInsert(ctx, ws) return ws, nil } -func GetObject(otype string, oid string) (waveobj.WaveObj, error) { - return nil, nil +func SetActiveTab(ctx context.Context, windowId string, tabId string) error { + return WithTx(ctx, func(tx *TxWrap) error { + window, _ := DBGet[*Window](tx.Context(), windowId) + if window == nil { + return fmt.Errorf("window not found: %q", windowId) + } + tab, _ := DBGet[*Tab](tx.Context(), tabId) + if tab == nil { + return fmt.Errorf("tab not found: %q", tabId) + } + window.ActiveTabId = tabId + DBUpdate(tx.Context(), window) + return nil + }) } func EnsureInitialData() error { + // does not need to run in a transaction since it is called on startup ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second) defer cancelFn() clientCount, err := DBGetCount[*Client](ctx) diff --git a/pkg/wstore/wstore_dbops.go b/pkg/wstore/wstore_dbops.go index 393ac628..ec7a6d3e 100644 --- a/pkg/wstore/wstore_dbops.go +++ b/pkg/wstore/wstore_dbops.go @@ -10,6 +10,8 @@ import ( "github.com/wavetermdev/thenextwave/pkg/waveobj" ) +var ErrNotFound = fmt.Errorf("not found") + func waveObjTableName(w waveobj.WaveObj) string { return "db_" + w.GetOType() } @@ -75,6 +77,19 @@ func DBGet[T waveobj.WaveObj](ctx context.Context, id string) (T, error) { return genericCastWithErr[T](rtn, err) } +func DBMustGet[T waveobj.WaveObj](ctx context.Context, id string) (T, error) { + rtn, err := DBGetORef(ctx, waveobj.ORef{OType: getOTypeGen[T](), OID: id}) + if err != nil { + var zeroVal T + return zeroVal, err + } + if rtn == nil { + var zeroVal T + return zeroVal, ErrNotFound + } + return rtn.(T), nil +} + func DBGetORef(ctx context.Context, oref waveobj.ORef) (waveobj.WaveObj, error) { return WithTxRtn(ctx, func(tx *TxWrap) (waveobj.WaveObj, error) { table := tableNameFromOType(oref.OType) @@ -144,11 +159,15 @@ func DBDelete(ctx context.Context, otype string, id string) error { table := tableNameFromOType(otype) query := fmt.Sprintf("DELETE FROM %s WHERE oid = ?", table) tx.Exec(query, id) + ContextAddUpdate(ctx, &waveobj.WaveObjTombstone{OType: otype, OID: id}) return nil }) } func DBUpdate(ctx context.Context, val waveobj.WaveObj) error { + if waveobj.IsTombstone(val) { + return fmt.Errorf("cannot update deleted object") + } oid := waveobj.GetOID(val) if oid == "" { return fmt.Errorf("cannot update %T value with empty id", val) @@ -159,13 +178,18 @@ func DBUpdate(ctx context.Context, val waveobj.WaveObj) error { } return WithTx(ctx, func(tx *TxWrap) error { table := waveObjTableName(val) - query := fmt.Sprintf("UPDATE %s SET data = ?, version = version+1 WHERE oid = ?", table) - tx.Exec(query, jsonData, oid) + query := fmt.Sprintf("UPDATE %s SET data = ?, version = version+1 WHERE oid = ? RETURNING version", table) + newVersion := tx.GetInt(query, jsonData, oid) + waveobj.SetVersion(val, newVersion) + ContextAddUpdate(ctx, val) return nil }) } func DBInsert(ctx context.Context, val waveobj.WaveObj) error { + if waveobj.IsTombstone(val) { + return fmt.Errorf("cannot insert deleted object") + } oid := waveobj.GetOID(val) if oid == "" { return fmt.Errorf("cannot insert %T value with empty id", val) @@ -176,8 +200,10 @@ func DBInsert(ctx context.Context, val waveobj.WaveObj) error { } return WithTx(ctx, func(tx *TxWrap) error { table := waveObjTableName(val) + waveobj.SetVersion(val, 1) query := fmt.Sprintf("INSERT INTO %s (oid, version, data) VALUES (?, ?, ?)", table) tx.Exec(query, oid, 1, jsonData) + ContextAddUpdate(ctx, val) return nil }) } diff --git a/pkg/wstore/wstore_dbsetup.go b/pkg/wstore/wstore_dbsetup.go index d1141490..f79e2cea 100644 --- a/pkg/wstore/wstore_dbsetup.go +++ b/pkg/wstore/wstore_dbsetup.go @@ -55,10 +55,26 @@ func MakeDB(ctx context.Context) (*sqlx.DB, error) { return rtn, nil } -func WithTx(ctx context.Context, fn func(tx *TxWrap) error) error { +func WithTx(ctx context.Context, fn func(tx *TxWrap) error) (rtnErr error) { + ContextUpdatesBeginTx(ctx) + defer func() { + if rtnErr != nil { + ContextUpdatesRollbackTx(ctx) + } else { + ContextUpdatesCommitTx(ctx) + } + }() return txwrap.WithTx(ctx, globalDB, fn) } -func WithTxRtn[RT any](ctx context.Context, fn func(tx *TxWrap) (RT, error)) (RT, error) { +func WithTxRtn[RT any](ctx context.Context, fn func(tx *TxWrap) (RT, error)) (rtnVal RT, rtnErr error) { + ContextUpdatesBeginTx(ctx) + defer func() { + if rtnErr != nil { + ContextUpdatesRollbackTx(ctx) + } else { + ContextUpdatesCommitTx(ctx) + } + }() return txwrap.WithTxRtn(ctx, globalDB, fn) } From abedca2364a878a6b524f56bb8b1a9999b1ba668 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 27 May 2024 14:31:12 -0700 Subject: [PATCH 09/11] setactivetab working, removed tombstones, created updatetype --- frontend/app/store/wos.ts | 37 +++++++------ frontend/app/workspace/workspace.tsx | 2 +- frontend/types/custom.d.ts | 19 ++++--- pkg/service/objectservice/objectservice.go | 20 ++++--- pkg/waveobj/waveobj.go | 61 ++-------------------- pkg/wstore/wstore.go | 53 +++++++++++++++---- pkg/wstore/wstore_dbops.go | 12 ++--- 7 files changed, 89 insertions(+), 115 deletions(-) diff --git a/frontend/app/store/wos.ts b/frontend/app/store/wos.ts index bd845f60..f9c625b4 100644 --- a/frontend/app/store/wos.ts +++ b/frontend/app/store/wos.ts @@ -41,10 +41,7 @@ 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)) { + if (isBlank(val.otype) || isBlank(val.oid) || isBlankNum(val.version)) { return false; } return true; @@ -151,34 +148,34 @@ function useWaveObject(oref: string): [T, boolean, (T) => void] { return [atomVal.value, atomVal.loading, simpleSet]; } -function updateWaveObject(val: WaveObj) { - if (val == null) { +function updateWaveObject(update: WaveObjUpdate) { + if (update == null) { return; } - if (!isValidWaveObj(val)) { - console.log("invalid wave object", val); - return; - } - let oref = makeORef(val.otype, val.oid); + let oref = makeORef(update.otype, update.oid); let wov = waveObjectValueCache.get(oref); if (wov == null) { wov = createWaveValueObject(oref, false); waveObjectValueCache.set(oref, wov); } - if (val.deleted) { + if (update.updatetype == "delete") { globalStore.set(wov.dataAtom, { value: null, loading: false }); } else { - let curValue: WaveObjectDataItemType = globalStore.get(wov.dataAtom); - if (curValue.value != null && curValue.value.version >= val.version) { + if (!isValidWaveObj(update.obj)) { + console.log("invalid wave object update", update); return; } - globalStore.set(wov.dataAtom, { value: val, loading: false }); + let curValue: WaveObjectDataItemType = globalStore.get(wov.dataAtom); + if (curValue.value != null && curValue.value.version >= update.obj.version) { + return; + } + globalStore.set(wov.dataAtom, { value: update.obj, loading: false }); } wov.holdTime = Date.now() + defaultHoldTime; return; } -function updateWaveObjects(vals: WaveObj[]) { +function updateWaveObjects(vals: WaveObjUpdate[]) { for (let val of vals) { updateWaveObject(val); } @@ -194,7 +191,7 @@ function cleanWaveObjectCache() { } Events.On("waveobj:update", (event: any) => { - const data: WaveObj[] = event?.data; + const data: WaveObjUpdate[] = event?.data; if (data == null) { return; } @@ -217,6 +214,7 @@ function wrapObjectServiceCall(fnName: string, ...args: any[]): Promise { ); prtn = prtn.then((val) => { if (val.updates) { + console.log(val.updates); updateWaveObjects(val.updates); } return val; @@ -228,6 +226,10 @@ function AddTabToWorkspace(tabName: string, activateTab: boolean): Promise<{ tab return wrapObjectServiceCall("AddTabToWorkspace", tabName, activateTab); } +function SetActiveTab(tabId: string): Promise { + return wrapObjectServiceCall("SetActiveTab", tabId); +} + function getStaticObjectValue(oref: string, getFn: jotai.Getter): T { let wov = waveObjectValueCache.get(oref); if (wov == null) { @@ -248,4 +250,5 @@ export { cleanWaveObjectCache, getStaticObjectValue, AddTabToWorkspace, + SetActiveTab, }; diff --git a/frontend/app/workspace/workspace.tsx b/frontend/app/workspace/workspace.tsx index b00428c9..7c986643 100644 --- a/frontend/app/workspace/workspace.tsx +++ b/frontend/app/workspace/workspace.tsx @@ -24,7 +24,7 @@ function Tab({ tabId }: { tabId: string }) { if (tabId == null) { return; } - // TODO + WOS.SetActiveTab(tabId); } return (
= 0; idx-- { if obj, ok := updates.UpdatesStack[idx][oref]; ok { - return obj + return &obj } } return nil } -func ContextAddUpdate(ctx context.Context, obj waveobj.WaveObj) { +func ContextAddUpdate(ctx context.Context, update WaveObjUpdate) { updatesVal := ctx.Value(waveObjUpdateKey) if updatesVal == nil { return } updates := updatesVal.(*contextUpdatesType) oref := waveobj.ORef{ - OType: obj.GetOType(), - OID: waveobj.GetOID(obj), + OType: update.OType, + OID: update.OID, } - updates.UpdatesStack[len(updates.UpdatesStack)-1][oref] = obj + updates.UpdatesStack[len(updates.UpdatesStack)-1][oref] = update } func ContextUpdatesBeginTx(ctx context.Context) context.Context { @@ -102,7 +103,7 @@ func ContextUpdatesBeginTx(ctx context.Context) context.Context { return ctx } updates := updatesVal.(*contextUpdatesType) - updates.UpdatesStack = append(updates.UpdatesStack, make(map[waveobj.ORef]waveobj.WaveObj)) + updates.UpdatesStack = append(updates.UpdatesStack, make(map[waveobj.ORef]WaveObjUpdate)) return ctx } @@ -136,6 +137,36 @@ func ContextUpdatesRollbackTx(ctx context.Context) { updates.UpdatesStack = updates.UpdatesStack[:len(updates.UpdatesStack)-1] } +type WaveObjTombstone struct { + OType string `json:"otype"` + OID string `json:"oid"` +} + +const ( + UpdateType_Update = "update" + UpdateType_Delete = "delete" +) + +type WaveObjUpdate struct { + UpdateType string `json:"updatetype"` + OType string `json:"otype"` + OID string `json:"oid"` + Obj waveobj.WaveObj `json:"obj,omitempty"` +} + +func (update WaveObjUpdate) MarshalJSON() ([]byte, error) { + rtn := make(map[string]any) + rtn["updatetype"] = update.UpdateType + rtn["otype"] = update.OType + rtn["oid"] = update.OID + var err error + rtn["obj"], err = waveobj.ToJsonMap(update.Obj) + if err != nil { + return nil, err + } + return json.Marshal(rtn) +} + type UIContext struct { WindowId string `json:"windowid"` } diff --git a/pkg/wstore/wstore_dbops.go b/pkg/wstore/wstore_dbops.go index ec7a6d3e..e912fe8d 100644 --- a/pkg/wstore/wstore_dbops.go +++ b/pkg/wstore/wstore_dbops.go @@ -159,15 +159,12 @@ func DBDelete(ctx context.Context, otype string, id string) error { table := tableNameFromOType(otype) query := fmt.Sprintf("DELETE FROM %s WHERE oid = ?", table) tx.Exec(query, id) - ContextAddUpdate(ctx, &waveobj.WaveObjTombstone{OType: otype, OID: id}) + ContextAddUpdate(ctx, WaveObjUpdate{UpdateType: UpdateType_Delete, OType: otype, OID: id}) return nil }) } func DBUpdate(ctx context.Context, val waveobj.WaveObj) error { - if waveobj.IsTombstone(val) { - return fmt.Errorf("cannot update deleted object") - } oid := waveobj.GetOID(val) if oid == "" { return fmt.Errorf("cannot update %T value with empty id", val) @@ -181,15 +178,12 @@ func DBUpdate(ctx context.Context, val waveobj.WaveObj) error { query := fmt.Sprintf("UPDATE %s SET data = ?, version = version+1 WHERE oid = ? RETURNING version", table) newVersion := tx.GetInt(query, jsonData, oid) waveobj.SetVersion(val, newVersion) - ContextAddUpdate(ctx, val) + ContextAddUpdate(ctx, WaveObjUpdate{UpdateType: UpdateType_Update, OType: val.GetOType(), OID: oid, Obj: val}) return nil }) } func DBInsert(ctx context.Context, val waveobj.WaveObj) error { - if waveobj.IsTombstone(val) { - return fmt.Errorf("cannot insert deleted object") - } oid := waveobj.GetOID(val) if oid == "" { return fmt.Errorf("cannot insert %T value with empty id", val) @@ -203,7 +197,7 @@ func DBInsert(ctx context.Context, val waveobj.WaveObj) error { waveobj.SetVersion(val, 1) query := fmt.Sprintf("INSERT INTO %s (oid, version, data) VALUES (?, ?, ?)", table) tx.Exec(query, oid, 1, jsonData) - ContextAddUpdate(ctx, val) + ContextAddUpdate(ctx, WaveObjUpdate{UpdateType: UpdateType_Update, OType: val.GetOType(), OID: oid, Obj: val}) return nil }) } From e6d7a4e674950f846e28aa2aa23723431035ab55 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 27 May 2024 15:44:57 -0700 Subject: [PATCH 10/11] app is working again. new structure for blocks. new useWaveObjectValueWithSuspense hook --- frontend/app/block/block.tsx | 10 ++-- frontend/app/store/global.ts | 12 ++-- frontend/app/store/wos.ts | 44 +++++++++++---- frontend/app/tab/tab.tsx | 2 +- frontend/app/view/preview.tsx | 16 ++++-- frontend/app/workspace/workspace.tsx | 33 ++++------- frontend/types/custom.d.ts | 3 +- frontend/wave.ts | 15 ++--- main.go | 2 +- pkg/blockcontroller/blockcontroller.go | 66 ++++++++-------------- pkg/eventbus/eventbus.go | 52 ++++++++++++++--- pkg/service/blockservice/blockservice.go | 32 ----------- pkg/service/objectservice/objectservice.go | 23 ++++++++ pkg/wstore/wstore.go | 27 ++++++++- 14 files changed, 193 insertions(+), 144 deletions(-) diff --git a/frontend/app/block/block.tsx b/frontend/app/block/block.tsx index 4a56d1fa..c5014303 100644 --- a/frontend/app/block/block.tsx +++ b/frontend/app/block/block.tsx @@ -3,8 +3,7 @@ import * as React from "react"; import * as jotai from "jotai"; -import { atoms, blockDataMap } from "@/store/global"; - +import * as WOS from "@/store/wos"; import { TerminalView } from "@/app/view/term"; import { PreviewView } from "@/app/view/preview"; import { PlotView } from "@/app/view/plotview"; @@ -33,9 +32,10 @@ const Block = ({ tabId, blockId }: { tabId: string; blockId: string }) => { }, [blockRef.current]); let blockElem: JSX.Element = null; - const blockAtom = blockDataMap.get(blockId); - const blockData = jotai.useAtomValue(blockAtom); - if (blockData.view === "term") { + const [blockData, blockDataLoading] = WOS.useWaveObjectValue(WOS.makeORef("block", blockId)); + if (blockDataLoading) { + blockElem = Loading...; + } else if (blockData.view === "term") { blockElem = ; } else if (blockData.view === "preview") { blockElem = ; diff --git a/frontend/app/store/global.ts b/frontend/app/store/global.ts index cf2dc10d..f6bc3af9 100644 --- a/frontend/app/store/global.ts +++ b/frontend/app/store/global.ts @@ -4,13 +4,9 @@ import * as jotai from "jotai"; import * as rxjs from "rxjs"; import { Events } from "@wailsio/runtime"; -import { produce } from "immer"; -import { BlockService } from "@/bindings/blockservice"; -import * as wstore from "@/gopkg/wstore"; import * as WOS from "./wos"; const globalStore = jotai.createStore(); -const blockDataMap = new Map>(); const urlParams = new URLSearchParams(window.location.search); const globalWindowId = urlParams.get("windowid"); const globalClientId = urlParams.get("clientid"); @@ -19,8 +15,10 @@ const clientIdAtom = jotai.atom(null) as jotai.PrimitiveAtom; 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; @@ -54,7 +52,6 @@ const atoms = { client: clientAtom, waveWindow: windowDataAtom, workspace: workspaceAtom, - blockDataMap: blockDataMap, }; type SubjectWithRef = rxjs.Subject & { refCount: number; release: () => void }; @@ -93,6 +90,8 @@ Events.On("block:ptydata", (event: any) => { subject.next(data); }); +const blockAtomCache = new Map>>(); + function useBlockAtom(blockId: string, name: string, makeFn: () => jotai.Atom): jotai.Atom { let blockCache = blockAtomCache.get(blockId); if (blockCache == null) { @@ -103,8 +102,9 @@ function useBlockAtom(blockId: string, name: string, makeFn: () => jotai.Atom if (atom == null) { atom = makeFn(); blockCache.set(name, atom); + console.log("New BlockAtom", blockId, name); } return atom as jotai.Atom; } -export { globalStore, atoms, getBlockSubject, blockDataMap, useBlockAtom, WOS }; +export { globalStore, atoms, getBlockSubject, useBlockAtom, WOS }; diff --git a/frontend/app/store/wos.ts b/frontend/app/store/wos.ts index f9c625b4..80c80624 100644 --- a/frontend/app/store/wos.ts +++ b/frontend/app/store/wos.ts @@ -94,7 +94,7 @@ function createWaveValueObject(oref: string, shouldFetch: boo } wov.pendingPromise = null; globalStore.set(wov.dataAtom, { value: val, loading: false }); - console.log("GetObject resolved", oref, Date.now() - startTs + "ms"); + console.log("WaveObj resolved", oref, Date.now() - startTs + "ms"); }); return wov; } @@ -113,6 +113,25 @@ function loadAndPinWaveObject(oref: string): Promise { return wov.pendingPromise; } +function useWaveObjectValueWithSuspense(oref: string): T { + 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 dataValue = jotai.useAtomValue(wov.dataAtom); + if (dataValue.loading) { + throw wov.pendingPromise; + } + return dataValue.value; +} + function useWaveObjectValue(oref: string): [T, boolean] { let wov = waveObjectValueCache.get(oref); if (wov == null) { @@ -214,7 +233,6 @@ function wrapObjectServiceCall(fnName: string, ...args: any[]): Promise { ); prtn = prtn.then((val) => { if (val.updates) { - console.log(val.updates); updateWaveObjects(val.updates); } return val; @@ -222,14 +240,6 @@ function wrapObjectServiceCall(fnName: string, ...args: any[]): Promise { return prtn; } -function AddTabToWorkspace(tabName: string, activateTab: boolean): Promise<{ tabId: string }> { - return wrapObjectServiceCall("AddTabToWorkspace", tabName, activateTab); -} - -function SetActiveTab(tabId: string): Promise { - return wrapObjectServiceCall("SetActiveTab", tabId); -} - function getStaticObjectValue(oref: string, getFn: jotai.Getter): T { let wov = waveObjectValueCache.get(oref); if (wov == null) { @@ -239,10 +249,23 @@ function getStaticObjectValue(oref: string, getFn: jotai.Getter): T { return atomVal.value; } +function AddTabToWorkspace(tabName: string, activateTab: boolean): Promise<{ tabId: string }> { + return wrapObjectServiceCall("AddTabToWorkspace", tabName, activateTab); +} + +function SetActiveTab(tabId: string): Promise { + return wrapObjectServiceCall("SetActiveTab", tabId); +} + +function CreateBlock(blockDef: BlockDef, rtOpts: RuntimeOpts): Promise<{ blockId: string }> { + return wrapObjectServiceCall("CreateBlock", blockDef, rtOpts); +} + export { makeORef, useWaveObject, useWaveObjectValue, + useWaveObjectValueWithSuspense, loadAndPinWaveObject, clearWaveObjectCache, updateWaveObject, @@ -251,4 +274,5 @@ export { getStaticObjectValue, AddTabToWorkspace, SetActiveTab, + CreateBlock, }; diff --git a/frontend/app/tab/tab.tsx b/frontend/app/tab/tab.tsx index 97aa653f..e237585b 100644 --- a/frontend/app/tab/tab.tsx +++ b/frontend/app/tab/tab.tsx @@ -23,7 +23,7 @@ const TabContent = ({ tabId }: { tabId: string }) => { {tabData.blockids.map((blockId: string) => { return (
- +
); })} diff --git a/frontend/app/view/preview.tsx b/frontend/app/view/preview.tsx index e3ecaad8..dabd2bdb 100644 --- a/frontend/app/view/preview.tsx +++ b/frontend/app/view/preview.tsx @@ -3,15 +3,16 @@ import * as React from "react"; import * as jotai from "jotai"; -import { atoms, blockDataMap, useBlockAtom } from "@/store/global"; +import { atoms, useBlockAtom } from "@/store/global"; import { Markdown } from "@/element/markdown"; import { FileService, FileInfo, FullFile } from "@/bindings/fileservice"; import * as util from "@/util/util"; import { CenteredDiv } from "../element/quickelems"; import { DirectoryTable } from "@/element/directorytable"; -import * as wstore from "@/gopkg/wstore"; +import * as WOS from "@/store/wos"; import "./view.less"; +import { first } from "rxjs"; const MaxFileSize = 1024 * 1024 * 10; // 10MB @@ -62,10 +63,17 @@ function DirectoryPreview({ contentAtom }: { contentAtom: jotai.Atom = blockDataMap.get(blockId); + const blockData = WOS.useWaveObjectValueWithSuspense(WOS.makeORef("block", blockId)); + if (blockData == null) { + return ( +
+ Block Not Found +
+ ); + } const fileNameAtom = useBlockAtom(blockId, "preview:filename", () => jotai.atom((get) => { - return get(blockDataAtom)?.meta?.file; + return blockData?.meta?.file; }) ); const statFileAtom = useBlockAtom(blockId, "preview:statfile", () => diff --git a/frontend/app/workspace/workspace.tsx b/frontend/app/workspace/workspace.tsx index 7c986643..a31443b5 100644 --- a/frontend/app/workspace/workspace.tsx +++ b/frontend/app/workspace/workspace.tsx @@ -5,13 +5,7 @@ import * as React from "react"; import * as jotai from "jotai"; import { TabContent } from "@/app/tab/tab"; import { clsx } from "clsx"; -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 { atoms } from "@/store/global"; import * as WOS from "@/store/wos"; import { CenteredLoadingDiv, CenteredDiv } from "../element/quickelems"; @@ -36,7 +30,7 @@ function Tab({ tabId }: { tabId: string }) { ); } -function TabBar({ workspace, waveWindow }: { workspace: Workspace; waveWindow: WaveWindow }) { +function TabBar({ workspace }: { workspace: Workspace }) { function handleAddTab() { const newTabName = `Tab-${workspace.tabids.length + 1}`; WOS.AddTabToWorkspace(newTabName, true); @@ -58,34 +52,31 @@ function Widgets() { const windowData = jotai.useAtomValue(atoms.waveWindow); const activeTabId = windowData.activetabid; - async function createBlock(blockDef: wstore.BlockDef) { - const rtOpts: wstore.RuntimeOpts = new wstore.RuntimeOpts({ termsize: { rows: 25, cols: 80 } }); - const rtnBlock: wstore.Block = await BlockService.CreateBlock(blockDef, rtOpts); - const newBlockAtom = jotai.atom(rtnBlock); - blockDataMap.set(rtnBlock.blockid, newBlockAtom); - addBlockIdToTab(activeTabId, rtnBlock.blockid); + async function createBlock(blockDef: BlockDef) { + const rtOpts: RuntimeOpts = { termsize: { rows: 25, cols: 80 } }; + await WOS.CreateBlock(blockDef, rtOpts); } async function clickTerminal() { - const termBlockDef = new wstore.BlockDef({ + const termBlockDef = { controller: "shell", view: "term", - }); + }; createBlock(termBlockDef); } async function clickPreview(fileName: string) { - const markdownDef = new wstore.BlockDef({ + const markdownDef = { view: "preview", meta: { file: fileName }, - }); + }; createBlock(markdownDef); } async function clickPlot() { - const plotDef = new wstore.BlockDef({ + const plotDef: BlockDef = { view: "plot", - }); + }; createBlock(plotDef); } @@ -122,7 +113,7 @@ function WorkspaceElem() { const ws = jotai.useAtomValue(atoms.workspace); return (
- +
diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index 04cddaf9..e94ff3e0 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -4,6 +4,7 @@ declare global { type UIContext = { windowid: string; + activetabid: string; }; type ORef = { @@ -33,7 +34,7 @@ declare global { }; type BlockDef = { - controller: string; + controller?: string; view?: string; files?: { [key: string]: FileDef }; meta?: { [key: string]: any }; diff --git a/frontend/wave.ts b/frontend/wave.ts index c9d116ac..98bdb412 100644 --- a/frontend/wave.ts +++ b/frontend/wave.ts @@ -18,20 +18,12 @@ const urlParams = new URLSearchParams(window.location.search); const windowId = urlParams.get("windowid"); const clientId = urlParams.get("clientid"); -wstore.Block.prototype[immerable] = true; -wstore.Tab.prototype[immerable] = true; -wstore.Client.prototype[immerable] = true; -wstore.Window.prototype[immerable] = true; -wstore.Workspace.prototype[immerable] = true; -wstore.BlockDef.prototype[immerable] = true; -wstore.RuntimeOpts.prototype[immerable] = true; -wstore.FileDef.prototype[immerable] = true; -wstore.Point.prototype[immerable] = true; -wstore.WinSize.prototype[immerable] = true; - loadFonts(); +console.log("Wave Starting"); + document.addEventListener("DOMContentLoaded", async () => { + console.log("DOMContentLoaded"); // ensures client/window are loaded into the cache before rendering await WOS.loadAndPinWaveObject(WOS.makeORef("client", clientId)); const waveWindow = await WOS.loadAndPinWaveObject(WOS.makeORef("window", windowId)); @@ -40,6 +32,7 @@ document.addEventListener("DOMContentLoaded", async () => { let elem = document.getElementById("main"); let root = createRoot(elem); document.fonts.ready.then(() => { + console.log("Wave First Render"); root.render(reactElem); }); }); diff --git a/main.go b/main.go index 007106e0..cb923c3a 100644 --- a/main.go +++ b/main.go @@ -72,7 +72,7 @@ func createWindow(windowData *wstore.Window, app *application.App) { Width: windowData.WinSize.Width, Height: windowData.WinSize.Height, }) - eventbus.RegisterWailsWindow(window) + eventbus.RegisterWailsWindow(window, windowData.OID) window.On(events.Common.WindowClosing, func(event *application.WindowEvent) { eventbus.UnregisterWailsWindow(window.ID()) }) diff --git a/pkg/blockcontroller/blockcontroller.go b/pkg/blockcontroller/blockcontroller.go index 70d03c7a..d78f4edb 100644 --- a/pkg/blockcontroller/blockcontroller.go +++ b/pkg/blockcontroller/blockcontroller.go @@ -14,7 +14,6 @@ import ( "time" "github.com/creack/pty" - "github.com/google/uuid" "github.com/wailsapp/wails/v3/pkg/application" "github.com/wavetermdev/thenextwave/pkg/eventbus" "github.com/wavetermdev/thenextwave/pkg/shellexec" @@ -61,41 +60,6 @@ func jsonDeepCopy(val map[string]any) (map[string]any, error) { return rtn, nil } -func CreateBlock(ctx context.Context, bdef *wstore.BlockDef, rtOpts *wstore.RuntimeOpts) (*wstore.Block, error) { - // TODO - blockId := uuid.New().String() - blockData := &wstore.Block{ - OID: blockId, - BlockDef: bdef, - Controller: bdef.Controller, - View: bdef.View, - RuntimeOpts: rtOpts, - } - var err error - blockData.Meta, err = jsonDeepCopy(bdef.Meta) - if err != nil { - return nil, fmt.Errorf("error copying meta: %w", err) - } - err = wstore.DBInsert(ctx, blockData) - if err != nil { - return nil, fmt.Errorf("error inserting block: %w", err) - } - if blockData.Controller != "" { - StartBlockController(blockId, blockData) - } - return blockData, nil -} - -func CloseBlock(blockId string) { - // TODO - bc := GetBlockController(blockId) - if bc == nil { - return - } - bc.Close() - close(bc.InputCh) -} - func (bc *BlockController) setShellProc(shellProc *shellexec.ShellProc) error { bc.Lock.Lock() defer bc.Lock.Unlock() @@ -232,15 +196,23 @@ func (bc *BlockController) Run(bdata *wstore.Block) { } } -func StartBlockController(blockId string, bdata *wstore.Block) { - if bdata.Controller != BlockController_Shell { - log.Printf("unknown controller %q\n", bdata.Controller) - return +func StartBlockController(ctx context.Context, blockId string) error { + blockData, err := wstore.DBMustGet[*wstore.Block](ctx, blockId) + if err != nil { + return fmt.Errorf("error getting block: %w", err) + } + if blockData.Controller == "" { + // nothing to start + return nil + } + if blockData.Controller != BlockController_Shell { + return fmt.Errorf("unknown controller %q", blockData.Controller) } globalLock.Lock() defer globalLock.Unlock() if _, ok := blockControllerMap[blockId]; ok { - return + // already running + return nil } bc := &BlockController{ Lock: &sync.Mutex{}, @@ -249,7 +221,17 @@ func StartBlockController(blockId string, bdata *wstore.Block) { InputCh: make(chan BlockCommand), } blockControllerMap[blockId] = bc - go bc.Run(bdata) + go bc.Run(blockData) + return nil +} + +func StopBlockController(blockId string) { + bc := GetBlockController(blockId) + if bc == nil { + return + } + bc.Close() + close(bc.InputCh) } func GetBlockController(blockId string) *BlockController { diff --git a/pkg/eventbus/eventbus.go b/pkg/eventbus/eventbus.go index 8300d76b..41a8933f 100644 --- a/pkg/eventbus/eventbus.go +++ b/pkg/eventbus/eventbus.go @@ -5,11 +5,13 @@ package eventbus import ( "errors" + "fmt" "log" "runtime/debug" "sync" "github.com/wailsapp/wails/v3/pkg/application" + "github.com/wavetermdev/thenextwave/pkg/waveobj" ) const EventBufferSize = 50 @@ -24,9 +26,16 @@ type WindowEvent struct { Event application.WailsEvent } +type WindowWatchData struct { + Window *application.WebviewWindow + WaveWindowId string + WailsWindowId uint + WatchedORefs map[waveobj.ORef]bool +} + var globalLock = &sync.Mutex{} var wailsApp *application.App -var wailsWindowMap = make(map[uint]*application.WebviewWindow) +var wailsWindowMap = make(map[uint]*WindowWatchData) func Start() { go processEvents() @@ -42,10 +51,18 @@ func RegisterWailsApp(app *application.App) { wailsApp = app } -func RegisterWailsWindow(window *application.WebviewWindow) { +func RegisterWailsWindow(window *application.WebviewWindow, windowId string) { globalLock.Lock() defer globalLock.Unlock() - wailsWindowMap[window.ID()] = window + if _, found := wailsWindowMap[window.ID()]; found { + panic(fmt.Errorf("wails window already registered with eventbus: %d", window.ID())) + } + wailsWindowMap[window.ID()] = &WindowWatchData{ + Window: window, + WailsWindowId: window.ID(), + WaveWindowId: "", + WatchedORefs: make(map[waveobj.ORef]bool), + } } func UnregisterWailsWindow(windowId uint) { @@ -56,18 +73,18 @@ func UnregisterWailsWindow(windowId uint) { func emitEventToWindow(event WindowEvent) { globalLock.Lock() - window := wailsWindowMap[event.WindowId] + wdata := wailsWindowMap[event.WindowId] globalLock.Unlock() - if window != nil { - window.DispatchWailsEvent(&event.Event) + if wdata != nil { + wdata.Window.DispatchWailsEvent(&event.Event) } } func emitEventToAllWindows(event *application.WailsEvent) { globalLock.Lock() wins := make([]*application.WebviewWindow, 0, len(wailsWindowMap)) - for _, window := range wailsWindowMap { - wins = append(wins, window) + for _, wdata := range wailsWindowMap { + wins = append(wins, wdata.Window) } globalLock.Unlock() for _, window := range wins { @@ -79,6 +96,25 @@ func SendEvent(event application.WailsEvent) { EventCh <- event } +func findWindowIdsByORef(oref waveobj.ORef) []uint { + globalLock.Lock() + defer globalLock.Unlock() + var ids []uint + for _, wdata := range wailsWindowMap { + if wdata.WatchedORefs[oref] { + ids = append(ids, wdata.WailsWindowId) + } + } + return ids +} + +func SendORefEvent(oref waveobj.ORef, event application.WailsEvent) { + wins := findWindowIdsByORef(oref) + for _, windowId := range wins { + SendWindowEvent(windowId, event) + } +} + func SendEventNonBlocking(event application.WailsEvent) error { select { case EventCh <- event: diff --git a/pkg/service/blockservice/blockservice.go b/pkg/service/blockservice/blockservice.go index 615cd7f6..e5fe2522 100644 --- a/pkg/service/blockservice/blockservice.go +++ b/pkg/service/blockservice/blockservice.go @@ -4,49 +4,17 @@ package blockservice import ( - "context" "fmt" "strings" "time" "github.com/wavetermdev/thenextwave/pkg/blockcontroller" - "github.com/wavetermdev/thenextwave/pkg/wstore" ) type BlockService struct{} const DefaultTimeout = 2 * time.Second -func (bs *BlockService) CreateBlock(bdef *wstore.BlockDef, rtOpts *wstore.RuntimeOpts) (*wstore.Block, error) { - ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second) - defer cancelFn() - if bdef == nil { - return nil, fmt.Errorf("block definition is nil") - } - if rtOpts == nil { - return nil, fmt.Errorf("runtime options is nil") - } - blockData, err := blockcontroller.CreateBlock(ctx, bdef, rtOpts) - if err != nil { - return nil, fmt.Errorf("error creating block: %w", err) - } - return blockData, nil -} - -func (bs *BlockService) CloseBlock(blockId string) { - blockcontroller.CloseBlock(blockId) -} - -func (bs *BlockService) GetBlockData(blockId string) (*wstore.Block, error) { - ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second) - defer cancelFn() - blockData, err := wstore.DBGet[*wstore.Block](ctx, blockId) - if err != nil { - return nil, fmt.Errorf("error getting block data: %w", err) - } - return blockData, nil -} - func (bs *BlockService) SendCommand(blockId string, cmdMap map[string]any) error { cmd, err := blockcontroller.ParseCmdMap(cmdMap) if err != nil { diff --git a/pkg/service/objectservice/objectservice.go b/pkg/service/objectservice/objectservice.go index cfa612c9..1aa7f42e 100644 --- a/pkg/service/objectservice/objectservice.go +++ b/pkg/service/objectservice/objectservice.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "github.com/wavetermdev/thenextwave/pkg/blockcontroller" "github.com/wavetermdev/thenextwave/pkg/waveobj" "github.com/wavetermdev/thenextwave/pkg/wstore" ) @@ -109,3 +110,25 @@ func (svc *ObjectService) SetActiveTab(uiContext wstore.UIContext, tabId string) } return updatesRtn(ctx, nil) } + +func (svc *ObjectService) CreateBlock(uiContext wstore.UIContext, blockDef *wstore.BlockDef, rtOpts *wstore.RuntimeOpts) (any, error) { + if uiContext.ActiveTabId == "" { + return nil, fmt.Errorf("no active tab") + } + ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) + defer cancelFn() + ctx = wstore.ContextWithUpdates(ctx) + blockData, err := wstore.CreateBlock(ctx, uiContext.ActiveTabId, blockDef, rtOpts) + if err != nil { + return nil, fmt.Errorf("error creating block: %w", err) + } + if blockData.Controller != "" { + err = blockcontroller.StartBlockController(ctx, blockData.OID) + if err != nil { + return nil, fmt.Errorf("error starting block controller: %w", err) + } + } + rtn := make(map[string]any) + rtn["blockid"] = blockData.OID + return updatesRtn(ctx, rtn) +} diff --git a/pkg/wstore/wstore.go b/pkg/wstore/wstore.go index 3bb9f078..8d92d3a4 100644 --- a/pkg/wstore/wstore.go +++ b/pkg/wstore/wstore.go @@ -168,7 +168,8 @@ func (update WaveObjUpdate) MarshalJSON() ([]byte, error) { } type UIContext struct { - WindowId string `json:"windowid"` + WindowId string `json:"windowid"` + ActiveTabId string `json:"activetabid"` } type Client struct { @@ -239,7 +240,7 @@ type FileDef struct { } type BlockDef struct { - Controller string `json:"controller"` + Controller string `json:"controller,omitempty"` View string `json:"view,omitempty"` Files map[string]*FileDef `json:"files,omitempty"` Meta map[string]any `json:"meta,omitempty"` @@ -317,6 +318,28 @@ func SetActiveTab(ctx context.Context, windowId string, tabId string) error { }) } +func CreateBlock(ctx context.Context, tabId string, blockDef *BlockDef, rtOpts *RuntimeOpts) (*Block, error) { + return WithTxRtn(ctx, func(tx *TxWrap) (*Block, error) { + tab, _ := DBGet[*Tab](tx.Context(), tabId) + if tab == nil { + return nil, fmt.Errorf("tab not found: %q", tabId) + } + blockId := uuid.New().String() + blockData := &Block{ + OID: blockId, + BlockDef: blockDef, + Controller: blockDef.Controller, + View: blockDef.View, + RuntimeOpts: rtOpts, + Meta: blockDef.Meta, + } + DBInsert(tx.Context(), blockData) + tab.BlockIds = append(tab.BlockIds, blockId) + DBUpdate(tx.Context(), tab) + return blockData, nil + }) +} + func EnsureInitialData() error { // does not need to run in a transaction since it is called on startup ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second) From 3f45945cb4e923003365e5d972f4bf60228d77b7 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 27 May 2024 16:33:31 -0700 Subject: [PATCH 11/11] delete block and close tab working --- README.md | 2 +- frontend/app/block/block.tsx | 2 +- frontend/app/store/wos.ts | 21 +++++-- frontend/app/tab/tab.tsx | 8 ++- frontend/app/workspace/workspace.less | 15 +++++ frontend/app/workspace/workspace.tsx | 25 +++++--- pkg/service/objectservice/objectservice.go | 47 +++++++++++++++ pkg/wstore/wstore.go | 69 +++++++++++++++++++--- 8 files changed, 165 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 846e245a..03cd6006 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Now to run the dev version of the app: wails3 dev ``` -You should see a very poorly laid out app :) +You should see the app! Now to build a MacOS application: diff --git a/frontend/app/block/block.tsx b/frontend/app/block/block.tsx index c5014303..328a86bf 100644 --- a/frontend/app/block/block.tsx +++ b/frontend/app/block/block.tsx @@ -16,7 +16,7 @@ const Block = ({ tabId, blockId }: { tabId: string; blockId: string }) => { const [dims, setDims] = React.useState({ width: 0, height: 0 }); function handleClose() { - // TODO + WOS.DeleteBlock(blockId); } React.useEffect(() => { diff --git a/frontend/app/store/wos.ts b/frontend/app/store/wos.ts index 80c80624..5e76fb42 100644 --- a/frontend/app/store/wos.ts +++ b/frontend/app/store/wos.ts @@ -178,6 +178,7 @@ function updateWaveObject(update: WaveObjUpdate) { waveObjectValueCache.set(oref, wov); } if (update.updatetype == "delete") { + console.log("WaveObj deleted", oref); globalStore.set(wov.dataAtom, { value: null, loading: false }); } else { if (!isValidWaveObj(update.obj)) { @@ -188,6 +189,7 @@ function updateWaveObject(update: WaveObjUpdate) { if (curValue.value != null && curValue.value.version >= update.obj.version) { return; } + console.log("WaveObj updated", oref); globalStore.set(wov.dataAtom, { value: update.obj, loading: false }); } wov.holdTime = Date.now() + defaultHoldTime; @@ -226,12 +228,14 @@ Events.On("waveobj:update", (event: any) => { function wrapObjectServiceCall(fnName: string, ...args: any[]): Promise { const uiContext = globalStore.get(atoms.uiContext); + const startTs = Date.now(); let prtn = $Call.ByName( "github.com/wavetermdev/thenextwave/pkg/service/objectservice.ObjectService." + fnName, uiContext, ...args ); prtn = prtn.then((val) => { + console.log("Call", fnName, Date.now() - startTs + "ms"); if (val.updates) { updateWaveObjects(val.updates); } @@ -249,18 +253,26 @@ function getStaticObjectValue(oref: string, getFn: jotai.Getter): T { return atomVal.value; } -function AddTabToWorkspace(tabName: string, activateTab: boolean): Promise<{ tabId: string }> { +export function AddTabToWorkspace(tabName: string, activateTab: boolean): Promise<{ tabId: string }> { return wrapObjectServiceCall("AddTabToWorkspace", tabName, activateTab); } -function SetActiveTab(tabId: string): Promise { +export function SetActiveTab(tabId: string): Promise { return wrapObjectServiceCall("SetActiveTab", tabId); } -function CreateBlock(blockDef: BlockDef, rtOpts: RuntimeOpts): Promise<{ blockId: string }> { +export function CreateBlock(blockDef: BlockDef, rtOpts: RuntimeOpts): Promise<{ blockId: string }> { return wrapObjectServiceCall("CreateBlock", blockDef, rtOpts); } +export function DeleteBlock(blockId: string): Promise { + return wrapObjectServiceCall("DeleteBlock", blockId); +} + +export function CloseTab(tabId: string): Promise { + return wrapObjectServiceCall("CloseTab", tabId); +} + export { makeORef, useWaveObject, @@ -272,7 +284,4 @@ export { updateWaveObjects, cleanWaveObjectCache, getStaticObjectValue, - AddTabToWorkspace, - SetActiveTab, - CreateBlock, }; diff --git a/frontend/app/tab/tab.tsx b/frontend/app/tab/tab.tsx index e237585b..609d94ec 100644 --- a/frontend/app/tab/tab.tsx +++ b/frontend/app/tab/tab.tsx @@ -8,7 +8,7 @@ import { atoms } from "@/store/global"; import * as WOS from "@/store/wos"; import "./tab.less"; -import { CenteredLoadingDiv } from "../element/quickelems"; +import { CenteredDiv, CenteredLoadingDiv } from "../element/quickelems"; const TabContent = ({ tabId }: { tabId: string }) => { const [tabData, tabLoading] = WOS.useWaveObjectValue(WOS.makeORef("tab", tabId)); @@ -16,7 +16,11 @@ const TabContent = ({ tabId }: { tabId: string }) => { return ; } if (!tabData) { - return
Tab not found
; + return ( +
+ Tab Not Found +
+ ); } return (
diff --git a/frontend/app/workspace/workspace.less b/frontend/app/workspace/workspace.less index 76ddc8a3..a37ced56 100644 --- a/frontend/app/workspace/workspace.less +++ b/frontend/app/workspace/workspace.less @@ -55,9 +55,24 @@ height: 100%; border-right: 1px solid var(--border-color); cursor: pointer; + position: relative; + &.active { background-color: var(--highlight-bg-color); } + + &.active:hover .tab-close { + display: block; + } + + .tab-close { + position: absolute; + display: none; + padding: 5px; + right: 2px; + top: 5px; + cursor: pointer; + } } .tab-add { diff --git a/frontend/app/workspace/workspace.tsx b/frontend/app/workspace/workspace.tsx index a31443b5..c3b8abce 100644 --- a/frontend/app/workspace/workspace.tsx +++ b/frontend/app/workspace/workspace.tsx @@ -14,17 +14,22 @@ import "./workspace.less"; function Tab({ tabId }: { tabId: string }) { const windowData = jotai.useAtomValue(atoms.waveWindow); const [tabData, tabLoading] = WOS.useWaveObjectValue(WOS.makeORef("tab", tabId)); - function setActiveTab(tabId: string) { - if (tabId == null) { - return; - } + function setActiveTab() { WOS.SetActiveTab(tabId); } + function handleCloseTab() { + WOS.CloseTab(tabId); + } return (
setActiveTab(tabData?.oid)} + onClick={() => setActiveTab()} > +
handleCloseTab()}> +
+ +
+
{tabData?.name ?? "..."}
); @@ -115,8 +120,14 @@ function WorkspaceElem() {
- - + {activeTabId == "" ? ( + No Active Tab + ) : ( + <> + + + + )}
); diff --git a/pkg/service/objectservice/objectservice.go b/pkg/service/objectservice/objectservice.go index 1aa7f42e..0a27a0a5 100644 --- a/pkg/service/objectservice/objectservice.go +++ b/pkg/service/objectservice/objectservice.go @@ -132,3 +132,50 @@ func (svc *ObjectService) CreateBlock(uiContext wstore.UIContext, blockDef *wsto rtn["blockid"] = blockData.OID return updatesRtn(ctx, rtn) } + +func (svc *ObjectService) DeleteBlock(uiContext wstore.UIContext, blockId string) (any, error) { + ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) + defer cancelFn() + ctx = wstore.ContextWithUpdates(ctx) + err := wstore.DeleteBlock(ctx, uiContext.ActiveTabId, blockId) + if err != nil { + return nil, fmt.Errorf("error deleting block: %w", err) + } + blockcontroller.StopBlockController(blockId) + return updatesRtn(ctx, nil) +} + +func (svc *ObjectService) CloseTab(uiContext wstore.UIContext, tabId string) (any, error) { + ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) + defer cancelFn() + ctx = wstore.ContextWithUpdates(ctx) + window, err := wstore.DBMustGet[*wstore.Window](ctx, uiContext.WindowId) + if err != nil { + return nil, fmt.Errorf("error getting window: %w", err) + } + tab, err := wstore.DBMustGet[*wstore.Tab](ctx, tabId) + if err != nil { + return nil, fmt.Errorf("error getting tab: %w", err) + } + for _, blockId := range tab.BlockIds { + blockcontroller.StopBlockController(blockId) + } + err = wstore.CloseTab(ctx, window.WorkspaceId, tabId) + if err != nil { + return nil, fmt.Errorf("error closing tab: %w", err) + } + if window.ActiveTabId == tabId { + ws, err := wstore.DBMustGet[*wstore.Workspace](ctx, window.WorkspaceId) + if err != nil { + return nil, fmt.Errorf("error getting workspace: %w", err) + } + var newActiveTabId string + if len(ws.TabIds) > 0 { + newActiveTabId = ws.TabIds[0] + } else { + newActiveTabId = "" + } + wstore.SetActiveTab(ctx, uiContext.WindowId, newActiveTabId) + } + return updatesRtn(ctx, nil) +} diff --git a/pkg/wstore/wstore.go b/pkg/wstore/wstore.go index 8d92d3a4..40d98493 100644 --- a/pkg/wstore/wstore.go +++ b/pkg/wstore/wstore.go @@ -159,10 +159,12 @@ func (update WaveObjUpdate) MarshalJSON() ([]byte, error) { rtn["updatetype"] = update.UpdateType rtn["otype"] = update.OType rtn["oid"] = update.OID - var err error - rtn["obj"], err = waveobj.ToJsonMap(update.Obj) - if err != nil { - return nil, err + if update.Obj != nil { + var err error + rtn["obj"], err = waveobj.ToJsonMap(update.Obj) + if err != nil { + return nil, err + } } return json.Marshal(rtn) } @@ -308,9 +310,11 @@ func SetActiveTab(ctx context.Context, windowId string, tabId string) error { if window == nil { return fmt.Errorf("window not found: %q", windowId) } - tab, _ := DBGet[*Tab](tx.Context(), tabId) - if tab == nil { - return fmt.Errorf("tab not found: %q", tabId) + if tabId != "" { + tab, _ := DBGet[*Tab](tx.Context(), tabId) + if tab == nil { + return fmt.Errorf("tab not found: %q", tabId) + } } window.ActiveTabId = tabId DBUpdate(tx.Context(), window) @@ -340,6 +344,57 @@ func CreateBlock(ctx context.Context, tabId string, blockDef *BlockDef, rtOpts * }) } +func findStringInSlice(slice []string, val string) int { + for idx, v := range slice { + if v == val { + return idx + } + } + return -1 +} + +func DeleteBlock(ctx context.Context, tabId string, blockId string) error { + return WithTx(ctx, func(tx *TxWrap) error { + tab, _ := DBGet[*Tab](tx.Context(), tabId) + if tab == nil { + return fmt.Errorf("tab not found: %q", tabId) + } + blockIdx := findStringInSlice(tab.BlockIds, blockId) + if blockIdx == -1 { + return nil + } + tab.BlockIds = append(tab.BlockIds[:blockIdx], tab.BlockIds[blockIdx+1:]...) + DBUpdate(tx.Context(), tab) + DBDelete(tx.Context(), "block", blockId) + return nil + }) + +} + +func CloseTab(ctx context.Context, workspaceId string, tabId string) error { + return WithTx(ctx, func(tx *TxWrap) error { + ws, _ := DBGet[*Workspace](tx.Context(), workspaceId) + if ws == nil { + return fmt.Errorf("workspace not found: %q", workspaceId) + } + tab, _ := DBGet[*Tab](tx.Context(), tabId) + if tab == nil { + return fmt.Errorf("tab not found: %q", tabId) + } + tabIdx := findStringInSlice(ws.TabIds, tabId) + if tabIdx == -1 { + return nil + } + ws.TabIds = append(ws.TabIds[:tabIdx], ws.TabIds[tabIdx+1:]...) + DBUpdate(tx.Context(), ws) + DBDelete(tx.Context(), "tab", tabId) + for _, blockId := range tab.BlockIds { + DBDelete(tx.Context(), "block", blockId) + } + return nil + }) +} + func EnsureInitialData() error { // does not need to run in a transaction since it is called on startup ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second)