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 f3f95c4c4..d12626e2e 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 06c44d839..992eed3c7 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 6995a7027..22e10f59b 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 53ccded96..182e07ef8 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 c6e879742..a4937ab97 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 dfd3ef2e0..20e08fc54 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 000000000..e86c5fe47
--- /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 000000000..c27f5a329
--- /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 62265770d..9fb6cd1de 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 3840c8935..003e319a5 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 3e58417d4..40ae85300 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)
}