checkpoint on integratng wstore. moved to wails data structures, got immer working again, Window object, transitioned to generic DB ops, lots more

This commit is contained in:
sawka 2024-05-24 15:08:24 -06:00
parent 8173bc3c61
commit 134ba3c34c
19 changed files with 542 additions and 289 deletions

View File

@ -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

View File

@ -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 (
<div className="mainapp">
<div>invalid configuration, client or window was not loaded</div>
</div>
);
}
return (
<div className="mainapp">
<div className="titlebar"></div>

View File

@ -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);

View File

@ -3,6 +3,10 @@
import "./quickelems.less";
function CenteredLoadingDiv() {
return <CenteredDiv>loading...</CenteredDiv>;
}
function CenteredDiv({ children }: { children: React.ReactNode }) {
return (
<div className="centered-div">
@ -11,4 +15,4 @@ function CenteredDiv({ children }: { children: React.ReactNode }) {
);
}
export { CenteredDiv as CenteredDiv };
export { CenteredDiv, CenteredLoadingDiv };

View File

@ -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<string, jotai.Atom<wstore.Block>>();
const blockAtomCache = new Map<string, Map<string, jotai.Atom<any>>>();
const atoms = {
activeTabId: jotai.atom<string>(tabId1),
tabsAtom: jotai.atom<TabData[]>(tabArr),
tabsAtom: jotai.atom<wstore.Tab[]>(tabArr),
blockDataMap: blockDataMap,
clientAtom: jotai.atom(null) as jotai.PrimitiveAtom<wstore.Client>,
// initialized in wave.ts (will not be null inside of application)
windowId: jotai.atom<string>(null) as jotai.PrimitiveAtom<string>,
windowData: jotai.atom<wstore.Window>(null) as jotai.PrimitiveAtom<wstore.Window>,
};
type SubjectWithRef<T> = rxjs.Subject<T> & { 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);

View File

@ -16,7 +16,7 @@ const TabContent = ({ tabId }: { tabId: string }) => {
}
return (
<div className="tabcontent">
{tabData.blockIds.map((blockId: string) => {
{tabData.blockids.map((blockId: string) => {
return (
<div key={blockId} className="block-container">
<Block tabId={tabId} blockId={blockId} />

View File

@ -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<Promise<str
}
function PreviewView({ blockId }: { blockId: string }) {
const blockDataAtom: jotai.Atom<BlockData> = blockDataMap.get(blockId);
const blockDataAtom: jotai.Atom<wstore.Block> = blockDataMap.get(blockId);
const fileNameAtom = useBlockAtom(blockId, "preview:filename", () =>
jotai.atom<string>((get) => {
return get(blockDataAtom)?.meta?.file;

View File

@ -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 (
<div className={clsx("tab", { active: activeTab === tab.tabid })} onClick={() => 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 <CenteredLoadingDiv />;
}
if (wsLoadable.state === "hasError") {
return <CenteredDiv>Error: {wsLoadable.error?.toString()}</CenteredDiv>;
}
const ws: Workspace = wsLoadable.data;
return (
<div className="workspace">
<TabBar />
<div className="workspace-tabcontent">
<TabContent key={activeTabId} tabId={activeTabId} />
<TabContent key={workspaceId} tabId={activeTabId} />
<Widgets />
</div>
</div>
);
}
export { Workspace };
export { WorkspaceElem as Workspace };

View File

@ -1,38 +1,6 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
declare global {
type MetaDataType = Record<string, any>;
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 {};

View File

@ -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);

46
main.go
View File

@ -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()

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
})
}

View File

@ -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)
}